From a0d9888daeb3176c1c3130e21803f263998ea902 Mon Sep 17 00:00:00 2001 From: Asaf Masa Date: Sun, 29 Mar 2026 15:25:14 +0300 Subject: [PATCH 1/6] refactor: move csw to request in code --- src/externalServices/catalog/cswClient.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/externalServices/catalog/cswClient.ts b/src/externalServices/catalog/cswClient.ts index 2eae241..8cdd0f9 100644 --- a/src/externalServices/catalog/cswClient.ts +++ b/src/externalServices/catalog/cswClient.ts @@ -65,7 +65,8 @@ export class CswClient { const logContext = { ...this.logContext, function: this.getRecords.name }; const body = this.generateCswBody(bbox, sortOrder, sortColumn, startPosition, maxRecords); try { - const res = await axios.post(this.cswUrl, body, { + const res = await axios.post('/csw', body, { + baseURL: this.cswUrl, params: { token: this.cswToken == '' ? undefined : this.cswToken }, headers: { 'Content-Type': 'text/xml' }, }); From 45eebe09c15c5abf397ca56cf565a8891c56f0f2 Mon Sep 17 00:00:00 2001 From: Asaf Masa Date: Mon, 30 Mar 2026 00:35:03 +0300 Subject: [PATCH 2/6] chore: fix tests --- .../configurations/integration/jest.config.js | 10 +-- tests/configurations/unit/jest.config.js | 1 - .../unit/records/models/recordsModel.spec.ts | 87 ++++++++++++++++++- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/tests/configurations/integration/jest.config.js b/tests/configurations/integration/jest.config.js index bae56c9..db1c210 100644 --- a/tests/configurations/integration/jest.config.js +++ b/tests/configurations/integration/jest.config.js @@ -18,15 +18,7 @@ module.exports = { testMatch: ['/tests/integration/**/*.spec.ts'], collectCoverage: true, - collectCoverageFrom: [ - '/src/**/*.ts', - '!*/node_modules/', - '!/vendor/**', - '!*/common/**', - '!**/models/**', - '!/src/*', - '!/src/externalServices/catalog/cswClient.ts', - ], + collectCoverageFrom: ['/src/**/*.ts', '!*/node_modules/', '!/vendor/**', '!*/common/**', '!**/models/**', '!/src/*'], coverageReporters: ['text', 'html'], coverageDirectory: '/coverage/integration', diff --git a/tests/configurations/unit/jest.config.js b/tests/configurations/unit/jest.config.js index 3fedce0..a877eb7 100644 --- a/tests/configurations/unit/jest.config.js +++ b/tests/configurations/unit/jest.config.js @@ -22,7 +22,6 @@ module.exports = { '!**/controllers/**', '!**/routes/**', '!/src/*', - '!/src/externalServices/catalog/cswClient.ts', ], coverageDirectory: '/coverage/unit', reporters: [ diff --git a/tests/unit/records/models/recordsModel.spec.ts b/tests/unit/records/models/recordsModel.spec.ts index 59bac0e..8e39053 100644 --- a/tests/unit/records/models/recordsModel.spec.ts +++ b/tests/unit/records/models/recordsModel.spec.ts @@ -18,15 +18,21 @@ import { mockAuditRepo, mockCatalogCall, mockExtractableFindAndCount, + mockExtractableFind, } from '@tests/mocks/unitMocks'; import { mapExtractableRecordToCamelCase } from '@src/utils/converter'; import { CatalogCall } from '@src/externalServices/catalog/catalogCall'; -import { CswClient } from '@src/externalServices/catalog/cswClient'; +import { CSWRecord } from '@src/externalServices/catalog/interfaces'; let recordsManager: RecordsManager; let validationsManager: ValidationsManager; +export const cswClientMock = { + getRecordsByCoordinate: jest.fn(), + getAllRecords: jest.fn(), +}; + describe('RecordsManager & ValidationsManager', () => { beforeEach(() => { const extractableRepo = mockExtractableRepo as unknown as Repository; @@ -69,15 +75,13 @@ describe('RecordsManager & ValidationsManager', () => { getRepository: fakeEntityManager.getRepository, }; - const cswClient = new CswClient(mockConfig as unknown as IConfig, jsLogger({ enabled: false }), trace.getTracer('testTracer')); - validationsManager = new ValidationsManager( jsLogger({ enabled: false }), mockConfig as unknown as IConfig, extractableRepo, mockCatalogCall as unknown as CatalogCall ); - recordsManager = new RecordsManager(jsLogger({ enabled: false }), trace.getTracer('testTracer'), extractableRepo, cswClient); + recordsManager = new RecordsManager(jsLogger({ enabled: false }), trace.getTracer('testTracer'), extractableRepo, cswClientMock as never); }); afterEach(() => { @@ -227,6 +231,81 @@ describe('RecordsManager & ValidationsManager', () => { }); }); + describe('#getRecordsByCoordinate', () => { + it('should return records from CSW client', async () => { + const cswRecords: CSWRecord[] = [ + { + productId: 'prod_123', + productName: 'rec_happy_create', + }, + { + productId: 'prod_12344', + productName: 'found_but_not_in_extractable', + }, + ]; + + const extractableRecord: ExtractableRecord[] = [ + { + id: 1, + record_name: 'rec_happy_create', + username: validCredentials.username, + authorized_by: recordInstance.authorizedBy, + data: recordInstance.data, + remarks: recordInstance.remarks, + }, + ]; + + const cswResponse = { + numberOfRecords: 1, + numberOfRecordsReturned: 1, + nextRecord: 0, + records: cswRecords, + }; + cswClientMock.getAllRecords.mockResolvedValueOnce(cswRecords); + cswClientMock.getRecordsByCoordinate.mockResolvedValueOnce(cswRecords); + mockExtractableFind.mockResolvedValueOnce(extractableRecord); + const result = await recordsManager.getRecordsByCoordinate(1, 2, 3); + expect(result).toEqual([ + { + id: extractableRecord[0]!.id, + recordName: extractableRecord[0]!.record_name, + username: extractableRecord[0]!.username, + authorizedBy: extractableRecord[0]!.authorized_by, + authorizedAt: undefined, + data: extractableRecord[0]!.data, + remarks: extractableRecord[0]!.remarks, + }, + ]); + }); + + it.each([ + { lon: undefined, lat: 34, errorMessage: 'Invalid coordinates' }, + { lon: 34, lat: undefined, errorMessage: 'Invalid coordinates' }, + { lon: -181, lat: 34, errorMessage: 'Coordinates Out Of Range' }, + { lon: 181, lat: 34, errorMessage: 'Coordinates Out Of Range' }, + { lon: 35, lat: -91, errorMessage: 'Coordinates Out Of Range' }, + { lon: 35, lat: 91, errorMessage: 'Coordinates Out Of Range' }, + ])( + 'should check if coordinate is valid and return false for invalid %p', + async (testInput: { lon: number | undefined; lat: number | undefined; errorMessage: string }) => { + const getResponse = recordsManager.getRecordsByCoordinate(testInput.lon as number, testInput.lat as number, 1); + await expect(getResponse).rejects.toThrow(testInput.errorMessage); + } + ); + + it.each([ + { distanceMeters: undefined, errorMessage: 'Invalid Distance' }, + { distanceMeters: 0, errorMessage: 'Invalid Distance' }, + ])( + 'should check if distance is valid and return false for invalid %p', + async (testInput: { distanceMeters: number | undefined; errorMessage: string }) => { + mockExtractableFind.mockResolvedValueOnce([]); + const getResponse = recordsManager.getRecordsByCoordinate(35, 34, testInput.distanceMeters as number); + await expect(getResponse).rejects.toThrow(testInput.errorMessage); + } + ); + }); + describe('ValidationsManager - uncovered branches', () => { describe('#validateCreate - userValidation failure', () => { it('should return invalid if username/password are missing', async () => { From 47f568c212233e0e8b227918ce64e8462985324e Mon Sep 17 00:00:00 2001 From: asafMasa Date: Mon, 30 Mar 2026 16:38:17 +0300 Subject: [PATCH 3/6] fix: fix unit tests --- package-lock.json | 197 ++++++++++-------- package.json | 2 + src/records/models/recordsManager.ts | 2 +- tests/configurations/unit/jest.config.js | 6 +- ...equestCall.spec.ts => catalogCall.spec.ts} | 0 .../externalServices/catalog/cswClent.spec.ts | 158 ++++++++++++++ .../unit/records/models/recordsModel.spec.ts | 138 ++++++------ 7 files changed, 347 insertions(+), 156 deletions(-) rename tests/unit/externalServices/catalog/{requestCall.spec.ts => catalogCall.spec.ts} (100%) create mode 100644 tests/unit/externalServices/catalog/cswClent.spec.ts diff --git a/package-lock.json b/package-lock.json index e6848fb..85d8176 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ }, "devDependencies": { "@commitlint/cli": "^19.8.0", + "@faker-js/faker": "^10.4.0", "@map-colonies/commitlint-config": "^1.1.1", "@map-colonies/eslint-config": "^6.0.0", "@map-colonies/openapi-helpers": "^2.0.0", @@ -58,6 +59,7 @@ "docker-compose": "^1.3.1", "eslint": "^9.23.0", "eslint-plugin-jest": "^28.11.0", + "geojson": "^0.5.0", "husky": "^9.1.7", "jest": "^29.7.0", "jest-html-reporters": "^3.1.4", @@ -1052,9 +1054,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1143,9 +1145,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1241,14 +1243,20 @@ "license": "MIT" }, "node_modules/@faker-js/faker": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", - "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz", + "integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], "license": "MIT", "engines": { - "node": ">=14.0.0", - "npm": ">=6.0.0" + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" } }, "node_modules/@godaddy/terminus": { @@ -8858,6 +8866,17 @@ "npm": ">=9.5.0" } }, + "node_modules/@redocly/respect-core/node_modules/@faker-js/faker": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-7.6.0.tgz", + "integrity": "sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, "node_modules/@redocly/respect-core/node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -12684,9 +12703,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13734,9 +13753,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -13911,9 +13930,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "license": "MIT", "peer": true, "dependencies": { @@ -15520,9 +15539,9 @@ } }, "node_modules/copyfiles/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -16205,9 +16224,9 @@ } }, "node_modules/dotgitignore/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -16661,9 +16680,9 @@ } }, "node_modules/eslint-plugin-import-x/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -16779,9 +16798,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -17213,9 +17232,9 @@ } }, "node_modules/express-openapi-validator/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", "license": "MIT", "funding": { "type": "opencollective", @@ -17334,9 +17353,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.3.tgz", - "integrity": "sha512-1o60KoFw2+LWKQu3IdcfcFlGTW4dpqEWmjhYec6H82AYZU2TVBXep6tMl8Z1Y+wM+ZrzCwe3BZ9Vyd9N2rIvmg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "funding": [ { "type": "github", @@ -17349,9 +17368,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.5.tgz", - "integrity": "sha512-NLY+V5NNbdmiEszx9n14mZBseJTC50bRq1VHsaxOmR72JDuZt+5J1Co+dC/4JPnyq+WrIHNM69r0sqf7BMb3Mg==", + "version": "5.5.9", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz", + "integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==", "funding": [ { "type": "github", @@ -17360,9 +17379,9 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.3", - "path-expression-matcher": "^1.1.3", - "strnum": "^2.1.2" + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -18512,9 +18531,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -21464,9 +21483,9 @@ } }, "node_modules/jest-mock-axios/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -23932,9 +23951,9 @@ } }, "node_modules/oas-linter/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", "engines": { @@ -23979,9 +23998,9 @@ } }, "node_modules/oas-resolver/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", "engines": { @@ -24019,9 +24038,9 @@ } }, "node_modules/oas-validator/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", "engines": { @@ -24513,9 +24532,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", "funding": [ { "type": "github", @@ -24588,9 +24607,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { @@ -24717,9 +24736,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -25195,9 +25214,9 @@ } }, "node_modules/pretty-quick/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -25970,9 +25989,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -27085,9 +27104,9 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", "funding": [ { "type": "github", @@ -27281,9 +27300,9 @@ } }, "node_modules/swagger2openapi/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", "engines": { @@ -27380,9 +27399,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -27521,9 +27540,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -28640,9 +28659,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index faf59f7..5401155 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@map-colonies/openapi-helpers": "^2.0.0", "@map-colonies/prettier-config": "0.0.1", "@map-colonies/tsconfig": "^1.0.1", + "@faker-js/faker": "^10.4.0", "@redocly/cli": "^1.34.0", "@swc/core": "^1.15.11", "@swc/jest": "^0.2.37", @@ -85,6 +86,7 @@ "docker-compose": "^1.3.1", "eslint": "^9.23.0", "eslint-plugin-jest": "^28.11.0", + "geojson": "^0.5.0", "husky": "^9.1.7", "jest": "^29.7.0", "jest-html-reporters": "^3.1.4", diff --git a/src/records/models/recordsManager.ts b/src/records/models/recordsManager.ts index 77c305e..33c96fc 100644 --- a/src/records/models/recordsManager.ts +++ b/src/records/models/recordsManager.ts @@ -172,7 +172,7 @@ export class RecordsManager { } } - private getPolygonByPointAndRadius(coordinates: [number, number], radiusInMeters = 1): BBox { + private getPolygonByPointAndRadius(coordinates: [number, number], radiusInMeters: number): BBox { const tPoint = point(coordinates); const circ = circle(tPoint, radiusInMeters, { units: 'meters' }); const bb = bbox(circ) as [number, number, number, number]; diff --git a/tests/configurations/unit/jest.config.js b/tests/configurations/unit/jest.config.js index a877eb7..aeff3a6 100644 --- a/tests/configurations/unit/jest.config.js +++ b/tests/configurations/unit/jest.config.js @@ -6,7 +6,7 @@ module.exports = { '^.+\\.(ts|js)$': ['@swc/jest'], }, transformIgnorePatterns: [ - '/node_modules/(?!(@map-colonies/mc-model-types|concaveman|@turf/convex|@turf/turf|@turf/clusters-dbscan|geokdbush|kdbush|tinyqueue|rbush|quickselect|robust-predicates)/)', + '/node_modules/(?!(@map-colonies/mc-model-types|concaveman|@faker-js/faker|@turf/convex|@turf/turf|@turf/clusters-dbscan|geokdbush|kdbush|tinyqueue|rbush|quickselect|robust-predicates)/)', ], moduleNameMapper: pathsToModuleNameMapper(tsconfigJson.compilerOptions.paths, { prefix: '/', @@ -42,10 +42,10 @@ module.exports = { extensionsToTreatAsEsm: ['.ts'], coverageThreshold: { global: { - branches: 66, + branches: 80, functions: 80, lines: 80, - statements: -30, + statements: -10, }, }, }; diff --git a/tests/unit/externalServices/catalog/requestCall.spec.ts b/tests/unit/externalServices/catalog/catalogCall.spec.ts similarity index 100% rename from tests/unit/externalServices/catalog/requestCall.spec.ts rename to tests/unit/externalServices/catalog/catalogCall.spec.ts diff --git a/tests/unit/externalServices/catalog/cswClent.spec.ts b/tests/unit/externalServices/catalog/cswClent.spec.ts new file mode 100644 index 0000000..326329e --- /dev/null +++ b/tests/unit/externalServices/catalog/cswClent.spec.ts @@ -0,0 +1,158 @@ +import axios from 'axios'; +jest.mock('axios'); +const mockAxios = axios as jest.Mocked; +import config from 'config'; +import jsLogger from '@map-colonies/js-logger'; +import { faker } from '@faker-js/faker'; +import { trace } from '@opentelemetry/api'; +import { CswClient } from '../../../../src/externalServices/catalog/cswClient'; +import { BBox } from 'geojson'; + +let cswCatalog: CswClient; + +describe('cswCatalog tests', () => { + const cswCatalogUrl = `${config.get('externalServices.csw.url')}`; + + beforeEach(() => { + cswCatalog = new CswClient(config, jsLogger({ enabled: false }), trace.getTracer('testTracer')); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + function generateBBox(): BBox { + const lat1 = faker.location.latitude(); + const lat2 = faker.location.latitude(); + const lng1 = faker.location.longitude(); + const lng2 = faker.location.longitude(); + + return [ + Math.min(lng1, lng2), // minLongitude + Math.min(lat1, lat2), // minLatitude + Math.max(lng1, lng2), // maxLongitude + Math.max(lat1, lat2), // maxLatitude + ]; + } + + const expected_CSW_4_results = ` + + + + + + 32d542c1-b956-4579-91df-2a43b183d8b3 + a + + + bcc9985f-50eb-4545-84ae-f668b5172681 + b + + + 33333333-3333-3333-3333-333333333333 + c + + + 47978ca9-232a-4be8-b2d1-b04f71dcafcf + d + + +`; + + const expected_CSW_0_results = ` + + + + +`; + + const expected_CSW_1_result = ` + + + + + + 33333333-3333-3333-3333-333333333333 + c + + +`; + + const expected_CSW_page_1_result_1 = ` + + + + + + alexccee-55ab-42c1-936b-e7fa81f518a3 + aaa + + +`; + const expected_CSW_page_2_result_1 = ` + + + + + + 33333333-3333-3333-3333-333333333333 + c + + +`; + + describe('getAllRecords Function', () => { + it('Returns 4 records for BBOX', async () => { + const bbox = generateBBox(); + + mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_4_results }); + const response = await cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName'); + + expect(response).toEqual([ + { productName: 'a', productId: '32d542c1-b956-4579-91df-2a43b183d8b3' }, + { productName: 'b', productId: 'bcc9985f-50eb-4545-84ae-f668b5172681' }, + { productName: 'c', productId: '33333333-3333-3333-3333-333333333333' }, + { productName: 'd', productId: '47978ca9-232a-4be8-b2d1-b04f71dcafcf' }, + ]); + }); + + it('Returns 0 records for BBOX', async () => { + const bbox = generateBBox(); + + mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_0_results }); + const response = await cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName'); + + expect(response).toEqual([]); + }); + + it('Returns 1 records for BBOX', async () => { + const bbox = generateBBox(); + + mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_1_result }); + const response = await cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName'); + + expect(response).toEqual([{ productName: 'c', productId: '33333333-3333-3333-3333-333333333333' }]); + }); + + it('Returns 2 records for BBOX with pagination of 1', async () => { + const bbox = generateBBox(); + + mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_page_1_result_1 }); + mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_page_2_result_1 }); + const response = await cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName', 1, 1); + + expect(response).toEqual([ + { productName: 'aaa', productId: 'alexccee-55ab-42c1-936b-e7fa81f518a3' }, + { productName: 'c', productId: '33333333-3333-3333-3333-333333333333' }, + ]); + }); + + it('rejects if CSW is not available', async () => { + const bbox = generateBBox(); + + mockAxios.post.mockRejectedValueOnce(new Error('catalog is not available')); + const response = cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName'); + + await expect(response).rejects.toThrow('CSW_CATALOG_ERROR'); + }); + }); +}); diff --git a/tests/unit/records/models/recordsModel.spec.ts b/tests/unit/records/models/recordsModel.spec.ts index 8e39053..4c93a81 100644 --- a/tests/unit/records/models/recordsModel.spec.ts +++ b/tests/unit/records/models/recordsModel.spec.ts @@ -28,8 +28,8 @@ import { CSWRecord } from '@src/externalServices/catalog/interfaces'; let recordsManager: RecordsManager; let validationsManager: ValidationsManager; -export const cswClientMock = { - getRecordsByCoordinate: jest.fn(), +const cswClientMock = { + getRecords: jest.fn(), getAllRecords: jest.fn(), }; @@ -232,75 +232,87 @@ describe('RecordsManager & ValidationsManager', () => { }); describe('#getRecordsByCoordinate', () => { - it('should return records from CSW client', async () => { - const cswRecords: CSWRecord[] = [ - { - productId: 'prod_123', - productName: 'rec_happy_create', - }, - { - productId: 'prod_12344', - productName: 'found_but_not_in_extractable', - }, - ]; - - const extractableRecord: ExtractableRecord[] = [ - { - id: 1, - record_name: 'rec_happy_create', - username: validCredentials.username, - authorized_by: recordInstance.authorizedBy, - data: recordInstance.data, - remarks: recordInstance.remarks, - }, - ]; - - const cswResponse = { - numberOfRecords: 1, - numberOfRecordsReturned: 1, - nextRecord: 0, - records: cswRecords, - }; - cswClientMock.getAllRecords.mockResolvedValueOnce(cswRecords); - cswClientMock.getRecordsByCoordinate.mockResolvedValueOnce(cswRecords); - mockExtractableFind.mockResolvedValueOnce(extractableRecord); - const result = await recordsManager.getRecordsByCoordinate(1, 2, 3); - expect(result).toEqual([ - { - id: extractableRecord[0]!.id, - recordName: extractableRecord[0]!.record_name, - username: extractableRecord[0]!.username, - authorizedBy: extractableRecord[0]!.authorized_by, - authorizedAt: undefined, - data: extractableRecord[0]!.data, - remarks: extractableRecord[0]!.remarks, - }, - ]); - }); - it.each([ - { lon: undefined, lat: 34, errorMessage: 'Invalid coordinates' }, - { lon: 34, lat: undefined, errorMessage: 'Invalid coordinates' }, - { lon: -181, lat: 34, errorMessage: 'Coordinates Out Of Range' }, - { lon: 181, lat: 34, errorMessage: 'Coordinates Out Of Range' }, - { lon: 35, lat: -91, errorMessage: 'Coordinates Out Of Range' }, - { lon: 35, lat: 91, errorMessage: 'Coordinates Out Of Range' }, + { lon: 35, lat: 34, distanceMeters: 1 }, + { lon: 35, lat: 34, distanceMeters: undefined }, ])( - 'should check if coordinate is valid and return false for invalid %p', - async (testInput: { lon: number | undefined; lat: number | undefined; errorMessage: string }) => { - const getResponse = recordsManager.getRecordsByCoordinate(testInput.lon as number, testInput.lat as number, 1); - await expect(getResponse).rejects.toThrow(testInput.errorMessage); + 'should return records from CSW client %p', + async (testInput: { lon: number | undefined; lat: number | undefined; distanceMeters: number | undefined }) => { + const cswRecords: CSWRecord[] = [ + { + productId: 'prod_123', + productName: 'rec_happy_create', + }, + { + productId: 'prod_12344', + productName: 'found_but_not_in_extractable', + }, + ]; + + const extractableRecord: ExtractableRecord[] = [ + { + id: 1, + record_name: 'rec_happy_create', + username: validCredentials.username, + authorized_by: recordInstance.authorizedBy, + data: recordInstance.data, + remarks: recordInstance.remarks, + }, + ]; + + const cswResponse = { + numberOfRecords: 1, + numberOfRecordsReturned: 1, + nextRecord: 0, + records: cswRecords, + }; + cswClientMock.getAllRecords.mockResolvedValueOnce(cswRecords); + cswClientMock.getRecords.mockResolvedValueOnce(cswResponse); + mockExtractableFind.mockResolvedValueOnce(extractableRecord); + const result = await recordsManager.getRecordsByCoordinate( + testInput.lon as number, + testInput.lat as number, + testInput.distanceMeters as number + ); + expect(result).toEqual([ + { + id: extractableRecord[0]!.id, + recordName: extractableRecord[0]!.record_name, + username: extractableRecord[0]!.username, + authorizedBy: extractableRecord[0]!.authorized_by, + authorizedAt: undefined, + data: extractableRecord[0]!.data, + remarks: extractableRecord[0]!.remarks, + }, + ]); } ); it.each([ - { distanceMeters: undefined, errorMessage: 'Invalid Distance' }, - { distanceMeters: 0, errorMessage: 'Invalid Distance' }, + { lon: undefined, lat: 34, distanceMeters: 1, errorMessage: 'Invalid coordinates' }, + { lon: 34, lat: undefined, distanceMeters: 1, errorMessage: 'Invalid coordinates' }, + { lon: -181, lat: 34, distanceMeters: 1, errorMessage: 'Coordinates Out Of Range' }, + { lon: 181, lat: 34, distanceMeters: 1, errorMessage: 'Coordinates Out Of Range' }, + { lon: 35, lat: -91, distanceMeters: 1, errorMessage: 'Coordinates Out Of Range' }, + { lon: 35, lat: 91, distanceMeters: 1, errorMessage: 'Coordinates Out Of Range' }, + { lon: 35, lat: 35, distanceMeters: 0, errorMessage: 'Invalid Distance' }, ])( - 'should check if distance is valid and return false for invalid %p', - async (testInput: { distanceMeters: number | undefined; errorMessage: string }) => { + 'should check if coordinate is valid and return false for invalid %p', + async (testInput: { lon: number | undefined; lat: number | undefined; distanceMeters: number | undefined; errorMessage: string }) => { + const cswResponse = { + numberOfRecords: 0, + numberOfRecordsReturned: 0, + nextRecord: 0, + records: [], + }; + cswClientMock.getAllRecords.mockResolvedValueOnce([]); + cswClientMock.getRecords.mockResolvedValueOnce(cswResponse); mockExtractableFind.mockResolvedValueOnce([]); - const getResponse = recordsManager.getRecordsByCoordinate(35, 34, testInput.distanceMeters as number); + const getResponse = recordsManager.getRecordsByCoordinate( + testInput.lon as number, + testInput.lat as number, + testInput.distanceMeters as number + ); await expect(getResponse).rejects.toThrow(testInput.errorMessage); } ); From e78dc5dd5fff24186896ab1ede632716ea1693e4 Mon Sep 17 00:00:00 2001 From: asafMasa Date: Sun, 12 Apr 2026 14:36:42 +0300 Subject: [PATCH 4/6] chore: fix unit tests --- tests/mocks/cswMocks.ts | 83 ++++++++++++++ .../externalServices/catalog/cswClent.spec.ts | 107 +++--------------- 2 files changed, 99 insertions(+), 91 deletions(-) create mode 100644 tests/mocks/cswMocks.ts diff --git a/tests/mocks/cswMocks.ts b/tests/mocks/cswMocks.ts new file mode 100644 index 0000000..443e905 --- /dev/null +++ b/tests/mocks/cswMocks.ts @@ -0,0 +1,83 @@ +import { faker } from '@faker-js/faker'; +import { BBox } from 'geojson'; + +export function generateBBox(): BBox { + const lat1 = faker.location.latitude(); + const lat2 = faker.location.latitude(); + const lng1 = faker.location.longitude(); + const lng2 = faker.location.longitude(); + + return [ + Math.min(lng1, lng2), // minLongitude + Math.min(lat1, lat2), // minLatitude + Math.max(lng1, lng2), // maxLongitude + Math.max(lat1, lat2), // maxLatitude + ]; +} + +export const expectedCSW4Results = ` + + + + + + 32d542c1-b956-4579-91df-2a43b183d8b3 + a + + + bcc9985f-50eb-4545-84ae-f668b5172681 + b + + + 33333333-3333-3333-3333-333333333333 + c + + + 47978ca9-232a-4be8-b2d1-b04f71dcafcf + d + + +`; + +export const expectedCSW0Results = ` + + + + +`; + +export const expectedCSW1Result = ` + + + + + + 33333333-3333-3333-3333-333333333333 + c + + +`; + +export const expectedCSWPage1Result1 = ` + + + + + + alexccee-55ab-42c1-936b-e7fa81f518a3 + aaa + + +`; + +export const expectedCSWPage2Result1 = ` + + + + + + 33333333-3333-3333-3333-333333333333 + c + + +`; diff --git a/tests/unit/externalServices/catalog/cswClent.spec.ts b/tests/unit/externalServices/catalog/cswClent.spec.ts index 326329e..56a1811 100644 --- a/tests/unit/externalServices/catalog/cswClent.spec.ts +++ b/tests/unit/externalServices/catalog/cswClent.spec.ts @@ -1,18 +1,23 @@ import axios from 'axios'; -jest.mock('axios'); -const mockAxios = axios as jest.Mocked; import config from 'config'; import jsLogger from '@map-colonies/js-logger'; -import { faker } from '@faker-js/faker'; import { trace } from '@opentelemetry/api'; +import { + expectedCSW0Results, + expectedCSW1Result, + expectedCSW4Results, + expectedCSWPage1Result1, + expectedCSWPage2Result1, + generateBBox, +} from '@tests/mocks/cswMocks'; import { CswClient } from '../../../../src/externalServices/catalog/cswClient'; -import { BBox } from 'geojson'; + +jest.mock('axios'); +const mockAxios = axios as jest.Mocked; let cswCatalog: CswClient; describe('cswCatalog tests', () => { - const cswCatalogUrl = `${config.get('externalServices.csw.url')}`; - beforeEach(() => { cswCatalog = new CswClient(config, jsLogger({ enabled: false }), trace.getTracer('testTracer')); }); @@ -20,91 +25,11 @@ describe('cswCatalog tests', () => { jest.clearAllMocks(); }); - function generateBBox(): BBox { - const lat1 = faker.location.latitude(); - const lat2 = faker.location.latitude(); - const lng1 = faker.location.longitude(); - const lng2 = faker.location.longitude(); - - return [ - Math.min(lng1, lng2), // minLongitude - Math.min(lat1, lat2), // minLatitude - Math.max(lng1, lng2), // maxLongitude - Math.max(lat1, lat2), // maxLatitude - ]; - } - - const expected_CSW_4_results = ` - - - - - - 32d542c1-b956-4579-91df-2a43b183d8b3 - a - - - bcc9985f-50eb-4545-84ae-f668b5172681 - b - - - 33333333-3333-3333-3333-333333333333 - c - - - 47978ca9-232a-4be8-b2d1-b04f71dcafcf - d - - -`; - - const expected_CSW_0_results = ` - - - - -`; - - const expected_CSW_1_result = ` - - - - - - 33333333-3333-3333-3333-333333333333 - c - - -`; - - const expected_CSW_page_1_result_1 = ` - - - - - - alexccee-55ab-42c1-936b-e7fa81f518a3 - aaa - - -`; - const expected_CSW_page_2_result_1 = ` - - - - - - 33333333-3333-3333-3333-333333333333 - c - - -`; - describe('getAllRecords Function', () => { it('Returns 4 records for BBOX', async () => { const bbox = generateBBox(); - mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_4_results }); + mockAxios.post.mockResolvedValueOnce({ data: expectedCSW4Results }); const response = await cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName'); expect(response).toEqual([ @@ -118,7 +43,7 @@ describe('cswCatalog tests', () => { it('Returns 0 records for BBOX', async () => { const bbox = generateBBox(); - mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_0_results }); + mockAxios.post.mockResolvedValueOnce({ data: expectedCSW0Results }); const response = await cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName'); expect(response).toEqual([]); @@ -127,7 +52,7 @@ describe('cswCatalog tests', () => { it('Returns 1 records for BBOX', async () => { const bbox = generateBBox(); - mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_1_result }); + mockAxios.post.mockResolvedValueOnce({ data: expectedCSW1Result }); const response = await cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName'); expect(response).toEqual([{ productName: 'c', productId: '33333333-3333-3333-3333-333333333333' }]); @@ -136,8 +61,8 @@ describe('cswCatalog tests', () => { it('Returns 2 records for BBOX with pagination of 1', async () => { const bbox = generateBBox(); - mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_page_1_result_1 }); - mockAxios.post.mockResolvedValueOnce({ data: expected_CSW_page_2_result_1 }); + mockAxios.post.mockResolvedValueOnce({ data: expectedCSWPage1Result1 }); + mockAxios.post.mockResolvedValueOnce({ data: expectedCSWPage2Result1 }); const response = await cswCatalog.getAllRecords(bbox, 'ASC', 'mc:productName', 1, 1); expect(response).toEqual([ From 70aeb9e57aacee44b58a8cc0d04d2127910bce44 Mon Sep 17 00:00:00 2001 From: asafMasa Date: Thu, 16 Apr 2026 10:07:41 +0300 Subject: [PATCH 5/6] chore: update integration tests to disable logs and use mockAxios package --- tests/integration/audit/audit.spec.ts | 34 ++++++++++--------- .../externalServices/catalog/catalog.spec.ts | 23 +++++++------ tests/integration/records/records.spec.ts | 30 +++++++++------- tests/integration/users/users.spec.ts | 10 +++++- tests/mocks/axios.ts | 3 ++ 5 files changed, 59 insertions(+), 41 deletions(-) create mode 100644 tests/mocks/axios.ts diff --git a/tests/integration/audit/audit.spec.ts b/tests/integration/audit/audit.spec.ts index 0e8b2ba..44d94a2 100644 --- a/tests/integration/audit/audit.spec.ts +++ b/tests/integration/audit/audit.spec.ts @@ -1,9 +1,12 @@ +import jsLogger from '@map-colonies/js-logger'; +import { trace } from '@opentelemetry/api'; import httpStatusCodes from 'http-status-codes'; -import axios from 'axios'; +import mockAxios from 'jest-mock-axios'; import { container as tsyringeContainer } from 'tsyringe'; import { createRequestSender, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; import { paths, operations } from '@openapi'; import { getApp } from '@src/app'; +import { CatalogCall } from '@src/externalServices/catalog/catalogCall'; import { SERVICES } from '@common/constants'; import { initConfig } from '@src/common/config'; import { ConnectionManager } from '@src/DAL/connectionManager'; @@ -11,29 +14,29 @@ import { IAuditAction } from '@src/common/interfaces'; import { AuditManager } from '@src/audit_logs/models/auditManager'; import { validCredentials, recordInstance } from '@tests/mocks/generalMocks'; import { getAxiosPostMockResponse } from '@tests/mocks/integrationMocks'; - -jest.mock('axios'); - -const mockedAxios = axios as jest.Mocked; - -jest.mock('@src/externalServices/catalog/catalogCall', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - CatalogCall: jest.fn().mockImplementation(() => ({ - findPublishedRecord: jest.fn().mockResolvedValue(true), - })), -})); +// ensure modules that import 'axios' get the jest-mock-axios instance +// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access +jest.mock('axios', () => require('@tests/mocks/axios').default); describe('records', function () { let requestSender: RequestSender; beforeAll(async () => { - mockedAxios.post.mockResolvedValue(getAxiosPostMockResponse()); + mockAxios.post.mockResolvedValue(getAxiosPostMockResponse()); + // prevent real network calls to catalog service during tests + jest.spyOn(CatalogCall.prototype, 'findPublishedRecord').mockResolvedValue(true); await initConfig(true); console.log('✅ ConnectionManager DataSource initialized.'); - const [app] = await getApp({ useChild: false }); + const [app] = await getApp({ + useChild: false, + override: [ + { token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } }, + { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, + ], + }); requestSender = await createRequestSender('openapi3.yaml', app); }); @@ -147,7 +150,7 @@ describe('records', function () { authorizedAt: new Date().toISOString(), }; - jest.spyOn(AuditManager.prototype, 'getAuditLogs').mockResolvedValueOnce({ + const getAuditLogsSpy = jest.spyOn(AuditManager.prototype, 'getAuditLogs').mockResolvedValueOnce({ numberOfRecords: 50, numberOfRecordsReturned: 10, nextRecord: 11, @@ -170,7 +173,6 @@ describe('records', function () { expect(body.numberOfRecordsReturned).toBe(10); expect(body.nextRecord).toBe(11); expect(Array.isArray(body.records)).toBe(true); - const getAuditLogsSpy = jest.spyOn(AuditManager.prototype, 'getAuditLogs'); expect(getAuditLogsSpy).toHaveBeenCalledWith('rec_pagination_test', 1, 10); }); }); diff --git a/tests/integration/externalServices/catalog/catalog.spec.ts b/tests/integration/externalServices/catalog/catalog.spec.ts index fbbb773..8122891 100644 --- a/tests/integration/externalServices/catalog/catalog.spec.ts +++ b/tests/integration/externalServices/catalog/catalog.spec.ts @@ -1,29 +1,30 @@ -import axios from 'axios'; +import { trace } from '@opentelemetry/api'; +import mockAxios from 'jest-mock-axios'; import { StatusCodes } from 'http-status-codes'; import { RecordStatus } from '@map-colonies/mc-model-types'; import type { IConfig } from '@src/common/interfaces'; import { CatalogCall } from '@src/externalServices/catalog/catalogCall'; import { AppError } from '@src/utils/appError'; -import { createTracerMock, createLoggerMock } from '@tests/mocks/integrationMocks'; +import { createLoggerMock } from '@tests/mocks/integrationMocks'; -jest.mock('axios'); -const mockedAxios = axios as jest.Mocked; +// ensure modules that import 'axios' get the jest-mock-axios instance +// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access +jest.mock('axios', () => require('@tests/mocks/axios').default); describe('CatalogCall Integration (axios mocked)', () => { let catalogCall: CatalogCall; const loggerMock = createLoggerMock(); - const tracerMock = createTracerMock(); const configMock = { get: jest.fn().mockReturnValue('http://mock-catalog-service'), } as unknown as jest.Mocked; beforeEach(() => { jest.clearAllMocks(); - catalogCall = new CatalogCall(configMock, loggerMock, tracerMock); + catalogCall = new CatalogCall(configMock, loggerMock, trace.getTracer('testTracer')); }); it('should throw AppError for axios rejection', async () => { - mockedAxios.post.mockRejectedValueOnce(new Error('Network error')); + mockAxios.post.mockRejectedValueOnce(new Error('Network error')); await expect(catalogCall.findPublishedRecord('rec_fail')).rejects.toThrow(AppError); @@ -33,7 +34,7 @@ describe('CatalogCall Integration (axios mocked)', () => { }); it('should throw AppError for unexpected status', async () => { - mockedAxios.post.mockResolvedValueOnce({ + mockAxios.post.mockResolvedValueOnce({ status: StatusCodes.BAD_REQUEST, data: [], }); @@ -46,7 +47,7 @@ describe('CatalogCall Integration (axios mocked)', () => { }); it('should return false when record exists but not published', async () => { - mockedAxios.post.mockResolvedValueOnce({ + mockAxios.post.mockResolvedValueOnce({ status: StatusCodes.OK, data: [{ id: '123', name: 'rec_unpublished', productStatus: 'draft' }], }); @@ -60,7 +61,7 @@ describe('CatalogCall Integration (axios mocked)', () => { }); it('should return true when record exists and is published', async () => { - mockedAxios.post.mockResolvedValueOnce({ + mockAxios.post.mockResolvedValueOnce({ status: StatusCodes.OK, data: [{ id: '123', name: 'rec_published', productStatus: RecordStatus.PUBLISHED }], }); @@ -74,7 +75,7 @@ describe('CatalogCall Integration (axios mocked)', () => { }); it('should return false when no records are found', async () => { - mockedAxios.post.mockResolvedValueOnce({ + mockAxios.post.mockResolvedValueOnce({ status: StatusCodes.OK, data: [], }); diff --git a/tests/integration/records/records.spec.ts b/tests/integration/records/records.spec.ts index 0b5b309..103f25e 100644 --- a/tests/integration/records/records.spec.ts +++ b/tests/integration/records/records.spec.ts @@ -1,5 +1,7 @@ +import jsLogger from '@map-colonies/js-logger'; +import { trace } from '@opentelemetry/api'; import httpStatusCodes from 'http-status-codes'; -import axios from 'axios'; +import mockAxios from 'jest-mock-axios'; import { container as tsyringeContainer } from 'tsyringe'; import { createRequestSender, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; import { paths, operations } from '@openapi'; @@ -11,29 +13,31 @@ import { invalidCredentials, recordInstance, validCredentials } from '@tests/moc import { initConfig } from '@src/common/config'; import { ConnectionManager } from '@src/DAL/connectionManager'; import { getAxiosPostMockResponse } from '@tests/mocks/integrationMocks'; +import { CatalogCall } from '@src/externalServices/catalog/catalogCall'; -jest.mock('axios'); - -const mockedAxios = axios as jest.Mocked; - -jest.mock('@src/externalServices/catalog/catalogCall', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - CatalogCall: jest.fn().mockImplementation(() => ({ - findPublishedRecord: jest.fn().mockResolvedValue(true), - })), -})); +// ensure modules that import 'axios' get the jest-mock-axios instance +// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-member-access +jest.mock('axios', () => require('@tests/mocks/axios').default); describe('records', function () { let requestSender: RequestSender; beforeAll(async () => { - mockedAxios.post.mockResolvedValue(getAxiosPostMockResponse()); + // prevent real network calls to catalog service during tests + jest.spyOn(CatalogCall.prototype, 'findPublishedRecord').mockResolvedValue(true); + mockAxios.post.mockResolvedValue(getAxiosPostMockResponse()); await initConfig(true); console.log('✅ ConnectionManager DataSource initialized.'); - const [app] = await getApp({ useChild: false }); + const [app] = await getApp({ + useChild: false, + override: [ + { token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } }, + { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, + ], + }); requestSender = await createRequestSender('openapi3.yaml', app); }); diff --git a/tests/integration/users/users.spec.ts b/tests/integration/users/users.spec.ts index 1b04f5d..5b25632 100644 --- a/tests/integration/users/users.spec.ts +++ b/tests/integration/users/users.spec.ts @@ -1,3 +1,5 @@ +import jsLogger from '@map-colonies/js-logger'; +import { trace } from '@opentelemetry/api'; import { container as tsyringeContainer } from 'tsyringe'; import httpStatusCodes from 'http-status-codes'; import { createRequestSender, RequestSender } from '@map-colonies/openapi-helpers/requestSender'; @@ -17,7 +19,13 @@ describe('users', function () { console.log('✅ ConnectionManager DataSource initialized.'); - const [app] = await getApp({ useChild: false }); + const [app] = await getApp({ + useChild: false, + override: [ + { token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } }, + { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, + ], + }); requestSender = await createRequestSender('openapi3.yaml', app); }); diff --git a/tests/mocks/axios.ts b/tests/mocks/axios.ts new file mode 100644 index 0000000..26021c9 --- /dev/null +++ b/tests/mocks/axios.ts @@ -0,0 +1,3 @@ +import mockAxios from 'jest-mock-axios'; + +export default mockAxios; From cb0c57a3e8c1481a0e416e1ab76240e93a32f374 Mon Sep 17 00:00:00 2001 From: asafMasa Date: Thu, 16 Apr 2026 18:17:29 +0300 Subject: [PATCH 6/6] test: middle of integration test update --- tests/integration/records/records.spec.ts | 330 ++++++++++++---------- 1 file changed, 175 insertions(+), 155 deletions(-) diff --git a/tests/integration/records/records.spec.ts b/tests/integration/records/records.spec.ts index 38c53bb..d14924d 100644 --- a/tests/integration/records/records.spec.ts +++ b/tests/integration/records/records.spec.ts @@ -53,128 +53,71 @@ describe('records', function () { } }); - describe('Happy Path', function () { - beforeAll(async () => { - await requestSender.createRecord({ - pathParams: { recordName: 'rec_happy_path' }, - requestBody: { - ...recordInstance, - username: validCredentials.username, - password: validCredentials.password, - }, - }); - }); - - it('should return 201 when recordName is valid', async function () { - const response = await requestSender.createRecord({ - pathParams: { recordName: 'rec_happy_create' }, - requestBody: { - ...recordInstance, - username: validCredentials.username, - password: validCredentials.password, - }, - }); - - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.CREATED); - - expect(response.body).toMatchObject({ - recordName: 'rec_happy_create', - authorizedBy: recordInstance.authorizedBy, - data: recordInstance.data, - remarks: recordInstance.remarks, - }); - }); - - it('should return 200 and the record', async function () { - const response = await requestSender.getRecord({ - pathParams: { recordName: 'rec_happy_path' }, + describe('Post /records/{identifier}', function () { + describe('Happy Path 🙂', function () { + beforeAll(async () => { + await requestSender.createRecord({ + pathParams: { recordName: 'rec_happy_path' }, + requestBody: { + ...recordInstance, + username: validCredentials.username, + password: validCredentials.password, + }, + }); }); - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.OK); - - expect(response.body).toMatchObject({ - recordName: 'rec_happy_path', - authorizedBy: recordInstance.authorizedBy, - data: recordInstance.data, - }); - }); - - it('should return 200 and the available records', async function () { - jest.spyOn(RecordsManager.prototype, 'getRecords').mockResolvedValueOnce({ - numberOfRecords: 1, - numberOfRecordsReturned: 1, - nextRecord: 0, - records: [recordInstance], - }); - const response = await requestSender.getRecords(); + it('should create record and return 201 when recordName is valid', async function () { + const response = await requestSender.createRecord({ + pathParams: { recordName: 'rec_happy_create' }, + requestBody: { + ...recordInstance, + username: validCredentials.username, + password: validCredentials.password, + }, + }); - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.OK); - const body = response.body as { numberOfRecords: number; numberOfRecordsReturned: number; nextRecord: number }; - expect(body.numberOfRecords).toBe(1); - expect(body.numberOfRecordsReturned).toBe(1); - expect(body.nextRecord).toBe(0); - }); + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.CREATED); - it('should return 200 and empty array when no records exist', async function () { - jest.spyOn(RecordsManager.prototype, 'getRecords').mockResolvedValueOnce({ - numberOfRecords: 0, - numberOfRecordsReturned: 0, - nextRecord: 0, - records: [], + expect(response.body).toMatchObject({ + recordName: 'rec_happy_create', + authorizedBy: recordInstance.authorizedBy, + data: recordInstance.data, + remarks: recordInstance.remarks, + }); }); - const response = await requestSender.getRecords(); - - expect(response.status).toBe(httpStatusCodes.OK); - const body = response.body as { numberOfRecords: number; numberOfRecordsReturned: number; nextRecord: number; records: IExtractableRecord[] }; - expect(body.numberOfRecords).toBe(0); - expect(body.numberOfRecordsReturned).toBe(0); - expect(body.nextRecord).toBe(0); - expect(body.records).toEqual([]); }); - it('should return 200 with default pagination parameters when not provided', async function () { - jest.spyOn(RecordsManager.prototype, 'getRecords').mockResolvedValueOnce({ - numberOfRecords: 2, - numberOfRecordsReturned: 2, - nextRecord: 0, - records: [recordInstance], + describe('Bad Path 😡', function () { + beforeAll(async () => { + await requestSender.createRecord({ + pathParams: { recordName: 'rec_bad_path' }, + requestBody: { + ...recordInstance, + username: validCredentials.username, + password: validCredentials.password, + }, + }); }); - const response = await requestSender.getRecords(); - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.OK); - const body = response.body as { numberOfRecords: number; numberOfRecordsReturned: number; nextRecord: number }; - expect(body.numberOfRecords).toBe(2); - expect(body.numberOfRecordsReturned).toBe(2); - expect(body.nextRecord).toBe(0); - }); + it('should return 400 when recordName is duplicated', async function () { + const response = await requestSender.createRecord({ + pathParams: { recordName: 'rec_bad_path' }, + requestBody: { + ...recordInstance, + username: validCredentials.username, + password: validCredentials.password, + }, + }); - it('should return 200 with pagination parameters', async function () { - jest.spyOn(RecordsManager.prototype, 'getRecords').mockResolvedValueOnce({ - numberOfRecords: 50, - numberOfRecordsReturned: 10, - nextRecord: 11, - records: [recordInstance], - }); - const response = await requestSender.getRecords({ - queryParams: { startPosition: 1, maxRecords: 10 }, + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + expect(response.body).toEqual({ + isValid: false, + message: `Record 'rec_bad_path' already exists`, + code: 'RECORD_NAME_ALREADY_EXIST', + }); }); - - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.OK); - const body = response.body as { - numberOfRecords: number; - numberOfRecordsReturned: number; - nextRecord: number; - records: (typeof recordInstance)[]; - }; - expect(body.numberOfRecords).toBe(50); - expect(body.numberOfRecordsReturned).toBe(10); - expect(body.nextRecord).toBe(11); - expect(Array.isArray(body.records)).toBe(true); }); it('should return 200 when credentials are valid', async function () { @@ -211,6 +154,127 @@ describe('records', function () { }); }); + describe('Get /record/{recordName}', function () { + describe('Happy Path 🙂', function () { + beforeAll(async () => { + await requestSender.createRecord({ + pathParams: { recordName: 'rec_happy_path' }, + requestBody: { + ...recordInstance, + username: validCredentials.username, + password: validCredentials.password, + }, + }); + }); + + it('should get record and return 200 when recordName is valid', async function () { + const response = await requestSender.getRecord({ + pathParams: { recordName: 'rec_happy_path' }, + }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + + expect(response.body).toMatchObject({ + recordName: 'rec_happy_path', + authorizedBy: recordInstance.authorizedBy, + data: recordInstance.data, + }); + }); + }); + }); + + describe('Get /records', function () { + describe('Happy Path 🙂', function () { + beforeAll(async () => { + await requestSender.createRecord({ + pathParams: { recordName: 'rec_happy_path' }, + requestBody: { + ...recordInstance, + username: validCredentials.username, + password: validCredentials.password, + }, + }); + }); + + it('should return 200 and the available records', async function () { + jest.spyOn(RecordsManager.prototype, 'getRecords').mockResolvedValueOnce({ + numberOfRecords: 1, + numberOfRecordsReturned: 1, + nextRecord: 0, + records: [recordInstance], + }); + const response = await requestSender.getRecords(); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + const body = response.body as { numberOfRecords: number; numberOfRecordsReturned: number; nextRecord: number }; + expect(body.numberOfRecords).toBe(1); + expect(body.numberOfRecordsReturned).toBe(1); + expect(body.nextRecord).toBe(0); + }); + + it('should return 200 and empty array when no records exist', async function () { + jest.spyOn(RecordsManager.prototype, 'getRecords').mockResolvedValueOnce({ + numberOfRecords: 0, + numberOfRecordsReturned: 0, + nextRecord: 0, + records: [], + }); + const response = await requestSender.getRecords(); + + expect(response.status).toBe(httpStatusCodes.OK); + const body = response.body as { numberOfRecords: number; numberOfRecordsReturned: number; nextRecord: number; records: IExtractableRecord[] }; + expect(body.numberOfRecords).toBe(0); + expect(body.numberOfRecordsReturned).toBe(0); + expect(body.nextRecord).toBe(0); + expect(body.records).toEqual([]); + }); + + it('should return 200 with default pagination parameters when not provided', async function () { + jest.spyOn(RecordsManager.prototype, 'getRecords').mockResolvedValueOnce({ + numberOfRecords: 2, + numberOfRecordsReturned: 2, + nextRecord: 0, + records: [recordInstance], + }); + const response = await requestSender.getRecords(); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + const body = response.body as { numberOfRecords: number; numberOfRecordsReturned: number; nextRecord: number }; + expect(body.numberOfRecords).toBe(2); + expect(body.numberOfRecordsReturned).toBe(2); + expect(body.nextRecord).toBe(0); + }); + + it('should return 200 with pagination parameters', async function () { + jest.spyOn(RecordsManager.prototype, 'getRecords').mockResolvedValueOnce({ + numberOfRecords: 50, + numberOfRecordsReturned: 10, + nextRecord: 11, + records: [recordInstance], + }); + const response = await requestSender.getRecords({ + queryParams: { startPosition: 1, maxRecords: 10 }, + }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + const body = response.body as { + numberOfRecords: number; + numberOfRecordsReturned: number; + nextRecord: number; + records: (typeof recordInstance)[]; + }; + expect(body.numberOfRecords).toBe(50); + expect(body.numberOfRecordsReturned).toBe(10); + expect(body.nextRecord).toBe(11); + expect(Array.isArray(body.records)).toBe(true); + }); + }); + }); + describe('Bad Path - Validation Failures', function () { it('should return 400 if startPosition is invalid', async () => { const response = await requestSender.getRecords({ @@ -254,50 +318,6 @@ describe('records', function () { }, }); }); - it('should return 404 when recordName is invalid', async function () { - const response = await requestSender.createRecord({ - pathParams: { recordName: 'rec_bad_path' }, - requestBody: { - ...recordInstance, - username: validCredentials.username, - password: validCredentials.password, - }, - }); - - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); - }); - - describe('Duplicate Creation', function () { - beforeAll(async () => { - await requestSender.createRecord({ - pathParams: { recordName: validCredentials.recordName }, - requestBody: { - username: validCredentials.username, - password: validCredentials.password, - authorizedBy: recordInstance.authorizedBy, - }, - }); - }); - - it('should return 400 when createRecord throws "Record not found"', async () => { - const response = await requestSender.createRecord({ - pathParams: { recordName: validCredentials.recordName }, - requestBody: { - username: validCredentials.username, - password: validCredentials.password, - authorizedBy: recordInstance.authorizedBy, - }, - }); - - expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); - expect(response.body).toEqual({ - isValid: false, - message: `Record 'rec_name' already exists`, - code: 'RECORD_NAME_ALREADY_EXIST', - }); - }); - }); it('should return 400 when credentials are missing in validateCreate', async () => { jest.spyOn(ValidationsManager.prototype, 'validateCreate').mockResolvedValueOnce({