Files
browser/platform/service/distributions_test.go
T

693 lines
30 KiB
Go

package service
import (
"errors"
"strings"
"testing"
"time"
"browser.local/platform/domain"
"browser.local/platform/repo"
)
type generatedPackageConfig struct {
Kind string
ServerInstanceID string
PluginID string
RunEndpointID string
ProfileKey string
TargetOS string
TargetArch string
SecretRef string
KeyGeneration int
AuthKey string
}
func TestCoreServiceGeneratesRunDistributionWithEncryptedSingletonKey(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "idem-run-generate",
})
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
if distribution.KeyGeneration != 1 || distribution.SecretRef == "" || distribution.PackageFormat != "raw-executable" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" || distribution.Checksum != "" {
t.Fatalf("unexpected run distribution: %+v", distribution)
}
job, err := svc.GetJob(distribution.BuildJobID)
if err != nil || job.Capability != domain.JobCapabilityDistributionBuild || job.State != domain.JobStateQueued {
t.Fatalf("expected queued backend build job, job=%+v err=%v", job, err)
}
if _, err := svc.GetArtifact(distribution.ArtifactID); !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("generation must not publish config JSON as an artifact, got %v", err)
}
keys, err := svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentRun,
Status: domain.ComponentKeyStatusActive,
})
if err != nil {
t.Fatalf("list component keys: %v", err)
}
if len(keys) != 1 || keys[0].Generation != 1 || !strings.HasPrefix(keys[0].EncryptedKey, "enc:v1:") {
t.Fatalf("expected one active encrypted run key, got %+v", keys)
}
config := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
if config.AuthKey == "" || config.AuthKey == keys[0].EncryptedKey || strings.Contains(distribution.SecretRef, config.AuthKey) {
t.Fatalf("run package key leaked through metadata or was not encrypted, config=%+v key=%+v distribution=%+v", config, keys[0], distribution)
}
if len(config.AuthKey) < 80 {
t.Fatalf("expected longer run component key, got length %d", len(config.AuthKey))
}
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentRun,
Generation: config.KeyGeneration,
Key: config.AuthKey,
})
if err != nil {
t.Fatalf("authenticate run: %v", err)
}
if !auth.Allowed {
t.Fatalf("expected current run key to authenticate, got %+v", auth)
}
second, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "idem-run-generate-second",
})
if err != nil {
t.Fatalf("generate second run distribution: %v", err)
}
if second.KeyGeneration != 1 || second.SecretRef != distribution.SecretRef {
t.Fatalf("expected second package to reuse current singleton key, got first=%+v second=%+v", distribution, second)
}
keys, err = svc.store.EncryptedComponentKeys().List(domain.EncryptedComponentKeyFilter{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentRun,
Status: domain.ComponentKeyStatusActive,
})
if err != nil || len(keys) != 1 {
t.Fatalf("expected one active key after second generation, keys=%+v err=%v", keys, err)
}
}
func TestCoreServiceBuildsDedicatedRunInPlatformBuilder(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
targetID := instance.RunEndpointID
instance.State = domain.ServerInstanceStateDraft
instance.DeploymentTargetID = targetID
instance.RunEndpointID = "server-run-" + instance.ID
if err := svc.store.ServerInstances().Update(instance); err != nil {
t.Fatalf("prepare target-bound draft: %v", err)
}
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "linux", TargetArch: "amd64", IdempotencyKey: "dedicated-target-build"})
if err != nil {
t.Fatalf("generate dedicated Run: %v", err)
}
job, err := svc.GetJob(distribution.BuildJobID)
if err != nil || job.RunEndpointID != platformDistributionBuilderEndpointID || distribution.RunEndpointID != instance.RunEndpointID {
t.Fatalf("expected platform build for dedicated Run %q, job=%+v distribution=%+v err=%v", instance.RunEndpointID, job, distribution, err)
}
}
func TestCoreServiceBuildsSCUMGuidedRunWithoutCompleteRuntimeBinding(t *testing.T) {
svc := newTestCoreService()
plugin := scumDeploymentTestPlugin()
plugin.Name = "SCUM"
plugin.Version = "0.1.1"
plugin.ServerType = "scum"
plugin.Status = domain.GamePluginStatusInstalled
plugin.SupportedOS = []string{"windows"}
plugin.DeclaredPermissions = []string{"server.run.distribution"}
plugin.LifecycleActions = domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Status: "actions/status.json"}
plugin.RuntimeProfiles.LifecycleProfiles = []domain.RuntimeLifecycleProfile{{Key: "run-local", Mode: "local-process", Capabilities: []string{domain.LifecycleCapabilityInstall, domain.LifecycleCapabilityStart, domain.LifecycleCapabilityStop, domain.LifecycleCapabilityStatus}, ActionRefs: domain.PluginLifecycleActions{Install: "actions/install.json", Start: "actions/start.json", Stop: "actions/stop.json", Status: "actions/status.json"}, Platforms: []string{"windows"}}}
requireManualSCUMRuntimeBindings(&plugin, "run-local")
if err := svc.store.GamePlugins().Create(plugin); err != nil {
t.Fatalf("create SCUM plugin: %v", err)
}
session := createServiceUserAndLogin(t, svc, domain.User{ID: "scum-run-builder-owner", DisplayName: "SCUM Run Builder Owner", Email: "scum-run-builder@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
created, err := svc.CreateServerInstanceWorkflowForSession(session, domain.ServerLifecycleCreate{
ID: "scum-guided-run-build", PluginID: plugin.ID, Name: "SCUM Guided Run Build", IdempotencyKey: "scum-guided-run-build-create", ProfileKey: "run-local",
Deployment: domain.ServerDeploymentDefinition{Mode: domain.ServerDeploymentModeGuided, ServerRoot: `D:\scum-guided-run-build`, CreateInputs: map[string]string{"serverName": "Moon", "gamePort": "27000", "queryPort": "27015", "maxPlayers": "128"}},
})
if err != nil {
t.Fatalf("create SCUM guided draft: %v", err)
}
if _, err := svc.runtimeBindingForServer(created.Instance.ID); !errors.Is(err, repo.ErrNotFound) {
t.Fatalf("SCUM guided draft should not need a complete runtime binding before run build: %v", err)
}
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: created.Instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "scum-guided-run-build"})
if err != nil {
t.Fatalf("generate SCUM guided run without runtime binding: %v", err)
}
if distribution.RunEndpointID != dedicatedRunEndpointID(created.Instance.ID) {
t.Fatalf("expected generated run endpoint to be promoted, got %+v", distribution)
}
}
func TestCoreServiceDoesNotPrebindLegacyRunBeforeDistributionBuild(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
legacyEndpointID := instance.RunEndpointID
instance.State = domain.ServerInstanceStateFailed
if err := svc.store.ServerInstances().Update(instance); err != nil {
t.Fatalf("mark legacy server failed: %v", err)
}
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{ServerInstanceID: instance.ID, TargetOS: "windows", TargetArch: "amd64", IdempotencyKey: "legacy-promote-build"})
if err != nil {
t.Fatalf("generate Run without prebinding: %v", err)
}
migrated, err := svc.GetServerInstance(instance.ID)
if err != nil {
t.Fatalf("get migrated server: %v", err)
}
if migrated.DeploymentTargetID != "" || migrated.RunEndpointID != legacyEndpointID {
t.Fatalf("Run generation must not change the server's active endpoint, got %+v", migrated)
}
job, err := svc.GetJob(distribution.BuildJobID)
if err != nil || job.RunEndpointID != platformDistributionBuilderEndpointID || distribution.RunEndpointID != legacyEndpointID {
t.Fatalf("expected platform build and unchanged package endpoint %q, job=%+v distribution=%+v err=%v", legacyEndpointID, job, distribution, err)
}
}
func TestCoreServiceDistributionBuildIgnoresStaleRunEndpoint(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
svc.now = func() time.Time { return fixedTime.Add(runHeartbeatStaleAfter + time.Second) }
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "idem-stale-run-endpoint",
})
if err != nil {
t.Fatalf("platform build must ignore stale Run endpoint: %v", err)
}
completeDistributionBuild(t, svc, distribution, nil)
actions, err := svc.GetServerRuntimeActionsForSession(session, instance.ID)
if err != nil {
t.Fatalf("get runtime actions: %v", err)
}
if actions.RunStatus != domain.RunEndpointStatusOffline {
t.Fatalf("stale Run endpoint must project offline runtime status, got %+v", actions)
}
staleRunGated := map[string]bool{"dependencies-check": false, "dependencies-install": false, "live-logs": false}
for _, action := range actions.Actions {
if action.Key == "generate-run" && !action.Available {
t.Fatalf("stale Run endpoint must not gate platform build availability: %+v", action)
}
if _, gated := staleRunGated[action.Key]; gated {
staleRunGated[action.Key] = true
if action.Available || action.Reason != "Run heartbeat has not been observed" {
t.Fatalf("stale Run endpoint must gate runtime action %s, got %+v", action.Key, action)
}
}
}
for key, seen := range staleRunGated {
if !seen {
t.Fatalf("expected stale Run gating action %s in %+v", key, actions.Actions)
}
}
}
func TestCoreServiceRuntimeActionsGateDependenciesOnPluginPermission(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)
}
actions, err := svc.GetServerRuntimeActionsForSession(session, instance.ID)
if err != nil {
t.Fatalf("get runtime actions: %v", err)
}
seen := 0
for _, action := range actions.Actions {
if action.Key != "dependencies-check" && action.Key != "dependencies-install" {
continue
}
seen++
if action.Available || action.Reason != "plugin permission is not declared" {
t.Fatalf("expected dependency action gated by plugin permission, got %+v", action)
}
}
if seen != 2 {
t.Fatalf("expected both dependency actions in %+v", actions.Actions)
}
}
func TestCoreServiceMachineEndpointCannotClaimPlatformDistributionBuild(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "idem-platform-build-ownership",
})
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
helloRequest := validRunControlHello()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityDistributionBuild)
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil {
t.Fatalf("register machine endpoint: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{
RunEndpointID: instance.RunEndpointID,
SessionToken: hello.SessionToken,
Capabilities: []string{domain.JobCapabilityDistributionBuild},
Capacity: domain.RunCapacity{MaxJobs: 1},
})
if err != nil {
t.Fatalf("claim machine-side build: %v", err)
}
if claim.HasJob {
t.Fatalf("machine-side endpoint must not receive platform build job: %+v", claim)
}
distribution = completeDistributionBuild(t, svc, distribution, nil)
job, err := svc.GetJob(distribution.BuildJobID)
if err != nil || job.State != domain.JobStateSucceeded || job.RunEndpointID != platformDistributionBuilderEndpointID {
t.Fatalf("expected completed platform build job, job=%+v err=%v", job, err)
}
artifact, err := svc.GetArtifact(distribution.ArtifactID)
if err != nil || artifact.OwnerKind != domain.ArtifactOwnerKindJob || artifact.OwnerID != job.ID {
t.Fatalf("expected build job-owned artifact, artifact=%+v err=%v", artifact, err)
}
}
func TestCoreServiceRunDistributionDownloadUsesTargetPackageName(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "windows",
TargetArch: "amd64",
IdempotencyKey: "idem-run-download-name",
})
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
payload := []byte("windows raw run executable")
completeDistributionBuild(t, svc, distribution, payload)
reference, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: distribution.ArtifactID})
if err != nil {
t.Fatalf("open run artifact download: %v", err)
}
if reference.Filename != "run-windows-amd64.exe" || reference.ContentType != "application/octet-stream" {
t.Fatalf("expected windows run package metadata, got %+v", reference)
}
content, err := svc.ReadArtifactContentForSession(session, domain.ArtifactContentRequest{ArtifactID: distribution.ArtifactID, Limit: len(payload)})
if err != nil {
t.Fatalf("read run artifact content: %v", err)
}
if content.Filename != reference.Filename || content.ContentType != reference.ContentType {
t.Fatalf("expected content metadata to match reference, reference=%+v content=%+v", reference, content)
}
}
func TestCoreServiceRunDistributionRetryReusesPartialArtifact(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
key, plainKey, err := svc.ensureActiveComponentKey(instance.ID, domain.DistributionComponentRun, "")
if err != nil {
t.Fatalf("ensure run key: %v", err)
}
idempotencyKey := "web:run.generate:scum-alpha:1784043453685"
distributionID := distributionID("run-dist", instance.ID, "windows", "amd64", key.Generation, idempotencyKey)
artifactID := artifactIDForDistribution(distributionID)
payload := []byte("legacy generated config is not a binary distribution")
partialArtifact, err := svc.createPlatformArtifactPayload(artifactID, domain.ArtifactOwnerKindServerInstance, instance.ID, payload)
if err != nil {
t.Fatalf("create partial artifact: %v", err)
}
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "windows",
TargetArch: "amd64",
IdempotencyKey: idempotencyKey,
})
if err != nil {
t.Fatalf("retry run generation should recover partial artifact: %v", err)
}
if distribution.ID != distributionID || distribution.ArtifactID == partialArtifact.ID || distribution.Checksum != "" {
t.Fatalf("expected real binary build to ignore legacy config artifact, distribution=%+v artifact=%+v", distribution, partialArtifact)
}
if distribution.PackageFormat != "raw-executable" || distribution.Status != domain.DistributionStatusBuilding || distribution.BuildJobID == "" {
t.Fatalf("unexpected recovered distribution: %+v", distribution)
}
recoveredConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
if recoveredConfig.AuthKey != plainKey || recoveredConfig.KeyGeneration != key.Generation {
t.Fatalf("expected recovered artifact payload to remain downloadable, got %+v", recoveredConfig)
}
}
func TestCoreServicePushRunUpdateReusesExistingUpdateJob(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
payload := []byte("compiled run archive")
svc.ConfigureDistributionBuilder(staticDistributionBuilder{payload: payload})
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "idem-run-before-update",
})
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
distribution = completeDistributionBuild(t, svc, distribution, payload)
request := domain.RunUpdateRequest{
ServerInstanceID: instance.ID,
ArtifactID: distribution.ArtifactID,
Checksum: distribution.Checksum,
IdempotencyKey: "idem-run-update-repeat",
}
first, err := svc.PushRunUpdateForSession(session, request)
if err != nil {
t.Fatalf("push run update: %v", err)
}
second, err := svc.PushRunUpdateForSession(session, request)
if err != nil {
t.Fatalf("push run update again should reuse existing update job: %v", err)
}
if second.ID != first.ID || second.JobID != first.JobID || second.ArtifactID != first.ArtifactID {
t.Fatalf("expected repeated push to return existing update job, first=%+v second=%+v", first, second)
}
if second.Status != domain.DistributionJobStatusQueued {
t.Fatalf("expected queued existing update job, got %+v", second)
}
}
func TestCoreServiceResetRunKeyRevokesOldPackagesAndRequiresRegeneration(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
distribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "idem-run-before-reset",
})
if err != nil {
t.Fatalf("generate run distribution: %v", err)
}
oldConfig := readGeneratedPackageConfig(t, svc, session, distribution.ArtifactID)
distribution = completeDistributionBuild(t, svc, distribution, []byte("compiled run archive before reset"))
helloRequest := validRunControlHello()
helloRequest.RunEndpointID = instance.RunEndpointID
helloRequest.RegistrationToken = oldConfig.AuthKey
helloRequest.ServerInstanceID = instance.ID
helloRequest.PluginID = instance.PluginID
helloRequest.ComponentKind = domain.DistributionComponentRun
helloRequest.KeyGeneration = oldConfig.KeyGeneration
hello, err := svc.RegisterRunHello(helloRequest)
if err != nil || !hello.Accepted {
t.Fatalf("register old run before reset: hello=%+v err=%v", hello, err)
}
reset, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentRun,
})
if err != nil {
t.Fatalf("reset run key: %v", err)
}
if reset.Generation != 2 || reset.Status != domain.ComponentKeyStatusActive {
t.Fatalf("expected reset key generation 2, got %+v", reset)
}
if _, err := svc.AcceptRunHeartbeat(domain.RunControlHeartbeat{
RunEndpointID: instance.RunEndpointID,
SessionToken: hello.SessionToken,
Status: domain.RunEndpointStatusOnline,
Version: "stale-run",
Capacity: domain.RunCapacity{MaxJobs: 1},
CapabilityFingerprint: helloRequest.CapabilityReport.Fingerprint,
}); err == nil {
t.Fatal("expected reset to revoke the active run control session")
}
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
if err != nil {
t.Fatalf("get endpoint after reset: %v", err)
}
if endpoint.Status != domain.RunEndpointStatusOffline {
t.Fatalf("expected reset endpoint to be marked offline, got %+v", endpoint)
}
endpoint.Status = domain.RunEndpointStatusOnline
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("restore endpoint for regeneration: %v", err)
}
oldDistribution, err := svc.store.RunDistributions().Get(distribution.ID)
if err != nil {
t.Fatalf("get old distribution: %v", err)
}
if oldDistribution.Status != domain.DistributionStatusRevoked {
t.Fatalf("expected old distribution revoked, got %+v", oldDistribution)
}
if _, err := svc.OpenArtifactDownloadForSession(session, domain.ArtifactDownloadReferenceRequest{ArtifactID: distribution.ArtifactID}); err == nil || !strings.Contains(err.Error(), "available") {
t.Fatalf("expected old artifact download to be unavailable, got %v", err)
}
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentRun,
Generation: oldConfig.KeyGeneration,
Key: oldConfig.AuthKey,
})
if err != nil {
t.Fatalf("authenticate old key: %v", err)
}
if auth.Allowed || !strings.Contains(auth.Reason, "generation") {
t.Fatalf("expected old package authentication denial, got %+v", auth)
}
newDistribution, err := svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "idem-run-after-reset",
})
if err != nil {
t.Fatalf("generate run distribution after reset: %v", err)
}
newConfig := readGeneratedPackageConfig(t, svc, session, newDistribution.ArtifactID)
if newDistribution.KeyGeneration != 2 || newConfig.AuthKey == oldConfig.AuthKey {
t.Fatalf("expected regenerated package with new generation/key, old=%+v new=%+v", oldConfig, newConfig)
}
auth, err = svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentRun,
Generation: newConfig.KeyGeneration,
Key: newConfig.AuthKey,
})
if err != nil {
t.Fatalf("authenticate new key: %v", err)
}
if !auth.Allowed {
t.Fatalf("expected regenerated package to authenticate, got %+v", auth)
}
}
func TestCoreServiceRejectsClientManagerComponentRequests(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
if _, err := svc.ResetComponentKeyForSession(session, domain.ComponentKeyResetRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentKind("client-manager"),
ComponentKey: "scum-client-manager",
}); err == nil || !strings.Contains(err.Error(), "componentKind") {
t.Fatalf("expected client-manager key reset to be rejected, got %v", err)
}
auth, err := svc.AuthenticateComponent(domain.ComponentAuthenticationRequest{
ServerInstanceID: instance.ID,
ComponentKind: domain.DistributionComponentKind("client-manager"),
ComponentKey: "scum-client-manager",
Generation: 1,
Key: "legacy-client-manager-key",
})
if err == nil || auth.Allowed {
t.Fatalf("expected client-manager component authentication to be rejected, auth=%+v err=%v", auth, err)
}
}
func newDistributionTestFixture(t *testing.T) (*CoreService, string, domain.ServerInstance) {
t.Helper()
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
plugin.SupportedOS = []string{"linux", "windows"}
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions,
"server.run.distribution",
"server.dependencies.manage",
)
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities,
domain.JobCapabilityRunSelfUpdate,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityLogsBackfill,
)
plugin.BridgeActions = append(plugin.BridgeActions,
string(domain.PluginBridgeActionRunDistribution),
string(domain.PluginBridgeActionDependenciesRequest),
string(domain.PluginBridgeActionLogsBackfillRequest),
)
plugin.RuntimeProfiles.DependencyProbes = []domain.RuntimeDependencyProbe{{Key: "java-runtime", Kind: "command.version", TargetKey: "java", Platforms: []string{"linux"}}}
plugin.RuntimeProfiles.InstallPlans = []domain.RuntimeInstallPlan{{Key: "java-install", Title: "Install Java", Platforms: []string{"linux"}, Steps: []domain.RuntimeInstallStep{{Type: "package", TargetKey: "java", PackageManager: "apt", PackageName: "openjdk-21-jre"}}}}
plugin.RuntimeProfiles.LogSources = []domain.RuntimeLogSource{{Key: "latest", Kind: "file.tail", TargetKey: "logs/latest", StreamKey: "latest-log", CursorKind: "offset", RetentionDays: 30}}
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update plugin fixture: %v", err)
}
endpoint.Capabilities = append(endpoint.Capabilities,
domain.JobCapabilityRunSelfUpdate,
domain.JobCapabilityDependenciesCheck,
domain.JobCapabilityDependenciesInstall,
domain.JobCapabilityLogsBackfill,
)
endpoint.Platform = "linux"
endpoint.Architecture = "amd64"
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update endpoint fixture: %v", err)
}
session := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-distribution-owner",
DisplayName: "Distribution Owner",
Email: "distribution-owner@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{
ID: "server-distribution",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Distribution Server",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create distribution server: %v", err)
}
binding, err := svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: "local", Bindings: map[string]string{"logs/latest": "runtime.logs.latest"}}, true)
if err != nil {
t.Fatalf("build complete runtime binding: %v", err)
}
if err := svc.store.RuntimeBindings().Create(binding); err != nil {
t.Fatalf("create runtime binding: %v", err)
}
return svc, session, instance
}
func TestQueueLogBackfillFreezesDeclaredFileLogSource(t *testing.T) {
svc, session, instance := newDistributionTestFixture(t)
job, err := svc.QueueLogBackfillForSession(session, domain.LogBackfillRequest{ServerInstanceID: instance.ID, SourceKey: "latest", CheckpointRef: "artifact://logs/checkpoint/1", IdempotencyKey: "log-source-freeze"})
if err != nil {
t.Fatalf("queue log backfill: %v", err)
}
if job.ExecutionInput.LogSource == nil || job.ExecutionInput.LogSource.Key != "latest" || job.ExecutionInput.LogSource.StreamKey != "latest-log" || job.ExecutionInput.LogSource.TargetKey != "logs/latest" {
t.Fatalf("expected frozen declared log source, got %+v", job.ExecutionInput.LogSource)
}
stream, err := svc.GetLogStream(jobLogStreamID(job.ID, "latest-log"))
if err != nil || stream.Source != domain.LogStreamSourceFile || stream.StreamKey != "latest-log" {
t.Fatalf("expected file log stream for backfill job, stream=%+v err=%v", stream, err)
}
if _, err := svc.QueueLogBackfillForSession(session, domain.LogBackfillRequest{ServerInstanceID: instance.ID, SourceKey: "missing", IdempotencyKey: "log-source-missing"}); err == nil || !strings.Contains(err.Error(), "not declared") {
t.Fatalf("expected undeclared source rejection, got %v", err)
}
}
func readGeneratedPackageConfig(t *testing.T, svc *CoreService, session string, artifactID string) generatedPackageConfig {
t.Helper()
_ = session
runs, err := svc.store.RunDistributions().List(domain.RunDistributionFilter{})
if err != nil {
t.Fatalf("list run distributions: %v", err)
}
for _, distribution := range runs {
if distribution.ArtifactID != artifactID {
continue
}
key, err := svc.activeComponentKey(distribution.ServerInstanceID, domain.DistributionComponentRun, "")
if err != nil {
t.Fatalf("get run key: %v", err)
}
plain, err := decryptRuntimeKey(key.EncryptedKey)
if err != nil {
t.Fatalf("decrypt run key: %v", err)
}
return generatedPackageConfig{Kind: "run", ServerInstanceID: distribution.ServerInstanceID, PluginID: distribution.PluginID, RunEndpointID: distribution.RunEndpointID, TargetOS: distribution.TargetOS, TargetArch: distribution.TargetArch, SecretRef: distribution.SecretRef, KeyGeneration: distribution.KeyGeneration, AuthKey: plain}
}
t.Fatalf("distribution for artifact %s was not found", artifactID)
return generatedPackageConfig{}
}
func completeDistributionBuild(t *testing.T, svc *CoreService, distribution domain.RunDistribution, _ []byte) domain.RunDistribution {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
updated, err := svc.store.RunDistributions().Get(distribution.ID)
if err != nil {
t.Fatalf("get run distribution: %v", err)
}
if updated.Status == domain.DistributionStatusAvailable {
return updated
}
if updated.Status == domain.DistributionStatusFailed {
t.Fatalf("platform run build failed: %+v", updated)
}
time.Sleep(time.Millisecond)
}
t.Fatalf("platform run build did not complete")
return domain.RunDistribution{}
}
func TestCoreServiceDeniesRunDistributionWithoutPluginDeclaration(t *testing.T) {
svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
session := createServiceUserAndLogin(t, svc, domain.User{
ID: "user-distribution-denied",
DisplayName: "Distribution Denied",
Email: "distribution-denied@example.test",
Roles: []string{"server-owner"},
PasswordHash: "secret-password",
})
instance, err := svc.CreateServerInstanceForSession(session, domain.ServerInstance{
ID: "server-distribution-denied",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Distribution Denied Server",
State: domain.ServerInstanceStateReady,
})
if err != nil {
t.Fatalf("create denied server: %v", err)
}
_, err = svc.GenerateRunDistributionForSession(session, domain.RunDistributionGenerateRequest{
ServerInstanceID: instance.ID,
TargetOS: "linux",
TargetArch: "amd64",
IdempotencyKey: "idem-run-denied",
})
if !errors.Is(err, ErrForbidden) {
t.Fatalf("expected plugin declaration denial, got %v", err)
}
}