package repository import ( "errors" "os" "path/filepath" "regexp" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/pkg/database" ) // 与 organization.go / organization_defaults.go 里的语句逐字一致。 const ( createOrganizationSQL = `INSERT INTO organizations (code, name, status, default_timezone) VALUES (?, ?, ?, ?)` seedUnitSQL = `INSERT INTO units (organization_id, code, name, precision_scale, status) VALUES (?, ?, ?, ?, 'active')` seedWarehouseSQL = `INSERT INTO warehouses (organization_id, code, name, status) VALUES (?, 'DEFAULT', '默认仓库', 'active')` seedLocationSQL = `INSERT INTO warehouse_locations (warehouse_id, code, name, status) VALUES (?, 'MAIN', '主库位', 'active')` ) func newOrganizationSeedMock(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 } // 单位是按公司隔离的,新公司没种单位的话,物料单位下拉是空的,base_unit_id 也无处可指。 func TestCreateOrganizationSeedsUnitsWarehouseAndLocation(t *testing.T) { mock := newOrganizationSeedMock(t) mock.ExpectBegin() mock.ExpectExec(regexp.QuoteMeta(createOrganizationSQL)). WithArgs("WUHAN", "武汉", "active", "Asia/Shanghai"). WillReturnResult(sqlmock.NewResult(7, 1)) for _, unit := range defaultMaterialUnits { mock.ExpectExec(regexp.QuoteMeta(seedUnitSQL)). WithArgs(int64(7), unit.Code, unit.Name, unit.PrecisionScale). WillReturnResult(sqlmock.NewResult(1, 1)) } mock.ExpectExec(regexp.QuoteMeta(seedWarehouseSQL)). WithArgs(int64(7)). WillReturnResult(sqlmock.NewResult(3, 1)) mock.ExpectExec(regexp.QuoteMeta(seedLocationSQL)). WithArgs(int64(3)). WillReturnResult(sqlmock.NewResult(5, 1)) mock.ExpectCommit() item := &model.Organization{Code: "WUHAN", Name: "武汉", Status: "active", DefaultTimezone: "Asia/Shanghai"} if err := NewOrganizationRepository().Create(item); err != nil { t.Fatalf("Create: %v", err) } if item.ID != 7 { t.Fatalf("回填的组织 ID = %d,期望 7", item.ID) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("有期望未被满足(漏种或多给了语句): %v", err) } } // 种子失败必须整体回滚:留下一个没有单位的公司,用户重试还会撞公司编码的唯一键。 func TestCreateOrganizationRollsBackWhenSeedingFails(t *testing.T) { mock := newOrganizationSeedMock(t) mock.ExpectBegin() mock.ExpectExec(regexp.QuoteMeta(createOrganizationSQL)). WithArgs("WUHAN", "武汉", "active", "Asia/Shanghai"). WillReturnResult(sqlmock.NewResult(7, 1)) // 第一个单位就失败 mock.ExpectExec(regexp.QuoteMeta(seedUnitSQL)). WithArgs(int64(7), defaultMaterialUnits[0].Code, defaultMaterialUnits[0].Name, defaultMaterialUnits[0].PrecisionScale). WillReturnError(errors.New("boom")) mock.ExpectRollback() err := NewOrganizationRepository().Create(&model.Organization{ Code: "WUHAN", Name: "武汉", Status: "active", DefaultTimezone: "Asia/Shanghai", }) if err == nil { t.Fatal("种子失败时 Create 应当返回错误") } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("有期望未被满足: %v", err) } } // 单位表有 uk_units_org_code 唯一键,清单里出现重复编码会让建公司直接失败。 func TestDefaultMaterialUnitsHaveNoDuplicateCodes(t *testing.T) { seen := make(map[string]bool, len(defaultMaterialUnits)) for _, unit := range defaultMaterialUnits { if seen[unit.Code] { t.Fatalf("默认单位清单里编码重复:%q", unit.Code) } seen[unit.Code] = true } } // 新建公司种下的单位,必须和 scripts/init.sql 给默认组织种的那套一致; // 两份清单各改各的,空库初始化与界面上新建的公司就会拿到不同的单位表。 func TestDefaultMaterialUnitsMatchInitSQL(t *testing.T) { raw, err := os.ReadFile(filepath.Join("..", "..", "scripts", "init.sql")) if err != nil { t.Fatalf("读取 init.sql: %v", err) } // units 段的行形如 (1,'个','个',0,'active');这个五字段模式在 init.sql 里 // 只匹配单位表(分类是四字段、仓库是五字段但首列是数字)。 row := regexp.MustCompile(`\(1,'([^']+)','[^']*',(\d+),'active'\)`) fromSQL := make(map[string]bool) for _, m := range row.FindAllStringSubmatch(string(raw), -1) { fromSQL[m[1]] = true } if len(fromSQL) == 0 { t.Fatalf("没从 init.sql 里解析出默认组织的单位,正则可能已失效") } fromGo := make(map[string]model.Unit, len(defaultMaterialUnits)) for _, unit := range defaultMaterialUnits { fromGo[unit.Code] = unit } for code := range fromGo { if !fromSQL[code] { t.Errorf("Go 默认单位清单里有 %q,init.sql 里没有", code) } } for code := range fromSQL { if _, ok := fromGo[code]; !ok { t.Errorf("init.sql 里有单位 %q,Go 默认清单里没有", code) } } if len(fromGo) != len(fromSQL) { t.Errorf("单位数量不一致:Go %d 个,init.sql %d 个", len(fromGo), len(fromSQL)) } }