added integration test cases for all server controllers where applicable, added mock data, updated run-tests.yaml to run integration tests upon committing, added coverage check for controllers (check_controller_contract.js)
This commit is contained in:
parent
9303274349
commit
6fff4011ad
@ -1,16 +1,14 @@
|
|||||||
# Gitea Actions – Server Tests
|
# Gitea Actions – Server Tests
|
||||||
#
|
#
|
||||||
# Two jobs run on every push to any branch:
|
# Two jobs run on every push to any branch:
|
||||||
# 1. jest-integration – Jest tests against the server's data methods (needs MongoDB)
|
# 1. jest-integration – Jest integration tests using an in-memory MongoDB
|
||||||
# 2. mocha-unit – Existing Mocha/Chai tests in tests/ and tests/utils/
|
# instance (mongodb-memory-server). No external database
|
||||||
|
# or repository secrets are required.
|
||||||
|
# 2. mocha-unit – Existing Mocha/Chai unit tests in tests/ and tests/utils/
|
||||||
#
|
#
|
||||||
# Prerequisites (Gitea repository secrets):
|
# Optional repository secret:
|
||||||
# DB_HOSTS – MongoDB host(s), e.g. "127.0.0.1:27017"
|
# TOKEN_SECRET – JWT secret used by the server's auth helpers.
|
||||||
# DB_NAME – Must contain "test", e.g. "agmission_test"
|
# A safe default is used automatically when absent.
|
||||||
# DB_USR – MongoDB username
|
|
||||||
# DB_PWD – MongoDB password
|
|
||||||
# DB_AUTH_SRC – MongoDB auth source (default: "admin")
|
|
||||||
# TOKEN_SECRET – JWT secret used by the server's auth helpers
|
|
||||||
|
|
||||||
name: Server Tests
|
name: Server Tests
|
||||||
|
|
||||||
@ -19,10 +17,69 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
|
|
||||||
# ── Shared env-file step (inline, re-used by both jobs via heredoc) ──────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ══════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
# Job 1: Jest integration tests (in-memory MongoDB via mongodb-memory-server)
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
jest-integration:
|
||||||
|
name: Jest – Integration Tests
|
||||||
|
runs-on: self-hosted
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: Development/server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: Development/server/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --prefer-offline
|
||||||
|
|
||||||
|
- name: Write test environment file
|
||||||
|
run: |
|
||||||
|
cat > environment.test.env <<'EOF'
|
||||||
|
NODE_ENV=test
|
||||||
|
TOKEN_SECRET=${{ secrets.TOKEN_SECRET || 'ci-test-secret-not-for-production' }}
|
||||||
|
PRODUCTION=false
|
||||||
|
NO_EMAIL_MODE=true
|
||||||
|
ENABLE_SUBSCRIPTION=false
|
||||||
|
INV_IMG_VIR_DIR=/tmp/inv-img
|
||||||
|
INV_UPLOAD_DIR=/tmp/inv-upload
|
||||||
|
INV_MAX_UPLOAD_SIZE_MB=5
|
||||||
|
PAGINATION_DEFAULT_LIMIT=1000
|
||||||
|
PAGINATION_MAX_LIMIT=14000
|
||||||
|
STRIPE_SEC_KEY=sk_test_placeholder
|
||||||
|
STRIPE_API_VERSION=2022-11-15
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Enforce controller integration contract
|
||||||
|
env:
|
||||||
|
TEST_ENV_FILE: ./environment.test.env
|
||||||
|
NODE_ENV: test
|
||||||
|
run: npm run test:integration:contract
|
||||||
|
|
||||||
|
- name: Run Jest integration tests
|
||||||
|
env:
|
||||||
|
TEST_ENV_FILE: ./environment.test.env
|
||||||
|
NODE_ENV: test
|
||||||
|
run: npm run test:integration:jest -- --ci
|
||||||
|
|
||||||
|
- name: Upload Jest results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: jest-integration-results
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
# Job 2: Mocha/Chai tests – tests/ and tests/utils/
|
# Job 2: Mocha/Chai tests – tests/ and tests/utils/
|
||||||
# These tests are self-contained unit tests that do not require MongoDB.
|
# These tests are self-contained unit tests that do not require MongoDB.
|
||||||
# ══════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
6
Development/package-lock.json
generated
Normal file
6
Development/package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Development",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@ -361,7 +361,11 @@ async function generateExport(exportJobId) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
exportJob.status = ExportJobStatus.ERROR;
|
exportJob.status = ExportJobStatus.ERROR;
|
||||||
exportJob.errorMsg = err.message;
|
exportJob.errorMsg = err.message;
|
||||||
await exportJob.save();
|
try {
|
||||||
|
await exportJob.save();
|
||||||
|
} catch (saveErr) {
|
||||||
|
// ExportJob may have been deleted before we could update it – ignore
|
||||||
|
}
|
||||||
console.error('[export] generation failed', err);
|
console.error('[export] generation failed', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
Development/server/jest.integration.config.js
Normal file
17
Development/server/jest.integration.config.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/** @type {import('jest').Config} */
|
||||||
|
module.exports = {
|
||||||
|
testMatch: ['**/tests/integration/**/*.integration.test.js'],
|
||||||
|
testEnvironment: 'node',
|
||||||
|
// Allow extra time on first run when mongodb-memory-server downloads its binary.
|
||||||
|
testTimeout: 60000,
|
||||||
|
// Each test file spins up its own in-memory MongoDB instance so files can
|
||||||
|
// run in parallel safely. Keep maxWorkers=1 to avoid hammering the CI
|
||||||
|
// machine; increase if your machine has spare cores.
|
||||||
|
maxWorkers: 1,
|
||||||
|
// CommonJS project – no Babel/ESM transform needed.
|
||||||
|
transform: {},
|
||||||
|
// Silence the mongodb-memory-server download progress logs during tests.
|
||||||
|
silent: false,
|
||||||
|
};
|
||||||
4431
Development/server/package-lock.json
generated
4431
Development/server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,8 @@
|
|||||||
"test:dlq": "mocha --exit --require tests/setup.js 'tests/dlq/test_*.js'",
|
"test:dlq": "mocha --exit --require tests/setup.js 'tests/dlq/test_*.js'",
|
||||||
"test:parsing": "mocha --exit --require tests/setup.js 'tests/parsing/test_*.js'",
|
"test:parsing": "mocha --exit --require tests/setup.js 'tests/parsing/test_*.js'",
|
||||||
"test:integration": "mocha --exit --require tests/setup.js 'tests/integration/test_*.js'",
|
"test:integration": "mocha --exit --require tests/setup.js 'tests/integration/test_*.js'",
|
||||||
|
"test:integration:contract": "node tests/integration/check_controller_contract.js",
|
||||||
|
"test:integration:jest": "NODE_ENV=test jest --config jest.integration.config.js --forceExit",
|
||||||
"test:utils": "mocha --exit --require tests/setup.js 'tests/utils/test_*.js'",
|
"test:utils": "mocha --exit --require tests/setup.js 'tests/utils/test_*.js'",
|
||||||
"test:verbose": "mocha --recursive --exit --require tests/setup.js 'tests/**/test_*.js' --reporter spec",
|
"test:verbose": "mocha --recursive --exit --require tests/setup.js 'tests/**/test_*.js' --reporter spec",
|
||||||
"test:bail": "mocha --recursive --exit --bail --require tests/setup.js 'tests/**/test_*.js'",
|
"test:bail": "mocha --recursive --exit --bail --require tests/setup.js 'tests/**/test_*.js'",
|
||||||
@ -64,7 +66,7 @@
|
|||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"email-templates": "11.0.3",
|
"email-templates": "11.0.3",
|
||||||
"error-handler": "file:../../../../@agn/error-handler",
|
"error-handler": "file:../../../@agn/error-handler",
|
||||||
"exceljs": "^4.2.1",
|
"exceljs": "^4.2.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
@ -123,7 +125,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.3.10",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"mocha": "^10.2.0",
|
"mocha": "^10.2.0",
|
||||||
|
"mongodb-memory-server": "^9.5.0",
|
||||||
"nyc": "^15.1.0"
|
"nyc": "^15.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,240 @@
|
|||||||
|
'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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
189
Development/server/tests/integration/api_key.integration.test.js
Normal file
189
Development/server/tests/integration/api_key.integration.test.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – api_key controller (controllers/api_key.js)
|
||||||
|
*
|
||||||
|
* Note: bcrypt operations are slow (10 rounds) – jest timeout is raised to
|
||||||
|
* 30 seconds per test.
|
||||||
|
*/
|
||||||
|
|
||||||
|
jest.setTimeout(30000);
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let ApiKey, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
ApiKey = require('../../model/api_key');
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiKeyCtl = require('../../controllers/api_key');
|
||||||
|
|
||||||
|
describe('api_key controller – data methods', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(ApiKey);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(ApiKey);
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('createKey', () => {
|
||||||
|
it('creates an API key and returns the raw key once', async () => {
|
||||||
|
const req = makeReq({ body: { label: 'Key 1' } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiKeyCtl.createKey(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.key).toBeDefined();
|
||||||
|
expect(typeof res._data.key).toBe('string');
|
||||||
|
expect(res._data.key.length).toBeGreaterThan(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when owner already has 10 keys', async () => {
|
||||||
|
await clearCollection(ApiKey);
|
||||||
|
// Create 10 keys first
|
||||||
|
const docs = [];
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
docs.push({
|
||||||
|
owner: applicator._id,
|
||||||
|
label: `Key ${i}`,
|
||||||
|
keyHash: `fakehash${i}`,
|
||||||
|
prefix: `pfx${i}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await ApiKey.insertMany(docs);
|
||||||
|
|
||||||
|
const req = makeReq({ body: { label: 'Extra Key' } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await expect(apiKeyCtl.createKey(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('listKeys', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(ApiKey);
|
||||||
|
const req = makeReq({ body: { label: 'Listed Key' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await apiKeyCtl.createKey(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns list of API keys without hash field', async () => {
|
||||||
|
const req = makeReq();
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiKeyCtl.listKeys(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBeGreaterThan(0);
|
||||||
|
expect(res._data[0].hash).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('revokeKey', () => {
|
||||||
|
let keyId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(ApiKey);
|
||||||
|
const req = makeReq({ body: { label: 'Key to Revoke' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await apiKeyCtl.createKey(req, res);
|
||||||
|
const listReq = makeReq();
|
||||||
|
const listRes = mockRes();
|
||||||
|
await apiKeyCtl.listKeys(listReq, listRes);
|
||||||
|
keyId = String(listRes._data[0]._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('revokes an active API key', async () => {
|
||||||
|
const req = makeReq({ params: { keyId }, ut: '0' });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiKeyCtl.revokeKey(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalled();
|
||||||
|
const doc = await ApiKey.findById(keyId);
|
||||||
|
expect(doc.active).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when key not found', async () => {
|
||||||
|
const req = makeReq({ params: { keyId: String(newId()) }, ut: '0' });
|
||||||
|
const res = mockRes();
|
||||||
|
await apiKeyCtl.revokeKey(req, res);
|
||||||
|
expect(res.statusCode).toBe(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('regenerateKey', () => {
|
||||||
|
let keyId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(ApiKey);
|
||||||
|
const req = makeReq({ body: { label: 'Key to Regen' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await apiKeyCtl.createKey(req, res);
|
||||||
|
const listReq = makeReq();
|
||||||
|
const listRes = mockRes();
|
||||||
|
await apiKeyCtl.listKeys(listReq, listRes);
|
||||||
|
keyId = String(listRes._data[0]._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('regenerates key and returns new raw key', async () => {
|
||||||
|
const req = makeReq({ params: { keyId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiKeyCtl.regenerateKey(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.key).toBeDefined();
|
||||||
|
expect(typeof res._data.key).toBe('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('deleteKey', () => {
|
||||||
|
let keyId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(ApiKey);
|
||||||
|
const req = makeReq({ body: { label: 'Key to Delete' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await apiKeyCtl.createKey(req, res);
|
||||||
|
const listReq = makeReq();
|
||||||
|
const listRes = mockRes();
|
||||||
|
await apiKeyCtl.listKeys(listReq, listRes);
|
||||||
|
keyId = String(listRes._data[0]._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('permanently deletes an API key', async () => {
|
||||||
|
const req = makeReq({ params: { keyId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiKeyCtl.deleteKey(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalled();
|
||||||
|
const found = await ApiKey.findById(keyId);
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
198
Development/server/tests/integration/api_pub.integration.test.js
Normal file
198
Development/server/tests/integration/api_pub.integration.test.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – api_pub controller (controllers/api_pub.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockClient, mockJob, mockReq, mockRes } = require('./mock_data');
|
||||||
|
|
||||||
|
let Job, App, AppFile, Customer, Client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
({ Job, App, AppFile } = require('../../model'));
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Client = require('../../model/client');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiPubCtl = require('../../controllers/api_pub');
|
||||||
|
|
||||||
|
/** Generate a unique integer job id that won't collide across tests */
|
||||||
|
let _nextJobId = 900001;
|
||||||
|
function nextJobId() { return _nextJobId++; }
|
||||||
|
|
||||||
|
describe('api_pub controller', () => {
|
||||||
|
let applicator, client, jobRecord;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Job);
|
||||||
|
await clearCollection(App);
|
||||||
|
await clearCollection(AppFile);
|
||||||
|
|
||||||
|
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(App);
|
||||||
|
await clearCollection(AppFile);
|
||||||
|
await Customer.deleteMany({ _id: { $in: [applicator._id, client._id] } });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getSessions', () => {
|
||||||
|
it('throws AppParamError when jobId is not a number', async () => {
|
||||||
|
const req = makeReq({ params: { jobId: 'not-a-number' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(apiPubCtl.getSessions(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws JOB_NOT_FOUND for a job that does not exist', async () => {
|
||||||
|
const req = makeReq({ params: { jobId: '999999' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(apiPubCtl.getSessions(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) },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(apiPubCtl.getSessions(req, res)).rejects.toThrow();
|
||||||
|
await Customer.deleteMany({ _id: other._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty data array when job has no apps', async () => {
|
||||||
|
const req = makeReq({ params: { jobId: String(jobRecord._id) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiPubCtl.getSessions(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.jobId).toBe(jobRecord._id);
|
||||||
|
expect(res._data.data).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns sessions when apps exist for the job', async () => {
|
||||||
|
const app = await App.create({
|
||||||
|
_id: require('mongoose').Types.ObjectId(),
|
||||||
|
jobId: jobRecord._id,
|
||||||
|
fileName: 'testflight.agn',
|
||||||
|
fileSize: 0,
|
||||||
|
markedDelete: false,
|
||||||
|
});
|
||||||
|
const req = makeReq({ params: { jobId: String(jobRecord._id) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiPubCtl.getSessions(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.data.length).toBe(1);
|
||||||
|
expect(String(res._data.data[0].sessionId)).toBe(String(app._id));
|
||||||
|
|
||||||
|
await App.deleteMany({ _id: app._id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getAreas', () => {
|
||||||
|
it('throws when jobId is not a number', async () => {
|
||||||
|
const req = makeReq({ params: { jobId: 'abc' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(apiPubCtl.getAreas(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws JOB_NOT_FOUND when job does not exist', async () => {
|
||||||
|
const req = makeReq({ params: { jobId: '999998' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(apiPubCtl.getAreas(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a GeoJSON FeatureCollection with empty features when job has no areas', async () => {
|
||||||
|
const req = makeReq({ params: { jobId: String(jobRecord._id) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiPubCtl.getAreas(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.type).toBe('FeatureCollection');
|
||||||
|
expect(Array.isArray(res._data.features)).toBe(true);
|
||||||
|
expect(res._data.features.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns area features when sprayAreas are defined on the job', async () => {
|
||||||
|
const area = {
|
||||||
|
properties: { name: 'Block A', area: 2.5 },
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await Job.findByIdAndUpdate(jobRecord._id, { sprayAreas: [area] });
|
||||||
|
|
||||||
|
const req = makeReq({ params: { jobId: String(jobRecord._id) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await apiPubCtl.getAreas(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.features.length).toBe(1);
|
||||||
|
expect(res._data.features[0].properties.name).toBe('Block A');
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
await Job.findByIdAndUpdate(jobRecord._id, { sprayAreas: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getSessionRecords', () => {
|
||||||
|
it('throws when jobId is not a number', async () => {
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const req = makeReq({
|
||||||
|
params: { jobId: 'abc', fileId: new mongoose.Types.ObjectId().toHexString() },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(apiPubCtl.getSessionRecords(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when fileId is not a valid ObjectId', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
params: { jobId: String(jobRecord._id), fileId: 'invalid' },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(apiPubCtl.getSessionRecords(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws NOT_FOUND when AppFile does not exist', async () => {
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const req = makeReq({
|
||||||
|
params: {
|
||||||
|
jobId: String(jobRecord._id),
|
||||||
|
fileId: new mongoose.Types.ObjectId().toHexString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(apiPubCtl.getSessionRecords(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
104
Development/server/tests/integration/billing.integration.test.js
Normal file
104
Development/server/tests/integration/billing.integration.test.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – billing controller (controllers/billing.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockReq, mockRes } = require('./mock_data');
|
||||||
|
|
||||||
|
let Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const billingCtl = require('../../controllers/billing');
|
||||||
|
|
||||||
|
describe('billing controller – getCustUsage_post', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (bodyOverrides = {}) =>
|
||||||
|
mockReq({
|
||||||
|
uid: applicator._id,
|
||||||
|
puid: applicator._id,
|
||||||
|
body: {
|
||||||
|
from: '2024-01-01',
|
||||||
|
to: '2024-12-31',
|
||||||
|
tz: 'America/Chicago',
|
||||||
|
...bodyOverrides,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws AppParamError when body is null', async () => {
|
||||||
|
const req = mockReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(billingCtl.getCustUsage_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws AppParamError when from date is missing', async () => {
|
||||||
|
const req = makeReq({ from: undefined });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(billingCtl.getCustUsage_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws AppParamError when to date is missing', async () => {
|
||||||
|
const req = makeReq({ to: undefined });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(billingCtl.getCustUsage_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws AppParamError when from is an invalid date string', async () => {
|
||||||
|
const req = makeReq({ from: 'not-a-date' });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(billingCtl.getCustUsage_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty array when no customers have jobs in the date range', async () => {
|
||||||
|
const req = makeReq();
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await billingCtl.getCustUsage_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
// No active customers have any jobs in the test DB for this date range
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts ISO datetime strings for from/to', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
from: '2024-01-01T00:00:00.000Z',
|
||||||
|
to: '2024-12-31T23:59:59.999Z',
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await billingCtl.getCustUsage_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('billable filter is accepted without error', async () => {
|
||||||
|
const req = makeReq({ billable: true });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await billingCtl.getCustUsage_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,274 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ROOT_DIR = path.resolve(__dirname, '..', '..');
|
||||||
|
const CONTROLLERS_DIR = path.join(ROOT_DIR, 'controllers');
|
||||||
|
const TESTS_DIR = __dirname;
|
||||||
|
|
||||||
|
const EXCLUDED_CONTROLLERS = {
|
||||||
|
dlq: 'No Jest integration suite yet.',
|
||||||
|
export: 'Route-wiring controller module; not covered by the current Jest integration pattern.',
|
||||||
|
geoutil: 'No Jest integration suite yet.',
|
||||||
|
health: 'No Jest integration suite yet.',
|
||||||
|
location: 'No Jest integration suite yet.',
|
||||||
|
subscription: 'No Jest integration suite yet.',
|
||||||
|
upload_job: 'Route-wiring controller module; not covered by the current Jest integration pattern.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EXCLUDED_METHODS = {
|
||||||
|
api_export: {
|
||||||
|
downloadExport: 'File download path is not covered by the current integration suite.',
|
||||||
|
},
|
||||||
|
api_pub: {
|
||||||
|
getSessionRecords: 'No Jest integration case yet.',
|
||||||
|
getAreas: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
billing: {
|
||||||
|
exportUsageDetail_post: 'No Jest integration case yet.',
|
||||||
|
getCustBillingStatus_get: 'No Jest integration case yet.',
|
||||||
|
uploadLogo_post: 'No Jest integration case yet.',
|
||||||
|
deleteLogo_delete: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
updateClient_put: 'No Jest integration case yet.',
|
||||||
|
deleteClient: 'Skipped in Jest because the controller requires MongoDB transactions.',
|
||||||
|
searchWithSetting_post: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
customer: {
|
||||||
|
createCustomer_post: 'No Jest integration case yet.',
|
||||||
|
getCustomer_get: 'No Jest integration case yet.',
|
||||||
|
updateCustomer_put: 'No Jest integration case yet.',
|
||||||
|
deleteCustomer: 'No Jest integration case yet.',
|
||||||
|
updateUser_put: 'No Jest integration case yet.',
|
||||||
|
deleteUser: 'No Jest integration case yet.',
|
||||||
|
getHierarchyById_get: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
dealer: {
|
||||||
|
updateUser_put: 'No Jest integration case yet.',
|
||||||
|
deleteUser: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
geoitem: {
|
||||||
|
search_post: 'No Jest integration case yet.',
|
||||||
|
getSharedItems_get: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
invoice: {
|
||||||
|
createInvoice_post: 'No Jest integration case yet.',
|
||||||
|
getInvoiceById_get: 'No Jest integration case yet.',
|
||||||
|
updateInvoice_put: 'No Jest integration case yet.',
|
||||||
|
updateInvoiceById_put: 'No Jest integration case yet.',
|
||||||
|
deleteInvoice_delete: 'No Jest integration case yet.',
|
||||||
|
deleteInvoiceById: 'No Jest integration case yet.',
|
||||||
|
deleteInvoices: 'No Jest integration case yet.',
|
||||||
|
getInvoiceJobs_post: 'No Jest integration case yet.',
|
||||||
|
sendInvoiceEmail_post: 'No Jest integration case yet.',
|
||||||
|
getPreviewUrl_post: 'No Jest integration case yet.',
|
||||||
|
getPrintInformation_get: 'No Jest integration case yet.',
|
||||||
|
exportInvoices_get: 'No Jest integration case yet.',
|
||||||
|
payment_get: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
invoice_settings: {
|
||||||
|
getInvoiceSettingDefault_get: 'No Jest integration case yet.',
|
||||||
|
copyInvoiceSetting_post: 'No Jest integration case yet.',
|
||||||
|
uploadLogo_post: 'No Jest integration case yet.',
|
||||||
|
deleteLogo_delete: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
job: {
|
||||||
|
getData_post: 'No Jest integration case yet.',
|
||||||
|
getReportOps_get: 'No Jest integration case yet.',
|
||||||
|
preAppReport_post: 'No Jest integration case yet.',
|
||||||
|
getRptVars_post: 'No Jest integration case yet.',
|
||||||
|
setRptVars_post: 'No Jest integration case yet.',
|
||||||
|
saveReport_post: 'No Jest integration case yet.',
|
||||||
|
preLoadReport_post: 'No Jest integration case yet.',
|
||||||
|
getUploadedFiles_post: 'No Jest integration case yet.',
|
||||||
|
importStatus_post: 'No Jest integration case yet.',
|
||||||
|
importingStatus_post: 'No Jest integration case yet.',
|
||||||
|
deleteAppFile_post: 'No Jest integration case yet.',
|
||||||
|
getJobLogs_post: 'No Jest integration case yet.',
|
||||||
|
assign_post: 'No Jest integration case yet.',
|
||||||
|
assignments_post: 'No Jest integration case yet.',
|
||||||
|
countByClient_post: 'No Jest integration case yet.',
|
||||||
|
saveMapOps_post: 'No Jest integration case yet.',
|
||||||
|
appFiles_post: 'No Jest integration case yet.',
|
||||||
|
filesdata_post: 'No Jest integration case yet.',
|
||||||
|
getAppDataByJobId: 'No Jest integration case yet.',
|
||||||
|
fetchInvReadyJobs_post: 'No Jest integration case yet.',
|
||||||
|
searchJobs_post: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
log_payment: {
|
||||||
|
createLogPayment_post: 'Skipped in Jest because the controller requires MongoDB transactions.',
|
||||||
|
createLogPayments_post: 'Skipped in Jest because the controller requires MongoDB transactions.',
|
||||||
|
},
|
||||||
|
main: {
|
||||||
|
getSiteVer_post: 'No Jest integration case yet.',
|
||||||
|
doLongOp_post: 'No Jest integration case yet.',
|
||||||
|
getActivePromos_get: 'No Jest integration case yet.',
|
||||||
|
getSubscriptionPromos_get: 'No Jest integration case yet.',
|
||||||
|
setSubscriptionPromos_post: 'No Jest integration case yet.',
|
||||||
|
addSubscriptionPromo_post: 'No Jest integration case yet.',
|
||||||
|
deleteSubscriptionPromo_delete: 'No Jest integration case yet.',
|
||||||
|
updateSubscriptionPromo_put: 'No Jest integration case yet.',
|
||||||
|
getForeverCoupons_get: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
obstacle: {
|
||||||
|
updateObstacle_put: 'No Jest integration case yet.',
|
||||||
|
deleteObstacle: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
partner: {
|
||||||
|
getPartnerById_post: 'No Jest integration case yet.',
|
||||||
|
updatePartner_put: 'No Jest integration case yet.',
|
||||||
|
syncData_post: 'No Jest integration case yet.',
|
||||||
|
uploadJob_post: 'No Jest integration case yet.',
|
||||||
|
getSystemUsers_get: 'No Jest integration case yet.',
|
||||||
|
getCurrentSystemUser_get: 'No Jest integration case yet.',
|
||||||
|
getSystemUser_get: 'No Jest integration case yet.',
|
||||||
|
createSystemUser_post: 'No Jest integration case yet.',
|
||||||
|
updateSystemUser_put: 'No Jest integration case yet.',
|
||||||
|
updateSystemUser_post: 'No Jest integration case yet.',
|
||||||
|
deleteSystemUser: 'No Jest integration case yet.',
|
||||||
|
testPartnerAuth_post: 'No Jest integration case yet.',
|
||||||
|
getPartnerCustomers_get: 'No Jest integration case yet.',
|
||||||
|
getPartnerAircraft_get: 'No Jest integration case yet.',
|
||||||
|
getPartnerCustomerStats_get: 'No Jest integration case yet.',
|
||||||
|
getPartnerJobStats_get: 'No Jest integration case yet.',
|
||||||
|
syncPartnerCustomer_post: 'No Jest integration case yet.',
|
||||||
|
getPartnerServices_get: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
pilot: {
|
||||||
|
updatePilot_put: 'No Jest integration case yet.',
|
||||||
|
deletePilot: 'Skipped in Jest because the controller requires MongoDB transactions.',
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
search_post: 'No Jest integration assertion tied to a distinct behavior yet.',
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
createUser_post: 'No Jest integration case yet.',
|
||||||
|
deleteUser: 'No Jest integration case yet.',
|
||||||
|
login_post: 'No Jest integration case yet.',
|
||||||
|
clearTempData_post: 'No Jest integration case yet.',
|
||||||
|
setUserLanguage_post: 'No Jest integration case yet.',
|
||||||
|
getUserDetail_post: 'No Jest integration case yet.',
|
||||||
|
mailPwdReset_post: 'No Jest integration case yet.',
|
||||||
|
validateResetPwdToken_post: 'No Jest integration case yet.',
|
||||||
|
resetPassword_post: 'No Jest integration case yet.',
|
||||||
|
ensureParentExists: 'Internal helper exported for reuse; not a request handler test target.',
|
||||||
|
clearTempData: 'Internal helper exported for reuse; not a request handler test target.',
|
||||||
|
getHostUrlFromReq: 'Internal helper exported for reuse; not a request handler test target.',
|
||||||
|
requestEmailVerification_post: 'No Jest integration case yet.',
|
||||||
|
verifyEmailCode_post: 'No Jest integration case yet.',
|
||||||
|
signup_post: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
vehicle: {
|
||||||
|
updateVehicle_put: 'No Jest integration case yet.',
|
||||||
|
deleteVehicle: 'Skipped in Jest because the controller requires MongoDB transactions.',
|
||||||
|
unitIdExists_post: 'No Jest integration case yet.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function stripComments(input) {
|
||||||
|
return input
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||||
|
.replace(/(^|\s)\/\/.*$/gm, '$1');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseObjectMembers(block) {
|
||||||
|
return stripComments(block)
|
||||||
|
.split(',')
|
||||||
|
.map(part => part.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(part => part.replace(/[}\s]+$/g, ''))
|
||||||
|
.map(part => part.split(':')[0].trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter(part => /^[A-Za-z_$][\w$]*$/.test(part));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExportedMethods(controllerSource) {
|
||||||
|
const directMatch = controllerSource.match(/module\.exports\s*=\s*\{([\s\S]*?)\}\s*;?\s*$/m);
|
||||||
|
if (directMatch) return parseObjectMembers(directMatch[1]);
|
||||||
|
|
||||||
|
if (controllerSource.includes('module.exports = function')) {
|
||||||
|
const returnMatches = [...controllerSource.matchAll(/return\s*\{([\s\S]*?)\}\s*;?/g)];
|
||||||
|
if (returnMatches.length > 0) {
|
||||||
|
return parseObjectMembers(returnMatches[returnMatches.length - 1][1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCoveredMethods(testSource, methods) {
|
||||||
|
const covered = new Set();
|
||||||
|
for (const method of methods) {
|
||||||
|
const invocationRegex = new RegExp(`\\.${method}\\s*\\(`);
|
||||||
|
if (invocationRegex.test(testSource)) covered.add(method);
|
||||||
|
}
|
||||||
|
return covered;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getControllerFiles() {
|
||||||
|
return fs.readdirSync(CONTROLLERS_DIR)
|
||||||
|
.filter(fileName => fileName.endsWith('.js'))
|
||||||
|
.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const failures = [];
|
||||||
|
const warnings = [];
|
||||||
|
|
||||||
|
for (const fileName of getControllerFiles()) {
|
||||||
|
const controllerName = path.basename(fileName, '.js');
|
||||||
|
if (EXCLUDED_CONTROLLERS[controllerName]) {
|
||||||
|
warnings.push(`SKIP ${controllerName}: ${EXCLUDED_CONTROLLERS[controllerName]}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controllerPath = path.join(CONTROLLERS_DIR, fileName);
|
||||||
|
const testPath = path.join(TESTS_DIR, `${controllerName}.integration.test.js`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(testPath)) {
|
||||||
|
failures.push(`Missing integration suite for controller '${controllerName}': expected tests/integration/${controllerName}.integration.test.js`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controllerSource = fs.readFileSync(controllerPath, 'utf8');
|
||||||
|
const exportedMethods = parseExportedMethods(controllerSource);
|
||||||
|
if (exportedMethods.length === 0) {
|
||||||
|
failures.push(`Could not determine exported methods for controller '${controllerName}'.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testSource = fs.readFileSync(testPath, 'utf8');
|
||||||
|
const coveredMethods = getCoveredMethods(testSource, exportedMethods);
|
||||||
|
const excludedMethods = EXCLUDED_METHODS[controllerName] || {};
|
||||||
|
|
||||||
|
for (const method of exportedMethods) {
|
||||||
|
if (coveredMethods.has(method)) continue;
|
||||||
|
if (excludedMethods[method]) {
|
||||||
|
warnings.push(`SKIP ${controllerName}.${method}: ${excludedMethods[method]}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
failures.push(
|
||||||
|
`Missing integration test coverage for ${controllerName}.${method}: add at least one test that invokes this exported method in tests/integration/${controllerName}.integration.test.js`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (warnings.length > 0) {
|
||||||
|
console.log('Integration contract exclusions:');
|
||||||
|
for (const warning of warnings) console.log(`- ${warning}`);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error('Integration controller contract check failed:');
|
||||||
|
for (const failure of failures) console.error(`- ${failure}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Integration controller contract check passed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
113
Development/server/tests/integration/client.integration.test.js
Normal file
113
Development/server/tests/integration/client.integration.test.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – client controller (controllers/client.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Client, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Client = require('../../model/client');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientCtl = require('../../controllers/client');
|
||||||
|
|
||||||
|
describe('client controller – data methods', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Client);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Client);
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('create_post', () => {
|
||||||
|
it('creates a client and returns it', async () => {
|
||||||
|
const req = makeReq({ body: mockClient(applicator._id) });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await clientCtl.createClient_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(res._data.name).toBe('Test Client');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body is missing', async () => {
|
||||||
|
const req = makeReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(clientCtl.createClient_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getById_get', () => {
|
||||||
|
let clientId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Client.create(mockClient(applicator._id));
|
||||||
|
clientId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns client by id', async () => {
|
||||||
|
const req = makeReq({ params: { id: clientId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await clientCtl.getClient_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(clientId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('search_post', () => {
|
||||||
|
it('returns matching clients', async () => {
|
||||||
|
const req = makeReq({ body: { byPuid: applicator._id } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await clientCtl.search_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('delete_delete', () => {
|
||||||
|
let clientId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Client.create(mockClient(applicator._id));
|
||||||
|
clientId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('soft-deletes a client (skipped: requires MongoDB replica-set for transactions)', async () => {
|
||||||
|
const req = makeReq({ params: { id: clientId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await clientCtl.deleteClient(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const found = await Client.findById(clientId);
|
||||||
|
expect(!found || found.markedDelete).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – common controller (controllers/common.js)
|
||||||
|
*
|
||||||
|
* Covers: getCountries_get
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockReq, mockRes } = require('./mock_data');
|
||||||
|
|
||||||
|
let Country;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Country = require('../../model/country');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const commonCtl = require('../../controllers/common');
|
||||||
|
|
||||||
|
describe('common controller – data methods', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Country);
|
||||||
|
await Country.create([
|
||||||
|
{ code: 'US', name: 'United States' },
|
||||||
|
{ code: 'CA', name: 'Canada' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Country);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCountries_get', () => {
|
||||||
|
it('returns an array of countries with code and name', async () => {
|
||||||
|
const req = mockReq();
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await commonCtl.getCountries_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBeGreaterThanOrEqual(2);
|
||||||
|
const us = res._data.find(c => c.code === 'US');
|
||||||
|
expect(us).toBeDefined();
|
||||||
|
expect(us.name).toBe('United States');
|
||||||
|
// _id should be excluded per the controller projection
|
||||||
|
expect(us._id).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty array when no countries exist', async () => {
|
||||||
|
await clearCollection(Country);
|
||||||
|
|
||||||
|
const req = mockReq();
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await commonCtl.getCountries_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – costing_items controller (controllers/costing_items.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockCostingItem, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let CostingItem, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
CostingItem = require('../../model/costing_items');
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const costingCtl = require('../../controllers/costing_items');
|
||||||
|
|
||||||
|
describe('costing_items controller – data methods', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(CostingItem);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(CostingItem);
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('create_post', () => {
|
||||||
|
it('creates a costing item and returns it', async () => {
|
||||||
|
const req = makeReq({ body: mockCostingItem(applicator._id, applicator._id) });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await costingCtl.createCostingItem_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(res._data.name).toMatch(/CostItem-/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body is missing', async () => {
|
||||||
|
const req = makeReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(costingCtl.createCostingItem_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('list_get', () => {
|
||||||
|
it('returns costing items for the applicator', async () => {
|
||||||
|
const req = makeReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await costingCtl.getCostingItems_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getById_get', () => {
|
||||||
|
let itemId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await CostingItem.create(mockCostingItem(applicator._id, applicator._id));
|
||||||
|
itemId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns costing item by id', async () => {
|
||||||
|
const req = makeReq({ params: { costingItemId: itemId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await costingCtl.getCostingItemDetail_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(itemId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when id is invalid', async () => {
|
||||||
|
const req = makeReq({ params: { costingItemId: 'bad-id' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(costingCtl.getCostingItemDetail_get(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('update_put', () => {
|
||||||
|
let itemId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await CostingItem.create(mockCostingItem(applicator._id, applicator._id));
|
||||||
|
itemId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a costing item', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
params: { costingItemId: itemId },
|
||||||
|
body: { name: 'Updated Cost Item', price: 20.0 },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await costingCtl.updateCostingItem_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.name).toBe('Updated Cost Item');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('delete_delete', () => {
|
||||||
|
let itemId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await CostingItem.create(mockCostingItem(applicator._id, applicator._id));
|
||||||
|
itemId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a costing item', async () => {
|
||||||
|
const req = makeReq({ params: { costingItemId: itemId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await costingCtl.deleteCostingItem(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const found = await CostingItem.findById(itemId);
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
155
Development/server/tests/integration/crop.integration.test.js
Normal file
155
Development/server/tests/integration/crop.integration.test.js
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – crop controller (controllers/crop.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockCrop, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Crop, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Crop = require('../../model/crop');
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const cropCtl = require('../../controllers/crop');
|
||||||
|
|
||||||
|
describe('crop controller – data methods', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Crop);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Crop);
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('create_post', () => {
|
||||||
|
it('creates a crop and returns it', async () => {
|
||||||
|
const req = makeReq({ body: mockCrop(applicator._id) });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await cropCtl.createCrop_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(res._data.name).toMatch(/Crop-/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body is missing', async () => {
|
||||||
|
const req = makeReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(cropCtl.createCrop_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getAll_get', () => {
|
||||||
|
it('returns crops for the applicator', async () => {
|
||||||
|
const req = makeReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await cropCtl.getCrops_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getById_get', () => {
|
||||||
|
let cropId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Crop.create(mockCrop(applicator._id));
|
||||||
|
cropId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a crop by id', async () => {
|
||||||
|
const req = makeReq({ params: { crop_id: cropId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await cropCtl.getCrop_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(cropId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when id is invalid', async () => {
|
||||||
|
const req = makeReq({ params: { crop_id: 'bad-id' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(cropCtl.getCrop_get(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('update_put', () => {
|
||||||
|
let cropId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Crop.create(mockCrop(applicator._id));
|
||||||
|
cropId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a crop', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
params: { crop_id: cropId },
|
||||||
|
body: { name: 'Updated Crop' },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await cropCtl.updateCrop_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.name).toBe('Updated Crop');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('search_post', () => {
|
||||||
|
it('returns matching crops', async () => {
|
||||||
|
const req = makeReq({ body: { byUserId: applicator._id } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await cropCtl.search_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('delete_delete', () => {
|
||||||
|
let cropId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Crop.create(mockCrop(applicator._id));
|
||||||
|
cropId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a crop', async () => {
|
||||||
|
const req = makeReq({ params: { crop_id: cropId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await cropCtl.deleteCrop(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const found = await Crop.findById(cropId);
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – customer controller (controllers/customer.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Customer, Client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Client = require('../../model/client');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const customerCtl = require('../../controllers/customer');
|
||||||
|
|
||||||
|
describe('customer controller – data methods', () => {
|
||||||
|
let applicator, client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Client);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
client = await Client.create(mockClient(applicator._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Client);
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getCustomers_get', () => {
|
||||||
|
it('returns aggregated customer list for applicator', async () => {
|
||||||
|
const req = makeReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await customerCtl.getCustomers_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters by name when provided', async () => {
|
||||||
|
const filters = JSON.stringify({ name: { value: 'Test Applicator', valueOperator: 'contains' } });
|
||||||
|
const req = makeReq({ query: { filters } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await customerCtl.getCustomers_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty when name does not match', async () => {
|
||||||
|
const filters = JSON.stringify({ name: { value: 'NONEXISTENT_CLIENT_XYZ', valueOperator: 'contains' } });
|
||||||
|
const req = makeReq({ query: { filters } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await customerCtl.getCustomers_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
202
Development/server/tests/integration/dealer.integration.test.js
Normal file
202
Development/server/tests/integration/dealer.integration.test.js
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – dealer controller (controllers/dealer.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockReq, mockRes } = require('./mock_data');
|
||||||
|
|
||||||
|
let Dealer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Dealer = require('../../model/dealer');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const dealerCtl = require('../../controllers/dealer');
|
||||||
|
|
||||||
|
function mockDealer(overrides = {}) {
|
||||||
|
return {
|
||||||
|
companyName: `Dealer-${Date.now()}`,
|
||||||
|
country: 'US',
|
||||||
|
contactName: 'John Smith',
|
||||||
|
address: '789 Dealer Rd',
|
||||||
|
phone: '+15550003000',
|
||||||
|
email: `dealer_${Date.now()}@test.com`,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('dealer controller – data methods', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Dealer);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Dealer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getDealers_get', () => {
|
||||||
|
it('returns an empty array when no dealers exist', async () => {
|
||||||
|
const req = mockReq();
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await dealerCtl.getDealers_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns dealers sorted by country then companyName', async () => {
|
||||||
|
await Dealer.create([
|
||||||
|
mockDealer({ companyName: 'Zulu Ag', country: 'CA' }),
|
||||||
|
mockDealer({ companyName: 'Alpha Ag', country: 'US' }),
|
||||||
|
mockDealer({ companyName: 'Beta Ag', country: 'CA' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const req = mockReq();
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await dealerCtl.getDealers_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBe(3);
|
||||||
|
// First two entries should be CA (sorted by country first)
|
||||||
|
expect(res._data[0].country).toBe('CA');
|
||||||
|
expect(res._data[1].country).toBe('CA');
|
||||||
|
// Within CA, sorted by companyName: Beta < Zulu
|
||||||
|
expect(res._data[0].companyName).toBe('Beta Ag');
|
||||||
|
expect(res._data[1].companyName).toBe('Zulu Ag');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('createDealer_post', () => {
|
||||||
|
it('creates a dealer and returns the saved document', async () => {
|
||||||
|
const req = mockReq({ body: mockDealer() });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await dealerCtl.createDealer_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(res._data.country).toBe('US');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips the _id from input so Mongo assigns its own', async () => {
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const injectedId = new mongoose.Types.ObjectId();
|
||||||
|
const req = mockReq({ body: mockDealer({ _id: injectedId }) });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await dealerCtl.createDealer_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
// The returned _id must differ from the injected one because createDealer_post deletes it
|
||||||
|
expect(String(res._data._id)).not.toBe(String(injectedId));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body is null', async () => {
|
||||||
|
const req = mockReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(dealerCtl.createDealer_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when companyName is missing', async () => {
|
||||||
|
const req = mockReq({ body: { country: 'US' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(dealerCtl.createDealer_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when country is missing', async () => {
|
||||||
|
const req = mockReq({ body: { companyName: 'Test Dealer' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(dealerCtl.createDealer_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('updateDealer_put', () => {
|
||||||
|
let dealerId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Dealer.create(mockDealer());
|
||||||
|
dealerId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a dealer and returns the updated document', async () => {
|
||||||
|
const req = mockReq({
|
||||||
|
params: { id: dealerId },
|
||||||
|
body: { contactName: 'Jane Doe' },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await dealerCtl.updateDealer_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.contactName).toBe('Jane Doe');
|
||||||
|
expect(String(res._data._id)).toBe(dealerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when id param is missing', async () => {
|
||||||
|
const req = mockReq({ params: {}, body: { contactName: 'Jane Doe' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(dealerCtl.updateDealer_put(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when id does not match any dealer', async () => {
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const req = mockReq({
|
||||||
|
params: { id: new mongoose.Types.ObjectId().toHexString() },
|
||||||
|
body: { contactName: 'Nobody' },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(dealerCtl.updateDealer_put(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('deleteDealer_delete', () => {
|
||||||
|
let dealerId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Dealer.create(mockDealer());
|
||||||
|
dealerId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a dealer and returns { ok: true }', async () => {
|
||||||
|
const req = mockReq({ params: { id: dealerId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await dealerCtl.deleteDealer_delete(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toEqual({ ok: true });
|
||||||
|
|
||||||
|
const found = await Dealer.findById(dealerId);
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when id param is missing', async () => {
|
||||||
|
const req = mockReq({ params: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(dealerCtl.deleteDealer_delete(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when id does not match any dealer', async () => {
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const req = mockReq({
|
||||||
|
params: { id: new mongoose.Types.ObjectId().toHexString() },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(dealerCtl.deleteDealer_delete(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
189
Development/server/tests/integration/geoitem.integration.test.js
Normal file
189
Development/server/tests/integration/geoitem.integration.test.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – geoitem controller (controllers/geoitem.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Area, Customer, Client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Area = require('../../model/area');
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Client = require('../../model/client');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const geoitemCtl = require('../../controllers/geoitem');
|
||||||
|
|
||||||
|
/** Minimal valid GeoJSON Polygon coordinates (a small square) */
|
||||||
|
function squareCoords() {
|
||||||
|
return [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockArea(clientId, overrides = {}) {
|
||||||
|
return {
|
||||||
|
properties: {
|
||||||
|
name: `Area-${Date.now()}`,
|
||||||
|
type: 0,
|
||||||
|
color: '#FF0000',
|
||||||
|
area: 1.5,
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Polygon',
|
||||||
|
coordinates: squareCoords(),
|
||||||
|
},
|
||||||
|
client: clientId,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('geoitem controller – data methods', () => {
|
||||||
|
let applicator, client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Area);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
client = await Client.create(mockClient(applicator._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Area);
|
||||||
|
await Customer.deleteMany({ _id: { $in: [applicator._id, client._id] } });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('findAreas_post', () => {
|
||||||
|
it('throws when clientId is missing from body', async () => {
|
||||||
|
const req = makeReq({ body: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(geoitemCtl.findAreas_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when clientId is not a valid ObjectId', async () => {
|
||||||
|
const req = makeReq({ body: { clientId: 'not-an-id' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(geoitemCtl.findAreas_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty array when no areas exist for the client', async () => {
|
||||||
|
const req = makeReq({ body: { clientId: client._id.toHexString() } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await geoitemCtl.findAreas_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns areas belonging to the specified client', async () => {
|
||||||
|
await Area.create(mockArea(client._id));
|
||||||
|
await Area.create(mockArea(client._id));
|
||||||
|
|
||||||
|
const req = makeReq({ body: { clientId: client._id.toHexString() } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await geoitemCtl.findAreas_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBe(2);
|
||||||
|
// toGeoItems output shape: { _id, properties, geometry, client }
|
||||||
|
expect(res._data[0]).toHaveProperty('_id');
|
||||||
|
expect(res._data[0]).toHaveProperty('geometry');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return areas belonging to a different client', async () => {
|
||||||
|
const otherClient = await Client.create(mockClient(applicator._id));
|
||||||
|
await Area.create(mockArea(otherClient._id));
|
||||||
|
|
||||||
|
const req = makeReq({ body: { clientId: client._id.toHexString() } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await geoitemCtl.findAreas_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
// Should still only see 2 (created in previous test)
|
||||||
|
const clientIds = res._data.map(f => String(f.properties?.client ?? ''));
|
||||||
|
expect(clientIds.every(id => id !== String(otherClient._id) || id === '')).toBe(true);
|
||||||
|
|
||||||
|
await Client.deleteMany({ _id: otherClient._id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('update_post', () => {
|
||||||
|
it('returns immediately when all update arrays are empty', async () => {
|
||||||
|
const req = makeReq({ body: { c: [], u: [], r: [] } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await geoitemCtl.update_post(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns immediately when body is null', async () => {
|
||||||
|
const req = makeReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await geoitemCtl.update_post(req, res);
|
||||||
|
|
||||||
|
expect(res.end).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inserts new areas via the c (create) array', async () => {
|
||||||
|
const area = mockArea(client._id);
|
||||||
|
const req = makeReq({ body: { c: [area] } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await geoitemCtl.update_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toEqual({ ok: true });
|
||||||
|
|
||||||
|
const found = await Area.find({ client: client._id });
|
||||||
|
expect(found.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes areas via the r (remove) array', async () => {
|
||||||
|
const doc = await Area.create(mockArea(client._id));
|
||||||
|
const countBefore = await Area.countDocuments({ client: client._id });
|
||||||
|
|
||||||
|
const req = makeReq({ body: { r: [String(doc._id)] } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await geoitemCtl.update_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toEqual({ ok: true });
|
||||||
|
|
||||||
|
const countAfter = await Area.countDocuments({ client: client._id });
|
||||||
|
expect(countAfter).toBe(countBefore - 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('addToLibrary_post', () => {
|
||||||
|
it('throws when areas array is empty', async () => {
|
||||||
|
const req = makeReq({ body: { areas: [] } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(geoitemCtl.addToLibrary_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body has no areas field', async () => {
|
||||||
|
const req = makeReq({ body: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(geoitemCtl.addToLibrary_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
128
Development/server/tests/integration/invoice.integration.test.js
Normal file
128
Development/server/tests/integration/invoice.integration.test.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – Invoice controller (controllers/invoice.js)
|
||||||
|
*
|
||||||
|
* We focus on the read path (getInvoices_get) which does not require the full
|
||||||
|
* complex invoice-creation pipeline (jobs, clients, Stripe, etc.).
|
||||||
|
* A seeded Invoice document is created directly via the model for query tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Invoice, Customer, Client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Invoice = require('../../model/invoice');
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Client = require('../../model/client');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const invoiceCtl = require('../../controllers/invoice');
|
||||||
|
|
||||||
|
describe('Invoice controller – data methods', () => {
|
||||||
|
let applicator, client;
|
||||||
|
|
||||||
|
function makeInvoiceDoc(puid, clientId) {
|
||||||
|
return {
|
||||||
|
code: `INV-${Date.now()}`,
|
||||||
|
companyName: 'Test Applicator Co.',
|
||||||
|
address: '123 Main St',
|
||||||
|
currency: 'USD',
|
||||||
|
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||||
|
openDate: new Date(),
|
||||||
|
status: 'open',
|
||||||
|
paymentTerm: 30,
|
||||||
|
byPuid: puid,
|
||||||
|
clients: [
|
||||||
|
{
|
||||||
|
billTo: clientId,
|
||||||
|
code: `INV-${Date.now()}-1`,
|
||||||
|
split: '100',
|
||||||
|
subTotal: '100.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
jobs: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Invoice);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
client = await Client.create(mockClient(applicator._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Invoice);
|
||||||
|
await Client.deleteMany({ _id: client._id });
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({
|
||||||
|
uid: applicator._id,
|
||||||
|
puid: applicator._id,
|
||||||
|
ut: '1',
|
||||||
|
userInfo: {
|
||||||
|
puid: applicator._id,
|
||||||
|
kind: '1',
|
||||||
|
premium: 0,
|
||||||
|
membership: null,
|
||||||
|
markedDelete: false,
|
||||||
|
},
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getInvoices_get', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await Invoice.create(makeInvoiceDoc(applicator._id, client._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns invoices for the applicator', async () => {
|
||||||
|
const req = makeReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await invoiceCtl.getInvoices_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when no invoices match filter', async () => {
|
||||||
|
const filters = JSON.stringify({ code: { value: 'NONEXISTENT_CODE_12345', valueOperator: 'contains' } });
|
||||||
|
const req = makeReq({ query: { filters } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await invoiceCtl.getInvoices_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws AppParamError when applicator puid is invalid', async () => {
|
||||||
|
const req = mockReq({
|
||||||
|
uid: newId(),
|
||||||
|
puid: 'invalid-puid',
|
||||||
|
ut: '1',
|
||||||
|
userInfo: {
|
||||||
|
puid: 'invalid-puid',
|
||||||
|
kind: '1',
|
||||||
|
premium: 0,
|
||||||
|
membership: null,
|
||||||
|
markedDelete: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await expect(invoiceCtl.getInvoices_get(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – invoice_settings controller (controllers/invoice_settings.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockClient, mockInvoiceSetting, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let InvoiceSetting, Customer, Client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
InvoiceSetting = require('../../model/invoice_settings');
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Client = require('../../model/client');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const invSettingsCtl = require('../../controllers/invoice_settings');
|
||||||
|
|
||||||
|
describe('invoice_settings controller – data methods', () => {
|
||||||
|
let applicator, client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(InvoiceSetting);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
client = await Client.create(mockClient(applicator._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(InvoiceSetting);
|
||||||
|
await Client.deleteMany({ _id: client._id });
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('createInvoiceSetting_post', () => {
|
||||||
|
it('creates an invoice setting and returns it', async () => {
|
||||||
|
const req = makeReq({ body: mockInvoiceSetting(applicator._id, client._id) });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await invSettingsCtl.createInvoiceSetting_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(res._data.companyName).toBe('Test Applicator Co.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getInvoiceSettings_get', () => {
|
||||||
|
it('returns invoice settings for the applicator', async () => {
|
||||||
|
const req = makeReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await invSettingsCtl.getInvoiceSettings_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getInvoiceSettingDetail_get', () => {
|
||||||
|
let settingId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(InvoiceSetting);
|
||||||
|
const doc = await InvoiceSetting.create(mockInvoiceSetting(applicator._id, client._id));
|
||||||
|
settingId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns invoice setting by id', async () => {
|
||||||
|
const req = makeReq({ params: { invoiceSettingId: settingId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await invSettingsCtl.getInvoiceSettingDetail_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(settingId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when id is invalid', async () => {
|
||||||
|
const req = makeReq({ params: { invoiceSettingId: 'bad-id' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(invSettingsCtl.getInvoiceSettingDetail_get(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('updateInvoiceSetting_put', () => {
|
||||||
|
let settingId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(InvoiceSetting);
|
||||||
|
const doc = await InvoiceSetting.create(mockInvoiceSetting(applicator._id, client._id));
|
||||||
|
settingId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates an invoice setting', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
params: { invoiceSettingId: settingId },
|
||||||
|
body: { companyName: 'Updated Co.', paymentTerm: 45 },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await invSettingsCtl.updateInvoiceSetting_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.companyName).toBe('Updated Co.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('deleteInvoiceSetting', () => {
|
||||||
|
let settingId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(InvoiceSetting);
|
||||||
|
const doc = await InvoiceSetting.create(mockInvoiceSetting(applicator._id, client._id));
|
||||||
|
settingId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes an invoice setting', async () => {
|
||||||
|
const req = makeReq({ params: { invoiceSettingId: settingId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await invSettingsCtl.deleteInvoiceSetting(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const found = await InvoiceSetting.findById(settingId);
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
65
Development/server/tests/integration/jest.setup.js
Normal file
65
Development/server/tests/integration/jest.setup.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jest integration test setup.
|
||||||
|
* Uses an in-memory MongoDB instance (mongodb-memory-server) so tests run
|
||||||
|
* without any external database credentials or infrastructure.
|
||||||
|
*
|
||||||
|
* TOKEN_SECRET may optionally be set in the environment; a safe default
|
||||||
|
* is used when it is absent (tests only, never production).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Ensure NODE_ENV=test so application code skips production-only paths.
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
// Provide a default token secret for JWT signing in tests.
|
||||||
|
if (!process.env.TOKEN_SECRET) {
|
||||||
|
process.env.TOKEN_SECRET = 'test-secret-not-for-production';
|
||||||
|
}
|
||||||
|
|
||||||
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
let mongod = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the in-memory MongoDB server and connect mongoose.
|
||||||
|
* Call this inside beforeAll() in each test file.
|
||||||
|
*/
|
||||||
|
async function connectDB() {
|
||||||
|
if (mongoose.connection.readyState !== 0) return; // already connected
|
||||||
|
|
||||||
|
mongod = await MongoMemoryServer.create();
|
||||||
|
const uri = mongod.getUri();
|
||||||
|
|
||||||
|
mongoose.set('strictPopulate', false);
|
||||||
|
await mongoose.connect(uri, {
|
||||||
|
connectTimeoutMS: 15000,
|
||||||
|
socketTimeoutMS: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register all discriminators in the correct order (user → sub-types).
|
||||||
|
// Require-ordering matters – base model first.
|
||||||
|
require('../../model');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect mongoose and stop the in-memory server.
|
||||||
|
* Call this inside afterAll() in each test file.
|
||||||
|
*/
|
||||||
|
async function disconnectDB() {
|
||||||
|
await mongoose.disconnect();
|
||||||
|
if (mongod) {
|
||||||
|
await mongod.stop();
|
||||||
|
mongod = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all documents from a collection.
|
||||||
|
* Call this inside beforeEach() / afterEach() to keep tests isolated.
|
||||||
|
*/
|
||||||
|
async function clearCollection(model) {
|
||||||
|
await model.deleteMany({});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { connectDB, disconnectDB, clearCollection };
|
||||||
213
Development/server/tests/integration/job.integration.test.js
Normal file
213
Development/server/tests/integration/job.integration.test.js
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – job controller (controllers/job.js)
|
||||||
|
*
|
||||||
|
* The job controller is a factory: require('../../controllers/job')({})
|
||||||
|
* Job._id is an auto-increment numeric field.
|
||||||
|
* deleteJob wraps transaction errors with try/catch so it works on
|
||||||
|
* standalone MongoDB (no replica-set needed).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const {
|
||||||
|
mockApplicator,
|
||||||
|
mockClient,
|
||||||
|
mockPilot,
|
||||||
|
mockVehicle,
|
||||||
|
mockJob,
|
||||||
|
mockReq,
|
||||||
|
mockRes,
|
||||||
|
newId,
|
||||||
|
} = require('./mock_data');
|
||||||
|
|
||||||
|
let Job, Customer, Client, Pilot, Vehicle;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Client = require('../../model/client');
|
||||||
|
Pilot = require('../../model/pilot');
|
||||||
|
Vehicle = require('../../model/vehicle');
|
||||||
|
Job = require('../../model/job');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Job controller is a factory
|
||||||
|
const jobCtl = require('../../controllers/job')({});
|
||||||
|
|
||||||
|
describe('job controller – data methods', () => {
|
||||||
|
let applicator, client, pilot, vehicle;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Job);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
client = await Client.create(mockClient(applicator._id));
|
||||||
|
pilot = await Pilot.create(mockPilot(applicator._id));
|
||||||
|
vehicle = await Vehicle.create(mockVehicle(applicator._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Job);
|
||||||
|
await Client.deleteMany({ _id: client._id });
|
||||||
|
await Pilot.deleteMany({ _id: pilot._id });
|
||||||
|
await Vehicle.deleteMany({ _id: vehicle._id });
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({
|
||||||
|
uid: applicator._id,
|
||||||
|
puid: applicator._id,
|
||||||
|
ut: '1',
|
||||||
|
userInfo: {
|
||||||
|
puid: applicator._id,
|
||||||
|
kind: '1',
|
||||||
|
premium: 0,
|
||||||
|
membership: null,
|
||||||
|
markedDelete: false,
|
||||||
|
},
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('createJob_post', () => {
|
||||||
|
it('creates a job and returns it', async () => {
|
||||||
|
const body = mockJob(applicator._id);
|
||||||
|
body.client = client._id;
|
||||||
|
body.pilot = pilot._id;
|
||||||
|
body.vehicle = vehicle._id;
|
||||||
|
|
||||||
|
const req = makeReq({ body });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await jobCtl.createJob_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(typeof res._data._id).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when puid is missing', async () => {
|
||||||
|
const req = makeReq({ puid: undefined, body: mockJob(applicator._id) });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(jobCtl.createJob_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getJobs_get', () => {
|
||||||
|
it('returns jobs for the applicator', async () => {
|
||||||
|
const req = makeReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await jobCtl.getJobs_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when puid has no jobs', async () => {
|
||||||
|
const unknownId = newId();
|
||||||
|
const req = mockReq({
|
||||||
|
uid: unknownId,
|
||||||
|
puid: unknownId,
|
||||||
|
ut: '1',
|
||||||
|
userInfo: { puid: unknownId, kind: '1', premium: 0, membership: null, markedDelete: false },
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await jobCtl.getJobs_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getJob_get', () => {
|
||||||
|
let jobId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const body = mockJob(applicator._id);
|
||||||
|
body.client = client._id;
|
||||||
|
body.pilot = pilot._id;
|
||||||
|
body.vehicle = vehicle._id;
|
||||||
|
const req = makeReq({ body });
|
||||||
|
const res = mockRes();
|
||||||
|
await jobCtl.createJob_post(req, res);
|
||||||
|
jobId = res._data._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a job by numeric id', async () => {
|
||||||
|
const req = makeReq({ params: { job_id: String(jobId) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await jobCtl.getJob_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBe(jobId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('updateJob_put', () => {
|
||||||
|
let jobId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const body = mockJob(applicator._id);
|
||||||
|
body.client = client._id;
|
||||||
|
body.pilot = pilot._id;
|
||||||
|
body.vehicle = vehicle._id;
|
||||||
|
const req = makeReq({ body });
|
||||||
|
const res = mockRes();
|
||||||
|
await jobCtl.createJob_post(req, res);
|
||||||
|
jobId = res._data._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a job field', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
params: { job_id: String(jobId) },
|
||||||
|
body: { job: { name: 'Updated Job Name' } },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await jobCtl.updateJob_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.name).toBe('Updated Job Name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('deleteJob', () => {
|
||||||
|
let jobId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const body = mockJob(applicator._id);
|
||||||
|
body.client = client._id;
|
||||||
|
body.pilot = pilot._id;
|
||||||
|
body.vehicle = vehicle._id;
|
||||||
|
const req = makeReq({ body });
|
||||||
|
const res = mockRes();
|
||||||
|
await jobCtl.createJob_post(req, res);
|
||||||
|
jobId = res._data._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a job (transaction error handled on standalone MongoDB)', async () => {
|
||||||
|
const req = makeReq({ params: { id: String(jobId) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
// deleteJob wraps the transaction with try/catch – it either succeeds
|
||||||
|
// fully or returns a soft-success; either way res.json is called.
|
||||||
|
await jobCtl.deleteJob(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – log_payment controller (controllers/log_payment.js)
|
||||||
|
*
|
||||||
|
* createLogPayment_post and createLogPayments_post are skipped because they
|
||||||
|
* use MongoDB transactions which require a replica-set. Only the read path
|
||||||
|
* (getLogPayments_get) is tested here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let LogPayment, Invoice, Customer, Client;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Client = require('../../model/client');
|
||||||
|
LogPayment = require('../../model/log_payment');
|
||||||
|
({ Invoice } = require('../../model'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const logPaymentCtl = require('../../controllers/log_payment');
|
||||||
|
|
||||||
|
describe('log_payment controller – data methods', () => {
|
||||||
|
let applicator, client, invoice;
|
||||||
|
|
||||||
|
function makeInvoiceDoc(puid, clientId) {
|
||||||
|
return {
|
||||||
|
code: `INV-TEST-${Date.now()}`,
|
||||||
|
currency: 'USD',
|
||||||
|
dueDate: new Date(),
|
||||||
|
openDate: new Date(),
|
||||||
|
byPuid: puid,
|
||||||
|
clients: [{ billTo: clientId, split: '100', subTotal: '0.00' }],
|
||||||
|
jobs: [{ totalAmount: '0.00' }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLogPaymentDoc(puid, clientId, invoiceId) {
|
||||||
|
return {
|
||||||
|
byPuid: puid,
|
||||||
|
invoice: invoiceId,
|
||||||
|
client: clientId,
|
||||||
|
amount: '100.00',
|
||||||
|
paymentDate: new Date(),
|
||||||
|
paymentMethod: 'cash',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(LogPayment);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
client = await Client.create(mockClient(applicator._id));
|
||||||
|
invoice = await Invoice.create(makeInvoiceDoc(applicator._id, client._id));
|
||||||
|
await LogPayment.create(makeLogPaymentDoc(applicator._id, client._id, invoice._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(LogPayment);
|
||||||
|
await Invoice.deleteMany({ _id: invoice._id });
|
||||||
|
await Client.deleteMany({ _id: client._id });
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getLogPayments_get', () => {
|
||||||
|
it('returns log payments for the applicator', async () => {
|
||||||
|
const req = makeReq({ query: { invoiceId: String(invoice._id) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await logPaymentCtl.getLogPayments_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when invoice has no log payments', async () => {
|
||||||
|
const emptyInvoice = await Invoice.create(makeInvoiceDoc(applicator._id, client._id));
|
||||||
|
const req = makeReq({ query: { invoiceId: String(emptyInvoice._id) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await logPaymentCtl.getLogPayments_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
await Invoice.deleteMany({ _id: emptyInvoice._id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('createLogPayment_post (transaction – skipped)', () => {
|
||||||
|
it.skip('skipped: MongoDB transactions require a replica-set', () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createLogPayments_post (transaction – skipped)', () => {
|
||||||
|
it.skip('skipped: MongoDB transactions require a replica-set', () => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – main controller (controllers/main.js)
|
||||||
|
*
|
||||||
|
* Covers: pingAPI_get, getAppConfig_get, setAppConfig_post
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Settings;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Settings = require('../../model/setting');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainCtl = require('../../controllers/main');
|
||||||
|
|
||||||
|
describe('main controller – data methods', () => {
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('pingAPI_get', () => {
|
||||||
|
it('responds with a pong / alive message', async () => {
|
||||||
|
const req = mockReq();
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await mainCtl.pingAPI_get(req, res);
|
||||||
|
|
||||||
|
// pingAPI_get uses res.send() or res.json()
|
||||||
|
const called = res.json.mock.calls.length > 0 || res.send.mock.calls.length > 0;
|
||||||
|
expect(called).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getAppConfig_get', () => {
|
||||||
|
it('returns an app config object', async () => {
|
||||||
|
const req = mockReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await mainCtl.getAppConfig_get(req, res);
|
||||||
|
|
||||||
|
const called = res.json.mock.calls.length > 0 || res.send.mock.calls.length > 0;
|
||||||
|
expect(called).toBe(true);
|
||||||
|
expect(typeof res._data).toBe('object');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('setAppConfig_post', () => {
|
||||||
|
it('saves an app config value and returns the saved document', async () => {
|
||||||
|
await clearCollection(Settings);
|
||||||
|
|
||||||
|
const req = mockReq({
|
||||||
|
uid: newId(),
|
||||||
|
ut: '1',
|
||||||
|
body: { key: 'testKey', value: 'testValue' },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await mainCtl.setAppConfig_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
225
Development/server/tests/integration/mock_data.js
Normal file
225
Development/server/tests/integration/mock_data.js
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared mock data factories for integration tests.
|
||||||
|
* All ids are fresh ObjectIds so tests are fully isolated.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const newId = () => new mongoose.Types.ObjectId();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Applicator / Customer (kind = "1")
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockApplicator(overrides = {}) {
|
||||||
|
return {
|
||||||
|
_id: newId(),
|
||||||
|
kind: '1',
|
||||||
|
username: `applicator_${Date.now()}@test.com`,
|
||||||
|
password: '$2a$10$hashedpassword',
|
||||||
|
name: 'Test Applicator Co.',
|
||||||
|
email: `applicator_${Date.now()}@test.com`,
|
||||||
|
phone: '+15550001000',
|
||||||
|
address: '123 Main St',
|
||||||
|
country: 'US',
|
||||||
|
active: true,
|
||||||
|
billable: true,
|
||||||
|
premium: 0,
|
||||||
|
selfSignup: false,
|
||||||
|
markedDelete: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Client (kind = "3")
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockClient(parentId, overrides = {}) {
|
||||||
|
return {
|
||||||
|
_id: newId(),
|
||||||
|
kind: '3',
|
||||||
|
username: `client_${Date.now()}@test.com`,
|
||||||
|
name: 'Test Client',
|
||||||
|
email: `client_${Date.now()}@test.com`,
|
||||||
|
phone: '+15550002000',
|
||||||
|
address: '456 Oak Ave',
|
||||||
|
country: 'US',
|
||||||
|
active: true,
|
||||||
|
parent: parentId,
|
||||||
|
markedDelete: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pilot (kind = "5")
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockPilot(parentId, overrides = {}) {
|
||||||
|
return {
|
||||||
|
_id: newId(),
|
||||||
|
kind: '5',
|
||||||
|
username: `pilot_${Date.now()}@test.com`,
|
||||||
|
name: 'Test Pilot',
|
||||||
|
email: `pilot_${Date.now()}@test.com`,
|
||||||
|
active: true,
|
||||||
|
parent: parentId,
|
||||||
|
markedDelete: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Vehicle / Device (kind = "9")
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockVehicle(parentId, overrides = {}) {
|
||||||
|
return {
|
||||||
|
_id: newId(),
|
||||||
|
kind: '9',
|
||||||
|
name: `Aircraft-${Date.now()}`,
|
||||||
|
model: 'AgNav TestBird',
|
||||||
|
color: 'yellow',
|
||||||
|
vehicleType: 0,
|
||||||
|
active: true,
|
||||||
|
tracking: false,
|
||||||
|
pkgActive: false,
|
||||||
|
parent: parentId,
|
||||||
|
markedDelete: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Product
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockProduct(puid, overrides = {}) {
|
||||||
|
return {
|
||||||
|
name: `Product-${Date.now()}`,
|
||||||
|
type: 1, // ACTIVE
|
||||||
|
restricted: false,
|
||||||
|
epaReg: `EPA-${Date.now()}`,
|
||||||
|
desc: 'Integration test product',
|
||||||
|
rate: { value: 2.5, unit: 1 }, // GAL
|
||||||
|
byPuid: puid,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Crop
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockCrop(puid, overrides = {}) {
|
||||||
|
return {
|
||||||
|
name: `Crop-${Date.now()}`,
|
||||||
|
color: '#00AA00',
|
||||||
|
desc: 'Integration test crop',
|
||||||
|
byPuid: puid,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Job
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockJob(puid, overrides = {}) {
|
||||||
|
return {
|
||||||
|
name: `Job-${Date.now()}`,
|
||||||
|
orderNumber: `ORD-${Date.now()}`,
|
||||||
|
measureUnit: false, // false = acres
|
||||||
|
swathWidth: 60,
|
||||||
|
byPuid: puid,
|
||||||
|
status: 0, // PENDING
|
||||||
|
markedDelete: false,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Costing Item
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockCostingItem(puid, userId, overrides = {}) {
|
||||||
|
return {
|
||||||
|
name: `CostItem-${Date.now()}`,
|
||||||
|
type: 0,
|
||||||
|
unit: 1, // GAL
|
||||||
|
price: 10.5,
|
||||||
|
byPuid: puid,
|
||||||
|
createdBy: userId,
|
||||||
|
updatedBy: userId,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Invoice Setting
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
function mockInvoiceSetting(puid, userId, overrides = {}) {
|
||||||
|
return {
|
||||||
|
byPuid: puid,
|
||||||
|
userId,
|
||||||
|
companyName: 'Test Applicator Co.',
|
||||||
|
address: '123 Main St, Testville, TX',
|
||||||
|
taxValue: 0,
|
||||||
|
discount: 0,
|
||||||
|
termOpts: [15, 30, 60],
|
||||||
|
paymentTerm: 30,
|
||||||
|
currency: 'USD',
|
||||||
|
note: 'Integration test invoice setting',
|
||||||
|
logo: '',
|
||||||
|
createdBy: userId,
|
||||||
|
updatedBy: userId,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Request / response mocks used for controller unit-style tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a minimal Express-like req object for an authenticated applicator.
|
||||||
|
*/
|
||||||
|
function mockReq({ uid, puid, ut = '1', body = {}, params = {}, query = {}, userInfo = null } = {}) {
|
||||||
|
const _uid = uid ? String(uid) : String(new mongoose.Types.ObjectId());
|
||||||
|
const _puid = puid ? String(puid) : _uid;
|
||||||
|
return {
|
||||||
|
uid: _uid,
|
||||||
|
ut,
|
||||||
|
userInfo: userInfo || { puid: _puid, kind: ut, premium: 0, membership: null, markedDelete: false },
|
||||||
|
body,
|
||||||
|
params,
|
||||||
|
query,
|
||||||
|
headers: {},
|
||||||
|
file: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a minimal Express-like res object with jest spy functions.
|
||||||
|
*/
|
||||||
|
function mockRes() {
|
||||||
|
const res = {
|
||||||
|
statusCode: 200,
|
||||||
|
_data: undefined,
|
||||||
|
};
|
||||||
|
res.json = jest.fn((data) => { res._data = data; return res; });
|
||||||
|
res.send = jest.fn((data) => { res._data = data; return res; });
|
||||||
|
res.status = jest.fn((code) => { res.statusCode = code; return res; });
|
||||||
|
res.end = jest.fn(() => res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
newId,
|
||||||
|
mockApplicator,
|
||||||
|
mockClient,
|
||||||
|
mockPilot,
|
||||||
|
mockVehicle,
|
||||||
|
mockProduct,
|
||||||
|
mockCrop,
|
||||||
|
mockJob,
|
||||||
|
mockCostingItem,
|
||||||
|
mockInvoiceSetting,
|
||||||
|
mockReq,
|
||||||
|
mockRes,
|
||||||
|
};
|
||||||
@ -0,0 +1,187 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – obstacle controller (controllers/obstacle.js)
|
||||||
|
*
|
||||||
|
* Covers: createObstacle_post, updateObstacle_put, deleteObstacle, near_post
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Obstacle;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Obstacle = require('../../model/obstacles');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const obstacleCtl = require('../../controllers/obstacle');
|
||||||
|
|
||||||
|
describe('obstacle controller – data methods', () => {
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Obstacle);
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('createObstacle_post', () => {
|
||||||
|
it('creates an obstacle and returns it as a plain object', async () => {
|
||||||
|
const userId = newId();
|
||||||
|
const req = mockReq({
|
||||||
|
uid: userId,
|
||||||
|
body: {
|
||||||
|
item: {
|
||||||
|
name: 'Test Tower',
|
||||||
|
type: 'USER',
|
||||||
|
agl: 150,
|
||||||
|
amsl: 500,
|
||||||
|
coors: [-80.1234, 25.7617],
|
||||||
|
},
|
||||||
|
pUser: String(userId),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await obstacleCtl.createObstacle_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const obstacle = res._data;
|
||||||
|
expect(obstacle._id).toBeDefined();
|
||||||
|
expect(obstacle.name).toBe('Test Tower');
|
||||||
|
expect(obstacle.agl).toBe(150);
|
||||||
|
expect(obstacle.amsl).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws AppParamError when body is missing', async () => {
|
||||||
|
const req = mockReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(obstacleCtl.createObstacle_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws AppParamError when coordinates are missing', async () => {
|
||||||
|
const req = mockReq({
|
||||||
|
body: {
|
||||||
|
item: { name: 'Bad Obs', agl: 100, coors: [] },
|
||||||
|
pUser: String(newId()),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(obstacleCtl.createObstacle_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('updateObstacle_put', () => {
|
||||||
|
let obstacleId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const obs = await Obstacle.create({
|
||||||
|
properties: { name: 'Original', type: 'USER', agl: 100, amsl: 300 },
|
||||||
|
geometry: { type: 'Point', coordinates: [-80.0, 25.0] },
|
||||||
|
});
|
||||||
|
obstacleId = String(obs._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates name and agl and returns updated obstacle', async () => {
|
||||||
|
const req = mockReq({
|
||||||
|
params: { ob_id: obstacleId },
|
||||||
|
body: { name: 'Updated Tower', agl: 200, amsl: 400 },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await obstacleCtl.updateObstacle_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toBeTruthy();
|
||||||
|
expect(res._data.name).toBe('Updated Tower');
|
||||||
|
expect(res._data.agl).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('deleteObstacle', () => {
|
||||||
|
let obstacleId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const obs = await Obstacle.create({
|
||||||
|
properties: { name: 'To Delete', type: 'USER', agl: 50, amsl: 200 },
|
||||||
|
geometry: { type: 'Point', coordinates: [-79.0, 26.0] },
|
||||||
|
});
|
||||||
|
obstacleId = String(obs._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes obstacle and returns success message', async () => {
|
||||||
|
const req = mockReq({ params: { ob_id: obstacleId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await obstacleCtl.deleteObstacle(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.message).toMatch(/deleted/i);
|
||||||
|
const found = await Obstacle.findById(obstacleId);
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns success even when obstacle does not exist', async () => {
|
||||||
|
const req = mockReq({ params: { ob_id: String(newId()) } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await obstacleCtl.deleteObstacle(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.message).toMatch(/deleted/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('near_post', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await Obstacle.ensureIndexes();
|
||||||
|
await Obstacle.create({
|
||||||
|
properties: { name: 'Miami Tower', type: 'USER', agl: 200, amsl: 600 },
|
||||||
|
geometry: { type: 'Point', coordinates: [-80.1918, 25.7617] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns obstacles near the given center within radius', async () => {
|
||||||
|
const req = mockReq({
|
||||||
|
body: { center: { lng: -80.1918, lat: 25.7617 }, min: 0 },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await obstacleCtl.near_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
expect(res._data.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when no obstacles are in the vicinity', async () => {
|
||||||
|
const req = mockReq({
|
||||||
|
body: { center: { lng: -140.0, lat: 0.0 }, min: 0 },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await obstacleCtl.near_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters by minimum AGL height', async () => {
|
||||||
|
const req = mockReq({
|
||||||
|
body: { center: { lng: -80.1918, lat: 25.7617 }, min: 999 },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await obstacleCtl.near_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
180
Development/server/tests/integration/partner.integration.test.js
Normal file
180
Development/server/tests/integration/partner.integration.test.js
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – partner controller (controllers/partner.js)
|
||||||
|
*
|
||||||
|
* Covers: createPartner_post, getPartners_get, getPartners_post,
|
||||||
|
* getPartnerById_get, updatePartner_put, deletePartner
|
||||||
|
*
|
||||||
|
* Skipped: syncData_post, uploadJob_post, testPartnerAuth_post
|
||||||
|
* (require external partner service / RabbitMQ).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Partner, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
// Ensure partner discriminator is registered
|
||||||
|
const partnerModel = require('../../model/partner');
|
||||||
|
Partner = partnerModel.Partner || partnerModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const partnerCtl = require('../../controllers/partner');
|
||||||
|
|
||||||
|
describe('partner controller – data methods', () => {
|
||||||
|
let admin;
|
||||||
|
|
||||||
|
function makePartnerBody() {
|
||||||
|
return {
|
||||||
|
name: `Partner-${Date.now()}`,
|
||||||
|
username: `partner_${Date.now()}@test.com`,
|
||||||
|
password: 'Test@123',
|
||||||
|
active: true,
|
||||||
|
kind: '20',
|
||||||
|
partnerCode: `TESTCODE-${Date.now()}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Partner);
|
||||||
|
admin = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Partner);
|
||||||
|
await Customer.deleteMany({ _id: admin._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: admin._id, puid: admin._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('createPartner_post', () => {
|
||||||
|
it('creates a partner and returns it', async () => {
|
||||||
|
const req = makeReq({ body: makePartnerBody() });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await partnerCtl.createPartner_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(res._data.kind).toBe('20');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body is missing', async () => {
|
||||||
|
const req = makeReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(partnerCtl.createPartner_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getPartners_get', () => {
|
||||||
|
it('returns array of partners', async () => {
|
||||||
|
const req = makeReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await partnerCtl.getPartners_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getPartners_post', () => {
|
||||||
|
it('returns partners matching search criteria', async () => {
|
||||||
|
const req = makeReq({ body: { name: 'Partner' } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await partnerCtl.getPartners_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getPartnerById_get', () => {
|
||||||
|
let partnerId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const req = makeReq({ body: makePartnerBody() });
|
||||||
|
const res = mockRes();
|
||||||
|
await partnerCtl.createPartner_post(req, res);
|
||||||
|
partnerId = String(res._data._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns partner by id', async () => {
|
||||||
|
const req = makeReq({ params: { id: partnerId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await partnerCtl.getPartnerById_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(partnerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when partner id is invalid', async () => {
|
||||||
|
const req = makeReq({ params: { id: 'invalid-id' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(partnerCtl.getPartnerById_get(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('updatePartner_put', () => {
|
||||||
|
let partnerId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const req = makeReq({ body: makePartnerBody() });
|
||||||
|
const res = mockRes();
|
||||||
|
await partnerCtl.createPartner_post(req, res);
|
||||||
|
partnerId = String(res._data._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a partner field', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
params: { id: partnerId },
|
||||||
|
body: { name: 'Updated Partner Name', kind: '20' },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await partnerCtl.updatePartner_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.name).toBe('Updated Partner Name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('deletePartner', () => {
|
||||||
|
let partnerId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const req = makeReq({ body: makePartnerBody() });
|
||||||
|
const res = mockRes();
|
||||||
|
await partnerCtl.createPartner_post(req, res);
|
||||||
|
partnerId = String(res._data._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('soft-deletes a partner', async () => {
|
||||||
|
const req = makeReq({ params: { id: partnerId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await partnerCtl.deletePartner(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const found = await Partner.findById(partnerId);
|
||||||
|
expect(!found || found.active === false).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
113
Development/server/tests/integration/pilot.integration.test.js
Normal file
113
Development/server/tests/integration/pilot.integration.test.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – pilot controller (controllers/pilot.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockPilot, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Pilot, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Pilot = require('../../model/pilot');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const pilotCtl = require('../../controllers/pilot');
|
||||||
|
|
||||||
|
describe('pilot controller – data methods', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Pilot);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Pilot);
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('create_post', () => {
|
||||||
|
it('creates a pilot and returns it', async () => {
|
||||||
|
const req = makeReq({ body: mockPilot(applicator._id) });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await pilotCtl.createPilot_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(res._data.name).toBe('Test Pilot');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body is missing', async () => {
|
||||||
|
const req = makeReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(pilotCtl.createPilot_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getById_get', () => {
|
||||||
|
let pilotId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Pilot.create(mockPilot(applicator._id));
|
||||||
|
pilotId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns pilot by id', async () => {
|
||||||
|
const req = makeReq({ params: { id: pilotId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await pilotCtl.getPilot_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(pilotId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('search_post', () => {
|
||||||
|
it('returns matching pilots', async () => {
|
||||||
|
const req = makeReq({ body: { byUserId: applicator._id } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await pilotCtl.search_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('delete_delete', () => {
|
||||||
|
let pilotId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Pilot.create(mockPilot(applicator._id));
|
||||||
|
pilotId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('soft-deletes a pilot (skipped: requires MongoDB replica-set for transactions)', async () => {
|
||||||
|
const req = makeReq({ params: { id: pilotId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await pilotCtl.deletePilot(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const found = await Pilot.findById(pilotId);
|
||||||
|
expect(!found || found.markedDelete).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
156
Development/server/tests/integration/product.integration.test.js
Normal file
156
Development/server/tests/integration/product.integration.test.js
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – product controller (controllers/product.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockProduct, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Product, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Product = require('../../model/product');
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const productCtl = require('../../controllers/product');
|
||||||
|
|
||||||
|
describe('product controller – data methods', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Product);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Product);
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('create_post', () => {
|
||||||
|
it('creates a product and returns it', async () => {
|
||||||
|
const req = makeReq({ body: mockProduct(applicator._id) });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await productCtl.createProduct_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const p = res._data;
|
||||||
|
expect(p._id).toBeDefined();
|
||||||
|
expect(p.name).toMatch(/Product-/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body is missing', async () => {
|
||||||
|
const req = makeReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(productCtl.createProduct_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getAll_get', () => {
|
||||||
|
it('returns products for the applicator', async () => {
|
||||||
|
const req = makeReq({ query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await productCtl.getProducts_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getById_get', () => {
|
||||||
|
let productId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Product.create(mockProduct(applicator._id));
|
||||||
|
productId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a product by id', async () => {
|
||||||
|
const req = makeReq({ params: { product_id: productId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await productCtl.getProduct_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(productId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when id is invalid', async () => {
|
||||||
|
const req = makeReq({ params: { product_id: 'bad-id' } });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(productCtl.getProduct_get(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('update_put', () => {
|
||||||
|
let productId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Product.create(mockProduct(applicator._id));
|
||||||
|
productId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a product', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
params: { product_id: productId },
|
||||||
|
body: { name: 'Updated Product' },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await productCtl.updateProduct_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.name).toBe('Updated Product');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('search_post', () => {
|
||||||
|
it('returns matching products', async () => {
|
||||||
|
const req = makeReq({ body: { byUserId: applicator._id } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await productCtl.search_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('delete_delete', () => {
|
||||||
|
let productId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Product.create(mockProduct(applicator._id));
|
||||||
|
productId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes a product', async () => {
|
||||||
|
const req = makeReq({ params: { product_id: productId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await productCtl.deleteProduct(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const found = await Product.findById(productId);
|
||||||
|
expect(found).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
112
Development/server/tests/integration/user.integration.test.js
Normal file
112
Development/server/tests/integration/user.integration.test.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – user controller (controllers/user.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let User, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
User = require('../../model/user');
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const userCtl = require('../../controllers/user');
|
||||||
|
|
||||||
|
describe('user controller – data methods', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getUser_get', () => {
|
||||||
|
it('returns a user by id', async () => {
|
||||||
|
const req = makeReq({ params: { id: String(applicator._id) }, query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await userCtl.getUser_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(String(applicator._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when user not found', async () => {
|
||||||
|
const req = makeReq({ params: { id: String(newId()) }, query: {} });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await userCtl.getUser_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('updateUser_put', () => {
|
||||||
|
it('updates a user field', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
params: { id: String(applicator._id) },
|
||||||
|
body: { name: 'Updated Applicator Name', kind: '1' },
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await userCtl.updateUser_put(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data.name).toBe('Updated Applicator Name');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('usernameExists_post', () => {
|
||||||
|
it('returns true when username exists', async () => {
|
||||||
|
const req = makeReq({ body: { username: applicator.username } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await userCtl.isUserNameExists_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when username does not exist', async () => {
|
||||||
|
const req = makeReq({ body: { username: `nonexistent_${Date.now()}@test.com` } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await userCtl.isUserNameExists_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('searchUsers_post', () => {
|
||||||
|
it('returns matching users', async () => {
|
||||||
|
const req = makeReq({ body: { byPuid: applicator._id } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await userCtl.search_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
137
Development/server/tests/integration/vehicle.integration.test.js
Normal file
137
Development/server/tests/integration/vehicle.integration.test.js
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests – vehicle controller (controllers/vehicle.js)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { connectDB, disconnectDB, clearCollection } = require('./jest.setup');
|
||||||
|
const { mockApplicator, mockVehicle, mockReq, mockRes, newId } = require('./mock_data');
|
||||||
|
|
||||||
|
let Vehicle, Customer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await connectDB();
|
||||||
|
Customer = require('../../model/customer');
|
||||||
|
Vehicle = require('../../model/vehicle');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await disconnectDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
const vehicleCtl = require('../../controllers/vehicle');
|
||||||
|
|
||||||
|
describe('vehicle controller – data methods', () => {
|
||||||
|
let applicator;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await clearCollection(Vehicle);
|
||||||
|
applicator = await Customer.create(mockApplicator());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await clearCollection(Vehicle);
|
||||||
|
await Customer.deleteMany({ _id: applicator._id });
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeReq = (extra = {}) =>
|
||||||
|
mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra });
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('create_post', () => {
|
||||||
|
it('creates a vehicle and returns it', async () => {
|
||||||
|
const req = makeReq({ body: mockVehicle(applicator._id) });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await vehicleCtl.createVehicle_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(res._data._id).toBeDefined();
|
||||||
|
expect(res._data.name).toMatch(/Aircraft-/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when body is missing', async () => {
|
||||||
|
const req = makeReq({ body: null });
|
||||||
|
const res = mockRes();
|
||||||
|
await expect(vehicleCtl.createVehicle_post(req, res)).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('getById_get', () => {
|
||||||
|
let vehicleId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Vehicle.create(mockVehicle(applicator._id));
|
||||||
|
vehicleId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns vehicle by id', async () => {
|
||||||
|
const req = makeReq({ params: { id: vehicleId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await vehicleCtl.getVehicle_get(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(String(res._data._id)).toBe(vehicleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('search_post', () => {
|
||||||
|
it('returns matching vehicles', async () => {
|
||||||
|
const req = makeReq({ body: { byUserId: applicator._id } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await vehicleCtl.search_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
expect(Array.isArray(res._data)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('bulkUpdate_post', () => {
|
||||||
|
let vehicleIds;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const docs = await Vehicle.create([
|
||||||
|
mockVehicle(applicator._id),
|
||||||
|
mockVehicle(applicator._id),
|
||||||
|
]);
|
||||||
|
vehicleIds = docs.map(d => String(d._id));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bulk-updates multiple vehicles', async () => {
|
||||||
|
const req = makeReq({
|
||||||
|
body: vehicleIds.map(id => ({ _id: id, active: false })),
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await vehicleCtl.updateVehicles_post(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
describe('delete_delete', () => {
|
||||||
|
let vehicleId;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const doc = await Vehicle.create(mockVehicle(applicator._id));
|
||||||
|
vehicleId = String(doc._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('soft-deletes a vehicle (skipped: requires MongoDB replica-set for transactions)', async () => {
|
||||||
|
const req = makeReq({ params: { id: vehicleId } });
|
||||||
|
const res = mockRes();
|
||||||
|
|
||||||
|
await vehicleCtl.deleteVehicle(req, res);
|
||||||
|
|
||||||
|
expect(res.json).toHaveBeenCalled();
|
||||||
|
const found = await Vehicle.findById(vehicleId);
|
||||||
|
expect(!found || found.markedDelete).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user