import type { DtoRecordCreate, DtoRecordUpdate } from 'vona-module-training-record'; import type { DtoStudentCreate } from 'vona-module-training-student'; import { ZodMetadata } from '@cabloy/zod-openapi'; import fse from 'fs-extra'; import assert from 'node:assert'; import os from 'node:os'; import path from 'node:path'; import { describe, it } from 'node:test'; import { app } from 'vona-mock'; import { getTargetDecoratorRules } from 'vona-module-a-openapiutils'; import { DtoDetailRecordSubjectResItem } from '../src/dto/detailRecordSubjectResItem.tsx'; import { DtoRecordSelectReq } from '../src/dto/recordSelectReq.tsx'; import { DtoRecordSelectResItem } from '../src/dto/recordSelectResItem.tsx'; import { DtoRecordView } from '../src/dto/recordView.tsx'; const dossierTextAttendance = Buffer.from('attendance dossier file'); const dossierTextAssessment = Buffer.from('assessment dossier file'); describe('record.test.ts', () => { it('action:record:emittedDtoSchemas', async () => { await app.bean.executor.mockCtx(async () => { const subjectApiJson = await app.bean.openapi.generateJsonOfClass( DtoDetailRecordSubjectResItem, ); const subjectComponent = Object.values(subjectApiJson.components!.schemas as any).find( item => (item as any).properties?._lineNumber, ) as any; assert.ok(subjectComponent?.properties?._lineNumber); assert.equal(subjectComponent.required?.includes('_lineNumber'), false); const selectReqRules = getTargetDecoratorRules(DtoRecordSelectReq.prototype); const studentIdMetadata = ZodMetadata.getOpenapiMetadata(selectReqRules.studentId!); assert.equal(selectReqRules.studentId?.type, 'optional'); assert.deepEqual(studentIdMetadata?.filter, { table: 'trainingStudent', joinType: 'innerJoin', joinOn: ['studentId', 'trainingStudent.id'], originalName: 'name', op: '_includesI_', }); const controller = app.bean.onion.controller .getOnionsEnabledCached() .find(item => item.beanOptions.beanFullName === 'training-record.controller.record') ?.beanOptions.beanClass; if (!controller) throw new Error('training-record.controller.record not found'); const controllerApiJson = await app.bean.openapi.generateJsonOfControllerAction( controller, 'select', 'V31', ); const parameters = controllerApiJson.paths?.['/api/training/record']?.get?.parameters ?? []; const studentIdParameter = parameters.find(parameter => parameter.name === 'studentId'); assert.ok(studentIdParameter); assert.equal(studentIdParameter.in, 'query'); assert.equal(studentIdParameter.required, false); assert.deepEqual(studentIdParameter.schema?.type, ['string', 'null']); assert.equal( parameters.some(parameter => parameter.name === 'studentName'), false, ); for (const [DtoClass, hasTrainingRecordSubjects] of [ [DtoRecordSelectResItem, false], [DtoRecordView, true], ] as const) { const apiJson = await app.bean.openapi.generateJsonOfClass(DtoClass); const component = Object.values(apiJson.components!.schemas as any).find(item => { const properties = (item as any).properties; return ( properties?.student && Boolean(properties?.trainingRecordSubjects) === hasTrainingRecordSubjects ); }) as any; assert.ok(component); assert.ok(component.properties.student); assert.equal( Boolean(component.properties.trainingRecordSubjects), hasTrainingRecordSubjects, ); assert.deepEqual(Object.keys(component.properties.student.properties).sort(), [ 'id', 'name', ]); assert.equal(component.properties.studentId.rest.table.enableSorting, true); } }); }); it('action:record', async () => { await app.bean.executor.mockCtx(async () => { const studentData: DtoStudentCreate = { name: '__Student__', description: 'This is a student for record test', mobile: '13812345678', level: 1, trainingRecords: [], }; const trainingTime = new Date('2026-01-15T09:30:00.000Z'); const trainingTimeUpdate = new Date('2026-02-20T14:45:00.000Z'); const dossierFilePathAttendance = path.join( os.tmpdir(), 'training-record-dossier-attendance.txt', ); const dossierFilePathAssessment = path.join( os.tmpdir(), 'training-record-dossier-assessment.txt', ); let dossierFileAttendance: any; let dossierFileAssessment: any; await app.bean.passport.signinMock(); try { await fse.writeFile(dossierFilePathAttendance, dossierTextAttendance); dossierFileAttendance = await app.bean.file.upload( 'file-native:native', { file: dossierFilePathAttendance, filename: 'attendance.txt', contentType: 'text/plain', public: false, }, { clientName: 'default', fileScene: 'training-record:dossierFile', }, ); const recordData = { studentId: 0 as any, name: '__Record__', subjectCount: 1, totalScore: 88, averageScore: 88, trainingTime, dossierFileIds: [dossierFileAttendance.id], description: 'This is a record', trainingRecordSubjects: [ { name: '__Math__', score: 95, description: 'Math subject', }, ], } as any as DtoRecordCreate; const studentId = await app.bean.executor.performAction('post', '/training/student', { body: studentData, }); recordData.studentId = studentId; const recordId = await app.bean.executor.performAction('post', '/training/record', { body: recordData, }); assert.equal(!!recordId, true); const selectRes: any = await app.bean.executor.performAction('get', '/training/record'); const recordItem = selectRes.list.find((item: any) => String(item.id) === String(recordId)); assert.equal(!!recordItem, true); assert.equal(recordItem!.name, recordData.name); assert.equal(String(recordItem!.studentId), String(studentId)); assert.equal(String(recordItem!.student?.id), String(studentId)); assert.equal(recordItem!.student?.name, studentData.name); const filteredSelectRes: any = await app.bean.executor.performAction( 'get', '/training/record', { query: { studentId: 'tude' } }, ); assert.equal( filteredSelectRes.list.some((item: any) => String(item.id) === String(recordId)), true, ); assert.equal( filteredSelectRes.list.every((item: any) => item.student?.name.includes('tude')), true, ); assert.equal(recordItem!.dossierFiles?.length, 1); assert.equal(recordItem!.dossierFiles?.[0]?.filename, 'attendance.txt'); let record: any = await app.bean.executor.performAction('get', '/training/record/:id', { params: { id: recordId }, }); const recordSubject = record.trainingRecordSubjects?.[0]; const recordDossierFile = record.dossierFiles?.[0]; assert.equal(record.name, recordData.name); assert.equal(String(record.studentId), String(studentId)); assert.equal(String(record.student?.id), String(studentId)); assert.equal(record.student?.name, studentData.name); assert.equal(record.subjectCount, recordData.subjectCount); assert.equal(record.totalScore, recordData.totalScore); assert.equal(Number(record.averageScore), recordData.averageScore); assert.equal(new Date(record.trainingTime).toISOString(), trainingTime.toISOString()); assert.equal(record.dossierFileIds?.length, 1); assert.equal(record.dossierFiles?.length, 1); assert.equal(String(recordDossierFile?.id), String(dossierFileAttendance.id)); assert.equal(recordDossierFile?.filename, 'attendance.txt'); assert.equal(recordDossierFile?.signed, true); assert.equal('meta' in (recordDossierFile ?? {}), false); assert.equal('fileScene' in (recordDossierFile ?? {}), false); assert.equal(record.trainingRecordSubjects?.length, 1); assert.equal(recordSubject?.name, '__Math__'); assert.equal(recordSubject?.score, 95); const passportCode = await app.bean.passport.createTempAuthToken({ path: '/api/file/download', }); const downloadUrl = new URL(recordDossierFile.downloadUrl); downloadUrl.searchParams.set('x-vona-passport-code', passportCode); const downloadResponse = await fetch(downloadUrl); assert.equal(downloadResponse.ok, true); assert.equal(await downloadResponse.text(), dossierTextAttendance.toString()); let student: any = await app.bean.executor.performAction('get', '/training/student/:id', { params: { id: studentId }, }); let studentRecord = student.trainingRecords?.find( (item: any) => String(item.id) === String(recordId), ); assert.equal(!!studentRecord, true); assert.equal(studentRecord?.dossierFiles?.length, 1); assert.equal(studentRecord?.trainingRecordSubjects?.length, 1); await fse.writeFile(dossierFilePathAssessment, dossierTextAssessment); dossierFileAssessment = await app.bean.file.upload( 'file-native:native', { file: dossierFilePathAssessment, filename: 'assessment.txt', contentType: 'text/plain', public: false, }, { clientName: 'default', fileScene: 'training-record:dossierFile', }, ); const dataUpdate = { studentId, name: '__RecordNew__', subjectCount: 2, totalScore: 183, averageScore: 91.5, trainingTime: trainingTimeUpdate, dossierFileIds: [dossierFileAttendance.id, dossierFileAssessment.id], description: 'This is an updated record', trainingRecordSubjects: [ { id: recordSubject.id, name: '__MathNew__', score: 96, description: 'Updated math subject', }, { name: '__English__', score: 87, description: 'English subject', }, ], } as any as DtoRecordUpdate; const updateRes = await app.bean.executor.performAction('patch', '/training/record/:id', { params: { id: recordId }, body: dataUpdate, }); assert.equal(updateRes, null); record = await app.bean.executor.performAction('get', '/training/record/:id', { params: { id: recordId }, }); const [updatedMathSubject, updatedEnglishSubject] = record.trainingRecordSubjects ?? []; assert.equal(record.name, dataUpdate.name); assert.equal(record.subjectCount, dataUpdate.subjectCount); assert.equal(record.totalScore, dataUpdate.totalScore); assert.equal(Number(record.averageScore), dataUpdate.averageScore); assert.equal(new Date(record.trainingTime).toISOString(), trainingTimeUpdate.toISOString()); assert.equal(record.dossierFileIds?.length, 2); assert.equal(record.dossierFiles?.length, 2); assert.equal( record.dossierFiles.some((item: any) => item.filename === 'attendance.txt'), true, ); assert.equal( record.dossierFiles.some((item: any) => item.filename === 'assessment.txt'), true, ); assert.equal(record.trainingRecordSubjects?.length, 2); assert.equal(updatedMathSubject?.name, '__MathNew__'); assert.equal(updatedMathSubject?.score, 96); assert.equal(updatedEnglishSubject?.name, '__English__'); assert.equal(updatedEnglishSubject?.score, 87); student = await app.bean.executor.performAction('get', '/training/student/:id', { params: { id: studentId }, }); studentRecord = student.trainingRecords?.find( (item: any) => String(item.id) === String(recordId), ); assert.equal(studentRecord?.name, dataUpdate.name); assert.equal(studentRecord?.dossierFiles?.length, 2); assert.equal(studentRecord?.trainingRecordSubjects?.length, 2); const deleteRes = await app.bean.executor.performAction('delete', '/training/record/:id', { params: { id: recordId }, }); assert.equal(deleteRes, null); record = await app.bean.executor.performAction('get', '/training/record/:id', { params: { id: recordId }, }); assert.equal(record, undefined); student = await app.bean.executor.performAction('get', '/training/student/:id', { params: { id: studentId }, }); studentRecord = student.trainingRecords?.find( (item: any) => String(item.id) === String(recordId), ); assert.equal(studentRecord, undefined); const retainedDossierFileAttendance = await app.bean.file.get(dossierFileAttendance.id); const retainedDossierFileAssessment = await app.bean.file.get(dossierFileAssessment.id); assert.equal(!!retainedDossierFileAttendance, true); assert.equal(!!retainedDossierFileAssessment, true); await app.bean.executor.performAction('delete', '/training/student/:id', { params: { id: studentId }, }); student = await app.bean.executor.performAction('get', '/training/student/:id', { params: { id: studentId }, }); assert.equal(student, undefined); } finally { if (dossierFileAssessment) { await app.bean.file.delete(dossierFileAssessment.id); } if (dossierFileAttendance) { await app.bean.file.delete(dossierFileAttendance.id); } await fse.remove(dossierFilePathAttendance); await fse.remove(dossierFilePathAssessment); await app.bean.passport.signout(); } }); }); it('action:record:systemAdmin', async () => { await app.bean.executor.mockCtx(async () => { await app.bean.passport.signinMock(); try { app.bean.passport.current!.roles = []; const actions = ['create', 'select', 'view', 'update', 'delete']; const permissions = await Promise.all( actions.map(action => app.bean.permission.retrievePermissionAction('training-record:record', action), ), ); assert.deepEqual( permissions, actions.map(() => false), ); } finally { await app.bean.passport.signout(); } }); }); });