Files
warehouse-management-BE/internal/repositories/component_item_repository_test.go

519 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repositories
import (
"context"
"fmt"
"os"
"testing"
"time"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/jackc/pgx/v5/pgxpool"
)
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
// testDB returns a connection pool for tests. Set WM_TEST_DB_URL to override
// the default connection string. If the DB is not available the test is skipped.
func testDB(t *testing.T) *pgxpool.Pool {
t.Helper()
dsn := os.Getenv("WM_TEST_DB_URL")
if dsn == "" {
dsn = "postgres://root:Smatec2026@localhost:5432/warehouse_management?sslmode=disable"
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Skipf("skipping integration test: cannot connect to DB: %v", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
t.Skipf("skipping integration test: cannot ping DB: %v", err)
}
return pool
}
// seedComponentItem inserts all required parent records and returns
// the created component_item ID, plus its component_id and container_id.
func seedComponentItem(t *testing.T, pool *pgxpool.Pool, q *db.Queries, quantity int32, status db.ComponentItemStatusEnum) (itemID, componentID, containerID int64) {
t.Helper()
ctx := context.Background()
// warehouse
w, err := q.CreateWarehouse(ctx, db.CreateWarehouseParams{
Name: fmt.Sprintf("test-wh-%d", time.Now().UnixNano()),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed warehouse: %v", err)
}
// room
r, err := q.CreateRoom(ctx, db.CreateRoomParams{
WarehouseID: w.ID,
Name: fmt.Sprintf("test-room-%d", time.Now().UnixNano()),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed room: %v", err)
}
// cabinet
cb, err := q.CreateCabinet(ctx, db.CreateCabinetParams{
RoomID: r.ID,
Name: fmt.Sprintf("test-cab-%d", time.Now().UnixNano()),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed cabinet: %v", err)
}
// shelf
s, err := q.CreateShelve(ctx, db.CreateShelveParams{
CabinetID: cb.ID,
Name: fmt.Sprintf("test-shelf-%d", time.Now().UnixNano()),
LevelIndex: 1,
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed shelf: %v", err)
}
// container
cn, err := q.CreateContainer(ctx, db.CreateContainerParams{
ShelfID: s.ID,
Name: fmt.Sprintf("test-cont-%d", time.Now().UnixNano()),
ContainerType: db.ContainerTypeEnumOther,
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed container: %v", err)
}
containerID = cn.ID
// component_type
ct, err := q.CreateComponentType(ctx, db.CreateComponentTypeParams{
Name: fmt.Sprintf("test-ctype-%d", time.Now().UnixNano()),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed component_type: %v", err)
}
// component
c, err := q.CreateComponent(ctx, db.CreateComponentParams{
ComponentTypeID: ct.ID,
Name: fmt.Sprintf("test-comp-%d", time.Now().UnixNano()),
Unit: "cái",
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed component: %v", err)
}
componentID = c.ID
// component_item
ci, err := q.CreateComponentItem(ctx, db.CreateComponentItemParams{
ComponentID: componentID,
ContainerID: containerID,
Quantity: quantity,
Status: status,
Metadata: []byte("{}"),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("seed component_item: %v", err)
}
return ci.ID, componentID, containerID
}
// cleanupSeeded removes test data in reverse dependency order.
func cleanupSeeded(t *testing.T, pool *pgxpool.Pool, itemID, componentID, containerID int64) {
t.Helper()
ctx := context.Background()
q := db.New(pool)
// Delete history rows referencing this item
_, _ = pool.Exec(ctx, "DELETE FROM component_status_history WHERE component_item_id = $1", itemID)
// Delete component_items (including those possibly created by the test)
_, _ = pool.Exec(ctx, "DELETE FROM component_items WHERE component_id = $1", componentID)
_, _ = q.DeleteComponent(ctx, componentID)
// Delete component_type we need to find it from the component
_, _ = pool.Exec(ctx, "DELETE FROM component_types WHERE id = (SELECT component_type_id FROM components WHERE id = $1)", componentID)
_, _ = q.DeleteContainer(ctx, containerID)
// Delete container's hierarchy
_, _ = pool.Exec(ctx, "DELETE FROM shelves WHERE id = (SELECT shelf_id FROM containers WHERE id = $1)", containerID)
_, _ = pool.Exec(ctx, "DELETE FROM cabinets WHERE id IN (SELECT cabinet_id FROM shelves WHERE id = (SELECT shelf_id FROM containers WHERE id = $1))", containerID)
_, _ = pool.Exec(ctx, "DELETE FROM rooms WHERE id IN (SELECT room_id FROM cabinets WHERE id IN (SELECT cabinet_id FROM shelves WHERE id = (SELECT shelf_id FROM containers WHERE id = $1)))", containerID)
_, _ = pool.Exec(ctx, "DELETE FROM warehouses WHERE id IN (SELECT warehouse_id FROM rooms WHERE id IN (SELECT room_id FROM cabinets WHERE id IN (SELECT cabinet_id FROM shelves WHERE id = (SELECT shelf_id FROM containers WHERE id = $1))))", containerID)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
func TestUpdateComponentItemStatus_Case1_ChangeAll_NilQuantity(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 20, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, nil, "test note", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify result
if result.StatusHistory.OldStatus != "normal" {
t.Errorf("expected old status 'normal', got '%s'", result.StatusHistory.OldStatus)
}
if result.StatusHistory.NewStatus != "damaged" {
t.Errorf("expected new status 'damaged', got '%s'", result.StatusHistory.NewStatus)
}
if result.StatusHistory.ChangedQuantity != 20 {
t.Errorf("expected changed quantity 20, got %d", result.StatusHistory.ChangedQuantity)
}
if result.StatusHistory.ChangedBy != "system" {
t.Errorf("expected changed_by 'system', got '%s'", result.StatusHistory.ChangedBy)
}
if result.NewComponentItemID != nil {
t.Error("expected NewComponentItemID to be nil for case 1")
}
if result.MergedComponentItemID != nil {
t.Error("expected MergedComponentItemID to be nil for case 1")
}
// Verify DB state
updated, err := q.GetComponentItemByID(context.Background(), itemID)
if err != nil {
t.Fatalf("get updated item: %v", err)
}
if updated.Status != db.ComponentItemStatusEnumDamaged {
t.Errorf("expected status 'damaged', got '%s'", updated.Status)
}
if updated.Quantity != 20 {
t.Errorf("expected quantity 20, got %d", updated.Quantity)
}
}
func TestUpdateComponentItemStatus_Case1_ChangeAll_ExplicitQuantity(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 15, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(15)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.StatusHistory.ChangedQuantity != 15 {
t.Errorf("expected changed quantity 15, got %d", result.StatusHistory.ChangedQuantity)
}
}
func TestUpdateComponentItemStatus_Case2_Split_NoExistingTarget(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 20, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(5)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "split test", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify result
if result.NewComponentItemID == nil {
t.Fatal("expected NewComponentItemID to be set for case 2")
}
if result.MergedComponentItemID != nil {
t.Error("expected MergedComponentItemID to be nil for case 2")
}
// Original record should have reduced quantity
orig, err := q.GetComponentItemByID(context.Background(), itemID)
if err != nil {
t.Fatalf("get original item: %v", err)
}
if orig.Quantity != 15 {
t.Errorf("expected original quantity 15, got %d", orig.Quantity)
}
if orig.Status != db.ComponentItemStatusEnumNormal {
t.Errorf("expected original status 'normal', got '%s'", orig.Status)
}
// New record should have the split quantity with target status
newItem, err := q.GetComponentItemByID(context.Background(), *result.NewComponentItemID)
if err != nil {
t.Fatalf("get new item: %v", err)
}
if newItem.Quantity != 5 {
t.Errorf("expected new item quantity 5, got %d", newItem.Quantity)
}
if newItem.Status != db.ComponentItemStatusEnumDamaged {
t.Errorf("expected new item status 'damaged', got '%s'", newItem.Status)
}
if newItem.ComponentID != orig.ComponentID {
t.Error("new item should have same component_id")
}
if newItem.ContainerID != orig.ContainerID {
t.Error("new item should have same container_id")
}
}
func TestUpdateComponentItemStatus_Case3_Merge_ExistingTarget(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 20, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
// Pre-create a damaged record to merge into
ctx := context.Background()
existingDamaged, err := q.CreateComponentItem(ctx, db.CreateComponentItemParams{
ComponentID: compID,
ContainerID: contID,
Quantity: 3,
Status: db.ComponentItemStatusEnumDamaged,
Metadata: []byte("{}"),
CreatedAt: time.Now(),
})
if err != nil {
t.Fatalf("pre-create damaged record: %v", err)
}
changedQty := int32(5)
result, err := UpdateComponentItemStatus(ctx, pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "merge test", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify result
if result.NewComponentItemID != nil {
t.Error("expected NewComponentItemID to be nil for case 3")
}
if result.MergedComponentItemID == nil {
t.Fatal("expected MergedComponentItemID to be set for case 3")
}
if *result.MergedComponentItemID != existingDamaged.ID {
t.Errorf("expected merged ID %d, got %d", existingDamaged.ID, *result.MergedComponentItemID)
}
// Original record should have reduced quantity
orig, err := q.GetComponentItemByID(ctx, itemID)
if err != nil {
t.Fatalf("get original item: %v", err)
}
if orig.Quantity != 15 {
t.Errorf("expected original quantity 15, got %d", orig.Quantity)
}
// Merged record should have increased quantity
merged, err := q.GetComponentItemByID(ctx, existingDamaged.ID)
if err != nil {
t.Fatalf("get merged item: %v", err)
}
if merged.Quantity != 8 { // 3 + 5
t.Errorf("expected merged quantity 8, got %d", merged.Quantity)
}
if merged.Status != db.ComponentItemStatusEnumDamaged {
t.Errorf("expected merged status 'damaged', got '%s'", merged.Status)
}
}
func TestUpdateComponentItemStatus_Edge_StatusUnchanged(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumNormal, nil, "", "system")
if err == nil {
t.Fatal("expected error for status unchanged")
}
if err.Error() != "status unchanged" {
t.Errorf("expected 'status unchanged', got '%s'", err.Error())
}
}
func TestUpdateComponentItemStatus_Edge_ChangedQuantityZero(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(0)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "", "system")
if err == nil {
t.Fatal("expected error for zero changed_quantity")
}
}
func TestUpdateComponentItemStatus_Edge_ChangedQuantityNegative(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(-5)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "", "system")
if err == nil {
t.Fatal("expected error for negative changed_quantity")
}
}
func TestUpdateComponentItemStatus_Edge_ChangedQuantityExceedsQuantity(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
changedQty := int32(25)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "", "system")
if err == nil {
t.Fatal("expected error for changed_quantity > quantity")
}
}
func TestUpdateComponentItemStatus_Edge_ItemNotFound(t *testing.T) {
pool := testDB(t)
defer pool.Close()
_, err := UpdateComponentItemStatus(context.Background(), pool, 999999,
db.ComponentItemStatusEnumDamaged, nil, "", "system")
if err == nil {
t.Fatal("expected error for non-existent item")
}
}
func TestUpdateComponentItemStatus_Edge_QuantityBecomesZero(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 5, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
// Change all 5 to damaged → original record quantity becomes 0 → should be deleted
changedQty := int32(5)
_, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, &changedQty, "zero qty test", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// The original record should be deleted (quantity became 0)
_, err = q.GetComponentItemByID(context.Background(), itemID)
if err == nil {
t.Error("expected original item to be deleted when quantity becomes 0")
}
}
func TestUpdateComponentItemStatus_ChangeToPendingInspection(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 30, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumPendingInspection, nil, "pending inspection check", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.StatusHistory.NewStatus != "pending_inspection" {
t.Errorf("expected new status 'pending_inspection', got '%s'", result.StatusHistory.NewStatus)
}
updated, _ := q.GetComponentItemByID(context.Background(), itemID)
if updated.Status != db.ComponentItemStatusEnumPendingInspection {
t.Errorf("expected status 'pending_inspection', got '%s'", updated.Status)
}
}
func TestUpdateComponentItemStatus_HistoryRecordIntegrity(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumLongUnused, nil, "history integrity test note", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
history := result.StatusHistory
if history.ComponentItemID != itemID {
t.Errorf("expected component_item_id %d, got %d", itemID, history.ComponentItemID)
}
if history.OldStatus != "normal" {
t.Errorf("expected old_status 'normal', got '%s'", history.OldStatus)
}
if history.NewStatus != "long_unused" {
t.Errorf("expected new_status 'long_unused', got '%s'", history.NewStatus)
}
if history.Note != "history integrity test note" {
t.Errorf("expected note 'history integrity test note', got '%s'", history.Note)
}
if history.ChangedBy != "system" {
t.Errorf("expected changed_by 'system', got '%s'", history.ChangedBy)
}
if history.ChangedQuantity != 10 {
t.Errorf("expected changed_quantity 10, got %d", history.ChangedQuantity)
}
if history.ID == 0 {
t.Error("expected history ID to be non-zero")
}
}
// TestUpdateComponentItemStatus_ResultType ensures the UpdateStatusResult domain
// model is populated correctly.
func TestUpdateComponentItemStatus_ResultType(t *testing.T) {
pool := testDB(t)
defer pool.Close()
q := db.New(pool)
itemID, compID, contID := seedComponentItem(t, pool, q, 10, db.ComponentItemStatusEnumNormal)
defer cleanupSeeded(t, pool, itemID, compID, contID)
result, err := UpdateComponentItemStatus(context.Background(), pool, itemID,
db.ComponentItemStatusEnumDamaged, nil, "", "system")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify UpdateStatusResult fields
var _ models.UpdateStatusResult = result // compile-time check
if result.ComponentItem.ID == 0 && result.NewComponentItemID == nil && result.MergedComponentItemID == nil {
// Case 1 doesn't set ComponentItem either in current impl — that's fine
// ComponentItem is only set in Case 1 (change all). Let's verify.
// Actually looking at the repo code, ComponentItem is only set in Case 1.
// For Case 2/3 it's not set because the original item is only partially changed.
// This is intentional.
// For Case 1, ComponentItem should be set
if result.ComponentItem.ID == 0 {
t.Error("expected ComponentItem.ID to be set for Case 1 (change all)")
}
}
if result.StatusHistory.ID == 0 {
t.Error("expected StatusHistory.ID to be non-zero")
}
}