adapt-authoring-integration-tests 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/releases.yml +32 -0
- package/.github/workflows/standardjs.yml +12 -0
- package/README.md +73 -0
- package/adapt-authoring.json +6 -0
- package/bin/run.js +83 -0
- package/fixtures/.gitkeep +0 -0
- package/fixtures/manifest.example.json +3 -0
- package/lib/app.js +49 -0
- package/lib/db.js +28 -0
- package/lib/fixtures.js +136 -0
- package/package.json +36 -0
- package/tests/adaptframework-build.spec.js +120 -0
- package/tests/adaptframework-import.spec.js +193 -0
- package/tests/api.spec.js +47 -0
- package/tests/auth.spec.js +306 -0
- package/tests/content.spec.js +179 -0
- package/tests/lib.spec.js +43 -0
- package/tests/mongodb.spec.js +110 -0
- package/tests/roles.spec.js +162 -0
- package/tests/users.spec.js +113 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { getApp, getModule, cleanDb } from '../lib/app.js'
|
|
4
|
+
|
|
5
|
+
let content
|
|
6
|
+
let authLocal
|
|
7
|
+
let createdBy
|
|
8
|
+
|
|
9
|
+
describe('Content CRUD operations', () => {
|
|
10
|
+
before(async () => {
|
|
11
|
+
await getApp()
|
|
12
|
+
content = await getModule('content')
|
|
13
|
+
authLocal = await getModule('auth-local')
|
|
14
|
+
const user = await authLocal.register({
|
|
15
|
+
email: 'content-test@example.com',
|
|
16
|
+
firstName: 'Content',
|
|
17
|
+
lastName: 'Tester',
|
|
18
|
+
password: 'Password123!'
|
|
19
|
+
})
|
|
20
|
+
createdBy = user._id.toString()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
after(async () => {
|
|
24
|
+
await cleanDb(['content', 'users', 'authtokens'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Course creation
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
describe('Course creation', () => {
|
|
31
|
+
let course
|
|
32
|
+
|
|
33
|
+
it('should insert a course content item', async () => {
|
|
34
|
+
course = await content.insert(
|
|
35
|
+
{ _type: 'course', title: 'Test Course', createdBy },
|
|
36
|
+
{ validate: false, schemaName: 'course' }
|
|
37
|
+
)
|
|
38
|
+
assert.ok(course, 'insert should return a document')
|
|
39
|
+
assert.ok(course._id, 'course should have an _id')
|
|
40
|
+
assert.equal(course._type, 'course')
|
|
41
|
+
assert.equal(course.title, 'Test Course')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should have set _courseId to its own _id', async () => {
|
|
45
|
+
assert.ok(course._courseId, 'course should have _courseId')
|
|
46
|
+
assert.equal(
|
|
47
|
+
course._courseId.toString(),
|
|
48
|
+
course._id.toString(),
|
|
49
|
+
'_courseId should equal _id for a course'
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should be retrievable via find', async () => {
|
|
54
|
+
const [found] = await content.find({ _id: course._id })
|
|
55
|
+
assert.ok(found, 'course should be found in the database')
|
|
56
|
+
assert.equal(found._type, 'course')
|
|
57
|
+
assert.equal(found.title, 'Test Course')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Content hierarchy
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
describe('Content hierarchy', () => {
|
|
65
|
+
let course, page, article, block
|
|
66
|
+
|
|
67
|
+
before(async () => {
|
|
68
|
+
course = await content.insert(
|
|
69
|
+
{ _type: 'course', title: 'Hierarchy Course', createdBy },
|
|
70
|
+
{ validate: false, schemaName: 'course' }
|
|
71
|
+
)
|
|
72
|
+
page = await content.insert(
|
|
73
|
+
{ _type: 'page', title: 'Test Page', _parentId: course._id.toString(), _courseId: course._id.toString(), createdBy },
|
|
74
|
+
{ validate: false, schemaName: 'contentobject' }
|
|
75
|
+
)
|
|
76
|
+
article = await content.insert(
|
|
77
|
+
{ _type: 'article', title: 'Test Article', _parentId: page._id.toString(), _courseId: course._id.toString(), createdBy },
|
|
78
|
+
{ validate: false, schemaName: 'article' }
|
|
79
|
+
)
|
|
80
|
+
block = await content.insert(
|
|
81
|
+
{ _type: 'block', title: 'Test Block', _parentId: article._id.toString(), _courseId: course._id.toString(), createdBy },
|
|
82
|
+
{ validate: false, schemaName: 'block' }
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should create a page with the course as parent', async () => {
|
|
87
|
+
assert.ok(page._id, 'page should have an _id')
|
|
88
|
+
assert.equal(page._parentId.toString(), course._id.toString())
|
|
89
|
+
assert.equal(page._courseId.toString(), course._id.toString())
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should create an article with the page as parent', async () => {
|
|
93
|
+
assert.ok(article._id, 'article should have an _id')
|
|
94
|
+
assert.equal(article._parentId.toString(), page._id.toString())
|
|
95
|
+
assert.equal(article._courseId.toString(), course._id.toString())
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should create a block with the article as parent', async () => {
|
|
99
|
+
assert.ok(block._id, 'block should have an _id')
|
|
100
|
+
assert.equal(block._parentId.toString(), article._id.toString())
|
|
101
|
+
assert.equal(block._courseId.toString(), course._id.toString())
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Content query by _type
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
describe('Content query', () => {
|
|
109
|
+
let courseId
|
|
110
|
+
|
|
111
|
+
before(async () => {
|
|
112
|
+
const course = await content.insert(
|
|
113
|
+
{ _type: 'course', title: 'Query Course', createdBy },
|
|
114
|
+
{ validate: false, schemaName: 'course' }
|
|
115
|
+
)
|
|
116
|
+
courseId = course._id.toString()
|
|
117
|
+
await content.insert(
|
|
118
|
+
{ _type: 'page', title: 'Page A', _parentId: courseId, _courseId: courseId, createdBy },
|
|
119
|
+
{ validate: false, schemaName: 'contentobject' }
|
|
120
|
+
)
|
|
121
|
+
await content.insert(
|
|
122
|
+
{ _type: 'page', title: 'Page B', _parentId: courseId, _courseId: courseId, createdBy },
|
|
123
|
+
{ validate: false, schemaName: 'contentobject' }
|
|
124
|
+
)
|
|
125
|
+
await content.insert(
|
|
126
|
+
{ _type: 'article', title: 'Article A', _parentId: courseId, _courseId: courseId, createdBy },
|
|
127
|
+
{ validate: false, schemaName: 'article' }
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should find only pages when querying by _type "page"', async () => {
|
|
132
|
+
const pages = await content.find({ _courseId: courseId, _type: 'page' })
|
|
133
|
+
assert.equal(pages.length, 2, 'should find exactly 2 pages')
|
|
134
|
+
assert.ok(pages.every(p => p._type === 'page'), 'all results should be pages')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should find items using $in operator on _type', async () => {
|
|
138
|
+
const items = await content.find({ _courseId: courseId, _type: { $in: ['page', 'article'] } })
|
|
139
|
+
assert.equal(items.length, 3, 'should find 2 pages + 1 article = 3 items')
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Content update
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
describe('Content update', () => {
|
|
147
|
+
it('should update a content item title', async () => {
|
|
148
|
+
const course = await content.insert(
|
|
149
|
+
{ _type: 'course', title: 'Original Title', createdBy },
|
|
150
|
+
{ validate: false, schemaName: 'course' }
|
|
151
|
+
)
|
|
152
|
+
const updated = await content.update(
|
|
153
|
+
{ _id: course._id },
|
|
154
|
+
{ title: 'Updated Title' },
|
|
155
|
+
{ validate: false }
|
|
156
|
+
)
|
|
157
|
+
assert.equal(updated.title, 'Updated Title')
|
|
158
|
+
assert.equal(updated._id.toString(), course._id.toString(), '_id should remain the same')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Content deletion
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
describe('Content deletion', () => {
|
|
166
|
+
it('should delete a content item and verify removal', async () => {
|
|
167
|
+
const course = await content.insert(
|
|
168
|
+
{ _type: 'course', title: 'To Be Deleted', createdBy },
|
|
169
|
+
{ validate: false, schemaName: 'course' }
|
|
170
|
+
)
|
|
171
|
+
const courseId = course._id.toString()
|
|
172
|
+
|
|
173
|
+
await content.delete({ _id: course._id })
|
|
174
|
+
|
|
175
|
+
const results = await content.find({ _id: courseId })
|
|
176
|
+
assert.equal(results.length, 0, 'deleted course should not be found')
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_CLEAN_COLLECTIONS } from '../lib/app.js'
|
|
5
|
+
import { dropTestDb } from '../lib/db.js'
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// DEFAULT_CLEAN_COLLECTIONS
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
describe('DEFAULT_CLEAN_COLLECTIONS', () => {
|
|
11
|
+
it('should include contentplugins to prevent stale plugin records', () => {
|
|
12
|
+
assert.ok(
|
|
13
|
+
DEFAULT_CLEAN_COLLECTIONS.includes('contentplugins'),
|
|
14
|
+
'contentplugins must be in the default clean list — stale records ' +
|
|
15
|
+
'cause MISSING_SCHEMA errors on subsequent test runs'
|
|
16
|
+
)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should include the core content collections', () => {
|
|
20
|
+
for (const name of ['content', 'assets', 'courseassets', 'tags', 'adaptbuilds']) {
|
|
21
|
+
assert.ok(
|
|
22
|
+
DEFAULT_CLEAN_COLLECTIONS.includes(name),
|
|
23
|
+
`expected "${name}" in DEFAULT_CLEAN_COLLECTIONS`
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// dropTestDb
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
describe('dropTestDb()', () => {
|
|
33
|
+
it('should return false when config directory does not exist', async () => {
|
|
34
|
+
// Point at a non-existent directory so config import fails
|
|
35
|
+
const result = await dropTestDb('/tmp/nonexistent-aat-dir')
|
|
36
|
+
assert.equal(result, false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should be a function that accepts a cwd argument', () => {
|
|
40
|
+
assert.equal(typeof dropTestDb, 'function')
|
|
41
|
+
assert.equal(dropTestDb.length, 0) // default param, so length is 0
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { getApp, getModule } from '../lib/app.js'
|
|
4
|
+
|
|
5
|
+
const COLLECTION = 'integrationtest'
|
|
6
|
+
|
|
7
|
+
let mongodb
|
|
8
|
+
|
|
9
|
+
describe('MongoDB core operations', () => {
|
|
10
|
+
before(async () => {
|
|
11
|
+
await getApp()
|
|
12
|
+
mongodb = await getModule('mongodb')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
after(async () => {
|
|
16
|
+
try { await mongodb.getCollection(COLLECTION).drop() } catch {}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('Insert & Find', () => {
|
|
20
|
+
let insertedId
|
|
21
|
+
|
|
22
|
+
it('should insert a document and return an _id', async () => {
|
|
23
|
+
const result = await mongodb.insert(COLLECTION, { name: 'Alice', score: 42 })
|
|
24
|
+
assert.ok(result._id, 'inserted document should have an _id')
|
|
25
|
+
insertedId = result._id
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should find the document by _id with matching fields', async () => {
|
|
29
|
+
const [doc] = await mongodb.find(COLLECTION, { _id: insertedId })
|
|
30
|
+
assert.ok(doc, 'document should be found')
|
|
31
|
+
assert.equal(doc.name, 'Alice')
|
|
32
|
+
assert.equal(doc.score, 42)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('Insert Multiple', () => {
|
|
37
|
+
it('should insert multiple documents individually and find them all', async () => {
|
|
38
|
+
await mongodb.insert(COLLECTION, { name: 'Bob', score: 10 })
|
|
39
|
+
await mongodb.insert(COLLECTION, { name: 'Carol', score: 20 })
|
|
40
|
+
await mongodb.insert(COLLECTION, { name: 'Dave', score: 30 })
|
|
41
|
+
|
|
42
|
+
const found = await mongodb.find(COLLECTION, { name: { $in: ['Bob', 'Carol', 'Dave'] } })
|
|
43
|
+
assert.equal(found.length, 3, 'should find all three documents')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('Update', () => {
|
|
48
|
+
let docId
|
|
49
|
+
|
|
50
|
+
it('should update a field on an existing document', async () => {
|
|
51
|
+
const inserted = await mongodb.insert(COLLECTION, { name: 'Eve', score: 50 })
|
|
52
|
+
docId = inserted._id
|
|
53
|
+
|
|
54
|
+
await mongodb.update(COLLECTION, { _id: docId }, { $set: { score: 99 } })
|
|
55
|
+
|
|
56
|
+
const [updated] = await mongodb.find(COLLECTION, { _id: docId })
|
|
57
|
+
assert.equal(updated.score, 99, 'score should be updated to 99')
|
|
58
|
+
assert.equal(updated.name, 'Eve', 'name should remain unchanged')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('Delete', () => {
|
|
63
|
+
it('should delete a document so it can no longer be found', async () => {
|
|
64
|
+
const inserted = await mongodb.insert(COLLECTION, { name: 'Frank', score: 0 })
|
|
65
|
+
|
|
66
|
+
await mongodb.delete(COLLECTION, { _id: inserted._id })
|
|
67
|
+
|
|
68
|
+
const results = await mongodb.find(COLLECTION, { _id: inserted._id })
|
|
69
|
+
assert.equal(results.length, 0, 'deleted document should not be found')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('Query Operators', () => {
|
|
74
|
+
before(async () => {
|
|
75
|
+
await mongodb.insert(COLLECTION, { group: 'queries', label: 'low', value: 5 })
|
|
76
|
+
await mongodb.insert(COLLECTION, { group: 'queries', label: 'mid', value: 15 })
|
|
77
|
+
await mongodb.insert(COLLECTION, { group: 'queries', label: 'high', value: 25 })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should support $gt operator', async () => {
|
|
81
|
+
const results = await mongodb.find(COLLECTION, { group: 'queries', value: { $gt: 10 } })
|
|
82
|
+
assert.equal(results.length, 2, 'should find two documents with value > 10')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should support $in operator', async () => {
|
|
86
|
+
const results = await mongodb.find(COLLECTION, { group: 'queries', label: { $in: ['low', 'high'] } })
|
|
87
|
+
assert.equal(results.length, 2, 'should find two documents matching $in')
|
|
88
|
+
const labels = results.map(r => r.label).sort()
|
|
89
|
+
assert.deepEqual(labels, ['high', 'low'])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should support $regex operator', async () => {
|
|
93
|
+
const results = await mongodb.find(COLLECTION, { group: 'queries', label: { $regex: '^h' } })
|
|
94
|
+
assert.equal(results.length, 1, 'should find one document matching regex')
|
|
95
|
+
assert.equal(results[0].label, 'high')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('Collection Access', () => {
|
|
100
|
+
it('should return a usable collection object from getCollection()', async () => {
|
|
101
|
+
const collection = mongodb.getCollection(COLLECTION)
|
|
102
|
+
assert.ok(collection, 'getCollection should return an object')
|
|
103
|
+
assert.equal(typeof collection.find, 'function', 'collection should have a find method')
|
|
104
|
+
assert.equal(typeof collection.insertOne, 'function', 'collection should have an insertOne method')
|
|
105
|
+
|
|
106
|
+
const count = await collection.countDocuments({})
|
|
107
|
+
assert.ok(count > 0, 'collection should contain documents from previous tests')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { getApp, getModule } from '../lib/app.js'
|
|
4
|
+
|
|
5
|
+
let roles
|
|
6
|
+
const createdRoleIds = []
|
|
7
|
+
|
|
8
|
+
describe('Roles', () => {
|
|
9
|
+
before(async () => {
|
|
10
|
+
await getApp()
|
|
11
|
+
roles = await getModule('roles')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
after(async () => {
|
|
15
|
+
for (const id of createdRoleIds) {
|
|
16
|
+
try {
|
|
17
|
+
await roles.delete({ _id: id })
|
|
18
|
+
} catch {
|
|
19
|
+
// role may already have been removed, that's fine
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// ── Default Roles ───────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
describe('Default roles', () => {
|
|
27
|
+
it('should have created the authuser role on boot', async () => {
|
|
28
|
+
const [authuser] = await roles.find({ shortName: 'authuser' })
|
|
29
|
+
assert.ok(authuser, 'authuser role should exist')
|
|
30
|
+
assert.equal(authuser.shortName, 'authuser')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should have created the contentcreator role on boot', async () => {
|
|
34
|
+
const [contentcreator] = await roles.find({ shortName: 'contentcreator' })
|
|
35
|
+
assert.ok(contentcreator, 'contentcreator role should exist')
|
|
36
|
+
assert.equal(contentcreator.displayName, 'Content creator')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should have created the superuser role on boot', async () => {
|
|
40
|
+
const [superuser] = await roles.find({ shortName: 'superuser' })
|
|
41
|
+
assert.ok(superuser, 'superuser role should exist')
|
|
42
|
+
assert.deepEqual(superuser.scopes, ['*:*'])
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// ── Scope Inheritance ──────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
describe('Scope inheritance', () => {
|
|
49
|
+
let roleA
|
|
50
|
+
let roleB
|
|
51
|
+
|
|
52
|
+
before(async () => {
|
|
53
|
+
roleA = await roles.insert({
|
|
54
|
+
shortName: 'testrole-a',
|
|
55
|
+
displayName: 'Test Role A',
|
|
56
|
+
scopes: ['read:foo']
|
|
57
|
+
})
|
|
58
|
+
createdRoleIds.push(roleA._id)
|
|
59
|
+
|
|
60
|
+
roleB = await roles.insert({
|
|
61
|
+
shortName: 'testrole-b',
|
|
62
|
+
displayName: 'Test Role B',
|
|
63
|
+
scopes: ['write:foo'],
|
|
64
|
+
extends: 'testrole-a'
|
|
65
|
+
})
|
|
66
|
+
createdRoleIds.push(roleB._id)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should return own scopes for a role with no parent', async () => {
|
|
70
|
+
const scopes = await roles.getScopesForRole(roleA._id)
|
|
71
|
+
assert.deepEqual(scopes, ['read:foo'])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should return both own and inherited scopes for a child role', async () => {
|
|
75
|
+
const scopes = await roles.getScopesForRole(roleB._id)
|
|
76
|
+
assert.ok(scopes.includes('write:foo'), 'should include own scope write:foo')
|
|
77
|
+
assert.ok(scopes.includes('read:foo'), 'should include inherited scope read:foo')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should return child scopes before parent scopes', async () => {
|
|
81
|
+
const scopes = await roles.getScopesForRole(roleB._id)
|
|
82
|
+
const writeIdx = scopes.indexOf('write:foo')
|
|
83
|
+
const readIdx = scopes.indexOf('read:foo')
|
|
84
|
+
assert.ok(writeIdx < readIdx, 'own scopes should come before inherited scopes')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should resolve the built-in contentcreator -> authuser chain', async () => {
|
|
88
|
+
const [contentcreator] = await roles.find({ shortName: 'contentcreator' })
|
|
89
|
+
const scopes = await roles.getScopesForRole(contentcreator._id)
|
|
90
|
+
assert.ok(scopes.includes('read:content'), 'should include contentcreator scope')
|
|
91
|
+
assert.ok(scopes.includes('read:me'), 'should include inherited authuser scope')
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// ── Super Role ─────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe('Super role', () => {
|
|
98
|
+
it('should return the ID of the role with *:* scope', async () => {
|
|
99
|
+
const superRoleId = await roles.getSuperRoleId()
|
|
100
|
+
assert.ok(superRoleId, 'should return a truthy ID')
|
|
101
|
+
const [superRole] = await roles.find({ _id: superRoleId })
|
|
102
|
+
assert.ok(superRole, 'should be a valid role')
|
|
103
|
+
assert.deepEqual(superRole.scopes, ['*:*'])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should match the superuser role by shortName', async () => {
|
|
107
|
+
const superRoleId = await roles.getSuperRoleId()
|
|
108
|
+
const [superuser] = await roles.find({ shortName: 'superuser' })
|
|
109
|
+
assert.equal(superRoleId, superuser._id.toString())
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
// ── Role CRUD ──────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
describe('Role CRUD', () => {
|
|
116
|
+
let customRole
|
|
117
|
+
|
|
118
|
+
it('should create a custom role', async () => {
|
|
119
|
+
customRole = await roles.insert({
|
|
120
|
+
shortName: 'testrole-crud',
|
|
121
|
+
displayName: 'Test CRUD Role',
|
|
122
|
+
scopes: ['read:test', 'write:test']
|
|
123
|
+
})
|
|
124
|
+
createdRoleIds.push(customRole._id)
|
|
125
|
+
assert.ok(customRole._id, 'inserted role should have an _id')
|
|
126
|
+
assert.equal(customRole.shortName, 'testrole-crud')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should find the custom role by shortName', async () => {
|
|
130
|
+
const [found] = await roles.find({ shortName: 'testrole-crud' })
|
|
131
|
+
assert.ok(found, 'should find the role')
|
|
132
|
+
assert.equal(found._id.toString(), customRole._id.toString())
|
|
133
|
+
assert.deepEqual(found.scopes, ['read:test', 'write:test'])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should find the custom role by _id', async () => {
|
|
137
|
+
const [found] = await roles.find({ _id: customRole._id })
|
|
138
|
+
assert.ok(found, 'should find the role by _id')
|
|
139
|
+
assert.equal(found.shortName, 'testrole-crud')
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
// ── Short Name Resolution ─────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
describe('Short name resolution', () => {
|
|
146
|
+
it('should resolve authuser shortName to its database ID', async () => {
|
|
147
|
+
const [authuser] = await roles.find({ shortName: 'authuser' })
|
|
148
|
+
const ids = await roles.shortNamesToIds(['authuser'])
|
|
149
|
+
assert.equal(ids.length, 1)
|
|
150
|
+
assert.equal(ids[0], authuser._id.toString())
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should resolve multiple shortNames in a single call', async () => {
|
|
154
|
+
const [authuser] = await roles.find({ shortName: 'authuser' })
|
|
155
|
+
const [superuser] = await roles.find({ shortName: 'superuser' })
|
|
156
|
+
const ids = await roles.shortNamesToIds(['authuser', 'superuser'])
|
|
157
|
+
assert.equal(ids.length, 2)
|
|
158
|
+
assert.equal(ids[0], authuser._id.toString())
|
|
159
|
+
assert.equal(ids[1], superuser._id.toString())
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { getApp, getModule, cleanDb } from '../lib/app.js'
|
|
4
|
+
|
|
5
|
+
let users
|
|
6
|
+
let authLocal
|
|
7
|
+
|
|
8
|
+
describe('Users module', () => {
|
|
9
|
+
before(async () => {
|
|
10
|
+
await getApp()
|
|
11
|
+
users = await getModule('users')
|
|
12
|
+
authLocal = await getModule('auth-local')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
after(async () => {
|
|
16
|
+
await cleanDb(['users', 'authtokens', 'passwordresets'])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('User creation', () => {
|
|
20
|
+
it('should create a user via authLocal.register()', async () => {
|
|
21
|
+
const user = await authLocal.register({
|
|
22
|
+
email: 'create-test@example.com',
|
|
23
|
+
firstName: 'Create',
|
|
24
|
+
lastName: 'Test',
|
|
25
|
+
password: 'Password123!'
|
|
26
|
+
})
|
|
27
|
+
assert.ok(user, 'register should return a user object')
|
|
28
|
+
assert.ok(user._id, 'user should have an _id')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should be retrievable via users.find()', async () => {
|
|
32
|
+
const results = await users.find({ email: 'create-test@example.com' })
|
|
33
|
+
assert.equal(results.length, 1, 'should find exactly one user')
|
|
34
|
+
assert.equal(results[0].firstName, 'Create')
|
|
35
|
+
assert.equal(results[0].lastName, 'Test')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('Email case insensitivity', () => {
|
|
40
|
+
it('should create a user with mixed case email', async () => {
|
|
41
|
+
const user = await authLocal.register({
|
|
42
|
+
email: 'MixedCase@Example.COM',
|
|
43
|
+
firstName: 'Mixed',
|
|
44
|
+
lastName: 'Case',
|
|
45
|
+
password: 'Password123!'
|
|
46
|
+
})
|
|
47
|
+
assert.ok(user._id, 'user should be created')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should store the email as lowercase', async () => {
|
|
51
|
+
const results = await users.find({ email: 'mixedcase@example.com' })
|
|
52
|
+
assert.equal(results.length, 1, 'should find the user with lowercase email')
|
|
53
|
+
assert.equal(results[0].email, 'mixedcase@example.com', 'stored email should be lowercase')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should find the user when querying with different case', async () => {
|
|
57
|
+
const results = await users.find({ email: 'MIXEDCASE@EXAMPLE.COM' })
|
|
58
|
+
assert.equal(results.length, 1, 'should find the user regardless of query case')
|
|
59
|
+
assert.equal(results[0].firstName, 'Mixed')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('Duplicate email', () => {
|
|
64
|
+
it('should reject a second user with the same email', async () => {
|
|
65
|
+
await assert.rejects(
|
|
66
|
+
() => authLocal.register({
|
|
67
|
+
email: 'create-test@example.com',
|
|
68
|
+
firstName: 'Duplicate',
|
|
69
|
+
lastName: 'User',
|
|
70
|
+
password: 'Password123!'
|
|
71
|
+
}),
|
|
72
|
+
(err) => {
|
|
73
|
+
const isDuplError = err.code === 'DUPL_USER' ||
|
|
74
|
+
err.code === 'MONGO_DUPL_INDEX' ||
|
|
75
|
+
/dupl/i.test(err.message) ||
|
|
76
|
+
/E11000/i.test(err.message)
|
|
77
|
+
assert.ok(isDuplError, `expected a duplicate error, got: ${err.code || err.message}`)
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('User update', () => {
|
|
85
|
+
it('should update a user firstName', async () => {
|
|
86
|
+
const [user] = await users.find({ email: 'create-test@example.com' })
|
|
87
|
+
await users.update({ _id: user._id }, { firstName: 'Updated' })
|
|
88
|
+
const [updated] = await users.find({ _id: user._id })
|
|
89
|
+
assert.equal(updated.firstName, 'Updated', 'firstName should be updated')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('User deletion', () => {
|
|
94
|
+
const deleteEmail = 'delete-me@example.com'
|
|
95
|
+
|
|
96
|
+
it('should create a user to be deleted', async () => {
|
|
97
|
+
const user = await authLocal.register({
|
|
98
|
+
email: deleteEmail,
|
|
99
|
+
firstName: 'Delete',
|
|
100
|
+
lastName: 'Me',
|
|
101
|
+
password: 'Password123!'
|
|
102
|
+
})
|
|
103
|
+
assert.ok(user._id, 'user should be created')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should delete the user', async () => {
|
|
107
|
+
const [user] = await users.find({ email: deleteEmail })
|
|
108
|
+
await users.delete({ _id: user._id })
|
|
109
|
+
const results = await users.find({ email: deleteEmail })
|
|
110
|
+
assert.equal(results.length, 0, 'user should no longer exist after deletion')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|