197 lines
6.0 KiB
Go
197 lines
6.0 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"spark-mcp-go/internal/audit"
|
|
"spark-mcp-go/internal/storage"
|
|
"spark-mcp-go/internal/uploads"
|
|
)
|
|
|
|
func newTestAdminUploads(t *testing.T) (*gin.Engine, *storage.UploadRepo, *storage.SubmissionRepo, *audit.Repo, *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(), audit.NewRepo(db), &uploadStore, []string{"good-token"})
|
|
return r, db.Uploads(), db.Submissions(), audit.NewRepo(db), &uploadStore
|
|
}
|
|
|
|
func TestUploadsWeb_RequiresAuth(t *testing.T) {
|
|
r, _, _, _, _ := newTestAdminUploads(t)
|
|
|
|
w := doReq(t, r, "GET", "/admin/uploads", "", nil)
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("GET /admin/uploads without auth: got %d, want %d", w.Code, http.StatusUnauthorized)
|
|
}
|
|
|
|
w = doReq(t, r, "GET", "/admin/uploads", "good-token", nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("GET /admin/uploads 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, "uploads-body") {
|
|
t.Errorf("body missing uploads table")
|
|
}
|
|
}
|
|
|
|
func TestListUploads(t *testing.T) {
|
|
r, repo, _, _, _ := newTestAdminUploads(t)
|
|
ctx := context.Background()
|
|
|
|
uploadedAt := time.Unix(0, time.Now().UnixNano())
|
|
if err := repo.Create(ctx, "00000000000000000000000000000001", "alpha.txt", 10, "deadbeef", uploadedAt); err != nil {
|
|
t.Fatalf("create upload: %v", err)
|
|
}
|
|
if err := repo.Create(ctx, "00000000000000000000000000000002", "beta.txt", 20, "cafebabe", uploadedAt.Add(time.Second)); err != nil {
|
|
t.Fatalf("create upload: %v", err)
|
|
}
|
|
|
|
w := doReq(t, r, "GET", "/admin/uploads/api", "good-token", nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("got status %d, want %d", w.Code, http.StatusOK)
|
|
}
|
|
var list []map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
|
|
t.Fatalf("unmarshal list: %v", err)
|
|
}
|
|
if len(list) != 2 {
|
|
t.Errorf("got %d uploads, want 2", len(list))
|
|
}
|
|
|
|
w = doReq(t, r, "GET", "/admin/uploads/api?search=alpha", "good-token", nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("search: got status %d", w.Code)
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
|
|
func TestDeleteUpload(t *testing.T) {
|
|
r, repo, _, auditRepo, store := newTestAdminUploads(t)
|
|
ctx := context.Background()
|
|
|
|
id := "00000000000000000000000000000003"
|
|
dataPath := filepath.Join(store.Root, id)
|
|
metaPath := dataPath + ".meta.json"
|
|
|
|
if err := os.WriteFile(dataPath, []byte("payload"), 0o640); err != nil {
|
|
t.Fatalf("create data file: %v", err)
|
|
}
|
|
sc := map[string]any{
|
|
"name": "delete-me.txt",
|
|
"size": 7,
|
|
"sha256": "feedface",
|
|
"uploaded_at": time.Now().Format(time.RFC3339Nano),
|
|
}
|
|
b, _ := json.Marshal(sc)
|
|
if err := os.WriteFile(metaPath, b, 0o600); err != nil {
|
|
t.Fatalf("create sidecar: %v", err)
|
|
}
|
|
if err := repo.Create(ctx, id, "delete-me.txt", 7, "feedface", time.Now()); err != nil {
|
|
t.Fatalf("create db row: %v", err)
|
|
}
|
|
|
|
w := doReq(t, r, "DELETE", "/admin/uploads/"+id, "good-token", nil)
|
|
if w.Code != http.StatusNoContent {
|
|
t.Fatalf("delete: got status %d, want %d: %s", w.Code, http.StatusNoContent, w.Body.String())
|
|
}
|
|
|
|
if _, err := repo.Get(ctx, id); !errors.Is(err, storage.ErrNotFound) {
|
|
t.Errorf("db row still exists: %v", err)
|
|
}
|
|
if _, err := os.Stat(dataPath); !os.IsNotExist(err) {
|
|
t.Errorf("data file still exists")
|
|
}
|
|
if _, err := os.Stat(metaPath); !os.IsNotExist(err) {
|
|
t.Errorf("sidecar still exists")
|
|
}
|
|
|
|
entries, err := auditRepo.List(ctx, 100)
|
|
if err != nil {
|
|
t.Fatalf("list audit: %v", err)
|
|
}
|
|
found := false
|
|
for _, e := range entries {
|
|
if e.Action == audit.ActionUploadDelete && e.ClusterID == id {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("missing upload.delete audit entry")
|
|
}
|
|
}
|
|
|
|
func TestListUploads_SearchByAppID(t *testing.T) {
|
|
r, uploadRepo, subRepo, _, _ := newTestAdminUploads(t)
|
|
ctx := context.Background()
|
|
|
|
uploadedAt := time.Unix(0, time.Now().UnixNano())
|
|
if err := uploadRepo.Create(ctx, "00000000000000000000000000000010", "gamma.py", 10, "deadbeef", uploadedAt); err != nil {
|
|
t.Fatalf("create upload: %v", err)
|
|
}
|
|
if err := uploadRepo.Create(ctx, "00000000000000000000000000000011", "delta.py", 20, "cafebabe", uploadedAt.Add(time.Second)); err != nil {
|
|
t.Fatalf("create upload: %v", err)
|
|
}
|
|
|
|
// Only gamma.py has an associated submission; searching by app_id should
|
|
// still return it even though the filename doesn't match the query.
|
|
if err := subRepo.Create(ctx, storage.Submission{
|
|
FileID: "00000000000000000000000000000010",
|
|
AppID: "application_gamma_0001",
|
|
TrackingURL: "http://gamma",
|
|
ClusterID: "c1",
|
|
ExitCode: 0,
|
|
DurationMS: 100,
|
|
SubmittedAt: time.Now(),
|
|
}); err != nil {
|
|
t.Fatalf("create submission: %v", err)
|
|
}
|
|
|
|
w := doReq(t, r, "GET", "/admin/uploads/api?search=application_gamma", "good-token", nil)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("search by app_id: 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 by app_id result: got %d, want 1", len(list))
|
|
}
|
|
if len(list) > 0 && list[0]["file_id"] != "00000000000000000000000000000010" {
|
|
t.Errorf("file_id=%v, want upload 10", list[0]["file_id"])
|
|
}
|
|
}
|