forked from wangziqi/gongxue-base
feat: manage tenant badges
This commit is contained in:
@@ -49,6 +49,7 @@ const ids = {
|
||||
tenantClassOther: '00000000-0000-0000-0000-000000000852',
|
||||
examDate: '00000000-0000-0000-0000-000000000861',
|
||||
tenantExamDate: '00000000-0000-0000-0000-000000000862',
|
||||
tenantBadge: '00000000-0000-0000-0000-000000000871',
|
||||
questionBank: '00000000-0000-0000-0000-000000000400',
|
||||
publicQuestionBankGrant: '00000000-0000-0000-0000-000000000906',
|
||||
};
|
||||
@@ -3562,6 +3563,181 @@ async function testTenantAdminOps() {
|
||||
});
|
||||
assert.ok(publicExamDates.items?.some(item => item.id === ids.tenantExamDate), 'public catalog should expose tenant admin exam date');
|
||||
|
||||
const studentBadgeAdminDenied = await request('/api/tenant-admin/badges', {
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentBadgeAdminDenied.code, 'TENANT_ADMIN_REQUIRED', 'student should not access badge admin');
|
||||
|
||||
const badge = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: ids.tenantBadge,
|
||||
legacyId: 'integration-badge-first-practice',
|
||||
name: '集成测试首练勋章',
|
||||
description: '完成首次练习后展示',
|
||||
category: 'practice',
|
||||
iconUrl: 'https://example.test/badges/first-practice.png',
|
||||
level: 1,
|
||||
unlockType: 'manual',
|
||||
conditionField: 'practice.completedCount',
|
||||
conditionOperator: 'gte',
|
||||
conditionValue: 1,
|
||||
conditionExtra: { mode: 'any' },
|
||||
metadata: { source: 'integration-test' },
|
||||
order: 3,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
assert.equal(badge.item?.id, ids.tenantBadge, 'tenant admin should create badge with stable id');
|
||||
assert.equal(badge.item?.category, 'practice', 'badge should persist category');
|
||||
|
||||
const badgeByLegacy = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
legacyId: 'integration-badge-first-practice',
|
||||
name: '集成测试首练勋章升级',
|
||||
description: '同 legacyId 应幂等更新原勋章',
|
||||
category: 'practice',
|
||||
iconUrl: 'https://example.test/badges/first-practice-v2.png',
|
||||
level: 2,
|
||||
unlockType: 'manual',
|
||||
conditionField: 'practice.completedCount',
|
||||
conditionOperator: 'gte',
|
||||
conditionValue: 1,
|
||||
conditionExtra: { mode: 'any', version: 2 },
|
||||
metadata: { source: 'integration-test', updated: true },
|
||||
order: 2,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
assert.equal(badgeByLegacy.item?.id, ids.tenantBadge, 'badge legacyId should update existing badge idempotently');
|
||||
assert.equal(badgeByLegacy.item?.level, 2, 'badge update should persist new level');
|
||||
|
||||
const badgeConflict = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: '00000000-0000-0000-0000-000000000872',
|
||||
legacyId: 'integration-badge-first-practice',
|
||||
name: '冲突勋章',
|
||||
},
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(badgeConflict.code, 'BADGE_ID_CONFLICT', 'badge id and legacyId conflict should be explicit');
|
||||
|
||||
const invalidBadgeCategory = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
name: '非法分类勋章',
|
||||
category: 'unsafe-category',
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidBadgeCategory.code, 'INVALID_FIELD_VALUE', 'badge admin should reject invalid category');
|
||||
|
||||
const invalidBadgeNumber = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
name: '非法数值勋章',
|
||||
category: 'practice',
|
||||
conditionValue: 'not-a-number',
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidBadgeNumber.code, 'INVALID_NUMBER', 'badge admin should reject invalid numeric condition');
|
||||
|
||||
const invalidBadgeId = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: 'not-a-uuid',
|
||||
name: '非法 ID 勋章',
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidBadgeId.code, 'INVALID_UUID', 'badge admin should reject invalid badge id before database cast');
|
||||
|
||||
const badges = await request('/api/tenant-admin/badges', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { category: 'practice', includeInactive: 'true', limit: 20 },
|
||||
});
|
||||
assert.ok(badges.items?.some(item => item.id === ids.tenantBadge), 'tenant admin should list badge by category');
|
||||
|
||||
const grant = await request('/api/tenant-admin/badge-grants', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
badgeId: ids.tenantBadge,
|
||||
userId: USER_ID,
|
||||
note: '集成测试手动授予',
|
||||
metadata: { source: 'integration-test', firstGrant: true },
|
||||
},
|
||||
});
|
||||
assert.equal(grant.item?.badgeId, ids.tenantBadge, 'tenant admin should grant badge');
|
||||
assert.equal(grant.item?.userId, USER_ID, 'badge grant should target student');
|
||||
|
||||
const repeatedGrant = await request('/api/tenant-admin/badge-grants', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
badgeId: ids.tenantBadge,
|
||||
userId: USER_ID,
|
||||
note: '重复授予不应产生重复记录',
|
||||
metadata: { source: 'integration-test-repeat', repeated: true },
|
||||
},
|
||||
});
|
||||
assert.equal(repeatedGrant.item?.id, grant.item?.id, 'repeated badge grant should be idempotent');
|
||||
|
||||
const badgeGrants = await request('/api/tenant-admin/badge-grants', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { userId: USER_ID, badgeId: ids.tenantBadge, limit: 20 },
|
||||
});
|
||||
assert.equal(
|
||||
badgeGrants.items?.filter(item => item.userId === USER_ID && item.badgeId === ids.tenantBadge).length,
|
||||
1,
|
||||
'badge grants should not duplicate per user and badge',
|
||||
);
|
||||
|
||||
const profileBadges = await request('/api/profile/badges', {
|
||||
query: { includeLocked: 'true', category: 'practice', limit: 20 },
|
||||
});
|
||||
const profileBadge = profileBadges.items?.find(item => item.badgeId === ids.tenantBadge);
|
||||
assert.equal(profileBadge?.unlocked, true, 'profile badges should expose granted badge as unlocked');
|
||||
assert.equal(profileBadges.summary?.unlocked >= 1, true, 'profile badges summary should count unlocked badges');
|
||||
|
||||
const profileBadgesInvalid = await request('/api/profile/badges', {
|
||||
query: { category: 'unsafe-category' },
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(profileBadgesInvalid.code, 'INVALID_BADGE_CATEGORY', 'profile badges should reject invalid category');
|
||||
|
||||
const badgeGrantInvalidId = await request('/api/tenant-admin/badge-grants', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
badgeId: 'not-a-uuid',
|
||||
userId: USER_ID,
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(badgeGrantInvalidId.code, 'INVALID_UUID', 'badge grant should reject invalid UUID input');
|
||||
|
||||
const badgeCrossTenantDenied = await request('/api/tenant-admin/badge-grants', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
badgeId: ids.tenantBadge,
|
||||
userId: USER_ID,
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(badgeCrossTenantDenied.code, 'TENANT_ADMIN_REQUIRED', 'badge grant must be tenant isolated');
|
||||
|
||||
const feedbacksBefore = await request('/api/tenant-admin/feedbacks', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { status: 'pending', limit: 20 },
|
||||
@@ -3705,9 +3881,11 @@ async function testTenantMemberPermissionsAndAudit() {
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'marketing:write'), 'permission matrix should expose marketing permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'dashboard:read'), 'permission matrix should expose dashboard read permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'roles:write'), 'permission matrix should expose role template permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'badges:grant'), 'permission matrix should expose badge grant permission');
|
||||
assert.ok(permissionMatrix.menuGroups?.some(item => item.key === 'sales'), 'permission matrix should expose menu groups');
|
||||
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('dashboard:read'), 'tenant operator defaults should include dashboard read');
|
||||
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('marketing:*'), 'permission matrix should include role defaults');
|
||||
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('badges:*'), 'tenant operator defaults should include badge management');
|
||||
|
||||
const roleTemplate = await request('/api/tenant-admin/role-templates', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
|
||||
Reference in New Issue
Block a user