Add SCUM file management workbench

This commit is contained in:
npc0-hue
2026-08-04 11:33:34 +08:00
parent 2921edb401
commit f028a343d7
36 changed files with 1384 additions and 158 deletions
+99
View File
@@ -301,6 +301,105 @@ func TestConfigWriteAndFileDispatchAPIAreScopedAndSafe(t *testing.T) {
}
}
func TestCoreAPIDeclaredFileReadSnapshotRouteIsScopedAndRedacted(t *testing.T) {
router := newTestRouter()
adminSession := createAdminSession(t, router)
postJSONWithAuth[dto.UserResponse](t, router, "/api/v1/users", dto.UserCreateRequest{
ID: "user-owner-file-snapshot-api",
DisplayName: "File Snapshot API Owner",
Email: "owner-file-snapshot-api@example.test",
Roles: []string{"server-owner"},
Password: "secret-password",
}, adminSession)
postJSONWithAuth[dto.UserResponse](t, router, "/api/v1/users", dto.UserCreateRequest{
ID: "user-other-file-snapshot-api",
DisplayName: "File Snapshot API Other",
Email: "other-file-snapshot-api@example.test",
Roles: []string{"server-admin"},
Password: "secret-password",
}, adminSession)
ownerSession := postOKJSON[dto.AuthSessionResponse](t, router, "/api/v1/auth/login", dto.LoginRequest{Account: "owner-file-snapshot-api@example.test", Password: "secret-password"}).SessionID
otherSession := postOKJSON[dto.AuthSessionResponse](t, router, "/api/v1/auth/login", dto.LoginRequest{Account: "other-file-snapshot-api@example.test", Password: "secret-password"}).SessionID
pluginRequest := validGamePluginRequest()
pluginRequest.RequiredRunCapabilities = append(pluginRequest.RequiredRunCapabilities, domain.JobCapabilityFilesRead)
pluginRequest.DeclaredPermissions = []string{"server.files.read", "server.files.write"}
pluginRequest.Permissions.Files = true
pluginRequest.FileWorkspace = dto.PluginFileWorkspaceBody{
DefaultDirectoryKey: "scum-config",
Directories: []dto.PluginLogicalDirectoryBody{
{Key: "scum-config", Label: "服务器配置", Scope: "config"},
{Key: "scum-logs", Label: "日志文件", Scope: "logs"},
},
Files: []dto.PluginLogicalFileBody{
{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: []dto.PluginConfigFieldBody{
{Key: "max-players", FileKey: "scum-server-settings", ConfigKey: "MaxPlayers", Label: "最大玩家数", Description: "玩家上限", Control: "number", Minimum: 1, Maximum: 128, DefaultValue: "128", RestartImpact: "restart-required"},
},
}
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", pluginRequest)
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", validRunEndpointRequest())
instance := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
ID: "server-file-snapshot-api",
PluginID: "server.scum",
RunEndpointID: "run-local",
Name: "File Snapshot API Server",
State: domain.ServerInstanceStateRunning,
}, ownerSession)
snapshot := getJSONWithAuth[dto.DeclaredFileReadSnapshotResponse](t, router, "/api/v1/server-instances/"+instance.ID+"/files/read-snapshot?key=scum-server-settings", ownerSession)
if snapshot.State != "not-read" || snapshot.Content != "" {
t.Fatalf("expected not-read snapshot, got %+v", snapshot)
}
assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/"+instance.ID+"/files/read-snapshot?key=logs/latest.log", "", ownerSession), http.StatusBadRequest, errorCodeValidation)
assertErrorResponse(t, requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/"+instance.ID+"/files/read-snapshot?key=scum-server-settings", "", otherSession), http.StatusForbidden, errorCodeForbidden)
postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{
ID: "job-file-snapshot-api-read",
ServerInstanceID: instance.ID,
RunEndpointID: "run-local",
Capability: domain.JobCapabilityFilesRead,
TargetKey: "scum-server-settings",
IdempotencyKey: "idem-file-snapshot-api-read",
})
helloRequest := validRunControlHelloRequest()
helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.JobCapabilityFilesRead)
hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, helloRequest))
claim := decodeBody[dto.RunJobClaimResponse](t, performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/claim", dto.RunJobClaimRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
Capabilities: []string{domain.JobCapabilityFilesRead},
Capacity: dto.RunCapacityResponse{MaxJobs: 4},
}))
if !claim.HasJob || claim.Job.JobID != "job-file-snapshot-api-read" {
t.Fatalf("expected file read job claim, got %+v", claim)
}
content := "ServerName=API\nRconPassword=secret\n"
resultRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/jobs/result", dto.RunJobResultRequest{
RunEndpointID: "run-local",
SessionToken: hello.SessionToken,
JobID: claim.Job.JobID,
LeaseToken: claim.Job.LeaseToken,
Attempt: claim.Job.Attempt,
State: domain.JobStateSucceeded,
Progress: dto.JobProgressBody{Percent: 100, Message: "file read completed"},
Message: "file read completed",
ExecutionResult: dto.RunJobExecutionResultBody{
Kind: "file.read",
Version: 9,
SizeBytes: int64(len(content)),
Content: content,
},
})
assertStatus(t, resultRecorder, http.StatusOK)
ready := getJSONWithAuth[dto.DeclaredFileReadSnapshotResponse](t, router, "/api/v1/server-instances/"+instance.ID+"/files/read-snapshot?key=scum-server-settings", ownerSession)
if ready.State != "ready" || ready.Version != 9 || !strings.Contains(ready.Content, "ServerName=API") || !strings.Contains(ready.Content, "RconPassword=<redacted>") || strings.Contains(ready.Content, "secret") {
t.Fatalf("expected ready redacted snapshot, got %+v", ready)
}
}
func TestCoreAPIServerRuntimeDistributionAndJobWorkflows(t *testing.T) {
releaseBuilds := make(chan struct{})
t.Cleanup(func() { close(releaseBuilds) })