- Duplicate part number constraint (PG 23505) - Hard delete, pagination, search filtering - Revision status/labels update, compare, rollback - Project-item association by code, list by project filter - Item file CRUD: create, list, get, delete
282 lines
7.8 KiB
Go
282 lines
7.8 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestItemCreateDuplicatePartNumber(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
repo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
item := &Item{PartNumber: "DUP-001", ItemType: "part", Description: "first"}
|
|
if err := repo.Create(ctx, item, nil); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
dup := &Item{PartNumber: "DUP-001", ItemType: "part", Description: "duplicate"}
|
|
err := repo.Create(ctx, dup, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for duplicate part number, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "23505") && !strings.Contains(err.Error(), "duplicate") {
|
|
t.Errorf("expected duplicate key error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestItemDelete(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
repo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
item := &Item{PartNumber: "HDEL-001", ItemType: "part", Description: "hard delete"}
|
|
if err := repo.Create(ctx, item, nil); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
if err := repo.Delete(ctx, item.ID); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
|
|
got, err := repo.GetByID(ctx, item.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetByID after delete: %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Error("expected nil after hard delete")
|
|
}
|
|
}
|
|
|
|
func TestItemListPagination(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
repo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 5; i++ {
|
|
item := &Item{
|
|
PartNumber: fmt.Sprintf("PAGE-%04d", i),
|
|
ItemType: "part",
|
|
Description: fmt.Sprintf("page item %d", i),
|
|
}
|
|
if err := repo.Create(ctx, item, nil); err != nil {
|
|
t.Fatalf("Create #%d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Fetch page of 2 with offset 2
|
|
items, err := repo.List(ctx, ListOptions{Limit: 2, Offset: 2})
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Errorf("expected 2 items, got %d", len(items))
|
|
}
|
|
}
|
|
|
|
func TestItemListSearch(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
repo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
repo.Create(ctx, &Item{PartNumber: "SRCH-001", ItemType: "part", Description: "alpha widget"}, nil)
|
|
repo.Create(ctx, &Item{PartNumber: "SRCH-002", ItemType: "part", Description: "beta gadget"}, nil)
|
|
repo.Create(ctx, &Item{PartNumber: "SRCH-003", ItemType: "part", Description: "alpha gizmo"}, nil)
|
|
|
|
items, err := repo.List(ctx, ListOptions{Search: "alpha"})
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Errorf("expected 2 items matching 'alpha', got %d", len(items))
|
|
}
|
|
}
|
|
|
|
func TestRevisionStatusUpdate(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
repo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
item := &Item{PartNumber: "STAT-001", ItemType: "part", Description: "status test"}
|
|
if err := repo.Create(ctx, item, map[string]any{"v": 1}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
status := "released"
|
|
if err := repo.UpdateRevisionStatus(ctx, item.ID, 1, &status, nil); err != nil {
|
|
t.Fatalf("UpdateRevisionStatus: %v", err)
|
|
}
|
|
|
|
rev, err := repo.GetRevision(ctx, item.ID, 1)
|
|
if err != nil {
|
|
t.Fatalf("GetRevision: %v", err)
|
|
}
|
|
if rev.Status != "released" {
|
|
t.Errorf("status: got %q, want %q", rev.Status, "released")
|
|
}
|
|
}
|
|
|
|
func TestRevisionLabelsUpdate(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
repo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
item := &Item{PartNumber: "LBL-001", ItemType: "part", Description: "label test"}
|
|
if err := repo.Create(ctx, item, nil); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
labels := []string{"prototype", "urgent"}
|
|
if err := repo.UpdateRevisionStatus(ctx, item.ID, 1, nil, labels); err != nil {
|
|
t.Fatalf("UpdateRevisionStatus: %v", err)
|
|
}
|
|
|
|
rev, err := repo.GetRevision(ctx, item.ID, 1)
|
|
if err != nil {
|
|
t.Fatalf("GetRevision: %v", err)
|
|
}
|
|
if len(rev.Labels) != 2 {
|
|
t.Errorf("labels count: got %d, want 2", len(rev.Labels))
|
|
}
|
|
}
|
|
|
|
func TestRevisionCompare(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
repo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
item := &Item{PartNumber: "CMP-001", ItemType: "part", Description: "compare test"}
|
|
if err := repo.Create(ctx, item, map[string]any{"color": "red", "weight": 10}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
// Rev 2: change color, remove weight, add size
|
|
repo.CreateRevision(ctx, &Revision{
|
|
ItemID: item.ID,
|
|
Properties: map[string]any{"color": "blue", "size": "large"},
|
|
})
|
|
|
|
diff, err := repo.CompareRevisions(ctx, item.ID, 1, 2)
|
|
if err != nil {
|
|
t.Fatalf("CompareRevisions: %v", err)
|
|
}
|
|
|
|
if len(diff.Added) == 0 {
|
|
t.Error("expected added fields (size)")
|
|
}
|
|
if len(diff.Removed) == 0 {
|
|
t.Error("expected removed fields (weight)")
|
|
}
|
|
if len(diff.Changed) == 0 {
|
|
t.Error("expected changed fields (color)")
|
|
}
|
|
}
|
|
|
|
func TestRevisionRollback(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
repo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
item := &Item{PartNumber: "RBK-001", ItemType: "part", Description: "rollback test"}
|
|
if err := repo.Create(ctx, item, map[string]any{"version": "original"}); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
// Rev 2: change property
|
|
repo.CreateRevision(ctx, &Revision{
|
|
ItemID: item.ID,
|
|
Properties: map[string]any{"version": "modified"},
|
|
})
|
|
|
|
// Rollback to rev 1 — should create rev 3
|
|
comment := "rollback to rev 1"
|
|
rev3, err := repo.CreateRevisionFromExisting(ctx, item.ID, 1, comment, nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateRevisionFromExisting: %v", err)
|
|
}
|
|
if rev3.RevisionNumber != 3 {
|
|
t.Errorf("revision number: got %d, want 3", rev3.RevisionNumber)
|
|
}
|
|
|
|
// Rev 3 should have rev 1's properties
|
|
got, err := repo.GetRevision(ctx, item.ID, 3)
|
|
if err != nil {
|
|
t.Fatalf("GetRevision: %v", err)
|
|
}
|
|
if got.Properties["version"] != "original" {
|
|
t.Errorf("rolled back version: got %v, want %q", got.Properties["version"], "original")
|
|
}
|
|
}
|
|
|
|
func TestProjectItemAssociationsByCode(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
projRepo := NewProjectRepository(database)
|
|
itemRepo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
proj := &Project{Code: "BYTAG", Name: "Tag Project"}
|
|
projRepo.Create(ctx, proj)
|
|
|
|
item := &Item{PartNumber: "TAG-001", ItemType: "part", Description: "taggable"}
|
|
itemRepo.Create(ctx, item, nil)
|
|
|
|
// Tag by code
|
|
if err := projRepo.AddItemToProjectByCode(ctx, item.ID, "BYTAG"); err != nil {
|
|
t.Fatalf("AddItemToProjectByCode: %v", err)
|
|
}
|
|
|
|
projects, err := projRepo.GetProjectsForItem(ctx, item.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetProjectsForItem: %v", err)
|
|
}
|
|
if len(projects) != 1 {
|
|
t.Fatalf("expected 1 project, got %d", len(projects))
|
|
}
|
|
if projects[0].Code != "BYTAG" {
|
|
t.Errorf("project code: got %q, want %q", projects[0].Code, "BYTAG")
|
|
}
|
|
|
|
// Untag by code
|
|
if err := projRepo.RemoveItemFromProjectByCode(ctx, item.ID, "BYTAG"); err != nil {
|
|
t.Fatalf("RemoveItemFromProjectByCode: %v", err)
|
|
}
|
|
|
|
projects, _ = projRepo.GetProjectsForItem(ctx, item.ID)
|
|
if len(projects) != 0 {
|
|
t.Errorf("expected 0 projects after removal, got %d", len(projects))
|
|
}
|
|
}
|
|
|
|
func TestListByProject(t *testing.T) {
|
|
database := mustConnectTestDB(t)
|
|
projRepo := NewProjectRepository(database)
|
|
itemRepo := NewItemRepository(database)
|
|
ctx := context.Background()
|
|
|
|
proj := &Project{Code: "FILT", Name: "Filter Project"}
|
|
projRepo.Create(ctx, proj)
|
|
|
|
// Create 3 items, tag only 2
|
|
for i := 0; i < 3; i++ {
|
|
item := &Item{
|
|
PartNumber: fmt.Sprintf("FILT-%04d", i),
|
|
ItemType: "part",
|
|
Description: fmt.Sprintf("filter item %d", i),
|
|
}
|
|
itemRepo.Create(ctx, item, nil)
|
|
if i < 2 {
|
|
projRepo.AddItemToProjectByCode(ctx, item.ID, "FILT")
|
|
}
|
|
}
|
|
|
|
items, err := itemRepo.List(ctx, ListOptions{Project: "FILT"})
|
|
if err != nil {
|
|
t.Fatalf("List with project filter: %v", err)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Errorf("expected 2 items in project FILT, got %d", len(items))
|
|
}
|
|
}
|