From 62f8cdf4de0baa34a4442f5272848cfe72e7eb69 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 7 Feb 2026 20:22:47 +0300 Subject: [PATCH] nix: cleanup Signed-off-by: NotAShelf Change-Id: Ia88656a1d6bb152398a5c4ce83d40a3e6a6a6964 --- flake.nix | 34 +- nix/demo-vm.nix | 7 +- nix/modules/nixos.nix | 386 ++++++++++-------- nix/tests/api-crud.nix | 61 ++- nix/tests/auth-rbac.nix | 16 +- nix/tests/basic-api.nix | 42 +- nix/tests/common.nix | 80 ---- nix/tests/e2e.nix | 87 +--- nix/tests/features.nix | 36 +- .../{service-startup.nix => startup.nix} | 14 +- nix/tests/webhooks.nix | 39 +- nix/vm-common.nix | 83 ++++ 12 files changed, 435 insertions(+), 450 deletions(-) delete mode 100644 nix/tests/common.nix rename nix/tests/{service-startup.nix => startup.nix} (96%) create mode 100644 nix/vm-common.nix diff --git a/flake.nix b/flake.nix index 72e2a6e..96d9459 100644 --- a/flake.nix +++ b/flake.nix @@ -46,12 +46,7 @@ cargoArtifacts = craneLib.buildDepsOnly commonArgs; in { - demo-vm = pkgs.callPackage ./nix/demo-vm.nix { - nixosModule = self.nixosModules.default; - fc-packages = { - inherit (self.packages.${system}) fc-common fc-evaluator fc-migrate-cli fc-queue-runner fc-server; - }; - }; + demo-vm = pkgs.callPackage ./nix/demo-vm.nix {inherit self;}; # FC Packages fc-common = pkgs.callPackage ./nix/packages/fc-common.nix { @@ -77,35 +72,26 @@ checks = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; - testArgs = { - nixosModule = self.nixosModules.default; - fc-packages = { - inherit (self.packages.${system}) fc-common fc-evaluator fc-migrate-cli fc-queue-runner fc-server; - }; - }; in { # Split VM integration tests - service-startup = pkgs.callPackage ./nix/tests/service-startup.nix testArgs; - basic-api = pkgs.callPackage ./nix/tests/basic-api.nix testArgs; - auth-rbac = pkgs.callPackage ./nix/tests/auth-rbac.nix testArgs; - api-crud = pkgs.callPackage ./nix/tests/api-crud.nix testArgs; - features = pkgs.callPackage ./nix/tests/features.nix testArgs; - e2e = pkgs.callPackage ./nix/tests/e2e.nix testArgs; - - # Legacy monolithic test (for reference, can be removed after split tests pass) - vm-test = pkgs.callPackage ./nix/vm-test.nix testArgs; + service-startup = pkgs.callPackage ./nix/tests/startup.nix {inherit self;}; + basic-api = pkgs.callPackage ./nix/tests/basic-api.nix {inherit self;}; + auth-rbac = pkgs.callPackage ./nix/tests/auth-rbac.nix {inherit self;}; + api-crud = pkgs.callPackage ./nix/tests/api-crud.nix {inherit self;}; + features = pkgs.callPackage ./nix/tests/features.nix {inherit self;}; + webhooks = pkgs.callPackage ./nix/tests/webhooks.nix {inherit self;}; + e2e = pkgs.callPackage ./nix/tests/e2e.nix {inherit self;}; }); devShells = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; in { default = pkgs.mkShell { - name = "fc"; + name = "fc-dev"; inputsFrom = [self.packages.${system}.fc-server]; strictDeps = true; packages = with pkgs; [ - postgresql pkg-config openssl @@ -116,5 +102,7 @@ ]; }; }); + + formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.alejandra); }; } diff --git a/nix/demo-vm.nix b/nix/demo-vm.nix index e994cae..2c1f034 100644 --- a/nix/demo-vm.nix +++ b/nix/demo-vm.nix @@ -1,15 +1,15 @@ { pkgs, - fc-packages, - nixosModule, + self, }: let + fc-packages = self.packages.${pkgs.stdenv.hostPlatform.system}; nixos = pkgs.nixos ({ modulesPath, pkgs, ... }: { imports = [ - nixosModule + self.nixosModules.fc-ci (modulesPath + "/virtualisation/qemu-vm.nix") ]; @@ -36,6 +36,7 @@ evaluatorPackage = fc-packages.fc-evaluator; queueRunnerPackage = fc-packages.fc-queue-runner; migratePackage = fc-packages.fc-migrate-cli; + server.enable = true; evaluator.enable = true; queueRunner.enable = true; diff --git a/nix/modules/nixos.nix b/nix/modules/nixos.nix index b394e3c..c5dcf63 100644 --- a/nix/modules/nixos.nix +++ b/nix/modules/nixos.nix @@ -4,60 +4,73 @@ lib, ... }: let + inherit (lib.modules) mkIf mkDefault; inherit (lib.options) mkOption mkEnableOption; inherit (lib.types) bool str int package listOf submodule nullOr; + inherit (lib.attrsets) recursiveUpdate optionalAttrs; + inherit (lib.lists) optional map; + cfg = config.services.fc; settingsFormat = pkgs.formats.toml {}; settingsType = settingsFormat.type; # Build the final settings by merging declarative config into settings - finalSettings = lib.recursiveUpdate cfg.settings (lib.optionalAttrs (cfg.declarative.projects != [] || cfg.declarative.apiKeys != []) { + finalSettings = recursiveUpdate cfg.settings (optionalAttrs (cfg.declarative.projects != [] || cfg.declarative.apiKeys != []) { declarative = { - projects = map (p: { - name = p.name; - repository_url = p.repositoryUrl; - description = p.description or null; - jobsets = map (j: { - name = j.name; - nix_expression = j.nixExpression; - enabled = j.enabled; - flake_mode = j.flakeMode; - check_interval = j.checkInterval; - }) p.jobsets; - }) cfg.declarative.projects; - api_keys = map (k: { - name = k.name; - key = k.key; - role = k.role; - }) cfg.declarative.apiKeys; + projects = + map (p: { + name = p.name; + repository_url = p.repositoryUrl; + description = p.description or null; + jobsets = + map (j: { + name = j.name; + nix_expression = j.nixExpression; + enabled = j.enabled; + flake_mode = j.flakeMode; + check_interval = j.checkInterval; + }) + p.jobsets; + }) + cfg.declarative.projects; + + api_keys = + map (k: { + name = k.name; + key = k.key; + role = k.role; + }) + cfg.declarative.apiKeys; }; }); settingsFile = settingsFormat.generate "fc.toml" finalSettings; - inherit (builtins) map; - jobsetOpts = { options = { - name = mkOption { - type = str; - description = "Jobset name."; - }; - nixExpression = mkOption { - type = str; - description = "Nix expression to evaluate (e.g. 'packages', 'checks', 'hydraJobs')."; - }; enabled = mkOption { type = bool; default = true; description = "Whether this jobset is enabled for evaluation."; }; + + name = mkOption { + type = str; + description = "Jobset name."; + }; + + nixExpression = mkOption { + type = str; + description = "Nix expression to evaluate (e.g. 'packages', 'checks', 'hydraJobs')."; + }; + flakeMode = mkOption { type = bool; default = true; description = "Whether to evaluate as a flake."; }; + checkInterval = mkOption { type = int; default = 60; @@ -72,15 +85,18 @@ type = str; description = "Project name (unique identifier)."; }; + repositoryUrl = mkOption { type = str; description = "Git repository URL."; }; + description = mkOption { type = nullOr str; default = null; description = "Optional project description."; }; + jobsets = mkOption { type = listOf (submodule jobsetOpts); default = []; @@ -95,6 +111,7 @@ type = str; description = "Human-readable name for this API key."; }; + key = mkOption { type = str; description = '' @@ -102,10 +119,21 @@ Will be hashed before storage. Consider using a secrets manager. ''; }; + role = mkOption { type = str; default = "admin"; - description = "Role: admin, read-only, create-projects, eval-jobset, cancel-build, restart-jobs, bump-to-front."; + description = '' + Role: + + * admin, + * read-only, + * create-projects, + * eval-jobset, + * cancel-build, + * restart-jobs, + * bump-to-front. + ''; }; }; }; @@ -121,13 +149,15 @@ in { evaluatorPackage = mkOption { type = package; default = cfg.package; - description = "The FC evaluator package. Defaults to cfg.package."; + defaultText = "cfg.package"; + description = "The FC evaluator package."; }; queueRunnerPackage = mkOption { type = package; default = cfg.package; - description = "The FC queue runner package. Defaults to cfg.package."; + defaultText = "cfg.package"; + description = "The FC queue runner package."; }; migratePackage = mkOption { @@ -139,8 +169,8 @@ in { type = settingsType; default = {}; description = '' - FC configuration as a Nix attribute set. - Will be converted to TOML and written to fc.toml. + FC configuration as a Nix attribute set. Will be converted to TOML + and written to {file}`fc.toml`. ''; }; @@ -152,19 +182,23 @@ in { Declarative project definitions. These are upserted on every server startup, ensuring the database matches this configuration. ''; - example = lib.literalExpression '' - [ - { - name = "my-project"; - repositoryUrl = "https://github.com/user/repo"; - description = "My Nix project"; - jobsets = [ - { name = "packages"; nixExpression = "packages"; } - { name = "checks"; nixExpression = "checks"; } - ]; - } - ] - ''; + example = [ + { + name = "my-project"; + repositoryUrl = "https://github.com/user/repo"; + description = "My Nix project"; + jobsets = [ + { + name = "packages"; + nixExpression = "packages"; + } + { + name = "checks"; + nixExpression = "checks"; + } + ]; + } + ]; }; apiKeys = mkOption { @@ -174,12 +208,18 @@ in { Declarative API key definitions. Keys are upserted on every server startup. Use a secrets manager for production deployments. ''; - example = lib.literalExpression '' - [ - { name = "admin"; key = "fc_admin_secret"; role = "admin"; } - { name = "ci-bot"; key = "fc_ci_bot_key"; role = "eval-jobset"; } - ] - ''; + example = [ + { + name = "admin"; + key = "fc_admin_secret"; + role = "admin"; + } + { + name = "ci-bot"; + key = "fc_ci_bot_key"; + role = "eval-jobset"; + } + ]; }; }; @@ -204,7 +244,7 @@ in { }; }; - config = lib.mkIf cfg.enable { + config = mkIf cfg.enable { users.users.fc = { isSystemUser = true; group = "fc"; @@ -214,7 +254,7 @@ in { users.groups.fc = {}; - services.postgresql = lib.mkIf cfg.database.createLocally { + services.postgresql = mkIf cfg.database.createLocally { enable = true; ensureDatabases = ["fc"]; ensureUsers = [ @@ -225,14 +265,18 @@ in { ]; }; - services.fc.settings = lib.mkDefault { + services.fc.settings = mkDefault { database.url = "postgresql:///fc?host=/run/postgresql"; server.host = "127.0.0.1"; server.port = 3000; - gc.gc_roots_dir = "/nix/var/nix/gcroots/per-user/fc/fc-roots"; - gc.enabled = true; - gc.max_age_days = 30; - gc.cleanup_interval = 3600; + + gc = { + gc_roots_dir = "/nix/var/nix/gcroots/per-user/fc/fc-roots"; + enabled = true; + max_age_days = 30; + cleanup_interval = 3600; + }; + logs.log_dir = "/var/lib/fc/logs"; cache.enabled = true; evaluator.restrict_eval = true; @@ -240,125 +284,129 @@ in { signing.enabled = false; }; - systemd.tmpfiles.rules = [ - (lib.mkIf cfg.server.enable "d /var/lib/fc/logs 0750 fc fc -") - (lib.mkIf cfg.queueRunner.enable "d /nix/var/nix/gcroots/per-user/fc 0755 fc fc -") - ]; - - systemd.services.fc-server = lib.mkIf cfg.server.enable { - description = "FC CI Server"; - wantedBy = ["multi-user.target"]; - after = ["network.target"] ++ lib.optional cfg.database.createLocally "postgresql.target"; - requires = lib.optional cfg.database.createLocally "postgresql.target"; - - path = with pkgs; [nix zstd]; - - serviceConfig = { - ExecStartPre = "${cfg.migratePackage}/bin/fc-migrate up ${finalSettings.database.url or "postgresql:///fc?host=/run/postgresql"}"; - ExecStart = "${cfg.package}/bin/fc-server"; - Restart = "on-failure"; - RestartSec = 5; - User = "fc"; - Group = "fc"; - StateDirectory = "fc"; - LogsDirectory = "fc"; - WorkingDirectory = "/var/lib/fc"; - ReadWritePaths = ["/var/lib/fc"]; - - # Hardening - ProtectSystem = "strict"; - ProtectHome = true; - NoNewPrivileges = true; - PrivateTmp = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - }; - - environment = { - FC_CONFIG_FILE = "${settingsFile}"; - }; - }; - - systemd.services.fc-evaluator = lib.mkIf cfg.evaluator.enable { - description = "FC CI Evaluator"; - wantedBy = ["multi-user.target"]; - after = ["network.target" "fc-server.service"] ++ lib.optional cfg.database.createLocally "postgresql.target"; - requires = ["fc-server.service"] ++ lib.optional cfg.database.createLocally "postgresql.target"; - - path = with pkgs; [ - nix - git - nix-eval-jobs + systemd = { + tmpfiles.rules = [ + (mkIf cfg.server.enable "d /var/lib/fc/logs 0750 fc fc -") + (mkIf cfg.queueRunner.enable "d /nix/var/nix/gcroots/per-user/fc 0755 fc fc -") ]; - serviceConfig = { - ExecStart = "${cfg.evaluatorPackage}/bin/fc-evaluator"; - Restart = "on-failure"; - RestartSec = 10; - User = "fc"; - Group = "fc"; - StateDirectory = "fc"; - WorkingDirectory = "/var/lib/fc"; - ReadWritePaths = ["/var/lib/fc"]; + services = { + fc-server = mkIf cfg.server.enable { + description = "FC CI Server"; + wantedBy = ["multi-user.target"]; + after = ["network.target"] ++ optional cfg.database.createLocally "postgresql.target"; + requires = optional cfg.database.createLocally "postgresql.target"; - # Hardening - ProtectSystem = "strict"; - ProtectHome = true; - NoNewPrivileges = true; - PrivateTmp = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - }; + path = with pkgs; [nix zstd]; - environment = { - FC_CONFIG_FILE = "${settingsFile}"; - FC_EVALUATOR__WORK_DIR = "/var/lib/fc/evaluator"; - FC_EVALUATOR__RESTRICT_EVAL = "true"; - }; - }; + serviceConfig = { + ExecStartPre = "${cfg.migratePackage}/bin/fc-migrate up ${finalSettings.database.url or "postgresql:///fc?host=/run/postgresql"}"; + ExecStart = "${cfg.package}/bin/fc-server"; + Restart = "on-failure"; + RestartSec = 5; + User = "fc"; + Group = "fc"; + StateDirectory = "fc"; + LogsDirectory = "fc"; + WorkingDirectory = "/var/lib/fc"; + ReadWritePaths = ["/var/lib/fc"]; - systemd.services.fc-queue-runner = lib.mkIf cfg.queueRunner.enable { - description = "FC CI Queue Runner"; - wantedBy = ["multi-user.target"]; - after = ["network.target" "fc-server.service"] ++ lib.optional cfg.database.createLocally "postgresql.target"; - requires = ["fc-server.service"] ++ lib.optional cfg.database.createLocally "postgresql.target"; + # Hardening + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + }; - path = with pkgs; [ - nix - ]; + environment = { + FC_CONFIG_FILE = "${settingsFile}"; + }; + }; - serviceConfig = { - ExecStart = "${cfg.queueRunnerPackage}/bin/fc-queue-runner"; - Restart = "on-failure"; - RestartSec = 10; - User = "fc"; - Group = "fc"; - StateDirectory = "fc"; - LogsDirectory = "fc"; - WorkingDirectory = "/var/lib/fc"; - ReadWritePaths = [ - "/var/lib/fc" - "/nix/var/nix/gcroots/per-user/fc" - ]; + fc-evaluator = mkIf cfg.evaluator.enable { + description = "FC CI Evaluator"; + wantedBy = ["multi-user.target"]; + after = ["network.target" "fc-server.service"] ++ optional cfg.database.createLocally "postgresql.target"; + requires = ["fc-server.service"] ++ optional cfg.database.createLocally "postgresql.target"; - # Hardening - ProtectSystem = "strict"; - ProtectHome = true; - NoNewPrivileges = true; - PrivateTmp = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - RestrictSUIDSGID = true; - }; + path = with pkgs; [ + nix + git + nix-eval-jobs + ]; - environment = { - FC_CONFIG_FILE = "${settingsFile}"; - FC_QUEUE_RUNNER__WORK_DIR = "/var/lib/fc/queue-runner"; + serviceConfig = { + ExecStart = "${cfg.evaluatorPackage}/bin/fc-evaluator"; + Restart = "on-failure"; + RestartSec = 10; + User = "fc"; + Group = "fc"; + StateDirectory = "fc"; + WorkingDirectory = "/var/lib/fc"; + ReadWritePaths = ["/var/lib/fc"]; + + # Hardening + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + }; + + environment = { + FC_CONFIG_FILE = "${settingsFile}"; + FC_EVALUATOR__WORK_DIR = "/var/lib/fc/evaluator"; + FC_EVALUATOR__RESTRICT_EVAL = "true"; + }; + }; + + fc-queue-runner = mkIf cfg.queueRunner.enable { + description = "FC CI Queue Runner"; + wantedBy = ["multi-user.target"]; + after = ["network.target" "fc-server.service"] ++ optional cfg.database.createLocally "postgresql.target"; + requires = ["fc-server.service"] ++ optional cfg.database.createLocally "postgresql.target"; + + path = with pkgs; [ + nix + ]; + + serviceConfig = { + ExecStart = "${cfg.queueRunnerPackage}/bin/fc-queue-runner"; + Restart = "on-failure"; + RestartSec = 10; + User = "fc"; + Group = "fc"; + StateDirectory = "fc"; + LogsDirectory = "fc"; + WorkingDirectory = "/var/lib/fc"; + ReadWritePaths = [ + "/var/lib/fc" + "/nix/var/nix/gcroots/per-user/fc" + ]; + + # Hardening + ProtectSystem = "strict"; + ProtectHome = true; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictSUIDSGID = true; + }; + + environment = { + FC_CONFIG_FILE = "${settingsFile}"; + FC_QUEUE_RUNNER__WORK_DIR = "/var/lib/fc/queue-runner"; + }; + }; }; }; }; diff --git a/nix/tests/api-crud.nix b/nix/tests/api-crud.nix index f6a197e..4f9ab59 100644 --- a/nix/tests/api-crud.nix +++ b/nix/tests/api-crud.nix @@ -1,14 +1,17 @@ -# API CRUD tests: dashboard content, project/jobset/evaluation/build/channel/builder CRUD, admin endpoints, pagination, search -{ - pkgs, - fc-packages, - nixosModule, -}: +{pkgs, self}: pkgs.testers.nixosTest { name = "fc-api-crud"; - nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;}; + nodes.machine = { + imports = [ + self.nixosModules.fc-ci + ../vm-common.nix + ]; + _module.args.self = self; + }; + # API CRUD tests: dashboard content, project/jobset/evaluation/build/channel/builder + # CRUD, admin endpoints, pagination, search testScript = '' import hashlib import json @@ -24,7 +27,7 @@ pkgs.testers.nixosTest { # Wait for the server to start listening machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30) - # ---- Seed an API key for write operations ---- + # Seed an API key for write operations # Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table api_token = "fc_testkey123" api_hash = hashlib.sha256(api_token.encode()).hexdigest() @@ -51,11 +54,7 @@ pkgs.testers.nixosTest { ) project_id = result.strip() - # ======================================================================== - # Phase 4: Dashboard Content & Deep Functional Tests - # ======================================================================== - - # ---- 4A: Dashboard content verification ---- + # Dashboard content verification with subtest("Home page contains Dashboard heading"): body = machine.succeed("curl -sf http://127.0.0.1:3000/") assert "Dashboard" in body, "Home page missing 'Dashboard' heading" @@ -111,7 +110,7 @@ pkgs.testers.nixosTest { assert "api_key" in body or "API" in body, "Login page missing API key input" assert "= 1, f"Expected at least 1 new job, got {data['new_jobs']}" assert any(j["job_name"] == "new-pkg" for j in data["new_jobs"]), "new-pkg should be in new_jobs" - # ---- 4F: Channel CRUD lifecycle ---- + # Channel CRUD lifecycle ---- with subtest("Create channel via API"): result = machine.succeed( "curl -sf -X POST http://127.0.0.1:3000/api/v1/channels " @@ -382,7 +381,7 @@ pkgs.testers.nixosTest { ) assert code.strip() == "200", f"Expected 200 for channel delete, got {code.strip()}" - # ---- 4G: Remote builder CRUD lifecycle ---- + # Remote builder CRUD lifecycle with subtest("List remote builders"): result = machine.succeed( "curl -sf http://127.0.0.1:3000/api/v1/admin/builders | jq 'length'" @@ -456,7 +455,7 @@ pkgs.testers.nixosTest { ) assert code.strip() == "200", f"Expected 200 for builder delete, got {code.strip()}" - # ---- 4H: Admin system status endpoint ---- + # Admin system status endpoint with subtest("System status endpoint requires admin"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' " @@ -473,7 +472,7 @@ pkgs.testers.nixosTest { ) assert int(result.strip()) >= 1, f"Expected at least 1 project in system status, got {result.strip()}" - # ---- 4I: API key listing ---- + # API key listing with subtest("List API keys requires admin"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' " @@ -490,7 +489,7 @@ pkgs.testers.nixosTest { ) assert int(result.strip()) >= 1, f"Expected at least 1 API key, got {result.strip()}" - # ---- 4J: Badge endpoints ---- + # Badge endpoints with subtest("Badge endpoint returns SVG for unknown project"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' " @@ -523,7 +522,7 @@ pkgs.testers.nixosTest { ) assert code.strip() in ("404", "500"), f"Expected 404/500 for latest build, got {code.strip()}" - # ---- 4K: Pagination tests ---- + # Pagination tests # Re-verify server is healthy before pagination tests machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=15) @@ -555,7 +554,7 @@ pkgs.testers.nixosTest { assert "limit" in data, f"Expected paginated response with 'limit' field, got: {result[:300]}" assert data["limit"] == 2, f"Expected limit=2, got {data['limit']}" - # ---- 4L: Build sub-resources ---- + # Build sub-resources with subtest("Build steps endpoint returns empty array for nonexistent build"): result = machine.succeed( "curl -sf " @@ -579,7 +578,7 @@ pkgs.testers.nixosTest { ) assert code.strip() == "404", f"Expected 404 for nonexistent build log, got {code.strip()}" - # ---- 4M: Search functionality ---- + # Search functionality with subtest("Search returns matching projects"): result = machine.succeed( "curl -sf 'http://127.0.0.1:3000/api/v1/search?q=test-project' | jq '.projects | length'" @@ -592,7 +591,7 @@ pkgs.testers.nixosTest { ) assert result.strip() == "0", f"Expected 0, got {result.strip()}" - # ---- 4N: Content-Type verification for API endpoints ---- + # Content-Type verification for API endpoints with subtest("API endpoints return application/json"): ct = machine.succeed( "curl -s -D - -o /dev/null http://127.0.0.1:3000/api/v1/projects | grep -i content-type" @@ -611,7 +610,7 @@ pkgs.testers.nixosTest { ) assert "text/plain" in ct.lower() or "text/" in ct.lower(), f"Expected text content type for metrics, got: {ct}" - # ---- 4O: Session/Cookie auth for dashboard ---- + # Session/Cookie auth for dashboard with subtest("Login with valid API key sets session cookie"): result = machine.succeed( "curl -s -D - -o /dev/null " @@ -661,7 +660,7 @@ pkgs.testers.nixosTest { assert "Max-Age=0" in result or "max-age=0" in result.lower(), \ "Logout should set Max-Age=0 to clear cookie" - # ---- 4P: RBAC with create-projects role ---- + # RBAC with create-projects role cp_token = "fc_createprojects_key" cp_hash = hashlib.sha256(cp_token.encode()).hexdigest() machine.succeed( @@ -709,7 +708,7 @@ pkgs.testers.nixosTest { ) assert code.strip() == "403", f"Expected 403 for create-projects system status, got {code.strip()}" - # ---- 4Q: Additional security tests ---- + # Additional security tests with subtest("DELETE project without auth returns 401"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' " diff --git a/nix/tests/auth-rbac.nix b/nix/tests/auth-rbac.nix index 9e8b8aa..ade5f2e 100644 --- a/nix/tests/auth-rbac.nix +++ b/nix/tests/auth-rbac.nix @@ -1,14 +1,16 @@ -# Authentication and RBAC tests -{ - pkgs, - fc-packages, - nixosModule, -}: +{pkgs, self}: pkgs.testers.nixosTest { name = "fc-auth-rbac"; - nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;}; + nodes.machine = { + imports = [ + self.nixosModules.fc-ci + ../vm-common.nix + ]; + _module.args.self = self; + }; + # Authentication and RBAC tests testScript = '' import hashlib import json diff --git a/nix/tests/basic-api.nix b/nix/tests/basic-api.nix index ea2a070..8ad8d3a 100644 --- a/nix/tests/basic-api.nix +++ b/nix/tests/basic-api.nix @@ -1,12 +1,14 @@ -{ - pkgs, - fc-packages, - nixosModule, -}: +{pkgs, self}: pkgs.testers.nixosTest { name = "fc-basic-api"; - nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;}; + nodes.machine = { + imports = [ + self.nixosModules.fc-ci + ../vm-common.nix + ]; + _module.args.self = self; + }; testScript = '' import hashlib @@ -22,7 +24,7 @@ pkgs.testers.nixosTest { # Wait for the server to start listening machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30) - # ---- Seed an API key for write operations ---- + ## Seed an API key for write operations # Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table api_token = "fc_testkey123" api_hash = hashlib.sha256(api_token.encode()).hexdigest() @@ -31,7 +33,7 @@ pkgs.testers.nixosTest { ) auth_header = f"-H 'Authorization: Bearer {api_token}'" - # ---- Health endpoint ---- + # Health endpoint with subtest("Health endpoint returns OK"): result = machine.succeed("curl -sf http://127.0.0.1:3000/health | jq -r .status") assert result.strip() == "ok", f"Expected 'ok', got '{result.strip()}'" @@ -40,13 +42,13 @@ pkgs.testers.nixosTest { result = machine.succeed("curl -sf http://127.0.0.1:3000/health | jq -r .database") assert result.strip() == "true", f"Expected 'true', got '{result.strip()}'" - # ---- Cache endpoint: nix-cache-info ---- + # Cache endpoint: nix-cache-info with subtest("Cache info endpoint returns correct data"): result = machine.succeed("curl -sf http://127.0.0.1:3000/nix-cache/nix-cache-info") assert "StoreDir: /nix/store" in result, f"Missing StoreDir in: {result}" assert "WantMassQuery: 1" in result, f"Missing WantMassQuery in: {result}" - # ---- Cache endpoint: invalid hash rejection ---- + # Cache endpoint: invalid hash rejection with subtest("Cache rejects short hash"): machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/tooshort.narinfo | grep -q 404") @@ -59,14 +61,14 @@ pkgs.testers.nixosTest { with subtest("Cache returns 404 for valid but nonexistent hash"): machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/abcdefghijklmnopqrstuvwxyz012345.narinfo | grep -q 404") - # ---- NAR endpoints: invalid hash rejection ---- + # NAR endpoints: invalid hash rejection with subtest("NAR zst rejects invalid hash"): machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/nar/INVALID.nar.zst | grep -q 404") with subtest("NAR plain rejects invalid hash"): machine.succeed("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/nix-cache/nar/INVALID.nar | grep -q 404") - # ---- Search endpoint: length validation ---- + # Search endpoint: length validation with subtest("Search rejects empty query"): result = machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/search?q=' | jq '.projects | length'") assert result.strip() == "0", f"Expected 0 projects, got {result.strip()}" @@ -76,12 +78,12 @@ pkgs.testers.nixosTest { result = machine.succeed(f"curl -sf 'http://127.0.0.1:3000/api/v1/search?q={long_q}' | jq '.projects | length'") assert result.strip() == "0", f"Expected 0 projects for long query, got {result.strip()}" - # ---- Error response format ---- + # Error response format with subtest("404 error response includes error_code field"): json_result = machine.succeed("curl -s http://127.0.0.1:3000/api/v1/projects/00000000-0000-0000-0000-000000000000 | jq -r .error_code") assert json_result.strip() == "NOT_FOUND", f"Expected NOT_FOUND, got {json_result.strip()}" - # ---- Empty page states (before any data is created) ---- + # Empty page states (before any data is created) with subtest("Empty evaluations page has proper empty state"): body = machine.succeed("curl -sf http://127.0.0.1:3000/evaluations") assert "Page 1 of 0" not in body, \ @@ -107,7 +109,7 @@ pkgs.testers.nixosTest { assert "table-wrap" in body, \ "Projects page should wrap tables in .table-wrap class" - # ---- API CRUD: create and list projects ---- + # API CRUD: create and list projects with subtest("Create a project via API"): result = machine.succeed( "curl -sf -X POST http://127.0.0.1:3000/api/v1/projects " @@ -123,7 +125,7 @@ pkgs.testers.nixosTest { result = machine.succeed("curl -sf http://127.0.0.1:3000/api/v1/projects | jq '.items[0].name'") assert "test-project" in result, f"Expected test-project in: {result}" - # ---- Builds list with filters ---- + # Builds list with filters with subtest("Builds list with system filter returns 200"): machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/builds?system=x86_64-linux' | jq '.items'") @@ -133,14 +135,14 @@ pkgs.testers.nixosTest { with subtest("Builds list with combined filters returns 200"): machine.succeed("curl -sf 'http://127.0.0.1:3000/api/v1/builds?system=x86_64-linux&status=pending&job_name=test' | jq '.items'") - # ---- Metrics endpoint ---- + # Metrics endpoint with subtest("Metrics endpoint returns prometheus format"): result = machine.succeed("curl -sf http://127.0.0.1:3000/metrics") assert "fc_builds_total" in result, "Missing fc_builds_total in metrics" assert "fc_projects_total" in result, "Missing fc_projects_total in metrics" assert "fc_evaluations_total" in result, "Missing fc_evaluations_total in metrics" - # ---- CORS: default restrictive (no Access-Control-Allow-Origin for cross-origin) ---- + # CORS: default restrictive (no Access-Control-Allow-Origin for cross-origin) with subtest("Default CORS does not allow arbitrary origins"): result = machine.succeed( "curl -s -D - " @@ -153,7 +155,7 @@ pkgs.testers.nixosTest { assert "access-control-allow-origin: http://evil.example.com" not in result.lower(), \ f"CORS should not allow arbitrary origins: {result}" - # ---- Systemd hardening ---- + # Systemd hardening with subtest("fc-server runs as fc user"): result = machine.succeed("systemctl show fc-server --property=User --value") assert result.strip() == "fc", f"Expected fc user, got '{result.strip()}'" @@ -168,7 +170,7 @@ pkgs.testers.nixosTest { with subtest("Log directory exists"): machine.succeed("test -d /var/lib/fc/logs || mkdir -p /var/lib/fc/logs") - # ---- Stats endpoint ---- + # Stats endpoint with subtest("Build stats endpoint returns data"): result = machine.succeed("curl -sf http://127.0.0.1:3000/api/v1/builds/stats | jq '.total_builds'") # Should be a number (possibly 0) diff --git a/nix/tests/common.nix b/nix/tests/common.nix deleted file mode 100644 index f209b4e..0000000 --- a/nix/tests/common.nix +++ /dev/null @@ -1,80 +0,0 @@ -# Common machine configuration for all FC integration tests -{ - pkgs, - fc-packages, - nixosModule, -}: { - imports = [nixosModule]; - - programs.git.enable = true; - security.sudo.enable = true; - - # Ensure nix and zstd are available for cache endpoints - environment.systemPackages = with pkgs; [nix nix-eval-jobs zstd curl jq openssl]; - - services.fc = { - enable = true; - package = fc-packages.fc-server; - evaluatorPackage = fc-packages.fc-evaluator; - queueRunnerPackage = fc-packages.fc-queue-runner; - migratePackage = fc-packages.fc-migrate-cli; - - server.enable = true; - evaluator.enable = true; - queueRunner.enable = true; - - settings = { - database.url = "postgresql:///fc?host=/run/postgresql"; - server = { - host = "127.0.0.1"; - port = 3000; - cors_permissive = false; - }; - - gc.enabled = false; - logs.log_dir = "/var/lib/fc/logs"; - cache.enabled = true; - signing.enabled = false; - - tracing = { - level = "info"; - format = "compact"; - show_targets = true; - show_timestamps = true; - }; - - evaluator.poll_interval = 5; - evaluator.work_dir = "/var/lib/fc/evaluator"; - queue_runner.poll_interval = 3; - queue_runner.work_dir = "/var/lib/fc/queue-runner"; - - # Declarative bootstrap: project + API key created on startup - declarative = { - projects = [ - { - name = "declarative-project"; - repository_url = "https://github.com/test/declarative"; - description = "Bootstrap test project"; - jobsets = [ - { - name = "packages"; - nix_expression = "packages"; - enabled = true; - flake_mode = true; - check_interval = 300; - } - ]; - } - ]; - - api_keys = [ - { - name = "bootstrap-admin"; - key = "fc_bootstrap_key"; - role = "admin"; - } - ]; - }; - }; - }; -} diff --git a/nix/tests/e2e.nix b/nix/tests/e2e.nix index aab719e..9674f4d 100644 --- a/nix/tests/e2e.nix +++ b/nix/tests/e2e.nix @@ -1,14 +1,17 @@ -# End-to-end tests: flake creation, evaluation, queue runner, notification, signing, GC, declarative, webhooks -{ - pkgs, - fc-packages, - nixosModule, -}: +{pkgs, self}: pkgs.testers.nixosTest { name = "fc-e2e"; - nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;}; + nodes.machine = { + imports = [ + self.nixosModules.fc-ci + ../vm-common.nix + ]; + _module.args.self = self; + }; + # End-to-end tests: flake creation, evaluation, queue runner, notification, + # signing, GC, declarative, webhooks testScript = '' import hashlib import json @@ -24,7 +27,7 @@ pkgs.testers.nixosTest { # Wait for the server to start listening machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30) - # ---- Seed an API key for write operations ---- + # Seed an API key for write operations # Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table api_token = "fc_testkey123" api_hash = hashlib.sha256(api_token.encode()).hexdigest() @@ -41,11 +44,7 @@ pkgs.testers.nixosTest { ) ro_header = f"-H 'Authorization: Bearer {ro_token}'" - # ======================================================================== - # Phase E2E-1: End-to-End Evaluator Integration Test - # ======================================================================== - - # ---- Create a test flake inside the VM ---- + # Create a test flake inside the VM with subtest("Create bare git repo with test flake"): machine.succeed("mkdir -p /var/lib/fc/test-repos") machine.succeed("git init --bare /var/lib/fc/test-repos/test-flake.git") @@ -78,7 +77,7 @@ pkgs.testers.nixosTest { # Set ownership for fc user machine.succeed("chown -R fc:fc /var/lib/fc/test-repos") - # ---- Create project + jobset pointing to the local repo via API ---- + # Create project + jobset pointing to the local repo via API with subtest("Create E2E project and jobset via API"): result = machine.succeed( "curl -sf -X POST http://127.0.0.1:3000/api/v1/projects " @@ -100,10 +99,10 @@ pkgs.testers.nixosTest { e2e_jobset_id = result.strip() assert len(e2e_jobset_id) == 36, f"Expected UUID for jobset, got '{e2e_jobset_id}'" - # ---- Wait for evaluator to pick it up and create an evaluation ---- + # Wait for evaluator to pick it up and create an evaluation with subtest("Evaluator discovers and evaluates the flake"): - # The evaluator is already running (started in Phase 1) - # Poll for evaluation to appear with status "completed" + # The evaluator is already running, poll for evaluation to appear + # with status "completed" machine.wait_until_succeeds( f"curl -sf 'http://127.0.0.1:3000/api/v1/evaluations?jobset_id={e2e_jobset_id}' " "| jq -e '.items[] | select(.status==\"completed\")'", @@ -136,7 +135,7 @@ pkgs.testers.nixosTest { f"curl -sf 'http://127.0.0.1:3000/api/v1/builds?evaluation_id={e2e_eval_id}' | jq -r '.items[0].id'" ).strip() - # ---- Test evaluation caching ---- + # Test evaluation caching with subtest("Same commit does not trigger a new evaluation"): # Get current evaluation count before_count = machine.succeed( @@ -149,7 +148,7 @@ pkgs.testers.nixosTest { ).strip() assert before_count == after_count, f"Evaluation count changed from {before_count} to {after_count} (should be cached)" - # ---- Test new commit triggers new evaluation ---- + # Test new commit triggers new evaluation with subtest("New commit triggers new evaluation"): before_count_int = int(machine.succeed( f"curl -sf 'http://127.0.0.1:3000/api/v1/evaluations?jobset_id={e2e_jobset_id}' | jq '.items | length'" @@ -181,10 +180,6 @@ pkgs.testers.nixosTest { timeout=60 ) - # ======================================================================== - # Phase E2E-2: End-to-End Queue Runner Integration Test - # ======================================================================== - with subtest("Queue runner builds pending derivation"): # Poll the E2E build until completed (queue-runner is already running) machine.wait_until_succeeds( @@ -230,10 +225,6 @@ pkgs.testers.nixosTest { ).strip() assert code == "200", f"Expected 200 for build log, got {code}" - # ======================================================================== - # Phase E2E-3: Jobset Input Management API - # ======================================================================== - with subtest("Create jobset input via API"): result = machine.succeed( f"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects/{e2e_project_id}/jobsets/{e2e_jobset_id}/inputs " @@ -285,10 +276,6 @@ pkgs.testers.nixosTest { ).strip() assert code == "403", f"Expected 403 for read-only input delete, got {code}" - # ======================================================================== - # Phase E2E-4: Notification Dispatch - # ======================================================================== - # Notifications are dispatched after builds complete (already tested above). # Verify run_command notifications work: with subtest("Notification run_command is invoked on build completion"): @@ -300,10 +287,6 @@ pkgs.testers.nixosTest { ).strip() assert result == "completed", f"Expected completed after notification, got {result}" - # ======================================================================== - # Phase E2E-5: Channel Auto-Promotion - # ======================================================================== - with subtest("Channel auto-promotion after all builds complete"): # Create a channel tracking the E2E jobset result = machine.succeed( @@ -324,10 +307,6 @@ pkgs.testers.nixosTest { timeout=30 ) - # ======================================================================== - # Phase E2E-6: Binary Cache NARinfo Test - # ======================================================================== - with subtest("Binary cache serves NARinfo for built output"): # Get the build output path output_path = machine.succeed( @@ -353,10 +332,6 @@ pkgs.testers.nixosTest { assert "StorePath:" in narinfo, f"NARinfo missing StorePath: {narinfo}" assert "NarHash:" in narinfo, f"NARinfo missing NarHash: {narinfo}" - # ======================================================================== - # Phase E2E-7: Build Retry on Failure - # ======================================================================== - with subtest("Build with invalid drv_path fails and retries"): # Insert a build with an invalid drv_path via SQL machine.succeed( @@ -378,10 +353,6 @@ pkgs.testers.nixosTest { ).strip() assert result == "failed", f"Expected failed for bad build, got '{result}'" - # ======================================================================== - # Phase E2E-8: Notification Dispatch (run_command) - # ======================================================================== - with subtest("Notification run_command invoked on build completion"): # Write a notification script machine.succeed("mkdir -p /var/lib/fc") @@ -458,10 +429,6 @@ pkgs.testers.nixosTest { f"Expected BUILD_STATUS in notification output, got: {output}" assert notify_build_id in output, f"Expected build ID {notify_build_id} in output, got: {output}" - # ======================================================================== - # Phase E2E-9: Nix Signing - # ======================================================================== - with subtest("Generate signing key and configure signing"): # Generate a Nix signing key machine.succeed("mkdir -p /var/lib/fc/keys") @@ -537,10 +504,6 @@ pkgs.testers.nixosTest { # The verify command should succeed (exit 0) if signatures are valid machine.succeed(f"nix store verify --sigs-needed 1 {output_path}") - # ======================================================================== - # Phase E2E-10: GC Roots - # ======================================================================== - with subtest("GC roots are created for build products"): # Enable GC in config machine.succeed(""" @@ -607,18 +570,14 @@ pkgs.testers.nixosTest { found_root = True break - # We might have GC roots - this is expected behavior - # The key is that the build output exists and is protected from GC + # We might have GC roots, this is expected behavior + # The key thing is that the build output exists and is protected from GC machine.succeed(f"test -e {gc_build_output}") else: # If no GC roots yet, at least verify the build output exists # GC roots might be created asynchronously machine.succeed(f"test -e {gc_build_output}") - # ======================================================================== - # Phase E2E-11: Declarative In-Repo Config - # ======================================================================== - with subtest("Declarative .fc.toml in repo auto-creates jobset"): # Add .fc.toml to the test repo with a new jobset definition machine.succeed(""" @@ -641,10 +600,6 @@ pkgs.testers.nixosTest { timeout=60 ) - # ======================================================================== - # Phase E2E-12: Webhook Endpoint - # ======================================================================== - with subtest("Webhook endpoint accepts valid GitHub push"): # Create a webhook config via SQL (no REST endpoint for creation) machine.succeed( @@ -697,7 +652,7 @@ pkgs.testers.nixosTest { ).strip() assert code == "401", f"Expected 401 for invalid webhook signature, got {code}" - # ---- Cleanup: Delete project ---- + # Cleanup: Delete project with subtest("Delete E2E project"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' " diff --git a/nix/tests/features.nix b/nix/tests/features.nix index 80bdcbe..cc6a920 100644 --- a/nix/tests/features.nix +++ b/nix/tests/features.nix @@ -1,14 +1,16 @@ -# Feature tests: logging, CSS, setup wizard, probe, metrics improvements -{ - pkgs, - fc-packages, - nixosModule, -}: +{pkgs, self}: pkgs.testers.nixosTest { name = "fc-features"; - nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;}; + nodes.machine = { + imports = [ + self.nixosModules.fc-ci + ../vm-common.nix + ]; + _module.args.self = self; + }; + # Feature tests: logging, CSS, setup wizard, probe, metrics improvements testScript = '' import hashlib @@ -23,7 +25,7 @@ pkgs.testers.nixosTest { # Wait for the server to start listening machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30) - # ---- Seed an API key for write operations ---- + # Seed an API key for write operations # Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table api_token = "fc_testkey123" api_hash = hashlib.sha256(api_token.encode()).hexdigest() @@ -40,11 +42,7 @@ pkgs.testers.nixosTest { ) ro_header = f"-H 'Authorization: Bearer {ro_token}'" - # ======================================================================== - # Phase 5: New Feature Tests (Structured Logging, Flake Probe, Setup Wizard, Dashboard) - # ======================================================================== - - # ---- 5A: Structured logging ---- + # Structured logging ---- with subtest("Server produces structured log output"): # The server should log via tracing with the configured format result = machine.succeed("journalctl -u fc-server --no-pager -n 50 2>&1") @@ -52,7 +50,7 @@ pkgs.testers.nixosTest { assert "INFO" in result or "info" in result, \ "Expected structured log lines with INFO level in journalctl output" - # ---- 5B: Static CSS serving ---- + # Static CSS serving ---- with subtest("Static CSS endpoint returns 200 with correct content type"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/static/style.css" @@ -63,7 +61,7 @@ pkgs.testers.nixosTest { ) assert "text/css" in ct.lower(), f"Expected text/css, got: {ct}" - # ---- 5C: Setup wizard page ---- + # Setup wizard page with subtest("Setup wizard page returns 200"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/projects/new" @@ -92,7 +90,7 @@ pkgs.testers.nixosTest { ) assert '/projects/new' in body, "Projects page should link to /projects/new wizard" - # ---- 5D: Flake probe endpoint ---- + # Flake probe endpoint with subtest("Probe endpoint exists and requires POST"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' " @@ -116,7 +114,7 @@ pkgs.testers.nixosTest { assert code.strip() in ("200", "408", "422", "500"), \ f"Expected 200/408/422/500 for probe of unreachable repo, got {code.strip()}" - # ---- 5E: Setup endpoint ---- + # Setup endpoint with subtest("Setup endpoint exists and requires POST"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' " @@ -157,7 +155,7 @@ pkgs.testers.nixosTest { f"{auth_header}" ) - # ---- 5F: Dashboard improvements ---- + # Dashboard improvements with subtest("Home page has dashboard-grid two-column layout"): body = machine.succeed("curl -sf http://127.0.0.1:3000/") assert "dashboard-grid" in body, "Home page should have dashboard-grid class" @@ -179,7 +177,7 @@ pkgs.testers.nixosTest { ) assert "escapeHtml" in body, "Admin page JS should use escapeHtml" - # ---- 4R: Metrics reflect actual data ---- + # Metrics reflect actual data with subtest("Metrics fc_projects_total reflects created projects"): result = machine.succeed("curl -sf http://127.0.0.1:3000/metrics") for line in result.split("\n"): diff --git a/nix/tests/service-startup.nix b/nix/tests/startup.nix similarity index 96% rename from nix/tests/service-startup.nix rename to nix/tests/startup.nix index 2829af9..62fc94b 100644 --- a/nix/tests/service-startup.nix +++ b/nix/tests/startup.nix @@ -1,12 +1,14 @@ -{ - pkgs, - fc-packages, - nixosModule, -}: +{pkgs, self}: pkgs.testers.nixosTest { name = "fc-service-startup"; - nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;}; + nodes.machine = { + imports = [ + self.nixosModules.fc-ci + ../vm-common.nix + ]; + _module.args.self = self; + }; testScript = '' machine.start() diff --git a/nix/tests/webhooks.nix b/nix/tests/webhooks.nix index b640c2d..f1df1e5 100644 --- a/nix/tests/webhooks.nix +++ b/nix/tests/webhooks.nix @@ -1,14 +1,16 @@ -# Webhook and PR integration tests -{ - pkgs, - fc-packages, - nixosModule, -}: +{pkgs, self}: pkgs.testers.nixosTest { name = "fc-webhooks"; - nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;}; + nodes.machine = { + imports = [ + self.nixosModules.fc-ci + ../vm-common.nix + ]; + _module.args.self = self; + }; + # Webhook and PR integration tests testScript = '' import hashlib import json @@ -24,7 +26,7 @@ pkgs.testers.nixosTest { # Wait for the server to start listening machine.wait_until_succeeds("curl -sf http://127.0.0.1:3000/health", timeout=30) - # ---- Seed an API key for write operations ---- + # Seed an API key for write operations api_token = "fc_testkey123" api_hash = hashlib.sha256(api_token.encode()).hexdigest() machine.succeed( @@ -32,7 +34,7 @@ pkgs.testers.nixosTest { ) auth_header = f"-H 'Authorization: Bearer {api_token}'" - # ---- Create a test project for webhook tests ---- + # Create a test project for webhook tests with subtest("Create test project for webhooks"): result = machine.succeed( "curl -sf -X POST http://127.0.0.1:3000/api/v1/projects " @@ -44,7 +46,7 @@ pkgs.testers.nixosTest { project_id = result.strip() assert len(project_id) == 36, f"Expected UUID, got '{project_id}'" - # ---- Create a jobset for the project ---- + # Create a jobset for the project with subtest("Create jobset for webhook project"): result = machine.succeed( f"curl -sf -X POST http://127.0.0.1:3000/api/v1/projects/{project_id}/jobsets " @@ -56,10 +58,7 @@ pkgs.testers.nixosTest { jobset_id = result.strip() assert len(jobset_id) == 36, f"Expected UUID, got '{jobset_id}'" - # ======================================================================== # GitHub Webhook Tests - # ======================================================================== - with subtest("GitHub webhook returns 404 when not configured"): code = machine.succeed( "curl -s -o /dev/null -w '%{http_code}' " @@ -196,10 +195,7 @@ pkgs.testers.nixosTest { ) assert "draft" in result.lower(), f"Expected draft PR to be skipped, got: {result}" - # ======================================================================== - # GitLab Webhook Tests - # ======================================================================== - + ## GitLab Webhook Tests # Create a GitLab project with subtest("Create GitLab test project"): result = machine.succeed( @@ -314,10 +310,7 @@ pkgs.testers.nixosTest { assert "draft" in result.lower() or "wip" in result.lower(), \ f"Expected draft MR to be skipped, got: {result}" - # ======================================================================== # Gitea/Forgejo Webhook Tests - # ======================================================================== - with subtest("Create Gitea test project"): result = machine.succeed( "curl -sf -X POST http://127.0.0.1:3000/api/v1/projects " @@ -365,10 +358,7 @@ pkgs.testers.nixosTest { assert int(count_after) > int(count_before), \ "Expected new evaluation from Gitea push" - # ======================================================================== # OAuth Routes Existence Tests - # ======================================================================== - with subtest("GitHub OAuth login route exists"): # Should redirect or return 404 if not configured code = machine.succeed( @@ -384,10 +374,7 @@ pkgs.testers.nixosTest { # Should fail gracefully (no OAuth configured) assert code.strip() in ("400", "404", "500"), f"Expected error code, got {code.strip()}" - # ======================================================================== # Cleanup - # ======================================================================== - with subtest("Cleanup test projects"): machine.succeed( f"curl -sf -X DELETE http://127.0.0.1:3000/api/v1/projects/{project_id} {auth_header}" diff --git a/nix/vm-common.nix b/nix/vm-common.nix new file mode 100644 index 0000000..ed582c1 --- /dev/null +++ b/nix/vm-common.nix @@ -0,0 +1,83 @@ +# Common VM configuration for FC integration tests +{ + self, + pkgs, + ... +}: let + fc-packages = self.packages.${pkgs.stdenv.hostPlatform.system}; +in { + # Common machine configuration for all FC integration tests + config = { + programs.git.enable = true; + security.sudo.enable = true; + + # Ensure nix and zstd are available for cache endpoints + environment.systemPackages = with pkgs; [nix nix-eval-jobs zstd curl jq openssl]; + + services.fc = { + enable = true; + package = fc-packages.fc-server; + evaluatorPackage = fc-packages.fc-evaluator; + queueRunnerPackage = fc-packages.fc-queue-runner; + migratePackage = fc-packages.fc-migrate-cli; + + server.enable = true; + evaluator.enable = true; + queueRunner.enable = true; + + settings = { + database.url = "postgresql:///fc?host=/run/postgresql"; + server = { + host = "127.0.0.1"; + port = 3000; + cors_permissive = false; + }; + + gc.enabled = false; + logs.log_dir = "/var/lib/fc/logs"; + cache.enabled = true; + signing.enabled = false; + + tracing = { + level = "info"; + format = "compact"; + show_targets = true; + show_timestamps = true; + }; + + evaluator.poll_interval = 5; + evaluator.work_dir = "/var/lib/fc/evaluator"; + queue_runner.poll_interval = 3; + queue_runner.work_dir = "/var/lib/fc/queue-runner"; + + # Declarative bootstrap: project + API key created on startup + declarative = { + projects = [ + { + name = "declarative-project"; + repository_url = "https://github.com/test/declarative"; + description = "Bootstrap test project"; + jobsets = [ + { + name = "packages"; + nix_expression = "packages"; + enabled = true; + flake_mode = true; + check_interval = 300; + } + ]; + } + ]; + + api_keys = [ + { + name = "bootstrap-admin"; + key = "fc_bootstrap_key"; + role = "admin"; + } + ]; + }; + }; + }; + }; +}