feat: add tenant class student scopes

This commit is contained in:
Codex
2026-06-29 00:50:22 +08:00
parent b262e87af9
commit 61240c5833
12 changed files with 1172 additions and 25 deletions

View File

@@ -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.');