adapt-authoring-tags 1.0.3 → 1.1.1
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,14 @@
|
|
|
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
|
+
- run: npm install --legacy-peer-deps
|
|
14
|
+
- run: npm test
|
package/package.json
CHANGED
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-tags",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Module for managing tags",
|
|
5
5
|
"homepage": "https://github.com/adapt-security/adapt-authoring-tags",
|
|
6
6
|
"license": "GPL-3.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"repository": "github:adapt-security/adapt-authoring-tags",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"adapt-authoring-jsonschema": "^1.2.0",
|
|
13
|
-
"adapt-authoring-mongodb": "^1.1.3"
|
|
14
|
-
},
|
|
15
|
-
"peerDependenciesMeta": {
|
|
16
|
-
"adapt-authoring-core": {
|
|
17
|
-
"optional": true
|
|
18
|
-
},
|
|
19
|
-
"adapt-authoring-jsonschema": {
|
|
20
|
-
"optional": true
|
|
21
|
-
},
|
|
22
|
-
"adapt-authoring-mongodb": {
|
|
23
|
-
"optional": true
|
|
24
|
-
}
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test 'tests/**/*.spec.js'"
|
|
25
12
|
},
|
|
26
13
|
"devDependencies": {
|
|
27
14
|
"@semantic-release/git": "^10.0.1",
|
|
@@ -55,5 +42,24 @@
|
|
|
55
42
|
}
|
|
56
43
|
]
|
|
57
44
|
]
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"adapt-authoring-api": "^2.0.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"adapt-authoring-core": "^2.0.0",
|
|
51
|
+
"adapt-authoring-jsonschema": "^1.2.0",
|
|
52
|
+
"adapt-authoring-mongodb": "^2.0.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"adapt-authoring-core": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"adapt-authoring-jsonschema": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"adapt-authoring-mongodb": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
58
64
|
}
|
|
59
65
|
}
|
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { describe, it, mock, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import TagsModule from '../lib/TagsModule.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a TagsModule instance with mocked dependencies.
|
|
7
|
+
* Prevents the AbstractModule constructor from calling init()
|
|
8
|
+
* by temporarily replacing the prototype method.
|
|
9
|
+
*/
|
|
10
|
+
function createInstance (overrides = {}) {
|
|
11
|
+
const mockJsonschema = {
|
|
12
|
+
registerSchemasHook: { tap: mock.fn() },
|
|
13
|
+
extendSchema: mock.fn()
|
|
14
|
+
}
|
|
15
|
+
const mockMongodb = {
|
|
16
|
+
setIndex: mock.fn(async () => {}),
|
|
17
|
+
find: mock.fn(async () => []),
|
|
18
|
+
insert: mock.fn(async () => ({})),
|
|
19
|
+
delete: mock.fn(async () => {})
|
|
20
|
+
}
|
|
21
|
+
const mockApp = {
|
|
22
|
+
waitForModule: mock.fn(async (name) => {
|
|
23
|
+
if (name === 'jsonschema') return mockJsonschema
|
|
24
|
+
if (name === 'mongodb') return mockMongodb
|
|
25
|
+
return {}
|
|
26
|
+
}),
|
|
27
|
+
errors: {},
|
|
28
|
+
dependencyloader: {
|
|
29
|
+
moduleLoadedHook: {
|
|
30
|
+
tap: () => {},
|
|
31
|
+
untap: () => {}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
...overrides
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const originalInit = TagsModule.prototype.init
|
|
38
|
+
TagsModule.prototype.init = async function () {}
|
|
39
|
+
|
|
40
|
+
const instance = new TagsModule(mockApp, { name: 'adapt-authoring-tags' })
|
|
41
|
+
|
|
42
|
+
TagsModule.prototype.init = originalInit
|
|
43
|
+
|
|
44
|
+
// Manually set properties that setValues() would set
|
|
45
|
+
instance.root = 'tags'
|
|
46
|
+
instance.schemaName = 'tag'
|
|
47
|
+
instance.schemaExtensionName = 'tags'
|
|
48
|
+
instance.collectionName = 'tags'
|
|
49
|
+
instance.modules = []
|
|
50
|
+
instance.routes = []
|
|
51
|
+
instance.log = mock.fn()
|
|
52
|
+
|
|
53
|
+
return { instance, mockApp, mockJsonschema, mockMongodb }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('TagsModule', () => {
|
|
57
|
+
describe('#setValues()', () => {
|
|
58
|
+
it('should set root to "tags"', async () => {
|
|
59
|
+
const { instance } = createInstance()
|
|
60
|
+
instance.root = undefined
|
|
61
|
+
instance.schemaName = undefined
|
|
62
|
+
instance.schemaExtensionName = undefined
|
|
63
|
+
instance.collectionName = undefined
|
|
64
|
+
instance.modules = undefined
|
|
65
|
+
instance.routes = undefined
|
|
66
|
+
// useDefaultRouteConfig normally sets this.routes; mock must do the same
|
|
67
|
+
instance.useDefaultRouteConfig = mock.fn(function () { this.routes = [] })
|
|
68
|
+
await instance.setValues()
|
|
69
|
+
assert.equal(instance.root, 'tags')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should set schemaName to "tag"', async () => {
|
|
73
|
+
const { instance } = createInstance()
|
|
74
|
+
instance.root = undefined
|
|
75
|
+
instance.schemaName = undefined
|
|
76
|
+
instance.useDefaultRouteConfig = mock.fn(function () { this.routes = [] })
|
|
77
|
+
await instance.setValues()
|
|
78
|
+
assert.equal(instance.schemaName, 'tag')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should set schemaExtensionName to "tags"', async () => {
|
|
82
|
+
const { instance } = createInstance()
|
|
83
|
+
instance.schemaExtensionName = undefined
|
|
84
|
+
instance.useDefaultRouteConfig = mock.fn(function () { this.routes = [] })
|
|
85
|
+
await instance.setValues()
|
|
86
|
+
assert.equal(instance.schemaExtensionName, 'tags')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should set collectionName to "tags"', async () => {
|
|
90
|
+
const { instance } = createInstance()
|
|
91
|
+
instance.collectionName = undefined
|
|
92
|
+
instance.useDefaultRouteConfig = mock.fn(function () { this.routes = [] })
|
|
93
|
+
await instance.setValues()
|
|
94
|
+
assert.equal(instance.collectionName, 'tags')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should initialise modules as an empty array', async () => {
|
|
98
|
+
const { instance } = createInstance()
|
|
99
|
+
instance.modules = undefined
|
|
100
|
+
instance.useDefaultRouteConfig = mock.fn(function () { this.routes = [] })
|
|
101
|
+
await instance.setValues()
|
|
102
|
+
assert.deepEqual(instance.modules, [])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should call useDefaultRouteConfig', async () => {
|
|
106
|
+
const { instance } = createInstance()
|
|
107
|
+
instance.useDefaultRouteConfig = mock.fn(function () { this.routes = [] })
|
|
108
|
+
await instance.setValues()
|
|
109
|
+
assert.equal(instance.useDefaultRouteConfig.mock.calls.length, 1)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should add autocomplete and transfer routes', async () => {
|
|
113
|
+
const { instance } = createInstance()
|
|
114
|
+
instance.useDefaultRouteConfig = mock.fn(function () { this.routes = [] })
|
|
115
|
+
await instance.setValues()
|
|
116
|
+
|
|
117
|
+
const autocompleteRoute = instance.routes.find(r => r.route === '/autocomplete')
|
|
118
|
+
assert.ok(autocompleteRoute, 'autocomplete route should exist')
|
|
119
|
+
assert.ok(autocompleteRoute.handlers.get, 'autocomplete should have GET handler')
|
|
120
|
+
assert.deepEqual(autocompleteRoute.permissions.get, ['read:content'])
|
|
121
|
+
|
|
122
|
+
const transferRoute = instance.routes.find(r => r.route === '/transfer/:_id')
|
|
123
|
+
assert.ok(transferRoute, 'transfer route should exist')
|
|
124
|
+
assert.ok(transferRoute.handlers.post, 'transfer should have POST handler')
|
|
125
|
+
assert.deepEqual(transferRoute.permissions.post, ['write:content'])
|
|
126
|
+
assert.equal(transferRoute.modifying, false)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should call mongodb.setIndex for unique title', async () => {
|
|
130
|
+
const { instance, mockMongodb } = createInstance()
|
|
131
|
+
instance.useDefaultRouteConfig = mock.fn(function () { this.routes = [] })
|
|
132
|
+
await instance.setValues()
|
|
133
|
+
assert.equal(mockMongodb.setIndex.mock.calls.length, 1)
|
|
134
|
+
const call = mockMongodb.setIndex.mock.calls[0]
|
|
135
|
+
assert.equal(call.arguments[0], 'tags')
|
|
136
|
+
assert.equal(call.arguments[1], 'title')
|
|
137
|
+
assert.deepEqual(call.arguments[2], { unique: true })
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('#registerModule()', () => {
|
|
142
|
+
it('should register a module with a schemaName', async () => {
|
|
143
|
+
const { instance } = createInstance()
|
|
144
|
+
const mod = { schemaName: 'testSchema', name: 'test-mod' }
|
|
145
|
+
await instance.registerModule(mod)
|
|
146
|
+
assert.equal(instance.modules.length, 1)
|
|
147
|
+
assert.equal(instance.modules[0], mod)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should call registerSchema when registering a module', async () => {
|
|
151
|
+
const { instance, mockJsonschema } = createInstance()
|
|
152
|
+
const mod = { schemaName: 'testSchema', name: 'test-mod' }
|
|
153
|
+
await instance.registerModule(mod)
|
|
154
|
+
assert.ok(mockJsonschema.extendSchema.mock.calls.length > 0)
|
|
155
|
+
const call = mockJsonschema.extendSchema.mock.calls[0]
|
|
156
|
+
assert.equal(call.arguments[0], 'testSchema')
|
|
157
|
+
assert.equal(call.arguments[1], 'tags')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should log a debug message after registering', async () => {
|
|
161
|
+
const { instance } = createInstance()
|
|
162
|
+
const mod = { schemaName: 'testSchema', name: 'test-mod' }
|
|
163
|
+
await instance.registerModule(mod)
|
|
164
|
+
const debugCalls = instance.log.mock.calls.filter(
|
|
165
|
+
c => c.arguments[0] === 'debug'
|
|
166
|
+
)
|
|
167
|
+
assert.ok(debugCalls.length > 0)
|
|
168
|
+
assert.ok(debugCalls[0].arguments[1].includes('test-mod'))
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should log a warning when module has no schemaName', async () => {
|
|
172
|
+
const { instance } = createInstance()
|
|
173
|
+
const mod = { name: 'no-schema-mod' }
|
|
174
|
+
await instance.registerModule(mod)
|
|
175
|
+
const warnCalls = instance.log.mock.calls.filter(
|
|
176
|
+
c => c.arguments[0] === 'warn'
|
|
177
|
+
)
|
|
178
|
+
assert.ok(warnCalls.length > 0)
|
|
179
|
+
assert.ok(warnCalls[0].arguments[1].includes('schemaName'))
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should not add module to modules array when schemaName is missing', async () => {
|
|
183
|
+
const { instance } = createInstance()
|
|
184
|
+
const mod = { name: 'no-schema-mod' }
|
|
185
|
+
await instance.registerModule(mod)
|
|
186
|
+
assert.equal(instance.modules.length, 0)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('#registerSchema()', () => {
|
|
191
|
+
it('should call jsonschema.extendSchema with the correct arguments', async () => {
|
|
192
|
+
const { instance, mockJsonschema } = createInstance()
|
|
193
|
+
const mod = { schemaName: 'mySchema' }
|
|
194
|
+
await instance.registerSchema(mod)
|
|
195
|
+
assert.equal(mockJsonschema.extendSchema.mock.calls.length, 1)
|
|
196
|
+
const call = mockJsonschema.extendSchema.mock.calls[0]
|
|
197
|
+
assert.equal(call.arguments[0], 'mySchema')
|
|
198
|
+
assert.equal(call.arguments[1], 'tags')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should silently catch errors from extendSchema', async () => {
|
|
202
|
+
const failingJsonschema = {
|
|
203
|
+
registerSchemasHook: { tap: mock.fn() },
|
|
204
|
+
extendSchema: mock.fn(() => { throw new Error('schema error') })
|
|
205
|
+
}
|
|
206
|
+
const { instance } = createInstance({
|
|
207
|
+
waitForModule: mock.fn(async () => failingJsonschema)
|
|
208
|
+
})
|
|
209
|
+
const mod = { schemaName: 'badSchema' }
|
|
210
|
+
await assert.doesNotReject(() => instance.registerSchema(mod))
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should silently catch errors from waitForModule', async () => {
|
|
214
|
+
const { instance } = createInstance({
|
|
215
|
+
waitForModule: mock.fn(async () => { throw new Error('module not found') })
|
|
216
|
+
})
|
|
217
|
+
const mod = { schemaName: 'testSchema' }
|
|
218
|
+
await assert.doesNotReject(() => instance.registerSchema(mod))
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('#registerSchemas()', () => {
|
|
223
|
+
it('should call registerSchema for each registered module', async () => {
|
|
224
|
+
const { instance, mockJsonschema } = createInstance()
|
|
225
|
+
instance.modules = [
|
|
226
|
+
{ schemaName: 'schema1' },
|
|
227
|
+
{ schemaName: 'schema2' },
|
|
228
|
+
{ schemaName: 'schema3' }
|
|
229
|
+
]
|
|
230
|
+
await instance.registerSchemas()
|
|
231
|
+
assert.equal(mockJsonschema.extendSchema.mock.calls.length, 3)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should handle empty modules array', async () => {
|
|
235
|
+
const { instance, mockJsonschema } = createInstance()
|
|
236
|
+
instance.modules = []
|
|
237
|
+
await instance.registerSchemas()
|
|
238
|
+
assert.equal(mockJsonschema.extendSchema.mock.calls.length, 0)
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('#autocompleteHandler()', () => {
|
|
243
|
+
it('should return mapped tag data as JSON', async () => {
|
|
244
|
+
const { instance } = createInstance()
|
|
245
|
+
const findResults = [
|
|
246
|
+
{ _id: 'id1', title: 'JavaScript' },
|
|
247
|
+
{ _id: 'id2', title: 'Java' }
|
|
248
|
+
]
|
|
249
|
+
instance.find = mock.fn(async () => findResults)
|
|
250
|
+
const jsonData = []
|
|
251
|
+
const req = {
|
|
252
|
+
apiData: { query: { term: 'Ja' } }
|
|
253
|
+
}
|
|
254
|
+
const res = {
|
|
255
|
+
json: mock.fn((data) => { jsonData.push(...data) })
|
|
256
|
+
}
|
|
257
|
+
const next = mock.fn()
|
|
258
|
+
await instance.autocompleteHandler(req, res, next)
|
|
259
|
+
assert.equal(res.json.mock.calls.length, 1)
|
|
260
|
+
assert.equal(jsonData.length, 2)
|
|
261
|
+
assert.deepEqual(jsonData[0], { _id: 'id1', title: 'JavaScript', value: 'JavaScript' })
|
|
262
|
+
assert.deepEqual(jsonData[1], { _id: 'id2', title: 'Java', value: 'Java' })
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should query find with a regex based on the term', async () => {
|
|
266
|
+
const { instance } = createInstance()
|
|
267
|
+
instance.find = mock.fn(async () => [])
|
|
268
|
+
const req = {
|
|
269
|
+
apiData: { query: { term: 'test' } }
|
|
270
|
+
}
|
|
271
|
+
const res = { json: mock.fn() }
|
|
272
|
+
const next = mock.fn()
|
|
273
|
+
await instance.autocompleteHandler(req, res, next)
|
|
274
|
+
assert.equal(instance.find.mock.calls.length, 1)
|
|
275
|
+
const query = instance.find.mock.calls[0].arguments[0]
|
|
276
|
+
assert.ok(query.title.$regex)
|
|
277
|
+
assert.equal(query.title.$regex, '^test')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should return empty array when no tags match', async () => {
|
|
281
|
+
const { instance } = createInstance()
|
|
282
|
+
instance.find = mock.fn(async () => [])
|
|
283
|
+
const jsonData = []
|
|
284
|
+
const req = {
|
|
285
|
+
apiData: { query: { term: 'nonexistent' } }
|
|
286
|
+
}
|
|
287
|
+
const res = {
|
|
288
|
+
json: mock.fn((data) => { jsonData.push(...data) })
|
|
289
|
+
}
|
|
290
|
+
const next = mock.fn()
|
|
291
|
+
await instance.autocompleteHandler(req, res, next)
|
|
292
|
+
assert.equal(res.json.mock.calls.length, 1)
|
|
293
|
+
assert.equal(jsonData.length, 0)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should map value to title for each result', async () => {
|
|
297
|
+
const { instance } = createInstance()
|
|
298
|
+
instance.find = mock.fn(async () => [
|
|
299
|
+
{ _id: 'id1', title: 'React', extraProp: 'ignored' }
|
|
300
|
+
])
|
|
301
|
+
let result
|
|
302
|
+
const req = {
|
|
303
|
+
apiData: { query: { term: 'Re' } }
|
|
304
|
+
}
|
|
305
|
+
const res = {
|
|
306
|
+
json: mock.fn((data) => { result = data })
|
|
307
|
+
}
|
|
308
|
+
const next = mock.fn()
|
|
309
|
+
await instance.autocompleteHandler(req, res, next)
|
|
310
|
+
assert.equal(result[0].value, result[0].title)
|
|
311
|
+
assert.equal(result[0].extraProp, undefined)
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe('#transferHandler()', () => {
|
|
316
|
+
let instance
|
|
317
|
+
|
|
318
|
+
beforeEach(() => {
|
|
319
|
+
({ instance } = createInstance())
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('should push destId tag to all registered modules', async () => {
|
|
323
|
+
const mod = {
|
|
324
|
+
updateMany: mock.fn(async () => {})
|
|
325
|
+
}
|
|
326
|
+
instance.modules = [mod]
|
|
327
|
+
const req = {
|
|
328
|
+
apiData: {
|
|
329
|
+
query: { _id: 'sourceTag' },
|
|
330
|
+
data: { destId: 'destTag' }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const res = { json: mock.fn() }
|
|
334
|
+
const next = mock.fn()
|
|
335
|
+
await instance.transferHandler(req, res, next)
|
|
336
|
+
assert.ok(mod.updateMany.mock.calls.length >= 1)
|
|
337
|
+
const pushCall = mod.updateMany.mock.calls[0]
|
|
338
|
+
assert.deepEqual(pushCall.arguments[0], { tags: 'sourceTag' })
|
|
339
|
+
assert.deepEqual(pushCall.arguments[1], { $push: { tags: 'destTag' } })
|
|
340
|
+
assert.deepEqual(pushCall.arguments[2], { rawUpdate: true })
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('should pull sourceId when deleteSourceTag is not "true"', async () => {
|
|
344
|
+
const mod = {
|
|
345
|
+
updateMany: mock.fn(async () => {})
|
|
346
|
+
}
|
|
347
|
+
instance.modules = [mod]
|
|
348
|
+
const req = {
|
|
349
|
+
apiData: {
|
|
350
|
+
query: { _id: 'sourceTag' },
|
|
351
|
+
data: { destId: 'destTag' }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const res = { json: mock.fn() }
|
|
355
|
+
const next = mock.fn()
|
|
356
|
+
await instance.transferHandler(req, res, next)
|
|
357
|
+
assert.equal(mod.updateMany.mock.calls.length, 2)
|
|
358
|
+
const pullCall = mod.updateMany.mock.calls[1]
|
|
359
|
+
assert.deepEqual(pullCall.arguments[0], { tags: 'sourceTag' })
|
|
360
|
+
assert.deepEqual(pullCall.arguments[1], { $pull: { tags: 'sourceTag' } })
|
|
361
|
+
assert.deepEqual(pullCall.arguments[2], { rawUpdate: true })
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('should delete the source tag when deleteSourceTag is "true"', async () => {
|
|
365
|
+
const mod = {
|
|
366
|
+
updateMany: mock.fn(async () => {})
|
|
367
|
+
}
|
|
368
|
+
instance.modules = [mod]
|
|
369
|
+
instance.delete = mock.fn(async () => ({}))
|
|
370
|
+
const req = {
|
|
371
|
+
apiData: {
|
|
372
|
+
query: { _id: 'sourceTag', deleteSourceTag: 'true' },
|
|
373
|
+
data: { destId: 'destTag' }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const res = { json: mock.fn() }
|
|
377
|
+
const next = mock.fn()
|
|
378
|
+
await instance.transferHandler(req, res, next)
|
|
379
|
+
assert.equal(instance.delete.mock.calls.length, 1)
|
|
380
|
+
assert.deepEqual(instance.delete.mock.calls[0].arguments[0], { _id: 'sourceTag' })
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it('should not pull sourceId when deleteSourceTag is "true"', async () => {
|
|
384
|
+
const mod = {
|
|
385
|
+
updateMany: mock.fn(async () => {})
|
|
386
|
+
}
|
|
387
|
+
instance.modules = [mod]
|
|
388
|
+
instance.delete = mock.fn(async () => ({}))
|
|
389
|
+
const req = {
|
|
390
|
+
apiData: {
|
|
391
|
+
query: { _id: 'sourceTag', deleteSourceTag: 'true' },
|
|
392
|
+
data: { destId: 'destTag' }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const res = { json: mock.fn() }
|
|
396
|
+
const next = mock.fn()
|
|
397
|
+
await instance.transferHandler(req, res, next)
|
|
398
|
+
// When deleteSourceTag is "true", it should only call $push, not $pull
|
|
399
|
+
assert.equal(mod.updateMany.mock.calls.length, 1)
|
|
400
|
+
assert.deepEqual(mod.updateMany.mock.calls[0].arguments[1], { $push: { tags: 'destTag' } })
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('should not delete source tag when deleteSourceTag is not "true"', async () => {
|
|
404
|
+
const mod = {
|
|
405
|
+
updateMany: mock.fn(async () => {})
|
|
406
|
+
}
|
|
407
|
+
instance.modules = [mod]
|
|
408
|
+
instance.delete = mock.fn(async () => ({}))
|
|
409
|
+
const req = {
|
|
410
|
+
apiData: {
|
|
411
|
+
query: { _id: 'sourceTag' },
|
|
412
|
+
data: { destId: 'destTag' }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const res = { json: mock.fn() }
|
|
416
|
+
const next = mock.fn()
|
|
417
|
+
await instance.transferHandler(req, res, next)
|
|
418
|
+
assert.equal(instance.delete.mock.calls.length, 0)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('should handle multiple modules', async () => {
|
|
422
|
+
const mod1 = { updateMany: mock.fn(async () => {}) }
|
|
423
|
+
const mod2 = { updateMany: mock.fn(async () => {}) }
|
|
424
|
+
instance.modules = [mod1, mod2]
|
|
425
|
+
const req = {
|
|
426
|
+
apiData: {
|
|
427
|
+
query: { _id: 'sourceTag' },
|
|
428
|
+
data: { destId: 'destTag' }
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const res = { json: mock.fn() }
|
|
432
|
+
const next = mock.fn()
|
|
433
|
+
await instance.transferHandler(req, res, next)
|
|
434
|
+
assert.ok(mod1.updateMany.mock.calls.length >= 1)
|
|
435
|
+
assert.ok(mod2.updateMany.mock.calls.length >= 1)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('should call next with error when delete throws', async () => {
|
|
439
|
+
instance.modules = []
|
|
440
|
+
instance.delete = mock.fn(async () => { throw new Error('delete failed') })
|
|
441
|
+
const req = {
|
|
442
|
+
apiData: {
|
|
443
|
+
query: { _id: 'sourceTag', deleteSourceTag: 'true' },
|
|
444
|
+
data: { destId: 'destTag' }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const res = { json: mock.fn() }
|
|
448
|
+
const next = mock.fn()
|
|
449
|
+
await instance.transferHandler(req, res, next)
|
|
450
|
+
assert.equal(next.mock.calls.length, 1)
|
|
451
|
+
assert.ok(next.mock.calls[0].arguments[0] instanceof Error)
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
it('should log a warning when a module updateMany fails', async () => {
|
|
455
|
+
const mod = {
|
|
456
|
+
updateMany: mock.fn(async () => { throw new Error('update failed') })
|
|
457
|
+
}
|
|
458
|
+
instance.modules = [mod]
|
|
459
|
+
const req = {
|
|
460
|
+
apiData: {
|
|
461
|
+
query: { _id: 'sourceTag' },
|
|
462
|
+
data: { destId: 'destTag' }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const res = { json: mock.fn() }
|
|
466
|
+
const next = mock.fn()
|
|
467
|
+
await instance.transferHandler(req, res, next)
|
|
468
|
+
const warnCalls = instance.log.mock.calls.filter(
|
|
469
|
+
c => c.arguments[0] === 'warn'
|
|
470
|
+
)
|
|
471
|
+
assert.ok(warnCalls.length > 0)
|
|
472
|
+
assert.ok(warnCalls[0].arguments[1].includes('Failed to transfer tag'))
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('should handle empty modules array', async () => {
|
|
476
|
+
instance.modules = []
|
|
477
|
+
const req = {
|
|
478
|
+
apiData: {
|
|
479
|
+
query: { _id: 'sourceTag' },
|
|
480
|
+
data: { destId: 'destTag' }
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const res = { json: mock.fn() }
|
|
484
|
+
const next = mock.fn()
|
|
485
|
+
await instance.transferHandler(req, res, next)
|
|
486
|
+
assert.equal(next.mock.calls.length, 0)
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
describe('#insert()', () => {
|
|
491
|
+
it('should return existing tag if one is found', async () => {
|
|
492
|
+
const { instance } = createInstance()
|
|
493
|
+
const existingTag = { _id: 'existing1', title: 'JavaScript' }
|
|
494
|
+
instance.find = mock.fn(async () => [existingTag])
|
|
495
|
+
const result = await instance.insert({ title: 'JavaScript' })
|
|
496
|
+
assert.equal(result, existingTag)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('should call find with the same arguments', async () => {
|
|
500
|
+
const { instance } = createInstance()
|
|
501
|
+
instance.find = mock.fn(async () => [{ _id: 'tag1', title: 'test' }])
|
|
502
|
+
const args = { title: 'test' }
|
|
503
|
+
await instance.insert(args)
|
|
504
|
+
assert.equal(instance.find.mock.calls.length, 1)
|
|
505
|
+
assert.equal(instance.find.mock.calls[0].arguments[0], args)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('should call super.insert when no existing tag is found', async () => {
|
|
509
|
+
const { instance } = createInstance()
|
|
510
|
+
const newTag = { _id: 'new1', title: 'NewTag' }
|
|
511
|
+
instance.find = mock.fn(async () => [])
|
|
512
|
+
// Mock the parent insert via the prototype chain
|
|
513
|
+
const parentInsert = mock.fn(async () => newTag)
|
|
514
|
+
Object.getPrototypeOf(TagsModule.prototype).insert = parentInsert
|
|
515
|
+
const result = await instance.insert({ title: 'NewTag' })
|
|
516
|
+
assert.equal(result, newTag)
|
|
517
|
+
assert.equal(parentInsert.mock.calls.length, 1)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('should pass through all arguments to find', async () => {
|
|
521
|
+
const { instance } = createInstance()
|
|
522
|
+
instance.find = mock.fn(async () => [{ _id: 'tag1', title: 'test' }])
|
|
523
|
+
const data = { title: 'test' }
|
|
524
|
+
const options = { validate: false }
|
|
525
|
+
const mongoOpts = { limit: 1 }
|
|
526
|
+
await instance.insert(data, options, mongoOpts)
|
|
527
|
+
const findArgs = instance.find.mock.calls[0].arguments
|
|
528
|
+
assert.equal(findArgs[0], data)
|
|
529
|
+
assert.equal(findArgs[1], options)
|
|
530
|
+
assert.equal(findArgs[2], mongoOpts)
|
|
531
|
+
})
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
describe('#delete()', () => {
|
|
535
|
+
it('should call super.delete and return its result', async () => {
|
|
536
|
+
const { instance } = createInstance()
|
|
537
|
+
const deletedTag = { _id: 'deleted1', title: 'OldTag' }
|
|
538
|
+
const parentDelete = mock.fn(async () => deletedTag)
|
|
539
|
+
Object.getPrototypeOf(TagsModule.prototype).delete = parentDelete
|
|
540
|
+
instance.modules = []
|
|
541
|
+
const result = await instance.delete({ _id: 'deleted1' })
|
|
542
|
+
assert.equal(result, deletedTag)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
it('should remove deleted tag from all registered modules', async () => {
|
|
546
|
+
const { instance } = createInstance()
|
|
547
|
+
const deletedTag = { _id: 'tagToDelete', title: 'RemoveMe' }
|
|
548
|
+
const parentDelete = mock.fn(async () => deletedTag)
|
|
549
|
+
Object.getPrototypeOf(TagsModule.prototype).delete = parentDelete
|
|
550
|
+
const mod1 = { updateMany: mock.fn(async () => {}) }
|
|
551
|
+
const mod2 = { updateMany: mock.fn(async () => {}) }
|
|
552
|
+
instance.modules = [mod1, mod2]
|
|
553
|
+
await instance.delete({ _id: 'tagToDelete' })
|
|
554
|
+
assert.equal(mod1.updateMany.mock.calls.length, 1)
|
|
555
|
+
assert.deepEqual(mod1.updateMany.mock.calls[0].arguments[0], { tags: 'tagToDelete' })
|
|
556
|
+
assert.deepEqual(mod1.updateMany.mock.calls[0].arguments[1], { $pull: { tags: 'tagToDelete' } })
|
|
557
|
+
assert.deepEqual(mod1.updateMany.mock.calls[0].arguments[2], { rawUpdate: true })
|
|
558
|
+
assert.equal(mod2.updateMany.mock.calls.length, 1)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('should log a warning when module updateMany fails during delete', async () => {
|
|
562
|
+
const { instance } = createInstance()
|
|
563
|
+
const deletedTag = { _id: 'tagToDelete', title: 'RemoveMe' }
|
|
564
|
+
const parentDelete = mock.fn(async () => deletedTag)
|
|
565
|
+
Object.getPrototypeOf(TagsModule.prototype).delete = parentDelete
|
|
566
|
+
const mod = {
|
|
567
|
+
updateMany: mock.fn(async () => { throw new Error('update failed') })
|
|
568
|
+
}
|
|
569
|
+
instance.modules = [mod]
|
|
570
|
+
await instance.delete({ _id: 'tagToDelete' })
|
|
571
|
+
const warnCalls = instance.log.mock.calls.filter(
|
|
572
|
+
c => c.arguments[0] === 'warn'
|
|
573
|
+
)
|
|
574
|
+
assert.ok(warnCalls.length > 0)
|
|
575
|
+
assert.ok(warnCalls[0].arguments[1].includes('Failed to remove tag'))
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('should handle empty modules array', async () => {
|
|
579
|
+
const { instance } = createInstance()
|
|
580
|
+
const deletedTag = { _id: 'tagToDelete', title: 'RemoveMe' }
|
|
581
|
+
const parentDelete = mock.fn(async () => deletedTag)
|
|
582
|
+
Object.getPrototypeOf(TagsModule.prototype).delete = parentDelete
|
|
583
|
+
instance.modules = []
|
|
584
|
+
const result = await instance.delete({ _id: 'tagToDelete' })
|
|
585
|
+
assert.equal(result, deletedTag)
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
it('should continue removing from other modules if one fails', async () => {
|
|
589
|
+
const { instance } = createInstance()
|
|
590
|
+
const deletedTag = { _id: 'tagToDelete', title: 'RemoveMe' }
|
|
591
|
+
const parentDelete = mock.fn(async () => deletedTag)
|
|
592
|
+
Object.getPrototypeOf(TagsModule.prototype).delete = parentDelete
|
|
593
|
+
const mod1 = {
|
|
594
|
+
updateMany: mock.fn(async () => { throw new Error('fail') })
|
|
595
|
+
}
|
|
596
|
+
const mod2 = { updateMany: mock.fn(async () => {}) }
|
|
597
|
+
instance.modules = [mod1, mod2]
|
|
598
|
+
await instance.delete({ _id: 'tagToDelete' })
|
|
599
|
+
assert.equal(mod2.updateMany.mock.calls.length, 1)
|
|
600
|
+
})
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
describe('#init()', () => {
|
|
604
|
+
it('should tap into jsonschema registerSchemasHook', async () => {
|
|
605
|
+
const { instance, mockJsonschema } = createInstance()
|
|
606
|
+
// Mock super.init to be a no-op
|
|
607
|
+
const origSuperInit = Object.getPrototypeOf(TagsModule.prototype).init
|
|
608
|
+
Object.getPrototypeOf(TagsModule.prototype).init = mock.fn(async function () {})
|
|
609
|
+
await instance.init()
|
|
610
|
+
Object.getPrototypeOf(TagsModule.prototype).init = origSuperInit
|
|
611
|
+
assert.equal(mockJsonschema.registerSchemasHook.tap.mock.calls.length, 1)
|
|
612
|
+
})
|
|
613
|
+
})
|
|
614
|
+
})
|