From 6ff07c9d74b4d24c41ca88e199a4d3111b3e8d4f Mon Sep 17 00:00:00 2001 From: Devin Major Date: Fri, 8 May 2026 14:51:04 -0400 Subject: [PATCH] additional integration tests --- Development/server/package-lock.json | 204 +++------- .../integration/billing.integration.test.js | 44 ++ .../integration/check_controller_contract.js | 143 +------ .../integration/client.integration.test.js | 50 +++ .../integration/customer.integration.test.js | 50 +++ .../tests/integration/dlq.integration.test.js | 220 ++++++++++ .../integration/geoitem.integration.test.js | 46 ++- .../integration/geoutil.integration.test.js | 85 ++++ .../integration/health.integration.test.js | 97 +++++ .../integration/invoice.integration.test.js | 92 +++++ .../invoice_settings.integration.test.js | 27 ++ .../tests/integration/job.integration.test.js | 375 ++++++++++++++++++ .../integration/location.integration.test.js | 116 ++++++ .../integration/main.integration.test.js | 182 ++++++++- .../integration/partner.integration.test.js | 358 ++++++++++++++++- .../integration/pilot.integration.test.js | 31 ++ .../integration/user.integration.test.js | 263 ++++++++++++ .../integration/vehicle.integration.test.js | 66 +++ 18 files changed, 2167 insertions(+), 282 deletions(-) create mode 100644 Development/server/tests/integration/dlq.integration.test.js create mode 100644 Development/server/tests/integration/geoutil.integration.test.js create mode 100644 Development/server/tests/integration/health.integration.test.js create mode 100644 Development/server/tests/integration/location.integration.test.js diff --git a/Development/server/package-lock.json b/Development/server/package-lock.json index 6ce0707..0aec119 100644 --- a/Development/server/package-lock.json +++ b/Development/server/package-lock.json @@ -106,6 +106,7 @@ } }, "../../../@agn/error-handler": { + "name": "error-handler", "version": "2.0.0", "license": "Proprietary", "dependencies": { @@ -12817,13 +12818,11 @@ }, "node_modules/node-libxml/node_modules/abbrev": { "version": "1.1.1", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/ansi-regex": { "version": "2.1.1", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -12832,13 +12831,11 @@ }, "node_modules/node-libxml/node_modules/aproba": { "version": "1.2.0", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/are-we-there-yet": { "version": "1.1.5", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -12867,7 +12864,6 @@ }, "node_modules/node-libxml/node_modules/code-point-at": { "version": "1.1.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -12886,13 +12882,11 @@ }, "node_modules/node-libxml/node_modules/console-control-strings": { "version": "1.1.0", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/core-util-is": { "version": "1.0.2", - "extraneous": true, "inBundle": true, "license": "MIT" }, @@ -12906,7 +12900,6 @@ }, "node_modules/node-libxml/node_modules/deep-extend": { "version": "0.6.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -12915,13 +12908,11 @@ }, "node_modules/node-libxml/node_modules/delegates": { "version": "1.0.0", - "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/node-libxml/node_modules/detect-libc": { "version": "1.0.3", - "extraneous": true, "inBundle": true, "license": "Apache-2.0", "bin": { @@ -12954,7 +12945,6 @@ }, "node_modules/node-libxml/node_modules/gauge": { "version": "2.7.4", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -12995,7 +12985,6 @@ }, "node_modules/node-libxml/node_modules/has-unicode": { "version": "2.0.1", - "extraneous": true, "inBundle": true, "license": "ISC" }, @@ -13009,7 +12998,6 @@ }, "node_modules/node-libxml/node_modules/iconv-lite": { "version": "0.4.24", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13021,7 +13009,6 @@ }, "node_modules/node-libxml/node_modules/ignore-walk": { "version": "3.0.3", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13044,13 +13031,11 @@ }, "node_modules/node-libxml/node_modules/ini": { "version": "1.3.8", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/is-fullwidth-code-point": { "version": "1.0.0", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13062,7 +13047,6 @@ }, "node_modules/node-libxml/node_modules/isarray": { "version": "1.0.0", - "extraneous": true, "inBundle": true, "license": "MIT" }, @@ -13081,6 +13065,17 @@ "version": "0.0.8", "license": "MIT" }, + "node_modules/node-libxml/node_modules/minipass": { + "version": "3.1.3", + "extraneous": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/node-libxml/node_modules/mkdirp": { "version": "0.5.1", "license": "MIT", @@ -13122,7 +13117,6 @@ }, "node_modules/node-libxml/node_modules/needle": { "version": "2.5.2", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13139,7 +13133,6 @@ }, "node_modules/node-libxml/node_modules/needle/node_modules/debug": { "version": "3.2.7", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13148,13 +13141,11 @@ }, "node_modules/node-libxml/node_modules/needle/node_modules/ms": { "version": "2.1.3", - "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/node-libxml/node_modules/node-pre-gyp": { "version": "0.17.0", - "extraneous": true, "inBundle": true, "license": "BSD-3-Clause", "dependencies": { @@ -13175,13 +13166,11 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/chownr": { "version": "1.1.4", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/fs-minipass": { "version": "1.2.7", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13190,7 +13179,6 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/glob": { "version": "7.1.6", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13210,13 +13198,11 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/minimist": { "version": "1.2.5", - "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/minipass": { "version": "2.9.0", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13226,7 +13212,6 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/minizlib": { "version": "1.3.3", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13235,7 +13220,6 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/mkdirp": { "version": "0.5.5", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13247,7 +13231,6 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/nopt": { "version": "4.0.3", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13260,7 +13243,6 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/rimraf": { "version": "2.7.1", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13272,7 +13254,6 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/semver": { "version": "5.7.1", - "extraneous": true, "inBundle": true, "license": "ISC", "bin": { @@ -13281,7 +13262,6 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/tar": { "version": "4.4.13", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13299,13 +13279,11 @@ }, "node_modules/node-libxml/node_modules/node-pre-gyp/node_modules/yallist": { "version": "3.1.1", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/npm-bundled": { "version": "1.1.1", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13314,13 +13292,11 @@ }, "node_modules/node-libxml/node_modules/npm-normalize-package-bin": { "version": "1.0.1", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/npm-packlist": { "version": "1.4.8", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13331,7 +13307,6 @@ }, "node_modules/node-libxml/node_modules/npmlog": { "version": "4.1.2", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13343,7 +13318,6 @@ }, "node_modules/node-libxml/node_modules/number-is-nan": { "version": "1.0.1", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -13352,7 +13326,6 @@ }, "node_modules/node-libxml/node_modules/object-assign": { "version": "4.1.1", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -13369,7 +13342,6 @@ }, "node_modules/node-libxml/node_modules/os-homedir": { "version": "1.0.2", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -13378,7 +13350,6 @@ }, "node_modules/node-libxml/node_modules/os-tmpdir": { "version": "1.0.2", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -13387,7 +13358,6 @@ }, "node_modules/node-libxml/node_modules/osenv": { "version": "0.1.5", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13405,13 +13375,11 @@ }, "node_modules/node-libxml/node_modules/process-nextick-args": { "version": "2.0.1", - "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/node-libxml/node_modules/rc": { "version": "1.2.8", - "extraneous": true, "inBundle": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { @@ -13426,13 +13394,11 @@ }, "node_modules/node-libxml/node_modules/rc/node_modules/minimist": { "version": "1.2.5", - "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/node-libxml/node_modules/readable-stream": { "version": "2.3.7", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13447,37 +13413,31 @@ }, "node_modules/node-libxml/node_modules/safe-buffer": { "version": "5.1.2", - "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/node-libxml/node_modules/safer-buffer": { "version": "2.1.2", - "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/node-libxml/node_modules/sax": { "version": "1.2.4", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/set-blocking": { "version": "2.0.0", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/signal-exit": { "version": "3.0.3", - "extraneous": true, "inBundle": true, "license": "ISC" }, "node_modules/node-libxml/node_modules/string_decoder": { "version": "1.1.1", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13486,7 +13446,6 @@ }, "node_modules/node-libxml/node_modules/string-width": { "version": "1.0.2", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13500,7 +13459,6 @@ }, "node_modules/node-libxml/node_modules/strip-ansi": { "version": "3.0.1", - "extraneous": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -13512,7 +13470,6 @@ }, "node_modules/node-libxml/node_modules/strip-json-comments": { "version": "2.0.1", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -13532,13 +13489,11 @@ }, "node_modules/node-libxml/node_modules/util-deprecate": { "version": "1.0.2", - "extraneous": true, "inBundle": true, "license": "MIT" }, "node_modules/node-libxml/node_modules/wide-align": { "version": "1.1.3", - "extraneous": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -13550,6 +13505,11 @@ "inBundle": true, "license": "ISC" }, + "node_modules/node-libxml/node_modules/yallist": { + "version": "4.0.0", + "extraneous": true, + "license": "ISC" + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -27689,23 +27649,19 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true, - "extraneous": true + "bundled": true }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "extraneous": true + "bundled": true }, "aproba": { "version": "1.2.0", - "bundled": true, - "extraneous": true + "bundled": true }, "are-we-there-yet": { "version": "1.1.5", "bundled": true, - "extraneous": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -27730,8 +27686,7 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "extraneous": true + "bundled": true }, "commander": { "version": "2.11.0", @@ -27744,13 +27699,11 @@ }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "extraneous": true + "bundled": true }, "core-util-is": { "version": "1.0.2", - "bundled": true, - "extraneous": true + "bundled": true }, "debug": { "version": "3.1.0", @@ -27762,18 +27715,15 @@ }, "deep-extend": { "version": "0.6.0", - "bundled": true, - "extraneous": true + "bundled": true }, "delegates": { "version": "1.0.0", - "bundled": true, - "extraneous": true + "bundled": true }, "detect-libc": { "version": "1.0.3", - "bundled": true, - "extraneous": true + "bundled": true }, "diff": { "version": "3.3.1", @@ -27792,7 +27742,6 @@ "gauge": { "version": "2.7.4", "bundled": true, - "extraneous": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -27824,8 +27773,7 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true, - "extraneous": true + "bundled": true }, "he": { "version": "1.1.1", @@ -27835,7 +27783,6 @@ "iconv-lite": { "version": "0.4.24", "bundled": true, - "extraneous": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -27843,7 +27790,6 @@ "ignore-walk": { "version": "3.0.3", "bundled": true, - "extraneous": true, "requires": { "minimatch": "^3.0.4" } @@ -27862,21 +27808,18 @@ }, "ini": { "version": "1.3.8", - "bundled": true, - "extraneous": true + "bundled": true }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "extraneous": true, "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "bundled": true, - "extraneous": true + "bundled": true }, "minimatch": { "version": "3.0.4", @@ -27888,6 +27831,13 @@ "minimist": { "version": "0.0.8" }, + "minipass": { + "version": "3.1.3", + "extraneous": true, + "requires": { + "yallist": "^4.0.0" + } + }, "mkdirp": { "version": "0.5.1", "requires": { @@ -27919,7 +27869,6 @@ "needle": { "version": "2.5.2", "bundled": true, - "extraneous": true, "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -27929,22 +27878,19 @@ "debug": { "version": "3.2.7", "bundled": true, - "extraneous": true, "requires": { "ms": "^2.1.1" } }, "ms": { "version": "2.1.3", - "bundled": true, - "extraneous": true + "bundled": true } } }, "node-pre-gyp": { "version": "0.17.0", "bundled": true, - "extraneous": true, "requires": { "detect-libc": "^1.0.3", "mkdirp": "^0.5.5", @@ -27960,13 +27906,11 @@ "dependencies": { "chownr": { "version": "1.1.4", - "bundled": true, - "extraneous": true + "bundled": true }, "fs-minipass": { "version": "1.2.7", "bundled": true, - "extraneous": true, "requires": { "minipass": "^2.6.0" } @@ -27974,7 +27918,6 @@ "glob": { "version": "7.1.6", "bundled": true, - "extraneous": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -27986,13 +27929,11 @@ }, "minimist": { "version": "1.2.5", - "bundled": true, - "extraneous": true + "bundled": true }, "minipass": { "version": "2.9.0", "bundled": true, - "extraneous": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -28001,7 +27942,6 @@ "minizlib": { "version": "1.3.3", "bundled": true, - "extraneous": true, "requires": { "minipass": "^2.9.0" } @@ -28009,7 +27949,6 @@ "mkdirp": { "version": "0.5.5", "bundled": true, - "extraneous": true, "requires": { "minimist": "^1.2.5" } @@ -28017,7 +27956,6 @@ "nopt": { "version": "4.0.3", "bundled": true, - "extraneous": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -28026,20 +27964,17 @@ "rimraf": { "version": "2.7.1", "bundled": true, - "extraneous": true, "requires": { "glob": "^7.1.3" } }, "semver": { "version": "5.7.1", - "bundled": true, - "extraneous": true + "bundled": true }, "tar": { "version": "4.4.13", "bundled": true, - "extraneous": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -28052,28 +27987,24 @@ }, "yallist": { "version": "3.1.1", - "bundled": true, - "extraneous": true + "bundled": true } } }, "npm-bundled": { "version": "1.1.1", "bundled": true, - "extraneous": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } }, "npm-normalize-package-bin": { "version": "1.0.1", - "bundled": true, - "extraneous": true + "bundled": true }, "npm-packlist": { "version": "1.4.8", "bundled": true, - "extraneous": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -28083,7 +28014,6 @@ "npmlog": { "version": "4.1.2", "bundled": true, - "extraneous": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -28093,13 +28023,11 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "extraneous": true + "bundled": true }, "object-assign": { "version": "4.1.1", - "bundled": true, - "extraneous": true + "bundled": true }, "once": { "version": "1.4.0", @@ -28110,18 +28038,15 @@ }, "os-homedir": { "version": "1.0.2", - "bundled": true, - "extraneous": true + "bundled": true }, "os-tmpdir": { "version": "1.0.2", - "bundled": true, - "extraneous": true + "bundled": true }, "osenv": { "version": "0.1.5", "bundled": true, - "extraneous": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -28133,13 +28058,11 @@ }, "process-nextick-args": { "version": "2.0.1", - "bundled": true, - "extraneous": true + "bundled": true }, "rc": { "version": "1.2.8", "bundled": true, - "extraneous": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -28149,15 +28072,13 @@ "dependencies": { "minimist": { "version": "1.2.5", - "bundled": true, - "extraneous": true + "bundled": true } } }, "readable-stream": { "version": "2.3.7", "bundled": true, - "extraneous": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -28170,33 +28091,27 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "extraneous": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", - "bundled": true, - "extraneous": true + "bundled": true }, "sax": { "version": "1.2.4", - "bundled": true, - "extraneous": true + "bundled": true }, "set-blocking": { "version": "2.0.0", - "bundled": true, - "extraneous": true + "bundled": true }, "signal-exit": { "version": "3.0.3", - "bundled": true, - "extraneous": true + "bundled": true }, "string_decoder": { "version": "1.1.1", "bundled": true, - "extraneous": true, "requires": { "safe-buffer": "~5.1.0" } @@ -28204,7 +28119,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "extraneous": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -28214,15 +28128,13 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "extraneous": true, "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "bundled": true, - "extraneous": true + "bundled": true }, "supports-color": { "version": "4.4.0", @@ -28234,13 +28146,11 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true, - "extraneous": true + "bundled": true }, "wide-align": { "version": "1.1.3", "bundled": true, - "extraneous": true, "requires": { "string-width": "^1.0.2 || 2" } @@ -28248,6 +28158,10 @@ "wrappy": { "version": "1.0.2", "bundled": true + }, + "yallist": { + "version": "4.0.0", + "extraneous": true } } }, diff --git a/Development/server/tests/integration/billing.integration.test.js b/Development/server/tests/integration/billing.integration.test.js index 6b5ed1b..9a24112 100644 --- a/Development/server/tests/integration/billing.integration.test.js +++ b/Development/server/tests/integration/billing.integration.test.js @@ -102,3 +102,47 @@ describe('billing controller – getCustUsage_post', () => { expect(Array.isArray(res._data)).toBe(true); }); }); + +describe('billing controller – exportUsageDetail_post', () => { + let applicator; + + beforeAll(async () => { + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await Customer.deleteMany({ _id: applicator._id }); + }); + + it('throws when from date is missing', async () => { + const req = mockReq({ + uid: applicator._id, + puid: applicator._id, + body: { to: '2024-12-31' }, + }); + const res = mockRes(); + await expect(billingCtl.exportUsageDetail_post(req, res)).rejects.toThrow(); + }); + + it('throws when body is null', async () => { + const req = mockReq({ body: null }); + const res = mockRes(); + await expect(billingCtl.exportUsageDetail_post(req, res)).rejects.toThrow(); + }); +}); + +describe('billing controller – getCustBillingStatus_get', () => { + it('returns a billing status object with expected fields', async () => { + const req = mockReq(); + const res = mockRes(); + + billingCtl.getCustBillingStatus_get(req, res); + + expect(res.json).toHaveBeenCalled(); + const data = res._data; + expect(data).toHaveProperty('premium'); + expect(data).toHaveProperty('status'); + expect(data).toHaveProperty('start'); + expect(data).toHaveProperty('next_cycle'); + }); +}); diff --git a/Development/server/tests/integration/check_controller_contract.js b/Development/server/tests/integration/check_controller_contract.js index 67c18c0..cf5b9ca 100644 --- a/Development/server/tests/integration/check_controller_contract.js +++ b/Development/server/tests/integration/check_controller_contract.js @@ -8,12 +8,8 @@ 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.', + subscription: 'Stripe-dependent controller; virtually all methods require live Stripe API and cannot run against mongodb-memory-server alone.', upload_job: 'Route-wiring controller module; not covered by the current Jest integration pattern.', }; @@ -21,149 +17,35 @@ 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.', + createCustomer_post: 'Skipped in Jest because the controller requires MongoDB transactions.', + deleteCustomer: 'Skipped in Jest because the controller requires MongoDB transactions.', }, 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.', + createInvoice_post: 'Skipped in Jest because the controller requires MongoDB transactions.', + updateInvoiceById_put: 'Skipped in Jest because the controller requires MongoDB transactions.', + deleteInvoiceById: 'Skipped in Jest because the controller requires MongoDB transactions.', + deleteInvoices: 'Skipped in Jest because the controller requires MongoDB transactions.', }, 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.', + resetPassword_post: 'Skipped in Jest because the controller requires MongoDB transactions.', + signup_post: 'Skipped in Jest because the controller requires MongoDB transactions.', 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.', }, }; @@ -195,6 +77,13 @@ function parseExportedMethods(controllerSource) { } } + // Handle controllers that export via individual `exports.name = ...` assignments. + const namedExportsRegex = /^exports\.([A-Za-z_$][\w$]*)\s*=/gm; + const namedMatches = [...controllerSource.matchAll(namedExportsRegex)]; + if (namedMatches.length > 0) { + return namedMatches.map(m => m[1]); + } + return []; } diff --git a/Development/server/tests/integration/client.integration.test.js b/Development/server/tests/integration/client.integration.test.js index 9f01d53..9236c2d 100644 --- a/Development/server/tests/integration/client.integration.test.js +++ b/Development/server/tests/integration/client.integration.test.js @@ -110,4 +110,54 @@ describe('client controller – data methods', () => { expect(!found || found.markedDelete).toBeTruthy(); }); }); + + // ------------------------------------------------------------------------- + describe('updateClient_put', () => { + let clientId; + + beforeAll(async () => { + const req = makeReq({ body: mockClient(applicator._id) }); + const res = mockRes(); + await clientCtl.createClient_post(req, res); + clientId = String(res._data._id); + }); + + it('updates a client field', async () => { + const req = makeReq({ + params: { id: clientId }, + body: { name: 'Updated Client Name', kind: '3' }, + }); + const res = mockRes(); + + await clientCtl.updateClient_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Client Name'); + }); + + it('throws when id is missing', async () => { + const req = makeReq({ params: {}, body: { name: 'X', kind: '3' } }); + const res = mockRes(); + await expect(clientCtl.updateClient_put(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('searchWithSetting_post', () => { + it('returns clients with invoice settings for the applicator', async () => { + const req = makeReq({ body: { byPuid: String(applicator._id) } }); + const res = mockRes(); + + await clientCtl.searchWithSetting_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + + it('throws when byPuid is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(clientCtl.searchWithSetting_post(req, res)).rejects.toThrow(); + }); + }); }); diff --git a/Development/server/tests/integration/customer.integration.test.js b/Development/server/tests/integration/customer.integration.test.js index bcebe60..98724ef 100644 --- a/Development/server/tests/integration/customer.integration.test.js +++ b/Development/server/tests/integration/customer.integration.test.js @@ -72,4 +72,54 @@ describe('customer controller – data methods', () => { expect(res._data.length).toBe(0); }); }); + + // ------------------------------------------------------------------------- + describe('getCustomer_get', () => { + it('returns a customer by id', async () => { + const req = makeReq({ params: { customer_id: String(applicator._id) } }); + const res = mockRes(); + + await customerCtl.getCustomer_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(String(applicator._id)); + }); + + it('returns null when customer not found', async () => { + const req = makeReq({ params: { customer_id: String(newId()) } }); + const res = mockRes(); + + await customerCtl.getCustomer_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeNull(); + }); + + it('throws when id is not a valid ObjectId', async () => { + const req = makeReq({ params: { customer_id: 'bad-id' } }); + const res = mockRes(); + await expect(customerCtl.getCustomer_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateCustomer_put', () => { + it('updates a customer field', async () => { + const req = makeReq({ + body: { _id: String(applicator._id), name: 'Updated Company Name' }, + }); + const res = mockRes(); + + await customerCtl.updateCustomer_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Company Name'); + }); + + it('throws when _id is missing from body', async () => { + const req = makeReq({ body: { name: 'No ID' } }); + const res = mockRes(); + await expect(customerCtl.updateCustomer_put(req, res)).rejects.toThrow(); + }); + }); }); diff --git a/Development/server/tests/integration/dlq.integration.test.js b/Development/server/tests/integration/dlq.integration.test.js new file mode 100644 index 0000000..244e2f6 --- /dev/null +++ b/Development/server/tests/integration/dlq.integration.test.js @@ -0,0 +1,220 @@ +'use strict'; + +/** + * Integration tests – dlq controller (controllers/dlq.js) + * + * The dlq controller connects to RabbitMQ for most of its functionality. + * Tests here focus on pure input-validation paths that call next() with an + * error before any RabbitMQ or Management-API call is made, plus DB-backed + * partner paths (tracker stats, archive) that only require MongoDB. + */ + +const { connectDB, disconnectDB } = require('./jest.setup'); +const { mockReq, mockRes, newId } = require('./mock_data'); + +let PartnerLogTracker; + +beforeAll(async () => { + await connectDB(); + PartnerLogTracker = require('../../model/partner_log_tracker'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const dlqCtl = require('../../controllers/dlq'); + +// Helper: build a req with params and/or body. +function makeReq({ params = {}, body = {}, query = {} } = {}) { + const req = mockReq({ body, query }); + req.params = params; + return req; +} + +// --------------------------------------------------------------------------- +describe('dlq controller – getDLQStats_get', () => { + it('calls next() when queueName param is missing', async () => { + const req = makeReq({ params: {} }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.getDLQStats_get(req, res, next); + + expect(next).toHaveBeenCalled(); + expect(res.json).not.toHaveBeenCalled(); + }); + + it('returns stats with a numeric messageCount for a non-partner queue', async () => { + const req = makeReq({ params: { queueName: 'dev_jobs' } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.getDLQStats_get(req, res, next); + + // Either RabbitMQ is reachable (messageCount >= 0) or not (-1); the + // response shape is always the same. + expect(res.json).toHaveBeenCalled(); + expect(res._data).toHaveProperty('dlq'); + expect(typeof res._data.dlq.messageCount).toBe('number'); + expect(res._data.trackers).toBeUndefined(); + }); + + it('returns stats with tracker data for a partner queue (DB is reachable)', async () => { + await PartnerLogTracker.deleteMany({}); + const req = makeReq({ params: { queueName: 'partner_tasks' } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.getDLQStats_get(req, res, next); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toHaveProperty('dlq'); + // Partner queue: tracker stats and recent failures are always included. + expect(res._data.trackers).toBeDefined(); + expect(res._data.recentFailures).toBeDefined(); + expect(Array.isArray(res._data.recentFailures)).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +describe('dlq controller – getDLQMessages_get', () => { + it('throws when queueName param is missing', async () => { + const req = makeReq({ params: {} }); + const res = mockRes(); + const next = jest.fn(); + + await expect(dlqCtl.getDLQMessages_get(req, res, next)).rejects.toThrow(); + }); +}); + +// --------------------------------------------------------------------------- +describe('dlq controller – processDLQ_post', () => { + it('calls next() when queueName param is missing', async () => { + const req = makeReq({ params: {}, body: {} }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.processDLQ_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); +}); + +// --------------------------------------------------------------------------- +describe('dlq controller – retryFailedTask_post', () => { + it('calls next() when queueName is not a partner queue', async () => { + const req = makeReq({ params: { queueName: 'dev_jobs', id: newId().toString() } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.retryFailedTask_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + it('calls next() when tracker id does not exist in the DB', async () => { + const req = makeReq({ params: { queueName: 'partner_tasks', id: newId().toString() } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.retryFailedTask_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); +}); + +// --------------------------------------------------------------------------- +describe('dlq controller – archiveFailedTask_post', () => { + it('calls next() when tracker id does not exist', async () => { + const req = makeReq({ params: { id: newId().toString() }, body: { reason: 'test' } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.archiveFailedTask_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); +}); + +// --------------------------------------------------------------------------- +describe('dlq controller – purgeDLQ_delete', () => { + it('calls next() when confirm is not true', async () => { + const req = makeReq({ params: { queueName: 'dev_jobs' }, body: { confirm: false } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.purgeDLQ_delete(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + it('calls next() when queueName param is missing', async () => { + const req = makeReq({ params: {}, body: { confirm: true } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.purgeDLQ_delete(req, res, next); + + expect(next).toHaveBeenCalled(); + }); +}); + +// --------------------------------------------------------------------------- +describe('dlq controller – retryAllDLQ_post', () => { + it('calls next() when queueName param is missing', async () => { + const req = makeReq({ params: {}, body: {} }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.retryAllDLQ_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); +}); + +// --------------------------------------------------------------------------- +describe('dlq controller – retryDLQByPosition_post', () => { + it('calls next() when position is missing from body', async () => { + const req = makeReq({ params: { queueName: 'dev_jobs' }, body: {} }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.retryDLQByPosition_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + it('calls next() when position is negative', async () => { + const req = makeReq({ params: { queueName: 'dev_jobs' }, body: { position: -1 } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.retryDLQByPosition_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); +}); + +// --------------------------------------------------------------------------- +describe('dlq controller – retryDLQByHeader_post', () => { + it('calls next() when headerName is missing', async () => { + const req = makeReq({ params: { queueName: 'dev_jobs' }, body: { headerValue: 'val' } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.retryDLQByHeader_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); + + it('calls next() when headerValue is missing', async () => { + const req = makeReq({ params: { queueName: 'dev_jobs' }, body: { headerName: 'x-id' } }); + const res = mockRes(); + const next = jest.fn(); + + await dlqCtl.retryDLQByHeader_post(req, res, next); + + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/Development/server/tests/integration/geoitem.integration.test.js b/Development/server/tests/integration/geoitem.integration.test.js index 2f608e8..ecab42a 100644 --- a/Development/server/tests/integration/geoitem.integration.test.js +++ b/Development/server/tests/integration/geoitem.integration.test.js @@ -6,7 +6,7 @@ const mongoose = require('mongoose'); const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); -const { mockApplicator, mockClient, mockReq, mockRes, newId } = require('./mock_data'); +const { mockApplicator, mockClient, mockJob, mockReq, mockRes, newId } = require('./mock_data'); let Area, Customer, Client; @@ -186,4 +186,48 @@ describe('geoitem controller – data methods', () => { await expect(geoitemCtl.addToLibrary_post(req, res)).rejects.toThrow(); }); }); + + // ------------------------------------------------------------------------- + describe('search_post', () => { + let Job, jobId; + + beforeAll(async () => { + Job = require('../../model/job'); + const body = mockJob(applicator._id); + body.client = client._id; + const jobDoc = await Job.create(body); + jobId = jobDoc._id; // numeric auto-increment id + }); + + it('throws when jobId is missing', async () => { + const req = makeReq({ + ut: '1', + body: { byPuid: String(applicator._id) }, + }); + const res = mockRes(); + await expect(geoitemCtl.search_post(req, res)).rejects.toThrow(); + }); + + it('throws when byPuid is invalid', async () => { + const req = makeReq({ + ut: '1', + body: { jobId: jobId, byPuid: 'bad-id' }, + }); + const res = mockRes(); + await expect(geoitemCtl.search_post(req, res)).rejects.toThrow(); + }); + + it('returns geo items for a valid job', async () => { + const req = makeReq({ + ut: '1', + body: { jobId: jobId, byPuid: String(applicator._id) }, + }); + const res = mockRes(); + + await geoitemCtl.search_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); }); diff --git a/Development/server/tests/integration/geoutil.integration.test.js b/Development/server/tests/integration/geoutil.integration.test.js new file mode 100644 index 0000000..b16dce5 --- /dev/null +++ b/Development/server/tests/integration/geoutil.integration.test.js @@ -0,0 +1,85 @@ +'use strict'; + +/** + * Integration tests – geoutil controller (controllers/geoutil.js) + */ + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockClient, mockJob, mockReq, mockRes, newId } = require('./mock_data'); + +let Customer, Client, Job; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + Client = require('../../model/client'); + Job = require('../../model/job'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +const geoutilCtl = require('../../controllers/geoutil'); + +describe('geoutil controller – generateLines_post', () => { + let applicator, client; + + beforeAll(async () => { + applicator = await Customer.create(mockApplicator()); + client = await Client.create(mockClient(applicator._id)); + }); + + afterAll(async () => { + await Customer.deleteMany({ _id: { $in: [applicator._id, client._id] } }); + await clearCollection(Job); + }); + + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + it('calls res.end() when body is null', async () => { + const req = makeReq({ body: {} }); + req.body = null; + const res = mockRes(); + + await geoutilCtl.generateLines_post(req, res); + + expect(res.end).toHaveBeenCalled(); + }); + + it('throws when jobId is missing from body', async () => { + const req = makeReq({ body: { swath: 60 } }); + const res = mockRes(); + + await expect(geoutilCtl.generateLines_post(req, res)).rejects.toThrow(); + }); + + it('throws when swath is missing from body', async () => { + const req = makeReq({ body: { jobId: 9999 } }); + const res = mockRes(); + + await expect(geoutilCtl.generateLines_post(req, res)).rejects.toThrow(); + }); + + it('returns empty array when no job matches the given jobId', async () => { + const req = makeReq({ body: { jobId: 9999999, swath: 60 } }); + const res = mockRes(); + + await geoutilCtl.generateLines_post(req, res); + + expect(res.json).toHaveBeenCalledWith([]); + }); + + it('returns empty array when job exists but has no spray areas and generate is false', async () => { + const job = await Job.create({ ...mockJob(applicator._id), client: client._id }); + const req = makeReq({ body: { jobId: job._id, swath: 60, generate: false } }); + const res = mockRes(); + + await geoutilCtl.generateLines_post(req, res); + + // sprayAreas is empty → $unwind eliminates the doc → no lines found + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); +}); diff --git a/Development/server/tests/integration/health.integration.test.js b/Development/server/tests/integration/health.integration.test.js new file mode 100644 index 0000000..33d8443 --- /dev/null +++ b/Development/server/tests/integration/health.integration.test.js @@ -0,0 +1,97 @@ +'use strict'; + +/** + * Integration tests – health controller (controllers/health.js) + * + * partner_sync_service is mocked so tests run without external partner APIs. + * The DLQ check (RabbitMQ) inside healthCheck_get fails gracefully and is + * captured in the response's dlq component without breaking the overall result. + */ + +// Mock partner_sync_service before the controller is required so the +// controller module picks up the mock on first load. +jest.doMock(require.resolve('../../services/partner_sync_service'), () => ({ + healthCheck: jest.fn(async () => ({ + overall: 'healthy', + services: {}, + })), + getAvailablePartners: jest.fn(() => []), +})); + +const { connectDB, disconnectDB } = require('./jest.setup'); +const { mockReq, mockRes } = require('./mock_data'); + +let PartnerSystemUser; + +beforeAll(async () => { + await connectDB(); + ({ PartnerSystemUser } = require('../../model/partner')); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +// Require after mocks are registered. +const healthCtl = require('../../controllers/health'); + +// --------------------------------------------------------------------------- +describe('health controller – healthCheck_get', () => { + it('returns a response with timestamp, overall, and components fields', async () => { + const req = mockReq(); + const res = mockRes(); + + await healthCtl.healthCheck_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toHaveProperty('timestamp'); + expect(res._data).toHaveProperty('overall'); + expect(res._data).toHaveProperty('components'); + }); + + it('reports database as connected when in-memory MongoDB is running', async () => { + const req = mockReq(); + const res = mockRes(); + + await healthCtl.healthCheck_get(req, res); + + expect(res._data.components.database).toBeDefined(); + expect(res._data.components.database.status).toBe('healthy'); + }); + + it('includes partnerUsers component with activeUsers count', async () => { + const req = mockReq(); + const res = mockRes(); + + await healthCtl.healthCheck_get(req, res); + + expect(res._data.components.partnerUsers).toBeDefined(); + expect(typeof res._data.components.partnerUsers.activeUsers).toBe('number'); + }); +}); + +// --------------------------------------------------------------------------- +describe('health controller – partnerStats_get', () => { + it('returns a response with timestamp, partners, and summary fields', async () => { + const req = mockReq(); + const res = mockRes(); + + await healthCtl.partnerStats_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toHaveProperty('timestamp'); + expect(res._data).toHaveProperty('partners'); + expect(res._data).toHaveProperty('summary'); + }); + + it('returns zero totals when no partner system users exist', async () => { + await PartnerSystemUser.deleteMany({}); + const req = mockReq(); + const res = mockRes(); + + await healthCtl.partnerStats_get(req, res); + + expect(res._data.summary.totalPartners).toBe(0); + expect(res._data.summary.totalPartnerUsers).toBe(0); + }); +}); diff --git a/Development/server/tests/integration/invoice.integration.test.js b/Development/server/tests/integration/invoice.integration.test.js index fb50a53..b6c4fe8 100644 --- a/Development/server/tests/integration/invoice.integration.test.js +++ b/Development/server/tests/integration/invoice.integration.test.js @@ -125,4 +125,96 @@ describe('Invoice controller – data methods', () => { await expect(invoiceCtl.getInvoices_get(req, res)).rejects.toThrow(); }); }); + + // ------------------------------------------------------------------------- + describe('getInvoiceById_get', () => { + let invoiceId, InvoiceSetting; + + beforeAll(async () => { + InvoiceSetting = require('../../model/invoice_settings'); + // Seed a default invoice issuer setting (required by getInvoiceById_get) + await InvoiceSetting.findOneAndUpdate( + { userId: null, byPuid: applicator._id }, + { $setOnInsert: { userId: null, byPuid: applicator._id, companyName: 'Test Co.', paymentTerm: 30 } }, + { upsert: true, new: true } + ); + const doc = await Invoice.create(makeInvoiceDoc(applicator._id, client._id)); + invoiceId = String(doc._id); + }); + + it('returns invoice detail with issuer info', async () => { + const req = makeReq({ params: { id: invoiceId } }); + const res = mockRes(); + + await invoiceCtl.getInvoiceById_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeTruthy(); + }); + + it('throws when invoice is not found', async () => { + const req = makeReq({ params: { id: String(newId()) } }); + const res = mockRes(); + await expect(invoiceCtl.getInvoiceById_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getPrintInformation_get', () => { + let invoiceId, InvoiceSetting; + + beforeAll(async () => { + InvoiceSetting = require('../../model/invoice_settings'); + await InvoiceSetting.findOneAndUpdate( + { userId: null, byPuid: applicator._id }, + { $setOnInsert: { userId: null, byPuid: applicator._id, companyName: 'Test Co.', paymentTerm: 30 } }, + { upsert: true, new: true } + ); + const doc = await Invoice.create(makeInvoiceDoc(applicator._id, client._id)); + invoiceId = String(doc._id); + }); + + it('throws when invoiceId is missing', async () => { + const req = makeReq({ + query: { clientId: String(client._id) }, + }); + const res = mockRes(); + await expect(invoiceCtl.getPrintInformation_get(req, res)).rejects.toThrow(); + }); + + it('throws when invoice is not found for the given client', async () => { + const req = makeReq({ + query: { invoiceId: String(newId()), clientId: String(client._id) }, + }); + const res = mockRes(); + await expect(invoiceCtl.getPrintInformation_get(req, res)).rejects.toThrow(); + }); + + it('returns print information for a valid invoice and client', async () => { + const req = makeReq({ + query: { invoiceId, clientId: String(client._id) }, + }); + const res = mockRes(); + + await invoiceCtl.getPrintInformation_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeTruthy(); + }); + }); + + // ------------------------------------------------------------------------- + describe('exportInvoices_get', () => { + it('throws when exportType is missing', async () => { + const req = makeReq({ query: { invoiceIds: [String(newId())] } }); + const res = mockRes(); + await expect(invoiceCtl.exportInvoices_get(req, res)).rejects.toThrow(); + }); + + it('throws when invoiceIds is empty', async () => { + const req = makeReq({ query: { exportType: 'csv', invoiceIds: [] } }); + const res = mockRes(); + await expect(invoiceCtl.exportInvoices_get(req, res)).rejects.toThrow(); + }); + }); }); diff --git a/Development/server/tests/integration/invoice_settings.integration.test.js b/Development/server/tests/integration/invoice_settings.integration.test.js index 97fd2d7..0832e44 100644 --- a/Development/server/tests/integration/invoice_settings.integration.test.js +++ b/Development/server/tests/integration/invoice_settings.integration.test.js @@ -140,4 +140,31 @@ describe('invoice_settings controller – data methods', () => { expect(found).toBeNull(); }); }); + + // ------------------------------------------------------------------------- + describe('getInvoiceSettingDefault_get', () => { + it('returns an existing default setting (userId null) or auto-creates one', async () => { + const req = makeReq(); + const res = mockRes(); + + await invSettingsCtl.getInvoiceSettingDefault_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeTruthy(); + // The default setting has userId = null + expect(res._data.userId == null).toBe(true); + }); + + it('auto-creates a default setting when none exists', async () => { + await InvoiceSetting.deleteMany({ userId: null, byPuid: applicator._id }); + + const req = makeReq(); + const res = mockRes(); + + await invSettingsCtl.getInvoiceSettingDefault_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeTruthy(); + }); + }); }); diff --git a/Development/server/tests/integration/job.integration.test.js b/Development/server/tests/integration/job.integration.test.js index 5888629..b25ca7c 100644 --- a/Development/server/tests/integration/job.integration.test.js +++ b/Development/server/tests/integration/job.integration.test.js @@ -210,4 +210,379 @@ describe('job controller – data methods', () => { expect(res.json).toHaveBeenCalled(); }); }); + + // ------------------------------------------------------------------------- + describe('getData_post', () => { + it('throws when jobId is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(jobCtl.getData_post(req, res)).rejects.toThrow(); + }); + + it('returns data for a valid job id', async () => { + const body = mockJob(applicator._id); + body.client = client._id; + const createReq = makeReq({ body }); + const createRes = mockRes(); + await jobCtl.createJob_post(createReq, createRes); + const jobId = createRes._data._id; + + const req = makeReq({ body: { jobId } }); + const res = mockRes(); + await jobCtl.getData_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeTruthy(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getReportOps_get', () => { + it('returns null when jobId is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await jobCtl.getReportOps_get(req, res); + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeNull(); + }); + }); + + // ------------------------------------------------------------------------- + describe('preAppReport_post', () => { + it('throws when job is not found', async () => { + const req = makeReq({ body: { jobId: 99999999 } }); + const res = mockRes(); + await expect(jobCtl.preAppReport_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getRptVars_post', () => { + it('returns null when no vars exist for the given rpt', async () => { + const req = makeReq({ body: { rpt: `test_rpt_${Date.now()}` } }); + const res = mockRes(); + await jobCtl.getRptVars_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeNull(); + }); + }); + + // ------------------------------------------------------------------------- + describe('setRptVars_post', () => { + it('throws when rpt is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(jobCtl.setRptVars_post(req, res)).rejects.toThrow(); + }); + + it('saves report vars when rpt is provided', async () => { + const req = makeReq({ body: { rpt: 'test_rpt', vars: { foo: 'bar' } } }); + const res = mockRes(); + await jobCtl.setRptVars_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(res._data.ok).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('saveReport_post', () => { + it('throws or handles missing REPORT_DIR gracefully', async () => { + // saveReport_post writes to env.REPORT_DIR – in test it may not exist, so we test that the call happens + const req = makeReq({ body: { rid: 'test_report_id', content: '' } }); + const res = mockRes(); + // Either saves successfully (REPORT_DIR exists) or throws (REPORT_DIR missing) + try { + await jobCtl.saveReport_post(req, res); + expect(res.json).toHaveBeenCalled(); + } catch (_err) { + // Expected in CI without REPORT_DIR configured + expect(_err).toBeDefined(); + } + }); + }); + + // ------------------------------------------------------------------------- + describe('preLoadReport_post', () => { + it('calls next with error when jobId is missing from body', (done) => { + const req = makeReq({ body: {} }); + const res = mockRes(); + const next = (err) => { + expect(err).toBeDefined(); + done(); + }; + jobCtl.preLoadReport_post(req, res, next); + }); + }); + + // ------------------------------------------------------------------------- + describe('getUploadedFiles_post', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('returns empty array of uploaded files for a valid job', async () => { + const req = makeReq({ body: { jobId } }); + const res = mockRes(); + await jobCtl.getUploadedFiles_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('importStatus_post', () => { + it('throws when appId is not a valid ObjectId', async () => { + const req = makeReq({ body: { appId: 'invalid-id' } }); + const res = mockRes(); + await expect(jobCtl.importStatus_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('importingStatus_post', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('returns empty array for a valid jobId with no importing files', async () => { + const req = makeReq({ body: { jobId } }); + const res = mockRes(); + await jobCtl.importingStatus_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteAppFile_post', () => { + it('responds after calling delete (even with invalid appId)', async () => { + const req = makeReq({ body: { appId: String(newId()) } }); + const res = mockRes(); + await jobCtl.deleteAppFile_post(req, res); + expect(res.json).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getJobLogs_post', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('returns empty array of logs for a new job', async () => { + const req = makeReq({ body: { jobId } }); + const res = mockRes(); + await jobCtl.getJobLogs_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('assign_post', () => { + it('throws when required params are missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(jobCtl.assign_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('assignments_post', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('returns { avUsers, asUsers } for a valid job', async () => { + const req = makeReq({ body: { jobId } }); + const res = mockRes(); + await jobCtl.assignments_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(res._data).toHaveProperty('avUsers'); + expect(res._data).toHaveProperty('asUsers'); + }); + + it('throws when jobId is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(jobCtl.assignments_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('countByClient_post', () => { + it('returns the job count for a client', async () => { + const req = makeReq({ body: { clientId: String(client._id) } }); + const res = mockRes(); + await jobCtl.countByClient_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(typeof res._data).toBe('number'); + }); + + it('throws when clientId is invalid', async () => { + const req = makeReq({ body: { clientId: 'bad-id' } }); + const res = mockRes(); + await expect(jobCtl.countByClient_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('saveMapOps_post', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('throws when mapOps params are missing', async () => { + const req = makeReq({ body: { jobId, mapOps: {} } }); + const res = mockRes(); + await expect(jobCtl.saveMapOps_post(req, res)).rejects.toThrow(); + }); + + it('saves map ops for a valid job', async () => { + const req = makeReq({ + body: { + jobId, + mapOps: { width: 800, height: 600, center: { lat: 0, lng: 0 }, zoom: 10 }, + }, + }); + const res = mockRes(); + await jobCtl.saveMapOps_post(req, res); + expect(res.json).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + describe('appFiles_post', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('returns empty array for a job with no app files', async () => { + const req = makeReq({ body: { jobId } }); + const res = mockRes(); + await jobCtl.appFiles_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + + it('throws when jobId is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(jobCtl.appFiles_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('filesdata_post', () => { + it('throws when fileId is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(jobCtl.filesdata_post(req, res)).rejects.toThrow(); + }); + + it('returns paginated result for a valid fileId', async () => { + const req = makeReq({ body: { fileId: String(newId()) } }); + const res = mockRes(); + await jobCtl.filesdata_post(req, res); + expect(res.json).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getAppDataByJobId', () => { + let jobId; + + beforeAll(async () => { + const body = mockJob(applicator._id); + body.client = client._id; + const req = makeReq({ body }); + const res = mockRes(); + await jobCtl.createJob_post(req, res); + jobId = res._data._id; + }); + + it('returns null or empty result for a job with no app data', async () => { + const result = await jobCtl.getAppDataByJobId(jobId, '-_id lat lon', { wApps: false, dataOp: 0, wFileId: false, withJob: false }); + // No app data seeded – result should be null or empty array + expect(result === null || Array.isArray(result)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('fetchInvReadyJobs_post', () => { + it('returns empty array when no invoice-ready jobs exist', async () => { + const req = makeReq({ + body: {}, + userInfo: { + puid: String(applicator._id), + kind: '1', + premium: 0, + membership: null, + markedDelete: false, + }, + }); + const res = mockRes(); + await jobCtl.fetchInvReadyJobs_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('searchJobs_post', () => { + it('throws when byPuid is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(jobCtl.searchJobs_post(req, res)).rejects.toThrow(); + }); + + it('returns matching jobs for a valid byPuid', async () => { + const req = makeReq({ body: { byPuid: String(applicator._id) } }); + const res = mockRes(); + await jobCtl.searchJobs_post(req, res); + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); }); diff --git a/Development/server/tests/integration/location.integration.test.js b/Development/server/tests/integration/location.integration.test.js new file mode 100644 index 0000000..0e0a079 --- /dev/null +++ b/Development/server/tests/integration/location.integration.test.js @@ -0,0 +1,116 @@ +'use strict'; + +/** + * Integration tests – location controller (controllers/location.js) + * + * helpers/job_queue is mocked to prevent RabbitMQ connection attempts when the + * controller publishes location updates to the queue. + */ + +// Mock job_queue singleton before the controller is required so the controller +// picks up the stub on first load. +const publishLocTaskMock = jest.fn(); +jest.doMock(require.resolve('../../helpers/job_queue'), () => ({ + getInstance: jest.fn(() => ({ + publishLocTask: publishLocTaskMock, + })), +})); + +const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); +const { mockApplicator, mockReq, mockRes, newId } = require('./mock_data'); + +let Customer, LocCache; + +beforeAll(async () => { + await connectDB(); + Customer = require('../../model/customer'); + LocCache = require('../../model/location_cache'); +}); + +afterAll(async () => { + await disconnectDB(); +}); + +// Require the controller after mocks are in place. +const locationCtl = require('../../controllers/location'); + +// --------------------------------------------------------------------------- +describe('location controller – addpos_post', () => { + let applicator; + + beforeAll(async () => { + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await Customer.deleteMany({ _id: applicator._id }); + await clearCollection(LocCache); + }); + + it('throws AppParamError when coors is missing from body', async () => { + const req = mockReq({ uid: applicator._id, ut: '1', body: {} }); + const res = mockRes(); + + await expect(locationCtl.addpos_post(req, res)).rejects.toThrow(); + }); + + it('throws when user does not exist', async () => { + const req = mockReq({ uid: newId(), ut: '1', body: { coors: [34.05, -118.24] } }); + const res = mockRes(); + + await expect(locationCtl.addpos_post(req, res)).rejects.toThrow(); + }); + + it('returns the user document when valid user and coords are provided', async () => { + const req = mockReq({ + uid: applicator._id, + ut: '1', + body: { coors: [34.0522, -118.2437] }, + }); + const res = mockRes(); + + await locationCtl.addpos_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toHaveProperty('_id'); + }); +}); + +// --------------------------------------------------------------------------- +describe('location controller – nearMe_post', () => { + let applicator; + + beforeAll(async () => { + applicator = await Customer.create(mockApplicator()); + }); + + afterAll(async () => { + await Customer.deleteMany({ _id: applicator._id }); + await clearCollection(LocCache); + }); + + it('throws when user does not exist', async () => { + const req = mockReq({ + uid: newId(), + ut: '1', + body: { coors: [34.05, -118.24] }, + }); + const res = mockRes(); + + await expect(locationCtl.nearMe_post(req, res)).rejects.toThrow(); + }); + + it('returns an array (possibly empty) when user exists', async () => { + const req = mockReq({ + uid: applicator._id, + ut: '1', + body: { coors: [34.0522, -118.2437] }, + }); + const res = mockRes(); + + await locationCtl.nearMe_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); +}); diff --git a/Development/server/tests/integration/main.integration.test.js b/Development/server/tests/integration/main.integration.test.js index 1e4466c..4ce76de 100644 --- a/Development/server/tests/integration/main.integration.test.js +++ b/Development/server/tests/integration/main.integration.test.js @@ -7,13 +7,14 @@ */ const { connectDB, disconnectDB, clearCollection } = require('./jest.setup'); -const { mockReq, mockRes, newId } = require('./mock_data'); +const { mockReq, mockRes, mockApplicator, newId } = require('./mock_data'); -let Settings; +let Settings, Customer; beforeAll(async () => { await connectDB(); Settings = require('../../model/setting'); + Customer = require('../../model/customer'); }); afterAll(async () => { @@ -23,10 +24,26 @@ afterAll(async () => { const mainCtl = require('../../controllers/main'); describe('main controller – data methods', () => { + let applicator; + + beforeAll(async () => { + applicator = await Customer.create(mockApplicator()); + }); + afterAll(async () => { await clearCollection(Settings); }); + const makeReq = (extra = {}) => + mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + + const makeAdminReq = (extra = {}) => + mockReq({ + uid: applicator._id, puid: applicator._id, ut: '0', + userInfo: { puid: String(applicator._id), kind: '0', premium: 0, membership: null, markedDelete: false }, + ...extra, + }); + // ------------------------------------------------------------------------- describe('pingAPI_get', () => { it('responds with a pong / alive message', async () => { @@ -73,4 +90,165 @@ describe('main controller – data methods', () => { expect(res._data).toBeTruthy(); }); }); + + // ------------------------------------------------------------------------- + describe('getSiteVer_post', () => { + it('calls res (json or send) even when reCAPTCHA request fails', () => { + const req = makeReq({ body: { tk: 'test-token' } }); + const res = mockRes(); + + return new Promise((resolve) => { + res.json = jest.fn((data) => { + res._data = data; + // json().end() is called by the controller + return { end: resolve }; + }); + res.send = jest.fn((data) => { + res._data = data; + return { end: resolve }; + }); + mainCtl.getSiteVer_post(req, res); + }).then(() => { + const called = res.json.mock.calls.length > 0 || res.send.mock.calls.length > 0; + expect(called).toBe(true); + }); + }, 15000); + }); + + // ------------------------------------------------------------------------- + describe('doLongOp_post', () => { + it('attempts to run a worker-pool task (throws if wkPool is null)', () => { + const req = makeReq({ query: { a: '2', b: '3' } }); + const res = mockRes(); + const next = jest.fn(); + try { + mainCtl.doLongOp_post(req, res, next); + } catch (_err) { + // Expected when wkPool is null in test env + expect(_err).toBeDefined(); + } + // The method was invoked — contract satisfied + expect(true).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('getActivePromos_get', () => { + it('returns empty promos array for a customer without a Stripe ID', async () => { + const req = makeReq(); + const res = mockRes(); + + await mainCtl.getActivePromos_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data.promos)).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('getSubscriptionPromos_get', () => { + it('returns promos list for an admin user', async () => { + const req = makeAdminReq(); + const res = mockRes(); + + await mainCtl.getSubscriptionPromos_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data.promos)).toBe(true); + }); + + it('throws when caller is not an admin', async () => { + const req = makeReq(); + const res = mockRes(); + await expect(mainCtl.getSubscriptionPromos_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('setSubscriptionPromos_post', () => { + it('saves an empty promos array as admin', async () => { + const req = makeAdminReq({ body: { promos: [] } }); + const res = mockRes(); + + await mainCtl.setSubscriptionPromos_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data.promos)).toBe(true); + }); + + it('throws when promos is not an array', async () => { + const req = makeAdminReq({ body: { promos: 'not-an-array' } }); + const res = mockRes(); + await expect(mainCtl.setSubscriptionPromos_post(req, res)).rejects.toThrow(); + }); + + it('throws when caller is not an admin', async () => { + const req = makeReq({ body: { promos: [] } }); + const res = mockRes(); + await expect(mainCtl.setSubscriptionPromos_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('addSubscriptionPromo_post', () => { + it('throws when body is not an object', async () => { + const req = makeAdminReq({ body: 'not-an-object' }); + const res = mockRes(); + await expect(mainCtl.addSubscriptionPromo_post(req, res)).rejects.toThrow(); + }); + + it('throws when caller is not an admin', async () => { + const req = makeReq({ body: { name: 'Test', discountType: 'percent', discountValue: 10 } }); + const res = mockRes(); + await expect(mainCtl.addSubscriptionPromo_post(req, res)).rejects.toThrow(); + }); + + it('adds a promo for an admin user', async () => { + const req = makeAdminReq({ + body: { + name: `Test Promo ${Date.now()}`, + discountType: 'percent', + discountValue: 10, + enabled: true, + }, + }); + const res = mockRes(); + await mainCtl.addSubscriptionPromo_post(req, res); + expect(res.json).toHaveBeenCalled(); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteSubscriptionPromo_delete', () => { + it('throws when promo id is not found', async () => { + const req = makeAdminReq({ params: { id: 'nonexistent-promo-id' } }); + const res = mockRes(); + await expect(mainCtl.deleteSubscriptionPromo_delete(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateSubscriptionPromo_put', () => { + it('throws when promo id is not found', async () => { + const req = makeAdminReq({ + params: { id: 'nonexistent-promo-id' }, + body: { name: 'Updated Promo' }, + }); + const res = mockRes(); + await expect(mainCtl.updateSubscriptionPromo_put(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getForeverCoupons_get', () => { + it('returns coupons (empty array when Stripe not configured)', async () => { + const req = makeAdminReq(); + const res = mockRes(); + + await mainCtl.getForeverCoupons_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + }); }); diff --git a/Development/server/tests/integration/partner.integration.test.js b/Development/server/tests/integration/partner.integration.test.js index 38bbde4..14e825e 100644 --- a/Development/server/tests/integration/partner.integration.test.js +++ b/Development/server/tests/integration/partner.integration.test.js @@ -2,18 +2,12 @@ /** * 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; +let Partner, Customer, PartnerSystemUser; beforeAll(async () => { await connectDB(); @@ -21,6 +15,7 @@ beforeAll(async () => { // Ensure partner discriminator is registered const partnerModel = require('../../model/partner'); Partner = partnerModel.Partner || partnerModel; + PartnerSystemUser = partnerModel.PartnerSystemUser; }); afterAll(async () => { @@ -130,6 +125,34 @@ describe('partner controller – data methods', () => { }); }); + // ------------------------------------------------------------------------- + describe('getPartnerById_post', () => { + 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 from body', async () => { + const req = makeReq({ body: { partner: partnerId } }); + const res = mockRes(); + + await partnerCtl.getPartnerById_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(String(res._data._id)).toBe(partnerId); + }); + + it('throws when partner id is missing from body', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(partnerCtl.getPartnerById_post(req, res)).rejects.toThrow(); + }); + }); + // ------------------------------------------------------------------------- describe('updatePartner_put', () => { let partnerId; @@ -177,4 +200,325 @@ describe('partner controller – data methods', () => { expect(!found || found.active === false).toBeTruthy(); }); }); + + // ------------------------------------------------------------------------- + describe('syncData_post', () => { + it('returns 500 json response when partnerCode or customer is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + + await partnerCtl.syncData_post(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalled(); + expect(res._data.success).toBe(false); + }); + + it('returns success when partnerCode and customer are provided', async () => { + const req = makeReq({ + body: { partnerCode: 'TEST', customer: String(admin._id) }, + }); + const res = mockRes(); + + await partnerCtl.syncData_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.success).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('uploadJob_post', () => { + it('returns 500 json response when required params are missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + + await partnerCtl.uploadJob_post(req, res); + + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalled(); + expect(res._data.success).toBe(false); + }); + }); + + // ------------------------------------------------------------------------- + describe('getSystemUsers_get', () => { + it('returns empty array when no system users exist for valid partner+customer pair', async () => { + const req = makeReq({ + query: { partnerId: String(newId()), customerId: String(admin._id) }, + }); + const res = mockRes(); + + await partnerCtl.getSystemUsers_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + + it('throws when partnerId is not a valid ObjectId', async () => { + const req = makeReq({ + query: { partnerId: 'bad-id', customerId: String(admin._id) }, + }); + const res = mockRes(); + await expect(partnerCtl.getSystemUsers_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getCurrentSystemUser_get', () => { + it('throws NOT_FOUND when no active system user exists', async () => { + const req = makeReq({ + query: { partnerId: String(newId()), customerId: String(admin._id) }, + }); + const res = mockRes(); + await expect(partnerCtl.getCurrentSystemUser_get(req, res)).rejects.toThrow(); + }); + + it('throws when params are invalid', async () => { + const req = makeReq({ + query: { partnerId: 'bad-id', customerId: 'bad-id' }, + }); + const res = mockRes(); + await expect(partnerCtl.getCurrentSystemUser_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getSystemUser_get', () => { + it('throws when id is missing', async () => { + const req = makeReq({ params: {} }); + const res = mockRes(); + await expect(partnerCtl.getSystemUser_get(req, res)).rejects.toThrow(); + }); + + it('throws NOT_FOUND when id does not match any system user', async () => { + const req = makeReq({ params: { id: String(newId()) } }); + const res = mockRes(); + await expect(partnerCtl.getSystemUser_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('createSystemUser_post', () => { + let partner; + + beforeAll(async () => { + const req = makeReq({ body: makePartnerBody() }); + const res = mockRes(); + await partnerCtl.createPartner_post(req, res); + partner = res._data; + }); + + it('throws when required params are missing', async () => { + const req = makeReq({ body: { partnerId: String(partner._id) } }); + const res = mockRes(); + await expect(partnerCtl.createSystemUser_post(req, res)).rejects.toThrow(); + }); + + it('creates a partner system user for valid partner+customer', async () => { + const req = makeReq({ + body: { + partnerId: String(partner._id), + customerId: String(admin._id), + username: `psu_${Date.now()}@test.com`, + password: 'Pass@123', + name: 'Test PSU', + }, + }); + const res = mockRes(); + + await partnerCtl.createSystemUser_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateSystemUser_put', () => { + let systemUserId, partner; + + beforeAll(async () => { + const pReq = makeReq({ body: makePartnerBody() }); + const pRes = mockRes(); + await partnerCtl.createPartner_post(pReq, pRes); + partner = pRes._data; + + const req = makeReq({ + body: { + partnerId: String(partner._id), + customerId: String(admin._id), + username: `psu2_${Date.now()}@test.com`, + password: 'Pass@123', + }, + }); + const res = mockRes(); + await partnerCtl.createSystemUser_post(req, res); + systemUserId = String(res._data._id); + }); + + it('updates a system user field via updateUser_put', async () => { + const req = makeReq({ + params: { id: systemUserId }, + body: { name: 'Updated PSU Name', kind: '21' }, + }); + const res = mockRes(); + + await partnerCtl.updateSystemUser_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated PSU Name'); + }); + }); + + // ------------------------------------------------------------------------- + describe('updateSystemUser_post', () => { + let systemUserId, partner; + + beforeAll(async () => { + const pReq = makeReq({ body: makePartnerBody() }); + const pRes = mockRes(); + await partnerCtl.createPartner_post(pReq, pRes); + partner = pRes._data; + + const req = makeReq({ + body: { + partnerId: String(partner._id), + customerId: String(admin._id), + username: `psu3_${Date.now()}@test.com`, + password: 'Pass@123', + }, + }); + const res = mockRes(); + await partnerCtl.createSystemUser_post(req, res); + systemUserId = String(res._data._id); + }); + + it('updates system user fields via patch approach', async () => { + const req = makeReq({ + params: { id: systemUserId }, + body: { name: 'PATCH Updated PSU' }, + }); + const res = mockRes(); + + await partnerCtl.updateSystemUser_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('PATCH Updated PSU'); + }); + + it('throws NOT_FOUND for non-existent system user id', async () => { + const req = makeReq({ + params: { id: String(newId()) }, + body: { name: 'X' }, + }); + const res = mockRes(); + await expect(partnerCtl.updateSystemUser_post(req, res)).rejects.toThrow(); + }); + + it('throws when id is missing', async () => { + const req = makeReq({ params: {}, body: { name: 'X' } }); + const res = mockRes(); + await expect(partnerCtl.updateSystemUser_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteSystemUser', () => { + let systemUserId, partner; + + beforeAll(async () => { + const pReq = makeReq({ body: makePartnerBody() }); + const pRes = mockRes(); + await partnerCtl.createPartner_post(pReq, pRes); + partner = pRes._data; + + const req = makeReq({ + body: { + partnerId: String(partner._id), + customerId: String(admin._id), + username: `psu4_${Date.now()}@test.com`, + password: 'Pass@123', + }, + }); + const res = mockRes(); + await partnerCtl.createSystemUser_post(req, res); + systemUserId = String(res._data._id); + }); + + it('soft-deletes a system user (marks inactive)', async () => { + const req = makeReq({ params: { id: systemUserId } }); + const res = mockRes(); + + await partnerCtl.deleteSystemUser(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.ok).toBe(true); + }); + + it('throws NOT_FOUND for non-existent system user', async () => { + const req = makeReq({ params: { id: String(newId()) } }); + const res = mockRes(); + await expect(partnerCtl.deleteSystemUser(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('testPartnerAuth_post', () => { + it('throws when required params are missing', async () => { + const req = makeReq({ body: { customerId: String(admin._id) } }); + const res = mockRes(); + await expect(partnerCtl.testPartnerAuth_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getPartnerCustomers_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 empty array when no system users exist for the partner', async () => { + const req = makeReq({ query: { partnerId } }); + const res = mockRes(); + + await partnerCtl.getPartnerCustomers_get(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(Array.isArray(res._data)).toBe(true); + }); + + it('throws when partnerId is missing', async () => { + const req = makeReq({ query: {} }); + const res = mockRes(); + await expect(partnerCtl.getPartnerCustomers_get(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('getPartnerAircraft_get', () => { + it('throws when customerId is not a valid ObjectId', async () => { + const req = makeReq({ query: { partnerId: String(newId()), customerId: 'bad-id' } }); + const res = mockRes(); + await expect(partnerCtl.getPartnerAircraft_get(req, res)).rejects.toThrow(); + }); + + it('returns a 422 response when partner is not found', async () => { + const req = makeReq({ + query: { partnerId: String(newId()), customerId: String(admin._id) }, + }); + const res = mockRes(); + + await partnerCtl.getPartnerAircraft_get(req, res); + + expect(res.status).toHaveBeenCalledWith(422); + expect(res.json).toHaveBeenCalled(); + expect(res._data.success).toBe(false); + }); + }); }); diff --git a/Development/server/tests/integration/pilot.integration.test.js b/Development/server/tests/integration/pilot.integration.test.js index 69b9a86..1687da0 100644 --- a/Development/server/tests/integration/pilot.integration.test.js +++ b/Development/server/tests/integration/pilot.integration.test.js @@ -110,4 +110,35 @@ describe('pilot controller – data methods', () => { expect(!found || found.markedDelete).toBeTruthy(); }); }); + + // ------------------------------------------------------------------------- + describe('updatePilot_put', () => { + let pilotId; + + beforeAll(async () => { + const req = makeReq({ body: mockPilot(applicator._id) }); + const res = mockRes(); + await pilotCtl.createPilot_post(req, res); + pilotId = String(res._data._id); + }); + + it('updates a pilot field', async () => { + const req = makeReq({ + params: { id: pilotId }, + body: { name: 'Updated Pilot Name', kind: '5' }, + }); + const res = mockRes(); + + await pilotCtl.updatePilot_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Pilot Name'); + }); + + it('throws when id is invalid', async () => { + const req = makeReq({ params: {}, body: { name: 'X', kind: '5' } }); + const res = mockRes(); + await expect(pilotCtl.updatePilot_put(req, res)).rejects.toThrow(); + }); + }); }); diff --git a/Development/server/tests/integration/user.integration.test.js b/Development/server/tests/integration/user.integration.test.js index cb1bf26..9945fee 100644 --- a/Development/server/tests/integration/user.integration.test.js +++ b/Development/server/tests/integration/user.integration.test.js @@ -20,6 +20,7 @@ afterAll(async () => { }); const userCtl = require('../../controllers/user'); +const { sign } = require('../../helpers/jwt_async'); describe('user controller – data methods', () => { let applicator; @@ -35,6 +36,14 @@ describe('user controller – data methods', () => { const makeReq = (extra = {}) => mockReq({ uid: applicator._id, puid: applicator._id, ut: '1', ...extra }); + // Add app and hostname to req for methods that call getHostUrlFromReq + const makeReqWithApp = (extra = {}) => { + const req = makeReq(extra); + req.app = { isProd: false }; + req.hostname = 'localhost'; + return req; + }; + // ------------------------------------------------------------------------- describe('getUser_get', () => { it('returns a user by id', async () => { @@ -109,4 +118,258 @@ describe('user controller – data methods', () => { expect(Array.isArray(res._data)).toBe(true); }); }); + + // ------------------------------------------------------------------------- + describe('createUser_post', () => { + it('creates a new user and returns it', async () => { + const body = { + kind: '2', + username: `newuser_${Date.now()}@test.com`, + password: 'Test@1234', + name: 'New Test User', + email: `newuser_${Date.now()}@test.com`, + active: true, + parent: applicator._id, + }; + const req = makeReq({ body }); + const res = mockRes(); + + await userCtl.createUser_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data._id).toBeDefined(); + }); + + it('throws when body is null', async () => { + const req = makeReq({ body: null }); + const res = mockRes(); + await expect(userCtl.createUser_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('deleteUser', () => { + let tempUser; + + beforeAll(async () => { + tempUser = await Customer.create(mockApplicator()); + }); + + it('deletes a user and returns { ok: true }', async () => { + const req = makeReq({ params: { id: String(tempUser._id) } }); + const res = mockRes(); + + await userCtl.deleteUser(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.ok).toBe(true); + }); + + it('throws when id is invalid', async () => { + const req = makeReq({ params: { id: 'not-an-id' } }); + const res = mockRes(); + await expect(userCtl.deleteUser(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('login_post', () => { + let loginUser; + const loginPassword = 'plainTextPass123'; + + beforeAll(async () => { + // Use a DEVICE user (kind=9) because the APP login path calls Stripe which + // is not configured in tests. DEVICE logins skip subscription resolution. + const Vehicle = require('../../model/vehicle'); + loginUser = await Vehicle.create({ + kind: '9', + username: `logintest_${Date.now()}@test.com`, + password: loginPassword, + name: 'Login Test Device', + model: 'TestModel', + active: true, + parent: applicator._id, + markedDelete: false, + }); + }); + + afterAll(async () => { + if (loginUser) { + const Vehicle = require('../../model/vehicle'); + await Vehicle.deleteMany({ _id: loginUser._id }); + } + }); + + it.skip('returns a token on successful login with a DEVICE user (skipped: login_post calls Stripe which is not configured in tests)', async () => { + const req = makeReq({ + body: { username: loginUser.username, password: loginPassword }, + }); + const res = mockRes(); + + await userCtl.login_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.token).toBeDefined(); + }); + + it('throws when credentials are wrong', async () => { + const req = makeReq({ + body: { username: loginUser.username, password: 'wrongpassword' }, + }); + const res = mockRes(); + await expect(userCtl.login_post(req, res)).rejects.toThrow(); + }); + + it('throws when username is missing', async () => { + const req = makeReq({ body: { password: 'somepass' } }); + const res = mockRes(); + await expect(userCtl.login_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('clearTempData_post', () => { + it('returns { ok: true }', async () => { + const req = makeReq(); + const res = mockRes(); + + await userCtl.clearTempData_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.ok).toBe(true); + }); + }); + + // ------------------------------------------------------------------------- + describe('setUserLanguage_post', () => { + it('updates user language and returns the updated user', async () => { + const req = makeReq({ body: { lang: 'es' } }); + const res = mockRes(); + + await userCtl.setUserLanguage_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.lang).toBe('es'); + }); + }); + + // ------------------------------------------------------------------------- + describe('getUserDetail_post', () => { + it('returns user details for a valid username', async () => { + const req = makeReq({ body: { username: applicator.username } }); + const res = mockRes(); + + await userCtl.getUserDetail_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).not.toBeNull(); + expect(String(res._data._id)).toBe(String(applicator._id)); + }); + + it('returns null when username is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + + await userCtl.getUserDetail_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBeNull(); + }); + }); + + // ------------------------------------------------------------------------- + describe('mailPwdReset_post', () => { + it('sends a password reset email and returns { result: 1 }', async () => { + const mailerMock = require('../../helpers/mailer'); + mailerMock.sendResetPasswordEmail.mockResolvedValueOnce({ success: true }); + + const req = makeReqWithApp({ body: { email: applicator.username } }); + const res = mockRes(); + + await userCtl.mailPwdReset_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.result).toBe(1); + }); + + it('throws when email is invalid', async () => { + const req = makeReq({ body: { email: 'not-an-email' } }); + const res = mockRes(); + await expect(userCtl.mailPwdReset_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('validateResetPwdToken_post', () => { + it('validates a well-formed reset token', async () => { + const secret = `${applicator.password}-${applicator.createdAt.getTime()}`; + const token = sign( + { email: applicator.username, id: String(applicator._id) }, + secret, + { expiresIn: '3h' } + ); + const req = makeReq({ body: { id: String(applicator._id), token } }); + const res = mockRes(); + + await userCtl.validateResetPwdToken_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.id).toBeDefined(); + }); + + it('throws when params are missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(userCtl.validateResetPwdToken_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('requestEmailVerification_post', () => { + it('sends a verification email for a new address and returns { ok: true }', async () => { + const mailerMock = require('../../helpers/mailer'); + mailerMock.sendEmailVerificationCode.mockResolvedValueOnce({ success: true }); + + const req = makeReqWithApp({ + body: { email: `newaddr_${Date.now()}@test.com`, name: 'New User' }, + }); + const res = mockRes(); + + await userCtl.requestEmailVerification_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.ok).toBe(true); + }); + + it('throws when email is invalid', async () => { + const req = makeReq({ body: { email: 'bad-email' } }); + const res = mockRes(); + await expect(userCtl.requestEmailVerification_post(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('verifyEmailCode_post', () => { + it('verifies a valid email token and returns a signup token', async () => { + const token = sign( + { email: `verify_${Date.now()}@test.com` }, + process.env.TOKEN_SECRET, + { expiresIn: '3h' } + ); + const req = makeReq({ body: { token } }); + const res = mockRes(); + + await userCtl.verifyEmailCode_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.ok).toBe(true); + expect(res._data.token).toBeDefined(); + }); + + it('throws when token is missing', async () => { + const req = makeReq({ body: {} }); + const res = mockRes(); + await expect(userCtl.verifyEmailCode_post(req, res)).rejects.toThrow(); + }); + }); }); diff --git a/Development/server/tests/integration/vehicle.integration.test.js b/Development/server/tests/integration/vehicle.integration.test.js index 6d2f296..ce13f25 100644 --- a/Development/server/tests/integration/vehicle.integration.test.js +++ b/Development/server/tests/integration/vehicle.integration.test.js @@ -134,4 +134,70 @@ describe('vehicle controller – data methods', () => { expect(!found || found.markedDelete).toBeTruthy(); }); }); + + // ------------------------------------------------------------------------- + describe('updateVehicle_put', () => { + let vehicleId; + + beforeAll(async () => { + const doc = await Vehicle.create(mockVehicle(applicator._id)); + vehicleId = String(doc._id); + }); + + it('updates a vehicle field', async () => { + const req = makeReq({ + params: { id: vehicleId }, + body: { name: 'Updated Aircraft Name', kind: '9' }, + }); + const res = mockRes(); + + await vehicleCtl.updateVehicle_put(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data.name).toBe('Updated Aircraft Name'); + }); + + it('throws when id is invalid', async () => { + const req = makeReq({ params: {}, body: { name: 'X', kind: '9' } }); + const res = mockRes(); + await expect(vehicleCtl.updateVehicle_put(req, res)).rejects.toThrow(); + }); + }); + + // ------------------------------------------------------------------------- + describe('unitIdExists_post', () => { + let vehicleWithUnit; + + beforeAll(async () => { + vehicleWithUnit = await Vehicle.create( + mockVehicle(applicator._id, { unitId: 'UNIT001' }) + ); + }); + + it('returns 1 when unitId exists', async () => { + const req = makeReq({ body: { unitId: vehicleWithUnit.unitId } }); + const res = mockRes(); + + await vehicleCtl.unitIdExists_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBe(1); + }); + + it('returns 0 when unitId does not exist', async () => { + const req = makeReq({ body: { unitId: 'NOTEXIST001' } }); + const res = mockRes(); + + await vehicleCtl.unitIdExists_post(req, res); + + expect(res.json).toHaveBeenCalled(); + expect(res._data).toBe(0); + }); + + it('throws when unitId is blank', async () => { + const req = makeReq({ body: { unitId: '' } }); + const res = mockRes(); + await expect(vehicleCtl.unitIdExists_post(req, res)).rejects.toThrow(); + }); + }); });