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 拼接; 全部做过变异验证(抽掉实现后对应测试确实变红)。
114 lines
4.1 KiB
Go
114 lines
4.1 KiB
Go
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)
|
||
}
|
||
}
|