forked from wangziqi/gongxue-base
feat: add tenant student operations
This commit is contained in:
@@ -2762,6 +2762,222 @@ async function testTenantClassStudentScopes() {
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.class_member.upserted'), 'audit logs should include class member upsert');
|
||||
}
|
||||
|
||||
async function testTenantStudentOperations() {
|
||||
let bulkStudentUserId = '';
|
||||
const bulk = await request('/api/tenant-admin/students/bulk-upsert', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
students: [
|
||||
{
|
||||
username: 'integration_bulk_student',
|
||||
phone: '13800000018',
|
||||
name: 'Integration Bulk Student',
|
||||
regionId: ids.region,
|
||||
legacyUserId: 'legacy-bulk-student',
|
||||
status: 'active',
|
||||
stats: { source: 'integration' },
|
||||
},
|
||||
{
|
||||
username: 'integration_bulk_missing_region',
|
||||
phone: '13800000019',
|
||||
name: 'Integration Bulk Missing Region',
|
||||
regionId: '00000000-0000-0000-0000-ffffffffffff',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(bulk.total, 2, 'bulk student upsert should report total');
|
||||
assert.equal(bulk.successCount, 1, 'bulk student upsert should import valid student');
|
||||
assert.equal(bulk.errorCount, 1, 'bulk student upsert should report invalid rows');
|
||||
bulkStudentUserId = bulk.items?.find(item => item.status === 'active')?.userId || '';
|
||||
assert.ok(bulkStudentUserId, 'bulk result should include created student id');
|
||||
assert.ok(bulk.errors?.some(item => item.code === 'REGION_NOT_FOUND'), 'bulk result should include row-level validation error');
|
||||
|
||||
const bulkLimitDenied = await request('/api/tenant-admin/students/bulk-upsert', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
students: Array.from({ length: 201 }, (_, index) => ({
|
||||
username: `too_many_${index}`,
|
||||
phone: `13988${String(index).padStart(6, '0')}`,
|
||||
})),
|
||||
},
|
||||
expectStatus: 413,
|
||||
});
|
||||
assert.ok(
|
||||
['BULK_LIMIT_EXCEEDED', 'JSON_BODY_TOO_LARGE'].includes(bulkLimitDenied.code),
|
||||
'bulk student upsert should enforce item or body limit',
|
||||
);
|
||||
|
||||
const bulkAssign = await request('/api/tenant-admin/classes/members/bulk-assign', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
classId: ids.tenantClass,
|
||||
assignments: [
|
||||
{ userId: bulkStudentUserId, memberType: 'student', status: 'active' },
|
||||
{ userId: '00000000-0000-0000-0000-ffffffffffff', memberType: 'student' },
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(bulkAssign.total, 2, 'bulk class assign should report total');
|
||||
assert.equal(bulkAssign.successCount, 1, 'bulk class assign should assign valid student');
|
||||
assert.equal(bulkAssign.errorCount, 1, 'bulk class assign should report invalid rows');
|
||||
|
||||
const adminClassStudents = await request('/api/tenant-admin/students', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { classId: ids.tenantClass, keyword: 'Bulk Student' },
|
||||
});
|
||||
assert.ok(adminClassStudents.items?.some(item => item.userId === bulkStudentUserId), 'bulk assigned student should appear in class student list');
|
||||
|
||||
const disabled = await request('/api/tenant-admin/students/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: bulkStudentUserId,
|
||||
status: 'disabled',
|
||||
reason: 'integration-test',
|
||||
},
|
||||
});
|
||||
assert.equal(disabled.item?.status, 'disabled', 'tenant admin should disable student membership');
|
||||
|
||||
const disabledStudents = await request('/api/tenant-admin/students', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { status: 'disabled', keyword: 'Bulk Student' },
|
||||
});
|
||||
assert.ok(disabledStudents.items?.some(item => item.userId === bulkStudentUserId), 'disabled student should be queryable by status');
|
||||
|
||||
const restored = await request('/api/tenant-admin/students/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: bulkStudentUserId,
|
||||
status: 'active',
|
||||
reason: 'restore-integration-test',
|
||||
},
|
||||
});
|
||||
assert.equal(restored.item?.status, 'active', 'tenant admin should restore student membership');
|
||||
|
||||
const teacherNote = await request('/api/tenant-admin/students/notes', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
studentUserId: USER_ID,
|
||||
noteType: 'learning',
|
||||
content: '该学生本周错题复习需要跟进。',
|
||||
visibility: 'class_staff',
|
||||
isPinned: true,
|
||||
metadata: { source: 'integration' },
|
||||
},
|
||||
});
|
||||
assert.equal(teacherNote.item?.noteType, 'learning', 'teacher should create note for scoped student');
|
||||
|
||||
const teacherNotes = await request('/api/tenant-admin/students/notes', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
query: { studentUserId: USER_ID },
|
||||
});
|
||||
assert.ok(teacherNotes.items?.some(item => item.id === teacherNote.item.id), 'teacher should list scoped student notes');
|
||||
|
||||
const teacherOtherNoteDenied = await request('/api/tenant-admin/students/notes', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
studentUserId: SECOND_STUDENT_USER_ID,
|
||||
noteType: 'learning',
|
||||
content: '不应允许教师给非负责学生写备注。',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(teacherOtherNoteDenied.code, 'STUDENT_SCOPE_REQUIRED', 'teacher should not write note for unscoped student');
|
||||
|
||||
const followup = await request('/api/tenant-admin/students/followups', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
studentUserId: USER_ID,
|
||||
assignedToUserId: TENANT_TEACHER_USER_ID,
|
||||
classId: ids.tenantClass,
|
||||
title: '错题复盘督导',
|
||||
description: '提醒学生完成本周错题复盘。',
|
||||
followupType: 'learning',
|
||||
priority: 'high',
|
||||
status: 'open',
|
||||
dueAt: '2026-07-01T10:00:00.000Z',
|
||||
},
|
||||
});
|
||||
assert.equal(followup.item?.status, 'open', 'teacher should create follow-up for scoped student');
|
||||
|
||||
const teacherFollowups = await request('/api/tenant-admin/students/followups', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
query: { studentUserId: USER_ID, status: 'open' },
|
||||
});
|
||||
assert.ok(teacherFollowups.items?.some(item => item.id === followup.item.id), 'teacher should list scoped follow-ups');
|
||||
const visibleFollowup = teacherFollowups.items?.find(item => item.id === followup.item.id);
|
||||
assert.equal(visibleFollowup?.studentPhone, null, 'teacher follow-up list should mask student phone');
|
||||
|
||||
const completedFollowup = await request('/api/tenant-admin/students/followups', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
id: followup.item.id,
|
||||
studentUserId: USER_ID,
|
||||
assignedToUserId: TENANT_TEACHER_USER_ID,
|
||||
classId: ids.tenantClass,
|
||||
title: '错题复盘督导',
|
||||
followupType: 'learning',
|
||||
priority: 'high',
|
||||
status: 'done',
|
||||
},
|
||||
});
|
||||
assert.equal(completedFollowup.item?.status, 'done', 'teacher should complete scoped follow-up');
|
||||
assert.equal(completedFollowup.item?.completedBy, TENANT_TEACHER_USER_ID, 'completed follow-up should record completer');
|
||||
|
||||
const teacherOtherFollowupDenied = await request('/api/tenant-admin/students/followups', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
studentUserId: SECOND_STUDENT_USER_ID,
|
||||
classId: ids.tenantClassOther,
|
||||
title: '不应创建',
|
||||
followupType: 'learning',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(teacherOtherFollowupDenied.code, 'STUDENT_SCOPE_REQUIRED', 'teacher should not create follow-up for unscoped student');
|
||||
|
||||
const studentStatusDenied = await request('/api/tenant-admin/students/status', {
|
||||
userId: TENANT_TEACHER_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: USER_ID,
|
||||
status: 'disabled',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentStatusDenied.code, 'TENANT_PERMISSION_REQUIRED', 'teacher should not disable students without status permission');
|
||||
|
||||
const partnerBulkDenied = await request('/api/tenant-admin/students/bulk-upsert', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
students: [{ userId: USER_ID, name: 'cross tenant denied' }],
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(partnerBulkDenied.code, 'TENANT_ADMIN_REQUIRED', 'student bulk import must be tenant isolated');
|
||||
|
||||
const auditLogs = await request('/api/tenant-admin/audit-logs', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { action: 'tenant.student', limit: 100 },
|
||||
});
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.students.bulk_upserted'), 'audit logs should include bulk student upsert');
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.student.status_updated'), 'audit logs should include student status update');
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.student_note.upserted'), 'audit logs should include student note upsert');
|
||||
assert.ok(auditLogs.items?.some(item => item.action === 'tenant.student_followup.upserted'), 'audit logs should include student follow-up upsert');
|
||||
}
|
||||
|
||||
async function testReferralAndCrmGrowth() {
|
||||
const salesMember = await request('/api/tenant-admin/members', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
@@ -2958,6 +3174,7 @@ async function main() {
|
||||
await check('tenant admin operations', testTenantAdminOps);
|
||||
await check('tenant member permissions and audit', testTenantMemberPermissionsAndAudit);
|
||||
await check('tenant class and student scopes', testTenantClassStudentScopes);
|
||||
await check('tenant student operations', testTenantStudentOperations);
|
||||
await check('referral and CRM growth', testReferralAndCrmGrowth);
|
||||
|
||||
console.log('API integration tests complete.');
|
||||
|
||||
Reference in New Issue
Block a user