import { NotFoundException } from '@nestjs/common'; import { GetStudentBasicTool } from './get-student-basic.tool'; import { CaslAbilityFactory } from '../../authorization/casl-ability.factory'; import { StudentAccessScopeFactory } from '../../students/student-access-scope.factory'; import { AgentToolContextFactory } from '../agent-tool.types'; import type { AgentToolContext } from '../agent-tool.types'; import type { AuthenticatedUser } from '../../authorization'; const abilityFactory = new CaslAbilityFactory(); const scopeFactory = new StudentAccessScopeFactory(abilityFactory); function makeCtx(overrides: Partial & { id: number; username: string }): AgentToolContext { const user: AuthenticatedUser = { id: overrides.id, username: overrides.username, permissions: overrides.permissions ?? [], isSuperAdmin: overrides.isSuperAdmin ?? false, roles: overrides.roles ?? [], }; return AgentToolContextFactory.fromAuthenticatedUser(user); } const studentViewerCtx = makeCtx({ id: 2, username: 'teacher', permissions: ['student:view'] }); const superAdminCtx = makeCtx({ id: 1, username: 'admin', isSuperAdmin: true }); const classEditorCtx = makeCtx({ id: 4, username: 'class_editor', permissions: ['student:view', 'class:edit'], }); function makeTool(svcOverride?: { agentGetStudentBasic: jest.Mock }): GetStudentBasicTool { const svc = svcOverride ?? { agentGetStudentBasic: jest.fn().mockResolvedValue(null) }; return new GetStudentBasicTool(svc as never, scopeFactory); } const basicOutput = { id: 1, name: '张三', studentNo: 'S001', gender: '男', status: 'active', organizationId: 10, organizationName: '杭州校区', classIds: [5], }; describe('GetStudentBasicTool', () => { it('has name "get_student_basic"', () => { const tool = makeTool(); expect(tool.name).toBe('get_student_basic'); }); it('requires permission "student:view"', () => { const tool = makeTool(); expect(tool.requiredPermission).toBe('student:view'); }); // ----------------------------------------------------------------------- // Validation // ----------------------------------------------------------------------- describe('validate', () => { it('accepts valid studentId', () => { const tool = makeTool(); const result = tool.validate({ studentId: 1 }); expect(result.ok).toBe(true); if (result.ok) expect(result.value.studentId).toBe(1); }); it('rejects missing studentId', () => { const tool = makeTool(); const result = tool.validate({}); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain('studentId'); }); it('rejects non-integer studentId', () => { const tool = makeTool(); const result = tool.validate({ studentId: 'abc' }); expect(result.ok).toBe(false); }); it('rejects extra unknown fields', () => { const tool = makeTool(); const result = tool.validate({ studentId: 1, extraField: 'hack' }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain('extraField'); }); it('rejects userId', () => { const tool = makeTool(); const result = tool.validate({ studentId: 1, userId: 999 }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error).toContain('userId'); }); }); // ----------------------------------------------------------------------- // P2-2: Scope construction // ----------------------------------------------------------------------- describe('P2-2: scope', () => { it('super admin uses manageAll scope', async () => { const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(basicOutput) }; const tool = makeTool(mockSvc); await tool.execute({ studentId: 1 }, superAdminCtx); expect(mockSvc.agentGetStudentBasic).toHaveBeenCalledWith( { type: 'manageAll' }, 1, ); }); it('non-admin uses teacher scope', async () => { const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(basicOutput) }; const tool = makeTool(mockSvc); await tool.execute({ studentId: 1 }, studentViewerCtx); expect(mockSvc.agentGetStudentBasic).toHaveBeenCalledWith( { type: 'teacher', userId: 2 }, 1, ); }); it('class:edit uses manageAll scope', async () => { const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(basicOutput) }; const tool = makeTool(mockSvc); await tool.execute({ studentId: 1 }, classEditorCtx); expect(mockSvc.agentGetStudentBasic).toHaveBeenCalledWith( { type: 'manageAll' }, 1, ); }); }); // ----------------------------------------------------------------------- // P2-1: NotFoundException for null result // ----------------------------------------------------------------------- describe('P2-1: NotFoundException', () => { it('null from service throws NotFoundException (not returned as success)', async () => { const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(null) }; const tool = makeTool(mockSvc); await expect(tool.execute({ studentId: 999 }, studentViewerCtx)).rejects.toThrow( NotFoundException, ); }); it('service NotFound message is "记录不存在或无权访问"', async () => { const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(null) }; const tool = makeTool(mockSvc); await expect(tool.execute({ studentId: 999 }, studentViewerCtx)).rejects.toThrow( '记录不存在或无权访问', ); }); }); // ----------------------------------------------------------------------- // Execute — happy path // ----------------------------------------------------------------------- describe('execute', () => { it('returns formatted student data', async () => { const mockSvc = { agentGetStudentBasic: jest.fn().mockResolvedValue(basicOutput) }; const tool = makeTool(mockSvc); const result = await tool.execute({ studentId: 1 }, superAdminCtx); expect(result).toEqual(basicOutput); const keys = Object.keys(result as Record); expect(keys).not.toContain('phone'); expect(keys).not.toContain('idNumber'); }); }); });