forked from wangziqi/gongxue-base
feat: add tenant class student scopes
This commit is contained in:
@@ -13,6 +13,8 @@ const TENANT_ADMIN_USER_ID = process.env.TENANT_ADMIN_USER_ID || '00000000-0000-
|
||||
const TENANT_OPERATOR_USER_ID = '00000000-0000-0000-0000-000000000103';
|
||||
const TENANT_SALES_USER_ID = '00000000-0000-0000-0000-000000000104';
|
||||
const TENANT_AGENT_USER_ID = '00000000-0000-0000-0000-000000000105';
|
||||
const TENANT_TEACHER_USER_ID = '00000000-0000-0000-0000-000000000106';
|
||||
const SECOND_STUDENT_USER_ID = '00000000-0000-0000-0000-000000000107';
|
||||
const AUTH_USER_ID = '00000000-0000-0000-0000-00000000a101';
|
||||
const AUTH_TENANT_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a102';
|
||||
const AUTH_PLATFORM_ADMIN_USER_ID = '00000000-0000-0000-0000-00000000a999';
|
||||
@@ -40,6 +42,8 @@ const ids = {
|
||||
video: '00000000-0000-0000-0000-000000000821',
|
||||
quotaVideo: '00000000-0000-0000-0000-000000000824',
|
||||
scorelineSchool: '00000000-0000-0000-0000-000000000831',
|
||||
tenantClass: '00000000-0000-0000-0000-000000000851',
|
||||
tenantClassOther: '00000000-0000-0000-0000-000000000852',
|
||||
};
|
||||
|
||||
const paymentFixture = (() => {
|
||||
@@ -2556,6 +2560,208 @@ async function testTenantMemberPermissionsAndAudit() {
|
||||
assert.equal(partnerAuditDenied.code, 'TENANT_ADMIN_REQUIRED', 'tenant audit logs must be tenant isolated');
|
||||
}
|
||||
|
||||
async function testTenantClassStudentScopes() {
|
||||
const permissionMatrix = await request('/api/tenant-admin/permissions', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
});
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'classes:write'), 'permission matrix should expose class write permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'students:read'), 'permission matrix should expose student read permission');
|
||||
|
||||
const createdClass = await request('/api/tenant-admin/classes', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: ids.tenantClass,
|
||||
regionId: ids.region,
|
||||
code: 'integration-main',
|
||||
name: '集成测试主班级',
|
||||
description: '教师和学生范围权限测试',
|
||||
sortOrder: 1,
|
||||
metadata: { stage: 'integration' },
|
||||
},
|
||||
});
|
||||
assert.equal(createdClass.item?.id, ids.tenantClass, 'tenant admin should upsert class');
|
||||
|
||||
const otherClass = await request('/api/tenant-admin/classes', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: ids.tenantClassOther,
|
||||
regionId: ids.region,
|
||||
code: 'integration-other',
|
||||
name: '集成测试其他班级',
|
||||
sortOrder: 2,
|
||||
},
|
||||
});
|
||||
assert.equal(otherClass.item?.id, ids.tenantClassOther, 'tenant admin should upsert another class');
|
||||
|
||||
const teacherTemplate = await request('/api/tenant-admin/role-templates', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
code: 'teacher-class-scope',
|
||||
name: '教师班级范围模板',
|
||||
baseRole: 'teacher',
|
||||
permissions: {
|
||||
'classes:read': true,
|
||||
'students:read': true,
|
||||
},
|
||||
menuPermissions: {
|
||||
teachers: true,
|
||||
students: true,
|
||||
},
|
||||
fieldPermissions: {
|
||||
'student.phone': false,
|
||||
},
|
||||
dataScope: {
|
||||
mode: 'classes',
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal(teacherTemplate.item?.code, 'teacher-class-scope', 'tenant admin should create teacher scope template');
|
||||
|
||||
const teacher = await request('/api/tenant-admin/members', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
username: 'integration_teacher',
|
||||
phone: '13800000015',
|
||||
name: 'Integration Teacher',
|
||||
role: 'teacher',
|
||||
roleTemplateId: teacherTemplate.item.id,
|
||||
status: 'active',
|
||||
permissions: {},
|
||||
},
|
||||
});
|
||||
assert.equal(teacher.item?.role, 'teacher', 'tenant admin should create teacher member');
|
||||
|
||||
const student = await request('/api/tenant-admin/students', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
userId: USER_ID,
|
||||
username: 'smoke_student',
|
||||
phone: '13800000000',
|
||||
name: 'Smoke Student',
|
||||
regionId: ids.region,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
assert.equal(student.item?.userId, USER_ID, 'tenant admin should upsert student profile');
|
||||
|
||||
const secondStudent = await request('/api/tenant-admin/students', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
userId: SECOND_STUDENT_USER_ID,
|
||||
username: 'integration_second_student',
|
||||
phone: '13800000016',
|
||||
name: 'Integration Second Student',
|
||||
regionId: ids.region,
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
assert.equal(secondStudent.item?.userId, SECOND_STUDENT_USER_ID, 'tenant admin should upsert another student');
|
||||
|
||||
const teacherAssignment = await request('/api/tenant-admin/classes/members', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
classId: ids.tenantClass,
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
memberType: 'teacher',
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
assert.equal(teacherAssignment.item?.memberType, 'teacher', 'tenant admin should assign teacher to class');
|
||||
|
||||
const studentAssignment = await request('/api/tenant-admin/classes/members', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
classId: ids.tenantClass,
|
||||
userId: USER_ID,
|
||||
memberType: 'student',
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
assert.equal(studentAssignment.item?.memberType, 'student', 'tenant admin should assign student to class');
|
||||
|
||||
const otherStudentAssignment = await request('/api/tenant-admin/classes/members', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
classId: ids.tenantClassOther,
|
||||
userId: SECOND_STUDENT_USER_ID,
|
||||
memberType: 'student',
|
||||
status: 'active',
|
||||
},
|
||||
});
|
||||
assert.equal(otherStudentAssignment.item?.classId, ids.tenantClassOther, 'tenant admin should assign second student to another class');
|
||||
|
||||
const adminClasses = await request('/api/tenant-admin/classes', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
});
|
||||
assert.ok(adminClasses.items?.some(item => item.id === ids.tenantClass && item.studentCount >= 1), 'admin should see main class student count');
|
||||
assert.ok(adminClasses.items?.some(item => item.id === ids.tenantClassOther), 'admin should see other class');
|
||||
|
||||
const teacherClasses = await request('/api/tenant-admin/classes', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
});
|
||||
assert.equal(teacherClasses.scoped, true, 'teacher class list should be scoped');
|
||||
assert.ok(teacherClasses.items?.some(item => item.id === ids.tenantClass), 'teacher should see assigned class');
|
||||
assert.ok(!teacherClasses.items?.some(item => item.id === ids.tenantClassOther), 'teacher should not see unassigned class');
|
||||
|
||||
const teacherStudents = await request('/api/tenant-admin/students', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
});
|
||||
assert.equal(teacherStudents.scoped, true, 'teacher student list should be scoped');
|
||||
assert.ok(teacherStudents.items?.some(item => item.userId === USER_ID), 'teacher should see student in assigned class');
|
||||
assert.ok(!teacherStudents.items?.some(item => item.userId === SECOND_STUDENT_USER_ID), 'teacher should not see student in unassigned class');
|
||||
const visibleStudent = teacherStudents.items?.find(item => item.userId === USER_ID);
|
||||
assert.equal(visibleStudent?.phone, null, 'teacher role template should mask student phone');
|
||||
|
||||
const teacherOtherClassDenied = await request('/api/tenant-admin/classes/members', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
query: { classId: ids.tenantClassOther },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(teacherOtherClassDenied.code, 'CLASS_SCOPE_REQUIRED', 'teacher should not read another class members');
|
||||
|
||||
const adminStudents = await request('/api/tenant-admin/students', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { classId: ids.tenantClassOther },
|
||||
});
|
||||
assert.ok(adminStudents.items?.some(item => item.userId === SECOND_STUDENT_USER_ID), 'tenant admin should filter students by class');
|
||||
|
||||
const studentDenied = await request('/api/tenant-admin/students', {
|
||||
userId: USER_ID,
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentDenied.code, 'TENANT_ADMIN_REQUIRED', 'student should not access tenant admin student list');
|
||||
|
||||
const partnerClassDenied = await request('/api/tenant-admin/classes/members', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
classId: ids.tenantClass,
|
||||
userId: USER_ID,
|
||||
memberType: 'student',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(partnerClassDenied.code, 'TENANT_ADMIN_REQUIRED', 'tenant admin must not assign another tenant class');
|
||||
|
||||
const auditLogs = await request('/api/tenant-admin/audit-logs', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { action: 'tenant.class', limit: 50 },
|
||||
});
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.class.upserted'), 'audit logs should include class upsert');
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.class_member.upserted'), 'audit logs should include class member upsert');
|
||||
}
|
||||
|
||||
async function testReferralAndCrmGrowth() {
|
||||
const salesMember = await request('/api/tenant-admin/members', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
@@ -2751,6 +2957,7 @@ async function main() {
|
||||
await check('tenant content assets and imports', testTenantContentAssetsAndImports);
|
||||
await check('tenant admin operations', testTenantAdminOps);
|
||||
await check('tenant member permissions and audit', testTenantMemberPermissionsAndAudit);
|
||||
await check('tenant class and student scopes', testTenantClassStudentScopes);
|
||||
await check('referral and CRM growth', testReferralAndCrmGrowth);
|
||||
|
||||
console.log('API integration tests complete.');
|
||||
|
||||
Reference in New Issue
Block a user