- 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
268 lines
7.1 KiB
Go
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")
|
|
}
|
|
}
|