- Test ListItemFiles, DeleteItemFile with real DB - Test cross-item file deletion guard (404) - Test storage-unavailable paths: presign, upload, associate, thumbnail (503) - Fix createItemDirect: StandardCost moved to revision properties
187 lines
5.9 KiB
Go
187 lines
5.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/kindredsystems/silo/internal/db"
|
|
)
|
|
|
|
// newFileRouter creates a chi router with file-related routes for testing.
|
|
func newFileRouter(s *Server) http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Route("/api/items/{partNumber}", func(r chi.Router) {
|
|
r.Get("/files", s.HandleListItemFiles)
|
|
r.Post("/files", s.HandleAssociateItemFile)
|
|
r.Delete("/files/{fileId}", s.HandleDeleteItemFile)
|
|
r.Put("/thumbnail", s.HandleSetItemThumbnail)
|
|
r.Post("/file", s.HandleUploadFile)
|
|
r.Get("/file/{revision}", s.HandleDownloadFile)
|
|
})
|
|
r.Post("/api/uploads/presign", s.HandlePresignUpload)
|
|
return r
|
|
}
|
|
|
|
// createFileDirect creates a file record directly via the DB for test setup.
|
|
func createFileDirect(t *testing.T, s *Server, itemID, filename string) *db.ItemFile {
|
|
t.Helper()
|
|
f := &db.ItemFile{
|
|
ItemID: itemID,
|
|
Filename: filename,
|
|
ContentType: "application/octet-stream",
|
|
Size: 1024,
|
|
ObjectKey: "items/" + itemID + "/files/" + filename,
|
|
}
|
|
if err := s.itemFiles.Create(context.Background(), f); err != nil {
|
|
t.Fatalf("creating file %s: %v", filename, err)
|
|
}
|
|
return f
|
|
}
|
|
|
|
func TestHandleListItemFiles(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newFileRouter(s)
|
|
|
|
createItemDirect(t, s, "FAPI-001", "file list item", nil)
|
|
item, _ := s.items.GetByPartNumber(context.Background(), "FAPI-001")
|
|
|
|
createFileDirect(t, s, item.ID, "drawing.pdf")
|
|
createFileDirect(t, s, item.ID, "model.step")
|
|
|
|
req := httptest.NewRequest("GET", "/api/items/FAPI-001/files", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusOK, w.Body.String())
|
|
}
|
|
|
|
var files []itemFileResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &files); err != nil {
|
|
t.Fatalf("decoding response: %v", err)
|
|
}
|
|
if len(files) != 2 {
|
|
t.Errorf("expected 2 files, got %d", len(files))
|
|
}
|
|
}
|
|
|
|
func TestHandleListItemFilesNotFound(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newFileRouter(s)
|
|
|
|
req := httptest.NewRequest("GET", "/api/items/NONEXISTENT/files", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status: got %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestHandleDeleteItemFile(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newFileRouter(s)
|
|
|
|
createItemDirect(t, s, "FDEL-API-001", "delete file item", nil)
|
|
item, _ := s.items.GetByPartNumber(context.Background(), "FDEL-API-001")
|
|
f := createFileDirect(t, s, item.ID, "removable.bin")
|
|
|
|
req := authRequest(httptest.NewRequest("DELETE", "/api/items/FDEL-API-001/files/"+f.ID, nil))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNoContent {
|
|
t.Errorf("status: got %d, want %d; body: %s", w.Code, http.StatusNoContent, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleDeleteItemFileCrossItem(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newFileRouter(s)
|
|
|
|
// Create two items, attach file to item A
|
|
createItemDirect(t, s, "CROSS-A", "item A", nil)
|
|
createItemDirect(t, s, "CROSS-B", "item B", nil)
|
|
itemA, _ := s.items.GetByPartNumber(context.Background(), "CROSS-A")
|
|
f := createFileDirect(t, s, itemA.ID, "belongs-to-a.pdf")
|
|
|
|
// Try to delete via item B — should fail
|
|
req := authRequest(httptest.NewRequest("DELETE", "/api/items/CROSS-B/files/"+f.ID, nil))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status: got %d, want %d", w.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestHandlePresignUploadNoStorage(t *testing.T) {
|
|
s := newTestServer(t) // storage is nil
|
|
router := newFileRouter(s)
|
|
|
|
body := `{"filename":"test.bin","content_type":"application/octet-stream","size":1024}`
|
|
req := authRequest(httptest.NewRequest("POST", "/api/uploads/presign", strings.NewReader(body)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("status: got %d, want %d; body: %s", w.Code, http.StatusServiceUnavailable, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleUploadFileNoStorage(t *testing.T) {
|
|
s := newTestServer(t) // storage is nil
|
|
router := newFileRouter(s)
|
|
|
|
createItemDirect(t, s, "UPNS-001", "upload no storage", nil)
|
|
|
|
req := authRequest(httptest.NewRequest("POST", "/api/items/UPNS-001/file", strings.NewReader("fake")))
|
|
req.Header.Set("Content-Type", "multipart/form-data")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("status: got %d, want %d; body: %s", w.Code, http.StatusServiceUnavailable, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleAssociateFileNoStorage(t *testing.T) {
|
|
s := newTestServer(t) // storage is nil
|
|
router := newFileRouter(s)
|
|
|
|
createItemDirect(t, s, "ASSNS-001", "associate no storage", nil)
|
|
|
|
body := `{"object_key":"uploads/tmp/abc/test.bin","filename":"test.bin"}`
|
|
req := authRequest(httptest.NewRequest("POST", "/api/items/ASSNS-001/files", strings.NewReader(body)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("status: got %d, want %d; body: %s", w.Code, http.StatusServiceUnavailable, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleSetThumbnailNoStorage(t *testing.T) {
|
|
s := newTestServer(t) // storage is nil
|
|
router := newFileRouter(s)
|
|
|
|
createItemDirect(t, s, "THNS-001", "thumbnail no storage", nil)
|
|
|
|
body := `{"object_key":"uploads/tmp/abc/thumb.png"}`
|
|
req := authRequest(httptest.NewRequest("PUT", "/api/items/THNS-001/thumbnail", strings.NewReader(body)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("status: got %d, want %d; body: %s", w.Code, http.StatusServiceUnavailable, w.Body.String())
|
|
}
|
|
}
|