//go:build integration package api import ( "context" "database/sql" "os" "path/filepath" "testing" "time" "github.com/cosmic-clash/cosmic-clash/server/domain" "github.com/cosmic-clash/cosmic-clash/server/migrations" "github.com/cosmic-clash/cosmic-clash/server/store" "github.com/cosmic-clash/cosmic-clash/server/workload" _ "github.com/jackc/pgx/v5/stdlib" ) // This binary is deliberately opt-in, matching store's integration suite: it // requires a disposable PostgreSQL instance supplied by // scripts/run_postgres_integration.sh. func openIntegrationPostgres(t *testing.T) *sql.DB { t.Helper() dsn := os.Getenv("COSMIC_CLASH_POSTGRES_DSN") if dsn == "" { t.Skip("COSMIC_CLASH_POSTGRES_DSN is not set") } db, err := sql.Open("pgx", dsn) if err != nil { t.Fatalf("open PostgreSQL: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { db.Close() t.Fatalf("ping PostgreSQL: %v", err) } t.Cleanup(func() { db.Close() }) migrationDir := os.Getenv("COSMIC_CLASH_MIGRATIONS_DIR") if migrationDir == "" { migrationDir = filepath.Join("..", "migrations") } if err := migrations.Apply(ctx, db, migrationDir); err != nil { t.Fatalf("apply migrations: %v", err) } return db } // seedRealAllocation claims a real ready server and allocation row, exactly // the durable state a signed workload token's allocation_id must resolve // against (see store.AllocationBindingByAllocationID). func seedRealAllocation(t *testing.T, db *sql.DB, allocationID, matchID string, now time.Time) domain.Allocation { t.Helper() ctx := context.Background() serverID := "server-" + allocationID if err := store.RegisterReadyServer(ctx, db, domain.ReadyServer{ServerID: serverID, Region: "EU", Build: "build-1", Protocol: 1, Transport: "enet", State: domain.ServerReady}, now); err != nil { t.Fatalf("register ready server: %v", err) } allocation, err := store.ClaimAllocation(ctx, db, domain.AllocationRequest{AllocationID: allocationID, MatchID: matchID, Region: "EU", Build: "build-1", Protocol: 1, Transport: "enet"}, now) if err != nil { t.Fatalf("claim allocation: %v", err) } return allocation } // TestWorkloadVerifierFromSignedTokenAcceptsARealAllocation proves the full // wired path: a token issued by workload.IssueSignedWorkloadToken naming only // a real allocation_id verifies successfully through // WorkloadVerifierFromSignedToken and returns a binding whose match_id/ // server_id came from the durable allocation record (the token itself never // carries them -- see signed_token.go), matching what serverMutation // actually checks (ServerID, MatchID). This is the "wired, working" // counterpart to cmd/control-plane's // TestServerRoutesRequireWorkloadVerifyToBeWired, which pins the // unconfigured-503 case. func TestWorkloadVerifierFromSignedTokenAcceptsARealAllocation(t *testing.T) { db := openIntegrationPostgres(t) now := time.Now().UTC() allocation := seedRealAllocation(t, db, "alloc-verify-1", "match-verify-1", now) secret := []byte("integration-test-secret") token, err := workload.IssueSignedWorkloadToken(secret, allocation.AllocationID, now, time.Minute) if err != nil { t.Fatalf("issue token: %v", err) } verify := WorkloadVerifierFromSignedToken(secret, db) if verify == nil { t.Fatal("WorkloadVerifierFromSignedToken returned nil with a real secret and database") } binding, err := verify(token, now.Add(30*time.Second)) if err != nil { t.Fatalf("verify: %v", err) } if binding.ServerID != allocation.ServerID || binding.MatchID != allocation.MatchID || binding.AllocationID != allocation.AllocationID { t.Fatalf("unexpected binding: %+v, want server=%s match=%s allocation=%s", binding, allocation.ServerID, allocation.MatchID, allocation.AllocationID) } } // TestWorkloadVerifierFromSignedTokenRejectsAnUnknownAllocation proves the // durable lookup actually runs: a validly-signed, unexpired token whose // allocation was never recorded (e.g. simply fabricated) must still be // rejected. Signature and expiry checks alone are not enough. func TestWorkloadVerifierFromSignedTokenRejectsAnUnknownAllocation(t *testing.T) { db := openIntegrationPostgres(t) now := time.Now().UTC() secret := []byte("integration-test-secret") token, err := workload.IssueSignedWorkloadToken(secret, "alloc-never-recorded", now, time.Minute) if err != nil { t.Fatalf("issue token: %v", err) } verify := WorkloadVerifierFromSignedToken(secret, db) if _, err := verify(token, now.Add(time.Second)); err == nil { t.Fatal("expected rejection for a token naming an allocation that was never recorded") } } // TestWorkloadVerifierFromSignedTokenNeverTrustsCallerSuppliedBinding proves // the binding returned is entirely derived from the durable allocation row, // never from anything embedded in or inferable from the token: two distinct // allocations produce tokens that resolve to their own, and only their own, // match/server pairing. func TestWorkloadVerifierFromSignedTokenNeverTrustsCallerSuppliedBinding(t *testing.T) { db := openIntegrationPostgres(t) now := time.Now().UTC() first := seedRealAllocation(t, db, "alloc-verify-2a", "match-verify-2a", now) second := seedRealAllocation(t, db, "alloc-verify-2b", "match-verify-2b", now) secret := []byte("integration-test-secret") verify := WorkloadVerifierFromSignedToken(secret, db) firstToken, err := workload.IssueSignedWorkloadToken(secret, first.AllocationID, now, time.Minute) if err != nil { t.Fatalf("issue first token: %v", err) } firstBinding, err := verify(firstToken, now.Add(time.Second)) if err != nil { t.Fatalf("verify first: %v", err) } if firstBinding.MatchID != first.MatchID || firstBinding.ServerID != first.ServerID { t.Fatalf("first binding %+v resolved to the wrong allocation", firstBinding) } secondToken, err := workload.IssueSignedWorkloadToken(secret, second.AllocationID, now, time.Minute) if err != nil { t.Fatalf("issue second token: %v", err) } secondBinding, err := verify(secondToken, now.Add(time.Second)) if err != nil { t.Fatalf("verify second: %v", err) } if secondBinding.MatchID != second.MatchID || secondBinding.ServerID != second.ServerID { t.Fatalf("second binding %+v resolved to the wrong allocation", secondBinding) } if secondBinding.MatchID == firstBinding.MatchID || secondBinding.ServerID == firstBinding.ServerID { t.Fatalf("distinct allocations resolved to the same binding: %+v vs %+v", firstBinding, secondBinding) } } // TestWorkloadVerifierFromSignedTokenClosesTheDefaultUnwiredGap proves the // 503-by-default gap pinned by // cmd/control-plane.TestServerRoutesRequireWorkloadVerifyToBeWired is // actually closed once a secret and database are wired: a Service built the // same way newAPIHandler builds one now accepts a validly-issued token for a // real allocation, through the exact Service.WorkloadVerify field the HTTP // handler calls. (serverMutation's deeper match-state transition -- // requiring the match to already be ALLOCATING -- is exercised separately by // the store package's own allocation/match integration tests; this test's // job is only the WorkloadVerify boundary itself.) func TestWorkloadVerifierFromSignedTokenClosesTheDefaultUnwiredGap(t *testing.T) { db := openIntegrationPostgres(t) now := time.Now().UTC() allocation := seedRealAllocation(t, db, "alloc-verify-3", "match-verify-3", now) secret := []byte("integration-test-secret") token, err := workload.IssueSignedWorkloadToken(secret, allocation.AllocationID, now, time.Minute) if err != nil { t.Fatalf("issue token: %v", err) } svc := &Service{ ServerRegistrar: ServerRegistrarFromStore(db), WorkloadVerify: WorkloadVerifierFromSignedToken(secret, db), Now: func() time.Time { return now.Add(time.Second) }, } binding, err := svc.WorkloadVerify(token, now.Add(time.Second)) if err != nil { t.Fatalf("WorkloadVerify rejected a validly-issued token for a real allocation: %v", err) } if binding.ServerID != allocation.ServerID { t.Fatalf("binding.ServerID = %q, want %q", binding.ServerID, allocation.ServerID) } }