Files
silo/internal/db/items_test.go
forbes-0023 b3c748ef10 refactor: move sourcing_link and standard_cost from item columns to revision properties
- Add migration 013 to copy sourcing_link/standard_cost values into
  current revision properties JSONB and drop the columns from items table
- Remove SourcingLink/StandardCost from Go Item struct and all DB queries
  (items.go, audit_queries.go, projects.go)
- Remove from API request/response structs and handlers
- Update CSV/ODS/BOM export/import to read these from revision properties
- Update audit handlers to score as regular property fields
- Remove from frontend Item type and hardcoded form fields
- MainTab now reads sourcing_link/standard_cost from item.properties
- CreateItemPane/EditItemPane no longer have dedicated fields for these;
  they will be rendered as schema-driven property fields
2026-02-11 09:50:31 -06:00

268 lines
7.1 KiB
Go

package db
import (
"context"
"fmt"
"testing"
)
func TestItemCreate(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
item := &Item{
PartNumber: "TEST-0001",
ItemType: "part",
Description: "Test item",
}
err := repo.Create(ctx, item, map[string]any{"color": "red"})
if err != nil {
t.Fatalf("Create: %v", err)
}
if item.ID == "" {
t.Error("expected item ID to be set")
}
if item.CurrentRevision != 1 {
t.Errorf("current revision: got %d, want 1", item.CurrentRevision)
}
}
func TestItemGetByPartNumber(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
item := &Item{PartNumber: "GET-PN-001", ItemType: "part", Description: "get by pn test"}
if err := repo.Create(ctx, item, nil); err != nil {
t.Fatalf("Create: %v", err)
}
got, err := repo.GetByPartNumber(ctx, "GET-PN-001")
if err != nil {
t.Fatalf("GetByPartNumber: %v", err)
}
if got == nil {
t.Fatal("expected item, got nil")
}
if got.Description != "get by pn test" {
t.Errorf("description: got %q, want %q", got.Description, "get by pn test")
}
// Non-existent should return nil, not error
missing, err := repo.GetByPartNumber(ctx, "DOES-NOT-EXIST")
if err != nil {
t.Fatalf("GetByPartNumber (missing): %v", err)
}
if missing != nil {
t.Error("expected nil for missing item")
}
}
func TestItemGetByID(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
item := &Item{PartNumber: "GET-ID-001", ItemType: "assembly", Description: "get by id"}
if err := repo.Create(ctx, item, nil); err != nil {
t.Fatalf("Create: %v", err)
}
got, err := repo.GetByID(ctx, item.ID)
if err != nil {
t.Fatalf("GetByID: %v", err)
}
if got == nil {
t.Fatal("expected item, got nil")
}
if got.PartNumber != "GET-ID-001" {
t.Errorf("part_number: got %q, want %q", got.PartNumber, "GET-ID-001")
}
}
func TestItemList(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
for i := 0; i < 3; i++ {
item := &Item{
PartNumber: fmt.Sprintf("LIST-%04d", i),
ItemType: "part",
Description: fmt.Sprintf("list item %d", i),
}
if err := repo.Create(ctx, item, nil); err != nil {
t.Fatalf("Create #%d: %v", i, err)
}
}
items, err := repo.List(ctx, ListOptions{})
if err != nil {
t.Fatalf("List: %v", err)
}
if len(items) != 3 {
t.Errorf("expected 3 items, got %d", len(items))
}
}
func TestItemListByType(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
repo.Create(ctx, &Item{PartNumber: "TYPE-P-001", ItemType: "part", Description: "a part"}, nil)
repo.Create(ctx, &Item{PartNumber: "TYPE-A-001", ItemType: "assembly", Description: "an assembly"}, nil)
repo.Create(ctx, &Item{PartNumber: "TYPE-P-002", ItemType: "part", Description: "another part"}, nil)
items, err := repo.List(ctx, ListOptions{ItemType: "part"})
if err != nil {
t.Fatalf("List: %v", err)
}
if len(items) != 2 {
t.Errorf("expected 2 parts, got %d", len(items))
}
}
func TestItemUpdate(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
item := &Item{PartNumber: "UPD-001", ItemType: "part", Description: "original"}
if err := repo.Create(ctx, item, nil); err != nil {
t.Fatalf("Create: %v", err)
}
err := repo.Update(ctx, item.ID, UpdateItemFields{
PartNumber: "UPD-001",
ItemType: "part",
Description: "updated",
})
if err != nil {
t.Fatalf("Update: %v", err)
}
got, _ := repo.GetByID(ctx, item.ID)
if got.Description != "updated" {
t.Errorf("description: got %q, want %q", got.Description, "updated")
}
}
func TestItemArchiveUnarchive(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
item := &Item{PartNumber: "ARC-001", ItemType: "part", Description: "archivable"}
if err := repo.Create(ctx, item, nil); err != nil {
t.Fatalf("Create: %v", err)
}
// Archive
if err := repo.Archive(ctx, item.ID); err != nil {
t.Fatalf("Archive: %v", err)
}
// Should not appear in GetByPartNumber (excludes archived)
got, _ := repo.GetByPartNumber(ctx, "ARC-001")
if got != nil {
t.Error("archived item should not be returned by GetByPartNumber")
}
// But should still be accessible by ID
gotByID, _ := repo.GetByID(ctx, item.ID)
if gotByID == nil {
t.Fatal("archived item should still be accessible by GetByID")
}
if gotByID.ArchivedAt == nil {
t.Error("archived_at should be set")
}
// Unarchive
if err := repo.Unarchive(ctx, item.ID); err != nil {
t.Fatalf("Unarchive: %v", err)
}
got, _ = repo.GetByPartNumber(ctx, "ARC-001")
if got == nil {
t.Error("unarchived item should be returned by GetByPartNumber")
}
}
func TestItemCreateRevision(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
item := &Item{PartNumber: "REV-001", ItemType: "part", Description: "revisable"}
if err := repo.Create(ctx, item, map[string]any{"v": 1}); err != nil {
t.Fatalf("Create: %v", err)
}
// Create second revision
rev := &Revision{
ItemID: item.ID,
Properties: map[string]any{"v": 2},
}
if err := repo.CreateRevision(ctx, rev); err != nil {
t.Fatalf("CreateRevision: %v", err)
}
if rev.RevisionNumber != 2 {
t.Errorf("revision number: got %d, want 2", rev.RevisionNumber)
}
// Item's current_revision should be updated by trigger
got, _ := repo.GetByPartNumber(ctx, "REV-001")
if got.CurrentRevision != 2 {
t.Errorf("current_revision: got %d, want 2", got.CurrentRevision)
}
}
func TestItemGetRevisions(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
item := &Item{PartNumber: "REVS-001", ItemType: "part", Description: "multi rev"}
if err := repo.Create(ctx, item, map[string]any{"step": "initial"}); err != nil {
t.Fatalf("Create: %v", err)
}
comment := "second revision"
repo.CreateRevision(ctx, &Revision{
ItemID: item.ID, Properties: map[string]any{"step": "updated"}, Comment: &comment,
})
revisions, err := repo.GetRevisions(ctx, item.ID)
if err != nil {
t.Fatalf("GetRevisions: %v", err)
}
if len(revisions) != 2 {
t.Errorf("expected 2 revisions, got %d", len(revisions))
}
// Revisions are returned newest first
if revisions[0].RevisionNumber != 2 {
t.Errorf("first revision should be #2 (newest), got #%d", revisions[0].RevisionNumber)
}
}
func TestItemSetThumbnailKey(t *testing.T) {
database := mustConnectTestDB(t)
repo := NewItemRepository(database)
ctx := context.Background()
item := &Item{PartNumber: "THUMB-001", ItemType: "part", Description: "thumbnail test"}
if err := repo.Create(ctx, item, nil); err != nil {
t.Fatalf("Create: %v", err)
}
if err := repo.SetThumbnailKey(ctx, item.ID, "items/thumb.png"); err != nil {
t.Fatalf("SetThumbnailKey: %v", err)
}
got, _ := repo.GetByID(ctx, item.ID)
if got.ThumbnailKey == nil || *got.ThumbnailKey != "items/thumb.png" {
t.Errorf("thumbnail_key: got %v, want %q", got.ThumbnailKey, "items/thumb.png")
}
}