adapt-authoring-docs 1.3.1 → 1.4.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.
@@ -25,7 +25,7 @@ jobs:
25
25
  - name: Update npm
26
26
  run: npm install -g npm@latest
27
27
  - name: Install dependencies
28
- run: npm ci
28
+ run: npm install
29
29
  - name: Release
30
30
  env:
31
31
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,4 +1,4 @@
1
- name: Standard.js formatting check
1
+ name: Lint
2
2
  on: push
3
3
  jobs:
4
4
  default:
@@ -8,6 +8,5 @@ jobs:
8
8
  - uses: actions/setup-node@master
9
9
  with:
10
10
  node-version: 'lts/*'
11
- cache: 'npm'
12
- - run: npm ci
13
- - run: npx standard
11
+ - run: npm install
12
+ - run: npx standard
@@ -10,6 +10,5 @@ jobs:
10
10
  - uses: actions/setup-node@v4
11
11
  with:
12
12
  node-version: 'lts/*'
13
- cache: 'npm'
14
- - run: npm ci
13
+ - run: npm install
15
14
  - run: npm test
package/bin/docgen.js CHANGED
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * Generates documentation for the installed modules.
4
4
  */
5
- import { App } from 'adapt-authoring-core'
5
+ import { readJson } from 'adapt-authoring-core'
6
+ import { loadDependencies, loadConfigDefaults, loadSchemas, loadErrors, buildRouterTree, buildPermissions } from '../lib/docsData.js'
6
7
  import docsify from '../docsify/docsify.js'
7
8
  import fs from 'fs/promises'
8
9
  import jsdoc3 from '../jsdoc3/jsdoc3.js'
@@ -11,10 +12,13 @@ import swagger from '../swagger/swagger.js'
11
12
 
12
13
  const DEBUG = process.argv.includes('--verbose')
13
14
 
14
- process.env.NODE_ENV ??= 'production'
15
- process.env.ADAPT_AUTHORING_LOGGER__mute = !DEBUG
15
+ function getArg (name) {
16
+ const idx = process.argv.indexOf(name)
17
+ return idx !== -1 && idx + 1 < process.argv.length ? process.argv[idx + 1] : undefined
18
+ }
19
+
20
+ const rootDir = path.resolve(getArg('--rootDir') || process.cwd())
16
21
 
17
- const app = App.instance
18
22
  let outputdir
19
23
 
