agmission/Development/server/tests/test_export_verify_endpoints.js
Devin Major df31b2080d
All checks were successful
Server Tests / Mocha – Unit & Utility Tests (push) Successful in 42s
-(#3013) Data Export - Implement Data Export API BE (Cont.)
+ Added public data export API enhancements, tests, and customer documentation
  + Extended /api/v1 data export endpoints with richer session, records, area, and async export output
  + Added confirmed/fallback report values, client metadata, mapped area, over-spray, volume/apprate (string) units, and weather blocks
  + Normalized flowController to "No FC" and align record field names with playback output
  + Converted record wind speed output to knots, add Fligh Mater only record/export fields behind fm=true, and persist fm on export jobs
  + Added export status/area constants, HTTP 202 support, route-level API docs, and per-account export rate limiting support
  + Added comprehensive endpoint, format, and verification test coverage plus test-suite README
  + Added customer-facing data export design, integration, rate-limit, and documentation index guides
  + Updated README/DLQ docs and related documentation links to current HTTPS dashboard paths
2026-04-24 09:05:55 -04:00

340 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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)`);
}
});
});