修复 21 个测试与代码脱节导致的既有失败
go test ./... 从 21 个失败回到 0。根因都是测试没跟上代码改动,其中两处
被「自动补位」的补丁 hack 掩盖住了:
- handler 的 materialRow 在「列数 20、值 18」时自动插两个 nil,掩盖了
materials 新增 supplier_id/manufacturer_id 后的列错位。它碰巧补对位置,
而 order 测试的 activeMaterialRows 没走这个 helper,直接 panic。
已删掉补丁、把测试数据补齐到 20 列。
- 导入测试的 buildTestWorkbookWithHeaders 按 7 列给数据补位,而表头这时
已经扩到 9 列(多了供应商、制造商),把「大类/小类」静默塞进了
「来源/备注」。已按 materialImportHeaders 的实际列序修正补位。
其余为非补丁式的期望过时:materials 的 INSERT 参数补到 15 个、物料列表
查询结果集补到 20 列、导入模板表头断言改为按 materialImportHeaders 取末列
而不是写死下标。
顺带清掉已废弃的升级路径残留:upgrades/ 目录在 85525c97 已删除,增量升级
不再使用,因此删除 scripts/sync-company-integrity.ps1(把 checks SQL 嵌入
upgrade SQL 的生成器)与它的测试,application.go 里指向 upgrades 的错误
文案改为只提 init.sql。
This commit is contained in:
parent
3cc783b5ff
commit
18df4f48e4
|
|
@ -35,10 +35,8 @@ const (
|
|||
)
|
||||
|
||||
// materialRow 构造单行结果集;不传值表示空结果集(用于"查不到"的路径)。
|
||||
// 传值时长度必须和 columns 对齐,列顺序见 materialColumns。
|
||||
func materialRow(columns []string, values ...driver.Value) *sqlmock.Rows {
|
||||
if len(columns) == 20 && len(values) == 18 {
|
||||
values = append(values[:7], append([]driver.Value{nil, nil}, values[7:]...)...)
|
||||
}
|
||||
rows := sqlmock.NewRows(columns)
|
||||
if len(values) > 0 {
|
||||
rows.AddRow(values...)
|
||||
|
|
@ -97,7 +95,7 @@ func TestMaterialListScopesToRequestedOrganization(t *testing.T) {
|
|||
WillReturnRows(materialRow([]string{"count"}, int64(1)))
|
||||
mock.ExpectQuery(materialListPattern).WithArgs(tc.orgID, "active", 20, 0).
|
||||
WillReturnRows(materialRow(materialColumns(),
|
||||
int64(7), tc.orgID, tc.wantCode, "轴承", nil, "", nil, nil, nil, "", "", int64(12), nil, "pcs", "active", 1, time.Now(), time.Now()))
|
||||
int64(7), tc.orgID, tc.wantCode, "轴承", nil, "", nil, nil, nil, nil, nil, "", "", int64(12), nil, "pcs", "active", 1, time.Now(), time.Now()))
|
||||
|
||||
w := newMaterialRequest(t, "GET", "/api/materials", "", tc.orgID)
|
||||
if w.Code != 200 || !strings.Contains(w.Body.String(), tc.wantCode) {
|
||||
|
|
@ -153,12 +151,12 @@ func TestCreateMaterialUsesHeaderOrganizationInsteadOfBody(t *testing.T) {
|
|||
mock.ExpectQuery(lockCodeRulePattern).WithArgs(int64(1), int64(12)).
|
||||
WillReturnRows(materialRow([]string{"id", "organization_id", "category_id", "prefix", "sequence_length", "next_sequence"}))
|
||||
mock.ExpectExec(materialInsertPattern).
|
||||
WithArgs(int64(1), "HDR-001", "轴承", nil, "", nil, nil, nil, int64(12), nil, "个", "active", 1).
|
||||
WithArgs(int64(1), "HDR-001", "轴承", nil, "", nil, "", "", nil, nil, int64(12), nil, "个", "active", 1).
|
||||
WillReturnResult(sqlmock.NewResult(7, 1))
|
||||
mock.ExpectCommit()
|
||||
mock.ExpectQuery(materialGetByIDPattern).WithArgs(int64(1), int64(7)).
|
||||
WillReturnRows(materialRow(materialColumns(),
|
||||
int64(7), int64(1), "HDR-001", "轴承", nil, "", nil, nil, nil, "", "", int64(12), nil, "个", "active", 1, time.Now(), time.Now()))
|
||||
int64(7), int64(1), "HDR-001", "轴承", nil, "", nil, nil, nil, nil, nil, "", "", int64(12), nil, "个", "active", 1, time.Now(), time.Now()))
|
||||
mock.ExpectExec(materialLogInsertPattern).
|
||||
WithArgs("CREATE", int64(7), "", sqlmock.AnyArg(), sqlmock.AnyArg(), int64(7)).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ func emptyWarehouseSummary() *sqlmock.Rows {
|
|||
func activeMaterialRows(id int64) *sqlmock.Rows {
|
||||
now := time.Now()
|
||||
return sqlmock.NewRows(materialColumns()).
|
||||
AddRow(id, 1, "MAT-005", "轴承", nil, "", nil, nil, nil, "", "", 1, 1, "个", "active", 1, now, now)
|
||||
AddRow(id, 1, "MAT-005", "轴承", nil, "", nil, nil, nil, nil, nil, "", "", 1, 1, "个", "active", 1, now, now)
|
||||
}
|
||||
|
||||
// TestOrderDetailScopesToOrganization:单据详情按请求公司过滤,传别家公司的单据 ID 读不到,回 404。
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ func (r *ApplicationRepository) CheckApplicationSchema() error {
|
|||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("数据库结构缺少 %s.%s:请先执行 scripts/init.sql(空库)或 scripts/upgrades/2026-10-08-company-master-data-isolation.sql(升级)完成建表/升级", c.table, c.column)
|
||||
return fmt.Errorf("数据库结构缺少 %s.%s:请用 scripts/init.sql 重建数据库(本地库没有增量升级路径)", c.table, c.column)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1,133 +0,0 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A forgotten regeneration must fail -Check without rewriting files; a source
|
||||
// rule change must subsequently govern the real self-contained migration.
|
||||
func isolationGenerator(t *testing.T, args ...string) ([]byte, error) {
|
||||
t.Helper()
|
||||
shell, err := exec.LookPath("pwsh")
|
||||
if err != nil {
|
||||
shell, err = exec.LookPath("powershell")
|
||||
}
|
||||
if err != nil {
|
||||
t.Skip("PowerShell is required to verify the SQL generation tool")
|
||||
}
|
||||
commandArgs := []string{"-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", filepath.Join("..", "..", "scripts", "sync-company-integrity.ps1")}
|
||||
return exec.Command(shell, append(commandArgs, args...)...).CombinedOutput()
|
||||
}
|
||||
|
||||
func generatedIsolationFixture(t *testing.T) (string, string) {
|
||||
t.Helper()
|
||||
if output, err := isolationGenerator(t, "-Check"); err != nil {
|
||||
t.Fatalf("committed migration must be in sync: %v\n%s", err, output)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
source := filepath.Join(dir, "checks.sql")
|
||||
migration := filepath.Join(dir, "upgrade.sql")
|
||||
for _, copy := range []struct{ from, to string }{
|
||||
{filepath.Join("..", "..", "scripts", "checks", "company-master-data-integrity.sql"), source},
|
||||
{filepath.Join("..", "..", "scripts", "upgrades", "2026-10-08-company-master-data-isolation.sql"), migration},
|
||||
} {
|
||||
content, err := os.ReadFile(copy.from)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = os.WriteFile(copy.to, content, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return source, migration
|
||||
}
|
||||
|
||||
func changeIsolationCheckFixture(t *testing.T, source string) {
|
||||
t.Helper()
|
||||
content, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The fixture adds a literal company-2 blocker. Expectations below are hand
|
||||
// derived from seedIsolationHistory (material 2 belongs to company 2).
|
||||
changed := strings.Replace(string(content), "-- END COMPANY INTEGRITY CORE", `INSERT INTO tmp_company_integrity_anomalies
|
||||
SELECT 'generated_rule',id,'Company two requires business review' FROM materials WHERE organization_id=2;
|
||||
-- END COMPANY INTEGRITY CORE`, 1)
|
||||
if changed == string(content) {
|
||||
t.Fatal("canonical check core marker missing")
|
||||
}
|
||||
if err = os.WriteFile(source, []byte(changed), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyIsolationIntegrityGeneratorDetectsAndRepairsDrift(t *testing.T) {
|
||||
source, migration := generatedIsolationFixture(t)
|
||||
before, err := os.ReadFile(migration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
changeIsolationCheckFixture(t, source)
|
||||
output, err := isolationGenerator(t, "-SourcePath", source, "-MigrationPath", migration, "-Check")
|
||||
if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 1 {
|
||||
t.Fatalf("stale embed must fail -Check with exit 1, got %v\n%s", err, output)
|
||||
}
|
||||
after, err := os.ReadFile(migration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(after, before) {
|
||||
t.Fatal("-Check mutated stale migration")
|
||||
}
|
||||
if output, err = isolationGenerator(t, "-SourcePath", source, "-MigrationPath", migration); err != nil {
|
||||
t.Fatalf("sync changed check: %v\n%s", err, output)
|
||||
}
|
||||
once, err := os.ReadFile(migration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if output, err = isolationGenerator(t, "-SourcePath", source, "-MigrationPath", migration); err != nil {
|
||||
t.Fatalf("repeat sync: %v\n%s", err, output)
|
||||
}
|
||||
twice, err := os.ReadFile(migration)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(once, twice) {
|
||||
t.Fatal("generation was not deterministic")
|
||||
}
|
||||
if output, err = isolationGenerator(t, "-SourcePath", source, "-MigrationPath", migration, "-Check"); err != nil {
|
||||
t.Fatalf("regenerated migration must pass -Check: %v\n%s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompanyIsolationGeneratedRuleBlocksActualUpgrade(t *testing.T) {
|
||||
db := isolationDB(t, true)
|
||||
seedIsolationHistory(t, db)
|
||||
source, migration := generatedIsolationFixture(t)
|
||||
changeIsolationCheckFixture(t, source)
|
||||
if output, err := isolationGenerator(t, "-SourcePath", source, "-MigrationPath", migration); err != nil {
|
||||
t.Fatalf("generate fixture: %v\n%s", err, output)
|
||||
}
|
||||
if err := runIsolationSQL(db, source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var count int
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM tmp_company_integrity_anomalies WHERE issue='generated_rule' AND entity_id=2`).Scan(&count); err != nil || count != 1 {
|
||||
t.Fatalf("standalone fixture rule=%d err=%v", count, err)
|
||||
}
|
||||
if err := runIsolationSQL(db, migration); err == nil || !strings.Contains(err.Error(), "integrity anomalies") {
|
||||
t.Fatalf("generated rule must block actual upgrade, got %v", err)
|
||||
}
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM tmp_company_integrity_anomalies WHERE issue='generated_rule' AND entity_id=2`).Scan(&count); err != nil || count != 1 {
|
||||
t.Fatalf("embedded fixture rule=%d err=%v", count, err)
|
||||
}
|
||||
if err := db.QueryRow(`SELECT COUNT(*) FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='stock_logs' AND column_name='organization_id'`).Scan(&count); err != nil || count != 0 {
|
||||
t.Fatalf("generated blocker changed business schema: %d %v", count, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -48,8 +48,8 @@ func TestMaterialRepoImportTxAllocatesCodesAndInsertsOneByOne(t *testing.T) {
|
|||
// 两行折叠成一条多值 INSERT
|
||||
mock.ExpectExec(regexp.QuoteMeta(insertMaterialBatchStmt)).
|
||||
WithArgs(
|
||||
int64(1), "A010202", "螺栓", nil, "", nil, nil, nil, nil, int64(12), nil, "个", "active", 1,
|
||||
int64(1), "A010203", "螺母", nil, "", nil, nil, nil, nil, int64(12), nil, "个", "active", 1,
|
||||
int64(1), "A010202", "螺栓", nil, "", nil, nil, nil, nil, nil, int64(12), nil, "个", "active", 1,
|
||||
int64(1), "A010203", "螺母", nil, "", nil, nil, nil, nil, nil, int64(12), nil, "个", "active", 1,
|
||||
).
|
||||
WillReturnResult(sqlmock.NewResult(31, 2))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, material_code FROM materials WHERE organization_id = ? AND material_code IN (?,?)`)).
|
||||
|
|
|
|||
|
|
@ -234,7 +234,9 @@ func TestBuildMaterialImportTemplateRoundTrips(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("read rows: %v", err)
|
||||
}
|
||||
if len(headers) == 0 || headers[0][0] != "物料名称" || headers[0][6] != "小类" {
|
||||
// 表头列数以 materialImportHeaders 为准:写死下标会在增删列时静默指向别的列。
|
||||
if len(headers) == 0 || len(headers[0]) != len(materialImportHeaders) ||
|
||||
headers[0][0] != "物料名称" || headers[0][len(headers[0])-1] != "小类" {
|
||||
t.Fatalf("表头 = %v", headers)
|
||||
}
|
||||
// 模板必须能被自己的解析器接受(表头一致)
|
||||
|
|
@ -254,11 +256,14 @@ func buildTestWorkbook(t *testing.T, dataRows [][]string) []byte {
|
|||
// buildTestWorkbookWithHeaders 允许指定表头,用来覆盖缺列、乱序、多余列的解析分支。
|
||||
func buildTestWorkbookWithHeaders(t *testing.T, headers []string, dataRows [][]string) []byte {
|
||||
t.Helper()
|
||||
// 旧的五列测试数据为「名称、规格、单位、大类、小类」;补上新增的两个选填列。
|
||||
// 测试数据用五列简写「名称、规格、单位、大类、小类」,其余列补空。
|
||||
// 补位的顺序必须与 materialImportHeaders 一致(物料名称、规格、单位、供应商、
|
||||
// 制造商、来源、备注、大类、小类):表头改列时这里要同步改,否则大类小类会被
|
||||
// 静默解析成来源和备注,测试却只是「结果对不上」。
|
||||
if strings.Join(headers, "\x00") == strings.Join(materialImportHeaders, "\x00") {
|
||||
for i, row := range dataRows {
|
||||
if len(row) == 5 {
|
||||
dataRows[i] = []string{row[0], row[1], row[2], "", "", row[3], row[4]}
|
||||
dataRows[i] = []string{row[0], row[1], row[2], "", "", "", "", row[3], row[4]}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ func TestMaterialServiceReportsDuplicateManuallyEditedCode(t *testing.T) {
|
|||
expectActiveSubcategory(mock)
|
||||
expectLockedCodeRule(mock, &next)
|
||||
mock.ExpectExec(regexp.QuoteMeta(insertMaterialStmt)).
|
||||
WithArgs(int64(1), "CUSTOM-001", "轴承", nil, "", nil, nil, nil, int64(12), nil, "个", "active", 1).
|
||||
WithArgs(int64(1), "CUSTOM-001", "轴承", nil, "", nil, "", "", nil, nil, int64(12), nil, "个", "active", 1).
|
||||
WillReturnError(errors.New("Error 1062: Duplicate entry 'CUSTOM-001' for key 'uk_materials_org_code'"))
|
||||
mock.ExpectRollback()
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ func outboundMock(t *testing.T) sqlmock.Sqlmock {
|
|||
|
||||
// activeMaterialRow 造一行启用物料,供物料归属/存在性校验与明细补名使用。
|
||||
func activeMaterialRow(id int64) *sqlmock.Rows {
|
||||
return sqlmock.NewRows([]string{"id", "organization_id", "material_code", "name", "material_image", "spec", "manufacturer_model", "supplier_id", "manufacturer_id", "supplier_name", "manufacturer_name", "category_id", "base_unit_id", "unit", "status", "version", "created_at", "updated_at"}).
|
||||
AddRow(id, 1, "M-1", "Material", nil, "", nil, nil, nil, "", "", 1, 1, "pc", "active", 1, time.Now(), time.Now())
|
||||
return sqlmock.NewRows([]string{"id", "organization_id", "material_code", "name", "material_image", "spec", "manufacturer_model", "source", "remark", "supplier_id", "manufacturer_id", "supplier_name", "manufacturer_name", "category_id", "base_unit_id", "unit", "status", "version", "created_at", "updated_at"}).
|
||||
AddRow(id, 1, "M-1", "Material", nil, "", nil, nil, nil, nil, nil, "", "", 1, 1, "pc", "active", 1, time.Now(), time.Now())
|
||||
}
|
||||
|
||||
func expectActiveMaterial(mock sqlmock.Sqlmock, id int64) {
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
# The standalone check is canonical; the upgrade stays self-contained for mysql SOURCE.
|
||||
# Run from any directory. Use -Check in verification to reject an outdated embed.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$SourcePath,
|
||||
[string]$MigrationPath,
|
||||
[switch]$Check
|
||||
)
|
||||
|
||||
if (-not $SourcePath) { $SourcePath = Join-Path $PSScriptRoot 'checks/company-master-data-integrity.sql' }
|
||||
if (-not $MigrationPath) { $MigrationPath = Join-Path $PSScriptRoot 'upgrades/2026-10-08-company-master-data-isolation.sql' }
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-MarkedContent {
|
||||
param([string]$Text, [string]$BeginMarker, [string]$EndMarker)
|
||||
|
||||
$begin = $Text.IndexOf($BeginMarker, [System.StringComparison]::Ordinal)
|
||||
$end = $Text.IndexOf($EndMarker, [System.StringComparison]::Ordinal)
|
||||
if ($begin -lt 0 -or $end -le $begin -or
|
||||
$Text.IndexOf($BeginMarker, $begin + $BeginMarker.Length, [System.StringComparison]::Ordinal) -ge 0 -or
|
||||
$Text.IndexOf($EndMarker, $end + $EndMarker.Length, [System.StringComparison]::Ordinal) -ge 0) {
|
||||
throw "Expected one ordered marker pair: $BeginMarker / $EndMarker"
|
||||
}
|
||||
$contentStart = $begin + $BeginMarker.Length
|
||||
return @{
|
||||
Start = $contentStart
|
||||
End = $end
|
||||
Content = $Text.Substring($contentStart, $end - $contentStart)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$source = (Resolve-Path -LiteralPath $SourcePath).Path
|
||||
$migration = (Resolve-Path -LiteralPath $MigrationPath).Path
|
||||
if ($source -eq $migration) { throw 'Source and migration must be different files' }
|
||||
|
||||
# Normalize only line endings: content, statement order and whitespace in the
|
||||
# canonical block otherwise remain exact. Output is deterministic UTF-8/LF.
|
||||
$sourceText = [System.IO.File]::ReadAllText($source).Replace("`r`n", "`n").Replace("`r", "`n")
|
||||
$migrationText = [System.IO.File]::ReadAllText($migration).Replace("`r`n", "`n").Replace("`r", "`n")
|
||||
$core = Get-MarkedContent $sourceText '-- BEGIN COMPANY INTEGRITY CORE' '-- END COMPANY INTEGRITY CORE'
|
||||
$embedded = Get-MarkedContent $migrationText '-- BEGIN GENERATED COMPANY INTEGRITY CHECKS' '-- END GENERATED COMPANY INTEGRITY CHECKS'
|
||||
$expected = $migrationText.Substring(0, $embedded.Start) + $core.Content + $migrationText.Substring($embedded.End)
|
||||
|
||||
if ($Check) {
|
||||
if ($migrationText -cne $expected) {
|
||||
Write-Error 'Integrity SQL embed is stale. Run scripts/sync-company-integrity.ps1 to regenerate it.' -ErrorAction Continue
|
||||
exit 1
|
||||
}
|
||||
Write-Output 'Integrity SQL embed is current.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
[System.IO.File]::WriteAllText($migration, $expected, [System.Text.UTF8Encoding]::new($false))
|
||||
Write-Output 'Integrity SQL embed regenerated from the canonical check.'
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Error $_ -ErrorAction Continue
|
||||
exit 1
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user