20
24
  const defaultPages = { // populated in cacheConfigs
@@ -27,10 +31,10 @@ const defaultPages = { // populated in cacheConfigs
27
31
  * Documentation for a module can be enabled in:
28
32
  * package.json > adapt_authoring.documentation.enable
29
33
  */
30
- function cacheConfigs () {
34
+ function cacheConfigs (appData) {
31
35
  const cache = []
32
- const excludes = app.pkg.documentation.excludes ?? []
33
- Object.values(app.dependencies).forEach(dep => {
36
+ const excludes = appData.pkg.documentation.excludes ?? []
37
+ Object.values(appData.dependencies).forEach(dep => {
34
38
  const c = dep.documentation
35
39
 
36
40
  let omitMsg
@@ -49,16 +53,16 @@ function cacheConfigs () {
49
53
  ...c,
50
54
  name: dep.name,
51
55
  version: dep.version,
52
- module: !!app.dependencyloader.instances[dep.name],
56
+ module: dep.module !== false,
53
57
  rootDir: dep.rootDir,
54
58
  includes: c.includes || {}
55
59
  })
56
60
  })
57
61
  cache.push({
58
- ...app.pkg.documentation,
62
+ ...appData.pkg.documentation,
59
63
  enable: true,
60
64
  name: 'adapt-authoring',
61
- rootDir: app.rootDir,
65
+ rootDir: appData.rootDir,
62
66
  includes: {}
63
67
  })
64
68
  return cache
@@ -71,19 +75,31 @@ async function copyRootFiles () {
71
75
  }
72
76
 
73
77
  async function docs () {
74
- console.log(`Generating documentation for ${app.pkg.name}@${app.pkg.version} ${DEBUG ? ' :: DEBUG' : ''}`)
78
+ const dependencies = await loadDependencies(rootDir)
79
+ const pkg = { ...await readJson(path.join(rootDir, 'package.json')), ...await readJson(path.join(rootDir, 'adapt-authoring.json')) }
80
+ const config = await loadConfigDefaults(dependencies)
81
+ const schemas = await loadSchemas(dependencies)
82
+ const errors = await loadErrors(dependencies)
83
+ const routerTree = await buildRouterTree(dependencies)
84
+ const permissions = buildPermissions(routerTree)
75
85
 
76
- try {
77
- await app.onReady()
78
- } catch (e) {
79
- console.log(`App failed to start, cannot continue.\n${e}`)
80
- process.exit(1)
86
+ const appData = {
87
+ rootDir,
88
+ pkg,
89
+ dependencies,
90
+ config,
91
+ errors,
92
+ schemas,
93
+ routerTree,
94
+ permissions
81
95
  }
82
- const config = await app.waitForModule('config')
96
+
97
+ console.log(`Generating documentation for ${appData.pkg.name}@${appData.pkg.version} ${DEBUG ? ' :: DEBUG' : ''}`)
98
+
83
99
  const { name } = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url)))
84
- outputdir = path.resolve(process.cwd(), config.get(`${name}.outputDir`))
100
+ outputdir = path.resolve(process.cwd(), getArg('--outputDir') || appData.config.get(`${name}.outputDir`))
85
101
 
86
- const cachedConfigs = cacheConfigs()
102
+ const cachedConfigs = cacheConfigs(appData)
87
103
 
88
104
  console.log('\nThis might take a minute or two...\n')
89
105
 
@@ -91,9 +107,9 @@ async function docs () {
91
107
  await fs.rm(outputdir, { recursive: true, force: true })
92
108
  await fs.mkdir(outputdir)
93
109
  await copyRootFiles()
94
- await jsdoc3(app, cachedConfigs, outputdir, defaultPages)
95
- await docsify(app, cachedConfigs, outputdir, defaultPages)
96
- await swagger(app, cachedConfigs, outputdir)
110
+ await jsdoc3(appData, cachedConfigs, outputdir, defaultPages)
111
+ await docsify(appData, cachedConfigs, outputdir, defaultPages)
112
+ await swagger(appData, cachedConfigs, outputdir)
97
113
  } catch (e) {
98
114
  console.log(e)
99
115
  process.exit(1)
package/bin/docserve.js CHANGED
@@ -1,51 +1,39 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Generates an HTTP server for viewing the local copy of the documentation (note these must be build first with `at-docgen`)
3
+ * Generates an HTTP server for viewing the local copy of the documentation (note these must be built first with `at-docgen`)
4
4
  */
5
- import { App } from 'adapt-authoring-core'
6
5
  import { spawn } from 'child_process'
7
6
  import http from 'http-server'
8
7
  import path from 'path'
9
- /*
10
- function getMime (filePath) {
11
- const ext = path.parse(filePath).ext
12
- return {
13
- '.ico': 'image/x-icon',
14
- '.html': 'text/html',
15
- '.js': 'text/javascript',
16
- '.json': 'application/json',
17
- '.css': 'text/css',
18
- '.png': 'image/png',
19
- '.jpg': 'image/jpeg',
20
- '.svg': 'image/svg+xml',
21
- '.pdf': 'application/pdf',
22
- '.doc': 'application/msword'
23
- }[ext] || 'text/plain'
8
+
9
+ function getArg (name) {
10
+ const idx = process.argv.indexOf(name)
11
+ return idx !== -1 && idx + 1 < process.argv.length ? process.argv[idx + 1] : undefined
24
12
  }
25
- */
26
- process.env.NODE_ENV ??= 'production'
27
- process.env.ADAPT_AUTHORING_LOGGER__mute = true
28
13
 
29
- console.log('Starting app, please wait\n')
30
- // TODO remove need to start app
31
- App.instance.onReady().then(async app => {
32
- console.log('App started\n');
33
- (await app.waitForModule('server')).close() // close connections so we can still run the app separately
14
+ async function getOutputDir () {
15
+ const arg = getArg('--outputDir')
16
+ if (arg) return path.resolve(arg)
17
+ const { loadDependencies, loadConfigDefaults } = await import('../lib/docsData.js')
18
+ const rootDir = path.resolve(getArg('--rootDir') || process.cwd())
19
+ const dependencies = await loadDependencies(rootDir)
20
+ const config = await loadConfigDefaults(dependencies)
21
+ return path.resolve(config.get('adapt-authoring-docs.outputDir'))
22
+ }
34
23
 
35
- const ROOT = path.resolve(app.config.get('adapt-authoring-docs.outputDir'))
36
- const PORT = 9000
37
- const OPEN = process.argv.some(a => a === '--open')
24
+ const ROOT = await getOutputDir()
25
+ const PORT = Number(getArg('--port')) || 9000
26
+ const OPEN = process.argv.some(a => a === '--open')
38
27
 
39
- const server = http.createServer({ root: ROOT, cache: -1 })
28
+ const server = http.createServer({ root: ROOT, cache: -1 })
40
29
 
41
- server.listen(PORT, () => {
42
- const url = `http://localhost:${PORT}`
43
- console.log(`Docs hosted at ${url}`)
30
+ server.listen(PORT, () => {
31
+ const url = `http://localhost:${PORT}`
32
+ console.log(`Docs hosted at ${url}`)
44
33
 
45
- if (OPEN) {
46
- const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
47
- spawn(`${command} ${url}`, { shell: true })
48
- .on('error', e => console.log('spawn error', e))
49
- }
50
- })
34
+ if (OPEN) {
35
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
36
+ spawn(`${command} ${url}`, { shell: true })
37
+ .on('error', e => console.log('spawn error', e))
38
+ }
51
39
  })
@@ -20,9 +20,9 @@ function generateSectionTitle (sectionName) {
20
20
  /**
21
21
  * Copies all doc files ready for the generator
22
22
  */
23
- export default async function docsify (app, configs, outputdir, defaultPages) {
23
+ export default async function docsify (appData, configs, outputdir, defaultPages) {
24
24
  const dir = path.resolve(outputdir, 'manual')
25
- const sectionsConf = app.config.get('adapt-authoring-docs.manualSections')
25
+ const sectionsConf = appData.config.get('adapt-authoring-docs.manualSections')
26
26
  const defaultSection = Object.entries(sectionsConf).find(([, data]) => data.default)?.[0]
27
27
  /**
28
28
  * init docsify folder
@@ -39,7 +39,7 @@ export default async function docsify (app, configs, outputdir, defaultPages) {
39
39
  try {
40
40
  const wrapper = await new DocsifyPluginWrapper({
41
41
  ...c,
42
- app,
42
+ app: appData,
43
43
  docsRootDir: outputdir,
44
44
  pluginEntry: path.resolve(c.rootDir, p),
45
45
  outputDir: dir
package/jsdoc3/jsdoc3.js CHANGED
@@ -14,7 +14,7 @@ function resolvePath (relativePath) {
14
14
  return fileURLToPath(new URL(relativePath, import.meta.url))
15
15
  }
16
16
 
17
- async function writeConfig (app, outputdir, indexFile) {
17
+ async function writeConfig (appData, outputdir, indexFile) {
18
18
  return fs.writeFile(configPath, JSON.stringify({
19
19
  source: {
20
20
  include: getSourceIncludes(indexFile)
@@ -25,7 +25,7 @@ async function writeConfig (app, outputdir, indexFile) {
25
25
  search: true,
26
26
  static: true,
27
27
  menu: {
28
- [`<img class="logo" src="assets/logo-outline-colour.png" />Adapt authoring tool back-end API documentation<br><span class="version">v${app.pkg.version}</span>`]: {
28
+ [`<img class="logo" src="assets/logo-outline-colour.png" />Adapt authoring tool back-end API documentation<br><span class="version">v${appData.pkg.version}</span>`]: {
29
29
  class: 'menu-title'
30
30
  },
31
31
  'Documentation home': {
@@ -60,7 +60,7 @@ async function writeConfig (app, outputdir, indexFile) {
60
60
  meta: {
61
61
  title: 'Adapt authoring tool UI documentation',
62
62
  description: 'Adapt authoring tool UI documentation',
63
- keyword: `v${app.pkg.version}`
63
+ keyword: `v${appData.pkg.version}`
64
64
  },
65
65
  scripts: [
66
66
  'styles/adapt.css',
@@ -88,10 +88,10 @@ function getSourceIncludes (indexFile) {
88
88
  return includes
89
89
  }
90
90
 
91
- export default async function jsdoc3 (app, configs, outputdir, defaultPages) {
91
+ export default async function jsdoc3 (appData, configs, outputdir, defaultPages) {
92
92
  cachedConfigs = configs
93
93
  const dir = `${outputdir}/backend`
94
- await writeConfig(app, dir, defaultPages.sourceIndex)
94
+ await writeConfig(appData, dir, defaultPages.sourceIndex)
95
95
  try {
96
96
  await execPromise(`npx jsdoc -c ${configPath}`)
97
97
  } catch (e) {
@@ -0,0 +1,298 @@
1
+ import { loadDependencyFiles, readJson } from 'adapt-authoring-core'
2
+ import { glob } from 'glob'
3
+ import os from 'os'
4
+ import path from 'path'
5
+ import { pathToRegexp } from 'path-to-regexp'
6
+
7
+ /**
8
+ * Globs node_modules for adapt-authoring.json files, merges each with its
9
+ * package.json and returns a { [name]: config } map.
10
+ * @param {string} rootDir Absolute path to the app root
11
+ * @returns {Promise<Object>}
12
+ */
13
+ export async function loadDependencies (rootDir) {
14
+ const files = await glob('node_modules/**/adapt-authoring.json', {
15
+ cwd: rootDir,
16
+ absolute: true
17
+ })
18
+ const seen = new Set()
19
+ const deps = {}
20
+ const sorted = files.sort((a, b) => a.length - b.length)
21
+ for (const file of sorted) {
22
+ const dir = path.dirname(file)
23
+ try {
24
+ const pkg = await readJson(path.join(dir, 'package.json'))
25
+ if (seen.has(pkg.name)) continue
26
+ seen.add(pkg.name)
27
+ const meta = await readJson(file)
28
+ deps[pkg.name] = { ...pkg, ...meta, rootDir: dir }
29
+ } catch {
30
+ // skip modules with unreadable config
31
+ }
32
+ }
33
+ return deps
34
+ }
35
+
36
+ /**
37
+ * Reads conf/config.schema.json from each dependency, extracts default values,
38
+ * and returns a { get(key) } object. Resolves $TEMP to os.tmpdir().
39
+ * @param {Object} dependencies Dependencies map from loadDependencies
40
+ * @returns {Promise<Object>}
41
+ */
42
+ export async function loadConfigDefaults (dependencies) {
43
+ const allConfigs = await loadDependencyFiles('conf/config.schema.json', {
44
+ parse: true,
45
+ dependencies
46
+ })
47
+ const defaults = {}
48
+ for (const [depName, [schema]] of Object.entries(allConfigs)) {
49
+ const props = schema.properties || {}
50
+ const modDefaults = {}
51
+ for (const [key, prop] of Object.entries(props)) {
52
+ if (prop.default !== undefined) modDefaults[key] = prop.default
53
+ }
54
+ if (Object.keys(modDefaults).length) defaults[depName] = modDefaults
55
+ }
56
+ return {
57
+ get: (key) => {
58
+ const dotIdx = key.indexOf('.')
59
+ if (dotIdx === -1) return defaults[key]
60
+ const modName = key.slice(0, dotIdx)
61
+ const propKey = key.slice(dotIdx + 1)
62
+ const val = defaults[modName]?.[propKey]
63
+ if (typeof val === 'string') return val.replace('$TEMP', os.tmpdir())
64
+ return val
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Loads and merges all schema/*.schema.json files across dependencies.
71
+ * @param {Object} dependencies Dependencies map from loadDependencies
72
+ * @returns {Promise<Object>} { schemas, raw, getSchema }
73
+ */
74
+ export async function loadSchemas (dependencies) {
75
+ const allSchemas = await loadDependencyFiles('schema/*.schema.json', {
76
+ parse: true,
77
+ dependencies
78
+ })
79
+ const schemaMap = {}
80
+ const rawMap = {}
81
+ for (const [, arr] of Object.entries(allSchemas)) {
82
+ for (const schema of arr) {
83
+ const name = schema.$anchor || schema.$id || 'unknown'
84
+ schemaMap[name] = true
85
+ rawMap[name] = schema
86
+ }
87
+ }
88
+ return {
89
+ schemas: schemaMap,
90
+ raw: rawMap,
91
+ getSchema: async (name) => ({ built: rawMap[name] || {} })
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Loads and merges all errors/*.json files across dependencies.
97
+ * @param {Object} dependencies Dependencies map from loadDependencies
98
+ * @returns {Promise<Object>} Merged error map
99
+ */
100
+ export async function loadErrors (dependencies) {
101
+ const allErrors = await loadDependencyFiles('errors/*.json', {
102
+ parse: true,
103
+ dependencies
104
+ })
105
+ const merged = {}
106
+ for (const arr of Object.values(allErrors)) {
107
+ for (const obj of arr) Object.assign(merged, obj)
108
+ }
109
+ // Match the AdaptError shape that plugins expect:
110
+ // { code, statusCode, meta: { description, data } }
111
+ return Object.entries(merged)
112
+ .sort()
113
+ .reduce((m, [code, { description, statusCode, data }]) => {
114
+ const meta = { description }
115
+ if (data) meta.data = data
116
+ m[code] = { code, statusCode, meta }
117
+ return m
118
+ }, {})
119
+ }
120
+
121
+ /**
122
+ * Reads routes.json files from dependencies and assembles an Express-like
123
+ * { path, routes, childRouters } tree rooted at /api.
124
+ * @param {Object} dependencies Dependencies map from loadDependencies
125
+ * @returns {Promise<Object>} Router tree
126
+ */
127
+ export async function buildRouterTree (dependencies) {
128
+ const apiRouter = { path: '/api', routes: [], childRouters: [] }
129
+ let authRouter = null
130
+ let apiDefaultRoutes = null
131
+ let authDefaultRoutes = null
132
+
133
+ const authDep = dependencies['adapt-authoring-auth']
134
+ const apiDep = dependencies['adapt-authoring-api']
135
+
136
+ if (apiDep) {
137
+ try {
138
+ apiDefaultRoutes = await readJson(path.join(apiDep.rootDir, 'lib', 'default-routes.json'))
139
+ } catch { /* no defaults */ }
140
+ }
141
+ if (authDep) {
142
+ try {
143
+ authDefaultRoutes = await readJson(path.join(authDep.rootDir, 'lib', 'default-routes.json'))
144
+ } catch { /* no defaults */ }
145
+ try {
146
+ const coreAuthConfig = await readJson(path.join(authDep.rootDir, 'lib', 'routes.json'))
147
+ authRouter = {
148
+ path: '/api/auth',
149
+ routes: (coreAuthConfig.routes || []).map(r => staticRouteEntry(r)),
150
+ childRouters: []
151
+ }
152
+ apiRouter.childRouters.push(authRouter)
153
+ } catch { /* no auth routes */ }
154
+ }
155
+
156
+ const allRoutes = await loadDependencyFiles('routes.json', {
157
+ parse: true,
158
+ dependencies
159
+ })
160
+ for (const [depName, [config]] of Object.entries(allRoutes)) {
161
+ if (dependencies[depName].module === false) continue
162
+ if (config.type !== undefined) {
163
+ addAuthTypeRouter(config, authRouter, authDefaultRoutes)
164
+ } else if (config.root) {
165
+ addApiRouter(config, apiRouter, apiDefaultRoutes)
166
+ }
167
+ }
168
+ return apiRouter
169
+ }
170
+
171
+ /**
172
+ * Walks the router tree and builds a permission store with
173
+ * { get: [], post: [], ... } arrays of [regexp, scopes] tuples.
174
+ * @param {Object} routerTree Router tree from buildRouterTree
175
+ * @returns {Object} Permission store
176
+ */
177
+ export function buildPermissions (routerTree) {
178
+ const store = { get: [], post: [], put: [], patch: [], delete: [] }
179
+ walkRouterPermissions(routerTree, store)
180
+ return store
181
+ }
182
+
183
+ // ------- internal helpers -------
184
+
185
+ function addApiRouter (config, parentRouter, defaultRoutes) {
186
+ const root = config.root
187
+ const scope = config.permissionsScope || root
188
+ const schemaName = config.schemaName ?? root
189
+ const collectionName = config.collectionName ?? root
190
+
191
+ let routes = config.routes || []
192
+
193
+ if (config.useDefaultRoutes !== false && defaultRoutes) {
194
+ routes = mergeWithDefaults(routes, defaultRoutes.routes || [])
195
+ }
196
+
197
+ /* eslint-disable no-template-curly-in-string */
198
+ const replacements = {
199
+ '${scope}': scope,
200
+ '${schemaName}': schemaName,
201
+ '${collectionName}': collectionName
202
+ }
203
+ /* eslint-enable no-template-curly-in-string */
204
+
205
+ const childRouter = {
206
+ path: `/api/${root}`,
207
+ routes: routes.map(r => staticRouteEntry(replacePlaceholders(r, replacements))),
208
+ childRouters: []
209
+ }
210
+ parentRouter.childRouters.push(childRouter)
211
+ }
212
+
213
+ function addAuthTypeRouter (config, authRouter, defaultRoutes) {
214
+ if (!authRouter) return
215
+ const type = config.type
216
+ let routes = config.routes || []
217
+
218
+ if (defaultRoutes) {
219
+ routes = mergeWithDefaults(routes, defaultRoutes.routes || [])
220
+ }
221
+
222
+ const childRouter = {
223
+ path: `/api/auth/${type}`,
224
+ routes: routes.map(r => staticRouteEntry(r)),
225
+ childRouters: []
226
+ }
227
+ authRouter.childRouters.push(childRouter)
228
+ }
229
+
230
+ function mergeWithDefaults (customRoutes, defaultRoutes) {
231
+ const overrides = new Map(
232
+ customRoutes.filter(r => r.override).map(r => [r.route, r])
233
+ )
234
+ const matched = new Set()
235
+ const mergedDefaults = defaultRoutes.map(d => {
236
+ const o = overrides.get(d.route)
237
+ if (!o) return d
238
+ matched.add(d.route)
239
+ const { override, ...rest } = o
240
+ return { ...d, ...rest, handlers: { ...d.handlers, ...rest.handlers } }
241
+ })
242
+ const remaining = customRoutes.filter(r => !r.override || !matched.has(r.route))
243
+ return [...mergedDefaults, ...remaining]
244
+ }
245
+
246
+ function staticRouteEntry (routeDef) {
247
+ const handlers = {}
248
+ if (routeDef.handlers) {
249
+ for (const method of Object.keys(routeDef.handlers)) {
250
+ handlers[method] = () => {}
251
+ }
252
+ }
253
+ return {
254
+ route: routeDef.route,
255
+ handlers,
256
+ meta: routeDef.meta || {},
257
+ internal: routeDef.internal || false,
258
+ permissions: routeDef.permissions || {}
259
+ }
260
+ }
261
+
262
+ function walkRouterPermissions (router, store) {
263
+ for (const routeDef of router.routes) {
264
+ const fullRoute = `${router.path}${routeDef.route !== '/' ? routeDef.route : ''}`
265
+ for (const [method, scopes] of Object.entries(routeDef.permissions || {})) {
266
+ if (scopes === null) continue
267
+ const m = method.toLowerCase()
268
+ if (!store[m]) continue
269
+ try {
270
+ const { regexp } = pathToRegexp(fullRoute)
271
+ store[m].push([regexp, scopes])
272
+ } catch {
273
+ // skip routes that can't be compiled (unusual patterns)
274
+ }
275
+ }
276
+ }
277
+ for (const child of router.childRouters) {
278
+ walkRouterPermissions(child, store)
279
+ }
280
+ }
281
+
282
+ function replacePlaceholders (obj, replacements) {
283
+ if (typeof obj === 'string') {
284
+ return Object.entries(replacements).reduce(
285
+ (s, [k, v]) => v != null ? s.replaceAll(k, v) : s,
286
+ obj
287
+ )
288
+ }
289
+ if (Array.isArray(obj)) {
290
+ return obj.map(item => replacePlaceholders(item, replacements))
291
+ }
292
+ if (obj && typeof obj === 'object' && obj.constructor === Object) {
293
+ return Object.fromEntries(
294
+ Object.entries(obj).map(([k, v]) => [k, replacePlaceholders(v, replacements)])
295
+ )
296
+ }
297
+ return obj
298
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-docs",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Tools for auto-generating documentation for the Adapt authoring tool",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-docs",
6
6
  "license": "GPL-3.0",
@@ -14,6 +14,7 @@
14
14
  "test": "node --test 'tests/**/*.spec.js'"
15
15
  },
16
16
  "dependencies": {
17
+ "adapt-authoring-core": "^2.0.0",
17
18
  "comment-parser": "^1.4.1",
18
19
  "docdash": "^2.0.2",
19
20
  "docsify-cli": "^4.4.4",
@@ -21,16 +22,9 @@
21
22
  "glob": "^13.0.0",
22
23
  "http-server": "^14.1.1",
23
24
  "jsdoc": "^4.0.3",
25
+ "path-to-regexp": "^8.0.0",
24
26
  "swagger-ui": "^5.17.14"
25
27
  },
26
- "peerDependencies": {
27
- "adapt-authoring-core": "^1.7.0"
28
- },
29
- "peerDependenciesMeta": {
30
- "adapt-authoring-core": {
31
- "optional": true
32
- }
33
- },
34
28
  "devDependencies": {
35
29
  "@semantic-release/git": "^10.0.1",
36
30
  "conventional-changelog-eslint": "^6.0.0",
@@ -8,14 +8,12 @@ function resolvePath (relativePath) {
8
8
  /**
9
9
  *
10
10
  */
11
- export default async function swagger (app, configs, outputdir) {
12
- const server = await app.waitForModule('server')
13
- await app.onReady()
11
+ export default async function swagger (appData, configs, outputdir) {
14
12
  const spec = {
15
13
  openapi: '3.0.3',
16
- info: { version: app.pkg.version },
17
- components: { schemas: await generateSchemaSpec(app) },
18
- paths: generatePathSpec(app, server.api)
14
+ info: { version: appData.pkg.version },
15
+ components: { schemas: await generateSchemaSpec(appData.schemas) },
16
+ paths: generatePathSpec(appData.permissions, appData.routerTree)
19
17
  }
20
18
  // generate UI
21
19
  const dir = path.resolve(outputdir, 'rest')
@@ -38,8 +36,7 @@ export default async function swagger (app, configs, outputdir) {
38
36
  ])
39
37
  }
40
38
 
41
- async function generateSchemaSpec (app) {
42
- const jsonschema = await app.waitForModule('jsonschema')
39
+ async function generateSchemaSpec (jsonschema) {
43
40
  const schemas = {}
44
41
  await Promise.all(Object.keys(jsonschema.schemas).map(async s => {
45
42
  schemas[s] = sanitiseSchema((await jsonschema.getSchema(s)).built)
@@ -57,8 +54,7 @@ function sanitiseSchema (schema) {
57
54
  return schema
58
55
  }
59
56
 
60
- function generatePathSpec (app, router, paths = {}) {
61
- const perms = app.dependencyloader.instances['adapt-authoring-auth'].permissions.routes
57
+ function generatePathSpec (permissions, router, paths = {}) {
62
58
  router.routes.forEach(r => {
63
59
  const parameters = r.route.split('/').filter(r => r.startsWith(':')).map(r => {
64
60
  return {
@@ -70,7 +66,7 @@ function generatePathSpec (app, router, paths = {}) {
70
66
  const route = `${router.path}${r.route !== '/' ? r.route : ''}`
71
67
  paths[route] = Object.keys(r.handlers).reduce((memo, method) => {
72
68
  const meta = r.meta?.[method] || {}
73
- const scopes = perms[method].find(p => route.match(p[0]))?.[1] || []
69
+ const scopes = permissions[method].find(p => route.match(p[0]))?.[1] || []
74
70
  let description = r.internal ? 'ROUTE IS ONLY ACCESSIBLE FROM LOCALHOST.<br/><br/>' : ''
75
71
  description += scopes.length
76
72
  ? `Required scopes: ${scopes.map(s => `<span>${s}</span>`).join(' ')}`
@@ -90,7 +86,7 @@ function generatePathSpec (app, router, paths = {}) {
90
86
  }, {})
91
87
  })
92
88
  if (router.childRouters.length) {
93
- router.childRouters.forEach(childRouter => generatePathSpec(app, childRouter, paths))
89
+ router.childRouters.forEach(childRouter => generatePathSpec(permissions, childRouter, paths))
94
90
  }
95
91
  return Object.keys(paths).sort().reduce((m, k) => Object.assign(m, { [k]: paths[k] }), {})
96
92
  }
@@ -0,0 +1,462 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it, before, after } from 'node:test'
3
+ import fs from 'fs/promises'
4
+ import os from 'os'
5
+ import path from 'path'
6
+
7
+ /* eslint-disable no-template-curly-in-string */
8
+ /**
9
+ * Creates a temporary fixture directory that simulates a minimal
10
+ * adapt-authoring app so docsData functions can be tested in isolation.
11
+ */
12
+ async function createFixture () {
13
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'docs-data-'))
14
+
15
+ // root package.json + adapt-authoring.json
16
+ await fs.writeFile(path.join(root, 'package.json'), JSON.stringify({
17
+ name: 'adapt-authoring',
18
+ version: '1.0.0'
19
+ }))
20
+ await fs.writeFile(path.join(root, 'adapt-authoring.json'), JSON.stringify({
21
+ module: false,
22
+ documentation: { enable: true },
23
+ essentialApis: []
24
+ }))
25
+
26
+ // --- fake api module (library, not a loaded module) ---
27
+ const apiDir = path.join(root, 'node_modules', 'adapt-authoring-api')
28
+ await fs.mkdir(path.join(apiDir, 'lib'), { recursive: true })
29
+ await fs.writeFile(path.join(apiDir, 'package.json'), JSON.stringify({
30
+ name: 'adapt-authoring-api',
31
+ version: '1.0.0'
32
+ }))
33
+ await fs.writeFile(path.join(apiDir, 'adapt-authoring.json'), JSON.stringify({
34
+ module: false,
35
+ documentation: { enable: true }
36
+ }))
37
+ await fs.writeFile(path.join(apiDir, 'lib', 'default-routes.json'), JSON.stringify({
38
+ routes: [
39
+ {
40
+ route: '/',
41
+ handlers: { post: 'requestHandler', get: 'queryHandler' },
42
+ permissions: { post: ['write:${scope}'], get: ['read:${scope}'] }
43
+ },
44
+ {
45
+ route: '/:_id',
46
+ handlers: { get: 'requestHandler', delete: 'requestHandler' },
47
+ permissions: { get: ['read:${scope}'], delete: ['write:${scope}'] }
48
+ }
49
+ ]
50
+ }))
51
+
52
+ // --- fake auth module ---
53
+ const authDir = path.join(root, 'node_modules', 'adapt-authoring-auth')
54
+ await fs.mkdir(path.join(authDir, 'lib'), { recursive: true })
55
+ await fs.writeFile(path.join(authDir, 'package.json'), JSON.stringify({
56
+ name: 'adapt-authoring-auth',
57
+ version: '1.0.0'
58
+ }))
59
+ await fs.writeFile(path.join(authDir, 'adapt-authoring.json'), JSON.stringify({
60
+ documentation: { enable: true }
61
+ }))
62
+ await fs.writeFile(path.join(authDir, 'lib', 'routes.json'), JSON.stringify({
63
+ routes: [
64
+ {
65
+ route: '/check',
66
+ handlers: { get: 'checkHandler' },
67
+ permissions: { get: null }
68
+ }
69
+ ]
70
+ }))
71
+ await fs.writeFile(path.join(authDir, 'lib', 'default-routes.json'), JSON.stringify({
72
+ routes: [
73
+ {
74
+ route: '/',
75
+ handlers: { post: 'authenticateHandler' },
76
+ permissions: { post: null }
77
+ },
78
+ {
79
+ route: '/register',
80
+ handlers: { post: 'registerHandler' },
81
+ permissions: { post: ['register:users'] }
82
+ }
83
+ ]
84
+ }))
85
+
86
+ // --- fake auth-local module (auth type) ---
87
+ const authLocalDir = path.join(root, 'node_modules', 'adapt-authoring-auth-local')
88
+ await fs.mkdir(authLocalDir, { recursive: true })
89
+ await fs.writeFile(path.join(authLocalDir, 'package.json'), JSON.stringify({
90
+ name: 'adapt-authoring-auth-local',
91
+ version: '1.0.0'
92
+ }))
93
+ await fs.writeFile(path.join(authLocalDir, 'adapt-authoring.json'), JSON.stringify({
94
+ documentation: { enable: true }
95
+ }))
96
+ await fs.writeFile(path.join(authLocalDir, 'routes.json'), JSON.stringify({
97
+ type: 'local',
98
+ routes: [
99
+ {
100
+ route: '/',
101
+ override: true,
102
+ handlers: { post: 'authenticateHandler' },
103
+ meta: { post: { summary: 'Local auth' } }
104
+ },
105
+ {
106
+ route: '/changepass',
107
+ handlers: { post: 'changePasswordHandler' },
108
+ permissions: { post: null }
109
+ }
110
+ ]
111
+ }))
112
+
113
+ // --- fake content module (API module) ---
114
+ const contentDir = path.join(root, 'node_modules', 'adapt-authoring-content')
115
+ await fs.mkdir(path.join(contentDir, 'schema'), { recursive: true })
116
+ await fs.mkdir(path.join(contentDir, 'errors'), { recursive: true })
117
+ await fs.mkdir(path.join(contentDir, 'conf'), { recursive: true })
118
+ await fs.writeFile(path.join(contentDir, 'package.json'), JSON.stringify({
119
+ name: 'adapt-authoring-content',
120
+ version: '1.0.0'
121
+ }))
122
+ await fs.writeFile(path.join(contentDir, 'adapt-authoring.json'), JSON.stringify({
123
+ documentation: { enable: true }
124
+ }))
125
+ await fs.writeFile(path.join(contentDir, 'routes.json'), JSON.stringify({
126
+ root: 'content',
127
+ routes: [
128
+ {
129
+ route: '/clone',
130
+ handlers: { post: 'handleClone' },
131
+ permissions: { post: ['write:${scope}'] }
132
+ }
133
+ ]
134
+ }))
135
+ await fs.writeFile(path.join(contentDir, 'schema', 'content.schema.json'), JSON.stringify({
136
+ $anchor: 'content',
137
+ type: 'object',
138
+ properties: {
139
+ title: { type: 'string' },
140
+ body: { type: 'string' }
141
+ }
142
+ }))
143
+ await fs.writeFile(path.join(contentDir, 'errors', 'errors.json'), JSON.stringify({
144
+ CONTENT_NOT_FOUND: { statusCode: 404, description: 'Content not found' }
145
+ }))
146
+ await fs.writeFile(path.join(contentDir, 'conf', 'config.schema.json'), JSON.stringify({
147
+ type: 'object',
148
+ properties: {
149
+ cacheDir: { type: 'string', default: '$TEMP/content-cache' }
150
+ }
151
+ }))
152
+
153
+ // --- fake docs module with config ---
154
+ const docsDir = path.join(root, 'node_modules', 'adapt-authoring-docs')
155
+ await fs.mkdir(path.join(docsDir, 'conf'), { recursive: true })
156
+ await fs.writeFile(path.join(docsDir, 'package.json'), JSON.stringify({
157
+ name: 'adapt-authoring-docs',
158
+ version: '1.0.0'
159
+ }))
160
+ await fs.writeFile(path.join(docsDir, 'adapt-authoring.json'), JSON.stringify({
161
+ module: false,
162
+ documentation: { enable: true }
163
+ }))
164
+ await fs.writeFile(path.join(docsDir, 'conf', 'config.schema.json'), JSON.stringify({
165
+ type: 'object',
166
+ properties: {
167
+ outputDir: { type: 'string', default: '$TEMP/docs-build' },
168
+ manualSections: {
169
+ type: 'object',
170
+ default: { 'getting-started': {}, 'other-guides': { default: true } }
171
+ }
172
+ }
173
+ }))
174
+
175
+ // --- module with useDefaultRoutes: false ---
176
+ const courseassetDir = path.join(root, 'node_modules', 'adapt-authoring-courseassets')
177
+ await fs.mkdir(courseassetDir, { recursive: true })
178
+ await fs.writeFile(path.join(courseassetDir, 'package.json'), JSON.stringify({
179
+ name: 'adapt-authoring-courseassets',
180
+ version: '1.0.0'
181
+ }))
182
+ await fs.writeFile(path.join(courseassetDir, 'adapt-authoring.json'), JSON.stringify({
183
+ documentation: { enable: true }
184
+ }))
185
+ await fs.writeFile(path.join(courseassetDir, 'routes.json'), JSON.stringify({
186
+ root: 'courseassets',
187
+ useDefaultRoutes: false,
188
+ routes: [
189
+ {
190
+ route: '/query',
191
+ handlers: { post: 'queryHandler' },
192
+ permissions: { post: ['read:${scope}'] }
193
+ }
194
+ ]
195
+ }))
196
+
197
+ // --- module with custom permissionsScope ---
198
+ const themeDir = path.join(root, 'node_modules', 'adapt-authoring-coursetheme')
199
+ await fs.mkdir(themeDir, { recursive: true })
200
+ await fs.writeFile(path.join(themeDir, 'package.json'), JSON.stringify({
201
+ name: 'adapt-authoring-coursetheme',
202
+ version: '1.0.0'
203
+ }))
204
+ await fs.writeFile(path.join(themeDir, 'adapt-authoring.json'), JSON.stringify({
205
+ documentation: { enable: true }
206
+ }))
207
+ await fs.writeFile(path.join(themeDir, 'routes.json'), JSON.stringify({
208
+ root: 'coursethemepresets',
209
+ permissionsScope: 'content',
210
+ routes: [
211
+ {
212
+ route: '/:_id/apply',
213
+ handlers: { post: 'applyHandler' },
214
+ permissions: { post: ['write:${scope}'] }
215
+ }
216
+ ]
217
+ }))
218
+
219
+ return root
220
+ }
221
+
222
+ describe('docsData', () => {
223
+ let fixtureDir
224
+ let dependencies
225
+
226
+ before(async () => {
227
+ fixtureDir = await createFixture()
228
+ const { loadDependencies } = await import('../lib/docsData.js')
229
+ dependencies = await loadDependencies(fixtureDir)
230
+ })
231
+
232
+ after(async () => {
233
+ if (fixtureDir) await fs.rm(fixtureDir, { recursive: true, force: true }).catch(() => {})
234
+ })
235
+
236
+ describe('loadDependencies', () => {
237
+ it('should discover all modules with adapt-authoring.json', () => {
238
+ assert.ok(dependencies['adapt-authoring-content'])
239
+ assert.ok(dependencies['adapt-authoring-auth'])
240
+ assert.ok(dependencies['adapt-authoring-auth-local'])
241
+ assert.ok(dependencies['adapt-authoring-docs'])
242
+ assert.ok(dependencies['adapt-authoring-api'])
243
+ })
244
+
245
+ it('should merge package.json and adapt-authoring.json for each dep', () => {
246
+ const content = dependencies['adapt-authoring-content']
247
+ assert.equal(content.name, 'adapt-authoring-content')
248
+ assert.equal(content.version, '1.0.0')
249
+ assert.ok(content.documentation)
250
+ assert.ok(content.rootDir)
251
+ })
252
+
253
+ it('should set rootDir on each dependency', () => {
254
+ for (const dep of Object.values(dependencies)) {
255
+ assert.ok(dep.rootDir, `${dep.name} should have rootDir`)
256
+ assert.ok(dep.rootDir.includes('node_modules'), `${dep.name} rootDir should be in node_modules`)
257
+ }
258
+ })
259
+ })
260
+
261
+ describe('loadConfigDefaults', () => {
262
+ it('should resolve $TEMP to os.tmpdir()', async () => {
263
+ const { loadConfigDefaults } = await import('../lib/docsData.js')
264
+ const config = await loadConfigDefaults(dependencies)
265
+ const outputDir = config.get('adapt-authoring-docs.outputDir')
266
+ assert.ok(outputDir.startsWith(os.tmpdir()), `expected ${outputDir} to start with ${os.tmpdir()}`)
267
+ assert.ok(outputDir.endsWith('/docs-build'))
268
+ })
269
+
270
+ it('should return object defaults', async () => {
271
+ const { loadConfigDefaults } = await import('../lib/docsData.js')
272
+ const config = await loadConfigDefaults(dependencies)
273
+ const sections = config.get('adapt-authoring-docs.manualSections')
274
+ assert.ok(sections)
275
+ assert.ok(sections['getting-started'] !== undefined)
276
+ assert.ok(sections['other-guides']?.default === true)
277
+ })
278
+
279
+ it('should return undefined for unknown config keys', async () => {
280
+ const { loadConfigDefaults } = await import('../lib/docsData.js')
281
+ const config = await loadConfigDefaults(dependencies)
282
+ assert.equal(config.get('nonexistent.key'), undefined)
283
+ })
284
+
285
+ it('should resolve $TEMP in non-docs modules', async () => {
286
+ const { loadConfigDefaults } = await import('../lib/docsData.js')
287
+ const config = await loadConfigDefaults(dependencies)
288
+ const cacheDir = config.get('adapt-authoring-content.cacheDir')
289
+ assert.ok(cacheDir.startsWith(os.tmpdir()))
290
+ assert.ok(cacheDir.endsWith('/content-cache'))
291
+ })
292
+ })
293
+
294
+ describe('buildRouterTree', () => {
295
+ it('should return api root router', async () => {
296
+ const { buildRouterTree } = await import('../lib/docsData.js')
297
+ const routerTree = await buildRouterTree(dependencies)
298
+ assert.equal(routerTree.path, '/api')
299
+ })
300
+
301
+ it('should create child routers for API modules', async () => {
302
+ const { buildRouterTree } = await import('../lib/docsData.js')
303
+ const routerTree = await buildRouterTree(dependencies)
304
+ const paths = routerTree.childRouters.map(r => r.path)
305
+ assert.ok(paths.includes('/api/content'))
306
+ })
307
+
308
+ it('should merge default routes for API modules', async () => {
309
+ const { buildRouterTree } = await import('../lib/docsData.js')
310
+ const routerTree = await buildRouterTree(dependencies)
311
+ const contentRouter = routerTree.childRouters.find(r => r.path === '/api/content')
312
+ assert.ok(contentRouter)
313
+ const routePaths = contentRouter.routes.map(r => r.route)
314
+ // default routes: / and /:_id, plus custom: /clone
315
+ assert.ok(routePaths.includes('/'), 'should have default root route')
316
+ assert.ok(routePaths.includes('/:_id'), 'should have default :_id route')
317
+ assert.ok(routePaths.includes('/clone'), 'should have custom clone route')
318
+ })
319
+
320
+ it('should skip default routes when useDefaultRoutes is false', async () => {
321
+ const { buildRouterTree } = await import('../lib/docsData.js')
322
+ const routerTree = await buildRouterTree(dependencies)
323
+ const router = routerTree.childRouters.find(r => r.path === '/api/courseassets')
324
+ assert.ok(router)
325
+ assert.equal(router.routes.length, 1)
326
+ assert.equal(router.routes[0].route, '/query')
327
+ })
328
+
329
+ it('should replace placeholders in route config', async () => {
330
+ const { buildRouterTree } = await import('../lib/docsData.js')
331
+ const routerTree = await buildRouterTree(dependencies)
332
+ const contentRouter = routerTree.childRouters.find(r => r.path === '/api/content')
333
+ const rootRoute = contentRouter.routes.find(r => r.route === '/')
334
+ assert.deepEqual(rootRoute.permissions.post, ['write:content'])
335
+ assert.deepEqual(rootRoute.permissions.get, ['read:content'])
336
+ })
337
+
338
+ it('should use permissionsScope for placeholder when defined', async () => {
339
+ const { buildRouterTree } = await import('../lib/docsData.js')
340
+ const routerTree = await buildRouterTree(dependencies)
341
+ const themeRouter = routerTree.childRouters.find(r => r.path === '/api/coursethemepresets')
342
+ assert.ok(themeRouter)
343
+ const applyRoute = themeRouter.routes.find(r => r.route === '/:_id/apply')
344
+ assert.deepEqual(applyRoute.permissions.post, ['write:content'])
345
+ })
346
+
347
+ it('should create auth router at /api/auth', async () => {
348
+ const { buildRouterTree } = await import('../lib/docsData.js')
349
+ const routerTree = await buildRouterTree(dependencies)
350
+ const authRouter = routerTree.childRouters.find(r => r.path === '/api/auth')
351
+ assert.ok(authRouter)
352
+ assert.ok(authRouter.routes.some(r => r.route === '/check'))
353
+ })
354
+
355
+ it('should create auth type child routers', async () => {
356
+ const { buildRouterTree } = await import('../lib/docsData.js')
357
+ const routerTree = await buildRouterTree(dependencies)
358
+ const authRouter = routerTree.childRouters.find(r => r.path === '/api/auth')
359
+ const localRouter = authRouter.childRouters.find(r => r.path === '/api/auth/local')
360
+ assert.ok(localRouter)
361
+ })
362
+
363
+ it('should merge auth type routes with auth defaults', async () => {
364
+ const { buildRouterTree } = await import('../lib/docsData.js')
365
+ const routerTree = await buildRouterTree(dependencies)
366
+ const authRouter = routerTree.childRouters.find(r => r.path === '/api/auth')
367
+ const localRouter = authRouter.childRouters.find(r => r.path === '/api/auth/local')
368
+ const routePaths = localRouter.routes.map(r => r.route)
369
+ // override route: /, default: /register, custom: /changepass
370
+ assert.ok(routePaths.includes('/'), 'should have root (overridden) route')
371
+ assert.ok(routePaths.includes('/register'), 'should have default register route')
372
+ assert.ok(routePaths.includes('/changepass'), 'should have custom changepass route')
373
+ })
374
+
375
+ it('should apply override merging from auth type routes', async () => {
376
+ const { buildRouterTree } = await import('../lib/docsData.js')
377
+ const routerTree = await buildRouterTree(dependencies)
378
+ const authRouter = routerTree.childRouters.find(r => r.path === '/api/auth')
379
+ const localRouter = authRouter.childRouters.find(r => r.path === '/api/auth/local')
380
+ const rootRoute = localRouter.routes.find(r => r.route === '/')
381
+ // override merged meta from auth-local onto default
382
+ assert.ok(rootRoute.meta?.post?.summary === 'Local auth')
383
+ })
384
+ })
385
+
386
+ describe('loadSchemas', () => {
387
+ it('should load schemas from schema/*.schema.json', async () => {
388
+ const { loadSchemas } = await import('../lib/docsData.js')
389
+ const schemas = await loadSchemas(dependencies)
390
+ assert.ok(schemas.schemas.content)
391
+ })
392
+
393
+ it('should provide getSchema returning built schema', async () => {
394
+ const { loadSchemas } = await import('../lib/docsData.js')
395
+ const schemas = await loadSchemas(dependencies)
396
+ const result = await schemas.getSchema('content')
397
+ assert.ok(result.built)
398
+ assert.equal(result.built.type, 'object')
399
+ assert.ok(result.built.properties.title)
400
+ })
401
+
402
+ it('should return empty object for unknown schemas', async () => {
403
+ const { loadSchemas } = await import('../lib/docsData.js')
404
+ const schemas = await loadSchemas(dependencies)
405
+ const result = await schemas.getSchema('nonexistent')
406
+ assert.deepEqual(result.built, {})
407
+ })
408
+ })
409
+
410
+ describe('loadErrors', () => {
411
+ it('should load and merge errors from errors/*.json', async () => {
412
+ const { loadErrors } = await import('../lib/docsData.js')
413
+ const errors = await loadErrors(dependencies)
414
+ assert.ok(errors.CONTENT_NOT_FOUND)
415
+ assert.equal(errors.CONTENT_NOT_FOUND.statusCode, 404)
416
+ assert.equal(errors.CONTENT_NOT_FOUND.code, 'CONTENT_NOT_FOUND')
417
+ assert.equal(errors.CONTENT_NOT_FOUND.meta.description, 'Content not found')
418
+ })
419
+ })
420
+
421
+ describe('buildPermissions', () => {
422
+ it('should build permission store with HTTP methods', async () => {
423
+ const { buildRouterTree, buildPermissions } = await import('../lib/docsData.js')
424
+ const routerTree = await buildRouterTree(dependencies)
425
+ const perms = buildPermissions(routerTree)
426
+ assert.ok(Array.isArray(perms.get))
427
+ assert.ok(Array.isArray(perms.post))
428
+ assert.ok(Array.isArray(perms.put))
429
+ assert.ok(Array.isArray(perms.patch))
430
+ assert.ok(Array.isArray(perms.delete))
431
+ })
432
+
433
+ it('should contain permission entries for secured routes', async () => {
434
+ const { buildRouterTree, buildPermissions } = await import('../lib/docsData.js')
435
+ const routerTree = await buildRouterTree(dependencies)
436
+ const perms = buildPermissions(routerTree)
437
+ assert.ok(perms.post.length > 0, 'should have POST permission entries')
438
+ assert.ok(perms.get.length > 0, 'should have GET permission entries')
439
+ })
440
+
441
+ it('should skip routes with null permissions (unsecured)', async () => {
442
+ const { buildRouterTree, buildPermissions } = await import('../lib/docsData.js')
443
+ const routerTree = await buildRouterTree(dependencies)
444
+ const perms = buildPermissions(routerTree)
445
+ // /api/auth/check has permissions: { get: null } — should not appear
446
+ const checkMatch = perms.get.find(([re]) => re.test('/api/auth/check'))
447
+ assert.equal(checkMatch, undefined, '/api/auth/check should not be in secured routes')
448
+ })
449
+
450
+ it('should store scopes as arrays in permission entries', async () => {
451
+ const { buildRouterTree, buildPermissions } = await import('../lib/docsData.js')
452
+ const routerTree = await buildRouterTree(dependencies)
453
+ const perms = buildPermissions(routerTree)
454
+ for (const entries of Object.values(perms)) {
455
+ for (const [re, scopes] of entries) {
456
+ assert.ok(re instanceof RegExp, 'first element should be RegExp')
457
+ assert.ok(Array.isArray(scopes), 'second element should be array')
458
+ }
459
+ }
460
+ })
461
+ })
462
+ })
@@ -331,39 +331,21 @@ function createMockApp (options = {}) {
331
331
 
332
332
  return {
333
333
  pkg: { version: '1.0.0' },
334
- dependencyloader: {
335
- instances: {
336
- 'adapt-authoring-auth': {
337
- permissions: {
338
- routes: {
339
- get: [],
340
- post: [],
341
- put: [],
342
- patch: [],
343
- delete: []
344
- }
345
- }
346
- }
347
- }
334
+ schemas: {
335
+ schemas: mockSchemas,
336
+ getSchema: mock.fn(async (s) => schemas[s])
348
337
  },
349
- waitForModule: mock.fn(async (name) => {
350
- if (name === 'jsonschema') {
351
- return {
352
- schemas: mockSchemas,
353
- getSchema: mock.fn(async (s) => schemas[s])
354
- }
355
- }
356
- if (name === 'server') {
357
- return {
358
- api: {
359
- path: routerPath,
360
- routes,
361
- childRouters
362
- }
363
- }
364
- }
365
- return {}
366
- }),
367
- onReady: mock.fn(async () => {})
338
+ routerTree: {
339
+ path: routerPath,
340
+ routes,
341
+ childRouters
342
+ },
343
+ permissions: {
344
+ get: [],
345
+ post: [],
346
+ put: [],
347
+ patch: [],
348
+ delete: []
349
+ }
368
350
  }
369
351
  }