package admin import ( "context" "encoding/json" "net/http" "os" "path/filepath" "strings" "testing" "time" "github.com/gin-gonic/gin" "spark-mcp-go/internal/storage" "spark-mcp-go/internal/uploads" ) func newTestAdminSubmissions(t *testing.T) (*gin.Engine, *storage.SubmissionRepo, *storage.UploadRepo, *uploads.Store) { t.Helper() db, err := storage.Open(":memory:") if err != nil { t.Fatalf("open: %v", err) } t.Cleanup(func() { _ = db.Close() }) uploadStore, err := uploads.New(filepath.Join(t.TempDir(), "uploads")) if err != nil { t.Fatalf("new upload store: %v", err) } r := gin.New() Mount(r, db.Clusters(), db.Uploads(), db.Submissions(), nil, &uploadStore, []string{"good-token"}) return r, db.Submissions(), db.Uploads(), &uploadStore } func TestSubmissionsWeb_RequiresAuth(t *testing.T) { r, _, _, _ := newTestAdminSubmissions(t) w := doReq(t, r, "GET", "/admin/submissions", "", nil) if w.Code != http.StatusUnauthorized { t.Errorf("GET /admin/submissions without auth: got %d, want %d", w.Code, http.StatusUnauthorized) } } func TestSubmissionsWeb_WithAuth(t *testing.T) { r, _, _, _ := newTestAdminSubmissions(t) w := doReq(t, r, "GET", "/admin/submissions", "good-token", nil) if w.Code != http.StatusOK { t.Fatalf("GET /admin/submissions with auth: got %d, want %d", w.Code, http.StatusOK) } ct := w.Header().Get("Content-Type") if !strings.Contains(ct, "text/html") { t.Errorf("Content-Type = %q, want text/html", ct) } body := w.Body.String() if !strings.Contains(body, "href=\"/admin/uploads\"") && !strings.Contains(body, "href='/admin/uploads'") { t.Errorf("body missing uploads nav link") } if !strings.Contains(body, "href=\"/admin/submissions\"") && !strings.Contains(body, "href='/admin/submissions'") { t.Errorf("body missing submissions nav link") } if !strings.Contains(body, "submissions-body") { t.Errorf("body missing submissions table") } } func TestListSubmissions_SearchByAppID(t *testing.T) { r, subRepo, uploadRepo, _ := newTestAdminSubmissions(t) ctx := context.Background() uploadedAt := time.Unix(0, time.Now().UnixNano()) if err := uploadRepo.Create(ctx, "00000000000000000000000000000001", "alpha.py", 10, "deadbeef", uploadedAt); err != nil { t.Fatalf("create upload: %v", err) } if err := uploadRepo.Create(ctx, "00000000000000000000000000000002", "beta.py", 20, "cafebabe", uploadedAt.Add(time.Second)); err != nil { t.Fatalf("create upload: %v", err) } submissions := []storage.Submission{ {FileID: "00000000000000000000000000000001", AppID: "application_111_0001", TrackingURL: "http://a", ClusterID: "c1", ExitCode: 0, DurationMS: 100, SubmittedAt: time.Now()}, {FileID: "00000000000000000000000000000002", AppID: "application_222_0001", TrackingURL: "http://b", ClusterID: "c1", ExitCode: 0, DurationMS: 100, SubmittedAt: time.Now().Add(time.Second)}, } for _, s := range submissions { if err := subRepo.Create(ctx, s); err != nil { t.Fatalf("create submission: %v", err) } } w := doReq(t, r, "GET", "/admin/submissions/api?search=application_111", "good-token", nil) if w.Code != http.StatusOK { t.Fatalf("search: got status %d: %s", w.Code, w.Body.String()) } var list []map[string]any if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil { t.Fatalf("unmarshal search: %v", err) } if len(list) != 1 { t.Errorf("search result: got %d, want 1", len(list)) } if len(list) > 0 && list[0]["app_id"] != "application_111_0001" { t.Errorf("app_id=%v, want application_111_0001", list[0]["app_id"]) } w = doReq(t, r, "GET", "/admin/submissions/api?search=beta.py", "good-token", nil) if w.Code != http.StatusOK { t.Fatalf("search by name: got status %d: %s", w.Code, w.Body.String()) } if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil { t.Fatalf("unmarshal search: %v", err) } if len(list) != 1 { t.Errorf("search by name result: got %d, want 1", len(list)) } } func TestDownloadUpload(t *testing.T) { r, _, uploadRepo, store := newTestAdminSubmissions(t) ctx := context.Background() id := "00000000000000000000000000000003" payload := []byte("hello payload") if err := uploadRepo.Create(ctx, id, "report.csv", int64(len(payload)), "feedface", time.Now()); err != nil { t.Fatalf("create upload: %v", err) } dataPath, err := store.AbsPath(id) if err != nil { t.Fatalf("abspath: %v", err) } if err := os.WriteFile(dataPath, payload, 0o640); err != nil { t.Fatalf("write data: %v", err) } w := doReq(t, r, "GET", "/admin/uploads/"+id+"/download", "good-token", nil) if w.Code != http.StatusOK { t.Fatalf("download: got status %d: %s", w.Code, w.Body.String()) } if string(w.Body.Bytes()) != string(payload) { t.Errorf("body = %q, want %q", w.Body.Bytes(), payload) } cd := w.Header().Get("Content-Disposition") if !strings.Contains(cd, `filename="report.csv"`) { t.Errorf("Content-Disposition = %q, want filename=\"report.csv\"", cd) } } func TestDownloadUpload_NotFound(t *testing.T) { r, _, _, _ := newTestAdminSubmissions(t) w := doReq(t, r, "GET", "/admin/uploads/00000000000000000000000000000099/download", "good-token", nil) if w.Code != http.StatusNotFound { t.Errorf("download unknown: got %d, want %d", w.Code, http.StatusNotFound) } }