diff --git a/index.js b/index.js index 734618d7..071e0d08 100644 --- a/index.js +++ b/index.js @@ -3,5 +3,6 @@ * @namespace api */ export { default as AbstractApiModule } from './lib/AbstractApiModule.js' -export { default as AbstractApiUtils } from './lib/AbstractApiUtils.js' export { default } from './lib/AbstractApiModule.js' +/** @deprecated Use named import { stringifyValues } from 'adapt-authoring-core' instead */ +export { stringifyValues } from 'adapt-authoring-core' diff --git a/lib/AbstractApiModule.js b/lib/AbstractApiModule.js index c2bb994f..f1262745 100644 --- a/lib/AbstractApiModule.js +++ b/lib/AbstractApiModule.js @@ -1,6 +1,6 @@ import _ from 'lodash' -import { AbstractModule, Hook } from 'adapt-authoring-core' -import ApiUtils from './AbstractApiUtils.js' +import { AbstractModule, Hook, stringifyValues } from 'adapt-authoring-core' +import { argsFromReq, generateApiMetadata, httpMethodToDBFunction } from './utils.js' import DataCache from './DataCache.js' /** * Abstract module for creating APIs @@ -177,7 +177,14 @@ class AbstractApiModule extends AbstractModule { return this.log('error', 'Must set API root before calling useDefaultConfig function') } /** @ignore */ this.routes = this.DEFAULT_ROUTES - ApiUtils.generateApiMetadata(this) + this.generateApiMetadata() + } + + /** + * Generates REST API metadata and stores on route config + */ + generateApiMetadata () { + generateApiMetadata(this) } /** @@ -349,7 +356,7 @@ class AbstractApiModule extends AbstractModule { requestHandler () { const requestHandler = async (req, res, next) => { const method = req.method.toLowerCase() - const func = this[ApiUtils.httpMethodToDBFunction(method)] + const func = this[httpMethodToDBFunction(method)] if (!func) { return next(this.app.errors.HTTP_METHOD_NOT_SUPPORTED.setData({ method })) } @@ -361,7 +368,7 @@ class AbstractApiModule extends AbstractModule { if (preCheck) { await this.checkAccess(req, req.apiData.query) } - data = await func.apply(this, ApiUtils.argsFromReq(req)) + data = await func.apply(this, argsFromReq(req)) if (postCheck) { data = await this.checkAccess(req, data) } @@ -654,7 +661,7 @@ class AbstractApiModule extends AbstractModule { if (options.invokePreHook !== false) await this.preUpdateHook.invoke(originalDoc, formattedData.$set, options, mongoOptions) formattedData.$set = await this.validate(options.schemaName, { - ...ApiUtils.stringifyValues(originalDoc), + ...stringifyValues(originalDoc), ...formattedData.$set }, options) diff --git a/lib/AbstractApiUtils.js b/lib/AbstractApiUtils.js deleted file mode 100644 index d107ed33..00000000 --- a/lib/AbstractApiUtils.js +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Utilities for APIs - * @memberof api - */ -class AbstractApiUtils { - /** - * Converts HTTP methods to a corresponding 'action' for use in auth - * @param {String} method The HTTP method - * @return {String} - */ - static httpMethodToAction (method) { - switch (method.toLowerCase()) { - case 'get': - return 'read' - case 'post': - case 'put': - case 'patch': - case 'delete': - return 'write' - default: - return '' - } - } - - /** - * Converts HTTP methods to a corresponding database function - * @param {String} method The HTTP method - * @return {String} - */ - static httpMethodToDBFunction (method) { - switch (method.toLowerCase()) { - case 'post': return 'insert' - case 'get': return 'find' - case 'put': case 'patch': return 'update' - case 'delete': return 'delete' - default: return '' - } - } - - /** - * Generates a list of arguments to be passed to the MongoDBModule from a request object - * @param {external:ExpressRequest} req - * @return {Array<*>} - */ - static argsFromReq (req) { - const opts = { schemaName: req.apiData.schemaName, collectionName: req.apiData.collectionName } - switch (req.method) { - case 'GET': case 'DELETE': - return [req.apiData.query, opts] - case 'POST': - return [req.apiData.data, opts] - case 'PUT': case 'PATCH': - return [req.apiData.query, req.apiData.data, opts] - } - } - - /** - * Generates REST API metadata and stores on route config - * @param {AbstractApiModule} instance The current AbstractApiModule instance - */ - static generateApiMetadata (instance) { - const getData = isList => { - const $ref = { $ref: `#/components/schemas/${instance.schemaName}` } - return { - description: `The ${instance.schemaName} data`, - content: { 'application/json': { schema: isList ? { type: 'array', items: $ref } : $ref } } - } - } - const queryParams = [ - { - name: 'limit', - in: 'query', - description: `How many results should be returned Default value is ${instance.app.config.get('adapt-authoring-api.defaultPageSize')} (max value is ${instance.app.config.get('adapt-authoring-api.maxPageSize')})` - }, - { - name: 'page', - in: 'query', - description: 'The page of results to return (determined from the limit value)' - } - ] - const verbMap = { - put: 'Replace', - get: 'Retrieve', - patch: 'Update', - delete: 'Delete', - post: 'Insert' - } - instance.routes.forEach(r => { - r.meta = {} - Object.keys(r.handlers).forEach(method => { - let summary, parameters, requestBody, responses - switch (r.route) { - case '/': - if (method === 'post') { - summary = `${verbMap.post} a new ${instance.schemaName} document` - requestBody = getData() - responses = { 201: getData() } - } else { - summary = `${verbMap.get} all ${instance.collectionName} documents` - parameters = queryParams - responses = { 200: getData(true) } - } - break - - case '/:_id': - summary = `${verbMap[method]} an existing ${instance.schemaName} document` - requestBody = method === 'put' || method === 'patch' ? getData() : method === 'delete' ? undefined : {} - responses = { [method === 'delete' ? 204 : 200]: getData() } - break - - case '/query': - summary = `Query all ${instance.collectionName}` - parameters = queryParams - responses = { 200: getData(true) } - break - - case '/schema': - summary = `Retrieve ${instance.schemaName} schema` - break - } - r.meta[method] = { summary, parameters, requestBody, responses } - }) - }) - } - - /** - * Clones an object and converts any Dates and ObjectIds to Strings - * @param {Object} data - * @returns A clone object with stringified ObjectIds - */ - static stringifyValues (data) { - return Object.entries(data).reduce((cloned, [key, val]) => { - const type = val?.constructor?.name - cloned[key] = - type === 'Date' || type === 'ObjectId' - ? val.toString() - : type === 'Array' || type === 'Object' - ? this.stringifyValues(val) - : val - return cloned - }, Array.isArray(data) ? [] : {}) - } -} - -export default AbstractApiUtils diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..70f4af46 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,4 @@ +export { argsFromReq } from './utils/argsFromReq.js' +export { generateApiMetadata } from './utils/generateApiMetadata.js' +export { httpMethodToAction } from './utils/httpMethodToAction.js' +export { httpMethodToDBFunction } from './utils/httpMethodToDBFunction.js' diff --git a/lib/utils/argsFromReq.js b/lib/utils/argsFromReq.js new file mode 100644 index 00000000..26ba57c2 --- /dev/null +++ b/lib/utils/argsFromReq.js @@ -0,0 +1,17 @@ +/** + * Generates a list of arguments to be passed to the MongoDBModule from a request object + * @param {external:ExpressRequest} req + * @return {Array<*>} + * @memberof api + */ +export function argsFromReq (req) { + const opts = { schemaName: req.apiData.schemaName, collectionName: req.apiData.collectionName } + switch (req.method) { + case 'GET': case 'DELETE': + return [req.apiData.query, opts] + case 'POST': + return [req.apiData.data, opts] + case 'PUT': case 'PATCH': + return [req.apiData.query, req.apiData.data, opts] + } +} diff --git a/lib/utils/generateApiMetadata.js b/lib/utils/generateApiMetadata.js new file mode 100644 index 00000000..51470c87 --- /dev/null +++ b/lib/utils/generateApiMetadata.js @@ -0,0 +1,69 @@ +/** + * Generates REST API metadata and stores on route config + * @param {AbstractApiModule} instance The current AbstractApiModule instance + * @memberof api + */ +export function generateApiMetadata (instance) { + const getData = isList => { + const $ref = { $ref: `#/components/schemas/${instance.schemaName}` } + return { + description: `The ${instance.schemaName} data`, + content: { 'application/json': { schema: isList ? { type: 'array', items: $ref } : $ref } } + } + } + const queryParams = [ + { + name: 'limit', + in: 'query', + description: `How many results should be returned Default value is ${instance.app.config.get('adapt-authoring-api.defaultPageSize')} (max value is ${instance.app.config.get('adapt-authoring-api.maxPageSize')})` + }, + { + name: 'page', + in: 'query', + description: 'The page of results to return (determined from the limit value)' + } + ] + const verbMap = { + put: 'Replace', + get: 'Retrieve', + patch: 'Update', + delete: 'Delete', + post: 'Insert' + } + instance.routes.forEach(r => { + r.meta = {} + Object.keys(r.handlers).forEach(method => { + let summary, parameters, requestBody, responses + switch (r.route) { + case '/': + if (method === 'post') { + summary = `${verbMap.post} a new ${instance.schemaName} document` + requestBody = getData() + responses = { 201: getData() } + } else { + summary = `${verbMap.get} all ${instance.collectionName} documents` + parameters = queryParams + responses = { 200: getData(true) } + } + break + + case '/:_id': + summary = `${verbMap[method]} an existing ${instance.schemaName} document` + requestBody = method === 'put' || method === 'patch' ? getData() : method === 'delete' ? undefined : {} + responses = { [method === 'delete' ? 204 : 200]: getData() } + break + + case '/query': + summary = `Query all ${instance.collectionName}` + parameters = queryParams + responses = { 200: getData(true) } + break + + case '/schema': + summary = `Retrieve ${instance.schemaName} schema` + break + } + r.meta[method] = { summary, parameters, requestBody, responses } + }) + }) +} diff --git a/lib/utils/httpMethodToAction.js b/lib/utils/httpMethodToAction.js new file mode 100644 index 00000000..dc45b26b --- /dev/null +++ b/lib/utils/httpMethodToAction.js @@ -0,0 +1,19 @@ +/** + * Converts HTTP methods to a corresponding 'action' for use in auth + * @param {String} method The HTTP method + * @return {String} + * @memberof api + */ +export function httpMethodToAction (method) { + switch (method.toLowerCase()) { + case 'get': + return 'read' + case 'post': + case 'put': + case 'patch': + case 'delete': + return 'write' + default: + return '' + } +} diff --git a/lib/utils/httpMethodToDBFunction.js b/lib/utils/httpMethodToDBFunction.js new file mode 100644 index 00000000..38bfc576 --- /dev/null +++ b/lib/utils/httpMethodToDBFunction.js @@ -0,0 +1,15 @@ +/** + * Converts HTTP methods to a corresponding database function + * @param {String} method The HTTP method + * @return {String} + * @memberof api + */ +export function httpMethodToDBFunction (method) { + switch (method.toLowerCase()) { + case 'post': return 'insert' + case 'get': return 'find' + case 'put': case 'patch': return 'update' + case 'delete': return 'delete' + default: return '' + } +} diff --git a/package-lock.json b/package-lock.json index b96521e8..d87af75e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.3.2", "license": "GPL-3.0", "dependencies": { - "adapt-authoring-core": "^1.7.0", + "adapt-authoring-core": "^2.0.0", "lodash": "^4.17.21" }, "devDependencies": { @@ -1156,9 +1156,9 @@ } }, "node_modules/adapt-authoring-core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/adapt-authoring-core/-/adapt-authoring-core-1.7.0.tgz", - "integrity": "sha512-Lh35JIKpzCsJKc6mmCvC1tJABvup76lulK+eKJRxi3+AhFr/apvN5DW4my/nbmG2YF3R5ig1t/wegS5eRn3/Aw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/adapt-authoring-core/-/adapt-authoring-core-2.0.0.tgz", + "integrity": "sha512-q+XVpSzo3PI+mfKyem2jaOtHGUNq+M09348Qd3y9+ut58bQ3PaAM9CzGQV+s43cgmfCBCPe5bdkmo8jZ/9iZSA==", "license": "GPL-3.0", "dependencies": { "fs-extra": "11.3.3", diff --git a/package.json b/package.json index 2e70c002..09f2af89 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "test": "node --test 'tests/**/*.spec.js'" }, "dependencies": { - "adapt-authoring-core": "^1.7.0", + "adapt-authoring-core": "^2.0.0", "lodash": "^4.17.21" }, "peerDependencies": { diff --git a/tests/AbstractApiUtils.spec.js b/tests/AbstractApiUtils.spec.js deleted file mode 100644 index 9587d134..00000000 --- a/tests/AbstractApiUtils.spec.js +++ /dev/null @@ -1,139 +0,0 @@ -import { describe, it } from 'node:test' -import assert from 'node:assert/strict' -import AbstractApiUtils from '../lib/AbstractApiUtils.js' - -describe('AbstractApiUtils', () => { - describe('.httpMethodToAction()', () => { - it('should return "read" for GET', () => { - assert.equal(AbstractApiUtils.httpMethodToAction('get'), 'read') - assert.equal(AbstractApiUtils.httpMethodToAction('GET'), 'read') - }) - - const writeMethods = ['post', 'put', 'patch', 'delete'] - writeMethods.forEach(method => { - it(`should return "write" for ${method.toUpperCase()}`, () => { - assert.equal(AbstractApiUtils.httpMethodToAction(method), 'write') - assert.equal(AbstractApiUtils.httpMethodToAction(method.toUpperCase()), 'write') - }) - }) - - it('should return empty string for unknown methods', () => { - assert.equal(AbstractApiUtils.httpMethodToAction('options'), '') - assert.equal(AbstractApiUtils.httpMethodToAction('head'), '') - }) - }) - - describe('.httpMethodToDBFunction()', () => { - const cases = [ - { method: 'post', expected: 'insert' }, - { method: 'get', expected: 'find' }, - { method: 'put', expected: 'update' }, - { method: 'patch', expected: 'update' }, - { method: 'delete', expected: 'delete' } - ] - cases.forEach(({ method, expected }) => { - it(`should return "${expected}" for ${method.toUpperCase()}`, () => { - assert.equal(AbstractApiUtils.httpMethodToDBFunction(method), expected) - }) - }) - - it('should be case-insensitive', () => { - assert.equal(AbstractApiUtils.httpMethodToDBFunction('POST'), 'insert') - assert.equal(AbstractApiUtils.httpMethodToDBFunction('Get'), 'find') - }) - - it('should return empty string for unknown methods', () => { - assert.equal(AbstractApiUtils.httpMethodToDBFunction('options'), '') - }) - }) - - describe('.argsFromReq()', () => { - const baseApiData = { - query: { _id: '123' }, - data: { name: 'test' }, - schemaName: 'testSchema', - collectionName: 'testCollection' - } - - it('should return [query, opts] for GET', () => { - const req = { method: 'GET', apiData: baseApiData } - const result = AbstractApiUtils.argsFromReq(req) - assert.deepEqual(result[0], baseApiData.query) - assert.deepEqual(result[1], { schemaName: 'testSchema', collectionName: 'testCollection' }) - assert.equal(result.length, 2) - }) - - it('should return [query, opts] for DELETE', () => { - const req = { method: 'DELETE', apiData: baseApiData } - const result = AbstractApiUtils.argsFromReq(req) - assert.deepEqual(result[0], baseApiData.query) - assert.equal(result.length, 2) - }) - - it('should return [data, opts] for POST', () => { - const req = { method: 'POST', apiData: baseApiData } - const result = AbstractApiUtils.argsFromReq(req) - assert.deepEqual(result[0], baseApiData.data) - assert.equal(result.length, 2) - }) - - it('should return [query, data, opts] for PUT', () => { - const req = { method: 'PUT', apiData: baseApiData } - const result = AbstractApiUtils.argsFromReq(req) - assert.deepEqual(result[0], baseApiData.query) - assert.deepEqual(result[1], baseApiData.data) - assert.equal(result.length, 3) - }) - - it('should return [query, data, opts] for PATCH', () => { - const req = { method: 'PATCH', apiData: baseApiData } - const result = AbstractApiUtils.argsFromReq(req) - assert.deepEqual(result[0], baseApiData.query) - assert.deepEqual(result[1], baseApiData.data) - assert.equal(result.length, 3) - }) - - it('should return undefined for unknown methods', () => { - const req = { method: 'OPTIONS', apiData: baseApiData } - assert.equal(AbstractApiUtils.argsFromReq(req), undefined) - }) - }) - - describe('.stringifyValues()', () => { - it('should pass through plain values unchanged', () => { - const data = { a: 'hello', b: 42, c: true, d: null } - const result = AbstractApiUtils.stringifyValues(data) - assert.deepEqual(result, data) - }) - - it('should convert Date values to strings', () => { - const date = new Date('2025-01-01T00:00:00.000Z') - const result = AbstractApiUtils.stringifyValues({ date }) - assert.equal(typeof result.date, 'string') - assert.equal(result.date, date.toString()) - }) - - it('should recursively process nested objects', () => { - const data = { nested: { value: 'test', date: new Date('2025-01-01') } } - const result = AbstractApiUtils.stringifyValues(data) - assert.equal(typeof result.nested, 'object') - assert.equal(typeof result.nested.date, 'string') - }) - - it('should recursively process arrays', () => { - const date = new Date('2025-01-01') - const data = { items: [date, 'text', 42] } - const result = AbstractApiUtils.stringifyValues(data) - assert.ok(Array.isArray(result.items)) - assert.equal(typeof result.items[0], 'string') - assert.equal(result.items[1], 'text') - assert.equal(result.items[2], 42) - }) - - it('should return an array when input is an array', () => { - const result = AbstractApiUtils.stringifyValues([{ a: 1 }, { b: 2 }]) - assert.ok(Array.isArray(result)) - assert.equal(result.length, 2) - }) - }) -}) diff --git a/tests/utils-argsFromReq.spec.js b/tests/utils-argsFromReq.spec.js new file mode 100644 index 00000000..19322ff6 --- /dev/null +++ b/tests/utils-argsFromReq.spec.js @@ -0,0 +1,42 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { argsFromReq } from '../lib/utils/argsFromReq.js' + +describe('argsFromReq()', () => { + const baseApiData = { + query: { _id: '123' }, + data: { name: 'test' }, + schemaName: 'testSchema', + collectionName: 'testCollection' + } + const expectedOpts = { schemaName: 'testSchema', collectionName: 'testCollection' } + + it('should return [query, opts] for GET', () => { + const result = argsFromReq({ method: 'GET', apiData: baseApiData }) + assert.deepEqual(result, [baseApiData.query, expectedOpts]) + }) + + it('should return [query, opts] for DELETE', () => { + const result = argsFromReq({ method: 'DELETE', apiData: baseApiData }) + assert.deepEqual(result, [baseApiData.query, expectedOpts]) + }) + + it('should return [data, opts] for POST', () => { + const result = argsFromReq({ method: 'POST', apiData: baseApiData }) + assert.deepEqual(result, [baseApiData.data, expectedOpts]) + }) + + it('should return [query, data, opts] for PUT', () => { + const result = argsFromReq({ method: 'PUT', apiData: baseApiData }) + assert.deepEqual(result, [baseApiData.query, baseApiData.data, expectedOpts]) + }) + + it('should return [query, data, opts] for PATCH', () => { + const result = argsFromReq({ method: 'PATCH', apiData: baseApiData }) + assert.deepEqual(result, [baseApiData.query, baseApiData.data, expectedOpts]) + }) + + it('should return undefined for unknown methods', () => { + assert.equal(argsFromReq({ method: 'OPTIONS', apiData: baseApiData }), undefined) + }) +}) diff --git a/tests/utils-generateApiMetadata.spec.js b/tests/utils-generateApiMetadata.spec.js new file mode 100644 index 00000000..9e5073e6 --- /dev/null +++ b/tests/utils-generateApiMetadata.spec.js @@ -0,0 +1,113 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { generateApiMetadata } from '../lib/utils/generateApiMetadata.js' + +describe('generateApiMetadata()', () => { + function createInstance (routes) { + return { + schemaName: 'TestSchema', + collectionName: 'testcollection', + app: { + config: { + get: (key) => { + const map = { + 'adapt-authoring-api.defaultPageSize': 50, + 'adapt-authoring-api.maxPageSize': 200 + } + return map[key] + } + } + }, + routes: routes || [] + } + } + + it('should set meta on each route', () => { + const instance = createInstance([ + { route: '/', handlers: { post: () => {}, get: () => {} } } + ]) + generateApiMetadata(instance) + assert.ok(instance.routes[0].meta) + assert.ok(instance.routes[0].meta.post) + assert.ok(instance.routes[0].meta.get) + }) + + it('should generate correct summary for POST /', () => { + const instance = createInstance([ + { route: '/', handlers: { post: () => {} } } + ]) + generateApiMetadata(instance) + assert.equal(instance.routes[0].meta.post.summary, 'Insert a new TestSchema document') + }) + + it('should generate correct summary for GET /', () => { + const instance = createInstance([ + { route: '/', handlers: { get: () => {} } } + ]) + generateApiMetadata(instance) + assert.equal(instance.routes[0].meta.get.summary, 'Retrieve all testcollection documents') + }) + + it('should include query parameters for GET /', () => { + const instance = createInstance([ + { route: '/', handlers: { get: () => {} } } + ]) + generateApiMetadata(instance) + const params = instance.routes[0].meta.get.parameters + assert.ok(Array.isArray(params)) + assert.equal(params.length, 2) + assert.equal(params[0].name, 'limit') + assert.equal(params[1].name, 'page') + }) + + it('should generate correct summary for /:_id routes', () => { + const instance = createInstance([ + { route: '/:_id', handlers: { get: () => {}, put: () => {}, patch: () => {}, delete: () => {} } } + ]) + generateApiMetadata(instance) + assert.equal(instance.routes[0].meta.get.summary, 'Retrieve an existing TestSchema document') + assert.equal(instance.routes[0].meta.put.summary, 'Replace an existing TestSchema document') + assert.equal(instance.routes[0].meta.patch.summary, 'Update an existing TestSchema document') + assert.equal(instance.routes[0].meta.delete.summary, 'Delete an existing TestSchema document') + }) + + it('should set 201 response for POST and 204 for DELETE', () => { + const instance = createInstance([ + { route: '/', handlers: { post: () => {} } }, + { route: '/:_id', handlers: { delete: () => {} } } + ]) + generateApiMetadata(instance) + assert.ok(instance.routes[0].meta.post.responses[201]) + assert.ok(instance.routes[1].meta.delete.responses[204]) + }) + + it('should generate correct summary for /query route', () => { + const instance = createInstance([ + { route: '/query', handlers: { post: () => {} } } + ]) + generateApiMetadata(instance) + assert.equal(instance.routes[0].meta.post.summary, 'Query all testcollection') + }) + + it('should generate correct summary for /schema route', () => { + const instance = createInstance([ + { route: '/schema', handlers: { get: () => {} } } + ]) + generateApiMetadata(instance) + assert.equal(instance.routes[0].meta.get.summary, 'Retrieve TestSchema schema') + }) + + it('should handle multiple routes', () => { + const instance = createInstance([ + { route: '/', handlers: { post: () => {}, get: () => {} } }, + { route: '/:_id', handlers: { get: () => {}, delete: () => {} } }, + { route: '/query', handlers: { post: () => {} } }, + { route: '/schema', handlers: { get: () => {} } } + ]) + generateApiMetadata(instance) + assert.equal(Object.keys(instance.routes[0].meta).length, 2) + assert.equal(Object.keys(instance.routes[1].meta).length, 2) + assert.equal(Object.keys(instance.routes[2].meta).length, 1) + assert.equal(Object.keys(instance.routes[3].meta).length, 1) + }) +}) diff --git a/tests/utils-httpMethodToAction.spec.js b/tests/utils-httpMethodToAction.spec.js new file mode 100644 index 00000000..89756bc1 --- /dev/null +++ b/tests/utils-httpMethodToAction.spec.js @@ -0,0 +1,27 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { httpMethodToAction } from '../lib/utils/httpMethodToAction.js' + +describe('httpMethodToAction()', () => { + it('should return "read" for GET', () => { + assert.equal(httpMethodToAction('get'), 'read') + }) + + it('should be case-insensitive', () => { + assert.equal(httpMethodToAction('GET'), 'read') + assert.equal(httpMethodToAction('Get'), 'read') + }) + + const writeMethods = ['post', 'put', 'patch', 'delete'] + writeMethods.forEach(method => { + it(`should return "write" for ${method.toUpperCase()}`, () => { + assert.equal(httpMethodToAction(method), 'write') + assert.equal(httpMethodToAction(method.toUpperCase()), 'write') + }) + }) + + it('should return empty string for unknown methods', () => { + assert.equal(httpMethodToAction('options'), '') + assert.equal(httpMethodToAction('head'), '') + }) +}) diff --git a/tests/utils-httpMethodToDBFunction.spec.js b/tests/utils-httpMethodToDBFunction.spec.js new file mode 100644 index 00000000..5547acf1 --- /dev/null +++ b/tests/utils-httpMethodToDBFunction.spec.js @@ -0,0 +1,30 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { httpMethodToDBFunction } from '../lib/utils/httpMethodToDBFunction.js' + +describe('httpMethodToDBFunction()', () => { + const cases = [ + { method: 'post', expected: 'insert' }, + { method: 'get', expected: 'find' }, + { method: 'put', expected: 'update' }, + { method: 'patch', expected: 'update' }, + { method: 'delete', expected: 'delete' } + ] + + cases.forEach(({ method, expected }) => { + it(`should return "${expected}" for ${method.toUpperCase()}`, () => { + assert.equal(httpMethodToDBFunction(method), expected) + }) + }) + + it('should be case-insensitive', () => { + assert.equal(httpMethodToDBFunction('POST'), 'insert') + assert.equal(httpMethodToDBFunction('Get'), 'find') + assert.equal(httpMethodToDBFunction('DELETE'), 'delete') + }) + + it('should return empty string for unknown methods', () => { + assert.equal(httpMethodToDBFunction('options'), '') + assert.equal(httpMethodToDBFunction('head'), '') + }) +})