import 'reflect-metadata'; import assert from 'node:assert/strict'; import test from 'node:test'; import { readFileSync } from 'node:fs'; import { validate } from 'class-validator'; import { plainToInstance } from 'class-transformer'; import { FieldInventoryController } from '../../src/inspection-visits/field-inventory.controller'; import { CreateFieldInventoryDto } from '../../src/inspection-visits/dto/create-field-inventory.dto'; import { UploadFieldInventoryPhotoDto } from '../../src/inspection-visits/dto/upload-field-inventory-photo.dto'; import { REQUIRED_PERMISSIONS_KEY } from '../../src/authorization/decorators/require-permissions.decorator'; function permissions(method: string): string[] { const controller = FieldInventoryController.prototype; const handler = controller[method as keyof typeof controller]; return Reflect.getMetadata(REQUIRED_PERMISSIONS_KEY, handler) as string[]; } test('F2.1 mantiene Inventario de campo bajo política móvil y permisos explícitos', () => { assert.deepEqual(permissions('list'), ['assets.read', 'inspections.execute']); assert.deepEqual(permissions('types'), ['assets.read', 'inspections.execute']); assert.deepEqual(permissions('selectExisting'), ['assets.read', 'inspections.execute']); assert.deepEqual(permissions('create'), ['assets.create', 'inspections.execute']); assert.deepEqual(permissions('uploadPhoto'), ['assets.create', 'inspections.execute']); }); test('F2.1 exige GPS del dispositivo al crear Inventario en campo', async () => { const dto = plainToInstance(CreateFieldInventoryDto, { typeId: '11111111-1111-4111-8111-111111111111', name: 'Batería 28', attributes: {}, deviceCapturedAt: '2026-09-05T17:30:00-03:00', }); const errors = await validate(dto); const properties = new Set(errors.map((error) => error.property)); assert.equal(properties.has('deviceLatitude'), true); assert.equal(properties.has('deviceLongitude'), true); }); test('F2.1 exige GPS del dispositivo en cada fotografía de campo', async () => { const dto = plainToInstance(UploadFieldInventoryPhotoDto, { deviceCapturedAt: '2026-09-05T17:30:00-03:00', }); const errors = await validate(dto); const properties = new Set(errors.map((error) => error.property)); assert.equal(properties.has('deviceLatitude'), true); assert.equal(properties.has('deviceLongitude'), true); }); test('F2.1 deriva Área y Operadora desde la Inspección y usa el asset anidado del alta provisional', () => { const source = readFileSync('src/inspection-visits/field-inventory.service.ts', 'utf8'); assert.match(source, /visit\.operational_area_id AS \"areaId\"/); assert.match(source, /visit\.operator_company_id AS \"companyId\"/); assert.match(source, /operationalAreaId: context\.areaId/); assert.match(source, /operatorCompanyId: context\.companyId/); assert.match(source, /const assetId = discovery\.asset\?\.id/); }); test('F2.1 bloquea hallazgos sobre altas de campo hasta tener GPS y foto', () => { const migration = readFileSync( 'src/database/migrations/1789668000000-phase-f2-1-field-inventory.ts', 'utf8', ); assert.match(migration, /enforce_field_inventory_capture_before_finding/); assert.match(migration, /FIELD_INVENTORY_GPS_REQUIRED/); assert.match(migration, /FIELD_INVENTORY_PHOTO_REQUIRED/); assert.match(migration, /REVOKE UPDATE, DELETE ON TABLE asset_field_capture_events/); });