fix: 已停用角色导致用户无法保存编辑
This commit is contained in:
parent
688dffebaa
commit
aa017a32b4
|
|
@ -1,5 +1,6 @@
|
||||||
import { flushPromises, mount } from '@vue/test-utils'
|
import { flushPromises, mount } from '@vue/test-utils'
|
||||||
import ElementPlus, { ElMessage } from 'element-plus'
|
import ElementPlus, { ElMessage } from 'element-plus'
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||||
import Users from './Users.vue'
|
import Users from './Users.vue'
|
||||||
import { organizationsApi } from '../api/organizations'
|
import { organizationsApi } from '../api/organizations'
|
||||||
|
|
@ -99,6 +100,25 @@ describe('用户管理角色', () => {
|
||||||
expect(wrapper.text()).toContain('采购人员')
|
expect(wrapper.text()).toContain('采购人员')
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 列表里同样要标出来:停用角色的 tag 和启用的一模一样,管理员会以为这个用户还在
|
||||||
|
// 通过它拿权限,实际上 GenerateToken 已经把它过滤掉了。
|
||||||
|
test('列表里已停用的角色也带后缀,启用的不带', async () => {
|
||||||
|
usersStore.users = [{
|
||||||
|
...usersStore.users[0],
|
||||||
|
roles: [
|
||||||
|
{ id: 2, code: 'warehouse_admin', name: '仓库管理员', enabled: true, permissions: [] },
|
||||||
|
{ id: 3, code: 'purchaser', name: '采购人员', enabled: false, permissions: [] }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
const wrapper = mountUsers()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('采购人员(已停用)')
|
||||||
|
expect(wrapper.text()).toContain('仓库管理员')
|
||||||
|
expect(wrapper.text()).not.toContain('仓库管理员(已停用)')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('新增用户后列表与搜索框保持一致', () => {
|
describe('新增用户后列表与搜索框保持一致', () => {
|
||||||
|
|
@ -155,3 +175,113 @@ describe('新增用户后列表与搜索框保持一致', () => {
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 已停用角色在下拉里怎么呈现,直接决定"编辑用户"这条路走不走得通:保存是全量替换
|
||||||
|
// 角色,前端若摘不掉那个停用角色、后端又要求角色全部启用,这个用户就永远存不回去。
|
||||||
|
// 现在后端已放行"本来就持有的停用角色",前端这两条是配套:既让人摘得掉,也不让人
|
||||||
|
// 新分配一个停用角色。
|
||||||
|
describe('已停用角色的选择状态', () => {
|
||||||
|
const disabledRole = { id: 3, code: 'purchaser', name: '采购人员', enabled: false, permissions: [] }
|
||||||
|
const unassignedDisabledRole = { id: 4, code: 'obsolete_role', name: '作废角色', enabled: false, permissions: [] }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// 用户 7 正持有已停用的角色 3;角色 4 同样停用但该用户没有
|
||||||
|
usersStore.users = [{
|
||||||
|
id: 7,
|
||||||
|
username: 'zhangsan',
|
||||||
|
description: '测试用户',
|
||||||
|
created_at: '2026-09-16T09:00:00+08:00',
|
||||||
|
roles: [disabledRole]
|
||||||
|
}]
|
||||||
|
usersStore.updateUser.mockReset()
|
||||||
|
usersStore.updateUser.mockResolvedValue({ data: {} })
|
||||||
|
organizationsApi.listRoles.mockResolvedValue({
|
||||||
|
data: [
|
||||||
|
{ id: 2, code: 'warehouse_admin', name: '仓库管理员', enabled: true, permissions: [] },
|
||||||
|
disabledRole,
|
||||||
|
unassignedDisabledRole
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function openEdit(wrapper) {
|
||||||
|
const state = wrapper.vm.$.setupState
|
||||||
|
state.showEditModal(usersStore.users[0])
|
||||||
|
await flushPromises()
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOption(wrapper, value) {
|
||||||
|
return wrapper.findAllComponents(RoleOptionStub).find(option => option.props('value') === value)
|
||||||
|
}
|
||||||
|
|
||||||
|
test('用户已持有的停用角色不禁用,否则它摘不掉', async () => {
|
||||||
|
const wrapper = mountUsers()
|
||||||
|
await flushPromises()
|
||||||
|
const state = await openEdit(wrapper)
|
||||||
|
|
||||||
|
expect(state.form.role_ids).toEqual([3])
|
||||||
|
// disabled 跟着 form.role_ids 走:已选中就不禁用,tag 上才有关闭按钮
|
||||||
|
expect(findOption(wrapper, 3).props('disabled')).toBe(false)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('未持有的停用角色仍然禁用,不能通过编辑新分配', async () => {
|
||||||
|
const wrapper = mountUsers()
|
||||||
|
await flushPromises()
|
||||||
|
await openEdit(wrapper)
|
||||||
|
|
||||||
|
expect(findOption(wrapper, 4).props('disabled')).toBe(true)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('停用的角色在下拉里标注出来', async () => {
|
||||||
|
const wrapper = mountUsers()
|
||||||
|
await flushPromises()
|
||||||
|
await openEdit(wrapper)
|
||||||
|
|
||||||
|
expect(findOption(wrapper, 3).props('label')).toBe('采购人员(已停用)')
|
||||||
|
expect(findOption(wrapper, 2).props('label')).toBe('仓库管理员')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上面几条用的是 stub,只能证明 :disabled 这个 prop 算对了;而"管理员究竟能不能把那个
|
||||||
|
// 停用角色摘掉"取决于 element-plus 内部——multiple 模式下 tag 的关闭按钮绑在 option 的
|
||||||
|
// isDisabled 上(select2.mjs 的 closable: !selectDisabled && !item.isDisabled)。这里是
|
||||||
|
// 用真实组件把这条依赖验一遍,否则 Users.vue 的修复只是纸面上成立。
|
||||||
|
describe('element-plus 多选下停用角色 tag 的可关闭性', () => {
|
||||||
|
const roles = [
|
||||||
|
{ id: 2, name: 'warehouse_admin', enabled: true },
|
||||||
|
{ id: 3, name: 'purchaser', enabled: false }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 只铺一个最小场景:已选中那个停用的角色 3
|
||||||
|
function buildSelect(disabledExpression) {
|
||||||
|
return defineComponent({
|
||||||
|
template: `
|
||||||
|
<el-select v-model="ids" multiple>
|
||||||
|
<el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id"
|
||||||
|
:disabled="${disabledExpression}" />
|
||||||
|
</el-select>
|
||||||
|
`,
|
||||||
|
setup() {
|
||||||
|
return { ids: ref([3]), roles }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeIconCount(disabledExpression) {
|
||||||
|
const wrapper = mount(buildSelect(disabledExpression), { global: { plugins: [ElementPlus] } })
|
||||||
|
await flushPromises()
|
||||||
|
return wrapper.findAll('.el-tag__close').length
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Users.vue 的写法:已选中的停用角色 tag 保留关闭按钮', async () => {
|
||||||
|
expect(await closeIconCount('!role.enabled && !ids.includes(role.id)')).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('静态 disabled 的写法:同一个 tag 没有关闭按钮(原死锁现场)', async () => {
|
||||||
|
expect(await closeIconCount('!role.enabled')).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
:type="role.code === 'super_admin' ? 'danger' : 'info'"
|
:type="role.code === 'super_admin' ? 'danger' : 'info'"
|
||||||
class="role-tag"
|
class="role-tag"
|
||||||
>
|
>
|
||||||
{{ role.name }}
|
{{ roleLabel(role) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span v-if="!row.roles || row.roles.length === 0">-</span>
|
<span v-if="!row.roles || row.roles.length === 0">-</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -60,13 +60,18 @@
|
||||||
<el-input v-model="form.password" type="password" />
|
<el-input v-model="form.password" type="password" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色" required>
|
<el-form-item label="角色" required>
|
||||||
|
<!-- disabled 依赖 form.role_ids 而不是写成静态的 !role.enabled:停用的角色只对
|
||||||
|
"尚未选中"的项禁用,已选中的必须留住关闭按钮。element-plus 把 tag 的
|
||||||
|
closable 绑在 option 的 isDisabled 上,所以这个表达式会跟着重算。若改回
|
||||||
|
静态判断,用户身上已有的停用角色就摘不掉,而保存又是全量替换角色,
|
||||||
|
这个用户将永远存不回去(后端会拒绝)。 -->
|
||||||
<el-select v-model="form.role_ids" multiple style="width: 100%" placeholder="请选择至少一个角色">
|
<el-select v-model="form.role_ids" multiple style="width: 100%" placeholder="请选择至少一个角色">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="role in roles"
|
v-for="role in roles"
|
||||||
:key="role.id"
|
:key="role.id"
|
||||||
:label="role.name"
|
:label="roleLabel(role)"
|
||||||
:value="role.id"
|
:value="role.id"
|
||||||
:disabled="!role.enabled"
|
:disabled="!role.enabled && !form.role_ids.includes(role.id)"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
@ -126,6 +131,13 @@ async function loadRoles() {
|
||||||
roles.value = response.data || []
|
roles.value = response.data || []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 列表的 tag 和编辑下拉都用它。已停用的角色照旧显示(用户可能正持有它,编辑时得
|
||||||
|
// 看得见、摘得掉),但必须把"已停用"标出来:停用意味着不再授予权限,只显示角色名
|
||||||
|
// 会让管理员以为这个角色还在生效。
|
||||||
|
function roleLabel(role) {
|
||||||
|
return role.enabled ? role.name : `${role.name}(已停用)`
|
||||||
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
usersStore.fetchUsers(1, searchKeyword.value)
|
usersStore.fetchUsers(1, searchKeyword.value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1076,8 +1076,8 @@ func (h *Handler) CreateUser(c *gin.Context) {
|
||||||
|
|
||||||
user, err := h.authSvc.CreateUser(req.Username, req.Password, req.Description, req.RoleIDs)
|
user, err := h.authSvc.CreateUser(req.Username, req.Password, req.Description, req.RoleIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidRoles) {
|
if message := roleErrorText(err); message != "" {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response.InternalError(c, err.Error())
|
response.InternalError(c, err.Error())
|
||||||
|
|
@ -1107,8 +1107,8 @@ func (h *Handler) UpdateUser(c *gin.Context) {
|
||||||
|
|
||||||
err = h.authSvc.UpdateUser(id, req.Description, req.RoleIDs, req.Password)
|
err = h.authSvc.UpdateUser(id, req.Description, req.RoleIDs, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrInvalidRoles) || errors.Is(err, repository.ErrLastSuperAdmin) {
|
if message := roleErrorText(err); message != "" {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response.InternalError(c, err.Error())
|
response.InternalError(c, err.Error())
|
||||||
|
|
@ -1133,8 +1133,8 @@ func (h *Handler) DeleteUser(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.authSvc.DeleteUser(id); err != nil {
|
if err := h.authSvc.DeleteUser(id); err != nil {
|
||||||
if errors.Is(err, repository.ErrLastSuperAdmin) {
|
if message := roleErrorText(err); message != "" {
|
||||||
response.BadRequest(c, err.Error())
|
response.BadRequest(c, message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response.InternalError(c, err.Error())
|
response.InternalError(c, err.Error())
|
||||||
|
|
@ -1144,6 +1144,21 @@ func (h *Handler) DeleteUser(c *gin.Context) {
|
||||||
response.SuccessWithMessage(c, "user deleted successfully", nil)
|
response.SuccessWithMessage(c, "user deleted successfully", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// roleErrorText 把角色相关的仓储层错误翻成面向用户的中文,返回空字符串表示这个错误
|
||||||
|
// 与角色无关。翻译放在 handler 而非仓储层:仓储层的错误常量是给开发者看的标识,不该
|
||||||
|
// 承担界面文案。前端的 utils/errorText.js 只放行带中文的消息,英文会被换成笼统的
|
||||||
|
// "请求参数有误",用户看不出问题出在角色上。
|
||||||
|
func roleErrorText(err error) string {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, repository.ErrInvalidRoles):
|
||||||
|
return "所选角色无效或已被停用,请重新选择"
|
||||||
|
case errors.Is(err, repository.ErrLastSuperAdmin):
|
||||||
|
return "不能移除最后一个超级管理员"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Application handlers
|
// Application handlers
|
||||||
|
|
||||||
func (h *Handler) CreateApplication(c *gin.Context) {
|
func (h *Handler) CreateApplication(c *gin.Context) {
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ func (r *UserRepository) update(user *model.User, roleIDs []int64, updatePasswor
|
||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
roleIDs = uniqueRoleIDs(roleIDs)
|
roleIDs = uniqueRoleIDs(roleIDs)
|
||||||
if err := validateRolesTx(tx, roleIDs); err != nil {
|
if err := validateRolesTx(tx, user.ID, roleIDs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := protectLastSuperAdminTx(tx, user.ID, roleIDs); err != nil {
|
if err := protectLastSuperAdminTx(tx, user.ID, roleIDs); err != nil {
|
||||||
|
|
@ -208,23 +208,37 @@ func loadRolesForUsers(queryer interface {
|
||||||
|
|
||||||
func assignRolesTx(tx *sql.Tx, userID int64, roleIDs []int64) error {
|
func assignRolesTx(tx *sql.Tx, userID int64, roleIDs []int64) error {
|
||||||
roleIDs = uniqueRoleIDs(roleIDs)
|
roleIDs = uniqueRoleIDs(roleIDs)
|
||||||
if err := validateRolesTx(tx, roleIDs); err != nil {
|
if err := validateRolesTx(tx, userID, roleIDs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return insertUserRolesTx(tx, userID, roleIDs)
|
return insertUserRolesTx(tx, userID, roleIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRolesTx(tx *sql.Tx, roleIDs []int64) error {
|
// validateRolesTx 校验 roleIDs 是否都是可分配给该用户的角色。
|
||||||
|
// 可分配 = 角色处于启用状态,或它本来就是该用户当前持有的角色。
|
||||||
|
// - 新建用户时 user_roles 里还没有该用户的记录,EXISTS 恒为假,等价于原来的
|
||||||
|
// "只允许启用中的角色"。
|
||||||
|
// - 编辑用户时前端会把现有角色原样回传,其中可能含已被停用的。停用只是不再授予
|
||||||
|
// 权限(GenerateToken 会过滤掉停用角色),没必要把已有分配一并抹掉;若在这里
|
||||||
|
// 拒绝,管理员就再也保存不了这个用户:下拉里的停用角色是禁用项,已选中的 tag
|
||||||
|
// 也没有关闭按钮。
|
||||||
|
//
|
||||||
|
// 必须在 DELETE FROM user_roles 之前调用,否则 EXISTS 取不到旧分配。
|
||||||
|
func validateRolesTx(tx *sql.Tx, userID int64, roleIDs []int64) error {
|
||||||
if len(roleIDs) == 0 {
|
if len(roleIDs) == 0 {
|
||||||
return ErrInvalidRoles
|
return ErrInvalidRoles
|
||||||
}
|
}
|
||||||
placeholders := strings.TrimSuffix(strings.Repeat("?, ", len(roleIDs)), ", ")
|
placeholders := strings.TrimSuffix(strings.Repeat("?, ", len(roleIDs)), ", ")
|
||||||
args := make([]interface{}, len(roleIDs))
|
args := make([]interface{}, 0, len(roleIDs)+1)
|
||||||
for index, id := range roleIDs {
|
for _, id := range roleIDs {
|
||||||
args[index] = id
|
args = append(args, id)
|
||||||
}
|
}
|
||||||
|
args = append(args, userID)
|
||||||
var count int
|
var count int
|
||||||
query := fmt.Sprintf(`SELECT COUNT(*) FROM roles WHERE enabled = TRUE AND id IN (%s)`, placeholders)
|
query := fmt.Sprintf(`SELECT COUNT(*) FROM roles r
|
||||||
|
WHERE r.id IN (%s)
|
||||||
|
AND (r.enabled = TRUE OR EXISTS (
|
||||||
|
SELECT 1 FROM user_roles ur WHERE ur.role_id = r.id AND ur.user_id = ?))`, placeholders)
|
||||||
if err := tx.QueryRow(query, args...).Scan(&count); err != nil {
|
if err := tx.QueryRow(query, args...).Scan(&count); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,11 @@ func TestUpdateRejectsRemovingRoleFromLastSuperAdmin(t *testing.T) {
|
||||||
database.DB = db
|
database.DB = db
|
||||||
|
|
||||||
mock.ExpectBegin()
|
mock.ExpectBegin()
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM roles WHERE enabled = TRUE AND id IN (?)`)).
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM roles r
|
||||||
WithArgs(int64(2)).
|
WHERE r.id IN (?)
|
||||||
|
AND (r.enabled = TRUE OR EXISTS (
|
||||||
|
SELECT 1 FROM user_roles ur WHERE ur.role_id = r.id AND ur.user_id = ?))`)).
|
||||||
|
WithArgs(int64(2), int64(7)).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM roles WHERE code = 'super_admin' FOR UPDATE`)).
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM roles WHERE code = 'super_admin' FOR UPDATE`)).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||||
|
|
@ -110,6 +113,80 @@ func TestUpdateRejectsRemovingRoleFromLastSuperAdmin(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 编辑用户时把已被停用的角色原样回传必须能保存成功。停用只是不再授予权限,不要求
|
||||||
|
// 管理员顺手把它从用户身上摘掉;旧实现在这里返回 ErrInvalidRoles,而前端下拉里的
|
||||||
|
// 停用角色是禁用项、已选中的 tag 也关不掉,结果这个用户再也编辑不了。
|
||||||
|
func TestUpdateKeepsDisabledRoleAlreadyAssignedToUser(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()
|
||||||
|
// count 命中 1:角色 3 虽然已停用,但该用户当前正持有它,EXISTS 放行。
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM roles r
|
||||||
|
WHERE r.id IN (?)
|
||||||
|
AND (r.enabled = TRUE OR EXISTS (
|
||||||
|
SELECT 1 FROM user_roles ur WHERE ur.role_id = r.id AND ur.user_id = ?))`)).
|
||||||
|
WithArgs(int64(3), int64(7)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM roles WHERE code = 'super_admin' FOR UPDATE`)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM user_roles WHERE user_id = ? AND role_id = ?`)).
|
||||||
|
WithArgs(int64(7), int64(1)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
||||||
|
mock.ExpectExec(regexp.QuoteMeta(`UPDATE users SET description = ? WHERE id = ?`)).
|
||||||
|
WithArgs("改个描述", int64(7)).
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM user_roles WHERE user_id = ?`)).
|
||||||
|
WithArgs(int64(7)).
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)`)).
|
||||||
|
WithArgs(int64(7), int64(3)).
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||||
|
mock.ExpectCommit()
|
||||||
|
|
||||||
|
user := &model.User{ID: 7, Description: "改个描述"}
|
||||||
|
if err := NewUserRepository().Update(user, []int64{3}); err != nil {
|
||||||
|
t.Fatalf("update user: %v", err)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反面:把一个该用户还没持有的停用角色塞进来仍要拒绝。EXISTS 为假,count 对不上,
|
||||||
|
// 这样"不能通过接口新分配停用角色"这条约束就没有被上面那个放行口子带松。
|
||||||
|
func TestUpdateRejectsNewlyAssignedDisabledRole(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()
|
||||||
|
// count 为 0:角色 4 已停用,且不在该用户当前持有的角色里。
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM roles r
|
||||||
|
WHERE r.id IN (?)
|
||||||
|
AND (r.enabled = TRUE OR EXISTS (
|
||||||
|
SELECT 1 FROM user_roles ur WHERE ur.role_id = r.id AND ur.user_id = ?))`)).
|
||||||
|
WithArgs(int64(4), int64(7)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
||||||
|
mock.ExpectRollback()
|
||||||
|
|
||||||
|
user := &model.User{ID: 7, Description: "改个描述"}
|
||||||
|
err = NewUserRepository().Update(user, []int64{4})
|
||||||
|
if !errors.Is(err, ErrInvalidRoles) {
|
||||||
|
t.Fatalf("update error = %v, want ErrInvalidRoles", err)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateUserAssignsMultipleEnabledRolesInTransaction(t *testing.T) {
|
func TestCreateUserAssignsMultipleEnabledRolesInTransaction(t *testing.T) {
|
||||||
db, mock, err := sqlmock.New()
|
db, mock, err := sqlmock.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -122,8 +199,13 @@ func TestCreateUserAssignsMultipleEnabledRolesInTransaction(t *testing.T) {
|
||||||
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO users (username, password, description) VALUES (?, ?, ?)`)).
|
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO users (username, password, description) VALUES (?, ?, ?)`)).
|
||||||
WithArgs("zhangsan", "hash", "测试用户").
|
WithArgs("zhangsan", "hash", "测试用户").
|
||||||
WillReturnResult(sqlmock.NewResult(7, 1))
|
WillReturnResult(sqlmock.NewResult(7, 1))
|
||||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM roles WHERE enabled = TRUE AND id IN (?, ?)`)).
|
// 新建的用户在 user_roles 里还没有记录,EXISTS 恒为假,所以这里等价于
|
||||||
WithArgs(int64(2), int64(3)).
|
// "只允许启用中的角色"。
|
||||||
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT COUNT(*) FROM roles r
|
||||||
|
WHERE r.id IN (?, ?)
|
||||||
|
AND (r.enabled = TRUE OR EXISTS (
|
||||||
|
SELECT 1 FROM user_roles ur WHERE ur.role_id = r.id AND ur.user_id = ?))`)).
|
||||||
|
WithArgs(int64(2), int64(3), int64(7)).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(2))
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(2))
|
||||||
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO user_roles (user_id, role_id) VALUES (?, ?), (?, ?)`)).
|
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO user_roles (user_id, role_id) VALUES (?, ?), (?, ?)`)).
|
||||||
WithArgs(int64(7), int64(2), int64(7), int64(3)).
|
WithArgs(int64(7), int64(2), int64(7), int64(3)).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user