json-rest-api 1.0.13 → 1.0.15
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +13 -0
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +9 -5
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
package/tests/multihome.test.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import {
|
|
5
|
-
validateJsonApiStructure,
|
|
6
|
-
cleanTables,
|
|
1
|
+
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import {
|
|
5
|
+
validateJsonApiStructure,
|
|
6
|
+
cleanTables,
|
|
7
7
|
createJsonApiDocument,
|
|
8
8
|
assertResourceAttributes,
|
|
9
9
|
createRelationship,
|
|
10
10
|
resourceIdentifier
|
|
11
|
-
} from './helpers/test-utils.js'
|
|
12
|
-
import { createMultiHomeApi } from './fixtures/api-configs.js'
|
|
11
|
+
} from './helpers/test-utils.js'
|
|
12
|
+
import { createMultiHomeApi } from './fixtures/api-configs.js'
|
|
13
13
|
|
|
14
14
|
// Create Knex instance for tests - always use SQLite in-memory
|
|
15
15
|
const knex = knexLib({
|
|
@@ -18,24 +18,24 @@ const knex = knexLib({
|
|
|
18
18
|
filename: ':memory:'
|
|
19
19
|
},
|
|
20
20
|
useNullAsDefault: true
|
|
21
|
-
})
|
|
21
|
+
})
|
|
22
22
|
|
|
23
23
|
// API instance that persists across ALL tests
|
|
24
|
-
let api
|
|
24
|
+
let api
|
|
25
25
|
|
|
26
26
|
describe('MultiHome Plugin Tests', () => {
|
|
27
27
|
// IMPORTANT: before() runs ONCE for the entire test suite
|
|
28
28
|
before(async () => {
|
|
29
29
|
// Create API instance ONCE - this is reused for all tests
|
|
30
|
-
api = await createMultiHomeApi(knex)
|
|
31
|
-
})
|
|
32
|
-
|
|
30
|
+
api = await createMultiHomeApi(knex)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
33
|
// IMPORTANT: after() cleans up resources
|
|
34
34
|
after(async () => {
|
|
35
35
|
// Always destroy knex connection to allow tests to exit
|
|
36
|
-
await knex.destroy()
|
|
37
|
-
})
|
|
38
|
-
|
|
36
|
+
await knex.destroy()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
39
|
// IMPORTANT: beforeEach() cleans data but does NOT recreate API
|
|
40
40
|
beforeEach(async () => {
|
|
41
41
|
// Clean all tables - list ALL tables your tests use
|
|
@@ -44,35 +44,35 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
44
44
|
'multihome_tasks',
|
|
45
45
|
'multihome_users',
|
|
46
46
|
'multihome_system_settings'
|
|
47
|
-
])
|
|
48
|
-
})
|
|
49
|
-
|
|
47
|
+
])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
50
|
describe('Tenant Isolation', () => {
|
|
51
51
|
it('should automatically set tenant_id on new records', async () => {
|
|
52
52
|
// Create a project for tenant A
|
|
53
53
|
const doc = createJsonApiDocument('projects', {
|
|
54
54
|
name: 'Tenant A Project',
|
|
55
55
|
description: 'A project for tenant A'
|
|
56
|
-
})
|
|
57
|
-
|
|
56
|
+
})
|
|
57
|
+
|
|
58
58
|
// Create with tenant context
|
|
59
59
|
const createResult = await api.resources.projects.post({
|
|
60
60
|
inputRecord: doc,
|
|
61
61
|
simplified: false
|
|
62
62
|
}, {
|
|
63
63
|
auth: { multihome_id: 'tenant-a' }
|
|
64
|
-
})
|
|
65
|
-
|
|
64
|
+
})
|
|
65
|
+
|
|
66
66
|
// Verify response
|
|
67
|
-
validateJsonApiStructure(createResult)
|
|
68
|
-
assert.equal(createResult.data.type, 'projects')
|
|
67
|
+
validateJsonApiStructure(createResult)
|
|
68
|
+
assert.equal(createResult.data.type, 'projects')
|
|
69
69
|
assertResourceAttributes(createResult.data, {
|
|
70
70
|
name: 'Tenant A Project',
|
|
71
71
|
description: 'A project for tenant A',
|
|
72
72
|
tenant_id: 'tenant-a' // Should be automatically set
|
|
73
|
-
})
|
|
74
|
-
})
|
|
75
|
-
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
76
|
it('should filter queries by tenant_id', async () => {
|
|
77
77
|
// Create projects for different tenants
|
|
78
78
|
const projectA1 = await api.resources.projects.post({
|
|
@@ -82,8 +82,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
82
82
|
simplified: false
|
|
83
83
|
}, {
|
|
84
84
|
auth: { multihome_id: 'tenant-a' }
|
|
85
|
-
})
|
|
86
|
-
|
|
85
|
+
})
|
|
86
|
+
|
|
87
87
|
const projectA2 = await api.resources.projects.post({
|
|
88
88
|
inputRecord: createJsonApiDocument('projects', {
|
|
89
89
|
name: 'Project A2'
|
|
@@ -91,8 +91,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
91
91
|
simplified: false
|
|
92
92
|
}, {
|
|
93
93
|
auth: { multihome_id: 'tenant-a' }
|
|
94
|
-
})
|
|
95
|
-
|
|
94
|
+
})
|
|
95
|
+
|
|
96
96
|
const projectB1 = await api.resources.projects.post({
|
|
97
97
|
inputRecord: createJsonApiDocument('projects', {
|
|
98
98
|
name: 'Project B1'
|
|
@@ -100,31 +100,31 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
100
100
|
simplified: false
|
|
101
101
|
}, {
|
|
102
102
|
auth: { multihome_id: 'tenant-b' }
|
|
103
|
-
})
|
|
104
|
-
|
|
103
|
+
})
|
|
104
|
+
|
|
105
105
|
// Query as tenant A - should only see tenant A's projects
|
|
106
106
|
const resultA = await api.resources.projects.query({
|
|
107
107
|
simplified: false
|
|
108
108
|
}, {
|
|
109
109
|
auth: { multihome_id: 'tenant-a' }
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
validateJsonApiStructure(resultA, true)
|
|
113
|
-
assert.equal(resultA.data.length, 2, 'Tenant A should see 2 projects')
|
|
114
|
-
assert(resultA.data.every(p => p.attributes.name.startsWith('Project A')))
|
|
115
|
-
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
validateJsonApiStructure(resultA, true) // true for collection
|
|
113
|
+
assert.equal(resultA.data.length, 2, 'Tenant A should see 2 projects')
|
|
114
|
+
assert(resultA.data.every(p => p.attributes.name.startsWith('Project A')))
|
|
115
|
+
|
|
116
116
|
// Query as tenant B - should only see tenant B's projects
|
|
117
117
|
const resultB = await api.resources.projects.query({
|
|
118
118
|
simplified: false
|
|
119
119
|
}, {
|
|
120
120
|
auth: { multihome_id: 'tenant-b' }
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
validateJsonApiStructure(resultB, true)
|
|
124
|
-
assert.equal(resultB.data.length, 1, 'Tenant B should see 1 project')
|
|
125
|
-
assert.equal(resultB.data[0].attributes.name, 'Project B1')
|
|
126
|
-
})
|
|
127
|
-
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
validateJsonApiStructure(resultB, true) // true for collection
|
|
124
|
+
assert.equal(resultB.data.length, 1, 'Tenant B should see 1 project')
|
|
125
|
+
assert.equal(resultB.data[0].attributes.name, 'Project B1')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
128
|
it('should prevent access to other tenant records', async () => {
|
|
129
129
|
// Create a project for tenant A
|
|
130
130
|
const projectA = await api.resources.projects.post({
|
|
@@ -134,8 +134,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
134
134
|
simplified: false
|
|
135
135
|
}, {
|
|
136
136
|
auth: { multihome_id: 'tenant-a' }
|
|
137
|
-
})
|
|
138
|
-
|
|
137
|
+
})
|
|
138
|
+
|
|
139
139
|
// Try to access as tenant B - should fail
|
|
140
140
|
await assert.rejects(
|
|
141
141
|
async () => {
|
|
@@ -144,15 +144,15 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
144
144
|
simplified: false
|
|
145
145
|
}, {
|
|
146
146
|
auth: { multihome_id: 'tenant-b' }
|
|
147
|
-
})
|
|
147
|
+
})
|
|
148
148
|
},
|
|
149
149
|
(err) => {
|
|
150
|
-
return err.code === 'REST_API_RESOURCE'
|
|
150
|
+
return err.code === 'REST_API_RESOURCE'
|
|
151
151
|
},
|
|
152
152
|
'Should not be able to access other tenant data'
|
|
153
|
-
)
|
|
154
|
-
})
|
|
155
|
-
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
156
|
it('should prevent updating tenant_id to different value', async () => {
|
|
157
157
|
// Create a project for tenant A
|
|
158
158
|
const project = await api.resources.projects.post({
|
|
@@ -162,8 +162,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
162
162
|
simplified: false
|
|
163
163
|
}, {
|
|
164
164
|
auth: { multihome_id: 'tenant-a' }
|
|
165
|
-
})
|
|
166
|
-
|
|
165
|
+
})
|
|
166
|
+
|
|
167
167
|
// Try to update with different tenant_id - should fail
|
|
168
168
|
await assert.rejects(
|
|
169
169
|
async () => {
|
|
@@ -182,18 +182,18 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
182
182
|
simplified: false
|
|
183
183
|
}, {
|
|
184
184
|
auth: { multihome_id: 'tenant-a' }
|
|
185
|
-
})
|
|
185
|
+
})
|
|
186
186
|
},
|
|
187
187
|
(err) => {
|
|
188
|
-
console.log('PATCH error:', err.message)
|
|
189
|
-
console.log('PATCH error stack:', err.stack)
|
|
190
|
-
return err.message.includes('must match current context')
|
|
188
|
+
console.log('PATCH error:', err.message)
|
|
189
|
+
console.log('PATCH error stack:', err.stack)
|
|
190
|
+
return err.message.includes('must match current context')
|
|
191
191
|
},
|
|
192
192
|
'Should not be able to change tenant_id'
|
|
193
|
-
)
|
|
194
|
-
})
|
|
195
|
-
})
|
|
196
|
-
|
|
193
|
+
)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
197
|
describe('Relationships Across Tenants', () => {
|
|
198
198
|
it('should maintain tenant isolation for related records', async () => {
|
|
199
199
|
// Create projects for different tenants
|
|
@@ -204,8 +204,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
204
204
|
simplified: false
|
|
205
205
|
}, {
|
|
206
206
|
auth: { multihome_id: 'tenant-a' }
|
|
207
|
-
})
|
|
208
|
-
|
|
207
|
+
})
|
|
208
|
+
|
|
209
209
|
const projectB = await api.resources.projects.post({
|
|
210
210
|
inputRecord: createJsonApiDocument('projects', {
|
|
211
211
|
name: 'Tenant B Project'
|
|
@@ -213,11 +213,11 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
213
213
|
simplified: false
|
|
214
214
|
}, {
|
|
215
215
|
auth: { multihome_id: 'tenant-b' }
|
|
216
|
-
})
|
|
217
|
-
|
|
216
|
+
})
|
|
217
|
+
|
|
218
218
|
// Create task for tenant A project
|
|
219
219
|
const taskA = await api.resources.tasks.post({
|
|
220
|
-
inputRecord: createJsonApiDocument('tasks',
|
|
220
|
+
inputRecord: createJsonApiDocument('tasks',
|
|
221
221
|
{
|
|
222
222
|
title: 'Task for Tenant A'
|
|
223
223
|
},
|
|
@@ -228,13 +228,13 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
228
228
|
simplified: false
|
|
229
229
|
}, {
|
|
230
230
|
auth: { multihome_id: 'tenant-a' }
|
|
231
|
-
})
|
|
232
|
-
|
|
231
|
+
})
|
|
232
|
+
|
|
233
233
|
// Try to create task for tenant B project as tenant A - should fail
|
|
234
234
|
await assert.rejects(
|
|
235
235
|
async () => {
|
|
236
236
|
await api.resources.tasks.post({
|
|
237
|
-
inputRecord: createJsonApiDocument('tasks',
|
|
237
|
+
inputRecord: createJsonApiDocument('tasks',
|
|
238
238
|
{
|
|
239
239
|
title: 'Invalid Task'
|
|
240
240
|
},
|
|
@@ -245,15 +245,15 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
245
245
|
simplified: false
|
|
246
246
|
}, {
|
|
247
247
|
auth: { multihome_id: 'tenant-a' }
|
|
248
|
-
})
|
|
248
|
+
})
|
|
249
249
|
},
|
|
250
250
|
(err) => {
|
|
251
251
|
// Should fail because project belongs to different tenant
|
|
252
|
-
return err.code === 'REST_API_VALIDATION' || err.code === 'REST_API_RESOURCE'
|
|
252
|
+
return err.code === 'REST_API_VALIDATION' || err.code === 'REST_API_RESOURCE'
|
|
253
253
|
},
|
|
254
254
|
'Should not be able to link to other tenant resources'
|
|
255
|
-
)
|
|
256
|
-
|
|
255
|
+
)
|
|
256
|
+
|
|
257
257
|
// Query with includes - should only see own tenant data
|
|
258
258
|
const projectWithTasks = await api.resources.projects.get({
|
|
259
259
|
id: projectA.data.id,
|
|
@@ -263,15 +263,15 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
263
263
|
simplified: false
|
|
264
264
|
}, {
|
|
265
265
|
auth: { multihome_id: 'tenant-a' }
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
validateJsonApiStructure(projectWithTasks)
|
|
269
|
-
assert(projectWithTasks.included, 'Should have included tasks')
|
|
270
|
-
assert.equal(projectWithTasks.included.length, 1, 'Should include one task')
|
|
271
|
-
assert.equal(projectWithTasks.included[0].attributes.tenant_id, 'tenant-a')
|
|
272
|
-
})
|
|
273
|
-
})
|
|
274
|
-
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
validateJsonApiStructure(projectWithTasks)
|
|
269
|
+
assert(projectWithTasks.included, 'Should have included tasks')
|
|
270
|
+
assert.equal(projectWithTasks.included.length, 1, 'Should include one task')
|
|
271
|
+
assert.equal(projectWithTasks.included[0].attributes.tenant_id, 'tenant-a')
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
275
|
describe('Excluded Resources', () => {
|
|
276
276
|
it('should allow access to excluded resources without tenant context', async () => {
|
|
277
277
|
// Create system setting without tenant context
|
|
@@ -281,31 +281,31 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
281
281
|
value: '1.0.0'
|
|
282
282
|
}),
|
|
283
283
|
simplified: false
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
validateJsonApiStructure(setting)
|
|
287
|
-
assert.equal(setting.data.type, 'system_settings')
|
|
288
|
-
assert(!setting.data.attributes.tenant_id, 'Should not have tenant_id')
|
|
289
|
-
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
validateJsonApiStructure(setting)
|
|
287
|
+
assert.equal(setting.data.type, 'system_settings')
|
|
288
|
+
assert(!setting.data.attributes.tenant_id, 'Should not have tenant_id')
|
|
289
|
+
|
|
290
290
|
// Query without tenant context should work
|
|
291
291
|
const allSettings = await api.resources.system_settings.query({
|
|
292
292
|
simplified: false
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
assert.equal(allSettings.data.length, 1)
|
|
296
|
-
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
assert.equal(allSettings.data.length, 1)
|
|
296
|
+
|
|
297
297
|
// Should be accessible by any tenant
|
|
298
298
|
const settingByTenantA = await api.resources.system_settings.get({
|
|
299
299
|
id: setting.data.id,
|
|
300
300
|
simplified: false
|
|
301
301
|
}, {
|
|
302
302
|
auth: { multihome_id: 'tenant-a' }
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
assert.equal(settingByTenantA.data.id, setting.data.id)
|
|
306
|
-
})
|
|
307
|
-
})
|
|
308
|
-
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
assert.equal(settingByTenantA.data.id, setting.data.id)
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
309
|
describe('Configuration Options', () => {
|
|
310
310
|
it('should require tenant context when requireAuth is true', async () => {
|
|
311
311
|
// Try to create without tenant context - should fail
|
|
@@ -316,26 +316,26 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
316
316
|
name: 'No Tenant Project'
|
|
317
317
|
}),
|
|
318
318
|
simplified: false
|
|
319
|
-
})
|
|
319
|
+
})
|
|
320
320
|
},
|
|
321
321
|
(err) => {
|
|
322
|
-
return err.message.includes('multihome context')
|
|
322
|
+
return err.message.includes('multihome context')
|
|
323
323
|
},
|
|
324
324
|
'Should require tenant context when requireAuth is true'
|
|
325
|
-
)
|
|
326
|
-
})
|
|
327
|
-
|
|
325
|
+
)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
328
|
it('should validate resources have tenant field when allowMissing is false', async () => {
|
|
329
329
|
// This test validates that the plugin threw an error during setup
|
|
330
330
|
// if a resource was missing the tenant field. Since our test resources
|
|
331
331
|
// all have tenant_id (except excluded ones), we can verify the
|
|
332
332
|
// configuration is working by checking vars
|
|
333
|
-
assert.equal(api.vars.multihome.field, 'tenant_id')
|
|
334
|
-
assert.equal(api.vars.multihome.allowMissing, false)
|
|
335
|
-
assert(api.vars.multihome.excludeResources.includes('system_settings'))
|
|
336
|
-
})
|
|
337
|
-
})
|
|
338
|
-
|
|
333
|
+
assert.equal(api.vars.multihome.field, 'tenant_id')
|
|
334
|
+
assert.equal(api.vars.multihome.allowMissing, false)
|
|
335
|
+
assert(api.vars.multihome.excludeResources.includes('system_settings'))
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
339
|
describe('Complex Queries', () => {
|
|
340
340
|
it('should apply tenant filter to filtered queries', async () => {
|
|
341
341
|
// Create users for different tenants
|
|
@@ -348,8 +348,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
348
348
|
simplified: false
|
|
349
349
|
}, {
|
|
350
350
|
auth: { multihome_id: 'tenant-a' }
|
|
351
|
-
})
|
|
352
|
-
|
|
351
|
+
})
|
|
352
|
+
|
|
353
353
|
await api.resources.users.post({
|
|
354
354
|
inputRecord: createJsonApiDocument('users', {
|
|
355
355
|
email: 'user@tenant-a.com',
|
|
@@ -359,8 +359,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
359
359
|
simplified: false
|
|
360
360
|
}, {
|
|
361
361
|
auth: { multihome_id: 'tenant-a' }
|
|
362
|
-
})
|
|
363
|
-
|
|
362
|
+
})
|
|
363
|
+
|
|
364
364
|
await api.resources.users.post({
|
|
365
365
|
inputRecord: createJsonApiDocument('users', {
|
|
366
366
|
email: 'admin@tenant-b.com',
|
|
@@ -370,8 +370,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
370
370
|
simplified: false
|
|
371
371
|
}, {
|
|
372
372
|
auth: { multihome_id: 'tenant-b' }
|
|
373
|
-
})
|
|
374
|
-
|
|
373
|
+
})
|
|
374
|
+
|
|
375
375
|
// Query for admins as tenant A - should only see tenant A admin
|
|
376
376
|
const adminsA = await api.resources.users.query({
|
|
377
377
|
queryParams: {
|
|
@@ -380,22 +380,22 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
380
380
|
simplified: false
|
|
381
381
|
}, {
|
|
382
382
|
auth: { multihome_id: 'tenant-a' }
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
assert.equal(adminsA.data.length, 1, 'Should only see tenant A admin')
|
|
386
|
-
assert.equal(adminsA.data[0].attributes.email, 'admin@tenant-a.com')
|
|
387
|
-
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
assert.equal(adminsA.data.length, 1, 'Should only see tenant A admin')
|
|
386
|
+
assert.equal(adminsA.data[0].attributes.email, 'admin@tenant-a.com')
|
|
387
|
+
|
|
388
388
|
// Query for all users as tenant B - should only see tenant B users
|
|
389
389
|
const usersB = await api.resources.users.query({
|
|
390
390
|
simplified: false
|
|
391
391
|
}, {
|
|
392
392
|
auth: { multihome_id: 'tenant-b' }
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
assert.equal(usersB.data.length, 1, 'Should only see tenant B users')
|
|
396
|
-
assert.equal(usersB.data[0].attributes.email, 'admin@tenant-b.com')
|
|
397
|
-
})
|
|
398
|
-
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
assert.equal(usersB.data.length, 1, 'Should only see tenant B users')
|
|
396
|
+
assert.equal(usersB.data[0].attributes.email, 'admin@tenant-b.com')
|
|
397
|
+
})
|
|
398
|
+
|
|
399
399
|
it('should apply tenant filter with sorting and pagination', async () => {
|
|
400
400
|
// Create multiple projects for tenant A
|
|
401
401
|
for (let i = 1; i <= 5; i++) {
|
|
@@ -407,9 +407,9 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
407
407
|
simplified: false
|
|
408
408
|
}, {
|
|
409
409
|
auth: { multihome_id: 'tenant-a' }
|
|
410
|
-
})
|
|
410
|
+
})
|
|
411
411
|
}
|
|
412
|
-
|
|
412
|
+
|
|
413
413
|
// Create projects for tenant B
|
|
414
414
|
for (let i = 1; i <= 3; i++) {
|
|
415
415
|
await api.resources.projects.post({
|
|
@@ -419,9 +419,9 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
419
419
|
simplified: false
|
|
420
420
|
}, {
|
|
421
421
|
auth: { multihome_id: 'tenant-b' }
|
|
422
|
-
})
|
|
422
|
+
})
|
|
423
423
|
}
|
|
424
|
-
|
|
424
|
+
|
|
425
425
|
// Query with pagination as tenant A
|
|
426
426
|
const page1 = await api.resources.projects.query({
|
|
427
427
|
queryParams: {
|
|
@@ -431,12 +431,12 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
431
431
|
simplified: false
|
|
432
432
|
}, {
|
|
433
433
|
auth: { multihome_id: 'tenant-a' }
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
assert.equal(page1.data.length, 2, 'Should return page size')
|
|
437
|
-
assert.equal(page1.data[0].attributes.name, 'Project A5', 'Should be sorted descending')
|
|
438
|
-
assert.equal(page1.data[1].attributes.name, 'Project A4')
|
|
439
|
-
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
assert.equal(page1.data.length, 2, 'Should return page size')
|
|
437
|
+
assert.equal(page1.data[0].attributes.name, 'Project A5', 'Should be sorted descending')
|
|
438
|
+
assert.equal(page1.data[1].attributes.name, 'Project A4')
|
|
439
|
+
|
|
440
440
|
// Get page 2
|
|
441
441
|
const page2 = await api.resources.projects.query({
|
|
442
442
|
queryParams: {
|
|
@@ -446,28 +446,28 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
446
446
|
simplified: false
|
|
447
447
|
}, {
|
|
448
448
|
auth: { multihome_id: 'tenant-a' }
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
assert.equal(page2.data.length, 2, 'Should return page size')
|
|
452
|
-
assert(page2.data.every(p => p.attributes.tenant_id === 'tenant-a'))
|
|
453
|
-
})
|
|
454
|
-
})
|
|
455
|
-
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
assert.equal(page2.data.length, 2, 'Should return page size')
|
|
452
|
+
assert(page2.data.every(p => p.attributes.tenant_id === 'tenant-a'))
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
456
|
describe('Error Handling', () => {
|
|
457
457
|
it('should provide clear error when no tenant context available', async () => {
|
|
458
458
|
await assert.rejects(
|
|
459
459
|
async () => {
|
|
460
460
|
await api.resources.projects.query({
|
|
461
461
|
simplified: false
|
|
462
|
-
})
|
|
462
|
+
})
|
|
463
463
|
},
|
|
464
464
|
(err) => {
|
|
465
|
-
return err.message.includes('No multihome context available')
|
|
465
|
+
return err.message.includes('No multihome context available')
|
|
466
466
|
},
|
|
467
467
|
'Should provide clear error message'
|
|
468
|
-
)
|
|
469
|
-
})
|
|
470
|
-
|
|
468
|
+
)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
471
|
it('should handle missing tenant field gracefully for excluded resources', async () => {
|
|
472
472
|
// system_settings is excluded and doesn't have tenant_id field
|
|
473
473
|
const result = await api.resources.system_settings.post({
|
|
@@ -478,13 +478,13 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
478
478
|
simplified: false
|
|
479
479
|
}, {
|
|
480
480
|
auth: { multihome_id: 'tenant-a' } // Should be ignored for excluded resources
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
validateJsonApiStructure(result)
|
|
484
|
-
assert(!result.data.attributes.tenant_id, 'Excluded resource should not have tenant_id')
|
|
485
|
-
})
|
|
486
|
-
})
|
|
487
|
-
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
validateJsonApiStructure(result)
|
|
484
|
+
assert(!result.data.attributes.tenant_id, 'Excluded resource should not have tenant_id')
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
488
|
describe('Security', () => {
|
|
489
489
|
it('should prevent bypassing tenant filter with malicious IDs', async () => {
|
|
490
490
|
// Create a project for tenant A
|
|
@@ -495,15 +495,15 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
495
495
|
simplified: false
|
|
496
496
|
}, {
|
|
497
497
|
auth: { multihome_id: 'tenant-a' }
|
|
498
|
-
})
|
|
499
|
-
|
|
498
|
+
})
|
|
499
|
+
|
|
500
500
|
// Try various bypass attempts as tenant B
|
|
501
501
|
const maliciousIds = [
|
|
502
502
|
project.data.id,
|
|
503
503
|
`${project.data.id} OR 1=1`,
|
|
504
504
|
`${project.data.id}'; DROP TABLE projects; --`
|
|
505
|
-
]
|
|
506
|
-
|
|
505
|
+
]
|
|
506
|
+
|
|
507
507
|
for (const id of maliciousIds) {
|
|
508
508
|
await assert.rejects(
|
|
509
509
|
async () => {
|
|
@@ -512,19 +512,19 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
512
512
|
simplified: false
|
|
513
513
|
}, {
|
|
514
514
|
auth: { multihome_id: 'tenant-b' }
|
|
515
|
-
})
|
|
515
|
+
})
|
|
516
516
|
},
|
|
517
517
|
undefined, // Any error is fine
|
|
518
518
|
`Should not bypass security with ID: ${id}`
|
|
519
|
-
)
|
|
519
|
+
)
|
|
520
520
|
}
|
|
521
|
-
})
|
|
522
|
-
|
|
521
|
+
})
|
|
522
|
+
|
|
523
523
|
it('should log security violations', async () => {
|
|
524
524
|
// This test verifies that the plugin logs security violations
|
|
525
525
|
// In a real implementation, you might capture logs and verify
|
|
526
526
|
// For now, we just ensure the security check happens
|
|
527
|
-
|
|
527
|
+
|
|
528
528
|
// Create data for tenant A
|
|
529
529
|
const project = await api.resources.projects.post({
|
|
530
530
|
inputRecord: createJsonApiDocument('projects', {
|
|
@@ -533,8 +533,8 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
533
533
|
simplified: false
|
|
534
534
|
}, {
|
|
535
535
|
auth: { multihome_id: 'tenant-a' }
|
|
536
|
-
})
|
|
537
|
-
|
|
536
|
+
})
|
|
537
|
+
|
|
538
538
|
// Attempt access as tenant B
|
|
539
539
|
try {
|
|
540
540
|
await api.resources.projects.get({
|
|
@@ -542,12 +542,12 @@ describe('MultiHome Plugin Tests', () => {
|
|
|
542
542
|
simplified: false
|
|
543
543
|
}, {
|
|
544
544
|
auth: { multihome_id: 'tenant-b' }
|
|
545
|
-
})
|
|
546
|
-
assert.fail('Should have thrown error')
|
|
545
|
+
})
|
|
546
|
+
assert.fail('Should have thrown error')
|
|
547
547
|
} catch (err) {
|
|
548
548
|
// Error is expected - security violation should be logged
|
|
549
|
-
assert(err.code === 'REST_API_RESOURCE' || err.message.includes('Access denied'))
|
|
549
|
+
assert(err.code === 'REST_API_RESOURCE' || err.message.includes('Access denied'))
|
|
550
550
|
}
|
|
551
|
-
})
|
|
552
|
-
})
|
|
553
|
-
})
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
})
|