154 lines
4.7 KiB
Go
154 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
func TestMaterialRepoListUsesStatusFilterInsteadOfCategoryArgument(t *testing.T) {
|
|
var list func(string, string, int, int) ([]model.Material, int64, error) = NewMaterialRepo().List
|
|
_ = list
|
|
}
|