Files
silo/internal/api/items_test.go
Forbes d08b178466 test: add comprehensive test suite for backend
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
2026-02-07 01:57:10 -06:00

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)
}
}