package repo import ( "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" "browser.local/platform/domain" ) func TestMemoryRepositoryRejectsDuplicateAndCopiesValues(t *testing.T) { store := NewMemoryStore() user := domain.User{ ID: "user-1", DisplayName: "Mary", Status: domain.UserStatusActive, Roles: []string{"admin"}, } if err := store.Users().Create(user); err != nil { t.Fatalf("create user: %v", err) } if err := store.Users().Create(user); !errors.Is(err, ErrDuplicate) { t.Fatalf("expected duplicate error, got %v", err) } got, err := store.Users().Get(user.ID) if err != nil { t.Fatalf("get user: %v", err) } got.Roles[0] = "mutated" again, err := store.Users().Get(user.ID) if err != nil { t.Fatalf("get user again: %v", err) } if again.Roles[0] != "admin" { t.Fatalf("expected stored roles to be isolated, got %+v", again.Roles) } } func TestMemoryRepositoryListFiltersAndSorts(t *testing.T) { store := NewMemoryStore() users := []domain.User{ {ID: "user-c", DisplayName: "C", Status: domain.UserStatusDisabled}, {ID: "user-b", DisplayName: "B", Status: domain.UserStatusActive}, {ID: "user-a", DisplayName: "A", Status: domain.UserStatusActive}, } for _, user := range users { if err := store.Users().Create(user); err != nil { t.Fatalf("create user %s: %v", user.ID, err) } } active, err := store.Users().List(domain.UserFilter{Status: domain.UserStatusActive}) if err != nil { t.Fatalf("list users: %v", err) } if len(active) != 2 || active[0].ID != "user-a" || active[1].ID != "user-b" { t.Fatalf("expected sorted active users, got %+v", active) } } func TestMemoryJobRepositoryFindsIdempotencyKey(t *testing.T) { store := NewMemoryStore() job := domain.Job{ ID: "job-1", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-1", State: domain.JobStateQueued, } if err := store.Jobs().Create(job); err != nil { t.Fatalf("create job: %v", err) } got, err := store.Jobs().GetByIdempotency("run-local", "idem-1") if err != nil { t.Fatalf("get by idempotency: %v", err) } if got.ID != job.ID { t.Fatalf("expected job %q, got %q", job.ID, got.ID) } _, err = store.Jobs().GetByIdempotency("run-local", "missing") if !errors.Is(err, ErrNotFound) { t.Fatalf("expected not found for missing idempotency key, got %v", err) } } func TestFileStorePersistsAndReloadsResources(t *testing.T) { path := filepath.Join(t.TempDir(), "metadata.json") store, err := NewFileStore(path) if err != nil { t.Fatalf("create file store: %v", err) } user := domain.User{ ID: "user-1", DisplayName: "Durable User", Status: domain.UserStatusActive, Roles: []string{"platform-admin"}, } if err := store.Users().Create(user); err != nil { t.Fatalf("create user: %v", err) } stamp := time.Date(2026, 7, 17, 12, 0, 0, 0, time.UTC) job := domain.Job{ ID: "job-1", RunEndpointID: "run-local", ServerInstanceID: "server-1", Capability: "process.start", IdempotencyKey: "idem-1", State: domain.JobStateQueued, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 4, InitialBackoffSeconds: 3, MaxBackoffSeconds: 30}, Attempt: 2, QueueEligibleAt: stamp, NextAttemptAt: stamp.Add(3 * time.Second), LeaseTokenHash: strings.Repeat("d", 64), LeaseSessionGen: 2, AckDeadlineAt: stamp.Add(15 * time.Second), LeaseExpiresAt: stamp.Add(time.Minute), LastProgressSeq: 7, CancelReason: "operator requested", CancelRequestedAt: stamp, LastReconciledAt: stamp, ReconcileCount: 2, ReconcileOutcome: "confirmed active attempt", ExecutionInput: domain.JobExecutionInput{WorkspaceScope: "local", Content: "name=approved\n", ExpectedVersion: 1, ExpectedChecksum: "sha256:" + strings.Repeat("1", 64), MaxReadBytes: 64 * 1024}, ExecutionResult: domain.JobExecutionResult{Kind: "file.read", Version: 2, Checksum: "sha256:" + strings.Repeat("2", 64), SizeBytes: 15, AuditSummary: "bounded read", Content: "private-read"}, } if err := store.Jobs().Create(job); err != nil { t.Fatalf("create job: %v", err) } plugin := domain.GamePlugin{ID: "game.runtime", Name: "Runtime", Version: "1.0.0", RuntimeProfiles: domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "local", Mode: "local-process", Capabilities: []string{"process.start"}}}}} if err := store.GamePlugins().Create(plugin); err != nil { t.Fatalf("create plugin: %v", err) } binding := domain.RuntimeBinding{ID: "runtime-binding-server-1", ServerInstanceID: "server-1", PluginID: plugin.ID, PluginVersion: plugin.Version, ProfileKey: "local", Mode: "local-process", Bindings: map[string]string{"rcon.password": "secret://server-1/rcon"}, Status: domain.RuntimeBindingStatusComplete, CreatedAt: stamp, UpdatedAt: stamp} if err := store.RuntimeBindings().Create(binding); err != nil { t.Fatalf("create runtime binding: %v", err) } runEndpoint := domain.RunEndpoint{ID: "run-target", DisplayName: "Target Run", Version: "release-1", Platform: "linux", Architecture: "amd64", Status: domain.RunEndpointStatusOnline, Capabilities: []string{domain.JobCapabilityDependenciesInstall, domain.JobCapabilityRunSelfUpdate}, Capacity: domain.RunCapacity{MaxJobs: 2}, LastHeartbeatAt: stamp} if err := store.RunEndpoints().Create(runEndpoint); err != nil { t.Fatalf("create target Run endpoint: %v", err) } planDigest := "sha256:" + strings.Repeat("e", 64) dependencyStatus := domain.DependencyStatus{ID: "dependency-server-1-java", ServerInstanceID: "server-1", PluginID: plugin.ID, ProbeKey: "java", TargetOS: "linux", TargetArch: "amd64", State: domain.DependencyStatePresent, Required: true, InstallPlanKey: "java-install", PlanDigest: planDigest, JobID: "job-dependency", Evidence: "OpenJDK 21", CompletedSteps: 1, Message: "dependency execution completed", CheckedAt: stamp, UpdatedAt: stamp} if err := store.DependencyStatuses().Create(dependencyStatus); err != nil { t.Fatalf("create dependency status: %v", err) } runUpdate := domain.RunUpdateJob{ID: "run-update-1", ServerInstanceID: "server-1", RunEndpointID: runEndpoint.ID, ArtifactID: "artifact-run-2", Checksum: planDigest, TargetOS: "linux", TargetArch: "amd64", TargetRelease: "release-2", PreviousVersion: "release-1", JobID: "job-update", IdempotencyKey: "run-update-idempotent", Status: domain.DistributionJobStatusFailed, Phase: domain.RunUpdatePhaseRolledBack, Message: "previous executable restored", Rollback: true, CreatedAt: stamp, UpdatedAt: stamp} if err := store.RunUpdateJobs().Create(runUpdate); err != nil { t.Fatalf("create Run update status: %v", err) } authSession := domain.AuthSessionRecord{ID: "auth-session-1", UserID: user.ID, TokenHash: strings.Repeat("a", 64), Status: domain.AuthSessionStatusActive, Generation: 1, IssuedAt: stamp, ExpiresAt: stamp.Add(time.Hour), LastSeenAt: stamp} if err := store.AuthSessions().Create(authSession); err != nil { t.Fatalf("create auth session: %v", err) } runSession := domain.RunControlSession{RunEndpointID: "run-local", SessionToken: "raw-run-token", SessionTokenHash: strings.Repeat("b", 64), Status: domain.AuthSessionStatusActive, Generation: 2, CapabilityFingerprint: "cap-v2", HeartbeatIntervalSeconds: 15, CreatedAt: stamp, UpdatedAt: stamp, ExpiresAt: stamp.Add(time.Hour), RequireSignedRequests: true, UsedNonces: []string{"nonce-1"}} if err := store.RunControlSessions().Create(runSession); err != nil { t.Fatalf("create run session: %v", err) } componentKey := domain.EncryptedComponentKey{ID: "component-key-1", ServerInstanceID: "server-1", ComponentKind: domain.DistributionComponentRun, EncryptedKey: "ciphertext-only", KeyHash: strings.Repeat("c", 64), Fingerprint: "fingerprint", SecretRef: "secret://components/server-1/run", Generation: 1, Status: domain.ComponentKeyStatusActive, CreatedAt: stamp, UpdatedAt: stamp} if err := store.EncryptedComponentKeys().Create(componentKey); err != nil { t.Fatalf("create component key: %v", err) } payload, err := os.ReadFile(path) if err != nil { t.Fatalf("read snapshot: %v", err) } if strings.Contains(string(payload), "raw-run-token") { t.Fatalf("snapshot exposed raw Run token: %s", payload) } if strings.Contains(string(payload), "raw-job-lease") { t.Fatalf("snapshot exposed raw job lease: %s", payload) } reloaded, err := NewFileStore(path) if err != nil { t.Fatalf("reload file store: %v", err) } got, err := reloaded.Users().Get("user-1") if err != nil { t.Fatalf("get reloaded user: %v", err) } if got.DisplayName != user.DisplayName || got.Roles[0] != "platform-admin" { t.Fatalf("unexpected reloaded user: %+v", got) } gotJob, err := reloaded.Jobs().GetByIdempotency("run-local", "idem-1") if err != nil { t.Fatalf("get reloaded job by idempotency: %v", err) } if gotJob.ID != "job-1" || gotJob.ServerInstanceID != "server-1" || gotJob.Attempt != 2 || gotJob.RetryPolicy.MaxAttempts != 4 || gotJob.LeaseTokenHash != strings.Repeat("d", 64) || gotJob.ReconcileCount != 2 || gotJob.ExecutionInput.Content != "name=approved\n" || gotJob.ExecutionResult.Content != "private-read" { t.Fatalf("unexpected reloaded job: %+v", gotJob) } gotPlugin, err := reloaded.GamePlugins().Get(plugin.ID) if err != nil || len(gotPlugin.RuntimeProfiles.LifecycleProfiles) != 1 || gotPlugin.RuntimeProfiles.LifecycleProfiles[0].Key != "local" { t.Fatalf("unexpected reloaded runtime profiles: plugin=%+v err=%v", gotPlugin, err) } gotBindings, err := reloaded.RuntimeBindings().List(domain.RuntimeBindingFilter{ServerInstanceID: "server-1"}) if err != nil || len(gotBindings) != 1 || gotBindings[0].Bindings["rcon.password"] != "secret://server-1/rcon" { t.Fatalf("unexpected reloaded runtime binding: bindings=%+v err=%v", gotBindings, err) } gotEndpoint, err := reloaded.RunEndpoints().Get(runEndpoint.ID) if err != nil || gotEndpoint.Platform != "linux" || gotEndpoint.Architecture != "amd64" || gotEndpoint.Version != "release-1" { t.Fatalf("unexpected reloaded Run target: endpoint=%+v err=%v", gotEndpoint, err) } gotDependencies, err := reloaded.DependencyStatuses().List(domain.DependencyStatusFilter{ServerInstanceID: "server-1"}) if err != nil || len(gotDependencies) != 1 || gotDependencies[0].PlanDigest != planDigest || gotDependencies[0].Evidence != "OpenJDK 21" { t.Fatalf("unexpected reloaded dependency status: statuses=%+v err=%v", gotDependencies, err) } gotUpdates, err := reloaded.RunUpdateJobs().List(domain.RunUpdateJobFilter{ServerInstanceID: "server-1"}) if err != nil || len(gotUpdates) != 1 || gotUpdates[0].Phase != domain.RunUpdatePhaseRolledBack || !gotUpdates[0].Rollback || gotUpdates[0].TargetRelease != "release-2" { t.Fatalf("unexpected reloaded Run update: updates=%+v err=%v", gotUpdates, err) } gotAuth, err := reloaded.AuthSessions().Get(authSession.ID) if err != nil || gotAuth.TokenHash != authSession.TokenHash || gotAuth.Generation != 1 { t.Fatalf("unexpected reloaded auth session: session=%+v err=%v", gotAuth, err) } gotRun, err := reloaded.RunControlSessions().Get(runSession.RunEndpointID) if err != nil || gotRun.SessionToken != "" || gotRun.SessionTokenHash != runSession.SessionTokenHash || len(gotRun.UsedNonces) != 1 { t.Fatalf("unexpected reloaded Run session: session=%+v err=%v", gotRun, err) } gotKey, err := reloaded.EncryptedComponentKeys().Get(componentKey.ID) if err != nil || gotKey.EncryptedKey != componentKey.EncryptedKey || gotKey.SecretRef != componentKey.SecretRef { t.Fatalf("unexpected reloaded component key metadata: key=%+v err=%v", gotKey, err) } } func TestMySQLSnapshotRoundTripsDurableJobSchedulingMetadata(t *testing.T) { stamp := time.Date(2026, 7, 18, 12, 0, 0, 0, time.UTC) source := &MySQLStore{MemoryStore: NewMemoryStore()} job := domain.Job{ ID: "job-mysql", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-mysql", State: domain.JobStateRunning, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 3, InitialBackoffSeconds: 2, MaxBackoffSeconds: 60}, Attempt: 2, QueueEligibleAt: stamp, LeaseTokenHash: strings.Repeat("e", 64), LeaseSessionGen: 4, LeaseExpiresAt: stamp.Add(time.Minute), LastProgressSeq: 8, CancelReason: "stop", CancelRequestedAt: stamp, LastReconciledAt: stamp, ReconcileCount: 3, ReconcileOutcome: "confirmed active attempt", CreatedAt: stamp, UpdatedAt: stamp, ExecutionInput: domain.JobExecutionInput{WorkspaceScope: "local", Content: "mysql-approved", ExpectedVersion: 1, ExpectedChecksum: "sha256:" + strings.Repeat("3", 64), MaxReadBytes: 64 * 1024}, ExecutionResult: domain.JobExecutionResult{Kind: "file.write", Version: 2, Checksum: "sha256:" + strings.Repeat("4", 64), SizeBytes: 14, AuditSummary: "atomic write"}, } if err := source.MemoryStore.Jobs().Create(job); err != nil { t.Fatalf("create source job: %v", err) } payload, err := json.Marshal(source.snapshot()) if err != nil { t.Fatalf("marshal mysql snapshot: %v", err) } var snapshot StoreSnapshot if err := json.Unmarshal(payload, &snapshot); err != nil { t.Fatalf("unmarshal mysql snapshot: %v", err) } target := &MySQLStore{MemoryStore: NewMemoryStore()} target.loadSnapshot(snapshot) got, err := target.MemoryStore.Jobs().Get(job.ID) if err != nil || got.Attempt != job.Attempt || got.LeaseTokenHash != job.LeaseTokenHash || got.LastProgressSeq != job.LastProgressSeq || got.ReconcileCount != job.ReconcileCount || got.ExecutionInput.Content != job.ExecutionInput.Content || got.ExecutionResult.Checksum != job.ExecutionResult.Checksum { t.Fatalf("unexpected MySQL snapshot job: job=%+v err=%v", got, err) } } func TestMySQLStoreRequiresDSN(t *testing.T) { _, err := NewMySQLStore("") if err == nil || !strings.Contains(err.Error(), "PLATFORM_MYSQL_DSN") { t.Fatalf("expected missing MySQL DSN error, got %v", err) } }