material_manage_system/internal/repository/organization_test.go
文睿 张 696e90893b fix: 主数据编辑的两类静默失败
UPDATE 漏列:
- code_rules 的 SET 补上 category_id。此前改分类提示"保存成功"但不落库,
  是唯一用户可达的活缺陷(同页的分类下拉没有 disabled,且规则只能改不能删,
  没有"删了重建"这条退路)
- code_rules 有 uk_code_rules_org_category,补列后改到已占用的分类会撞唯一键,
  新建与更新统一翻成中文的 ErrDuplicateCodeRule
- 分类/单位/仓库/库位/伙伴的编码定为不可变:分类编码是编码规则前缀的来源
  (init.sql 的种子数据直接取 material_categories.code),伙伴编码是对外业务
  编号,改掉会让历史数据脱节且查不回来。service 的 Update* 不再要求编码非空
  ——原先是"必须传、传了却被丢弃";新建侧的必填校验保留
- 顺带补齐两处同款漏网:organization.go 的 code 与 partner.go 的 partner_code
  (前者 service 本就不校验编码,自洽,无需改)

记录不存在时的静默成功:
- DSN 开启 clientFoundRows,让 RowsAffected 返回"匹配"行数而非"实际改变"行数。
  否则"编辑时一个字段都没改"和"记录不存在"都返回 0,无法区分,直接加检查会把
  正常保存误判成记录不存在(真 MySQL 8.0 实测)
- 新增 repository.execExpectingRow,7 个 Update* 影响 0 行时返回 ErrRecordNotFound;
  伙伴走事务内的 SELECT ... FOR UPDATE,只需翻译 sql.ErrNoRows
- 既有 4 处 RowsAffected 用法逐处核对过,均不受该 flag 影响

测试:补 6 个 Update* 的存在性检查、编码必填约束的两端、唯一键翻译、DSN 拼接;
全部做过变异验证(抽掉实现后对应测试确实变红)。
2026-09-21 17:26:53 +08:00

114 lines
4.1 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"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/pkg/database"
)
func TestListRolesIncludesChinesePermissionNames(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
now := time.Date(2026, 9, 16, 9, 0, 0, 0, time.Local)
roleQuery := `SELECT id, code, name, enabled, created_at, updated_at FROM roles ORDER BY id`
mock.ExpectQuery(regexp.QuoteMeta(roleQuery)).WillReturnRows(
sqlmock.NewRows([]string{"id", "code", "name", "enabled", "created_at", "updated_at"}).
AddRow(2, "warehouse_admin", "仓库管理员", true, now, now),
)
permissionQuery := `SELECT rp.role_id, p.id, p.code, p.name
FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id
ORDER BY rp.role_id, p.code`
mock.ExpectQuery(regexp.QuoteMeta(permissionQuery)).WillReturnRows(
sqlmock.NewRows([]string{"role_id", "id", "code", "name"}).
AddRow(2, 12, "inventory.stock.read", "查看库存"),
)
roles, err := NewOrganizationRepository().ListRoles()
if err != nil {
t.Fatalf("list roles: %v", err)
}
if len(roles) != 1 || len(roles[0].Permissions) != 1 {
t.Fatalf("unexpected roles: %+v", roles)
}
if roles[0].Permissions[0].Name != "查看库存" {
t.Fatalf("permission name not returned: %+v", roles[0].Permissions[0])
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestUpdateRoleRejectsSuperAdminChanges(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
mock.ExpectBegin()
mock.ExpectQuery(regexp.QuoteMeta(`SELECT code FROM roles WHERE id = ? FOR UPDATE`)).WithArgs(int64(1)).WillReturnRows(sqlmock.NewRows([]string{"code"}).AddRow("super_admin"))
mock.ExpectRollback()
err = NewOrganizationRepository().UpdateRole(1, "超级管理员", false, nil)
if !errors.Is(err, ErrProtectedRole) {
t.Fatalf("expected protected role error, got %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestUpdateRoleReplacesPermissionsInTransaction(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
mock.ExpectBegin()
mock.ExpectQuery(regexp.QuoteMeta(`SELECT code FROM roles WHERE id = ? FOR UPDATE`)).WithArgs(int64(3)).WillReturnRows(sqlmock.NewRows([]string{"code"}).AddRow("purchaser"))
mock.ExpectExec(regexp.QuoteMeta(`UPDATE roles SET name = ?, enabled = ? WHERE id = ?`)).WithArgs("采购人员", true, int64(3)).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM role_permissions WHERE role_id = ?`)).WithArgs(int64(3)).WillReturnResult(sqlmock.NewResult(0, 2))
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO role_permissions (role_id, permission_id)
SELECT ?, id FROM permissions WHERE code IN (?, ?)`)).WithArgs(int64(3), "inventory.stock.read", "partner.supplier.read").WillReturnResult(sqlmock.NewResult(0, 2))
mock.ExpectCommit()
err = NewOrganizationRepository().UpdateRole(3, "采购人员", true, []string{"inventory.stock.read", "partner.supplier.read"})
if err != nil {
t.Fatalf("update role: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
// 组织不存在时(比如 id 打错)更新影响 0 行。此前返回 nil,接口提示"保存成功"
// 而实际什么都没改。
func TestOrganizationUpdateReportsMissingRecord(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
mock.ExpectExec("UPDATE organizations").WillReturnResult(sqlmock.NewResult(0, 0))
err = NewOrganizationRepository().Update(&model.Organization{ID: 404, Name: "改名后的组织"})
if !errors.Is(err, ErrRecordNotFound) {
t.Fatalf("影响 0 行应当返回 ErrRecordNotFound,实际 %v", err)
}
}