diff --git a/flake.nix b/flake.nix index 5e085d1..d875aaa 100644 --- a/flake.nix +++ b/flake.nix @@ -77,29 +77,39 @@ checks = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; - in { - vm-test = pkgs.callPackage ./nix/vm-test.nix { + 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; }); devShells = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; - craneLib = crane.mkLib pkgs; in { - default = craneLib.devShell { + default = pkgs.mkShell { name = "fc"; inputsFrom = [self.packages.${system}.fc-server]; - strictDeps = true; packages = with pkgs; [ - rust-analyzer postgresql pkg-config openssl + + taplo + (rustfmt.override {asNightly = true;}) ]; }; }); diff --git a/nix/tests/api-crud.nix b/nix/tests/api-crud.nix new file mode 100644 index 0000000..f6a197e --- /dev/null +++ b/nix/tests/api-crud.nix @@ -0,0 +1,768 @@ +# API CRUD tests: dashboard content, project/jobset/evaluation/build/channel/builder CRUD, admin endpoints, pagination, search +{ + pkgs, + fc-packages, + nixosModule, +}: +pkgs.testers.nixosTest { + name = "fc-api-crud"; + + nodes.machine = import ./common.nix {inherit pkgs fc-packages nixosModule;}; + + testScript = '' + import hashlib + import json + + machine.start() + machine.wait_for_unit("postgresql.service") + + # Ensure PostgreSQL is actually ready to accept connections before fc-server starts + machine.wait_until_succeeds("sudo -u fc psql -U fc -d fc -c 'SELECT 1'", timeout=30) + + machine.wait_for_unit("fc-server.service") + + # 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 ---- + # Token: fc_testkey123 -> SHA-256 hash inserted into api_keys table + api_token = "fc_testkey123" + api_hash = hashlib.sha256(api_token.encode()).hexdigest() + machine.succeed( + f"sudo -u fc psql -U fc -d fc -c \"INSERT INTO api_keys (name, key_hash, role) VALUES ('test', '{api_hash}', 'admin')\"" + ) + auth_header = f"-H 'Authorization: Bearer {api_token}'" + + # Seed a read-only key + ro_token = "fc_readonly_key" + ro_hash = hashlib.sha256(ro_token.encode()).hexdigest() + machine.succeed( + f"sudo -u fc psql -U fc -d fc -c \"INSERT INTO api_keys (name, key_hash, role) VALUES ('readonly', '{ro_hash}', 'read-only')\"" + ) + ro_header = f"-H 'Authorization: Bearer {ro_token}'" + + # Create initial project for tests + result = machine.succeed( + "curl -sf -X POST http://127.0.0.1:3000/api/v1/projects " + f"{auth_header} " + "-H 'Content-Type: application/json' " + "-d '{\"name\": \"test-project\", \"repository_url\": \"https://github.com/test/repo\"}' " + "| jq -r .id" + ) + project_id = result.strip() + + # ======================================================================== + # Phase 4: Dashboard Content & Deep Functional Tests + # ======================================================================== + + # ---- 4A: 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" + + with subtest("Home page contains stats grid"): + body = machine.succeed("curl -sf http://127.0.0.1:3000/") + assert "stat-card" in body, "Home page missing stats grid" + assert "Completed" in body, "Home page missing 'Completed' stat" + + with subtest("Home page shows project overview table"): + body = machine.succeed("curl -sf http://127.0.0.1:3000/") + # We created projects earlier, they should appear + assert "test-project" in body, "Home page should list test-project in overview" + + with subtest("Projects page contains created projects"): + body = machine.succeed("curl -sf http://127.0.0.1:3000/projects") + assert "test-project" in body, "Projects page should list test-project" + + with subtest("Projects page returns HTML content type"): + ct = machine.succeed( + "curl -s -D - -o /dev/null http://127.0.0.1:3000/projects | grep -i content-type" + ) + assert "text/html" in ct.lower(), f"Expected text/html, got: {ct}" + + with subtest("Admin page shows system status"): + body = machine.succeed("curl -sf http://127.0.0.1:3000/admin") + assert "Administration" in body, "Admin page missing heading" + assert "System Status" in body, "Admin page missing system status section" + assert "Remote Builders" in body, "Admin page missing remote builders section" + + with subtest("Queue page renders"): + body = machine.succeed("curl -sf http://127.0.0.1:3000/queue") + assert "Queue" in body or "Pending" in body or "Running" in body, \ + "Queue page missing expected content" + + with subtest("Channels page renders"): + body = machine.succeed("curl -sf http://127.0.0.1:3000/channels") + # Page should render even if empty + assert "Channel" in body or "channel" in body, "Channels page missing expected content" + + with subtest("Builds page renders with filter params"): + body = machine.succeed( + "curl -sf 'http://127.0.0.1:3000/builds?status=pending&system=x86_64-linux'" + ) + assert "Build" in body or "build" in body, "Builds page missing expected content" + + with subtest("Evaluations page renders"): + body = machine.succeed("curl -sf http://127.0.0.1:3000/evaluations") + assert "Evaluation" in body or "evaluation" in body, "Evaluations page missing expected content" + + with subtest("Login page contains form"): + body = machine.succeed("curl -sf http://127.0.0.1:3000/login") + assert "api_key" in body or "API" in body, "Login page missing API key input" + assert "