admin: submissions page, download endpoint, uploads search by app_id

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
tao.chen
2026-07-14 13:45:11 +08:00
co-authored by Claude
parent 573397dbb4
commit 25f93dfb59
10 changed files with 683 additions and 26 deletions
+155
View File
@@ -0,0 +1,155 @@
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)
}
}