package api import ( "bytes" "net/http" "net/http/httptest" "strconv" "strings" "testing" "browser.local/platform/domain" "browser.local/platform/dto" "browser.local/platform/validator" ) func TestArtifactDownloadAPIWorkflowIsPlatformMediated(t *testing.T) { router := newTestRouter() adminSession := createAdminSession(t, router) postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest()) hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, artifactDownloadHelloRequest())) postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-download-api", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Download API"}, adminSession) postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{ID: "job-download-api", ServerInstanceID: "server-download-api", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-download-api"}) payload := []byte("artifact payload for browser mediated download") uploadCompletedArtifact(t, router, hello.SessionToken, "artifact-download-api", "job-download-api", payload, 9) reference := postOKJSONWithAuth[dto.ArtifactDownloadReferenceResponse](t, router, "/api/v1/artifacts/artifact-download-api/download", map[string]string{}, adminSession) if reference.ArtifactID != "artifact-download-api" || reference.DownloadURL != "/api/v1/artifacts/artifact-download-api/content" || !reference.RangeSupported || reference.ChunkSizeBytes != validator.MaxArtifactDownloadBytes { t.Fatalf("unexpected artifact reference: %+v", reference) } if reference.Checksum != validator.BytesChecksum(payload) || reference.SizeBytes != int64(len(payload)) || reference.StorageBehavior == "" { t.Fatalf("expected integrity metadata in reference, got %+v", reference) } contentRecorder := requestWithAuth(t, router, http.MethodGet, "/api/v1/artifacts/artifact-download-api/content?offset=9&limit=7", "", adminSession) assertStatus(t, contentRecorder, http.StatusPartialContent) if got, want := contentRecorder.Body.Bytes(), payload[9:16]; !bytes.Equal(got, want) { t.Fatalf("expected range payload %q, got %q", want, got) } if contentRecorder.Header().Get("Content-Range") != "bytes 9-15/46" || contentRecorder.Header().Get("X-Artifact-Checksum") != validator.BytesChecksum(payload) || contentRecorder.Header().Get("X-Artifact-Content-Checksum") != validator.BytesChecksum(payload[9:16]) { t.Fatalf("expected safe integrity headers, got %+v", contentRecorder.Header()) } rangeRequest := httptest.NewRequest(http.MethodGet, "/api/v1/artifacts/artifact-download-api/content", nil) rangeRequest.Header.Set("Authorization", "Bearer "+adminSession) rangeRequest.Header.Set("Range", "bytes=0-7") rangeRecorder := httptest.NewRecorder() router.ServeHTTP(rangeRecorder, rangeRequest) assertStatus(t, rangeRecorder, http.StatusPartialContent) if !bytes.Equal(rangeRecorder.Body.Bytes(), payload[:8]) { t.Fatalf("expected range header payload, got %q", rangeRecorder.Body.String()) } for _, body := range []string{mustJSON(t, reference), contentRecorder.Header().Get("Content-Disposition"), contentRecorder.Header().Get("X-Artifact-Storage")} { assertNoArtifactForbiddenFragments(t, body) } } func TestArtifactContentWithoutRangeReturnsWholePayload(t *testing.T) { router := newTestRouter() adminSession := createAdminSession(t, router) postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest()) hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, artifactDownloadHelloRequest())) postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-download-full", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Download Full"}, adminSession) postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{ID: "job-download-full", ServerInstanceID: "server-download-full", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-download-full"}) payload := bytes.Repeat([]byte("full-download-payload-"), (validator.MaxArtifactDownloadBytes/len("full-download-payload-"))+2) uploadCompletedArtifact(t, router, hello.SessionToken, "artifact-download-full", "job-download-full", payload, validator.MaxArtifactChunkBytes) contentRecorder := requestWithAuth(t, router, http.MethodGet, "/api/v1/artifacts/artifact-download-full/content", "", adminSession) assertStatus(t, contentRecorder, http.StatusOK) if got := contentRecorder.Body.Bytes(); !bytes.Equal(got, payload) { t.Fatalf("expected whole payload length %d, got %d", len(payload), len(got)) } if contentRecorder.Header().Get("Content-Range") != "" || contentRecorder.Header().Get("Content-Length") != strconv.Itoa(len(payload)) { t.Fatalf("expected full content headers, got %+v", contentRecorder.Header()) } } func TestArtifactDownloadAPIDeniesUnavailableAndUnauthorizedArtifacts(t *testing.T) { router := newTestRouter() adminSession := createAdminSession(t, router) postJSONWithAuth[dto.UserResponse](t, router, "/api/v1/users", dto.UserCreateRequest{ ID: "user-download-other", DisplayName: "Other Operator", Email: "download-other@example.test", Roles: []string{"server-admin"}, Password: "secret-password", }, adminSession) otherSession := postOKJSON[dto.AuthSessionResponse](t, router, "/api/v1/auth/login", dto.LoginRequest{Account: "download-other@example.test", Password: "secret-password"}).SessionID postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins", validGamePluginRequest()) hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, artifactDownloadHelloRequest())) postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-download-denied", PluginID: "server.scum", RunEndpointID: "run-local", Name: "Download Denied"}, adminSession) postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{ID: "job-download-denied", ServerInstanceID: "server-download-denied", RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-download-denied"}) payload := []byte("download denied payload") uploadCompletedArtifact(t, router, hello.SessionToken, "artifact-download-denied", "job-download-denied", payload, 8) unauthorized := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/artifacts/artifact-download-denied/download", map[string]string{}, otherSession) assertErrorResponse(t, unauthorized, http.StatusForbidden, errorCodeForbidden) unauthorizedContent := requestWithAuth(t, router, http.MethodGet, "/api/v1/artifacts/artifact-download-denied/content?limit=8", "", otherSession) assertErrorResponse(t, unauthorizedContent, http.StatusForbidden, errorCodeForbidden) unavailable := postJSON[dto.ArtifactResponse](t, router, "/api/v1/artifacts", dto.ArtifactCreateRequest{ID: "artifact-uploading-denied", OwnerKind: domain.ArtifactOwnerKindJob, OwnerID: "job-download-denied", SizeBytes: 12, Checksum: validator.BytesChecksum([]byte("not-complete!"))}) if unavailable.State != domain.ArtifactStateUploading { t.Fatalf("expected uploading metadata, got %+v", unavailable) } unavailableDownload := requestJSONWithAuth(t, router, http.MethodPost, "/api/v1/artifacts/artifact-uploading-denied/download", map[string]string{}, adminSession) assertErrorResponse(t, unavailableDownload, http.StatusBadRequest, errorCodeValidation) for _, body := range []string{unauthorized.Body.String(), unauthorizedContent.Body.String(), unavailableDownload.Body.String()} { assertNoArtifactForbiddenFragments(t, body) } } func TestPluginBridgeArtifactOpenReturnsSafeReference(t *testing.T) { router := newTestRouter() adminSession := createAdminSession(t, router) hello := decodeBody[dto.RunControlHelloResponse](t, performRunControlHello(t, router, artifactDownloadHelloRequest())) registration := validGamePluginManifestRegistrationRequest() registration.Manifest.Bridge.Actions = []string{string(domain.PluginBridgeActionServerInstancesRead), string(domain.PluginBridgeActionArtifactsOpen)} registration.Manifest.Pages[0].Permissions = []string{"server.read", "server.artifacts.read"} registration.Manifest.Pages[0].BridgeActions = []string{string(domain.PluginBridgeActionServerInstancesRead), string(domain.PluginBridgeActionArtifactsOpen)} postJSON[dto.GamePluginResponse](t, router, "/api/v1/game-plugins/register-manifest", registration) instance := postJSONWithAuth[dto.ServerInstanceResponse](t, router, "/api/v1/server-instances", dto.ServerInstanceCreateRequest{ID: "server-bridge-artifact", PluginID: "game.example", RunEndpointID: "run-local", Name: "Bridge Artifact"}, adminSession) postJSON[dto.JobResponse](t, router, "/api/v1/jobs", dto.JobCreateRequest{ID: "job-bridge-artifact", ServerInstanceID: instance.ID, RunEndpointID: "run-local", Capability: "process.start", IdempotencyKey: "idem-bridge-artifact"}) payload := []byte("bridge artifact reference payload") uploadCompletedArtifact(t, router, hello.SessionToken, "artifact-bridge-open", "job-bridge-artifact", payload, 8) bridge := postOKJSONWithAuth[dto.PluginBridgeExecuteResponse](t, router, "/api/v1/plugin-bridge/execute", dto.PluginBridgeExecuteRequest{ RequestID: "bridge-artifact-open", PluginID: "game.example", RouteKey: "logs", ServerInstanceID: instance.ID, Action: string(domain.PluginBridgeActionArtifactsOpen), Payload: map[string]string{"artifactId": "artifact-bridge-open"}, }, adminSession) if bridge.Status != "ok" || bridge.Result["downloadUrl"] != "/api/v1/artifacts/artifact-bridge-open/content" || bridge.Result["sizeBytes"] == "" || bridge.Result["checksum"] != validator.BytesChecksum(payload) { t.Fatalf("expected safe artifact reference through bridge, got %+v", bridge) } assertNoArtifactForbiddenFragments(t, mustJSON(t, bridge)) } func uploadCompletedArtifact(t *testing.T, router http.Handler, sessionToken string, artifactID string, jobID string, payload []byte, chunkSize int) dto.ArtifactTransferCompleteResponse { t.Helper() open := dto.ArtifactTransferOpenRequest{ RunEndpointID: "run-local", SessionToken: sessionToken, ArtifactID: artifactID, Direction: domain.ArtifactTransferDirectionUpload, OwnerKind: domain.ArtifactOwnerKindJob, OwnerID: jobID, SizeBytes: int64(len(payload)), ChunkSizeBytes: chunkSize, Checksum: validator.BytesChecksum(payload), IdempotencyKey: artifactID + "-upload", } opened := decodeBody[dto.ArtifactTransferOpenResponse](t, performArtifactTransferOpen(t, router, open)) for index := 0; index < opened.TotalChunks; index++ { offset := index * chunkSize end := offset + chunkSize if end > len(payload) { end = len(payload) } part := payload[offset:end] chunk := dto.ArtifactChunkUploadRequest{ RunEndpointID: "run-local", SessionToken: sessionToken, TransferID: opened.TransferID, ArtifactID: artifactID, ChunkIndex: index, Offset: int64(offset), SizeBytes: len(part), Checksum: validator.BytesChecksum(part), Payload: part, } assertStatus(t, performArtifactChunkUpload(t, router, chunk), http.StatusOK) } completeRecorder := performJSON(t, router, http.MethodPost, "/api/v1/run/artifacts/complete", dto.ArtifactTransferCompleteRequest{RunEndpointID: "run-local", SessionToken: sessionToken, TransferID: opened.TransferID, ArtifactID: artifactID, Checksum: validator.BytesChecksum(payload), SizeBytes: int64(len(payload))}) assertStatus(t, completeRecorder, http.StatusOK) return decodeBody[dto.ArtifactTransferCompleteResponse](t, completeRecorder) } func artifactDownloadHelloRequest() dto.RunControlHelloRequest { request := validRunControlHelloRequest() request.CapabilityReport.Capabilities = []string{"control.hello", "control.heartbeat", "process.install", "process.start", "process.stop", "logs.read", "files.read", "artifacts.read", "ai.invoke"} request.CapabilityReport.Fingerprint = "cap-artifact-download" return request } func assertNoArtifactForbiddenFragments(t *testing.T, body string) { t.Helper() for _, forbidden := range []string{"/Users/", "/private/", "unix://", "tcp://", "Bearer ", "sk-", "password=", "apiKeyRef", "rawApiKey", "storage://", "file://"} { if strings.Contains(body, forbidden) { t.Fatalf("artifact response exposed forbidden fragment %q: %s", forbidden, body) } } }