package repository import ( "encoding/json" "regexp" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/pkg/database" ) func TestMaterialRepoGetByIDReturnsOnlyMasterData(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("open sql mock: %v", err) } t.Cleanup(func() { database.DB = nil db.Close() }) database.DB = db query := `SELECT id, organization_id, material_code, name, material_image, spec, manufacturer_model, category_id, base_unit_id, unit, status, version, created_at, updated_at FROM materials WHERE id = ?` now := time.Date(2026, 9, 15, 9, 0, 0, 0, time.UTC) mock.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(int64(7)).WillReturnRows( sqlmock.NewRows([]string{"id", "organization_id", "material_code", "name", "material_image", "spec", "manufacturer_model", "category_id", "base_unit_id", "unit", "status", "version", "created_at", "updated_at"}). AddRow(7, 1, "MAT-007", "轴承", nil, "6205", nil, 12, 3, "件", "active", 2, now, now), ) var material model.Material if err := NewMaterialRepo().GetByID(7, &material); err != nil { t.Fatalf("get material: %v", err) } payload, err := json.Marshal(material) if err != nil { t.Fatalf("marshal material: %v", err) } if regexp.MustCompile(`current_stock|warning_stock`).Match(payload) { t.Fatalf("material payload leaks inventory fields: %s", payload) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestMaterialRepoRecognizesOnlyActiveSubcategories(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("open sql mock: %v", err) } t.Cleanup(func() { database.DB = nil db.Close() }) database.DB = db query := `SELECT EXISTS( SELECT 1 FROM material_categories WHERE id = ? AND organization_id = ? AND parent_id IS NOT NULL AND status = 'active' )` mock.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(int64(12), int64(1)). WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true)) exists, err := NewMaterialRepo().IsActiveSubcategory(12, 1) if err != nil { t.Fatalf("check category: %v", err) } if !exists { t.Fatal("active subcategory should be accepted") } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } const ( obsoleteStockQuery = `SELECT COUNT(*) FROM inventory_balances WHERE material_id = ? AND (on_hand <> 0 OR reserved <> 0 OR locked <> 0 OR in_transit <> 0)` obsoleteUpdateStmt = `UPDATE materials SET status = 'obsolete', version = version + 1 WHERE id = ?` ) func newObsoleteMock(t *testing.T) (sqlmock.Sqlmock, func()) { t.Helper() db, mock, err := sqlmock.New() if err != nil { t.Fatalf("open sql mock: %v", err) } database.DB = db return mock, func() { database.DB = nil db.Close() } } func TestMaterialRepoObsoleteRejectsUnknownMaterial(t *testing.T) { mock, cleanup := newObsoleteMock(t) t.Cleanup(cleanup) mock.ExpectQuery(regexp.QuoteMeta(obsoleteStockQuery)).WithArgs(int64(999999)). WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) mock.ExpectExec(regexp.QuoteMeta(obsoleteUpdateStmt)).WithArgs(int64(999999)). WillReturnResult(sqlmock.NewResult(0, 0)) err := NewMaterialRepo().Obsolete(999999) if err == nil || !strings.Contains(err.Error(), "物料不存在") { t.Fatalf("obsoleting a missing material should report 物料不存在, got %v", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestMaterialRepoObsoleteUpdatesExistingMaterial(t *testing.T) { mock, cleanup := newObsoleteMock(t) t.Cleanup(cleanup) mock.ExpectQuery(regexp.QuoteMeta(obsoleteStockQuery)).WithArgs(int64(7)). WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) mock.ExpectExec(regexp.QuoteMeta(obsoleteUpdateStmt)).WithArgs(int64(7)). WillReturnResult(sqlmock.NewResult(0, 1)) if err := NewMaterialRepo().Obsolete(7); err != nil { t.Fatalf("obsolete existing material: %v", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestMaterialRepoObsoleteIsBlockedByStock(t *testing.T) { mock, cleanup := newObsoleteMock(t) t.Cleanup(cleanup) mock.ExpectQuery(regexp.QuoteMeta(obsoleteStockQuery)).WithArgs(int64(7)). WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) err := NewMaterialRepo().Obsolete(7) if err == nil || !strings.Contains(err.Error(), "仍有库存") { t.Fatalf("obsoleting a stocked material should be blocked, got %v", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }