import 'reflect-metadata'; import assert from 'node:assert/strict'; import test from 'node:test'; import { readFileSync } from 'node:fs'; import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; import { REQUIRED_PERMISSIONS_KEY } from '../../src/authorization/decorators/require-permissions.decorator'; import { CreateFieldFindingDto } from '../../src/inspection-visits/dto/create-field-finding.dto'; import { FieldFindingsController } from '../../src/inspection-visits/field-findings.controller'; function permissions(method: string): string[] { const controller = FieldFindingsController.prototype; const handler = controller[method as keyof typeof controller]; return Reflect.getMetadata(REQUIRED_PERMISSIONS_KEY, handler) as string[]; } test('F2.3 mantiene los Hallazgos de campo bajo ejecución móvil y permisos explícitos', () => { assert.deepEqual(permissions('options'), ['inspection_findings.create', 'inspections.execute']); assert.deepEqual(permissions('list'), ['inspection_findings.read', 'inspections.execute']); assert.deepEqual(permissions('create'), ['inspection_findings.create', 'inspections.execute']); }); test('F2.3 exige descripción y título cuando el inspector usa OTROS', async () => { const dto = plainToInstance(CreateFieldFindingDto, { description: '', catalogItemId: null, customTitle: '', }); const errors = await validate(dto); const properties = new Set(errors.map((error) => error.property)); assert.equal(properties.has('description'), true); assert.equal(properties.has('customTitle'), true); }); test('F2.3 vuelve a validar contexto, selección y GPS+foto en servidor antes del Hallazgo', () => { const source = readFileSync('src/inspection-visits/field-findings.service.ts', 'utf8'); assert.match(source, /assertMobileInspector\(principal\)/); assert.match(source, /row\.visitStatus !== 'IN_PROGRESS'/); assert.match(source, /FIELD_FINDING_INSPECTOR_NOT_ASSIGNED/); assert.match(source, /FIELD_FINDING_INVENTORY_OUTSIDE_CONTEXT/); assert.match(source, /FIELD_FINDING_INVENTORY_NOT_SELECTED/); assert.match(source, /hasGeometry && creationGpsCaptured && fieldPhotoCount > 0/); assert.match(source, /FIELD_FINDING_CAPTURE_REQUIRED/); }); test('F2.3 resuelve exactamente el Acta DRAFT actual y no vuelve a la regla de un Acta por inspección', () => { const source = readFileSync('src/inspection-visits/field-findings.service.ts', 'utf8'); assert.match(source, /FROM inspection_acts/); assert.match(source, /status = 'DRAFT'/); assert.match(source, /LIMIT 2/); assert.match(source, /FIELD_FINDING_DRAFT_ACT_REQUIRED/); assert.match(source, /FIELD_FINDING_MULTIPLE_DRAFT_ACTS/); }); test('F2.3 reutiliza catálogo contextual y creación canónica, conserva OTROS y permite varios Hallazgos', () => { const fieldSource = readFileSync('src/inspection-visits/field-findings.service.ts', 'utf8'); const catalogSource = readFileSync('src/inspection-findings/finding-catalog.service.ts', 'utf8'); const findingSource = readFileSync('src/inspection-findings/inspection-findings.service.ts', 'utf8'); assert.match(fieldSource, /this\.catalog\.listApplicableForAsset\(assetId, \{\}\)/); assert.match(fieldSource, /this\.findings\.create\(act\.id, payload, principal, request\)/); assert.match(fieldSource, /canAddAnother: true/); assert.match(catalogSource, /code: 'OTHER'/); assert.match(catalogSource, /label: 'OTROS'/); assert.match(findingSource, /finding_catalog_proposals/); assert.match(findingSource, /PENDING/); });