adapt-authoring-adaptframework 1.12.0 → 2.0.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.
- package/index.js +0 -1
- package/lib/AdaptFrameworkBuild.js +17 -28
- package/lib/AdaptFrameworkImport.js +47 -47
- package/lib/AdaptFrameworkModule.js +12 -11
- package/lib/handlers.js +164 -0
- package/lib/utils/copyFrameworkSource.js +35 -0
- package/lib/utils/getImportContentCounts.js +13 -0
- package/lib/utils/getImportSummary.js +55 -0
- package/lib/utils/getPluginUpdateStatus.js +20 -0
- package/lib/utils/inferBuildAction.js +9 -0
- package/lib/utils/log.js +37 -0
- package/lib/utils/retrieveBuildData.js +18 -0
- package/lib/utils/runCliCommand.js +24 -0
- package/lib/utils/slugifyTitle.js +18 -0
- package/lib/utils.js +9 -0
- package/package.json +2 -2
- package/tests/AdaptFrameworkBuild.spec.js +5 -9
- package/tests/utils-getImportContentCounts.spec.js +30 -0
- package/tests/utils-getPluginUpdateStatus.spec.js +34 -0
- package/tests/utils-inferBuildAction.spec.js +21 -0
- package/lib/AdaptFrameworkUtils.js +0 -398
- package/tests/AdaptFrameworkUtils.spec.js +0 -163
|
@@ -1,398 +0,0 @@
|
|
|
1
|
-
import AdaptCli from 'adapt-cli'
|
|
2
|
-
import { App } from 'adapt-authoring-core'
|
|
3
|
-
import bytes from 'bytes'
|
|
4
|
-
import fs from 'fs/promises'
|
|
5
|
-
/* eslint-disable import/no-named-default */import { default as fsSync } from 'fs'
|
|
6
|
-
import path from 'upath'
|
|
7
|
-
import semver from 'semver'
|
|
8
|
-
|
|
9
|
-
/** @ignore */ const buildCache = {}
|
|
10
|
-
|
|
11
|
-
let fw
|
|
12
|
-
/**
|
|
13
|
-
* Utilities for use with the AdaptFrameworkModule
|
|
14
|
-
* @memberof adaptframework
|
|
15
|
-
*/
|
|
16
|
-
class AdaptFrameworkUtils {
|
|
17
|
-
/**
|
|
18
|
-
* Wrapper for running adapt-cli commands
|
|
19
|
-
* @param {string} command Command to run
|
|
20
|
-
* @param {object} opts Extra options
|
|
21
|
-
*/
|
|
22
|
-
static async runCliCommand (command, opts = {}) {
|
|
23
|
-
if (typeof AdaptCli[command] !== 'function') {
|
|
24
|
-
throw App.instance.errors.FW_CLI_UNKNOWN_CMD.setData({ command })
|
|
25
|
-
}
|
|
26
|
-
const debugLog = (...args) => App.instance.logger.log('debug', 'adapt-cli', ...args)
|
|
27
|
-
|
|
28
|
-
App.instance.logger.log('verbose', 'adapt-cli', 'CMD_START', command, opts)
|
|
29
|
-
const res = await AdaptCli[command]({
|
|
30
|
-
cwd: App.instance.config.get('adapt-authoring-adaptframework.frameworkDir'),
|
|
31
|
-
repository: App.instance.config.get('adapt-authoring-adaptframework.frameworkRepository'),
|
|
32
|
-
logger: { log: debugLog, logProgress: debugLog, write: debugLog },
|
|
33
|
-
...opts
|
|
34
|
-
})
|
|
35
|
-
App.instance.logger.log('verbose', 'adapt-cli', 'CMD_END', command, opts)
|
|
36
|
-
return res
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Logs a message using the framework module
|
|
41
|
-
* @param {...*} args Arguments to be logged
|
|
42
|
-
*/
|
|
43
|
-
static async log (...args) {
|
|
44
|
-
if (!fw) fw = await App.instance.waitForModule('adaptframework')
|
|
45
|
-
return fw.log(...args)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
static logDir (label, dir) {
|
|
49
|
-
try {
|
|
50
|
-
const resolved = dir ? path.resolve(dir) : undefined
|
|
51
|
-
AdaptFrameworkUtils.log('verbose', 'DIR', label, resolved)
|
|
52
|
-
if (resolved) AdaptFrameworkUtils.log('verbose', 'DIR_MODE', label, fsSync.statSync(resolved).mode)
|
|
53
|
-
} catch (e) {
|
|
54
|
-
AdaptFrameworkUtils.log('warn', `failed to log dir ${label} (${dir}), ${e.code}`)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
static logMemory () {
|
|
59
|
-
AdaptFrameworkUtils.log('verbose', 'MEMORY', Object.entries(process.memoryUsage()).reduce((m, [k, v]) => Object.assign(m, { [k]: bytes.parse(v) }), {}))
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
static async readJson (filepath) {
|
|
63
|
-
return JSON.parse(await fs.readFile(filepath))
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
static writeJson (filepath, data) {
|
|
67
|
-
return fs.writeFile(filepath, (JSON.stringify(data, null, 2)))
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Infers the framework action to be executed from a given request URL
|
|
72
|
-
* @param {external:ExpressRequest} req
|
|
73
|
-
* @return {String}
|
|
74
|
-
*/
|
|
75
|
-
static inferBuildAction (req) {
|
|
76
|
-
return req.url.slice(1, req.url.indexOf('/', 1))
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Retrieves metadata for a build attempt
|
|
81
|
-
* @param {String} id ID of build document
|
|
82
|
-
* @return {Promise}
|
|
83
|
-
*/
|
|
84
|
-
static async retrieveBuildData (id) {
|
|
85
|
-
if (buildCache[id]) {
|
|
86
|
-
return buildCache[id]
|
|
87
|
-
}
|
|
88
|
-
const mdb = await App.instance.waitForModule('mongodb')
|
|
89
|
-
const [data] = await mdb.find('adaptbuilds', { _id: id })
|
|
90
|
-
buildCache[id] = data
|
|
91
|
-
return data
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* @typedef {AdaptFrameworkImportSummary}
|
|
96
|
-
* @property {String} title Course title
|
|
97
|
-
* @property {String} courseId Course _id
|
|
98
|
-
* @property {Object} statusReport Status report
|
|
99
|
-
* @property {Object<String>} statusReport.info Information messages
|
|
100
|
-
* @property {Array<String>} statusReport.warn Warning messages
|
|
101
|
-
* @property {Object} content Object mapping content types to the number of items of that type found in the imported course
|
|
102
|
-
* @property {Object} versions A map of plugins used in the imported course and their versions
|
|
103
|
-
*
|
|
104
|
-
* @param {AdaptFrameworkImport} importer The import instance
|
|
105
|
-
* @return {AdaptFrameworkImportSummary} Object mapping all import versions to server installed versions
|
|
106
|
-
* @example
|
|
107
|
-
* {
|
|
108
|
-
* adapt_framework: [1.0.0, 2.0.0],
|
|
109
|
-
* adapt-contrib-vanilla: [1.0.0, 2.0.0]
|
|
110
|
-
* }
|
|
111
|
-
*/
|
|
112
|
-
static async getImportSummary (importer) {
|
|
113
|
-
const [framework, contentplugin] = await App.instance.waitForModule('adaptframework', 'contentplugin')
|
|
114
|
-
const installedPlugins = await contentplugin.find()
|
|
115
|
-
const {
|
|
116
|
-
pkg: { name: fwName, version: fwVersion },
|
|
117
|
-
idMap: { course: courseId },
|
|
118
|
-
contentJson,
|
|
119
|
-
usedContentPlugins: usedPlugins,
|
|
120
|
-
newContentPlugins: newPlugins,
|
|
121
|
-
statusReport,
|
|
122
|
-
settings: { updatePlugins }
|
|
123
|
-
} = importer
|
|
124
|
-
const versions = [
|
|
125
|
-
{ name: fwName, versions: [framework.version, fwVersion] },
|
|
126
|
-
...Object.values(usedPlugins),
|
|
127
|
-
...Object.values(newPlugins)
|
|
128
|
-
].map(meta => {
|
|
129
|
-
const p = installedPlugins.find(p => p.name === meta.name)
|
|
130
|
-
const versions = meta.versions ?? [p?.version, meta.version]
|
|
131
|
-
return {
|
|
132
|
-
name: meta.name,
|
|
133
|
-
status: this.getPluginUpdateStatus(versions, p?.isLocalInstall, updatePlugins),
|
|
134
|
-
versions
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
return {
|
|
138
|
-
title: contentJson.course.displayTitle || contentJson.course.title,
|
|
139
|
-
courseId,
|
|
140
|
-
statusReport,
|
|
141
|
-
content: this.getImportContentCounts(contentJson),
|
|
142
|
-
versions
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Determines the update status code
|
|
148
|
-
* @param {Array} versions
|
|
149
|
-
* @param {Boolean} isLocalInstall
|
|
150
|
-
* @param {Boolean} updatePlugins
|
|
151
|
-
* @returns {String} The update status code
|
|
152
|
-
*/
|
|
153
|
-
static getPluginUpdateStatus (versions, isLocalInstall, updatePlugins) {
|
|
154
|
-
const [installedVersion, importVersion] = versions
|
|
155
|
-
if (!semver.valid(importVersion)) return 'INVALID'
|
|
156
|
-
if (!installedVersion) return 'INSTALLED'
|
|
157
|
-
if (semver.lt(importVersion, installedVersion)) return 'OLDER'
|
|
158
|
-
if (semver.gt(importVersion, installedVersion)) {
|
|
159
|
-
if (!updatePlugins && !isLocalInstall) return 'UPDATE_BLOCKED'
|
|
160
|
-
return 'UPDATED'
|
|
161
|
-
}
|
|
162
|
-
return 'NO_CHANGE'
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Returns a map of content types and their instance count in the content JSON
|
|
167
|
-
* @param {Object} content Course content
|
|
168
|
-
* @returns {Object}
|
|
169
|
-
*/
|
|
170
|
-
static getImportContentCounts (content) {
|
|
171
|
-
return Object.values(content).reduce((m, c) => {
|
|
172
|
-
const items = c._type ? [c] : Object.values(c)
|
|
173
|
-
return items.reduce((m, { _type }) => {
|
|
174
|
-
return { ...m, [_type]: m[_type] !== undefined ? m[_type] + 1 : 1 }
|
|
175
|
-
}, m)
|
|
176
|
-
}, {})
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Returns a 'slugified' version of the course title appropriate for a filename
|
|
181
|
-
* @param {Object} buildData The course build data
|
|
182
|
-
* @returns {string} The slugified title
|
|
183
|
-
*/
|
|
184
|
-
static async slugifyTitle (buildData) {
|
|
185
|
-
const content = await App.instance.waitForModule('content')
|
|
186
|
-
const [course] = await content.find({ _id: buildData.courseId })
|
|
187
|
-
const sanitisedTitle = course.title
|
|
188
|
-
.toLowerCase()
|
|
189
|
-
.trim()
|
|
190
|
-
.replace(/[^a-z0-9 -]/g, '') // remove non-alphanumeric
|
|
191
|
-
.replace(/\s+/g, '-') // replace spaces with hyphens
|
|
192
|
-
.replace(/-+/g, '-') // remove duplicate hyphens
|
|
193
|
-
return `${sanitisedTitle}${buildData.action === 'export' ? '-export' : ''}`
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Copies the framework source directory
|
|
198
|
-
* @param {Object} options
|
|
199
|
-
* @param {String} options.destDir The destination directory path
|
|
200
|
-
* @param {Array<String>} options.enabledPlugins List of plugins to include
|
|
201
|
-
* @param {Boolean} options.copyNodeModules Whether to physically copy node_modules
|
|
202
|
-
* @param {Boolean} options.linkNodeModules Whether to symlink node_modules
|
|
203
|
-
* @return {Promise}
|
|
204
|
-
*/
|
|
205
|
-
static async copyFrameworkSource (options) {
|
|
206
|
-
const { path: fwPath } = await App.instance.waitForModule('adaptframework')
|
|
207
|
-
const BLACKLIST = ['.git', '.DS_Store', 'thumbs.db', 'course', 'migrations']
|
|
208
|
-
if (options.copyNodeModules !== true) BLACKLIST.push('node_modules')
|
|
209
|
-
|
|
210
|
-
const srcDir = path.join(fwPath, 'src')
|
|
211
|
-
const enabledPlugins = options.enabledPlugins ?? []
|
|
212
|
-
await fs.cp(fwPath, options.destDir, {
|
|
213
|
-
recursive: true,
|
|
214
|
-
filter: f => {
|
|
215
|
-
f = path.normalize(f)
|
|
216
|
-
const [type, name] = path.relative(srcDir, f).split('/')
|
|
217
|
-
const isPlugin = f.startsWith(srcDir) && type && type !== 'core' && !!name
|
|
218
|
-
|
|
219
|
-
if (isPlugin && !enabledPlugins.includes(name)) {
|
|
220
|
-
return false
|
|
221
|
-
}
|
|
222
|
-
return !BLACKLIST.includes(path.basename(f))
|
|
223
|
-
}
|
|
224
|
-
})
|
|
225
|
-
if (options.linkNodeModules !== false) await fs.symlink(`${fwPath}/node_modules`, `${options.destDir}/node_modules`, 'junction')
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Handles GET requests to the API
|
|
230
|
-
* @param {external:ExpressRequest} req
|
|
231
|
-
* @param {external:ExpressResponse} res
|
|
232
|
-
* @param {Function} next
|
|
233
|
-
* @return {Promise}
|
|
234
|
-
*/
|
|
235
|
-
static async getHandler (req, res, next) {
|
|
236
|
-
const action = AdaptFrameworkUtils.inferBuildAction(req)
|
|
237
|
-
const id = req.params.id
|
|
238
|
-
let buildData
|
|
239
|
-
try {
|
|
240
|
-
buildData = await AdaptFrameworkUtils.retrieveBuildData(id)
|
|
241
|
-
} catch (e) {
|
|
242
|
-
return next(e)
|
|
243
|
-
}
|
|
244
|
-
if (!buildData || new Date(buildData.expiresAt).getTime() < Date.now()) {
|
|
245
|
-
return next(App.instance.errors.FW_BUILD_NOT_FOUND.setData({ _id: id }))
|
|
246
|
-
}
|
|
247
|
-
if (action === 'publish' || action === 'export') {
|
|
248
|
-
res.set('content-disposition', `attachment; filename="${await AdaptFrameworkUtils.slugifyTitle(buildData)}.zip"`)
|
|
249
|
-
return res.sendFile(path.resolve(buildData.location), e => e && next(e))
|
|
250
|
-
}
|
|
251
|
-
if (action === 'preview') {
|
|
252
|
-
if (!req.auth.user) {
|
|
253
|
-
return res.status(App.instance.errors.MISSING_AUTH_HEADER.statusCode).end()
|
|
254
|
-
}
|
|
255
|
-
const filePath = path.resolve(buildData.location, req.path.slice(req.path.indexOf(id) + id.length + 1) || 'index.html')
|
|
256
|
-
await res.sendFile(filePath, e => {
|
|
257
|
-
if (!e) return
|
|
258
|
-
if (e.code === 'ENOENT') e = App.instance.errors.NOT_FOUND.setData({ type: 'file', id: filePath })
|
|
259
|
-
next(e)
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Handles POST requests to the API
|
|
266
|
-
* @param {external:ExpressRequest} req
|
|
267
|
-
* @param {external:ExpressResponse} res
|
|
268
|
-
* @param {Function} next
|
|
269
|
-
* @return {Promise}
|
|
270
|
-
*/
|
|
271
|
-
static async postHandler (req, res, next) {
|
|
272
|
-
const framework = await App.instance.waitForModule('adaptframework')
|
|
273
|
-
const startTime = Date.now()
|
|
274
|
-
const action = AdaptFrameworkUtils.inferBuildAction(req)
|
|
275
|
-
const courseId = req.params.id
|
|
276
|
-
const userId = req.auth.user._id.toString()
|
|
277
|
-
|
|
278
|
-
AdaptFrameworkUtils.log('info', `running ${action} for course '${courseId}' initiated by ${userId}`)
|
|
279
|
-
try {
|
|
280
|
-
const { isPreview, buildData } = await framework.buildCourse({ action, courseId, userId })
|
|
281
|
-
const duration = Math.round((Date.now() - startTime) / 10) / 100
|
|
282
|
-
AdaptFrameworkUtils.log('info', `finished ${action} for course '${courseId}' in ${duration} seconds`)
|
|
283
|
-
const urlRoot = isPreview ? framework.rootRouter.url : framework.apiRouter.url
|
|
284
|
-
res.json({
|
|
285
|
-
[`${action}_url`]: `${urlRoot}/${action}/${buildData._id}/`,
|
|
286
|
-
versions: buildData.versions
|
|
287
|
-
})
|
|
288
|
-
} catch (e) {
|
|
289
|
-
AdaptFrameworkUtils.log('error', `failed to ${action} course '${courseId}'`)
|
|
290
|
-
return next(e)
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Handles POST /import requests to the API
|
|
296
|
-
* @param {external:ExpressRequest} req
|
|
297
|
-
* @param {external:ExpressResponse} res
|
|
298
|
-
* @param {Function} next
|
|
299
|
-
* @return {Promise}
|
|
300
|
-
*/
|
|
301
|
-
static async importHandler (req, res, next) {
|
|
302
|
-
try {
|
|
303
|
-
const framework = await App.instance.waitForModule('adaptframework')
|
|
304
|
-
let importPath = req.body.importPath
|
|
305
|
-
if (req.get('Content-Type').indexOf('multipart/form-data') === 0) {
|
|
306
|
-
await AdaptFrameworkUtils.handleImportFile(req, res)
|
|
307
|
-
const [course] = req.fileUpload.files.course
|
|
308
|
-
importPath = course.filepath
|
|
309
|
-
}
|
|
310
|
-
const importer = await framework.importCourse({
|
|
311
|
-
importPath,
|
|
312
|
-
userId: req.auth.user._id.toString(),
|
|
313
|
-
isDryRun: AdaptFrameworkUtils.toBoolean(req.body.dryRun),
|
|
314
|
-
assetFolders: req.body.formAssetFolders,
|
|
315
|
-
tags: req.body.tags?.length > 0 ? req.body.tags?.split(',') : [],
|
|
316
|
-
importContent: AdaptFrameworkUtils.toBoolean(req.body.importContent),
|
|
317
|
-
importPlugins: AdaptFrameworkUtils.toBoolean(req.body.importPlugins),
|
|
318
|
-
migrateContent: AdaptFrameworkUtils.toBoolean(req.body.migrateContent),
|
|
319
|
-
updatePlugins: AdaptFrameworkUtils.toBoolean(req.body.updatePlugins)
|
|
320
|
-
})
|
|
321
|
-
res.json(importer.summary)
|
|
322
|
-
} catch (e) {
|
|
323
|
-
return next(e?.statusCode ? e : App.instance.errors.FW_IMPORT_FAILED.setData({ error: e }))
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Handles POST /update requests to the API
|
|
329
|
-
* @param {external:ExpressRequest} req
|
|
330
|
-
* @param {external:ExpressResponse} res
|
|
331
|
-
* @param {Function} next
|
|
332
|
-
* @return {Promise}
|
|
333
|
-
*/
|
|
334
|
-
static async postUpdateHandler (req, res, next) {
|
|
335
|
-
try {
|
|
336
|
-
AdaptFrameworkUtils.log('info', 'running framework update')
|
|
337
|
-
const framework = await App.instance.waitForModule('adaptframework')
|
|
338
|
-
const previousVersion = framework.version
|
|
339
|
-
await framework.updateFramework(req.body.version)
|
|
340
|
-
const currentVersion = framework.version !== previousVersion ? framework.version : undefined
|
|
341
|
-
res.json({
|
|
342
|
-
from: previousVersion,
|
|
343
|
-
to: currentVersion
|
|
344
|
-
})
|
|
345
|
-
} catch (e) {
|
|
346
|
-
return next(e)
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Handles GET /update requests to the API
|
|
352
|
-
* @param {external:ExpressRequest} req
|
|
353
|
-
* @param {external:ExpressResponse} res
|
|
354
|
-
* @param {Function} next
|
|
355
|
-
* @return {Promise}
|
|
356
|
-
*/
|
|
357
|
-
static async getUpdateHandler (req, res, next) {
|
|
358
|
-
try {
|
|
359
|
-
const framework = await App.instance.waitForModule('adaptframework')
|
|
360
|
-
const current = framework.version
|
|
361
|
-
const latest = await framework.getLatestVersion()
|
|
362
|
-
res.json({
|
|
363
|
-
canBeUpdated: semver.gt(latest, current),
|
|
364
|
-
currentVersion: current,
|
|
365
|
-
latestCompatibleVersion: latest
|
|
366
|
-
})
|
|
367
|
-
} catch (e) {
|
|
368
|
-
return next(e)
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Converts a body value to a valid boolean
|
|
374
|
-
* @param {*} val
|
|
375
|
-
* @returns {Boolean}
|
|
376
|
-
*/
|
|
377
|
-
static toBoolean (val) {
|
|
378
|
-
if (val !== undefined) return val === true || val === 'true'
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Deals with an incoming course (supports both local zip and remote URL stream)
|
|
383
|
-
* @param {external:ExpressRequest} req
|
|
384
|
-
* @param {external:ExpressResponse} res
|
|
385
|
-
* @return {Promise}
|
|
386
|
-
*/
|
|
387
|
-
static async handleImportFile (req, res) {
|
|
388
|
-
const [fw, middleware] = await App.instance.waitForModule('adaptframework', 'middleware')
|
|
389
|
-
const handler = req.get('Content-Type').indexOf('multipart/form-data') === 0
|
|
390
|
-
? middleware.fileUploadParser
|
|
391
|
-
: middleware.urlUploadParser
|
|
392
|
-
return new Promise((resolve, reject) => {
|
|
393
|
-
handler(middleware.zipTypes, { maxFileSize: fw.getConfig('importMaxFileSize'), unzip: true })(req, res, e => e ? reject(e) : resolve())
|
|
394
|
-
})
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
export default AdaptFrameworkUtils
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import fs from 'fs/promises'
|
|
4
|
-
import path from 'path'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
|
-
import AdaptFrameworkUtils from '../lib/AdaptFrameworkUtils.js'
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
-
|
|
10
|
-
describe('AdaptFrameworkUtils', () => {
|
|
11
|
-
describe('#inferBuildAction()', () => {
|
|
12
|
-
const cases = [
|
|
13
|
-
{ url: '/preview/abc123', expected: 'preview' },
|
|
14
|
-
{ url: '/publish/abc123', expected: 'publish' },
|
|
15
|
-
{ url: '/export/abc123', expected: 'export' }
|
|
16
|
-
]
|
|
17
|
-
cases.forEach(({ url, expected }) => {
|
|
18
|
-
it(`should return "${expected}" for URL "${url}"`, () => {
|
|
19
|
-
assert.equal(AdaptFrameworkUtils.inferBuildAction({ url }), expected)
|
|
20
|
-
})
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// TODO: inferBuildAction returns truncated result for URLs without a second slash
|
|
24
|
-
// e.g. '/import' returns 'impor' because indexOf('/', 1) returns -1
|
|
25
|
-
// causing slice(1, -1) to strip the last character
|
|
26
|
-
it('should truncate action for URLs without trailing slash/path (known bug)', () => {
|
|
27
|
-
assert.equal(AdaptFrameworkUtils.inferBuildAction({ url: '/import' }), 'impor')
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe('#toBoolean()', () => {
|
|
32
|
-
it('should return true for boolean true', () => {
|
|
33
|
-
assert.equal(AdaptFrameworkUtils.toBoolean(true), true)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should return true for string "true"', () => {
|
|
37
|
-
assert.equal(AdaptFrameworkUtils.toBoolean('true'), true)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('should return false for boolean false', () => {
|
|
41
|
-
assert.equal(AdaptFrameworkUtils.toBoolean(false), false)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('should return false for string "false"', () => {
|
|
45
|
-
assert.equal(AdaptFrameworkUtils.toBoolean('false'), false)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('should return undefined for undefined', () => {
|
|
49
|
-
assert.equal(AdaptFrameworkUtils.toBoolean(undefined), undefined)
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('should return false for null', () => {
|
|
53
|
-
assert.equal(AdaptFrameworkUtils.toBoolean(null), false)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('should return false for 0', () => {
|
|
57
|
-
assert.equal(AdaptFrameworkUtils.toBoolean(0), false)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('should return false for empty string', () => {
|
|
61
|
-
assert.equal(AdaptFrameworkUtils.toBoolean(''), false)
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
describe('#getPluginUpdateStatus()', () => {
|
|
66
|
-
it('should return "INVALID" for an invalid import version', () => {
|
|
67
|
-
assert.equal(AdaptFrameworkUtils.getPluginUpdateStatus(['1.0.0', 'not-valid'], false, false), 'INVALID')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('should return "INSTALLED" when no installed version exists', () => {
|
|
71
|
-
assert.equal(AdaptFrameworkUtils.getPluginUpdateStatus([undefined, '1.0.0'], false, false), 'INSTALLED')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('should return "OLDER" when import version is older', () => {
|
|
75
|
-
assert.equal(AdaptFrameworkUtils.getPluginUpdateStatus(['2.0.0', '1.0.0'], false, false), 'OLDER')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('should return "NO_CHANGE" when versions are equal', () => {
|
|
79
|
-
assert.equal(AdaptFrameworkUtils.getPluginUpdateStatus(['1.0.0', '1.0.0'], false, false), 'NO_CHANGE')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('should return "UPDATE_BLOCKED" when import is newer but updates not enabled and not local', () => {
|
|
83
|
-
assert.equal(AdaptFrameworkUtils.getPluginUpdateStatus(['1.0.0', '2.0.0'], false, false), 'UPDATE_BLOCKED')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('should return "UPDATED" when import is newer and updates are enabled', () => {
|
|
87
|
-
assert.equal(AdaptFrameworkUtils.getPluginUpdateStatus(['1.0.0', '2.0.0'], false, true), 'UPDATED')
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('should return "UPDATED" when import is newer and is a local install', () => {
|
|
91
|
-
assert.equal(AdaptFrameworkUtils.getPluginUpdateStatus(['1.0.0', '2.0.0'], true, false), 'UPDATED')
|
|
92
|
-
})
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
describe('#getImportContentCounts()', () => {
|
|
96
|
-
it('should count single items by _type', () => {
|
|
97
|
-
const content = {
|
|
98
|
-
course: { _type: 'course' },
|
|
99
|
-
config: { _type: 'config' }
|
|
100
|
-
}
|
|
101
|
-
const result = AdaptFrameworkUtils.getImportContentCounts(content)
|
|
102
|
-
assert.deepEqual(result, { course: 1, config: 1 })
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('should count arrays of items by _type', () => {
|
|
106
|
-
const content = {
|
|
107
|
-
course: { _type: 'course' },
|
|
108
|
-
contentObjects: {
|
|
109
|
-
co1: { _type: 'page' },
|
|
110
|
-
co2: { _type: 'page' },
|
|
111
|
-
co3: { _type: 'menu' }
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const result = AdaptFrameworkUtils.getImportContentCounts(content)
|
|
115
|
-
assert.deepEqual(result, { course: 1, page: 2, menu: 1 })
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('should return empty object for empty content', () => {
|
|
119
|
-
assert.deepEqual(AdaptFrameworkUtils.getImportContentCounts({}), {})
|
|
120
|
-
})
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
describe('#readJson()', () => {
|
|
124
|
-
const testFile = path.join(__dirname, 'data', 'test-read.json')
|
|
125
|
-
|
|
126
|
-
before(async () => {
|
|
127
|
-
await fs.mkdir(path.join(__dirname, 'data'), { recursive: true })
|
|
128
|
-
await fs.writeFile(testFile, JSON.stringify({ key: 'value' }))
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
after(async () => {
|
|
132
|
-
await fs.rm(testFile, { force: true })
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('should read and parse a JSON file', async () => {
|
|
136
|
-
const result = await AdaptFrameworkUtils.readJson(testFile)
|
|
137
|
-
assert.deepEqual(result, { key: 'value' })
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('should throw for a non-existent file', async () => {
|
|
141
|
-
await assert.rejects(
|
|
142
|
-
AdaptFrameworkUtils.readJson('/nonexistent/file.json'),
|
|
143
|
-
{ code: 'ENOENT' }
|
|
144
|
-
)
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
describe('#writeJson()', () => {
|
|
149
|
-
const testFile = path.join(__dirname, 'data', 'test-write.json')
|
|
150
|
-
|
|
151
|
-
after(async () => {
|
|
152
|
-
await fs.rm(testFile, { force: true })
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('should write formatted JSON to a file', async () => {
|
|
156
|
-
const data = { hello: 'world', num: 42 }
|
|
157
|
-
await AdaptFrameworkUtils.writeJson(testFile, data)
|
|
158
|
-
const content = await fs.readFile(testFile, 'utf8')
|
|
159
|
-
assert.deepEqual(JSON.parse(content), data)
|
|
160
|
-
assert.ok(content.includes('\n'), 'should be formatted with indentation')
|
|
161
|
-
})
|
|
162
|
-
})
|
|
163
|
-
})
|