/** * Simple Data Export Verification Test * * Tests all data export API endpoints with real database data * Run: mocha tests/test_export_verify_endpoints.js --timeout 60000 */ const path = require('path'); const crypto = require('crypto'); const args = process.argv.slice(2); let envFile = './environment.env'; for (let i = 0; i < args.length; i++) { if (args[i] === '--env' && args[i + 1]) { envFile = args[i + 1]; i++; } } require('dotenv').config({ path: path.resolve(process.cwd(), envFile) }); const { expect } = require('chai'); const axios = require('axios'); const bcrypt = require('bcryptjs'); const https = require('https'); const { ObjectId } = require('mongodb'); const moment = require('moment'); const { Job, App, AppFile, AppDetail, User, Pilot, Vehicle } = require('../model'); const ApiKey = require('../model/api_key'); const { ApiKeyServices, ExportUnits } = require('../helpers/constants'); const dbConnect = require('../helpers/db/connect'); const BASE_URL = `https://localhost:${process.env.AGM_PORT || process.env.PORT || 4100}`; const httpClient = axios.create({ baseURL: BASE_URL, httpsAgent: new https.Agent({ rejectUnauthorized: false }) }); describe('Data Export API - Endpoint Verification', function() { this.timeout(60000); let testUserId, testJobId, testAppId, testFileId, testApiKey; let testClientId; before(async function() { console.log('\n๐Ÿ”ง Setting up test data...'); await dbConnect(); // Create users const adminUser = new User({ username: `admin_${Date.now()}`, email: `admin_${Date.now()}@test.com`, passwordHash: 'hash', status: 'active', role: 'admin', kind: 'REGULAR' }); await adminUser.save(); testUserId = adminUser._id; const clientUser = new User({ username: `client_${Date.now()}`, email: `client_${Date.now()}@test.com`, passwordHash: 'hash', status: '3', role: 'client', kind: 'REGULAR' }); await clientUser.save(); testClientId = clientUser._id; // Create pilot and vehicle const pilot = new Pilot({ name: `Pilot${Date.now()}`, licenseNum: 'TEST001', active: true }); await pilot.save(); const vehicle = new Vehicle({ name: `Aircraft${Date.now()}`, tailNumber: `N${Math.random().toString().slice(2,7)}`, active: true }); await vehicle.save(); // Create Job const job = new Job({ _id: Math.floor(Math.random() * 900000) + 100000, name: `Job_${Date.now()}`, orderNumber: String(Math.floor(Math.random() * 10000)), byPuid: testUserId, client: testClientId, operator: pilot._id, vehicle: vehicle._id, status: 0, swathWidth: 12, measureUnit: false, sprayAreas: [{ properties: { name: 'Area1', appRate: 50, area: 10 }, geometry: { type: 'Polygon', coordinates: [[[-50, -30], [-50, -20], [-40, -20], [-40, -30], [-50, -30]]] } }] }); await job.save(); testJobId = job._id; // Create App (session) const app = new App({ jobId: testJobId, fileName: `session_${Date.now()}.log`, fileSize: 2048, status: 3, totalFlightTime: 3600, totalSprayTime: 2400, totalTurnTime: 1200, totalSprayed: 5.0, totalSprayMat: 250, totalSprayMatUnit: 1, avgSpraySpeed: 40, markedDelete: false }); await app.save(); testAppId = app._id; // Create AppFile const appFile = new AppFile({ appId: testAppId, name: `file_${Date.now()}.log`, agn: 1, meta: { areaOrZone: 'Main Area', sprCoverage: [100, 5.0], appRate: 50, appRateUnitStr: 'L/ha', fcName: 'Controller1', sprOnLag: 0.5, sprOffLag: 0.3, pulsesPerLit: 10, operator: 'Test Pilot', matType: 'wet' } }); await appFile.save(); testFileId = appFile._id; // Create AppDetail records const baseTime = moment().unix(); const records = []; for (let i = 0; i < 10; i++) { records.push({ fileId: testFileId, gpsTime: baseTime + (i * 10), lat: 40.71 + (i * 0.0001), lon: -74.00 + (i * 0.0001), utmX: 583960 + (i * 10), utmY: 4506721 + (i * 10), alt: 100 + (i * 2), grSpeed: 35 + (i * 0.5), head: 45, xTrack: 0.5, llnum: 1, stdHdop: 0.8, satsIn: 12, tslu: 0, calcodeFreq: 0, sprayStat: i === 0 ? 3 : (i % 2 === 0 ? 0 : 1), lminApp: i % 2 === 0 ? 0 : 45, lminReq: 45, lhaReq: 50, swath: 12, psi: 2.5, rpm: 1800, windSpd: 2.5, windDir: 180, temp: 22, humid: 65 }); } await AppDetail.insertMany(records); // Create API key const plainApiKey = crypto.randomBytes(32).toString('hex'); const prefix = plainApiKey.substring(0, 8); const keyHash = await bcrypt.hash(plainApiKey, 10); const apiKey = new ApiKey({ owner: testUserId, label: `key_${Date.now()}`, prefix, keyHash, service: ApiKeyServices.DATA_EXPORT, active: true }); await apiKey.save(); testApiKey = plainApiKey; console.log('โœ… Test data ready\n'); }); after(async function() { console.log('\n๐Ÿงน Cleaning up...'); try { await AppDetail.deleteMany({ fileId: testFileId }); await AppFile.deleteOne({ _id: testFileId }); await App.deleteOne({ _id: testAppId }); await Job.deleteOne({ _id: testJobId }); await User.deleteMany({ _id: { $in: [testUserId, testClientId] } }); console.log('โœ… Cleaned up\n'); } catch (err) { console.error('Cleanup error:', err.message); } }); it('โœ… GET /api/v1/jobs/:jobId/sessions - returns session summary', async function() { const res = await httpClient.get(`/api/v1/jobs/${testJobId}/sessions`, { headers: { 'X-API-Key': testApiKey } }); expect(res.status).to.equal(200); expect(res.data.data).to.be.an('array').with.length.greaterThan(0); const session = res.data.data[0]; expect(session.totalFlightTime_s).to.exist; expect(session.totalSprayed_ha).to.exist; expect(session.avgSpraySpeed_ms).to.exist; console.log(` โœ… Sessions endpoint: ${res.data.data.length} session(s)`); console.log(` - totalFlightTime_s: ${session.totalFlightTime_s}s`); console.log(` - avgSpraySpeed_ms: ${session.avgSpraySpeed_ms} m/s`); }); it('โœ… GET /api/v1/jobs/:jobId/sessions/:fileId/records - returns GPS trace', async function() { const res = await httpClient.get( `/api/v1/jobs/${testJobId}/sessions/${testFileId}/records`, { headers: { 'X-API-Key': testApiKey }, params: { limit: 100 } } ); expect(res.status).to.equal(200); expect(res.data.data).to.be.an('array').with.length.greaterThan(0); const record = res.data.data[0]; expect(record.gpsTime).to.exist; expect(record.lat).to.exist; expect(record.lon).to.exist; // Verify sprayStat=3 is excluded const hasSprying3 = res.data.data.some(r => r.sprayStat === 3); expect(hasSprying3).to.be.false; console.log(` โœ… Records endpoint: ${res.data.data.length} records`); console.log(` - sprayStat=3 properly filtered`); }); it('โœ… GET /api/v1/jobs/:jobId/areas - returns GeoJSON areas', async function() { const res = await httpClient.get(`/api/v1/jobs/${testJobId}/areas`, { headers: { 'X-API-Key': testApiKey } }); expect(res.status).to.equal(200); expect(res.data.type).to.equal('FeatureCollection'); expect(res.data.features).to.be.an('array').with.length.greaterThan(0); const feature = res.data.features[0]; expect(feature.type).to.equal('Feature'); expect(feature.properties).to.exist; console.log(` โœ… Areas endpoint: ${res.data.features.length} features`); }); it('โœ… POST /api/v1/jobs/:jobId/export - triggers export', async function() { const res = await httpClient.post( `/api/v1/jobs/${testJobId}/export`, { format: 'csv', interval: null, units: ExportUnits.METRIC }, { headers: { 'X-API-Key': testApiKey } } ); expect(res.status).to.equal(202); expect(res.data.exportId).to.exist; expect(res.data.status).to.equal('pending'); expect(res.data.units).to.equal(ExportUnits.METRIC); this.exportId = res.data.exportId; console.log(` โœ… Export triggered: ${res.data.exportId}`); }); it('โœ… GET /api/v1/exports/:exportId - polls status', async function() { const exportId = this.exportId; if (!exportId) { console.log(' โญ๏ธ Skipping (no exportId)'); this.skip(); } let status = 'pending'; for (let i = 0; i < 30 && ['pending', 'processing'].includes(status); i++) { const res = await httpClient.get( `/api/v1/exports/${exportId}`, { headers: { 'X-API-Key': testApiKey } } ); expect(res.data.status).to.be.oneOf(['pending', 'processing', 'ready', 'error']); status = res.data.status; if (['pending', 'processing'].includes(status)) await new Promise(r => setTimeout(r, 1000)); } expect(status).to.be.oneOf(['ready', 'error']); this.exportId = exportId; console.log(` โœ… Export status: ${status}`); }); it('โœ… GET /api/v1/exports/:exportId/download - downloads file', async function() { const exportId = this.exportId; if (!exportId) { console.log(' โญ๏ธ Skipping'); this.skip(); } try { const res = await httpClient.get( `/api/v1/exports/${exportId}/download`, { headers: { 'X-API-Key': testApiKey }, responseType: 'text' } ); expect(res.status).to.equal(200); expect(res.data).to.be.a('string').with.length.greaterThan(0); const lines = res.data.split('\n'); const headers = lines[0].split(','); expect(headers.length).to.be.greaterThan(0); console.log(` โœ… Downloaded: ${lines.length} lines, ${headers.length} columns`); } catch (err) { if (err.response?.status === 404) { console.log(' โ„น๏ธ Export not ready'); this.skip(); } else { throw err; } } }); it('โœ… Authorization - rejects invalid key', async function() { try { await httpClient.get(`/api/v1/jobs/${testJobId}/sessions`, { headers: { 'X-API-Key': 'invalid' } }); expect.fail('Should reject invalid key'); } catch (err) { expect(err.response.status).to.equal(401); console.log(` โœ… Invalid key rejected (401)`); } }); });