"""Tests for chain.yaml config parsing and CRD generation.""" import pytest from bootnode.core.chains.chain_config import ChainConfig, load_chain_config from bootnode.api.launch import generate_manifests, register_chain_networks from bootnode.core.chains.fleet_models import CHAIN_NETWORKS VALID_CHAIN_YAML = """ version: "1" chain: name: "Test Chain" slug: test type: l1 vm: evm networks: devnet: networkId: 3 chainId: 99999 validators: 3 token: name: "Test Token" symbol: "TST" decimals: 18 brand: displayName: "Test Chain" domains: explorer: "explorer.test.network" rpc: "api.test.network" services: node: enabled: true indexer: enabled: true explorer: enabled: true gateway: enabled: true exchange: enabled: false wallet: enabled: false faucet: enabled: true dripAmount: "1000000000000000000" rateLimit: "1/hour" deploy: platform: hanzo namespace: "test-{network}" ingressClass: hanzo """ PARS_CHAIN_YAML = """ version: "1" chain: name: "Pars Network" slug: pars type: l1 vm: pars vmPlugins: - name: sessionvm vmId: "speKUgLBX6WRD5cfGeEfLa43LxTXUBckvtv4td6F3eTXvRP48" dbType: zapdb compression: zstd networks: mainnet: networkId: 494949 chainId: 494949 validators: 5 rpcUrl: "https://api.pars.network/v1/bc/C/rpc" snapshotInterval: 3600 testnet: networkId: 494950 chainId: 494950 validators: 3 sybilProtection: false snapshotInterval: 1800 devnet: networkId: 494951 chainId: 494951 validators: 3 sybilProtection: false token: name: "Pars" symbol: "PARS" decimals: 18 brand: displayName: "Pars Network" primaryColor: "#F59E0B" domains: explorer: "explorer.pars.network" rpc: "api.pars.network" services: node: enabled: true image: "ghcr.io/hanzoai/parsd" storageSize: "100Gi" storageClass: do-block-storage stakingKms: hostApi: "http://kms.lux-system.svc.cluster.local/api" projectSlug: pars-infra envSlug: mainnet secretsPath: /staking indexer: enabled: true traceEnabled: true contractVerification: true pollInterval: 2 explorer: enabled: true gateway: enabled: true rateLimitRps: 100 rateLimitBurst: 200 exchange: enabled: true image: "ghcr.io/luxfi/exchange:main" brandPackage: "@parsdao/brand" faucet: enabled: true dripAmount: "1000000000000000000" rateLimit: "1/hour" deploy: platform: hanzo namespace: "pars-{network}" registry: "ghcr.io/hanzoai" ingressClass: hanzo precompiles: - name: mldsaVerify blockTimestamp: 0 - name: mlkemConfig blockTimestamp: 0 - name: blake3Config blockTimestamp: 0 """ class TestLoadChainConfig: def test_load_valid(self): cfg = load_chain_config(VALID_CHAIN_YAML) assert cfg.chain.name == "Test Chain" assert cfg.chain.slug == "test" assert cfg.chain.type == "l1" assert cfg.token.symbol == "TST" def test_load_defaults(self): cfg = load_chain_config(VALID_CHAIN_YAML) assert cfg.chain.sequencer == "lux" assert cfg.chain.db_type == "zapdb" assert cfg.chain.compression == "zstd" assert cfg.deploy.ingress_class == "hanzo" def test_load_pars(self): cfg = load_chain_config(PARS_CHAIN_YAML) assert cfg.chain.slug == "pars" assert cfg.chain.vm == "pars" assert len(cfg.networks) == 3 assert cfg.networks["mainnet"].chain_id == 494949 assert cfg.networks["testnet"].chain_id == 494950 assert cfg.networks["devnet"].chain_id == 494951 assert len(cfg.precompiles) == 3 assert cfg.services.node.staking_kms is not None assert cfg.services.node.staking_kms.project_slug == "pars-infra" def test_load_invalid_yaml(self): with pytest.raises(ValueError, match="Invalid YAML"): load_chain_config("not: [valid: yaml: {{") def test_load_non_mapping(self): with pytest.raises(ValueError, match="must be a YAML mapping"): load_chain_config("- just\n- a\n- list\n") def test_namespace_for(self): cfg = load_chain_config(VALID_CHAIN_YAML) assert cfg.namespace_for("devnet") == "test-devnet" assert cfg.namespace_for("mainnet") == "test-mainnet" def test_namespace_for_pars(self): cfg = load_chain_config(PARS_CHAIN_YAML) assert cfg.namespace_for("mainnet") == "pars-mainnet" assert cfg.namespace_for("testnet") == "pars-testnet" assert cfg.namespace_for("devnet") == "pars-devnet" class TestValidate: def test_valid_config(self): cfg = load_chain_config(VALID_CHAIN_YAML) errors = cfg.validate_config() assert errors == [] def test_nginx_rejected(self): yaml_str = VALID_CHAIN_YAML.replace("ingressClass: hanzo", "ingressClass: nginx") cfg = load_chain_config(yaml_str) errors = cfg.validate_config() assert any("nginx" in e for e in errors) def test_caddy_rejected(self): yaml_str = VALID_CHAIN_YAML.replace("ingressClass: hanzo", "ingressClass: caddy") cfg = load_chain_config(yaml_str) errors = cfg.validate_config() assert any("caddy" in e for e in errors) def test_bad_chain_type(self): yaml_str = VALID_CHAIN_YAML.replace("type: l1", "type: l4") with pytest.raises(Exception): load_chain_config(yaml_str) class TestGenerateManifests: def test_generate_devnet(self): cfg = load_chain_config(VALID_CHAIN_YAML) result = generate_manifests(cfg, "devnet") assert result.network == "devnet" assert result.namespace == "test-devnet" # namespace + node + indexer + explorer + gateway + faucet = 6 assert len(result.manifests) == 6 def test_generate_kinds(self): cfg = load_chain_config(VALID_CHAIN_YAML) result = generate_manifests(cfg, "devnet") kinds = {m.kind for m in result.manifests} assert "Namespace" in kinds assert "LuxNetwork" in kinds assert "LuxIndexer" in kinds assert "Deployment" in kinds # explorer is now a Deployment, not a CRD assert "LuxGateway" in kinds def test_luxnetwork_content(self): cfg = load_chain_config(VALID_CHAIN_YAML) result = generate_manifests(cfg, "devnet") net_manifest = next(m for m in result.manifests if m.kind == "LuxNetwork") assert "kind: LuxNetwork" in net_manifest.yaml_content assert "networkId: 3" in net_manifest.yaml_content assert "validators: 3" in net_manifest.yaml_content def test_luxindexer_content(self): cfg = load_chain_config(VALID_CHAIN_YAML) result = generate_manifests(cfg, "devnet") idx_manifest = next(m for m in result.manifests if m.kind == "LuxIndexer") assert "kind: LuxIndexer" in idx_manifest.yaml_content assert "chainId: 99999" in idx_manifest.yaml_content def test_explorer_content(self): cfg = load_chain_config(VALID_CHAIN_YAML) result = generate_manifests(cfg, "devnet") exp_manifest = next(m for m in result.manifests if m.name == "explorer.yaml") assert "ghcr.io/luxfi/explorer:latest" in exp_manifest.yaml_content assert "explorer.test.network" in exp_manifest.yaml_content assert "ingressClassName: hanzo" in exp_manifest.yaml_content assert "containerPort: 8090" in exp_manifest.yaml_content assert 'COIN_SYMBOL' in exp_manifest.yaml_content assert 'CHAIN_NAME' in exp_manifest.yaml_content assert 'RPC_ENDPOINT' in exp_manifest.yaml_content def test_faucet_on_devnet(self): cfg = load_chain_config(VALID_CHAIN_YAML) result = generate_manifests(cfg, "devnet") faucet = [m for m in result.manifests if m.name == "faucet.yaml"] assert len(faucet) == 1 assert "DRIP_AMOUNT" in faucet[0].yaml_content def test_no_faucet_on_mainnet(self): cfg = load_chain_config(PARS_CHAIN_YAML) result = generate_manifests(cfg, "mainnet") faucet = [m for m in result.manifests if m.name == "faucet.yaml"] assert len(faucet) == 0 def test_exchange_disabled(self): cfg = load_chain_config(VALID_CHAIN_YAML) result = generate_manifests(cfg, "devnet") exchange = [m for m in result.manifests if m.name == "exchange.yaml"] assert len(exchange) == 0 def test_exchange_enabled(self): cfg = load_chain_config(PARS_CHAIN_YAML) result = generate_manifests(cfg, "devnet") exchange = [m for m in result.manifests if m.name == "exchange.yaml"] assert len(exchange) == 1 assert "@parsdao/brand" in exchange[0].yaml_content def test_precompiles(self): cfg = load_chain_config(PARS_CHAIN_YAML) result = generate_manifests(cfg, "devnet") net_manifest = next(m for m in result.manifests if m.kind == "LuxNetwork") assert "mldsaVerify" in net_manifest.yaml_content assert "mlkemConfig" in net_manifest.yaml_content assert "blake3Config" in net_manifest.yaml_content def test_kms_staking(self): cfg = load_chain_config(PARS_CHAIN_YAML) result = generate_manifests(cfg, "mainnet") net_manifest = next(m for m in result.manifests if m.kind == "LuxNetwork") assert "pars-infra" in net_manifest.yaml_content assert "kms:" in net_manifest.yaml_content def test_bad_network(self): cfg = load_chain_config(VALID_CHAIN_YAML) with pytest.raises(ValueError, match="not defined"): generate_manifests(cfg, "nonexistent") def test_snapshot_schedule(self): cfg = load_chain_config(PARS_CHAIN_YAML) result = generate_manifests(cfg, "mainnet") net_manifest = next(m for m in result.manifests if m.kind == "LuxNetwork") assert "snapshotSchedule:" in net_manifest.yaml_content assert "intervalSeconds: 3600" in net_manifest.yaml_content def test_sybil_protection_disabled(self): cfg = load_chain_config(PARS_CHAIN_YAML) result = generate_manifests(cfg, "devnet") net_manifest = next(m for m in result.manifests if m.kind == "LuxNetwork") assert "sybilProtectionEnabled: false" in net_manifest.yaml_content class TestRegisterChainNetworks: def test_register(self): cfg = load_chain_config(VALID_CHAIN_YAML) register_chain_networks(cfg) assert "test" in CHAIN_NETWORKS assert "devnet" in CHAIN_NETWORKS["test"] assert CHAIN_NETWORKS["test"]["devnet"].chain_id == 99999 # Cleanup del CHAIN_NETWORKS["test"] def test_register_pars(self): cfg = load_chain_config(PARS_CHAIN_YAML) # Save original pars config original = CHAIN_NETWORKS.get("pars", {}).copy() register_chain_networks(cfg) assert CHAIN_NETWORKS["pars"]["mainnet"].chain_id == 494949 assert CHAIN_NETWORKS["pars"]["testnet"].chain_id == 494950 assert CHAIN_NETWORKS["pars"]["devnet"].chain_id == 494951 # Restore CHAIN_NETWORKS["pars"] = original class TestListSettingsFromEnv: """ALLOWED_ORIGINS is written comma-separated everywhere (Deployment env, .env, compose). pydantic-settings decodes list[str] from env as JSON, so that value made Settings() raise SettingsError — which surfaced as HTTP 500 on every admin-gated route, including the lux.cloud fleet overview.""" def test_comma_separated_origins_parse(self, monkeypatch): from bootnode.config import Settings monkeypatch.setenv( "ALLOWED_ORIGINS", "https://lux.cloud,https://www.lux.cloud,https://cloud.lux.network", ) s = Settings() assert s.allowed_origins == [ "https://lux.cloud", "https://www.lux.cloud", "https://cloud.lux.network", ] def test_comma_separated_orgs_parse(self, monkeypatch): from bootnode.config import Settings monkeypatch.setenv("ALLOWED_ORGS", "lux, zoo ,hanzo") assert Settings().allowed_orgs == ["lux", "zoo", "hanzo"] def test_unset_keeps_default(self, monkeypatch): from bootnode.config import Settings monkeypatch.delenv("ALLOWED_ORIGINS", raising=False) assert Settings().allowed_origins == []