Files
silo/internal/schema/schema_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

252 lines
5.8 KiB
Go

package schema
import (
"os"
"path/filepath"
"testing"
)
// findSchemasDir walks upward to find the project root and returns
// the path to the schemas/ directory.
func findSchemasDir(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getting working directory: %v", err)
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return filepath.Join(dir, "schemas")
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("could not find project root")
}
dir = parent
}
}
func TestLoadSchema(t *testing.T) {
schemasDir := findSchemasDir(t)
path := filepath.Join(schemasDir, "kindred-rd.yaml")
s, err := Load(path)
if err != nil {
t.Fatalf("Load returned error: %v", err)
}
if s.Name != "kindred-rd" {
t.Errorf("schema name: got %q, want %q", s.Name, "kindred-rd")
}
if s.Separator != "-" {
t.Errorf("separator: got %q, want %q", s.Separator, "-")
}
if len(s.Segments) == 0 {
t.Fatal("expected at least one segment")
}
// First segment should be the category enum
cat := s.Segments[0]
if cat.Name != "category" {
t.Errorf("first segment name: got %q, want %q", cat.Name, "category")
}
if cat.Type != "enum" {
t.Errorf("first segment type: got %q, want %q", cat.Type, "enum")
}
if len(cat.Values) == 0 {
t.Error("category segment has no values")
}
}
func TestLoadSchemaDir(t *testing.T) {
schemasDir := findSchemasDir(t)
schemas, err := LoadAll(schemasDir)
if err != nil {
t.Fatalf("LoadAll returned error: %v", err)
}
if len(schemas) == 0 {
t.Fatal("expected at least one schema")
}
if _, ok := schemas["kindred-rd"]; !ok {
t.Error("kindred-rd schema not found in loaded schemas")
}
}
func TestLoadSchemaValidation(t *testing.T) {
schemasDir := findSchemasDir(t)
schemas, err := LoadAll(schemasDir)
if err != nil {
t.Fatalf("LoadAll returned error: %v", err)
}
for name, s := range schemas {
if s.Name == "" {
continue // non-part-numbering schemas (e.g., location_schema)
}
if err := s.Validate(); err != nil {
t.Errorf("schema %q failed validation: %v", name, err)
}
}
}
func TestGetPropertiesForCategory(t *testing.T) {
ps := &PropertySchemas{
Version: 1,
Defaults: map[string]PropertyDefinition{
"weight": {Type: "number", Unit: "kg"},
"color": {Type: "string"},
},
Categories: map[string]map[string]PropertyDefinition{
"F": {
"thread_size": {Type: "string", Required: true},
"weight": {Type: "number", Unit: "g"}, // override default
},
},
}
props := ps.GetPropertiesForCategory("F01")
// Should have all three: weight (overridden), color (default), thread_size (category)
if len(props) != 3 {
t.Errorf("expected 3 properties, got %d", len(props))
}
if props["weight"].Unit != "g" {
t.Errorf("weight unit: got %q, want %q (should be overridden by category)", props["weight"].Unit, "g")
}
if props["color"].Type != "string" {
t.Errorf("color type: got %q, want %q", props["color"].Type, "string")
}
if !props["thread_size"].Required {
t.Error("thread_size should be required")
}
}
func TestGetPropertiesForUnknownCategory(t *testing.T) {
ps := &PropertySchemas{
Version: 1,
Defaults: map[string]PropertyDefinition{
"weight": {Type: "number"},
},
Categories: map[string]map[string]PropertyDefinition{
"F": {"thread_size": {Type: "string"}},
},
}
props := ps.GetPropertiesForCategory("Z99")
// Only defaults, no category-specific properties
if len(props) != 1 {
t.Errorf("expected 1 property (defaults only), got %d", len(props))
}
if _, ok := props["weight"]; !ok {
t.Error("default property 'weight' should be present")
}
}
func TestApplyDefaults(t *testing.T) {
ps := &PropertySchemas{
Version: 1,
Defaults: map[string]PropertyDefinition{
"status": {Type: "string", Default: "draft"},
"weight": {Type: "number"},
},
}
props := map[string]any{"custom": "value"}
result := ps.ApplyDefaults(props, "X")
if result["status"] != "draft" {
t.Errorf("status: got %v, want %q", result["status"], "draft")
}
if result["custom"] != "value" {
t.Errorf("custom: got %v, want %q", result["custom"], "value")
}
// weight has no default, should not be added
if _, ok := result["weight"]; ok {
t.Error("weight should not be added (no default value)")
}
}
func TestSchemaValidate(t *testing.T) {
tests := []struct {
name string
schema Schema
wantErr bool
}{
{
name: "empty name",
schema: Schema{Name: "", Segments: []Segment{{Name: "s", Type: "constant", Value: "X"}}},
wantErr: true,
},
{
name: "no segments",
schema: Schema{Name: "test", Segments: nil},
wantErr: true,
},
{
name: "valid minimal",
schema: Schema{
Name: "test",
Segments: []Segment{
{Name: "prefix", Type: "constant", Value: "X"},
},
},
wantErr: false,
},
{
name: "enum without values",
schema: Schema{
Name: "test",
Segments: []Segment{
{Name: "cat", Type: "enum"},
},
},
wantErr: true,
},
{
name: "serial without length",
schema: Schema{
Name: "test",
Segments: []Segment{
{Name: "seq", Type: "serial", Length: 0},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.schema.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestGetSegment(t *testing.T) {
s := &Schema{
Segments: []Segment{
{Name: "prefix", Type: "constant", Value: "KS"},
{Name: "serial", Type: "serial", Length: 4},
},
}
seg := s.GetSegment("serial")
if seg == nil {
t.Fatal("GetSegment returned nil for existing segment")
}
if seg.Type != "serial" {
t.Errorf("segment type: got %q, want %q", seg.Type, "serial")
}
if s.GetSegment("nonexistent") != nil {
t.Error("GetSegment should return nil for nonexistent segment")
}
}