agmission/Development/server/tests/integration/api_export.integration.test.js
2026-04-29 09:40:51 -04:00

241 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

'use strict';
/**
* Integration tests api_export controller (controllers/api_export.js)
*
* Note: generateExport (async file-writing) runs via setImmediate — we do not wait for
* it in these tests. triggerExport returns status='pending' immediately and that is what
* we assert against. downloadExport requires a ready file path and is excluded here.
*/
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
const { mockApplicator, mockClient, mockJob, mockReq, mockRes } = require('./mock_data');
let Job, ExportJob, Customer, Client;
beforeAll(async () => {
await connectDB();
({ Job } = require('../../model'));
ExportJob = require('../../model/export_job');
Customer = require('../../model/customer');
Client = require('../../model/client');
});
afterAll(async () => {
// Flush pending setImmediate callbacks (e.g. generateExport) before disconnecting
await new Promise(resolve => setImmediate(resolve));
await disconnectDB();
});
const apiExportCtl = require('../../controllers/api_export');
let _nextJobId = 800001;
function nextJobId() { return _nextJobId++; }
describe('api_export controller', () => {
let applicator, client, jobRecord;
beforeAll(async () => {
await clearCollection(Job);
await clearCollection(ExportJob);
applicator = await Customer.create(mockApplicator());
client = await Client.create(mockClient(applicator._id));
const jobId = nextJobId();
jobRecord = await Job.create(
mockJob(applicator._id, { _id: jobId, client: client._id })
);
});
afterAll(async () => {
await clearCollection(Job);
await clearCollection(ExportJob);
await Customer.deleteMany({ _id: { $in: [applicator._id, client._id] } });
});
const makeReq = (extra = {}) =>
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
// -------------------------------------------------------------------------
describe('triggerExport', () => {
it('throws when jobId param is not a finite number', async () => {
const req = makeReq({
params: { jobId: 'nan' },
body: { format: 'csv' },
});
const res = mockRes();
await expect(apiExportCtl.triggerExport(req, res)).rejects.toThrow();
});
it('throws JOB_NOT_FOUND when job does not exist', async () => {
const req = makeReq({
params: { jobId: '999999' },
body: { format: 'csv' },
});
const res = mockRes();
await expect(apiExportCtl.triggerExport(req, res)).rejects.toThrow();
});
it('throws AppAuthError when uid does not own the job', async () => {
const other = await Customer.create(mockApplicator());
const req = mockReq({
uid: other._id,
puid: other._id,
params: { jobId: String(jobRecord._id) },
body: { format: 'csv' },
});
const res = mockRes();
await expect(apiExportCtl.triggerExport(req, res)).rejects.toThrow();
await Customer.deleteMany({ _id: other._id });
});
it('returns 400 when format is invalid', async () => {
const req = makeReq({
params: { jobId: String(jobRecord._id) },
body: { format: 'xlsx' },
});
const res = mockRes();
await apiExportCtl.triggerExport(req, res);
expect(res.status).toHaveBeenCalledWith(400);
});
it('creates a pending ExportJob and returns exportId + status=pending for csv', async () => {
await ExportJob.deleteMany({});
const req = makeReq({
params: { jobId: String(jobRecord._id) },
body: { format: 'csv' },
});
const res = mockRes();
await apiExportCtl.triggerExport(req, res);
expect(res.statusCode).toBe(202);
expect(res._data.status).toBe('pending');
expect(res._data.exportId).toBeDefined();
expect(res._data.format).toBe('csv');
const saved = await ExportJob.findById(res._data.exportId);
expect(saved).not.toBeNull();
expect(saved.jobId).toBe(jobRecord._id);
});
it('creates a pending ExportJob for geojson format', async () => {
await ExportJob.deleteMany({});
const req = makeReq({
params: { jobId: String(jobRecord._id) },
body: { format: 'geojson' },
});
const res = mockRes();
await apiExportCtl.triggerExport(req, res);
expect(res.statusCode).toBe(202);
expect(res._data.format).toBe('geojson');
});
it('deduplicates: returns existing pending export on second identical request', async () => {
await ExportJob.deleteMany({});
const reqBody = { format: 'csv', units: 'metric' };
const req1 = makeReq({ params: { jobId: String(jobRecord._id) }, body: reqBody });
const res1 = mockRes();
await apiExportCtl.triggerExport(req1, res1);
const firstExportId = String(res1._data.exportId);
// Second identical request within dedup window
const req2 = makeReq({ params: { jobId: String(jobRecord._id) }, body: reqBody });
const res2 = mockRes();
await apiExportCtl.triggerExport(req2, res2);
expect(res2._data.reused).toBe(true);
expect(String(res2._data.exportId)).toBe(firstExportId);
});
it('deduplicates: different format creates a new export', async () => {
await ExportJob.deleteMany({});
const req1 = makeReq({
params: { jobId: String(jobRecord._id) },
body: { format: 'csv' },
});
const res1 = mockRes();
await apiExportCtl.triggerExport(req1, res1);
const req2 = makeReq({
params: { jobId: String(jobRecord._id) },
body: { format: 'geojson' },
});
const res2 = mockRes();
await apiExportCtl.triggerExport(req2, res2);
expect(res2._data.reused).toBeUndefined();
expect(String(res2._data.exportId)).not.toBe(String(res1._data.exportId));
});
});
// -------------------------------------------------------------------------
describe('getExportStatus', () => {
it('throws when exportId is not a valid ObjectId', async () => {
const req = makeReq({ params: { exportId: 'not-an-id' } });
const res = mockRes();
await expect(apiExportCtl.getExportStatus(req, res)).rejects.toThrow();
});
it('returns 404 when exportId does not exist', async () => {
const mongoose = require('mongoose');
const req = makeReq({
params: { exportId: new mongoose.Types.ObjectId().toHexString() },
});
const res = mockRes();
await apiExportCtl.getExportStatus(req, res);
expect(res.statusCode).toBe(404);
});
it('returns the export status for a valid exportId owned by the user', async () => {
const req = makeReq({
params: { jobId: String(jobRecord._id) },
body: { format: 'csv' },
});
const createRes = mockRes();
await apiExportCtl.triggerExport(req, createRes);
const exportId = String(createRes._data.exportId);
const statusReq = makeReq({ params: { exportId } });
const statusRes = mockRes();
await apiExportCtl.getExportStatus(statusReq, statusRes);
expect(statusRes.json).toHaveBeenCalled();
expect(statusRes._data.status).toBeDefined();
expect(String(statusRes._data.exportId)).toBe(exportId);
});
it('returns 404 when export belongs to a different user', async () => {
const mongoose = require('mongoose');
const { ObjectId } = require('mongodb');
// Create an ExportJob owned by someone else
const otherOwnerId = new mongoose.Types.ObjectId();
const job = await ExportJob.create({
owner: ObjectId(otherOwnerId.toHexString()),
jobId: jobRecord._id,
format: 'csv',
units: 'metric',
fm: false,
status: 'pending',
});
const req = makeReq({ params: { exportId: String(job._id) } });
const res = mockRes();
await apiExportCtl.getExportStatus(req, res);
expect(res.statusCode).toBe(404);
});
});
});