Add SCUM file management workbench
This commit is contained in:
@@ -839,6 +839,165 @@ func TestCoreServiceConfigWriteAndFileDispatchAreScoped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeclaredPluginFileWorkspaceConstrainsFileDispatch(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
plugin.FileWorkspace = scumTestFileWorkspace()
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin workspace: %v", err)
|
||||
}
|
||||
ownerSession := createServiceUserAndLogin(t, svc, domain.User{
|
||||
ID: "user-owner-file-workspace",
|
||||
DisplayName: "File Workspace Owner",
|
||||
Email: "file-workspace-owner@example.test",
|
||||
Roles: []string{"server-owner"},
|
||||
PasswordHash: "secret-password",
|
||||
})
|
||||
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
|
||||
ID: "server-file-workspace",
|
||||
PluginID: plugin.ID,
|
||||
RunEndpointID: endpoint.ID,
|
||||
Name: "File Workspace Server",
|
||||
State: domain.ServerInstanceStateRunning,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
createCompleteRuntimeBinding(t, svc, instance, "local")
|
||||
|
||||
allowed, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
Operation: domain.FileOperationRead,
|
||||
Key: "scum-server-settings",
|
||||
IdempotencyKey: "idem-file-workspace-read",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("dispatch declared file read: %v", err)
|
||||
}
|
||||
if allowed.Job.TargetKey != "scum-server-settings" || allowed.Job.Capability != domain.JobCapabilityFilesRead {
|
||||
t.Fatalf("unexpected declared file dispatch: %+v", allowed)
|
||||
}
|
||||
if _, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
Operation: domain.FileOperationRead,
|
||||
Key: "logs/latest.log",
|
||||
IdempotencyKey: "idem-file-workspace-unknown",
|
||||
}); err == nil || !strings.Contains(err.Error(), "plugin-declared file") {
|
||||
t.Fatalf("expected undeclared file key rejection, got %v", err)
|
||||
}
|
||||
if _, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
|
||||
ServerInstanceID: instance.ID,
|
||||
PluginID: plugin.ID,
|
||||
Operation: domain.FileOperationWrite,
|
||||
Key: "scum-chat-log",
|
||||
InputRef: "input://file-workspace/update",
|
||||
Content: "line",
|
||||
IdempotencyKey: "idem-file-workspace-log-write",
|
||||
}); err == nil || !strings.Contains(err.Error(), "not writable") {
|
||||
t.Fatalf("expected log write rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeclaredFileReadSnapshotProjectionStatesAndRedaction(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
plugin.FileWorkspace = scumTestFileWorkspace()
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("update plugin workspace: %v", err)
|
||||
}
|
||||
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-snapshot-owner", DisplayName: "File Snapshot Owner", Email: "file-snapshot-owner@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||
otherSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-snapshot-other", DisplayName: "File Snapshot Other", Email: "file-snapshot-other@example.test", Roles: []string{"server-admin"}, PasswordHash: "secret-password"})
|
||||
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-snapshot", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Snapshot Server", State: domain.ServerInstanceStateRunning})
|
||||
if err != nil {
|
||||
t.Fatalf("create server: %v", err)
|
||||
}
|
||||
|
||||
snapshot, err := svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
|
||||
if err != nil || snapshot.State != "not-read" {
|
||||
t.Fatalf("expected not-read without jobs, snapshot=%+v err=%v", snapshot, err)
|
||||
}
|
||||
queued := createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-queued", domain.JobStateQueued, 1, "")
|
||||
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
|
||||
if err != nil || snapshot.State != "pending" || snapshot.JobID != queued.ID {
|
||||
t.Fatalf("expected pending queued job, snapshot=%+v err=%v", snapshot, err)
|
||||
}
|
||||
queued.State = domain.JobStateFailed
|
||||
queued.UpdatedAt = fixedTime.Add(2 * time.Minute)
|
||||
queued.TerminalAt = fixedTime.Add(2 * time.Minute)
|
||||
if err := svc.store.Jobs().Update(queued); err != nil {
|
||||
t.Fatalf("update failed read job: %v", err)
|
||||
}
|
||||
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-cancelled", domain.JobStateCancelled, 3, "")
|
||||
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
|
||||
if err != nil || snapshot.State != "not-read" {
|
||||
t.Fatalf("failed/cancelled reads must not mask not-read, snapshot=%+v err=%v", snapshot, err)
|
||||
}
|
||||
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-success-old", domain.JobStateSucceeded, 4, "ServerName=Old\nRconPassword=secret\n")
|
||||
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-failed-newer", domain.JobStateFailed, 5, "")
|
||||
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
|
||||
if err != nil || snapshot.State != "ready" || snapshot.JobID != "job-file-snapshot-success-old" || !strings.Contains(snapshot.Content, "RconPassword=<redacted>") {
|
||||
t.Fatalf("expected older successful redacted result, snapshot=%+v err=%v", snapshot, err)
|
||||
}
|
||||
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-success-new", domain.JobStateSucceeded, 6, "ServerName=New\nApiToken=secret\n")
|
||||
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
|
||||
if err != nil || snapshot.JobID != "job-file-snapshot-success-new" || !strings.Contains(snapshot.Content, "ServerName=New") || strings.Contains(snapshot.Content, "secret") {
|
||||
t.Fatalf("expected newest successful redacted result, snapshot=%+v err=%v", snapshot, err)
|
||||
}
|
||||
if _, err := svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "logs/latest.log"); err == nil || !strings.Contains(err.Error(), "plugin-declared file") {
|
||||
t.Fatalf("expected unknown logical key rejection, got %v", err)
|
||||
}
|
||||
if _, err := svc.GetDeclaredFileReadSnapshotForSession(otherSession, instance.ID, "scum-server-settings"); !errors.Is(err, ErrForbidden) {
|
||||
t.Fatalf("expected unrelated session forbidden, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func scumTestFileWorkspace() domain.PluginFileWorkspace {
|
||||
return domain.PluginFileWorkspace{
|
||||
DefaultDirectoryKey: "scum-config",
|
||||
Directories: []domain.PluginLogicalDirectory{
|
||||
{Key: "scum-config", Label: "服务器配置", Scope: "config"},
|
||||
{Key: "scum-logs", Label: "日志文件", Scope: "logs"},
|
||||
},
|
||||
Files: []domain.PluginLogicalFile{
|
||||
{Key: "scum-server-settings", DirectoryKey: "scum-config", Label: "ServerSettings.ini", Kind: "config", Editable: true},
|
||||
{Key: "scum-chat-log", DirectoryKey: "scum-logs", Label: "Chat.log", Kind: "log", StreamKey: "scum.chat"},
|
||||
},
|
||||
ConfigFields: []domain.PluginConfigField{
|
||||
{Key: "max-players", FileKey: "scum-server-settings", ConfigKey: "MaxPlayers", Label: "最大玩家数", Description: "玩家上限", Control: "number", Minimum: 1, Maximum: 128, DefaultValue: "128", RestartImpact: "restart-required"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createDeclaredFileReadJob(t *testing.T, svc *CoreService, instance domain.ServerInstance, endpoint domain.RunEndpoint, id string, state domain.JobState, minuteOffset int, content string) domain.Job {
|
||||
t.Helper()
|
||||
job, err := svc.CreateJob(domain.Job{
|
||||
ID: id,
|
||||
ServerInstanceID: instance.ID,
|
||||
RunEndpointID: endpoint.ID,
|
||||
Capability: domain.JobCapabilityFilesRead,
|
||||
TargetKey: "scum-server-settings",
|
||||
IdempotencyKey: id,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create declared file read job: %v", err)
|
||||
}
|
||||
stamp := fixedTime.Add(time.Duration(minuteOffset) * time.Minute)
|
||||
job.State = state
|
||||
job.UpdatedAt = stamp
|
||||
if state == domain.JobStateSucceeded || state == domain.JobStateFailed || state == domain.JobStateCancelled {
|
||||
job.TerminalAt = stamp
|
||||
}
|
||||
if state == domain.JobStateSucceeded {
|
||||
job.ExecutionResult = domain.JobExecutionResult{Kind: "file.read", Version: minuteOffset, Checksum: validator.BytesChecksum([]byte(content)), SizeBytes: int64(len(content)), Content: content}
|
||||
}
|
||||
if err := svc.store.Jobs().Update(job); err != nil {
|
||||
t.Fatalf("update declared file read job: %v", err)
|
||||
}
|
||||
return job
|
||||
}
|
||||
|
||||
func TestConfigWriteTerminalResultAppliesDurableTypedProjection(t *testing.T) {
|
||||
svc := newTestCoreService()
|
||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||
|
||||
Reference in New Issue
Block a user