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) } }