Add 56 tests covering the core backend packages: Unit tests (no database required): - internal/partnum: 7 tests for part number generation logic (sequence, format templates, enum validation, constants) - internal/schema: 8 tests for YAML schema loading, property merging, validation, and default application Integration tests (require TEST_DATABASE_URL): - internal/db/items: 10 tests for item CRUD, archive/unarchive, revisions, and thumbnail operations - internal/db/relationships: 10 tests for BOM CRUD, cycle detection, self-reference blocking, where-used, expanded/flat BOM - internal/db/projects: 5 tests for project CRUD and item association - internal/api/bom_handlers: 6 HTTP handler tests for BOM endpoints including flat BOM, cost calculation, add/delete entries - internal/api/items: 5 HTTP handler tests for item CRUD endpoints Infrastructure: - internal/testutil: shared helpers for test DB pool setup, migration runner, and table truncation - internal/db/helpers_test.go: DB wrapper for integration tests - internal/db/db.go: add NewFromPool constructor - Makefile: add test-integration target with default DSN Integration tests skip gracefully when TEST_DATABASE_URL is unset. Dev-mode auth (nil authConfig) used for API handler tests. Fixes: fmt.Errorf Go vet warning in partnum/generator.go Closes #2
134 lines
3.5 KiB
Go
134 lines
3.5 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// newItemRouter creates a chi router with item routes for testing.
|
|
func newItemRouter(s *Server) http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Route("/api/items", func(r chi.Router) {
|
|
r.Get("/", s.HandleListItems)
|
|
r.Post("/", s.HandleCreateItem)
|
|
r.Route("/{partNumber}", func(r chi.Router) {
|
|
r.Get("/", s.HandleGetItem)
|
|
r.Put("/", s.HandleUpdateItem)
|
|
r.Delete("/", s.HandleDeleteItem)
|
|
})
|
|
})
|
|
return r
|
|
}
|
|
|
|
func TestHandleCreateItem(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newItemRouter(s)
|
|
|
|
body := `{"part_number":"NEW-001","item_type":"part","description":"new item"}`
|
|
req := authRequest(httptest.NewRequest("POST", "/api/items", strings.NewReader(body)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("status: got %d, want %d; body: %s", w.Code, http.StatusCreated, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decoding response: %v", err)
|
|
}
|
|
if resp["part_number"] != "NEW-001" {
|
|
t.Errorf("part_number: got %v, want %q", resp["part_number"], "NEW-001")
|
|
}
|
|
}
|
|
|
|
func TestHandleGetItem(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newItemRouter(s)
|
|
|
|
createItemDirect(t, s, "GET-001", "get test", nil)
|
|
|
|
req := httptest.NewRequest("GET", "/api/items/GET-001", 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 resp map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if resp["part_number"] != "GET-001" {
|
|
t.Errorf("part_number: got %v, want %q", resp["part_number"], "GET-001")
|
|
}
|
|
}
|
|
|
|
func TestHandleGetItemNotFound(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newItemRouter(s)
|
|
|
|
req := httptest.NewRequest("GET", "/api/items/NOPE-999", 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 TestHandleListItems(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newItemRouter(s)
|
|
|
|
createItemDirect(t, s, "LST-001", "list item 1", nil)
|
|
createItemDirect(t, s, "LST-002", "list item 2", nil)
|
|
|
|
req := httptest.NewRequest("GET", "/api/items", 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 resp map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
items, ok := resp["items"].([]any)
|
|
if !ok {
|
|
t.Fatalf("expected items array in response, got: %s", w.Body.String())
|
|
}
|
|
if len(items) < 2 {
|
|
t.Errorf("expected at least 2 items, got %d", len(items))
|
|
}
|
|
}
|
|
|
|
func TestHandleDeleteItem(t *testing.T) {
|
|
s := newTestServer(t)
|
|
router := newItemRouter(s)
|
|
|
|
createItemDirect(t, s, "DEL-ITEM-001", "deletable", nil)
|
|
|
|
req := authRequest(httptest.NewRequest("DELETE", "/api/items/DEL-ITEM-001", nil))
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNoContent && w.Code != http.StatusOK {
|
|
t.Fatalf("status: got %d, want 200 or 204; body: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Should be gone (archived)
|
|
req2 := httptest.NewRequest("GET", "/api/items/DEL-ITEM-001", nil)
|
|
w2 := httptest.NewRecorder()
|
|
router.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusNotFound {
|
|
t.Errorf("after delete, expected 404, got %d", w2.Code)
|
|
}
|
|
}
|