adapt-authoring-api 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ name: Tests
2
+ on: push
3
+ jobs:
4
+ default:
5
+ runs-on: ubuntu-latest
6
+ permissions:
7
+ contents: read
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: actions/setup-node@v4
11
+ with:
12
+ node-version: 'lts/*'
13
+ cache: 'npm'
14
+ - run: npm ci
15
+ - run: npm test
@@ -497,8 +497,8 @@ class AbstractApiModule extends AbstractModule {
497
497
  * @param {Object} mongoOpts The MongoDB options
498
498
  */
499
499
  async setUpPagination (req, res, mongoOpts) {
500
- const maxPageSize = this.app.config.get('adapt-authoring-api.maxPageSize')
501
- let pageSize = mongoOpts.limit ?? this.app.config.get('adapt-authoring-api.defaultPageSize')
500
+ const maxPageSize = this.getConfig('maxPageSize') ?? this.app.config.get('adapt-authoring-api.maxPageSize')
501
+ let pageSize = mongoOpts.limit ?? this.getConfig('defaultPageSize') ?? this.app.config.get('adapt-authoring-api.defaultPageSize')
502
502
 
503
503
  if (pageSize > maxPageSize) pageSize = maxPageSize
504
504
 
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "adapt-authoring-api",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Abstract module for creating APIs",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-api",
6
6
  "license": "GPL-3.0",
7
7
  "type": "module",
8
8
  "main": "index.js",
9
9
  "repository": "github:adapt-security/adapt-authoring-api",
10
+ "scripts": {
11
+ "test": "node --test 'tests/**/*.spec.js'"
12
+ },
10
13
  "dependencies": {
11
14
  "adapt-authoring-core": "^1.7.0",
12
15
  "lodash": "^4.17.21"
@@ -0,0 +1,59 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import AbstractApiModule from '../lib/AbstractApiModule.js'
4
+
5
+ describe('AbstractApiModule', () => {
6
+ describe('#mapStatusCode()', () => {
7
+ const instance = Object.create(AbstractApiModule.prototype)
8
+
9
+ const cases = [
10
+ { method: 'post', expected: 201 },
11
+ { method: 'get', expected: 200 },
12
+ { method: 'put', expected: 200 },
13
+ { method: 'patch', expected: 200 },
14
+ { method: 'delete', expected: 204 }
15
+ ]
16
+ cases.forEach(({ method, expected }) => {
17
+ it(`should return ${expected} for ${method.toUpperCase()}`, () => {
18
+ assert.equal(instance.mapStatusCode(method), expected)
19
+ })
20
+ })
21
+
22
+ it('should return undefined for unknown methods', () => {
23
+ assert.equal(instance.mapStatusCode('options'), undefined)
24
+ })
25
+ })
26
+
27
+ describe('#setDefaultOptions()', () => {
28
+ it('should populate defaults on an empty options object', () => {
29
+ const instance = Object.create(AbstractApiModule.prototype)
30
+ instance.schemaName = 'testSchema'
31
+ instance.collectionName = 'testCollection'
32
+ const options = {}
33
+ instance.setDefaultOptions(options)
34
+ assert.equal(options.schemaName, 'testSchema')
35
+ assert.equal(options.collectionName, 'testCollection')
36
+ assert.equal(options.validate, true)
37
+ assert.equal(options.invokePreHook, true)
38
+ assert.equal(options.invokePostHook, true)
39
+ })
40
+
41
+ it('should not override existing values', () => {
42
+ const instance = Object.create(AbstractApiModule.prototype)
43
+ instance.schemaName = 'testSchema'
44
+ instance.collectionName = 'testCollection'
45
+ const options = { schemaName: 'customSchema', validate: false }
46
+ instance.setDefaultOptions(options)
47
+ assert.equal(options.schemaName, 'customSchema')
48
+ assert.equal(options.validate, false)
49
+ })
50
+
51
+ it('should handle undefined options by creating defaults', () => {
52
+ const instance = Object.create(AbstractApiModule.prototype)
53
+ instance.schemaName = 'testSchema'
54
+ instance.collectionName = 'testCollection'
55
+ const options = instance.setDefaultOptions()
56
+ assert.equal(options, undefined)
57
+ })
58
+ })
59
+ })
@@ -0,0 +1,139 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import AbstractApiUtils from '../lib/AbstractApiUtils.js'
4
+
5
+ describe('AbstractApiUtils', () => {
6
+ describe('.httpMethodToAction()', () => {
7
+ it('should return "read" for GET', () => {
8
+ assert.equal(AbstractApiUtils.httpMethodToAction('get'), 'read')
9
+ assert.equal(AbstractApiUtils.httpMethodToAction('GET'), 'read')
10
+ })
11
+
12
+ const writeMethods = ['post', 'put', 'patch', 'delete']
13
+ writeMethods.forEach(method => {
14
+ it(`should return "write" for ${method.toUpperCase()}`, () => {
15
+ assert.equal(AbstractApiUtils.httpMethodToAction(method), 'write')
16
+ assert.equal(AbstractApiUtils.httpMethodToAction(method.toUpperCase()), 'write')
17
+ })
18
+ })
19
+
20
+ it('should return empty string for unknown methods', () => {
21
+ assert.equal(AbstractApiUtils.httpMethodToAction('options'), '')
22
+ assert.equal(AbstractApiUtils.httpMethodToAction('head'), '')
23
+ })
24
+ })
25
+
26
+ describe('.httpMethodToDBFunction()', () => {
27
+ const cases = [
28
+ { method: 'post', expected: 'insert' },
29
+ { method: 'get', expected: 'find' },
30
+ { method: 'put', expected: 'update' },
31
+ { method: 'patch', expected: 'update' },
32
+ { method: 'delete', expected: 'delete' }
33
+ ]
34
+ cases.forEach(({ method, expected }) => {
35
+ it(`should return "${expected}" for ${method.toUpperCase()}`, () => {
36
+ assert.equal(AbstractApiUtils.httpMethodToDBFunction(method), expected)
37
+ })
38
+ })
39
+
40
+ it('should be case-insensitive', () => {
41
+ assert.equal(AbstractApiUtils.httpMethodToDBFunction('POST'), 'insert')
42
+ assert.equal(AbstractApiUtils.httpMethodToDBFunction('Get'), 'find')
43
+ })
44
+
45
+ it('should return empty string for unknown methods', () => {
46
+ assert.equal(AbstractApiUtils.httpMethodToDBFunction('options'), '')
47
+ })
48
+ })
49
+
50
+ describe('.argsFromReq()', () => {
51
+ const baseApiData = {
52
+ query: { _id: '123' },
53
+ data: { name: 'test' },
54
+ schemaName: 'testSchema',
55
+ collectionName: 'testCollection'
56
+ }
57
+
58
+ it('should return [query, opts] for GET', () => {
59
+ const req = { method: 'GET', apiData: baseApiData }
60
+ const result = AbstractApiUtils.argsFromReq(req)
61
+ assert.deepEqual(result[0], baseApiData.query)
62
+ assert.deepEqual(result[1], { schemaName: 'testSchema', collectionName: 'testCollection' })
63
+ assert.equal(result.length, 2)
64
+ })
65
+
66
+ it('should return [query, opts] for DELETE', () => {
67
+ const req = { method: 'DELETE', apiData: baseApiData }
68
+ const result = AbstractApiUtils.argsFromReq(req)
69
+ assert.deepEqual(result[0], baseApiData.query)
70
+ assert.equal(result.length, 2)
71
+ })
72
+
73
+ it('should return [data, opts] for POST', () => {
74
+ const req = { method: 'POST', apiData: baseApiData }
75
+ const result = AbstractApiUtils.argsFromReq(req)
76
+ assert.deepEqual(result[0], baseApiData.data)
77
+ assert.equal(result.length, 2)
78
+ })
79
+
80
+ it('should return [query, data, opts] for PUT', () => {
81
+ const req = { method: 'PUT', apiData: baseApiData }
82
+ const result = AbstractApiUtils.argsFromReq(req)
83
+ assert.deepEqual(result[0], baseApiData.query)
84
+ assert.deepEqual(result[1], baseApiData.data)
85
+ assert.equal(result.length, 3)
86
+ })
87
+
88
+ it('should return [query, data, opts] for PATCH', () => {
89
+ const req = { method: 'PATCH', apiData: baseApiData }
90
+ const result = AbstractApiUtils.argsFromReq(req)
91
+ assert.deepEqual(result[0], baseApiData.query)
92
+ assert.deepEqual(result[1], baseApiData.data)
93
+ assert.equal(result.length, 3)
94
+ })
95
+
96
+ it('should return undefined for unknown methods', () => {
97
+ const req = { method: 'OPTIONS', apiData: baseApiData }
98
+ assert.equal(AbstractApiUtils.argsFromReq(req), undefined)
99
+ })
100
+ })
101
+
102
+ describe('.stringifyValues()', () => {
103
+ it('should pass through plain values unchanged', () => {
104
+ const data = { a: 'hello', b: 42, c: true, d: null }
105
+ const result = AbstractApiUtils.stringifyValues(data)
106
+ assert.deepEqual(result, data)
107
+ })
108
+
109
+ it('should convert Date values to strings', () => {
110
+ const date = new Date('2025-01-01T00:00:00.000Z')
111
+ const result = AbstractApiUtils.stringifyValues({ date })
112
+ assert.equal(typeof result.date, 'string')
113
+ assert.equal(result.date, date.toString())
114
+ })
115
+
116
+ it('should recursively process nested objects', () => {
117
+ const data = { nested: { value: 'test', date: new Date('2025-01-01') } }
118
+ const result = AbstractApiUtils.stringifyValues(data)
119
+ assert.equal(typeof result.nested, 'object')
120
+ assert.equal(typeof result.nested.date, 'string')
121
+ })
122
+
123
+ it('should recursively process arrays', () => {
124
+ const date = new Date('2025-01-01')
125
+ const data = { items: [date, 'text', 42] }
126
+ const result = AbstractApiUtils.stringifyValues(data)
127
+ assert.ok(Array.isArray(result.items))
128
+ assert.equal(typeof result.items[0], 'string')
129
+ assert.equal(result.items[1], 'text')
130
+ assert.equal(result.items[2], 42)
131
+ })
132
+
133
+ it('should return an array when input is an array', () => {
134
+ const result = AbstractApiUtils.stringifyValues([{ a: 1 }, { b: 2 }])
135
+ assert.ok(Array.isArray(result))
136
+ assert.equal(result.length, 2)
137
+ })
138
+ })
139
+ })
@@ -0,0 +1,47 @@
1
+ import { describe, it, before } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ describe('DataCache', () => {
5
+ let DataCache
6
+
7
+ before(async () => {
8
+ // DataCache constructor references App.instance.config, so we need to
9
+ // dynamically import after ensuring no app instance is required for prune tests.
10
+ // We import the module directly and test what we can without the full app.
11
+ DataCache = (await import('../lib/DataCache.js')).default
12
+ })
13
+
14
+ describe('#prune()', () => {
15
+ it('should remove expired entries from the cache', () => {
16
+ const instance = Object.create(DataCache.prototype)
17
+ instance.lifespan = 100
18
+ instance.cache = {
19
+ expired: { data: [1], timestamp: Date.now() - 200 },
20
+ valid: { data: [2], timestamp: Date.now() }
21
+ }
22
+ instance.prune()
23
+ assert.equal(instance.cache.expired, undefined)
24
+ assert.ok(instance.cache.valid)
25
+ })
26
+
27
+ it('should keep entries that have not expired', () => {
28
+ const instance = Object.create(DataCache.prototype)
29
+ instance.lifespan = 10000
30
+ instance.cache = {
31
+ a: { data: [1], timestamp: Date.now() },
32
+ b: { data: [2], timestamp: Date.now() }
33
+ }
34
+ instance.prune()
35
+ assert.ok(instance.cache.a)
36
+ assert.ok(instance.cache.b)
37
+ })
38
+
39
+ it('should handle an empty cache', () => {
40
+ const instance = Object.create(DataCache.prototype)
41
+ instance.lifespan = 100
42
+ instance.cache = {}
43
+ instance.prune()
44
+ assert.deepEqual(instance.cache, {})
45
+ })
46
+ })
47
+ })