material_manage_system/internal/repository/warehouse_test.go
2026-10-09 10:38:02 +08:00

111 lines
4.4 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 repository
import (
"errors"
"regexp"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/pkg/database"
)
// 与 warehouse.go 里的语句逐字一致:换行和缩进都参与匹配。
const createLocationSQL = `INSERT INTO warehouse_locations (warehouse_id, code, name, status)
SELECT id, ?, ?, ? FROM warehouses WHERE id = ? AND organization_id = ?`
func newWarehouseMock(t *testing.T) sqlmock.Sqlmock {
t.Helper()
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
return mock
}
// 仓库不存在时(id 打错、仓库已被删、或这条仓库属于别的组织)这条 INSERT...SELECT
// 影响 0 行,而 MySQL 既不报错、LastInsertId() 也只回 0。此前只看 err,接口会回
// 201「Location created successfully」,库里却没有新行,刷新后库位消失、重试还是
// "成功"(实测请求前后 warehouse_locations 行数都是 1)。
func TestCreateLocationReportsMissingWarehouse(t *testing.T) {
mock := newWarehouseMock(t)
mock.ExpectExec(regexp.QuoteMeta(createLocationSQL)).
WithArgs("L1", "库位一", "active", int64(999), int64(1)).
WillReturnResult(sqlmock.NewResult(0, 0))
item := &model.WarehouseLocation{WarehouseID: 999, Code: "L1", Name: "库位一", Status: "active"}
err := NewWarehouseRepository().CreateLocation(item, 1)
if !errors.Is(err, ErrWarehouseNotFound) {
t.Fatalf("影响 0 行应当返回 ErrWarehouseNotFound,实际 %v", err)
}
if item.ID != 0 {
t.Fatalf("没插进去就不该有 id,实际 %d", item.ID)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// 反面:真的插进去了就必须回填 id,别把成功路径也一起判成失败。
func TestCreateLocationReturnsInsertedID(t *testing.T) {
mock := newWarehouseMock(t)
mock.ExpectExec(regexp.QuoteMeta(createLocationSQL)).
WithArgs("L1", "库位一", "active", int64(3), int64(1)).
WillReturnResult(sqlmock.NewResult(7, 1))
item := &model.WarehouseLocation{WarehouseID: 3, Code: "L1", Name: "库位一", Status: "active"}
if err := NewWarehouseRepository().CreateLocation(item, 1); err != nil {
t.Fatalf("插入成功不应报错,实际 %v", err)
}
if item.ID != 7 {
t.Fatalf("应当回填自增 id 7,实际 %d", item.ID)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// warehouses(organization_id) 的唯一约束是「每公司一个仓库」的兜底。撞约束属于
// 正常业务冲突,要翻成可展示的中文错误(接口据此回 409),不能把原始英文
// Duplicate entry 冒到前端。
func TestCreateWarehouseReportsDuplicatePerCompany(t *testing.T) {
mock := newWarehouseMock(t)
mock.ExpectExec("INSERT INTO warehouses").
WithArgs(int64(1), "WH2", "二号仓", "active").
WillReturnError(errors.New("Error 1062: Duplicate entry '1' for key 'uk_warehouses_organization'"))
err := NewWarehouseRepository().Create(&model.Warehouse{OrganizationID: 1, Code: "WH2", Name: "二号仓", Status: "active"})
if !errors.Is(err, ErrDuplicateWarehouse) {
t.Fatalf("撞唯一键应当翻成 ErrDuplicateWarehouse,实际 %v", err)
}
if !strings.Contains(err.Error(), "已配置仓库") {
t.Fatalf("错误消息应当提示已配置仓库,实际 %q", err.Error())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// warehouse_locations(warehouse_id) 的唯一约束是「每仓库一个库位」的兜底,冲突同样
// 要翻成中文并回 409;停用的库位也占名额,因此冲突与库位状态无关。
func TestCreateWarehouseLocationReportsDuplicatePerWarehouse(t *testing.T) {
mock := newWarehouseMock(t)
mock.ExpectExec(regexp.QuoteMeta(createLocationSQL)).
WithArgs("L2", "库位二", "active", int64(3), int64(1)).
WillReturnError(errors.New("Error 1062: Duplicate entry '3' for key 'uk_warehouse_locations_warehouse'"))
err := NewWarehouseRepository().CreateLocation(&model.WarehouseLocation{WarehouseID: 3, Code: "L2", Name: "库位二", Status: "active"}, 1)
if !errors.Is(err, ErrDuplicateLocation) {
t.Fatalf("撞唯一键应当翻成 ErrDuplicateLocation,实际 %v", err)
}
if !strings.Contains(err.Error(), "已配置库位") {
t.Fatalf("错误消息应当提示已配置库位,实际 %q", err.Error())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}