mirror of
https://github.com/mayanayza/netvisor.git
synced 2025-12-10 08:24:08 -06:00
feat: billing plan fixture generation
This commit is contained in:
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -83,6 +83,7 @@ jobs:
|
||||
mv backend/src/tests/daemon_config-next.json backend/src/tests/daemon_config.json
|
||||
mv ui/static/services-next.json ui/static/services.json
|
||||
mv docs/SERVICES-NEXT.md docs/SERVICES.md
|
||||
mv ui/static/billing-plans-next.json ui/static/billing-plans.json
|
||||
echo "✅ Updated test fixtures"
|
||||
|
||||
- name: Commit and push fixture updates
|
||||
@@ -94,7 +95,7 @@ jobs:
|
||||
git add backend/Cargo.toml backend/Cargo.lock ui/package.json ui/package-lock.json
|
||||
|
||||
# Stage fixture updates
|
||||
git add backend/src/tests/netvisor.sql backend/src/tests/daemon_config.json ui/static/services.json docs/SERVICES.md
|
||||
git add backend/src/tests/netvisor.sql backend/src/tests/daemon_config.json ui/static/services.json docs/SERVICES.md ui/static/billing-plans.json
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "No fixture changes to commit"
|
||||
|
||||
@@ -12,7 +12,7 @@ use netvisor::server::{
|
||||
auth::AuthenticatedEntity, logging::request_logging_middleware,
|
||||
rate_limit::rate_limit_middleware,
|
||||
},
|
||||
billing::types::base::{BillingPlan, BillingRate, PlanConfig},
|
||||
billing::plans::get_all_plans,
|
||||
config::{AppState, ServerCli, ServerConfig},
|
||||
organizations::r#impl::base::{Organization, OrganizationBase},
|
||||
shared::{
|
||||
@@ -207,46 +207,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let all_users = user_service.get_all(EntityFilter::unfiltered()).await?;
|
||||
|
||||
if let Some(billing_service) = billing_service {
|
||||
billing_service
|
||||
.initialize_products(vec![
|
||||
BillingPlan::Starter(PlanConfig {
|
||||
base_cents: 1499,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 0,
|
||||
seat_cents: None,
|
||||
network_cents: None,
|
||||
included_seats: Some(1),
|
||||
included_networks: Some(1),
|
||||
}),
|
||||
BillingPlan::Pro(PlanConfig {
|
||||
base_cents: 2499,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 7,
|
||||
seat_cents: None,
|
||||
network_cents: None,
|
||||
included_seats: Some(1),
|
||||
included_networks: Some(3),
|
||||
}),
|
||||
BillingPlan::Team(PlanConfig {
|
||||
base_cents: 7999,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 7,
|
||||
seat_cents: Some(1000),
|
||||
network_cents: Some(800),
|
||||
included_seats: Some(5),
|
||||
included_networks: Some(5),
|
||||
}),
|
||||
BillingPlan::Business(PlanConfig {
|
||||
base_cents: 14999,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 14,
|
||||
seat_cents: Some(800),
|
||||
network_cents: Some(500),
|
||||
included_seats: Some(10),
|
||||
included_networks: Some(25),
|
||||
}),
|
||||
])
|
||||
.await?;
|
||||
billing_service.initialize_products(get_all_plans()).await?;
|
||||
}
|
||||
|
||||
// First load - populate user and org
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod handlers;
|
||||
pub mod plans;
|
||||
pub mod service;
|
||||
pub mod subscriber;
|
||||
pub mod types;
|
||||
|
||||
71
backend/src/server/billing/plans.rs
Normal file
71
backend/src/server/billing/plans.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use super::types::base::{BillingPlan, BillingRate, PlanConfig};
|
||||
|
||||
/// Returns the canonical list of billing plans for NetVisor.
|
||||
/// This is the single source of truth for plan definitions.
|
||||
fn get_default_plans() -> Vec<BillingPlan> {
|
||||
vec![
|
||||
BillingPlan::Starter(PlanConfig {
|
||||
base_cents: 1499,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 0,
|
||||
seat_cents: None,
|
||||
network_cents: None,
|
||||
included_seats: Some(1),
|
||||
included_networks: Some(1),
|
||||
}),
|
||||
BillingPlan::Pro(PlanConfig {
|
||||
base_cents: 2499,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 7,
|
||||
seat_cents: None,
|
||||
network_cents: None,
|
||||
included_seats: Some(1),
|
||||
included_networks: Some(3),
|
||||
}),
|
||||
BillingPlan::Team(PlanConfig {
|
||||
base_cents: 7999,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 7,
|
||||
seat_cents: Some(1000),
|
||||
network_cents: Some(800),
|
||||
included_seats: Some(5),
|
||||
included_networks: Some(5),
|
||||
}),
|
||||
BillingPlan::Business(PlanConfig {
|
||||
base_cents: 14999,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 14,
|
||||
seat_cents: Some(800),
|
||||
network_cents: Some(500),
|
||||
included_seats: Some(10),
|
||||
included_networks: Some(25),
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
fn get_enterprise_plan() -> BillingPlan {
|
||||
BillingPlan::Enterprise(PlanConfig {
|
||||
base_cents: 0,
|
||||
rate: BillingRate::Month,
|
||||
trial_days: 14,
|
||||
seat_cents: None,
|
||||
network_cents: None,
|
||||
included_seats: None,
|
||||
included_networks: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns both monthly and yearly versions of all plans.
|
||||
/// Yearly plans get a 20% discount.
|
||||
pub fn get_all_plans() -> Vec<BillingPlan> {
|
||||
let monthly_plans = get_default_plans();
|
||||
let mut all_plans = monthly_plans.clone();
|
||||
all_plans.push(get_enterprise_plan());
|
||||
|
||||
// Add yearly versions with 20% discount
|
||||
for plan in monthly_plans {
|
||||
all_plans.push(plan.to_yearly(0.20));
|
||||
}
|
||||
|
||||
all_plans
|
||||
}
|
||||
@@ -102,15 +102,8 @@ impl BillingService {
|
||||
pub async fn initialize_products(&self, plans: Vec<BillingPlan>) -> Result<(), Error> {
|
||||
let mut created_plans = Vec::new();
|
||||
|
||||
let all_plans: Vec<BillingPlan> = plans
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|p| p.to_yearly(0.20))
|
||||
.chain(plans)
|
||||
.collect();
|
||||
|
||||
tracing::info!(
|
||||
plan_count = all_plans.len(),
|
||||
plan_count = plans.len(),
|
||||
"Initializing Stripe products and prices"
|
||||
);
|
||||
|
||||
@@ -157,7 +150,7 @@ impl BillingService {
|
||||
}
|
||||
};
|
||||
|
||||
for plan in all_plans {
|
||||
for plan in plans {
|
||||
// Check if product exists, create if not
|
||||
let product_id = plan.stripe_product_id();
|
||||
let product = match RetrieveProduct::new(product_id.clone())
|
||||
@@ -731,6 +724,7 @@ impl BillingService {
|
||||
}
|
||||
BillingPlan::Team { .. } => {}
|
||||
BillingPlan::Business { .. } => {}
|
||||
BillingPlan::Enterprise { .. } => {}
|
||||
}
|
||||
|
||||
organization.base.plan_status = Some(sub.status.to_string());
|
||||
|
||||
@@ -27,6 +27,7 @@ pub enum BillingPlan {
|
||||
Pro(PlanConfig),
|
||||
Team(PlanConfig),
|
||||
Business(PlanConfig),
|
||||
Enterprise(PlanConfig),
|
||||
}
|
||||
|
||||
impl PartialEq for BillingPlan {
|
||||
@@ -128,6 +129,7 @@ impl BillingPlan {
|
||||
"pro" => Some(Self::Pro(plan_config)),
|
||||
"team" => Some(Self::Team(plan_config)),
|
||||
"business" => Some(Self::Business(plan_config)),
|
||||
"enterprise" => Some(Self::Enterprise(plan_config)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -139,6 +141,7 @@ impl BillingPlan {
|
||||
BillingPlan::Pro(plan_config) => *plan_config,
|
||||
BillingPlan::Team(plan_config) => *plan_config,
|
||||
BillingPlan::Business(plan_config) => *plan_config,
|
||||
BillingPlan::Enterprise(plan_config) => *plan_config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,11 +152,12 @@ impl BillingPlan {
|
||||
BillingPlan::Pro(plan_config) => *plan_config = config,
|
||||
BillingPlan::Team(plan_config) => *plan_config = config,
|
||||
BillingPlan::Business(plan_config) => *plan_config = config,
|
||||
BillingPlan::Enterprise(plan_config) => *plan_config = config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_commercial(&self) -> bool {
|
||||
matches!(self, BillingPlan::Team(_) | BillingPlan::Business(_))
|
||||
matches!(self, BillingPlan::Team(_) | BillingPlan::Business(_) | BillingPlan::Enterprise(_))
|
||||
}
|
||||
|
||||
pub fn stripe_product_id(&self) -> String {
|
||||
@@ -238,6 +242,15 @@ impl BillingPlan {
|
||||
audit_logs: true,
|
||||
remove_powered_by: true,
|
||||
},
|
||||
BillingPlan::Enterprise { .. } => BillingPlanFeatures {
|
||||
share_views: true,
|
||||
onboarding_call: true,
|
||||
dedicated_support_channel: true,
|
||||
commercial_license: true,
|
||||
api_access: true,
|
||||
audit_logs: true,
|
||||
remove_powered_by: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,7 +315,8 @@ impl EntityMetadataProvider for BillingPlan {
|
||||
BillingPlan::Starter { .. } => "ThumbsUp",
|
||||
BillingPlan::Pro { .. } => "Zap",
|
||||
BillingPlan::Team { .. } => "Users",
|
||||
BillingPlan::Business { .. } => "Building",
|
||||
BillingPlan::Business { .. } => "Briefcase",
|
||||
BillingPlan::Enterprise { .. } => "Building",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +326,8 @@ impl EntityMetadataProvider for BillingPlan {
|
||||
BillingPlan::Starter { .. } => "blue",
|
||||
BillingPlan::Pro { .. } => "yellow",
|
||||
BillingPlan::Team { .. } => "orange",
|
||||
BillingPlan::Business { .. } => "gray",
|
||||
BillingPlan::Business { .. } => "brown",
|
||||
BillingPlan::Enterprise { .. } => "gray",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,6 +340,7 @@ impl TypeMetadataProvider for BillingPlan {
|
||||
BillingPlan::Pro { .. } => "Pro",
|
||||
BillingPlan::Team { .. } => "Team",
|
||||
BillingPlan::Business { .. } => "Business",
|
||||
BillingPlan::Enterprise { .. } => "Enterprise",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,11 +357,25 @@ impl TypeMetadataProvider for BillingPlan {
|
||||
BillingPlan::Business { .. } => {
|
||||
"Manage multi-site and multi-customer documentation with advanced features"
|
||||
}
|
||||
BillingPlan::Enterprise { .. } => {
|
||||
"Deploy NetVisor with enterprise-grade features and functionality"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn metadata(&self) -> serde_json::Value {
|
||||
let config = self.config();
|
||||
|
||||
serde_json::json!({
|
||||
// Pricing information
|
||||
"base_cents": config.base_cents,
|
||||
"rate": config.rate,
|
||||
"trial_days": config.trial_days,
|
||||
"seat_cents": config.seat_cents,
|
||||
"network_cents": config.network_cents,
|
||||
"included_seats": config.included_seats,
|
||||
"included_networks": config.included_networks,
|
||||
// Feature flags and metadata
|
||||
"features": self.features(),
|
||||
"is_commercial": self.is_commercial()
|
||||
})
|
||||
|
||||
250
backend/tests/fixtures.rs
Normal file
250
backend/tests/fixtures.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use netvisor::server::{
|
||||
services::{definitions::ServiceDefinitionRegistry, r#impl::definitions::ServiceDefinition},
|
||||
shared::types::metadata::TypeMetadata,
|
||||
};
|
||||
|
||||
pub async fn generate_fixtures() {
|
||||
generate_db_fixture()
|
||||
.await
|
||||
.expect("Failed to generate db fixture");
|
||||
|
||||
generate_daemon_config_fixture()
|
||||
.await
|
||||
.expect("Failed to generate daemon config fixture");
|
||||
|
||||
generate_services_json()
|
||||
.await
|
||||
.expect("Failed to generate services json");
|
||||
|
||||
generate_services_markdown()
|
||||
.await
|
||||
.expect("Failed to generate services markdown");
|
||||
|
||||
generate_billing_plans_json()
|
||||
.await
|
||||
.expect("Failed to generate billing and features json");
|
||||
|
||||
println!("✅ Generated test fixtures");
|
||||
}
|
||||
|
||||
async fn generate_db_fixture() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let output = std::process::Command::new("docker")
|
||||
.args([
|
||||
"exec",
|
||||
"netvisor-postgres-dev-1",
|
||||
"pg_dump",
|
||||
"-U",
|
||||
"postgres",
|
||||
"-d",
|
||||
"netvisor",
|
||||
"--clean",
|
||||
"--if-exists",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"pg_dump failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let fixture_path =
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/netvisor-next.sql");
|
||||
std::fs::write(&fixture_path, output.stdout)?;
|
||||
|
||||
println!("✅ Generated netvisor-next.sql from test data");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_daemon_config_fixture() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// First, find the config file location in the container
|
||||
let find_output = std::process::Command::new("docker")
|
||||
.args([
|
||||
"exec",
|
||||
"netvisor-daemon-1",
|
||||
"find",
|
||||
"/root/.config",
|
||||
"-name",
|
||||
"config.json",
|
||||
"-type",
|
||||
"f",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !find_output.status.success() {
|
||||
return Err(format!(
|
||||
"Failed to find daemon config: {}",
|
||||
String::from_utf8_lossy(&find_output.stderr)
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let config_path = String::from_utf8_lossy(&find_output.stdout)
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if config_path.is_empty() {
|
||||
return Err("No config.json found in container".into());
|
||||
}
|
||||
|
||||
println!("Found daemon config at: {}", config_path);
|
||||
|
||||
// Now read the config file
|
||||
let output = std::process::Command::new("docker")
|
||||
.args(["exec", "netvisor-daemon-1", "cat", &config_path])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"Failed to read daemon config: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let fixture_path =
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/tests/daemon_config-next.json");
|
||||
std::fs::write(&fixture_path, output.stdout)?;
|
||||
|
||||
println!("✅ Generated daemon_config-next.json from test daemon");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_services_json() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let services: Vec<serde_json::Value> = ServiceDefinitionRegistry::all_service_definitions()
|
||||
.iter()
|
||||
.map(|s| {
|
||||
serde_json::json!({
|
||||
"logo_url": s.logo_url(),
|
||||
"name": s.name(),
|
||||
"description": s.description(),
|
||||
"discovery_pattern": s.discovery_pattern().to_string(),
|
||||
"category": s.category()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Write JSON file
|
||||
let json_string = serde_json::to_string_pretty(&services)?;
|
||||
let json_path = std::path::Path::new("../ui/static/services-next.json");
|
||||
tokio::fs::write(json_path, json_string).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_services_markdown() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let services = ServiceDefinitionRegistry::all_service_definitions();
|
||||
|
||||
// Group services by category
|
||||
let mut by_category: HashMap<String, Vec<&Box<dyn ServiceDefinition>>> = HashMap::new();
|
||||
for service in &services {
|
||||
let category = service.category().to_string();
|
||||
by_category.entry(category).or_default().push(service);
|
||||
}
|
||||
|
||||
// Sort categories for consistent output
|
||||
let mut categories: Vec<String> = by_category.keys().cloned().collect();
|
||||
categories.sort();
|
||||
|
||||
let mut markdown = String::from("# NetVisor Service Definitions\n\n");
|
||||
markdown.push_str("This document lists all services that NetVisor can automatically discover and identify.\n\n");
|
||||
|
||||
for category in categories {
|
||||
let services = by_category.get(&category).unwrap();
|
||||
|
||||
// Add category header
|
||||
markdown.push_str(&format!("## {}\n\n", category));
|
||||
|
||||
// Use HTML table with dark theme styling
|
||||
markdown.push_str("<table style=\"background-color: #1a1d29; border-collapse: collapse; width: 100%;\">\n");
|
||||
markdown.push_str("<thead>\n");
|
||||
markdown.push_str(
|
||||
"<tr style=\"background-color: #1f2937; border-bottom: 2px solid #374151;\">\n",
|
||||
);
|
||||
markdown.push_str("<th width=\"60\" style=\"padding: 12px; text-align: center; color: #e5e7eb; font-weight: 600;\">Logo</th>\n");
|
||||
markdown.push_str("<th width=\"200\" style=\"padding: 12px; text-align: left; color: #e5e7eb; font-weight: 600;\">Name</th>\n");
|
||||
markdown.push_str("<th width=\"300\" style=\"padding: 12px; text-align: left; color: #e5e7eb; font-weight: 600;\">Description</th>\n");
|
||||
markdown.push_str("<th style=\"padding: 12px; text-align: left; color: #e5e7eb; font-weight: 600;\">Discovery Pattern</th>\n");
|
||||
markdown.push_str("</tr>\n");
|
||||
markdown.push_str("</thead>\n");
|
||||
markdown.push_str("<tbody>\n");
|
||||
|
||||
// Sort services by name within category
|
||||
let mut sorted_services = services.clone();
|
||||
sorted_services.sort_by_key(|s| s.name());
|
||||
|
||||
for service in sorted_services {
|
||||
let logo_url = service.logo_url();
|
||||
let name = service.name();
|
||||
let description = service.description();
|
||||
let pattern = service.discovery_pattern().to_string();
|
||||
|
||||
// Format logo
|
||||
let logo = if !logo_url.is_empty() {
|
||||
format!(
|
||||
"<img src=\"{}\" alt=\"{}\" width=\"32\" height=\"32\" />",
|
||||
logo_url, name
|
||||
)
|
||||
} else {
|
||||
"—".to_string()
|
||||
};
|
||||
|
||||
markdown.push_str("<tr style=\"border-bottom: 1px solid #374151;\">\n");
|
||||
markdown.push_str(&format!(
|
||||
"<td align=\"center\" style=\"padding: 12px; color: #d1d5db;\">{}</td>\n",
|
||||
logo
|
||||
));
|
||||
markdown.push_str(&format!(
|
||||
"<td style=\"padding: 12px; color: #f3f4f6; font-weight: 500;\">{}</td>\n",
|
||||
name
|
||||
));
|
||||
markdown.push_str(&format!(
|
||||
"<td style=\"padding: 12px; color: #d1d5db;\">{}</td>\n",
|
||||
description
|
||||
));
|
||||
markdown.push_str(&format!("<td style=\"padding: 12px;\"><code style=\"background-color: #374151; color: #e5e7eb; padding: 2px 6px; border-radius: 3px; font-size: 0.875em;\">{}</code></td>\n", pattern));
|
||||
markdown.push_str("</tr>\n");
|
||||
}
|
||||
|
||||
markdown.push_str("</tbody>\n");
|
||||
markdown.push_str("</table>\n\n");
|
||||
}
|
||||
|
||||
let md_path = std::path::Path::new("../docs/SERVICES-NEXT.md");
|
||||
tokio::fs::write(md_path, markdown).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn generate_billing_plans_json() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use netvisor::server::billing::plans::get_all_plans;
|
||||
use netvisor::server::billing::types::features::Feature;
|
||||
use netvisor::server::shared::types::metadata::MetadataProvider;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
// Get all plans (monthly + yearly)
|
||||
let plans = get_all_plans();
|
||||
|
||||
// Convert to metadata format (same as API returns)
|
||||
let plan_metadata: Vec<TypeMetadata> = plans.iter().map(|p| p.to_metadata()).collect();
|
||||
|
||||
// Get all features metadata
|
||||
let feature_metadata: Vec<TypeMetadata> = Feature::iter().map(|f| f.to_metadata()).collect();
|
||||
|
||||
// Combine into a single structure
|
||||
let fixture = serde_json::json!({
|
||||
"billing_plans": plan_metadata,
|
||||
"features": feature_metadata,
|
||||
});
|
||||
|
||||
let json_string = serde_json::to_string_pretty(&fixture)?;
|
||||
let path = std::path::Path::new("../ui/static/billing-plans-next.json");
|
||||
tokio::fs::write(path, json_string).await?;
|
||||
|
||||
println!("✅ Generated billing-plans-next.json");
|
||||
Ok(())
|
||||
}
|
||||
@@ -5,14 +5,9 @@ use netvisor::server::daemons::r#impl::base::Daemon;
|
||||
use netvisor::server::discovery::r#impl::types::DiscoveryType;
|
||||
use netvisor::server::networks::r#impl::Network;
|
||||
use netvisor::server::organizations::r#impl::base::Organization;
|
||||
use netvisor::server::services::definitions::ServiceDefinitionRegistry;
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
use netvisor::server::services::definitions::ServiceDefinitionRegistry;
|
||||
|
||||
use netvisor::server::services::definitions::home_assistant::HomeAssistant;
|
||||
use netvisor::server::services::r#impl::base::Service;
|
||||
use netvisor::server::services::r#impl::definitions::ServiceDefinition;
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
use netvisor::server::services::r#impl::definitions::ServiceDefinition;
|
||||
use netvisor::server::shared::handlers::factory::OnboardingRequest;
|
||||
use netvisor::server::shared::types::api::ApiResponse;
|
||||
use netvisor::server::shared::types::metadata::HasId;
|
||||
@@ -409,6 +404,100 @@ async fn verify_home_assistant_discovered(client: &TestClient) -> Result<Service
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_full_integration() {
|
||||
// Start containers
|
||||
let mut container_manager = ContainerManager::new();
|
||||
container_manager
|
||||
.start()
|
||||
.expect("Failed to start containers");
|
||||
|
||||
let client = TestClient::new();
|
||||
|
||||
// Authenticate
|
||||
let user = setup_authenticated_user(&client)
|
||||
.await
|
||||
.expect("Failed to authenticate user");
|
||||
println!("✅ Authenticated as: {}", user.base.email);
|
||||
|
||||
// Wait for organization
|
||||
println!("\n=== Waiting for Organization ===");
|
||||
let organization = wait_for_organization(&client)
|
||||
.await
|
||||
.expect("Failed to find organization");
|
||||
println!("✅ Organization: {}", organization.base.name);
|
||||
|
||||
// Onboard
|
||||
println!("\n=== Onboarding ===");
|
||||
onboard(&client).await.expect("Failed to onboard");
|
||||
println!("✅ Onboarded");
|
||||
|
||||
// Wait for network
|
||||
println!("\n=== Waiting for Network ===");
|
||||
let network = wait_for_network(&client)
|
||||
.await
|
||||
.expect("Failed to find network");
|
||||
println!("✅ Network: {}", network.base.name);
|
||||
|
||||
// Wait for daemon
|
||||
println!("\n=== Waiting for Daemon ===");
|
||||
let daemon = wait_for_daemon(&client)
|
||||
.await
|
||||
.expect("Failed to find daemon");
|
||||
println!("✅ Daemon registered: {}", daemon.id);
|
||||
|
||||
// Run discovery
|
||||
run_discovery(&client).await.expect("Discovery failed");
|
||||
|
||||
// Verify service discovered
|
||||
let _service = verify_home_assistant_discovered(&client)
|
||||
.await
|
||||
.expect("Failed to find Home Assistant");
|
||||
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
{
|
||||
generate_fixtures().await;
|
||||
}
|
||||
|
||||
println!("\n✅ All integration tests passed!");
|
||||
println!(" ✓ User authenticated");
|
||||
println!(" ✓ Network created");
|
||||
println!(" ✓ Daemon registered");
|
||||
println!(" ✓ Discovery completed");
|
||||
println!(" ✓ Home Assistant discovered");
|
||||
}
|
||||
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
use netvisor::server::{
|
||||
services::{definitions::ServiceDefinitionRegistry, r#impl::definitions::ServiceDefinition},
|
||||
shared::types::metadata::TypeMetadata,
|
||||
};
|
||||
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
pub async fn generate_fixtures() {
|
||||
generate_db_fixture()
|
||||
.await
|
||||
.expect("Failed to generate db fixture");
|
||||
|
||||
generate_daemon_config_fixture()
|
||||
.await
|
||||
.expect("Failed to generate daemon config fixture");
|
||||
|
||||
generate_services_json()
|
||||
.await
|
||||
.expect("Failed to generate services json");
|
||||
|
||||
generate_services_markdown()
|
||||
.await
|
||||
.expect("Failed to generate services markdown");
|
||||
|
||||
generate_billing_plans_json()
|
||||
.await
|
||||
.expect("Failed to generate billing and features json");
|
||||
|
||||
println!("✅ Generated test fixtures");
|
||||
}
|
||||
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
async fn generate_db_fixture() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let output = std::process::Command::new("docker")
|
||||
@@ -519,8 +608,7 @@ async fn generate_services_json() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// #[cfg(feature = "generate-fixtures")]
|
||||
#[tokio::test]
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
async fn generate_services_markdown() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -607,81 +695,32 @@ async fn generate_services_markdown() -> Result<(), Box<dyn std::error::Error>>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_full_integration() {
|
||||
// Start containers
|
||||
let mut container_manager = ContainerManager::new();
|
||||
container_manager
|
||||
.start()
|
||||
.expect("Failed to start containers");
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
async fn generate_billing_plans_json() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use netvisor::server::billing::plans::get_all_plans;
|
||||
use netvisor::server::billing::types::features::Feature;
|
||||
use netvisor::server::shared::types::metadata::MetadataProvider;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
let client = TestClient::new();
|
||||
// Get all plans (monthly + yearly)
|
||||
let plans = get_all_plans();
|
||||
|
||||
// Authenticate
|
||||
let user = setup_authenticated_user(&client)
|
||||
.await
|
||||
.expect("Failed to authenticate user");
|
||||
println!("✅ Authenticated as: {}", user.base.email);
|
||||
// Convert to metadata format (same as API returns)
|
||||
let plan_metadata: Vec<TypeMetadata> = plans.iter().map(|p| p.to_metadata()).collect();
|
||||
|
||||
// Wait for organization
|
||||
println!("\n=== Waiting for Organization ===");
|
||||
let organization = wait_for_organization(&client)
|
||||
.await
|
||||
.expect("Failed to find organization");
|
||||
println!("✅ Organization: {}", organization.base.name);
|
||||
// Get all features metadata
|
||||
let feature_metadata: Vec<TypeMetadata> = Feature::iter().map(|f| f.to_metadata()).collect();
|
||||
|
||||
// Onboard
|
||||
println!("\n=== Onboarding ===");
|
||||
onboard(&client).await.expect("Failed to onboard");
|
||||
println!("✅ Onboarded");
|
||||
// Combine into a single structure
|
||||
let fixture = serde_json::json!({
|
||||
"billing_plans": plan_metadata,
|
||||
"features": feature_metadata,
|
||||
});
|
||||
|
||||
// Wait for network
|
||||
println!("\n=== Waiting for Network ===");
|
||||
let network = wait_for_network(&client)
|
||||
.await
|
||||
.expect("Failed to find network");
|
||||
println!("✅ Network: {}", network.base.name);
|
||||
let json_string = serde_json::to_string_pretty(&fixture)?;
|
||||
let path = std::path::Path::new("../ui/static/billing-plans-next.json");
|
||||
tokio::fs::write(path, json_string).await?;
|
||||
|
||||
// Wait for daemon
|
||||
println!("\n=== Waiting for Daemon ===");
|
||||
let daemon = wait_for_daemon(&client)
|
||||
.await
|
||||
.expect("Failed to find daemon");
|
||||
println!("✅ Daemon registered: {}", daemon.id);
|
||||
|
||||
// Run discovery
|
||||
run_discovery(&client).await.expect("Discovery failed");
|
||||
|
||||
// Verify service discovered
|
||||
let _service = verify_home_assistant_discovered(&client)
|
||||
.await
|
||||
.expect("Failed to find Home Assistant");
|
||||
|
||||
#[cfg(feature = "generate-fixtures")]
|
||||
{
|
||||
generate_db_fixture()
|
||||
.await
|
||||
.expect("Failed to generate db fixture");
|
||||
|
||||
generate_daemon_config_fixture()
|
||||
.await
|
||||
.expect("Failed to generate daemon config fixture");
|
||||
|
||||
generate_services_json()
|
||||
.await
|
||||
.expect("Failed to generate services json");
|
||||
|
||||
generate_services_markdown()
|
||||
.await
|
||||
.expect("Failed to generate services markdown");
|
||||
|
||||
println!("✅ Generated test fixtures");
|
||||
}
|
||||
|
||||
println!("\n✅ All integration tests passed!");
|
||||
println!(" ✓ User authenticated");
|
||||
println!(" ✓ Network created");
|
||||
println!(" ✓ Daemon registered");
|
||||
println!(" ✓ Discovery completed");
|
||||
println!(" ✓ Home Assistant discovered");
|
||||
println!("✅ Generated billing-plans-next.json");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
1
backend/tests/mod.rs
Normal file
1
backend/tests/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod integration;
|
||||
Reference in New Issue
Block a user