adapt-authoring-docs 1.2.3 → 1.3.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/.github/workflows/tests.yml +15 -0
- package/package.json +4 -1
- package/tests/DocsifyPluginWrapper.spec.js +164 -0
- package/tests/docsify.spec.js +263 -0
- package/tests/fixtures/bad-run-plugin.js +5 -0
- package/tests/fixtures/good-plugin.js +5 -0
- package/tests/fixtures/no-run-plugin.js +1 -0
- package/tests/jsdoc3.spec.js +226 -0
- package/tests/swagger.spec.js +369 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
on: push
|
|
3
|
+
jobs:
|
|
4
|
+
default:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- uses: actions/setup-node@v4
|
|
11
|
+
with:
|
|
12
|
+
node-version: 'lts/*'
|
|
13
|
+
cache: 'npm'
|
|
14
|
+
- run: npm ci
|
|
15
|
+
- run: npm test
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-docs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"at-docserve": "./bin/docserve.js"
|
|
11
11
|
},
|
|
12
12
|
"repository": "github:adapt-security/adapt-authoring-docs",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test 'tests/**/*.spec.js'"
|
|
15
|
+
},
|
|
13
16
|
"dependencies": {
|
|
14
17
|
"comment-parser": "^1.4.1",
|
|
15
18
|
"docdash": "^2.0.2",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it, beforeEach } from 'node:test'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import DocsifyPluginWrapper from '../docsify/DocsifyPluginWrapper.js'
|
|
5
|
+
|
|
6
|
+
describe('DocsifyPluginWrapper', () => {
|
|
7
|
+
describe('constructor', () => {
|
|
8
|
+
it('should store the config object', () => {
|
|
9
|
+
const config = { pluginEntry: '/some/path/plugin.js' }
|
|
10
|
+
const wrapper = new DocsifyPluginWrapper(config)
|
|
11
|
+
assert.equal(wrapper.config, config)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should set config.srcDir to the dirname of pluginEntry', () => {
|
|
15
|
+
const config = { pluginEntry: '/some/path/plugin.js' }
|
|
16
|
+
const wrapper = new DocsifyPluginWrapper(config)
|
|
17
|
+
assert.equal(wrapper.config.srcDir, '/some/path')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should handle pluginEntry in current directory', () => {
|
|
21
|
+
const config = { pluginEntry: 'plugin.js' }
|
|
22
|
+
const wrapper = new DocsifyPluginWrapper(config)
|
|
23
|
+
assert.equal(wrapper.config.srcDir, '.')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should handle pluginEntry with nested paths', () => {
|
|
27
|
+
const config = { pluginEntry: '/a/b/c/d/plugin.js' }
|
|
28
|
+
const wrapper = new DocsifyPluginWrapper(config)
|
|
29
|
+
assert.equal(wrapper.config.srcDir, '/a/b/c/d')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('customFiles', () => {
|
|
34
|
+
it('should return empty array when plugin is undefined', () => {
|
|
35
|
+
const config = { pluginEntry: '/some/path/plugin.js' }
|
|
36
|
+
const wrapper = new DocsifyPluginWrapper(config)
|
|
37
|
+
// plugin is not set yet (before init), so accessing customFiles
|
|
38
|
+
// will throw because this.plugin is undefined
|
|
39
|
+
assert.throws(() => wrapper.customFiles, TypeError)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should return plugin.customFiles when set', () => {
|
|
43
|
+
const config = { pluginEntry: '/some/path/plugin.js' }
|
|
44
|
+
const wrapper = new DocsifyPluginWrapper(config)
|
|
45
|
+
wrapper.plugin = { customFiles: ['/a.js', '/b.js'] }
|
|
46
|
+
assert.deepEqual(wrapper.customFiles, ['/a.js', '/b.js'])
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should return empty array when plugin.customFiles is not set', () => {
|
|
50
|
+
const config = { pluginEntry: '/some/path/plugin.js' }
|
|
51
|
+
const wrapper = new DocsifyPluginWrapper(config)
|
|
52
|
+
wrapper.plugin = {}
|
|
53
|
+
assert.deepEqual(wrapper.customFiles, [])
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('generateTOC', () => {
|
|
58
|
+
let wrapper
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
wrapper = new DocsifyPluginWrapper({ pluginEntry: '/some/path/plugin.js' })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should generate TOC HTML with string items', () => {
|
|
65
|
+
wrapper.plugin = { manualFile: 'guide.md' }
|
|
66
|
+
const result = wrapper.generateTOC(['Introduction', 'Setup'])
|
|
67
|
+
assert.ok(result.includes('### Quick navigation'))
|
|
68
|
+
assert.ok(result.includes('<ul class="toc">'))
|
|
69
|
+
assert.ok(result.includes('</ul>'))
|
|
70
|
+
assert.ok(result.includes('<li><a href="#/guide?id=Introduction">Introduction</a></li>'))
|
|
71
|
+
assert.ok(result.includes('<li><a href="#/guide?id=Setup">Setup</a></li>'))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should generate TOC HTML with array items [text, link]', () => {
|
|
75
|
+
wrapper.plugin = { manualFile: 'guide.md' }
|
|
76
|
+
const result = wrapper.generateTOC([['Display Text', 'link-id']])
|
|
77
|
+
assert.ok(result.includes('<li><a href="#/guide?id=link-id">Display Text</a></li>'))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should handle mixed string and array items', () => {
|
|
81
|
+
wrapper.plugin = { manualFile: 'guide.md' }
|
|
82
|
+
const result = wrapper.generateTOC(['Simple', ['Complex', 'complex-link']])
|
|
83
|
+
assert.ok(result.includes('<li><a href="#/guide?id=Simple">Simple</a></li>'))
|
|
84
|
+
assert.ok(result.includes('<li><a href="#/guide?id=complex-link">Complex</a></li>'))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should handle empty items array', () => {
|
|
88
|
+
wrapper.plugin = { manualFile: 'guide.md' }
|
|
89
|
+
const result = wrapper.generateTOC([])
|
|
90
|
+
assert.ok(result.includes('### Quick navigation'))
|
|
91
|
+
assert.ok(result.includes('<ul class="toc">'))
|
|
92
|
+
assert.ok(result.includes('</ul>'))
|
|
93
|
+
// No list items
|
|
94
|
+
assert.ok(!result.includes('<li>'))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should strip file extension from pageName', () => {
|
|
98
|
+
wrapper.plugin = { manualFile: 'my-guide.md' }
|
|
99
|
+
const result = wrapper.generateTOC(['Item'])
|
|
100
|
+
assert.ok(result.includes('#/my-guide?id='))
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should handle manualFile with no extension', () => {
|
|
104
|
+
wrapper.plugin = { manualFile: 'guide' }
|
|
105
|
+
const result = wrapper.generateTOC(['Item'])
|
|
106
|
+
assert.ok(result.includes('#/guide?id='))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should use empty string as pageName when plugin has no manualFile', () => {
|
|
110
|
+
wrapper.plugin = {}
|
|
111
|
+
const result = wrapper.generateTOC(['Item'])
|
|
112
|
+
assert.ok(result.includes('#/?id=Item'))
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('init', () => {
|
|
117
|
+
it('should throw if plugin has no run function', async () => {
|
|
118
|
+
const wrapper = new DocsifyPluginWrapper({
|
|
119
|
+
pluginEntry: path.resolve('tests/fixtures/no-run-plugin.js')
|
|
120
|
+
})
|
|
121
|
+
await assert.rejects(() => wrapper.init(), {
|
|
122
|
+
message: /must define a 'run' function/
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should throw if plugin run is not a function', async () => {
|
|
127
|
+
const wrapper = new DocsifyPluginWrapper({
|
|
128
|
+
pluginEntry: path.resolve('tests/fixtures/bad-run-plugin.js')
|
|
129
|
+
})
|
|
130
|
+
await assert.rejects(() => wrapper.init(), {
|
|
131
|
+
message: /must define a 'run' function/
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should call plugin.run and set defaults', async () => {
|
|
136
|
+
const wrapper = new DocsifyPluginWrapper({
|
|
137
|
+
pluginEntry: path.resolve('tests/fixtures/good-plugin.js'),
|
|
138
|
+
app: {}
|
|
139
|
+
})
|
|
140
|
+
await wrapper.init()
|
|
141
|
+
assert.ok(Array.isArray(wrapper.plugin.contents))
|
|
142
|
+
assert.ok(Array.isArray(wrapper.plugin.customFiles))
|
|
143
|
+
assert.equal(typeof wrapper.plugin.replace, 'object')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('writeFile', () => {
|
|
148
|
+
it('should throw when manualFile does not exist', async () => {
|
|
149
|
+
const wrapper = new DocsifyPluginWrapper({
|
|
150
|
+
pluginEntry: '/some/path/plugin.js',
|
|
151
|
+
outputDir: '/tmp/test-output'
|
|
152
|
+
})
|
|
153
|
+
wrapper.plugin = {
|
|
154
|
+
manualFile: 'nonexistent.md',
|
|
155
|
+
contents: [],
|
|
156
|
+
replace: {},
|
|
157
|
+
customFiles: []
|
|
158
|
+
}
|
|
159
|
+
await assert.rejects(() => wrapper.writeFile(), {
|
|
160
|
+
message: /Failed to load manual file/
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it, mock, afterEach } from 'node:test'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The docsify module's main export relies heavily on the app object and
|
|
9
|
+
* external tools (npx docsify). We test the generateSectionTitle function
|
|
10
|
+
* indirectly through integration with mocked configs, and validate the
|
|
11
|
+
* overall output structure.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
describe('docsify', () => {
|
|
15
|
+
let tmpDir
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
if (tmpDir) {
|
|
19
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('generateSectionTitle (tested indirectly)', () => {
|
|
24
|
+
it('should capitalise first letter and replace dashes with spaces in section sidebar', async () => {
|
|
25
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-test-'))
|
|
26
|
+
const outputDir = tmpDir
|
|
27
|
+
const docsSrcDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-src-'))
|
|
28
|
+
|
|
29
|
+
// Create a dummy docs/ directory with a markdown file
|
|
30
|
+
await fs.mkdirp(path.join(docsSrcDir, 'docs'))
|
|
31
|
+
await fs.writeFile(path.join(docsSrcDir, 'docs', 'test-page.md'), '# Test Page Title\nContent here')
|
|
32
|
+
|
|
33
|
+
const mockApp = createMockApp({
|
|
34
|
+
manualSections: {
|
|
35
|
+
'getting-started': { title: 'Getting started' },
|
|
36
|
+
'other-guides': { default: true }
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const configs = [{
|
|
41
|
+
enable: true,
|
|
42
|
+
name: 'test-module',
|
|
43
|
+
rootDir: docsSrcDir,
|
|
44
|
+
includes: {}
|
|
45
|
+
}]
|
|
46
|
+
|
|
47
|
+
const { default: docsify } = await import('../docsify/docsify.js')
|
|
48
|
+
await docsify(mockApp, configs, outputDir, {})
|
|
49
|
+
|
|
50
|
+
const sidebar = await fs.readFile(path.join(outputDir, 'manual', '_sidebar.md'), 'utf8')
|
|
51
|
+
// The section for 'other-guides' (default) should have title 'Other guides'
|
|
52
|
+
assert.ok(sidebar.includes('Other guides'))
|
|
53
|
+
|
|
54
|
+
await fs.rm(docsSrcDir, { recursive: true, force: true })
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('output structure', () => {
|
|
59
|
+
it('should create manual directory with required files', async () => {
|
|
60
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-test-'))
|
|
61
|
+
const outputDir = tmpDir
|
|
62
|
+
|
|
63
|
+
const mockApp = createMockApp({
|
|
64
|
+
manualSections: {
|
|
65
|
+
'other-guides': { default: true }
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const { default: docsify } = await import('../docsify/docsify.js')
|
|
70
|
+
await docsify(mockApp, [], outputDir, {})
|
|
71
|
+
|
|
72
|
+
const manualDir = path.join(outputDir, 'manual')
|
|
73
|
+
assert.ok((await fs.stat(manualDir)).isDirectory())
|
|
74
|
+
assert.ok((await fs.stat(path.join(manualDir, 'index.html'))).isFile())
|
|
75
|
+
assert.ok((await fs.stat(path.join(manualDir, '_sidebar.md'))).isFile())
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should write sidebar with introduction link', async () => {
|
|
79
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-test-'))
|
|
80
|
+
const outputDir = tmpDir
|
|
81
|
+
|
|
82
|
+
const mockApp = createMockApp({
|
|
83
|
+
manualSections: {
|
|
84
|
+
'other-guides': { default: true }
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const { default: docsify } = await import('../docsify/docsify.js')
|
|
89
|
+
await docsify(mockApp, [], outputDir, {})
|
|
90
|
+
|
|
91
|
+
const sidebar = await fs.readFile(path.join(outputDir, 'manual', '_sidebar.md'), 'utf8')
|
|
92
|
+
assert.ok(sidebar.includes('Introduction'))
|
|
93
|
+
assert.ok(sidebar.includes('<ul class="intro">'))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should copy docs files and include them in sidebar', async () => {
|
|
97
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-test-'))
|
|
98
|
+
const outputDir = tmpDir
|
|
99
|
+
const docsSrcDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-src-'))
|
|
100
|
+
|
|
101
|
+
await fs.mkdirp(path.join(docsSrcDir, 'docs'))
|
|
102
|
+
await fs.writeFile(path.join(docsSrcDir, 'docs', 'my-guide.md'), '# My Guide\nSome content')
|
|
103
|
+
|
|
104
|
+
const mockApp = createMockApp({
|
|
105
|
+
manualSections: {
|
|
106
|
+
guides: { title: 'Guides', pages: [] },
|
|
107
|
+
'other-guides': { default: true }
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const configs = [{
|
|
112
|
+
enable: true,
|
|
113
|
+
name: 'test-module',
|
|
114
|
+
rootDir: docsSrcDir,
|
|
115
|
+
includes: {}
|
|
116
|
+
}]
|
|
117
|
+
|
|
118
|
+
const { default: docsify } = await import('../docsify/docsify.js')
|
|
119
|
+
await docsify(mockApp, configs, outputDir, {})
|
|
120
|
+
|
|
121
|
+
const sidebar = await fs.readFile(path.join(outputDir, 'manual', '_sidebar.md'), 'utf8')
|
|
122
|
+
assert.ok(sidebar.includes('My Guide'))
|
|
123
|
+
assert.ok(sidebar.includes('my-guide.md'))
|
|
124
|
+
|
|
125
|
+
await fs.rm(docsSrcDir, { recursive: true, force: true })
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should handle configs with manualPages mapping', async () => {
|
|
129
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-test-'))
|
|
130
|
+
const outputDir = tmpDir
|
|
131
|
+
const docsSrcDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-src-'))
|
|
132
|
+
|
|
133
|
+
await fs.mkdirp(path.join(docsSrcDir, 'docs'))
|
|
134
|
+
await fs.writeFile(path.join(docsSrcDir, 'docs', 'setup.md'), '# Setup Guide\nContent')
|
|
135
|
+
|
|
136
|
+
const mockApp = createMockApp({
|
|
137
|
+
manualSections: {
|
|
138
|
+
'getting-started': { title: 'Getting started', pages: [] },
|
|
139
|
+
'other-guides': { default: true }
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const configs = [{
|
|
144
|
+
enable: true,
|
|
145
|
+
name: 'test-module',
|
|
146
|
+
rootDir: docsSrcDir,
|
|
147
|
+
includes: {},
|
|
148
|
+
manualPages: {
|
|
149
|
+
'setup.md': 'getting-started'
|
|
150
|
+
}
|
|
151
|
+
}]
|
|
152
|
+
|
|
153
|
+
const { default: docsify } = await import('../docsify/docsify.js')
|
|
154
|
+
await docsify(mockApp, configs, outputDir, {})
|
|
155
|
+
|
|
156
|
+
const sidebar = await fs.readFile(path.join(outputDir, 'manual', '_sidebar.md'), 'utf8')
|
|
157
|
+
assert.ok(sidebar.includes('Getting started'))
|
|
158
|
+
assert.ok(sidebar.includes('setup.md'))
|
|
159
|
+
|
|
160
|
+
await fs.rm(docsSrcDir, { recursive: true, force: true })
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should replace OPTIONS placeholder in adapt.js with docsify config', async () => {
|
|
164
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-test-'))
|
|
165
|
+
const outputDir = tmpDir
|
|
166
|
+
|
|
167
|
+
const mockApp = createMockApp({
|
|
168
|
+
manualSections: {
|
|
169
|
+
guides: {},
|
|
170
|
+
'other-guides': { default: true }
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const { default: docsify } = await import('../docsify/docsify.js')
|
|
175
|
+
await docsify(mockApp, [], outputDir, {
|
|
176
|
+
manualCover: '/some/path/cover.md',
|
|
177
|
+
manualIndex: '/some/path/index.md'
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const adaptJs = await fs.readFile(path.join(outputDir, 'manual', 'js', 'adapt.js'), 'utf8')
|
|
181
|
+
// The OPTIONS placeholder in window.$docsify = OPTIONS should be replaced
|
|
182
|
+
// Note: the /* global OPTIONS */ comment will still contain 'OPTIONS'
|
|
183
|
+
assert.ok(!adaptJs.includes('window.$docsify = OPTIONS'))
|
|
184
|
+
assert.ok(adaptJs.includes('themeColor'))
|
|
185
|
+
assert.ok(adaptJs.includes('cover.md'))
|
|
186
|
+
assert.ok(adaptJs.includes('index.md'))
|
|
187
|
+
|
|
188
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should set coverpage to false when no manualCover provided', async () => {
|
|
192
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-test-'))
|
|
193
|
+
const outputDir = tmpDir
|
|
194
|
+
|
|
195
|
+
const mockApp = createMockApp({
|
|
196
|
+
manualSections: { 'other-guides': { default: true } }
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const { default: docsify } = await import('../docsify/docsify.js')
|
|
200
|
+
await docsify(mockApp, [], outputDir, {})
|
|
201
|
+
|
|
202
|
+
const adaptJs = await fs.readFile(path.join(outputDir, 'manual', 'js', 'adapt.js'), 'utf8')
|
|
203
|
+
assert.ok(adaptJs.includes('"coverpage":false'))
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('bugs', () => {
|
|
208
|
+
it('TODO: defaultSection reduce with single entry returns array tuple instead of string', async () => {
|
|
209
|
+
// BUG: In docsify.js line 26, Object.entries(sectionsConf).reduce((m, [id, data]) => ...)
|
|
210
|
+
// has no initial value. When there is only one section entry, reduce() returns
|
|
211
|
+
// the entry itself (an [id, data] array) without calling the callback.
|
|
212
|
+
// This causes generateSectionTitle to fail with:
|
|
213
|
+
// TypeError: sectionName.slice(...).replaceAll is not a function
|
|
214
|
+
// because sectionName is an array, not a string.
|
|
215
|
+
//
|
|
216
|
+
// To fix: add an initial value to reduce, e.g.:
|
|
217
|
+
// Object.entries(sectionsConf).reduce((m, [id, data]) => data.default ? id : m, undefined)
|
|
218
|
+
// or use Array.find instead:
|
|
219
|
+
// Object.entries(sectionsConf).find(([, data]) => data.default)?.[0]
|
|
220
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-test-'))
|
|
221
|
+
const outputDir = tmpDir
|
|
222
|
+
const docsSrcDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docsify-src-'))
|
|
223
|
+
|
|
224
|
+
await fs.mkdirp(path.join(docsSrcDir, 'docs'))
|
|
225
|
+
await fs.writeFile(path.join(docsSrcDir, 'docs', 'test.md'), '# Test\nContent')
|
|
226
|
+
|
|
227
|
+
const mockApp = createMockApp({
|
|
228
|
+
manualSections: {
|
|
229
|
+
'only-section': { default: true }
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const configs = [{
|
|
234
|
+
enable: true,
|
|
235
|
+
name: 'test-module',
|
|
236
|
+
rootDir: docsSrcDir,
|
|
237
|
+
includes: {}
|
|
238
|
+
}]
|
|
239
|
+
|
|
240
|
+
const { default: docsify } = await import('../docsify/docsify.js')
|
|
241
|
+
// This should work but fails due to the bug described above
|
|
242
|
+
await assert.rejects(
|
|
243
|
+
() => docsify(mockApp, configs, outputDir, {}),
|
|
244
|
+
TypeError
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
await fs.rm(docsSrcDir, { recursive: true, force: true })
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
function createMockApp (options = {}) {
|
|
253
|
+
const { manualSections = {} } = options
|
|
254
|
+
return {
|
|
255
|
+
pkg: { version: '1.0.0' },
|
|
256
|
+
config: {
|
|
257
|
+
get: mock.fn((key) => {
|
|
258
|
+
if (key === 'adapt-authoring-docs.manualSections') return { ...manualSections }
|
|
259
|
+
return undefined
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default class NoRunPlugin {}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it, afterEach } from 'node:test'
|
|
3
|
+
import fs from 'fs-extra'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The jsdoc3 module generates JSDoc documentation. Its main export function
|
|
9
|
+
* requires a fully initialised app, configs, and runs npx jsdoc.
|
|
10
|
+
* We test writeConfig indirectly by checking the generated config file,
|
|
11
|
+
* and getSourceIncludes through the config output.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
describe('jsdoc3', () => {
|
|
15
|
+
let tmpDir
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
if (tmpDir) {
|
|
19
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('getSourceIncludes (tested indirectly)', () => {
|
|
24
|
+
it('should include lib/**/*.js files from config rootDirs', async () => {
|
|
25
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-test-'))
|
|
26
|
+
const srcDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-src-'))
|
|
27
|
+
|
|
28
|
+
// Create lib directory with JS files
|
|
29
|
+
await fs.mkdirp(path.join(srcDir, 'lib'))
|
|
30
|
+
await fs.writeFile(path.join(srcDir, 'lib', 'module.js'), '/** @class */ class Foo {}')
|
|
31
|
+
|
|
32
|
+
const configs = [{
|
|
33
|
+
enable: true,
|
|
34
|
+
name: 'test-module',
|
|
35
|
+
rootDir: srcDir,
|
|
36
|
+
module: false,
|
|
37
|
+
includes: {}
|
|
38
|
+
}]
|
|
39
|
+
|
|
40
|
+
const mockApp = createMockApp()
|
|
41
|
+
|
|
42
|
+
// We can check the generated config file to verify includes
|
|
43
|
+
const configPath = path.resolve(
|
|
44
|
+
path.dirname(new URL('../jsdoc3/jsdoc3.js', import.meta.url).pathname),
|
|
45
|
+
'.jsdocConfig.json'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const { default: jsdoc3 } = await import('../jsdoc3/jsdoc3.js')
|
|
50
|
+
await jsdoc3(mockApp, configs, tmpDir, {})
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// jsdoc may fail due to missing files, but the config should still be written
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'))
|
|
56
|
+
const includes = config.source.include
|
|
57
|
+
assert.ok(includes.some(f => f.endsWith('module.js')))
|
|
58
|
+
|
|
59
|
+
await fs.rm(srcDir, { recursive: true, force: true })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should include index.js for configs marked as module', async () => {
|
|
63
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-test-'))
|
|
64
|
+
const srcDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-src-'))
|
|
65
|
+
|
|
66
|
+
await fs.writeFile(path.join(srcDir, 'index.js'), '/** @module */ export default class {}')
|
|
67
|
+
|
|
68
|
+
const configs = [{
|
|
69
|
+
enable: true,
|
|
70
|
+
name: 'test-module',
|
|
71
|
+
rootDir: srcDir,
|
|
72
|
+
module: true,
|
|
73
|
+
includes: {}
|
|
74
|
+
}]
|
|
75
|
+
|
|
76
|
+
const mockApp = createMockApp()
|
|
77
|
+
const configPath = path.resolve(
|
|
78
|
+
path.dirname(new URL('../jsdoc3/jsdoc3.js', import.meta.url).pathname),
|
|
79
|
+
'.jsdocConfig.json'
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const { default: jsdoc3 } = await import('../jsdoc3/jsdoc3.js')
|
|
84
|
+
await jsdoc3(mockApp, configs, tmpDir, {})
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// jsdoc may fail but config should be written
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'))
|
|
90
|
+
const includes = config.source.include
|
|
91
|
+
assert.ok(includes.some(f => f.endsWith('index.js')))
|
|
92
|
+
|
|
93
|
+
await fs.rm(srcDir, { recursive: true, force: true })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should not include index.js for configs not marked as module', async () => {
|
|
97
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-test-'))
|
|
98
|
+
const srcDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-src-'))
|
|
99
|
+
|
|
100
|
+
await fs.mkdirp(path.join(srcDir, 'lib'))
|
|
101
|
+
await fs.writeFile(path.join(srcDir, 'index.js'), '/** @module */ export default class {}')
|
|
102
|
+
|
|
103
|
+
const configs = [{
|
|
104
|
+
enable: true,
|
|
105
|
+
name: 'test-module',
|
|
106
|
+
rootDir: srcDir,
|
|
107
|
+
module: false,
|
|
108
|
+
includes: {}
|
|
109
|
+
}]
|
|
110
|
+
|
|
111
|
+
const mockApp = createMockApp()
|
|
112
|
+
const configPath = path.resolve(
|
|
113
|
+
path.dirname(new URL('../jsdoc3/jsdoc3.js', import.meta.url).pathname),
|
|
114
|
+
'.jsdocConfig.json'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const { default: jsdoc3 } = await import('../jsdoc3/jsdoc3.js')
|
|
119
|
+
await jsdoc3(mockApp, configs, tmpDir, {})
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// jsdoc may fail but config should be written
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'))
|
|
125
|
+
const includes = config.source.include
|
|
126
|
+
assert.ok(!includes.some(f => f.endsWith('index.js')))
|
|
127
|
+
|
|
128
|
+
await fs.rm(srcDir, { recursive: true, force: true })
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should include sourceIndex page when provided', async () => {
|
|
132
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-test-'))
|
|
133
|
+
|
|
134
|
+
const configs = [{
|
|
135
|
+
enable: true,
|
|
136
|
+
name: 'test-module',
|
|
137
|
+
rootDir: tmpDir,
|
|
138
|
+
module: false,
|
|
139
|
+
includes: {}
|
|
140
|
+
}]
|
|
141
|
+
|
|
142
|
+
const sourceIndex = '/some/path/to/source-index.js'
|
|
143
|
+
const mockApp = createMockApp()
|
|
144
|
+
const configPath = path.resolve(
|
|
145
|
+
path.dirname(new URL('../jsdoc3/jsdoc3.js', import.meta.url).pathname),
|
|
146
|
+
'.jsdocConfig.json'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const { default: jsdoc3 } = await import('../jsdoc3/jsdoc3.js')
|
|
151
|
+
await jsdoc3(mockApp, configs, tmpDir, { sourceIndex })
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// jsdoc may fail but config should be written
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'))
|
|
157
|
+
const includes = config.source.include
|
|
158
|
+
assert.ok(includes.includes(sourceIndex))
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('writeConfig (tested indirectly)', () => {
|
|
163
|
+
it('should write valid JSON config with expected structure', async () => {
|
|
164
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-test-'))
|
|
165
|
+
|
|
166
|
+
const configs = [{
|
|
167
|
+
enable: true,
|
|
168
|
+
name: 'test-module',
|
|
169
|
+
rootDir: tmpDir,
|
|
170
|
+
module: false,
|
|
171
|
+
includes: {}
|
|
172
|
+
}]
|
|
173
|
+
|
|
174
|
+
const mockApp = createMockApp()
|
|
175
|
+
const configPath = path.resolve(
|
|
176
|
+
path.dirname(new URL('../jsdoc3/jsdoc3.js', import.meta.url).pathname),
|
|
177
|
+
'.jsdocConfig.json'
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const { default: jsdoc3 } = await import('../jsdoc3/jsdoc3.js')
|
|
182
|
+
await jsdoc3(mockApp, configs, tmpDir, {})
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// jsdoc may fail but config should be written
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'))
|
|
188
|
+
assert.ok(config.source)
|
|
189
|
+
assert.ok(Array.isArray(config.source.include))
|
|
190
|
+
assert.ok(config.docdash)
|
|
191
|
+
assert.ok(config.docdash.collapse === true)
|
|
192
|
+
assert.ok(config.docdash.search === true)
|
|
193
|
+
assert.ok(config.opts)
|
|
194
|
+
assert.ok(config.opts.destination.endsWith('/backend'))
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should include app version in meta and menu', async () => {
|
|
198
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'jsdoc-test-'))
|
|
199
|
+
|
|
200
|
+
const configs = []
|
|
201
|
+
const mockApp = createMockApp('2.5.0')
|
|
202
|
+
const configPath = path.resolve(
|
|
203
|
+
path.dirname(new URL('../jsdoc3/jsdoc3.js', import.meta.url).pathname),
|
|
204
|
+
'.jsdocConfig.json'
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const { default: jsdoc3 } = await import('../jsdoc3/jsdoc3.js')
|
|
209
|
+
await jsdoc3(mockApp, configs, tmpDir, {})
|
|
210
|
+
} catch (e) {
|
|
211
|
+
// jsdoc may fail but config should be written
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'))
|
|
215
|
+
assert.ok(config.docdash.meta.keyword.includes('2.5.0'))
|
|
216
|
+
const menuHtml = Object.keys(config.docdash.menu)[0]
|
|
217
|
+
assert.ok(menuHtml.includes('2.5.0'))
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
function createMockApp (version = '1.0.0') {
|
|
223
|
+
return {
|
|
224
|
+
pkg: { version }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { describe, it, mock, afterEach } from 'node:test'
|
|
3
|
+
import fs from 'fs/promises'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The swagger module's internal functions (sanitiseSchema, generatePathSpec)
|
|
9
|
+
* are not exported, so we test the main export function with a mocked app.
|
|
10
|
+
* We also test the internal logic indirectly through the generated output.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// We need to import swagger dynamically to avoid import.meta.url resolution issues
|
|
14
|
+
// The swagger function requires a fully configured app object with waitForModule
|
|
15
|
+
// and other dependencies, so we create thorough mocks.
|
|
16
|
+
|
|
17
|
+
describe('swagger', () => {
|
|
18
|
+
let tmpDir
|
|
19
|
+
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
if (tmpDir) {
|
|
22
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('sanitiseSchema (tested indirectly via swagger output)', () => {
|
|
27
|
+
it('should remove isInternal properties from schemas', async () => {
|
|
28
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
29
|
+
const outputDir = tmpDir
|
|
30
|
+
|
|
31
|
+
const mockApp = createMockApp({
|
|
32
|
+
schemas: {
|
|
33
|
+
TestSchema: {
|
|
34
|
+
built: {
|
|
35
|
+
properties: {
|
|
36
|
+
publicProp: { type: 'string' },
|
|
37
|
+
internalProp: { type: 'string', isInternal: true }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
routes: []
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
46
|
+
await swagger(mockApp, [], outputDir)
|
|
47
|
+
|
|
48
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
49
|
+
assert.ok(spec.components.schemas.TestSchema)
|
|
50
|
+
assert.ok(spec.components.schemas.TestSchema.properties.publicProp)
|
|
51
|
+
assert.equal(spec.components.schemas.TestSchema.properties.internalProp, undefined)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should remove isReadOnly properties from schemas', async () => {
|
|
55
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
56
|
+
const outputDir = tmpDir
|
|
57
|
+
|
|
58
|
+
const mockApp = createMockApp({
|
|
59
|
+
schemas: {
|
|
60
|
+
TestSchema: {
|
|
61
|
+
built: {
|
|
62
|
+
properties: {
|
|
63
|
+
publicProp: { type: 'string' },
|
|
64
|
+
readOnlyProp: { type: 'string', isReadOnly: true }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
routes: []
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
73
|
+
await swagger(mockApp, [], outputDir)
|
|
74
|
+
|
|
75
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
76
|
+
assert.ok(spec.components.schemas.TestSchema.properties.publicProp)
|
|
77
|
+
assert.equal(spec.components.schemas.TestSchema.properties.readOnlyProp, undefined)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should recursively sanitise nested object properties', async () => {
|
|
81
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
82
|
+
const outputDir = tmpDir
|
|
83
|
+
|
|
84
|
+
const mockApp = createMockApp({
|
|
85
|
+
schemas: {
|
|
86
|
+
TestSchema: {
|
|
87
|
+
built: {
|
|
88
|
+
properties: {
|
|
89
|
+
nested: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
visible: { type: 'string' },
|
|
93
|
+
hidden: { type: 'string', isInternal: true }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
routes: []
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
104
|
+
await swagger(mockApp, [], outputDir)
|
|
105
|
+
|
|
106
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
107
|
+
const nested = spec.components.schemas.TestSchema.properties.nested
|
|
108
|
+
assert.ok(nested.properties.visible)
|
|
109
|
+
assert.equal(nested.properties.hidden, undefined)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('generatePathSpec (tested indirectly via swagger output)', () => {
|
|
114
|
+
it('should generate paths from router routes', async () => {
|
|
115
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
116
|
+
const outputDir = tmpDir
|
|
117
|
+
|
|
118
|
+
const mockApp = createMockApp({
|
|
119
|
+
schemas: {},
|
|
120
|
+
routes: [
|
|
121
|
+
{
|
|
122
|
+
route: '/',
|
|
123
|
+
handlers: { get: () => {} },
|
|
124
|
+
meta: {},
|
|
125
|
+
internal: false
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
routerPath: '/api/test'
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
132
|
+
await swagger(mockApp, [], outputDir)
|
|
133
|
+
|
|
134
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
135
|
+
assert.ok(spec.paths['/api/test'])
|
|
136
|
+
assert.ok(spec.paths['/api/test'].get)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should extract path parameters from route', async () => {
|
|
140
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
141
|
+
const outputDir = tmpDir
|
|
142
|
+
|
|
143
|
+
const mockApp = createMockApp({
|
|
144
|
+
schemas: {},
|
|
145
|
+
routes: [
|
|
146
|
+
{
|
|
147
|
+
route: '/:id',
|
|
148
|
+
handlers: { get: () => {} },
|
|
149
|
+
meta: {},
|
|
150
|
+
internal: false
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
routerPath: '/api/test'
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
157
|
+
await swagger(mockApp, [], outputDir)
|
|
158
|
+
|
|
159
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
160
|
+
const params = spec.paths['/api/test/:id'].get.parameters
|
|
161
|
+
assert.ok(params.some(p => p.name === 'id' && p.in === 'path' && p.required === true))
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should handle optional path parameters', async () => {
|
|
165
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
166
|
+
const outputDir = tmpDir
|
|
167
|
+
|
|
168
|
+
const mockApp = createMockApp({
|
|
169
|
+
schemas: {},
|
|
170
|
+
routes: [
|
|
171
|
+
{
|
|
172
|
+
route: '/:id?',
|
|
173
|
+
handlers: { get: () => {} },
|
|
174
|
+
meta: {},
|
|
175
|
+
internal: false
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
routerPath: '/api/test'
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
182
|
+
await swagger(mockApp, [], outputDir)
|
|
183
|
+
|
|
184
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
185
|
+
const params = spec.paths['/api/test/:id?'].get.parameters
|
|
186
|
+
assert.ok(params.some(p => p.name === 'id' && p.required === false))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should mark internal routes in description', async () => {
|
|
190
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
191
|
+
const outputDir = tmpDir
|
|
192
|
+
|
|
193
|
+
const mockApp = createMockApp({
|
|
194
|
+
schemas: {},
|
|
195
|
+
routes: [
|
|
196
|
+
{
|
|
197
|
+
route: '/',
|
|
198
|
+
handlers: { get: () => {} },
|
|
199
|
+
meta: {},
|
|
200
|
+
internal: true
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
routerPath: '/api/test'
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
207
|
+
await swagger(mockApp, [], outputDir)
|
|
208
|
+
|
|
209
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
210
|
+
assert.ok(spec.paths['/api/test'].get.description.includes('ONLY ACCESSIBLE FROM LOCALHOST'))
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('should sort paths alphabetically', async () => {
|
|
214
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
215
|
+
const outputDir = tmpDir
|
|
216
|
+
|
|
217
|
+
const mockApp = createMockApp({
|
|
218
|
+
schemas: {},
|
|
219
|
+
routes: [
|
|
220
|
+
{
|
|
221
|
+
route: '/zebra',
|
|
222
|
+
handlers: { get: () => {} },
|
|
223
|
+
meta: {},
|
|
224
|
+
internal: false
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
route: '/alpha',
|
|
228
|
+
handlers: { get: () => {} },
|
|
229
|
+
meta: {},
|
|
230
|
+
internal: false
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
routerPath: '/api/test'
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
237
|
+
await swagger(mockApp, [], outputDir)
|
|
238
|
+
|
|
239
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
240
|
+
const keys = Object.keys(spec.paths)
|
|
241
|
+
assert.ok(keys.indexOf('/api/test/alpha') < keys.indexOf('/api/test/zebra'))
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should process child routers recursively', async () => {
|
|
245
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
246
|
+
const outputDir = tmpDir
|
|
247
|
+
|
|
248
|
+
const mockApp = createMockApp({
|
|
249
|
+
schemas: {},
|
|
250
|
+
routes: [
|
|
251
|
+
{
|
|
252
|
+
route: '/',
|
|
253
|
+
handlers: { get: () => {} },
|
|
254
|
+
meta: {},
|
|
255
|
+
internal: false
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
routerPath: '/api/parent',
|
|
259
|
+
childRouters: [
|
|
260
|
+
{
|
|
261
|
+
path: '/api/parent/child',
|
|
262
|
+
routes: [
|
|
263
|
+
{
|
|
264
|
+
route: '/',
|
|
265
|
+
handlers: { post: () => {} },
|
|
266
|
+
meta: {},
|
|
267
|
+
internal: false
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
childRouters: []
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
276
|
+
await swagger(mockApp, [], outputDir)
|
|
277
|
+
|
|
278
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
279
|
+
assert.ok(spec.paths['/api/parent'])
|
|
280
|
+
assert.ok(spec.paths['/api/parent/child'])
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
describe('swagger output structure', () => {
|
|
285
|
+
it('should generate valid OpenAPI 3.0.3 spec', async () => {
|
|
286
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
287
|
+
const outputDir = tmpDir
|
|
288
|
+
|
|
289
|
+
const mockApp = createMockApp({ schemas: {}, routes: [] })
|
|
290
|
+
|
|
291
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
292
|
+
await swagger(mockApp, [], outputDir)
|
|
293
|
+
|
|
294
|
+
const spec = JSON.parse(await fs.readFile(path.join(outputDir, 'rest', 'api.json'), 'utf8'))
|
|
295
|
+
assert.equal(spec.openapi, '3.0.3')
|
|
296
|
+
assert.ok(spec.info)
|
|
297
|
+
assert.ok(spec.info.version)
|
|
298
|
+
assert.ok(spec.components)
|
|
299
|
+
assert.ok(spec.paths)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should create rest directory with required files', async () => {
|
|
303
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'swagger-test-'))
|
|
304
|
+
const outputDir = tmpDir
|
|
305
|
+
|
|
306
|
+
const mockApp = createMockApp({ schemas: {}, routes: [] })
|
|
307
|
+
|
|
308
|
+
const { default: swagger } = await import('../swagger/swagger.js')
|
|
309
|
+
await swagger(mockApp, [], outputDir)
|
|
310
|
+
|
|
311
|
+
const restDir = path.join(outputDir, 'rest')
|
|
312
|
+
const stat = await fs.stat(restDir)
|
|
313
|
+
assert.ok(stat.isDirectory())
|
|
314
|
+
|
|
315
|
+
const indexHtml = await fs.stat(path.join(restDir, 'index.html'))
|
|
316
|
+
assert.ok(indexHtml.isFile())
|
|
317
|
+
|
|
318
|
+
const apiJson = await fs.stat(path.join(restDir, 'api.json'))
|
|
319
|
+
assert.ok(apiJson.isFile())
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
function createMockApp (options = {}) {
|
|
325
|
+
const { schemas = {}, routes = [], routerPath = '/api/test', childRouters = [] } = options
|
|
326
|
+
|
|
327
|
+
const mockSchemas = {}
|
|
328
|
+
for (const name of Object.keys(schemas)) {
|
|
329
|
+
mockSchemas[name] = true
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
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
|
+
}
|
|
348
|
+
},
|
|
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 () => {})
|
|
368
|
+
}
|
|
369
|
+
}
|