功能修改
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -391,6 +392,37 @@ func TestCoreAPIServerRuntimeDistributionAndJobWorkflows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreAPIRunDistributionDenialNamesMissingPluginPermission(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins/register-manifest", validGamePluginManifestRegistrationRequest())
|
||||
postJSON[dto.RunEndpointResponse](t, router, "/api/v1/run/endpoints", validRunEndpointRequest())
|
||||
server := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{
|
||||
ID: "server-plugin-denied",
|
||||
PluginID: "game.example",
|
||||
RunEndpointID: "run-local",
|
||||
Name: "Plugin Denied",
|
||||
}, adminSession)
|
||||
|
||||
recorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/server-instances/"+server.ID+"/run/generate", dto.RunDistributionGenerateRequest{
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
IdempotencyKey: "missing-plugin-permission",
|
||||
}, adminSession)
|
||||
assertStatus(t, recorder, http.StatusForbidden)
|
||||
body := decodeBody[dto.ErrorResponse](t, recorder)
|
||||
if body.Code != errorCodeForbidden || body.Message != "plugin does not declare required permission: server.run.distribution" {
|
||||
t.Fatalf("expected plugin permission denial, got %+v", body)
|
||||
}
|
||||
|
||||
dependenciesRecorder := requestWithAuth(t, router, http.MethodGet, "/api/v1/server-instances/"+server.ID+"/dependencies", "", adminSession)
|
||||
assertStatus(t, dependenciesRecorder, http.StatusForbidden)
|
||||
dependenciesBody := decodeBody[dto.ErrorResponse](t, dependenciesRecorder)
|
||||
if dependenciesBody.Code != errorCodeForbidden || dependenciesBody.Message != "plugin does not declare required permission: server.dependencies.manage" {
|
||||
t.Fatalf("expected dependency permission denial, got %+v", dependenciesBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreAPIErrorResponses(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
@@ -817,7 +849,7 @@ func TestServerAccessAPIScopesOwnersAndAdministrators(t *testing.T) {
|
||||
assertErrorResponse(t, forbiddenDetail, http.StatusForbidden, errorCodeForbidden)
|
||||
}
|
||||
|
||||
func TestAIProviderAPIResponseDoesNotExposeRawKeyFields(t *testing.T) {
|
||||
func TestAIProviderAPIResponseDoesNotExposeRawSecretFields(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
recorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/ai-providers", validAIProviderRequest(), adminSession)
|
||||
@@ -836,6 +868,9 @@ func TestAIProviderAPIResponseDoesNotExposeRawKeyFields(t *testing.T) {
|
||||
if _, exists := body["apiKeyRef"]; exists || body["apiKeyConfigured"] != true {
|
||||
t.Fatalf("expected API key presence only, got %+v", body)
|
||||
}
|
||||
if _, exists := body["baseUrl"]; exists || body["baseUrlConfigured"] != true {
|
||||
t.Fatalf("expected base URL presence only, got %+v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIProviderManagementAPI(t *testing.T) {
|
||||
@@ -844,13 +879,13 @@ func TestAIProviderManagementAPI(t *testing.T) {
|
||||
createAIProviderFixture(t, router, adminSession)
|
||||
|
||||
update := validAIProviderUpdateRequest()
|
||||
update.BaseURL = ""
|
||||
updatedRecorder := requestJSONWithAuth(t, router, http.MethodPut, "/api/v1/ai-providers/ai.openai", update, adminSession)
|
||||
assertStatus(t, updatedRecorder, http.StatusOK)
|
||||
updated := decodeBody[dto.AIProviderResponse](t, updatedRecorder)
|
||||
if updated.Name != "OpenAI Relay" || !updated.APIKeyConfigured || updated.Status != domain.AIProviderStatusActive {
|
||||
if updated.Name != "OpenAI Relay" || !updated.BaseURLConfigured || !updated.APIKeyConfigured || updated.Status != domain.AIProviderStatusActive {
|
||||
t.Fatalf("unexpected updated provider: %+v", updated)
|
||||
}
|
||||
|
||||
statusRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/ai-providers/ai.openai/status", dto.AIProviderStatusRequest{Status: domain.AIProviderStatusDisabled}, adminSession)
|
||||
assertStatus(t, statusRecorder, http.StatusOK)
|
||||
disabled := decodeBody[dto.AIProviderResponse](t, statusRecorder)
|
||||
@@ -861,7 +896,7 @@ func TestAIProviderManagementAPI(t *testing.T) {
|
||||
testRecorder := requestWithAuth(t, router, http.MethodPost, "/api/v1/ai-providers/ai.openai/test", "", adminSession)
|
||||
assertStatus(t, testRecorder, http.StatusOK)
|
||||
testResult := decodeBody[dto.AIProviderTestResponse](t, testRecorder)
|
||||
if testResult.Success || testResult.Mode != "metadata" {
|
||||
if testResult.Success || testResult.Mode != "provider" {
|
||||
t.Fatalf("expected metadata test failure for disabled provider, got %+v", testResult)
|
||||
}
|
||||
|
||||
@@ -1408,6 +1443,56 @@ func TestGamePluginRegistryResponseDoesNotExposeRawInternals(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionOperationsGovernanceRoutesAreDurableAndRedacted(t *testing.T) {
|
||||
router := newTestRouter()
|
||||
adminSession := createAdminSession(t, router)
|
||||
serverID := createRuntimeAPIFixtures(t, router, adminSession)
|
||||
createAIProviderFixture(t, router, adminSession)
|
||||
|
||||
capacity := getJSONWithAuth[dto.ProductionCapacitySummaryResponse](t, router, "/api/v1/production/capacity", adminSession)
|
||||
if len(capacity.Endpoints) == 0 {
|
||||
t.Fatalf("expected persisted capacity endpoints, got %+v", capacity)
|
||||
}
|
||||
decision := postOKJSONWithAuth[dto.CapacityAdmissionDecisionResponse](t, router, "/api/v1/production/capacity/admission", dto.CapacityAdmissionRequest{ServerInstanceID: serverID, Capability: domain.JobCapabilityConfigWrite, IdempotencyKey: "api-capacity-check"}, adminSession)
|
||||
if decision.Accepted || decision.AlertID == "" || decision.AuditEventID == "" {
|
||||
t.Fatalf("expected stale endpoint admission to create durable evidence, got %+v", decision)
|
||||
}
|
||||
alerts := getJSONWithAuth[dto.AlertListResponse](t, router, "/api/v1/alerts?state=active", adminSession)
|
||||
if alerts.Count == 0 {
|
||||
t.Fatalf("expected durable alert list, got %+v", alerts)
|
||||
}
|
||||
acknowledged := postOKJSONWithAuth[dto.AlertResponse](t, router, "/api/v1/alerts/"+decision.AlertID+"/acknowledge", dto.AlertAcknowledgeRequest{Note: "operator review"}, adminSession)
|
||||
if acknowledged.State != string(domain.AlertStateAcknowledged) {
|
||||
t.Fatalf("expected acknowledged state, got %+v", acknowledged)
|
||||
}
|
||||
resolved := postOKJSONWithAuth[dto.AlertResponse](t, router, "/api/v1/alerts/"+decision.AlertID+"/resolve", dto.AlertResolveRequest{Note: "review complete"}, adminSession)
|
||||
if resolved.State != string(domain.AlertStateResolved) {
|
||||
t.Fatalf("expected resolved state, got %+v", resolved)
|
||||
}
|
||||
|
||||
invocation := postOKJSONWithAuth[dto.AIInvocationResponse](t, router, "/api/v1/ai/invocations", dto.AIInvocationRequest{RequestID: "api-ai-config-diff", ServerInstanceID: serverID, Purpose: "config.suggest", ProviderID: "ai.openai", Prompt: "disable pvp"}, adminSession)
|
||||
if invocation.ConfigRecommendation == nil || invocation.ConfigRecommendation.DiffID == "" {
|
||||
t.Fatalf("expected persisted AI config diff, got %+v", invocation)
|
||||
}
|
||||
diffs := getJSONWithAuth[dto.AIConfigDiffListResponse](t, router, "/api/v1/ai/config-diffs?serverInstanceId="+serverID, adminSession)
|
||||
if diffs.Count != 1 || diffs.Items[0].State != string(domain.AIConfigDiffStatePending) {
|
||||
t.Fatalf("expected one pending AI diff, got %+v", diffs)
|
||||
}
|
||||
approvalRecorder := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/ai/config-diffs/"+invocation.ConfigRecommendation.DiffID+"/approve", dto.AIConfigDiffApprovalRequest{IdempotencyKey: "api-ai-diff-approve"}, adminSession)
|
||||
assertStatus(t, approvalRecorder, http.StatusAccepted)
|
||||
approval := decodeBody[dto.AIConfigDiffApprovalResponse](t, approvalRecorder)
|
||||
if approval.Preview.State != string(domain.AIConfigDiffStateApproved) || approval.Dispatch.Job.Capability != domain.JobCapabilityConfigWrite {
|
||||
t.Fatalf("expected approved diff with config write job, got %+v", approval)
|
||||
}
|
||||
|
||||
evidence := fmt.Sprintf("%+v %+v %+v %+v %+v %+v", capacity, decision, alerts, resolved, diffs, approval)
|
||||
for _, forbidden := range []string{"/Users/", "/private/", "unix://", "tcp://", "Bearer ", "sk-", "password=", "apiKeyRef", "rawApiKey", "https://api.openai.com"} {
|
||||
if strings.Contains(evidence, forbidden) {
|
||||
t.Fatalf("production governance response leaked forbidden fragment %q: %s", forbidden, evidence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTestRouter() http.Handler {
|
||||
core := service.NewCoreService(repo.NewMemoryStore())
|
||||
if err := core.SeedLocalPlatformAdmin(); err != nil {
|
||||
@@ -1613,6 +1698,7 @@ func createRuntimeAPIFixtures(t *testing.T, router http.Handler, adminSession st
|
||||
"process.start",
|
||||
"process.stop",
|
||||
"logs.read",
|
||||
domain.JobCapabilityConfigWrite,
|
||||
domain.JobCapabilityRunSelfUpdate,
|
||||
domain.JobCapabilityDependenciesCheck,
|
||||
domain.JobCapabilityDependenciesInstall,
|
||||
@@ -1771,8 +1857,9 @@ func validGamePluginManifestRegistrationRequest() dto.GamePluginManifestRegistra
|
||||
BridgeActions: []string{string(domain.PluginBridgeActionLogsQuery), string(domain.PluginBridgeActionFilesRequest), string(domain.PluginBridgeActionAIInvoke)},
|
||||
},
|
||||
},
|
||||
AI: dto.GamePluginManifestAIBody{Purposes: []string{"logs.diagnose"}},
|
||||
RuntimeProfiles: dto.GamePluginRuntimeProfilesBody{LifecycleProfiles: []dto.RuntimeLifecycleProfileBody{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
|
||||
AI: dto.GamePluginManifestAIBody{Purposes: []string{"logs.diagnose"}, Mediation: "platform", ConfigWritePolicy: "review-required"},
|
||||
ProductionLifecycle: dto.GamePluginProductionLifecycleBody{Operations: []string{"install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"}, DependencyPolicy: "optional", ApprovalRequired: []string{"disable", "rollback", "retire"}},
|
||||
RuntimeProfiles: dto.GamePluginRuntimeProfilesBody{LifecycleProfiles: []dto.RuntimeLifecycleProfileBody{{Key: "local", Mode: "local-process", Capabilities: []string{"process.install", "process.start", "process.stop"}}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user