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
|
package/lib/AbstractApiModule.js
CHANGED
|
@@ -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.
|
|
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
|
+
})
|