init
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidateRunJobAssignmentScopedFilePayloads(t *testing.T) {
|
||||
assignment := RunJobAssignment{
|
||||
JobID: "job-config-write",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityConfigWrite,
|
||||
TargetKey: "server.properties",
|
||||
InputRef: "input://server-config/server-1/server.properties/v1",
|
||||
IdempotencyKey: "idem-config",
|
||||
}
|
||||
if err := ValidateRunJobAssignment(assignment); err != nil {
|
||||
t.Fatalf("expected valid config write assignment: %v", err)
|
||||
}
|
||||
|
||||
assignment.TargetKey = "/Users/tasia/server.properties"
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "targetKey") {
|
||||
t.Fatalf("expected raw host path rejection, got %v", err)
|
||||
}
|
||||
|
||||
assignment.TargetKey = "server.properties"
|
||||
assignment.InputRef = "sk-raw-secret"
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "inputRef") {
|
||||
t.Fatalf("expected raw credential ref rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunJobAssignmentScopedReadDoesNotRequireInputRef(t *testing.T) {
|
||||
assignment := RunJobAssignment{
|
||||
JobID: "job-files-read",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityFilesRead,
|
||||
TargetKey: "logs/latest.log",
|
||||
IdempotencyKey: "idem-read",
|
||||
}
|
||||
if err := ValidateRunJobAssignment(assignment); err != nil {
|
||||
t.Fatalf("expected valid file read assignment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunJobAssignmentRequiresBoundedSQLiteSchemaProbe(t *testing.T) {
|
||||
assignment := RunJobAssignment{JobID: "job-probe", ServerInstanceID: "server-1", RunEndpointID: "run-1", Capability: RunCapabilityRemoteRunDBSQLiteProbe, TargetKey: "databases/current.db", IdempotencyKey: "idem-probe", MaxAttempts: 1, FencingToken: 1, ExecutionInput: RunJobExecutionInput{WorkspaceScope: "profile-1", SQLiteSchemaProbe: &SQLiteSchemaProbeRequest{RequestID: "probe-1", Binding: SQLiteSchemaProbeBinding{ServerInstanceID: "server-1", RunBindingID: "binding-1", RunEndpointID: "run-1", PluginID: "game.example", PluginVersion: "1.0.0", AdapterVersion: "adapter-1", DatabaseIdentity: "database-1"}, Limits: SQLiteSchemaProbeLimits{MaxObjects: 8, MaxColumnsPerObject: 8, MaxIndexesPerObject: 8, MaxForeignKeys: 8, MaxCardinalityReads: 8, MaxSampleRows: 2, TimeoutMS: 1000, MaxResultBytes: 1024}}}}
|
||||
if err := ValidateRunJobAssignment(assignment); err != nil {
|
||||
t.Fatalf("expected bounded SQLite schema probe to validate: %v", err)
|
||||
}
|
||||
assignment.ExecutionInput.SQLiteSchemaProbe.Limits.MaxSampleRows = 4
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "limits") {
|
||||
t.Fatalf("expected oversized sample limit rejection, got %v", err)
|
||||
}
|
||||
assignment.ExecutionInput.SQLiteSchemaProbe.Limits.MaxSampleRows = 2
|
||||
assignment.ExecutionInput.Content = "SELECT * FROM private"
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "must not carry") {
|
||||
t.Fatalf("expected embedded query rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunJobAssignmentRejectsLegacyGameSpecificDeploymentPlan(t *testing.T) {
|
||||
assignment := RunJobAssignment{
|
||||
JobID: "job-legacy-scum-plan",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityProcessInstall,
|
||||
IdempotencyKey: "idem-legacy-scum",
|
||||
ExecutionInput: RunJobExecutionInput{
|
||||
Deployment: &ServerDeploymentExecution{SchemaVersion: "1", Mode: "guided-install", ServerRoot: "C:/scumserver", Revision: 1},
|
||||
ServerDeploymentPlan: &ServerDeploymentPlan{SchemaVersion: "1", PluginID: "game.scum", TemplateKey: "scum-steamcmd-windows"},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "legacy unsupported") {
|
||||
t.Fatalf("expected legacy game-specific plan rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunJobAssignmentAllowsDeclaredProcessLogSourcesOnlyForStart(t *testing.T) {
|
||||
assignment := RunJobAssignment{
|
||||
JobID: "job-process-logs",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityProcessStart,
|
||||
TargetKey: "actions/start.json",
|
||||
IdempotencyKey: "idem-process-logs",
|
||||
ExecutionInput: RunJobExecutionInput{LogSources: []RuntimeLogSourcePlan{
|
||||
{Key: "console-stdout", Kind: "process.stdout", TargetKey: "process/server", StreamKey: "game.console.stdout", CursorKind: "sequence", RetentionDays: 30},
|
||||
{Key: "console-stderr", Kind: "process.stderr", TargetKey: "process/server", StreamKey: "game.console.stderr", CursorKind: "sequence", RetentionDays: 30},
|
||||
}},
|
||||
}
|
||||
if err := ValidateRunJobAssignment(assignment); err != nil {
|
||||
t.Fatalf("expected process log sources to validate: %v", err)
|
||||
}
|
||||
|
||||
assignment.ExecutionInput.LogSources[0].Kind = "file.tail"
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "process log source kind") {
|
||||
t.Fatalf("expected file log source rejection on process.start, got %v", err)
|
||||
}
|
||||
|
||||
assignment.ExecutionInput.LogSources[0].Kind = "process.stdout"
|
||||
assignment.Capability = RunCapabilityProcessStop
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "process log sources") {
|
||||
t.Fatalf("expected process log source rejection outside start, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunJobAssignmentRemoteCapabilitiesAreBounded(t *testing.T) {
|
||||
assignment := RunJobAssignment{
|
||||
JobID: "job-remote-rcon",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityRemoteRunRCONCommand,
|
||||
TargetKey: "rcon/command",
|
||||
InputRef: "input://server-1/rcon/command/1",
|
||||
IdempotencyKey: "idem-rcon",
|
||||
}
|
||||
if err := ValidateRunJobAssignment(assignment); err != nil {
|
||||
t.Fatalf("expected valid remote rcon assignment: %v", err)
|
||||
}
|
||||
|
||||
assignment.InputRef = "password=raw"
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "inputRef") {
|
||||
t.Fatalf("expected unsafe inputRef rejection, got %v", err)
|
||||
}
|
||||
|
||||
assignment.InputRef = "input://server-1/rcon/command/1"
|
||||
assignment.TargetKey = "/Users/tasia/server.db"
|
||||
if err := ValidateRunJobAssignment(assignment); err == nil || !strings.Contains(err.Error(), "targetKey") {
|
||||
t.Fatalf("expected unsafe targetKey rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunJobAssignmentDistributionCapabilitiesAreBounded(t *testing.T) {
|
||||
selfUpdate := RunJobAssignment{
|
||||
JobID: "job-update",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityRunSelfUpdate,
|
||||
TargetKey: "run/update",
|
||||
InputRef: "artifact://artifact-run-latest",
|
||||
IdempotencyKey: "idem-update",
|
||||
}
|
||||
if err := ValidateRunJobAssignment(selfUpdate); err != nil {
|
||||
t.Fatalf("expected valid self-update assignment: %v", err)
|
||||
}
|
||||
selfUpdate.InputRef = "input://not-an-artifact"
|
||||
if err := ValidateRunJobAssignment(selfUpdate); err == nil || !strings.Contains(err.Error(), "artifact") {
|
||||
t.Fatalf("expected non-artifact self-update ref rejection, got %v", err)
|
||||
}
|
||||
|
||||
check := RunJobAssignment{
|
||||
JobID: "job-dependency-check",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityDependenciesCheck,
|
||||
TargetKey: "dependencies/java-21",
|
||||
IdempotencyKey: "idem-dep-check",
|
||||
}
|
||||
if err := ValidateRunJobAssignment(check); err != nil {
|
||||
t.Fatalf("expected valid dependency check assignment: %v", err)
|
||||
}
|
||||
check.TargetKey = "dependencies/install/java;rm"
|
||||
if err := ValidateRunJobAssignment(check); err == nil || !strings.Contains(err.Error(), "targetKey") {
|
||||
t.Fatalf("expected shell-like dependency target rejection, got %v", err)
|
||||
}
|
||||
|
||||
backfill := RunJobAssignment{
|
||||
JobID: "job-log-backfill",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityLogsBackfill,
|
||||
TargetKey: "logs/latest-log",
|
||||
InputRef: "artifact://logs/checkpoint/1",
|
||||
IdempotencyKey: "idem-log-backfill",
|
||||
ExecutionInput: RunJobExecutionInput{LogSource: &RuntimeLogSourcePlan{Key: "latest-log", Kind: "file.tail", TargetKey: "logs/latest.log", StreamKey: "latest-log", CursorKind: "offset", RetentionDays: 30}},
|
||||
}
|
||||
if err := ValidateRunJobAssignment(backfill); err != nil {
|
||||
t.Fatalf("expected valid log backfill assignment: %v", err)
|
||||
}
|
||||
backfill.ExecutionInput.LogSource.Kind = "sql.query"
|
||||
if err := ValidateRunJobAssignment(backfill); err == nil || !strings.Contains(err.Error(), "unsupported") {
|
||||
t.Fatalf("expected unsupported log source rejection, got %v", err)
|
||||
}
|
||||
backfill.ExecutionInput.LogSource.Kind = "file.tail"
|
||||
backfill.InputRef = "password=raw"
|
||||
if err := ValidateRunJobAssignment(backfill); err == nil || !strings.Contains(err.Error(), "inputRef") {
|
||||
t.Fatalf("expected unsafe log checkpoint rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunJobAssignmentDLLExtensionsAreBounded(t *testing.T) {
|
||||
assignment := RunJobAssignment{
|
||||
JobID: "job-dll-extension",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityProcessStart,
|
||||
TargetKey: "actions/start.json",
|
||||
IdempotencyKey: "idem-dll-extension",
|
||||
ExecutionInput: RunJobExecutionInput{
|
||||
WorkspaceScope: "run-local",
|
||||
DLLExtensions: []RuntimeDLLExtensionPlan{validRuntimeDLLExtensionPlan()},
|
||||
},
|
||||
}
|
||||
if err := ValidateRunJobAssignment(assignment); err != nil {
|
||||
t.Fatalf("expected valid DLL extension assignment: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
mutate func(*RunJobAssignment)
|
||||
want string
|
||||
}{
|
||||
{name: "not process start", mutate: func(value *RunJobAssignment) { value.Capability = RunCapabilityProcessStop }, want: "process.start"},
|
||||
{name: "query URL", mutate: func(value *RunJobAssignment) { value.ExecutionInput.DLLExtensions[0].ReleaseURL += "?release=1" }, want: "release integrity"},
|
||||
{name: "unsafe DLL path", mutate: func(value *RunJobAssignment) {
|
||||
value.ExecutionInput.DLLExtensions[0].DLLRef = "ue4ss/Mods/scum_simple_rcon/dlls/other.dll"
|
||||
}, want: "deployment path"},
|
||||
{name: "bad checksum", mutate: func(value *RunJobAssignment) { value.ExecutionInput.DLLExtensions[0].Checksum = "sha256:bad" }, want: "release integrity"},
|
||||
{name: "missing scope", mutate: func(value *RunJobAssignment) { value.ExecutionInput.WorkspaceScope = "" }, want: "process.start"},
|
||||
}
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
value := assignment
|
||||
value.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), assignment.ExecutionInput.DLLExtensions...)
|
||||
testCase.mutate(&value)
|
||||
if err := ValidateRunJobAssignment(value); err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||
t.Fatalf("expected %q validation error, got %v", testCase.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRunJobAssignmentSourceRCONPlanIsFrozenAndOneShot(t *testing.T) {
|
||||
assignment := RunJobAssignment{
|
||||
JobID: "job-source-rcon",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityRemoteRunRCONCommand,
|
||||
TargetKey: "rcon.password",
|
||||
InputRef: "input://source-rcon/job-source-rcon",
|
||||
IdempotencyKey: "idem-source-rcon",
|
||||
Attempt: 1,
|
||||
MaxAttempts: 1,
|
||||
ExecutionInput: RunJobExecutionInput{
|
||||
WorkspaceScope: "run-local",
|
||||
RemoteAdapterKey: "rcon",
|
||||
RemoteAdapterKind: "rcon",
|
||||
TimeoutSeconds: 30,
|
||||
SourceRCON: &RuntimeSourceRCONPlan{Protocol: "source-rcon", ExtensionKey: "scum-simple-rcon", ModKey: "scum_simple_rcon", ConfigRef: "ue4ss/Mods/scum_simple_rcon/config.ini", DeploymentStateRef: "runtime/ue4ss-dll/ue4ss/scum-simple-rcon/release.json", Port: 27015},
|
||||
},
|
||||
}
|
||||
if err := ValidateRunJobAssignment(assignment); err != nil {
|
||||
t.Fatalf("expected valid frozen Source RCON plan: %v", err)
|
||||
}
|
||||
protected := assignment
|
||||
protected.Capability = RunCapabilityRemoteRunProtectedRCON
|
||||
protected.TargetKey = "scum-management"
|
||||
protected.InputRef = "input://protected-request/job-protected-rcon"
|
||||
protected.FencingToken = 7
|
||||
protected.ExecutionInput.RemoteAdapterKey = "scum-management"
|
||||
protected.ExecutionInput.RemoteAdapterKind = "protected-rcon"
|
||||
protected.ExecutionInput.TimeoutSeconds = 120
|
||||
plan := *assignment.ExecutionInput.SourceRCON
|
||||
protected.ExecutionInput.SourceRCON = &plan
|
||||
if err := ValidateRunJobAssignment(protected); err != nil {
|
||||
t.Fatalf("expected valid protected Source RCON plan: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
mutate func(*RunJobAssignment)
|
||||
want string
|
||||
}{
|
||||
{name: "multiple attempts", mutate: func(value *RunJobAssignment) { value.MaxAttempts = 2 }, want: "exactly one attempt"},
|
||||
{name: "wrong capability", mutate: func(value *RunJobAssignment) { value.Capability = RunCapabilityRemoteRunDBSQLiteQuery }, want: "remote.run.rcon.command or remote.run.protected.rcon"},
|
||||
{name: "unsafe config reference", mutate: func(value *RunJobAssignment) {
|
||||
value.ExecutionInput.SourceRCON.ConfigRef = "ue4ss/Mods/scum_simple_rcon/other.ini"
|
||||
}, want: "config reference"},
|
||||
{name: "unsafe deployment state reference", mutate: func(value *RunJobAssignment) {
|
||||
value.ExecutionInput.SourceRCON.DeploymentStateRef = "runtime/ue4ss-dll/../../release.json"
|
||||
}, want: "deployment state reference"},
|
||||
{name: "unsafe port", mutate: func(value *RunJobAssignment) { value.ExecutionInput.SourceRCON.Port = 80 }, want: "port"},
|
||||
{name: "ordinary input ref", mutate: func(value *RunJobAssignment) { value.InputRef = "input://server-1/rcon/command" }, want: "source-rcon input ref"},
|
||||
{name: "missing plan", mutate: func(value *RunJobAssignment) { value.ExecutionInput.SourceRCON = nil }, want: "requires a Source RCON plan"},
|
||||
}
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
value := assignment
|
||||
plan := *assignment.ExecutionInput.SourceRCON
|
||||
value.ExecutionInput.SourceRCON = &plan
|
||||
testCase.mutate(&value)
|
||||
if err := ValidateRunJobAssignment(value); err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||
t.Fatalf("expected %q validation error, got %v", testCase.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateProtectedRequestAssignmentAndOneTimeInput(t *testing.T) {
|
||||
assignment := RunJobAssignment{
|
||||
JobID: "job-protected",
|
||||
ServerInstanceID: "server-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: RunCapabilityRemoteRunProtectedSQL,
|
||||
TargetKey: "scum-database",
|
||||
InputRef: "input://protected-request/job-protected",
|
||||
IdempotencyKey: "protected-1",
|
||||
LeaseToken: "lease-protected",
|
||||
FencingToken: 7,
|
||||
Attempt: 1,
|
||||
MaxAttempts: 1,
|
||||
ExecutionInput: RunJobExecutionInput{
|
||||
RemoteAdapterKey: "scum-database",
|
||||
RemoteAdapterKind: "protected-sql",
|
||||
TimeoutSeconds: 30,
|
||||
},
|
||||
}
|
||||
if err := ValidateRunJobAssignment(assignment); err != nil {
|
||||
t.Fatalf("expected valid protected assignment: %v", err)
|
||||
}
|
||||
input := ProtectedRequestExecutionInputResponse{JobID: assignment.JobID, ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: assignment.RunEndpointID, FencingToken: assignment.FencingToken, Authorized: true, ApprovalState: "approved", QueueState: "claimed", ExpiresAt: time.Now().UTC().Add(time.Minute), Kind: "sql", TransportKey: "scum-database", TargetKey: assignment.TargetKey, RequestText: "SELECT player_id FROM players LIMIT 1"}
|
||||
if !ValidProtectedRequestExecutionInput(input) {
|
||||
t.Fatal("expected approved unexpired protected input")
|
||||
}
|
||||
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
mutate func(*RunJobAssignment)
|
||||
want string
|
||||
}{
|
||||
{name: "missing fence", mutate: func(value *RunJobAssignment) { value.FencingToken = 0 }, want: "fenced"},
|
||||
{name: "multiple attempts", mutate: func(value *RunJobAssignment) { value.MaxAttempts = 2 }, want: "single"},
|
||||
{name: "wrong input ref", mutate: func(value *RunJobAssignment) { value.InputRef = "input://ordinary/request" }, want: "inputRef"},
|
||||
{name: "wrong adapter", mutate: func(value *RunJobAssignment) { value.ExecutionInput.RemoteAdapterKind = "database" }, want: "adapter kind"},
|
||||
{name: "inline text", mutate: func(value *RunJobAssignment) { value.ExecutionInput.Content = "SELECT secret" }, want: "must not"},
|
||||
} {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
value := assignment
|
||||
testCase.mutate(&value)
|
||||
if err := ValidateRunJobAssignment(value); err == nil || !strings.Contains(err.Error(), testCase.want) {
|
||||
t.Fatalf("expected %q validation error, got %v", testCase.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
input.ExpiresAt = time.Now().UTC().Add(-time.Second)
|
||||
if ValidProtectedRequestExecutionInput(input) {
|
||||
t.Fatal("expired protected input was accepted")
|
||||
}
|
||||
input.ExpiresAt = time.Now().UTC().Add(time.Minute)
|
||||
input.QueueState = "pending"
|
||||
if ValidProtectedRequestExecutionInput(input) {
|
||||
t.Fatal("unclaimed protected input was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func validRuntimeDLLExtensionPlan() RuntimeDLLExtensionPlan {
|
||||
return RuntimeDLLExtensionPlan{
|
||||
Key: "scum-simple-rcon",
|
||||
Version: "1.0.0",
|
||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll",
|
||||
Checksum: "sha256:" + strings.Repeat("a", 64),
|
||||
SizeBytes: 1024,
|
||||
TargetKey: "ue4ss/scum-simple-rcon",
|
||||
ModKey: "scum_simple_rcon",
|
||||
DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64),
|
||||
UE4SSABI: "ue4ss-3.0",
|
||||
RCONPort: 27015,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user