Files
popiart-server/internal/server/artifact_list_test.go
T
wtgoku d0318292c1 Make artifact and media metadata first-class server records
Artifact and media metadata were previously reconstructed from job result
refs and JSON sidecar files. This change regularizes metadata into SQLite,
keeps filesystem blobs in place, and preserves backward compatibility via
lazy fallback and backfill from existing job refs and JSON metadata.

Constraint: Blob storage remains on the local filesystem in this phase
Rejected: Migrate blobs into SQLite | larger scope and worse operational profile for current media sizes
Rejected: Hard cutover without fallback | unsafe for historical data already on the test server
Confidence: medium
Scope-risk: moderate
Directive: Treat SQLite as the metadata source of truth; JSON sidecars are compatibility fallback only
Tested: go test ./...; deployed to test server 101.42.99.35; verified /v1/artifacts, /v1/artifacts/:id, signed media URL 200, unsigned content 401
Not-tested: Full historical backfill sweep over all existing artifact rows under production-sized data volume
2026-04-18 23:06:34 +08:00

153 lines
5.2 KiB
Go

package server
import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
)
func TestArtifactsListReturnsOnlyOwnerArtifacts(t *testing.T) {
cfg := Config{
SQLitePath: filepath.Join(t.TempDir(), "popiart.db"),
SkillhubDir: makeEmptySkillhub(t),
SessionSecret: "test-secret",
}
server, err := NewWithConfig(cfg)
if err != nil {
t.Fatalf("NewWithConfig: %v", err)
}
srv := httptest.NewServer(server.Handler())
defer srv.Close()
server.cfg.PublicBaseURL = srv.URL
ownerToken, _, ok, err := server.store.createSession("sk-artifacts-owner")
if err != nil {
t.Fatalf("createSession owner: %v", err)
}
if !ok {
t.Fatal("expected owner session creation to succeed")
}
otherToken, _, ok, err := server.store.createSession("sk-artifacts-other")
if err != nil {
t.Fatalf("createSession other: %v", err)
}
if !ok {
t.Fatal("expected other session creation to succeed")
}
ownerArtifact := uploadArtifactForListTest(t, server, ownerToken, "proj_owner", "owner.png")
otherArtifact := uploadArtifactForListTest(t, server, otherToken, "proj_other", "other.png")
req := httptest.NewRequest(http.MethodGet, "/v1/artifacts", nil)
req.Header.Set("Authorization", "Bearer "+ownerToken)
rec := httptest.NewRecorder()
server.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected list 200, got %d body=%s", rec.Code, rec.Body.String())
}
var envelope struct {
OK bool `json:"ok"`
Data struct {
Items []artifact `json:"items"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
} `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode artifacts list: %v body=%s", err, rec.Body.String())
}
if !envelope.OK {
t.Fatalf("expected ok artifacts list, got %s", rec.Body.String())
}
if envelope.Data.Total != 1 {
t.Fatalf("expected total 1, got %d payload=%s", envelope.Data.Total, rec.Body.String())
}
if len(envelope.Data.Items) != 1 {
t.Fatalf("expected one artifact, got %d payload=%s", len(envelope.Data.Items), rec.Body.String())
}
if envelope.Data.Items[0].ID != ownerArtifact.ID {
t.Fatalf("expected owner artifact %q, got %#v", ownerArtifact.ID, envelope.Data.Items[0])
}
if envelope.Data.Items[0].ID == otherArtifact.ID {
t.Fatalf("expected foreign artifact to be filtered out, got %#v", envelope.Data.Items[0])
}
filterReq := httptest.NewRequest(http.MethodGet, "/v1/artifacts?project_id=proj_owner", nil)
filterReq.Header.Set("Authorization", "Bearer "+ownerToken)
filterRec := httptest.NewRecorder()
server.Handler().ServeHTTP(filterRec, filterReq)
if filterRec.Code != http.StatusOK {
t.Fatalf("expected filtered list 200, got %d body=%s", filterRec.Code, filterRec.Body.String())
}
if err := json.Unmarshal(filterRec.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode filtered artifacts list: %v body=%s", err, filterRec.Body.String())
}
if envelope.Data.Total != 1 || len(envelope.Data.Items) != 1 {
t.Fatalf("expected one filtered owner artifact, got payload=%s", filterRec.Body.String())
}
foreignReq := httptest.NewRequest(http.MethodGet, "/v1/artifacts?job_id="+otherArtifact.JobID, nil)
foreignReq.Header.Set("Authorization", "Bearer "+ownerToken)
foreignRec := httptest.NewRecorder()
server.Handler().ServeHTTP(foreignRec, foreignReq)
if foreignRec.Code != http.StatusOK {
t.Fatalf("expected foreign job list 200, got %d body=%s", foreignRec.Code, foreignRec.Body.String())
}
if err := json.Unmarshal(foreignRec.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode foreign job artifacts list: %v body=%s", err, foreignRec.Body.String())
}
if envelope.Data.Total != 0 || len(envelope.Data.Items) != 0 {
t.Fatalf("expected foreign job artifacts to be hidden, got payload=%s", foreignRec.Body.String())
}
}
func uploadArtifactForListTest(t *testing.T, server *Server, sessionToken, projectID, filename string) artifact {
t.Helper()
imageBytes := tinyPNG(t)
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if projectID != "" {
if err := writer.WriteField("project_id", projectID); err != nil {
t.Fatalf("write project_id: %v", err)
}
}
part, err := writer.CreateFormFile("file", filename)
if err != nil {
t.Fatalf("create form file: %v", err)
}
if _, err := part.Write(imageBytes); err != nil {
t.Fatalf("write image bytes: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("close multipart writer: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/v1/artifacts/upload", &body)
req.Header.Set("Authorization", "Bearer "+sessionToken)
req.Header.Set("Content-Type", writer.FormDataContentType())
rec := httptest.NewRecorder()
server.Handler().ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("expected artifact upload 201, got %d body=%s", rec.Code, rec.Body.String())
}
var envelope struct {
OK bool `json:"ok"`
Data artifact `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode artifact upload: %v body=%s", err, rec.Body.String())
}
if !envelope.OK {
t.Fatalf("expected ok artifact upload, got %s", rec.Body.String())
}
return envelope.Data
}