package service import ( "encoding/json" "errors" "strings" "testing" "browser.local/platform/domain" ) func TestDependencyCatalogRequiresCurrentReviewedDigest(t *testing.T) { svc, session, instance := newDistributionTestFixture(t) otherSession := createServiceUserAndLogin(t, svc, domain.User{ID: "dependency-other-owner", DisplayName: "Other Owner", Email: "dependency-other@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"}) if _, err := svc.GetDependencyCatalogForSession(otherSession, instance.ID); !errors.Is(err, ErrForbidden) { t.Fatalf("expected cross-owner dependency catalog denial, got %v", err) } catalog, err := svc.GetDependencyCatalogForSession(session, instance.ID) if err != nil { t.Fatalf("get dependency catalog: %v", err) } if catalog.TargetOS != "linux" || catalog.TargetArch != "amd64" || len(catalog.Probes) != 1 || len(catalog.Plans) != 1 || !strings.HasPrefix(catalog.Plans[0].Digest, "sha256:") { t.Fatalf("unexpected dependency catalog: %+v", catalog) } request := domain.DependencyJobRequest{ServerInstanceID: instance.ID, ProbeKey: catalog.Probes[0].Key, Install: true, InstallPlanKey: catalog.Plans[0].Key, PlanDigest: "sha256:" + strings.Repeat("f", 64), IdempotencyKey: "dependency-stale-digest"} if _, err := svc.QueueDependencyJobForSession(session, request); err == nil || !strings.Contains(err.Error(), "planDigest") { t.Fatalf("expected stale digest rejection, got %v", err) } jobs, err := svc.store.Jobs().List(domain.JobFilter{ServerInstanceID: instance.ID}) if err != nil { t.Fatalf("list jobs: %v", err) } for _, job := range jobs { if job.IdempotencyKey == request.IdempotencyKey { t.Fatalf("stale digest created a job: %+v", job) } } request.PlanDigest = catalog.Plans[0].Digest request.IdempotencyKey = "dependency-current-digest" job, err := svc.QueueDependencyJobForSession(session, request) if err != nil { t.Fatalf("queue reviewed dependency plan: %v", err) } if job.Capability != domain.JobCapabilityDependenciesInstall || job.TargetKey != "dependencies/install/"+catalog.Plans[0].Key { t.Fatalf("unexpected dependency install job: %+v", job) } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { t.Fatalf("get plugin: %v", err) } plugin.RuntimeProfiles.InstallPlans[0].Steps[0].PackageName = "openjdk-22-jre" if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("mutate plugin declaration: %v", err) } runSession := registerDependencyUpdateRun(t, svc, instance) claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, Capabilities: []string{domain.JobCapabilityDependenciesInstall}, Capacity: domain.RunCapacity{MaxJobs: 1}}) if err != nil || !claim.HasJob { t.Fatalf("claim dependency install: claim=%+v err=%v", claim, err) } _, err = svc.GetDependencyExecutionInput(domain.DependencyExecutionInputRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt}) if err == nil || !strings.Contains(err.Error(), "changed after dispatch") { t.Fatalf("expected changed declaration rejection, got %v", err) } } func TestDependencyCatalogNamesMissingPluginPermission(t *testing.T) { svc, session, instance := newDistributionTestFixture(t) plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { t.Fatalf("get plugin: %v", err) } permissions := plugin.DeclaredPermissions[:0] for _, permission := range plugin.DeclaredPermissions { if permission != "server.dependencies.manage" { permissions = append(permissions, permission) } } plugin.DeclaredPermissions = permissions if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update plugin: %v", err) } _, err = svc.GetDependencyCatalogForSession(session, instance.ID) if !errors.Is(err, ErrForbidden) || !strings.Contains(err.Error(), "server.dependencies.manage") { t.Fatalf("expected named dependency permission denial, got %v", err) } } func TestDependencyInputFencingCancellationAndTerminalProjection(t *testing.T) { svc, session, instance := newDistributionTestFixture(t) catalog, err := svc.GetDependencyCatalogForSession(session, instance.ID) if err != nil { t.Fatalf("catalog: %v", err) } job, err := svc.QueueDependencyJobForSession(session, domain.DependencyJobRequest{ServerInstanceID: instance.ID, ProbeKey: catalog.Probes[0].Key, IdempotencyKey: "dependency-check-fencing"}) if err != nil { t.Fatalf("queue dependency check: %v", err) } runSession := registerDependencyUpdateRun(t, svc, instance) claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, Capabilities: []string{domain.JobCapabilityDependenciesCheck}, Capacity: domain.RunCapacity{MaxJobs: 1}}) if err != nil || !claim.HasJob || claim.Job.JobID != job.ID { t.Fatalf("claim dependency check: claim=%+v err=%v", claim, err) } base := domain.DependencyExecutionInputRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt} input, err := svc.GetDependencyExecutionInput(base) if err != nil || input.PlanDigest == "" || input.Bindings["java"] == "" { t.Fatalf("get fenced dependency input: input=%+v err=%v", input, err) } wrongSession := base wrongSession.SessionToken = "stale-session" if _, err := svc.GetDependencyExecutionInput(wrongSession); err == nil { t.Fatal("expected wrong session rejection") } wrongAttempt := base wrongAttempt.Attempt++ if _, err := svc.GetDependencyExecutionInput(wrongAttempt); err == nil { t.Fatal("expected wrong attempt rejection") } wrongLease := base wrongLease.LeaseToken = "stale-lease" if _, err := svc.GetDependencyExecutionInput(wrongLease); err == nil { t.Fatal("expected wrong lease rejection") } evidence, _ := json.Marshal(domain.DependencyExecutionEvidence{ProbeKey: input.Probe.Key, PlanDigest: input.PlanDigest, State: string(domain.DependencyStatePresent), Evidence: "OpenJDK 21"}) if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "dependency probe completed"}, ResultRef: "artifact://jobs/dependency-check/result", Message: "dependency probe completed", ExecutionResult: domain.JobExecutionResult{Kind: "dependency.check", Checksum: input.PlanDigest, Summary: "dependency probe completed", Content: string(evidence)}}); err != nil { t.Fatalf("complete dependency result: %v", err) } projected, err := svc.GetDependencyCatalogForSession(session, instance.ID) if err != nil || projected.Probes[0].State != domain.DependencyStatePresent || projected.Probes[0].Evidence != "OpenJDK 21" { t.Fatalf("unexpected dependency projection: catalog=%+v err=%v", projected, err) } cancelJob, err := svc.QueueDependencyJobForSession(session, domain.DependencyJobRequest{ServerInstanceID: instance.ID, ProbeKey: catalog.Probes[0].Key, IdempotencyKey: "dependency-check-cancel"}) if err != nil { t.Fatalf("queue cancellable dependency check: %v", err) } claim, err = svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, Capabilities: []string{domain.JobCapabilityDependenciesCheck}, Capacity: domain.RunCapacity{MaxJobs: 1}}) if err != nil || claim.Job.JobID != cancelJob.ID { t.Fatalf("claim cancellable dependency check: claim=%+v err=%v", claim, err) } if _, err := svc.RequestRunJobCancelForSession(session, domain.RunJobCancelRequest{JobID: cancelJob.ID, Reason: "operator cancelled"}); err != nil { t.Fatalf("request cancel: %v", err) } if _, err := svc.GetDependencyExecutionInput(domain.DependencyExecutionInputRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt}); err == nil || !strings.Contains(err.Error(), "cancelled") { t.Fatalf("expected cancelled input rejection, got %v", err) } } func TestPluginBridgeDependencyInstallUsesReviewedPlanDigest(t *testing.T) { svc, session, instance := newDistributionTestFixture(t) plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { t.Fatalf("get plugin: %v", err) } plugin.Pages = append(plugin.Pages, domain.GamePluginPage{Key: "runtime", Title: "Runtime", Path: "/runtime", Permissions: []string{"server.dependencies.manage"}, BridgeActions: []string{string(domain.PluginBridgeActionDependenciesRequest)}}) if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("add dependency bridge page: %v", err) } catalog, err := svc.GetDependencyCatalogForSession(session, instance.ID) if err != nil { t.Fatalf("get dependency catalog: %v", err) } request := domain.PluginBridgeExecuteRequest{RequestID: "bridge-dependency-install", PluginID: plugin.ID, RouteKey: "runtime", ServerInstanceID: instance.ID, Action: domain.PluginBridgeActionDependenciesRequest, Payload: map[string]string{"operation": "install", "probeKey": catalog.Probes[0].Key, "planKey": catalog.Plans[0].Key, "idempotencyKey": "bridge-dependency-install"}} denied, err := svc.ExecutePluginBridgeAction(session, request) if err != nil { t.Fatalf("execute bridge without digest: %v", err) } if denied.Status == "queued" || denied.Error == nil { t.Fatalf("bridge install without reviewed digest must be denied: %+v", denied) } request.Payload["planDigest"] = catalog.Plans[0].Digest request.Payload["idempotencyKey"] = "bridge-dependency-install-approved" approved, err := svc.ExecutePluginBridgeAction(session, request) if err != nil { t.Fatalf("execute reviewed bridge install: %v", err) } if approved.Status != "queued" || approved.Result["capability"] != domain.JobCapabilityDependenciesInstall { t.Fatalf("expected reviewed bridge dependency job, got %+v", approved) } } func TestRunUpdateTargetFencingChunksHealthAndRollbackProjection(t *testing.T) { svc, session, instance := newDistributionTestFixture(t) payload := []byte("compiled target-matched run archive") svc.ConfigureDistributionBuilder(staticDistributionBuilder{payload: payload}) distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "linux", TargetArch: "amd64", IdempotencyKey: "run-update-build"}) if err != nil { t.Fatalf("generate update distribution: %v", err) } distribution = completeDistributionBuild(t, svc, distribution, payload) otherInstance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{ID: "server-update-other", PluginID: instance.PluginID, RunEndpointID: instance.RunEndpointID, Name: "Other Update Server", State: domain.ServerInstanceStateReady}) if err != nil { t.Fatalf("create other update server: %v", err) } createCompleteRuntimeBinding(t, svc, otherInstance, "local") if _, err := svc.PushRunUpdateForSession(session, domain.RunUpdateRequest{ServerInstanceID: otherInstance.ID, ArtifactID: distribution.ArtifactID, Checksum: distribution.Checksum, IdempotencyKey: "run-update-cross-server"}); err == nil { t.Fatal("expected cross-server update artifact rejection") } endpoint, _ := svc.store.RunEndpoints().Get(instance.RunEndpointID) endpoint.Architecture = "arm64" if err := svc.store.RunEndpoints().Update(endpoint); err != nil { t.Fatalf("change endpoint target: %v", err) } request := domain.RunUpdateRequest{ServerInstanceID: instance.ID, ArtifactID: distribution.ArtifactID, Checksum: distribution.Checksum, IdempotencyKey: "run-update-target-check"} if _, err := svc.PushRunUpdateForSession(session, request); err == nil || !strings.Contains(err.Error(), "target-matched") { t.Fatalf("expected cross-target update rejection, got %v", err) } endpoint.Architecture = "amd64" if err := svc.store.RunEndpoints().Update(endpoint); err != nil { t.Fatalf("restore endpoint target: %v", err) } request.IdempotencyKey = "run-update-fenced" update, err := svc.PushRunUpdateForSession(session, request) if err != nil { t.Fatalf("push target-matched update: %v", err) } runSession := registerDependencyUpdateRun(t, svc, instance) claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRunSelfUpdate}, Capacity: domain.RunCapacity{MaxJobs: 1}}) if err != nil || !claim.HasJob || claim.Job.JobID != update.JobID { t.Fatalf("claim Run update: claim=%+v err=%v", claim, err) } inputRequest := domain.RunUpdateInputRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt} input, err := svc.GetRunUpdateInput(inputRequest) if err != nil || input.TargetRelease != update.TargetRelease || input.Checksum != distribution.Checksum { t.Fatalf("get Run update input: input=%+v err=%v", input, err) } chunk, err := svc.ReadRunUpdateChunk(domain.RunUpdateChunkRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 0, Length: 8}) if err != nil || string(chunk.Payload) != string(payload[:8]) || chunk.Offset != 0 || chunk.TotalBytes != int64(len(payload)) { t.Fatalf("read bounded update chunk: chunk=%+v err=%v", chunk, err) } if _, err := svc.ReadRunUpdateChunk(domain.RunUpdateChunkRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt + 1, Offset: 0, Length: 8}); err == nil { t.Fatal("expected stale update chunk attempt rejection") } evidence, _ := json.Marshal(domain.RunUpdateExecutionEvidence{TargetRelease: update.TargetRelease, Phase: "staged"}) if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100, Message: "Run update verified and staged"}, ResultRef: "artifact://jobs/run-update/staged", Message: "Run update verified and staged", ExecutionResult: domain.JobExecutionResult{Kind: "run.update.staged", Checksum: update.Checksum, SizeBytes: int64(len(payload)), Summary: "verified update staged", Content: string(evidence)}}); err != nil { t.Fatalf("complete staged Run update: %v", err) } updates, err := svc.ListRunUpdateJobsForSession(session, instance.ID) if err != nil || len(updates) != 1 || updates[0].Phase != domain.RunUpdatePhaseRestartRequested { t.Fatalf("expected restart-requested projection, updates=%+v err=%v", updates, err) } newHello := dependencyUpdateHello(instance) newHello.Version = update.TargetRelease newRegistration, err := svc.RegisterRunHello(newHello) if err != nil { t.Fatalf("register updated Run: %v", err) } health := domain.RunUpdateHealthReport{RunEndpointID: instance.RunEndpointID, SessionToken: newRegistration.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Outcome: "succeeded", Version: update.TargetRelease} if _, err := svc.ReportRunUpdateHealth(domain.RunUpdateHealthReport{RunEndpointID: health.RunEndpointID, SessionToken: health.SessionToken, JobID: health.JobID, LeaseToken: "stale-lease", Attempt: health.Attempt, Outcome: health.Outcome, Version: health.Version}); err == nil { t.Fatal("expected stale health lease rejection") } result, err := svc.ReportRunUpdateHealth(health) if err != nil || !result.Accepted || result.Phase != domain.RunUpdatePhaseSucceeded { t.Fatalf("report updated Run health: result=%+v err=%v", result, err) } rollbackHello := dependencyUpdateHello(instance) rollbackHello.Version = update.PreviousVersion rollbackRegistration, err := svc.RegisterRunHello(rollbackHello) if err != nil { t.Fatalf("register rolled-back Run: %v", err) } health.SessionToken = rollbackRegistration.SessionToken health.Outcome = "rolled-back" health.Version = update.PreviousVersion result, err = svc.ReportRunUpdateHealth(health) if err != nil || result.Phase != domain.RunUpdatePhaseRolledBack { t.Fatalf("report rollback: result=%+v err=%v", result, err) } updates, _ = svc.ListRunUpdateJobsForSession(session, instance.ID) if !updates[0].Rollback || updates[0].Status != domain.DistributionJobStatusFailed || updates[0].Phase != domain.RunUpdatePhaseRolledBack { t.Fatalf("unexpected rollback projection: %+v", updates[0]) } } func registerDependencyUpdateRun(t *testing.T, svc *CoreService, instance domain.ServerInstance) string { t.Helper() result, err := svc.RegisterRunHello(dependencyUpdateHello(instance)) if err != nil || !result.Accepted { t.Fatalf("register dependency/update Run: result=%+v err=%v", result, err) } return result.SessionToken } func dependencyUpdateHello(instance domain.ServerInstance) domain.RunControlHello { return domain.RunControlHello{ RegistrationToken: "registration-token", RunEndpointID: instance.RunEndpointID, DisplayName: "Dependency Update Run", Version: "0.1.0", Status: domain.RunEndpointStatusOnline, Platform: "linux", Architecture: "amd64", CapabilityReport: domain.RunCapabilityReport{Capabilities: []string{domain.JobCapabilityDependenciesCheck, domain.JobCapabilityDependenciesInstall, domain.JobCapabilityRunSelfUpdate}, Fingerprint: "dependency-update-v1"}, Capacity: domain.RunCapacity{MaxJobs: 2}, } }