241 lines
7.8 KiB
JavaScript
241 lines
7.8 KiB
JavaScript
'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);
|
||
});
|
||
});
|
||
});
|