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/cors.test.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import express from 'express'
|
|
5
|
-
import request from 'supertest'
|
|
6
|
-
import {
|
|
7
|
-
validateJsonApiStructure,
|
|
8
|
-
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 express from 'express'
|
|
5
|
+
import request from 'supertest'
|
|
6
|
+
import {
|
|
7
|
+
validateJsonApiStructure,
|
|
8
|
+
cleanTables,
|
|
9
9
|
createJsonApiDocument,
|
|
10
10
|
assertResourceAttributes
|
|
11
|
-
} from './helpers/test-utils.js'
|
|
12
|
-
import { createBasicApi } from './fixtures/api-configs.js'
|
|
13
|
-
import { CorsPlugin } from '../plugins/core/rest-api-cors-plugin.js'
|
|
11
|
+
} from './helpers/test-utils.js'
|
|
12
|
+
import { createBasicApi } from './fixtures/api-configs.js'
|
|
13
|
+
import { CorsPlugin } from '../plugins/core/rest-api-cors-plugin.js'
|
|
14
14
|
|
|
15
15
|
// Create Knex instance for tests - always use SQLite in-memory
|
|
16
16
|
const knex = knexLib({
|
|
@@ -19,37 +19,37 @@ const knex = knexLib({
|
|
|
19
19
|
filename: ':memory:'
|
|
20
20
|
},
|
|
21
21
|
useNullAsDefault: true
|
|
22
|
-
})
|
|
22
|
+
})
|
|
23
23
|
|
|
24
24
|
// API instance and Express app that persist across tests
|
|
25
|
-
let api
|
|
26
|
-
let app
|
|
25
|
+
let api
|
|
26
|
+
let app
|
|
27
27
|
|
|
28
28
|
describe('CORS Plugin Tests', { timeout: 30000 }, () => {
|
|
29
29
|
describe('CORS with baseUrl', () => {
|
|
30
|
-
let baseUrlApi
|
|
31
|
-
let baseUrlApp
|
|
32
|
-
|
|
30
|
+
let baseUrlApi
|
|
31
|
+
let baseUrlApp
|
|
32
|
+
|
|
33
33
|
before(async () => {
|
|
34
34
|
// Create a new API instance with Express configured with a baseUrl
|
|
35
|
-
baseUrlApp = express()
|
|
35
|
+
baseUrlApp = express()
|
|
36
36
|
// Create API with different name to avoid registry conflict
|
|
37
|
-
const Api = (await import('hooked-api')).Api
|
|
38
|
-
baseUrlApi = new Api({
|
|
37
|
+
const Api = (await import('hooked-api')).Api
|
|
38
|
+
baseUrlApi = new Api({
|
|
39
39
|
name: 'cors-baseurl-test-api',
|
|
40
40
|
log: { level: 'info' }
|
|
41
|
-
})
|
|
42
|
-
|
|
41
|
+
})
|
|
42
|
+
|
|
43
43
|
// Install plugins
|
|
44
44
|
await baseUrlApi.use((await import('../plugins/core/rest-api-plugin.js')).RestApiPlugin, {
|
|
45
45
|
simplifiedApi: false
|
|
46
|
-
})
|
|
47
|
-
await baseUrlApi.use((await import('../plugins/core/rest-api-knex-plugin.js')).RestApiKnexPlugin, { knex })
|
|
46
|
+
})
|
|
47
|
+
await baseUrlApi.use((await import('../plugins/core/rest-api-knex-plugin.js')).RestApiKnexPlugin, { knex })
|
|
48
48
|
await baseUrlApi.use((await import('../plugins/core/connectors/express-plugin.js')).ExpressPlugin, {
|
|
49
49
|
app: baseUrlApp,
|
|
50
50
|
mountPath: '/api'
|
|
51
|
-
})
|
|
52
|
-
|
|
51
|
+
})
|
|
52
|
+
|
|
53
53
|
// Add basic scope
|
|
54
54
|
await baseUrlApi.addScope('countries', {
|
|
55
55
|
restApi: {
|
|
@@ -60,116 +60,115 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
-
})
|
|
64
|
-
|
|
63
|
+
})
|
|
64
|
+
|
|
65
65
|
// Install CORS plugin
|
|
66
|
-
console.log('[TEST] vars.transport before CORS install:', baseUrlApi.vars.transport)
|
|
67
|
-
|
|
66
|
+
console.log('[TEST] vars.transport before CORS install:', baseUrlApi.vars.transport)
|
|
67
|
+
|
|
68
68
|
try {
|
|
69
69
|
await baseUrlApi.use(CorsPlugin, {
|
|
70
70
|
origin: '*',
|
|
71
71
|
credentials: true
|
|
72
|
-
})
|
|
72
|
+
})
|
|
73
73
|
} catch (error) {
|
|
74
|
-
console.log('[TEST] CORS installation error:', error.message)
|
|
75
|
-
console.log('[TEST] vars.transport.matchAll:', baseUrlApi.vars.transport?.matchAll)
|
|
76
|
-
console.log('[TEST] mountPath from transport:', baseUrlApi.vars.transport?.mountPath)
|
|
77
|
-
console.log('[TEST] Full wildcard path would be:', '' + (baseUrlApi.vars.transport?.matchAll || '*'))
|
|
78
|
-
throw error
|
|
74
|
+
console.log('[TEST] CORS installation error:', error.message)
|
|
75
|
+
console.log('[TEST] vars.transport.matchAll:', baseUrlApi.vars.transport?.matchAll)
|
|
76
|
+
console.log('[TEST] mountPath from transport:', baseUrlApi.vars.transport?.mountPath)
|
|
77
|
+
console.log('[TEST] Full wildcard path would be:', '' + (baseUrlApi.vars.transport?.matchAll || '*'))
|
|
78
|
+
throw error
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
// Mount the routes
|
|
82
|
-
baseUrlApi.http.express.mount(baseUrlApp)
|
|
83
|
-
})
|
|
84
|
-
|
|
82
|
+
baseUrlApi.http.express.mount(baseUrlApp)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
85
|
it('should handle OPTIONS preflight with baseUrl', async () => {
|
|
86
86
|
const response = await request(baseUrlApp)
|
|
87
87
|
.options('/api/countries')
|
|
88
88
|
.set('Origin', 'https://example.com')
|
|
89
89
|
.set('Access-Control-Request-Method', 'POST')
|
|
90
|
-
.set('Access-Control-Request-Headers', 'Content-Type')
|
|
91
|
-
|
|
90
|
+
.set('Access-Control-Request-Headers', 'Content-Type')
|
|
91
|
+
|
|
92
92
|
console.log('[TEST] BaseUrl OPTIONS response:', {
|
|
93
93
|
status: response.status,
|
|
94
94
|
headers: response.headers,
|
|
95
95
|
body: response.body
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
assert.equal(response.status, 204)
|
|
99
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
100
|
-
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
101
|
-
})
|
|
102
|
-
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
assert.equal(response.status, 204)
|
|
99
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
100
|
+
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
103
|
it('should handle GET requests with baseUrl', async () => {
|
|
104
104
|
// Just test the GET endpoint without creating data first
|
|
105
105
|
// This tests CORS headers on the response regardless of data
|
|
106
106
|
const response = await request(baseUrlApp)
|
|
107
107
|
.get('/api/countries')
|
|
108
108
|
.set('Origin', 'https://example.com')
|
|
109
|
-
.set('Accept', 'application/vnd.api+json')
|
|
110
|
-
|
|
109
|
+
.set('Accept', 'application/vnd.api+json')
|
|
110
|
+
|
|
111
111
|
console.log('[TEST] BaseUrl GET response:', {
|
|
112
112
|
status: response.status,
|
|
113
113
|
headers: response.headers
|
|
114
|
-
})
|
|
115
|
-
|
|
114
|
+
})
|
|
115
|
+
|
|
116
116
|
// We might get 500 due to missing table, but CORS headers should still be present
|
|
117
|
-
assert(response.status === 200 || response.status === 500, `Expected 200 or 500, got ${response.status}`)
|
|
118
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
119
|
-
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
120
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
121
|
-
})
|
|
122
|
-
|
|
117
|
+
assert(response.status === 200 || response.status === 500, `Expected 200 or 500, got ${response.status}`)
|
|
118
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
119
|
+
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
120
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
123
|
it('should handle wildcard OPTIONS route with baseUrl', async () => {
|
|
124
124
|
// Test a non-existent endpoint to check if wildcard OPTIONS still works
|
|
125
125
|
const response = await request(baseUrlApp)
|
|
126
126
|
.options('/api/non-existent-endpoint')
|
|
127
127
|
.set('Origin', 'https://example.com')
|
|
128
|
-
.set('Access-Control-Request-Method', 'POST')
|
|
129
|
-
|
|
128
|
+
.set('Access-Control-Request-Method', 'POST')
|
|
129
|
+
|
|
130
130
|
console.log('[TEST] BaseUrl wildcard OPTIONS response:', {
|
|
131
131
|
status: response.status,
|
|
132
132
|
headers: response.headers
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// This will tell us if the wildcard '*' route is working with baseUrl
|
|
136
|
-
assert.equal(response.status, 204);
|
|
137
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com');
|
|
138
|
-
});
|
|
139
|
-
});
|
|
133
|
+
})
|
|
140
134
|
|
|
135
|
+
// This will tell us if the wildcard '*' route is working with baseUrl
|
|
136
|
+
assert.equal(response.status, 204)
|
|
137
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
141
140
|
|
|
142
141
|
// IMPORTANT: before() runs ONCE for the entire test suite
|
|
143
142
|
before(async () => {
|
|
144
143
|
// Initialize API once with Express
|
|
145
|
-
app = express()
|
|
144
|
+
app = express()
|
|
146
145
|
api = await createBasicApi(knex, {
|
|
147
146
|
includeExpress: true,
|
|
148
147
|
express: { app }
|
|
149
|
-
})
|
|
150
|
-
|
|
148
|
+
})
|
|
149
|
+
|
|
151
150
|
// Install CORS plugin
|
|
152
151
|
await api.use(CorsPlugin, {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
|
|
152
|
+
|
|
153
|
+
origin: '*',
|
|
154
|
+
credentials: true,
|
|
155
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
156
|
+
allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
|
|
157
|
+
exposedHeaders: ['X-Total-Count', 'X-Page-Count', 'Link'],
|
|
158
|
+
maxAge: 86400
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
163
162
|
// Mount the routes on the Express app
|
|
164
|
-
api.http.express.mount(app)
|
|
165
|
-
})
|
|
166
|
-
|
|
163
|
+
api.http.express.mount(app)
|
|
164
|
+
})
|
|
165
|
+
|
|
167
166
|
// IMPORTANT: after() cleans up resources
|
|
168
167
|
after(async () => {
|
|
169
168
|
// Always destroy knex connection to allow tests to exit
|
|
170
|
-
await knex.destroy()
|
|
171
|
-
})
|
|
172
|
-
|
|
169
|
+
await knex.destroy()
|
|
170
|
+
})
|
|
171
|
+
|
|
173
172
|
// IMPORTANT: beforeEach() cleans data but does NOT recreate API
|
|
174
173
|
beforeEach(async () => {
|
|
175
174
|
// Clean all tables - list ALL tables your tests use
|
|
@@ -179,141 +178,141 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
|
|
|
179
178
|
'basic_authors',
|
|
180
179
|
'basic_books',
|
|
181
180
|
'basic_book_authors'
|
|
182
|
-
])
|
|
183
|
-
})
|
|
181
|
+
])
|
|
182
|
+
})
|
|
184
183
|
|
|
185
184
|
describe('CORS Headers for Regular Requests', () => {
|
|
186
|
-
|
|
185
|
+
it('should add CORS headers for GET requests', async () => {
|
|
187
186
|
// Create test data
|
|
188
187
|
const doc = createJsonApiDocument('countries', {
|
|
189
188
|
name: 'Test Country',
|
|
190
189
|
code: 'TC'
|
|
191
|
-
})
|
|
192
|
-
|
|
190
|
+
})
|
|
191
|
+
|
|
193
192
|
const createResult = await api.resources.countries.post({
|
|
194
193
|
inputRecord: doc,
|
|
195
194
|
simplified: false
|
|
196
|
-
})
|
|
197
|
-
|
|
195
|
+
})
|
|
196
|
+
|
|
198
197
|
// Make HTTP request with origin header
|
|
199
198
|
const response = await request(app)
|
|
200
199
|
.get('/api/countries')
|
|
201
200
|
.set('Origin', 'https://example.com')
|
|
202
|
-
.set('Accept', 'application/vnd.api+json')
|
|
203
|
-
|
|
204
|
-
assert.equal(response.status, 200)
|
|
201
|
+
.set('Accept', 'application/vnd.api+json')
|
|
202
|
+
|
|
203
|
+
assert.equal(response.status, 200)
|
|
205
204
|
// When credentials are true, the origin must be specific, not wildcard
|
|
206
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
207
|
-
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
208
|
-
assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
|
|
205
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
206
|
+
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
207
|
+
assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
|
|
209
208
|
// Vary header should be set when specific origin is returned
|
|
210
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should add CORS headers for POST requests', async () => {
|
|
214
213
|
const doc = createJsonApiDocument('countries', {
|
|
215
214
|
name: 'New Country',
|
|
216
215
|
code: 'NC'
|
|
217
|
-
})
|
|
218
|
-
|
|
216
|
+
})
|
|
217
|
+
|
|
219
218
|
const response = await request(app)
|
|
220
219
|
.post('/api/countries')
|
|
221
220
|
.send(doc)
|
|
222
221
|
.set('Origin', 'https://example.com')
|
|
223
222
|
.set('Content-Type', 'application/vnd.api+json')
|
|
224
|
-
.set('Accept', 'application/vnd.api+json')
|
|
225
|
-
|
|
226
|
-
assert.equal(response.status, 201)
|
|
223
|
+
.set('Accept', 'application/vnd.api+json')
|
|
224
|
+
|
|
225
|
+
assert.equal(response.status, 201)
|
|
227
226
|
// When credentials are true, the origin must be specific, not wildcard
|
|
228
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
229
|
-
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
230
|
-
assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
|
|
227
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
228
|
+
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
229
|
+
assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
|
|
231
230
|
// Vary header should be set when specific origin is returned
|
|
232
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
|
|
231
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should handle requests without Origin header', async () => {
|
|
236
235
|
// Same-origin request (no Origin header)
|
|
237
236
|
const response = await request(app)
|
|
238
237
|
.get('/api/countries')
|
|
239
|
-
.set('Accept', 'application/vnd.api+json')
|
|
240
|
-
|
|
241
|
-
assert.equal(response.status, 200)
|
|
238
|
+
.set('Accept', 'application/vnd.api+json')
|
|
239
|
+
|
|
240
|
+
assert.equal(response.status, 200)
|
|
242
241
|
// Should not set CORS headers for same-origin requests
|
|
243
|
-
assert.equal(response.headers['access-control-allow-origin'], undefined)
|
|
244
|
-
})
|
|
245
|
-
})
|
|
242
|
+
assert.equal(response.headers['access-control-allow-origin'], undefined)
|
|
243
|
+
})
|
|
244
|
+
})
|
|
246
245
|
|
|
247
246
|
describe('CORS Preflight Requests', () => {
|
|
248
|
-
|
|
247
|
+
it('should handle OPTIONS preflight requests', async () => {
|
|
249
248
|
const response = await request(app)
|
|
250
249
|
.options('/api/countries')
|
|
251
250
|
.set('Origin', 'https://example.com')
|
|
252
251
|
.set('Access-Control-Request-Method', 'POST')
|
|
253
|
-
.set('Access-Control-Request-Headers', 'Content-Type')
|
|
254
|
-
|
|
255
|
-
assert.equal(response.status, 204)
|
|
252
|
+
.set('Access-Control-Request-Headers', 'Content-Type')
|
|
253
|
+
|
|
254
|
+
assert.equal(response.status, 204)
|
|
256
255
|
// When credentials are true, the origin must be specific, not wildcard
|
|
257
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
258
|
-
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
259
|
-
assert.equal(response.headers['access-control-allow-methods'], 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
|
260
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
261
|
-
assert.equal(response.headers['access-control-allow-headers'], 'Content-Type, Authorization, X-Custom-Header')
|
|
262
|
-
assert.equal(response.headers['access-control-max-age'], '86400')
|
|
263
|
-
assert.equal(response.text, '')
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
|
|
256
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
257
|
+
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
258
|
+
assert.equal(response.headers['access-control-allow-methods'], 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
|
259
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
260
|
+
assert.equal(response.headers['access-control-allow-headers'], 'Content-Type, Authorization, X-Custom-Header')
|
|
261
|
+
assert.equal(response.headers['access-control-max-age'], '86400')
|
|
262
|
+
assert.equal(response.text, '') // Empty body
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('should handle preflight for custom headers', async () => {
|
|
267
266
|
const response = await request(app)
|
|
268
267
|
.options('/api/countries/1')
|
|
269
268
|
.set('Origin', 'https://example.com')
|
|
270
269
|
.set('Access-Control-Request-Method', 'PATCH')
|
|
271
|
-
.set('Access-Control-Request-Headers', 'X-Custom-Header')
|
|
272
|
-
|
|
273
|
-
assert.equal(response.status, 204)
|
|
274
|
-
assert(response.headers['access-control-allow-headers'].includes('X-Custom-Header'))
|
|
275
|
-
})
|
|
276
|
-
})
|
|
270
|
+
.set('Access-Control-Request-Headers', 'X-Custom-Header')
|
|
271
|
+
|
|
272
|
+
assert.equal(response.status, 204)
|
|
273
|
+
assert(response.headers['access-control-allow-headers'].includes('X-Custom-Header'))
|
|
274
|
+
})
|
|
275
|
+
})
|
|
277
276
|
|
|
278
277
|
describe('CORS with Specific Origins', () => {
|
|
279
|
-
|
|
278
|
+
it('should work with wildcard origin', async () => {
|
|
280
279
|
// Current configuration uses wildcard
|
|
281
280
|
const response = await request(app)
|
|
282
281
|
.get('/api/countries')
|
|
283
282
|
.set('Origin', 'https://any-origin.com')
|
|
284
|
-
.set('Accept', 'application/vnd.api+json')
|
|
285
|
-
|
|
286
|
-
assert.equal(response.status, 200)
|
|
283
|
+
.set('Accept', 'application/vnd.api+json')
|
|
284
|
+
|
|
285
|
+
assert.equal(response.status, 200)
|
|
287
286
|
// When credentials are true and origin is sent, specific origin is returned
|
|
288
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://any-origin.com')
|
|
289
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
290
|
-
})
|
|
291
|
-
})
|
|
287
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://any-origin.com')
|
|
288
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
289
|
+
})
|
|
290
|
+
})
|
|
292
291
|
|
|
293
292
|
describe('CORS Runtime Configuration', () => {
|
|
294
293
|
// Tests for dynamic configuration have been removed
|
|
295
294
|
// The dynamic configuration API is still available for other tests to use
|
|
296
|
-
})
|
|
295
|
+
})
|
|
297
296
|
|
|
298
297
|
describe('CORS with Different HTTP Methods', () => {
|
|
299
|
-
let testCountryId
|
|
300
|
-
|
|
298
|
+
let testCountryId
|
|
299
|
+
|
|
301
300
|
beforeEach(async () => {
|
|
302
301
|
// Create test data for update/delete operations
|
|
303
302
|
const doc = createJsonApiDocument('countries', {
|
|
304
303
|
name: 'Update Test Country',
|
|
305
304
|
code: 'UT'
|
|
306
|
-
})
|
|
307
|
-
|
|
305
|
+
})
|
|
306
|
+
|
|
308
307
|
const result = await api.resources.countries.post({
|
|
309
308
|
inputRecord: doc,
|
|
310
309
|
simplified: false
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
testCountryId = result.data.id
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
testCountryId = result.data.id
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('should handle PATCH requests with CORS', async () => {
|
|
317
316
|
const patchDoc = {
|
|
318
317
|
data: {
|
|
319
318
|
type: 'countries',
|
|
@@ -322,34 +321,34 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
|
|
|
322
321
|
name: 'Updated Country Name'
|
|
323
322
|
}
|
|
324
323
|
}
|
|
325
|
-
}
|
|
326
|
-
|
|
324
|
+
}
|
|
325
|
+
|
|
327
326
|
const response = await request(app)
|
|
328
327
|
.patch(`/api/countries/${testCountryId}`)
|
|
329
328
|
.send(patchDoc)
|
|
330
329
|
.set('Origin', 'https://example.com')
|
|
331
330
|
.set('Content-Type', 'application/vnd.api+json')
|
|
332
|
-
.set('Accept', 'application/vnd.api+json')
|
|
333
|
-
|
|
334
|
-
assert.equal(response.status, 200)
|
|
331
|
+
.set('Accept', 'application/vnd.api+json')
|
|
332
|
+
|
|
333
|
+
assert.equal(response.status, 200)
|
|
335
334
|
// When credentials are true and origin is sent, specific origin is returned
|
|
336
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
337
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
|
|
335
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
336
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should handle DELETE requests with CORS', async () => {
|
|
341
340
|
const response = await request(app)
|
|
342
341
|
.delete(`/api/countries/${testCountryId}`)
|
|
343
342
|
.set('Origin', 'https://example.com')
|
|
344
|
-
.set('Accept', 'application/vnd.api+json')
|
|
345
|
-
|
|
346
|
-
assert.equal(response.status, 204)
|
|
343
|
+
.set('Accept', 'application/vnd.api+json')
|
|
344
|
+
|
|
345
|
+
assert.equal(response.status, 204)
|
|
347
346
|
// When credentials are true and origin is sent, specific origin is returned
|
|
348
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
349
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
350
|
-
})
|
|
347
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
348
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
349
|
+
})
|
|
351
350
|
|
|
352
|
-
|
|
351
|
+
it('should handle PUT requests with CORS', async () => {
|
|
353
352
|
const putDoc = {
|
|
354
353
|
data: {
|
|
355
354
|
type: 'countries',
|
|
@@ -359,364 +358,364 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
|
|
|
359
358
|
code: 'RC'
|
|
360
359
|
}
|
|
361
360
|
}
|
|
362
|
-
}
|
|
361
|
+
}
|
|
363
362
|
|
|
364
363
|
const response = await request(app)
|
|
365
364
|
.put(`/api/countries/${testCountryId}`)
|
|
366
365
|
.set('Origin', 'https://example.com')
|
|
367
366
|
.set('Accept', 'application/vnd.api+json')
|
|
368
367
|
.set('Content-Type', 'application/vnd.api+json')
|
|
369
|
-
.send(putDoc)
|
|
370
|
-
|
|
371
|
-
assert.equal(response.status, 204)
|
|
372
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
373
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
374
|
-
assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
|
|
375
|
-
})
|
|
376
|
-
})
|
|
368
|
+
.send(putDoc)
|
|
369
|
+
|
|
370
|
+
assert.equal(response.status, 204)
|
|
371
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
372
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
373
|
+
assert.equal(response.headers['access-control-expose-headers'], 'X-Total-Count, X-Page-Count, Link')
|
|
374
|
+
})
|
|
375
|
+
})
|
|
377
376
|
|
|
378
377
|
describe('CORS with Array and Regex Origins', () => {
|
|
379
378
|
it('should support array of allowed origins', async () => {
|
|
380
379
|
// Create a new API instance with array origin configuration
|
|
381
|
-
const arrayOriginApp = express()
|
|
382
|
-
const arrayOriginApi = await createBasicApi(knex, {
|
|
380
|
+
const arrayOriginApp = express()
|
|
381
|
+
const arrayOriginApi = await createBasicApi(knex, {
|
|
383
382
|
apiName: 'cors-array-origins-test',
|
|
384
383
|
tablePrefix: 'cors_array',
|
|
385
384
|
includeExpress: true,
|
|
386
385
|
express: { app: arrayOriginApp }
|
|
387
|
-
})
|
|
388
|
-
|
|
386
|
+
})
|
|
387
|
+
|
|
389
388
|
await arrayOriginApi.use(CorsPlugin, {
|
|
390
389
|
origin: ['https://app1.com', 'https://app2.com', 'https://app3.com'],
|
|
391
390
|
credentials: true
|
|
392
|
-
})
|
|
393
|
-
|
|
391
|
+
})
|
|
392
|
+
|
|
394
393
|
// Mount routes after CORS plugin is installed
|
|
395
|
-
arrayOriginApi.http.express.mount(arrayOriginApp)
|
|
396
|
-
|
|
394
|
+
arrayOriginApi.http.express.mount(arrayOriginApp)
|
|
395
|
+
|
|
397
396
|
// Test allowed origin
|
|
398
397
|
const response1 = await request(arrayOriginApp)
|
|
399
398
|
.get('/api/countries')
|
|
400
399
|
.set('Origin', 'https://app2.com')
|
|
401
|
-
.set('Accept', 'application/vnd.api+json')
|
|
402
|
-
|
|
403
|
-
assert.equal(response1.status, 200)
|
|
404
|
-
assert.equal(response1.headers['access-control-allow-origin'], 'https://app2.com')
|
|
405
|
-
|
|
400
|
+
.set('Accept', 'application/vnd.api+json')
|
|
401
|
+
|
|
402
|
+
assert.equal(response1.status, 200)
|
|
403
|
+
assert.equal(response1.headers['access-control-allow-origin'], 'https://app2.com')
|
|
404
|
+
|
|
406
405
|
// Test disallowed origin
|
|
407
406
|
const response2 = await request(arrayOriginApp)
|
|
408
407
|
.get('/api/countries')
|
|
409
408
|
.set('Origin', 'https://app4.com')
|
|
410
|
-
.set('Accept', 'application/vnd.api+json')
|
|
411
|
-
|
|
412
|
-
assert.equal(response2.status, 200)
|
|
413
|
-
assert.equal(response2.headers['access-control-allow-origin'], undefined)
|
|
414
|
-
})
|
|
415
|
-
|
|
409
|
+
.set('Accept', 'application/vnd.api+json')
|
|
410
|
+
|
|
411
|
+
assert.equal(response2.status, 200)
|
|
412
|
+
assert.equal(response2.headers['access-control-allow-origin'], undefined)
|
|
413
|
+
})
|
|
414
|
+
|
|
416
415
|
it('should support regex origin matching', async () => {
|
|
417
416
|
// Create a new API instance with regex origin configuration
|
|
418
|
-
const regexOriginApp = express()
|
|
419
|
-
const regexOriginApi = await createBasicApi(knex, {
|
|
417
|
+
const regexOriginApp = express()
|
|
418
|
+
const regexOriginApi = await createBasicApi(knex, {
|
|
420
419
|
apiName: 'cors-regex-origins-test',
|
|
421
420
|
tablePrefix: 'cors_regex',
|
|
422
421
|
includeExpress: true,
|
|
423
422
|
express: { app: regexOriginApp }
|
|
424
|
-
})
|
|
425
|
-
|
|
423
|
+
})
|
|
424
|
+
|
|
426
425
|
await regexOriginApi.use(CorsPlugin, {
|
|
427
426
|
origin: /^https:\/\/.*\.example\.com$/,
|
|
428
427
|
credentials: true
|
|
429
|
-
})
|
|
430
|
-
|
|
428
|
+
})
|
|
429
|
+
|
|
431
430
|
// Mount routes after CORS plugin is installed
|
|
432
|
-
regexOriginApi.http.express.mount(regexOriginApp)
|
|
433
|
-
|
|
431
|
+
regexOriginApi.http.express.mount(regexOriginApp)
|
|
432
|
+
|
|
434
433
|
// Test matching subdomain
|
|
435
434
|
const response1 = await request(regexOriginApp)
|
|
436
435
|
.get('/api/countries')
|
|
437
436
|
.set('Origin', 'https://app.example.com')
|
|
438
|
-
.set('Accept', 'application/vnd.api+json')
|
|
439
|
-
|
|
440
|
-
assert.equal(response1.status, 200)
|
|
441
|
-
assert.equal(response1.headers['access-control-allow-origin'], 'https://app.example.com')
|
|
442
|
-
|
|
437
|
+
.set('Accept', 'application/vnd.api+json')
|
|
438
|
+
|
|
439
|
+
assert.equal(response1.status, 200)
|
|
440
|
+
assert.equal(response1.headers['access-control-allow-origin'], 'https://app.example.com')
|
|
441
|
+
|
|
443
442
|
// Test another matching subdomain
|
|
444
443
|
const response2 = await request(regexOriginApp)
|
|
445
444
|
.get('/api/countries')
|
|
446
445
|
.set('Origin', 'https://api.example.com')
|
|
447
|
-
.set('Accept', 'application/vnd.api+json')
|
|
448
|
-
|
|
449
|
-
assert.equal(response2.status, 200)
|
|
450
|
-
assert.equal(response2.headers['access-control-allow-origin'], 'https://api.example.com')
|
|
451
|
-
|
|
446
|
+
.set('Accept', 'application/vnd.api+json')
|
|
447
|
+
|
|
448
|
+
assert.equal(response2.status, 200)
|
|
449
|
+
assert.equal(response2.headers['access-control-allow-origin'], 'https://api.example.com')
|
|
450
|
+
|
|
452
451
|
// Test non-matching domain
|
|
453
452
|
const response3 = await request(regexOriginApp)
|
|
454
453
|
.get('/api/countries')
|
|
455
454
|
.set('Origin', 'https://example.org')
|
|
456
|
-
.set('Accept', 'application/vnd.api+json')
|
|
457
|
-
|
|
458
|
-
assert.equal(response3.status, 200)
|
|
459
|
-
assert.equal(response3.headers['access-control-allow-origin'], undefined)
|
|
460
|
-
})
|
|
461
|
-
|
|
455
|
+
.set('Accept', 'application/vnd.api+json')
|
|
456
|
+
|
|
457
|
+
assert.equal(response3.status, 200)
|
|
458
|
+
assert.equal(response3.headers['access-control-allow-origin'], undefined)
|
|
459
|
+
})
|
|
460
|
+
|
|
462
461
|
it('should support function-based origin validation', async () => {
|
|
463
462
|
// Create a new API instance with function origin configuration
|
|
464
|
-
const functionOriginApp = express()
|
|
465
|
-
const functionOriginApi = await createBasicApi(knex, {
|
|
463
|
+
const functionOriginApp = express()
|
|
464
|
+
const functionOriginApi = await createBasicApi(knex, {
|
|
466
465
|
apiName: 'cors-function-origins-test',
|
|
467
466
|
tablePrefix: 'cors_func',
|
|
468
467
|
includeExpress: true,
|
|
469
468
|
express: { app: functionOriginApp }
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
const allowedOrigins = new Set(['https://dynamic1.com', 'https://dynamic2.com'])
|
|
473
|
-
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
const allowedOrigins = new Set(['https://dynamic1.com', 'https://dynamic2.com'])
|
|
472
|
+
|
|
474
473
|
await functionOriginApi.use(CorsPlugin, {
|
|
475
474
|
origin: (origin) => allowedOrigins.has(origin),
|
|
476
475
|
credentials: true
|
|
477
|
-
})
|
|
478
|
-
|
|
476
|
+
})
|
|
477
|
+
|
|
479
478
|
// Mount routes after CORS plugin is installed
|
|
480
|
-
functionOriginApi.http.express.mount(functionOriginApp)
|
|
481
|
-
|
|
479
|
+
functionOriginApi.http.express.mount(functionOriginApp)
|
|
480
|
+
|
|
482
481
|
// Test allowed origin
|
|
483
482
|
const response1 = await request(functionOriginApp)
|
|
484
483
|
.get('/api/countries')
|
|
485
484
|
.set('Origin', 'https://dynamic1.com')
|
|
486
|
-
.set('Accept', 'application/vnd.api+json')
|
|
487
|
-
|
|
488
|
-
assert.equal(response1.status, 200)
|
|
489
|
-
assert.equal(response1.headers['access-control-allow-origin'], 'https://dynamic1.com')
|
|
490
|
-
|
|
485
|
+
.set('Accept', 'application/vnd.api+json')
|
|
486
|
+
|
|
487
|
+
assert.equal(response1.status, 200)
|
|
488
|
+
assert.equal(response1.headers['access-control-allow-origin'], 'https://dynamic1.com')
|
|
489
|
+
|
|
491
490
|
// Test disallowed origin
|
|
492
491
|
const response2 = await request(functionOriginApp)
|
|
493
492
|
.get('/api/countries')
|
|
494
493
|
.set('Origin', 'https://dynamic3.com')
|
|
495
|
-
.set('Accept', 'application/vnd.api+json')
|
|
496
|
-
|
|
497
|
-
assert.equal(response2.status, 200)
|
|
498
|
-
assert.equal(response2.headers['access-control-allow-origin'], undefined)
|
|
499
|
-
})
|
|
500
|
-
})
|
|
494
|
+
.set('Accept', 'application/vnd.api+json')
|
|
495
|
+
|
|
496
|
+
assert.equal(response2.status, 200)
|
|
497
|
+
assert.equal(response2.headers['access-control-allow-origin'], undefined)
|
|
498
|
+
})
|
|
499
|
+
})
|
|
501
500
|
|
|
502
501
|
describe('CORS Error Handling', () => {
|
|
503
|
-
|
|
502
|
+
it('should reject preflight from disallowed origin', async () => {
|
|
504
503
|
// Create a new API instance with specific allowed origin
|
|
505
|
-
const restrictedApp = express()
|
|
506
|
-
const restrictedApi = await createBasicApi(knex, {
|
|
504
|
+
const restrictedApp = express()
|
|
505
|
+
const restrictedApi = await createBasicApi(knex, {
|
|
507
506
|
apiName: 'cors-restricted-test',
|
|
508
507
|
tablePrefix: 'cors_restrict',
|
|
509
508
|
includeExpress: true,
|
|
510
509
|
express: { app: restrictedApp }
|
|
511
|
-
})
|
|
512
|
-
|
|
510
|
+
})
|
|
511
|
+
|
|
513
512
|
await restrictedApi.use(CorsPlugin, {
|
|
514
513
|
origin: 'https://allowed-only.com',
|
|
515
514
|
credentials: true
|
|
516
|
-
})
|
|
517
|
-
|
|
515
|
+
})
|
|
516
|
+
|
|
518
517
|
// Mount routes after CORS plugin is installed
|
|
519
|
-
restrictedApi.http.express.mount(restrictedApp)
|
|
520
|
-
|
|
518
|
+
restrictedApi.http.express.mount(restrictedApp)
|
|
519
|
+
|
|
521
520
|
const response = await request(restrictedApp)
|
|
522
521
|
.options('/api/countries')
|
|
523
522
|
.set('Origin', 'https://not-allowed.com')
|
|
524
|
-
.set('Access-Control-Request-Method', 'POST')
|
|
525
|
-
|
|
526
|
-
assert.equal(response.status, 403)
|
|
527
|
-
assert(response.body.error.includes('CORS origin not allowed'))
|
|
528
|
-
assert.equal(response.headers['access-control-allow-origin'], undefined)
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
|
|
523
|
+
.set('Access-Control-Request-Method', 'POST')
|
|
524
|
+
|
|
525
|
+
assert.equal(response.status, 403)
|
|
526
|
+
assert(response.body.error.includes('CORS origin not allowed'))
|
|
527
|
+
assert.equal(response.headers['access-control-allow-origin'], undefined)
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('should handle lowercase HTTP methods', async () => {
|
|
532
531
|
// Some clients might send lowercase methods
|
|
533
532
|
const response = await request(app)
|
|
534
533
|
.options('/api/countries')
|
|
535
534
|
.set('Origin', 'https://example.com')
|
|
536
|
-
.set('Access-Control-Request-Method', 'post')
|
|
537
|
-
|
|
538
|
-
assert.equal(response.status, 204)
|
|
535
|
+
.set('Access-Control-Request-Method', 'post') // lowercase
|
|
536
|
+
|
|
537
|
+
assert.equal(response.status, 204)
|
|
539
538
|
// When credentials are true and origin is sent, specific origin is returned
|
|
540
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
541
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
542
|
-
})
|
|
543
|
-
})
|
|
539
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
540
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
541
|
+
})
|
|
542
|
+
})
|
|
544
543
|
|
|
545
544
|
describe('CORS Integration with REST API', () => {
|
|
546
|
-
|
|
545
|
+
it('should handle CORS for error responses', async () => {
|
|
547
546
|
// Try to get non-existent resource
|
|
548
547
|
const response = await request(app)
|
|
549
548
|
.get('/api/countries/999999')
|
|
550
549
|
.set('Origin', 'https://example.com')
|
|
551
|
-
.set('Accept', 'application/vnd.api+json')
|
|
552
|
-
|
|
553
|
-
assert.equal(response.status, 404)
|
|
550
|
+
.set('Accept', 'application/vnd.api+json')
|
|
551
|
+
|
|
552
|
+
assert.equal(response.status, 404)
|
|
554
553
|
// CORS headers should still be set for error responses
|
|
555
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
556
|
-
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
557
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
|
|
554
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
555
|
+
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
556
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it('should handle CORS for validation errors', async () => {
|
|
561
560
|
// Send invalid data
|
|
562
561
|
const invalidDoc = createJsonApiDocument('countries', {
|
|
563
562
|
// Missing required 'name' field
|
|
564
563
|
code: 'XX'
|
|
565
|
-
})
|
|
566
|
-
|
|
564
|
+
})
|
|
565
|
+
|
|
567
566
|
const response = await request(app)
|
|
568
567
|
.post('/api/countries')
|
|
569
568
|
.send(invalidDoc)
|
|
570
569
|
.set('Origin', 'https://example.com')
|
|
571
570
|
.set('Content-Type', 'application/vnd.api+json')
|
|
572
|
-
.set('Accept', 'application/vnd.api+json')
|
|
573
|
-
|
|
574
|
-
assert.equal(response.status, 422)
|
|
571
|
+
.set('Accept', 'application/vnd.api+json')
|
|
572
|
+
|
|
573
|
+
assert.equal(response.status, 422)
|
|
575
574
|
// CORS headers should be set for validation errors
|
|
576
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
577
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
578
|
-
})
|
|
579
|
-
})
|
|
575
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
576
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
577
|
+
})
|
|
578
|
+
})
|
|
580
579
|
|
|
581
580
|
describe('CORS Configuration Options', () => {
|
|
582
581
|
it('should respect custom max-age configuration', async () => {
|
|
583
582
|
// Create API with custom max-age
|
|
584
|
-
const maxAgeApp = express()
|
|
583
|
+
const maxAgeApp = express()
|
|
585
584
|
const maxAgeApi = await createBasicApi(knex, {
|
|
586
585
|
apiName: 'cors-maxage-test',
|
|
587
586
|
tablePrefix: 'cors_maxage',
|
|
588
587
|
includeExpress: true,
|
|
589
588
|
express: { app: maxAgeApp }
|
|
590
|
-
})
|
|
591
|
-
|
|
589
|
+
})
|
|
590
|
+
|
|
592
591
|
await maxAgeApi.use(CorsPlugin, {
|
|
593
592
|
origin: '*',
|
|
594
593
|
credentials: false,
|
|
595
594
|
maxAge: 3600 // 1 hour instead of default 24 hours
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
maxAgeApi.http.express.mount(maxAgeApp)
|
|
599
|
-
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
maxAgeApi.http.express.mount(maxAgeApp)
|
|
598
|
+
|
|
600
599
|
const response = await request(maxAgeApp)
|
|
601
600
|
.options('/api/countries')
|
|
602
601
|
.set('Origin', 'https://example.com')
|
|
603
|
-
.set('Access-Control-Request-Method', 'POST')
|
|
604
|
-
|
|
605
|
-
assert.equal(response.status, 204)
|
|
606
|
-
assert.equal(response.headers['access-control-max-age'], '3600')
|
|
607
|
-
})
|
|
608
|
-
|
|
602
|
+
.set('Access-Control-Request-Method', 'POST')
|
|
603
|
+
|
|
604
|
+
assert.equal(response.status, 204)
|
|
605
|
+
assert.equal(response.headers['access-control-max-age'], '3600')
|
|
606
|
+
})
|
|
607
|
+
|
|
609
608
|
it('should respect custom exposed headers configuration', async () => {
|
|
610
609
|
// Create API with custom exposed headers
|
|
611
|
-
const exposedApp = express()
|
|
610
|
+
const exposedApp = express()
|
|
612
611
|
const exposedApi = await createBasicApi(knex, {
|
|
613
612
|
apiName: 'cors-exposed-test',
|
|
614
613
|
tablePrefix: 'cors_exposed',
|
|
615
614
|
includeExpress: true,
|
|
616
615
|
express: { app: exposedApp }
|
|
617
|
-
})
|
|
618
|
-
|
|
616
|
+
})
|
|
617
|
+
|
|
619
618
|
await exposedApi.use(CorsPlugin, {
|
|
620
619
|
origin: '*',
|
|
621
620
|
credentials: false,
|
|
622
621
|
exposedHeaders: ['X-Custom-Header', 'X-Rate-Limit', 'X-Request-Id']
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
exposedApi.http.express.mount(exposedApp)
|
|
626
|
-
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
exposedApi.http.express.mount(exposedApp)
|
|
625
|
+
|
|
627
626
|
const response = await request(exposedApp)
|
|
628
627
|
.get('/api/countries')
|
|
629
628
|
.set('Origin', 'https://example.com')
|
|
630
|
-
.set('Accept', 'application/vnd.api+json')
|
|
631
|
-
|
|
632
|
-
assert.equal(response.status, 200)
|
|
633
|
-
assert.equal(response.headers['access-control-expose-headers'], 'X-Custom-Header, X-Rate-Limit, X-Request-Id')
|
|
634
|
-
})
|
|
635
|
-
|
|
629
|
+
.set('Accept', 'application/vnd.api+json')
|
|
630
|
+
|
|
631
|
+
assert.equal(response.status, 200)
|
|
632
|
+
assert.equal(response.headers['access-control-expose-headers'], 'X-Custom-Header, X-Rate-Limit, X-Request-Id')
|
|
633
|
+
})
|
|
634
|
+
|
|
636
635
|
it('should respect custom allowed methods configuration', async () => {
|
|
637
636
|
// Create API with limited methods
|
|
638
|
-
const methodsApp = express()
|
|
637
|
+
const methodsApp = express()
|
|
639
638
|
const methodsApi = await createBasicApi(knex, {
|
|
640
639
|
apiName: 'cors-methods-test',
|
|
641
640
|
tablePrefix: 'cors_methods',
|
|
642
641
|
includeExpress: true,
|
|
643
642
|
express: { app: methodsApp }
|
|
644
|
-
})
|
|
645
|
-
|
|
643
|
+
})
|
|
644
|
+
|
|
646
645
|
await methodsApi.use(CorsPlugin, {
|
|
647
646
|
origin: '*',
|
|
648
647
|
credentials: false,
|
|
649
648
|
methods: ['GET', 'POST'] // Only allow GET and POST
|
|
650
|
-
})
|
|
651
|
-
|
|
652
|
-
methodsApi.http.express.mount(methodsApp)
|
|
653
|
-
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
methodsApi.http.express.mount(methodsApp)
|
|
652
|
+
|
|
654
653
|
const response = await request(methodsApp)
|
|
655
654
|
.options('/api/countries')
|
|
656
655
|
.set('Origin', 'https://example.com')
|
|
657
|
-
.set('Access-Control-Request-Method', 'DELETE')
|
|
658
|
-
|
|
659
|
-
assert.equal(response.status, 204)
|
|
660
|
-
assert.equal(response.headers['access-control-allow-methods'], 'GET, POST')
|
|
661
|
-
})
|
|
662
|
-
|
|
656
|
+
.set('Access-Control-Request-Method', 'DELETE')
|
|
657
|
+
|
|
658
|
+
assert.equal(response.status, 204)
|
|
659
|
+
assert.equal(response.headers['access-control-allow-methods'], 'GET, POST')
|
|
660
|
+
})
|
|
661
|
+
|
|
663
662
|
it('should handle credentials false configuration', async () => {
|
|
664
663
|
// Create API with credentials disabled
|
|
665
|
-
const noCredApp = express()
|
|
664
|
+
const noCredApp = express()
|
|
666
665
|
const noCredApi = await createBasicApi(knex, {
|
|
667
666
|
apiName: 'cors-nocred-test',
|
|
668
667
|
tablePrefix: 'cors_nocred',
|
|
669
668
|
includeExpress: true,
|
|
670
669
|
express: { app: noCredApp }
|
|
671
|
-
})
|
|
672
|
-
|
|
670
|
+
})
|
|
671
|
+
|
|
673
672
|
await noCredApi.use(CorsPlugin, {
|
|
674
673
|
origin: '*',
|
|
675
674
|
credentials: false
|
|
676
|
-
})
|
|
677
|
-
|
|
678
|
-
noCredApi.http.express.mount(noCredApp)
|
|
679
|
-
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
noCredApi.http.express.mount(noCredApp)
|
|
678
|
+
|
|
680
679
|
const response = await request(noCredApp)
|
|
681
680
|
.get('/api/countries')
|
|
682
681
|
.set('Origin', 'https://example.com')
|
|
683
|
-
.set('Accept', 'application/vnd.api+json')
|
|
684
|
-
|
|
685
|
-
assert.equal(response.status, 200)
|
|
686
|
-
assert.equal(response.headers['access-control-allow-origin'], '*')
|
|
687
|
-
assert.equal(response.headers['access-control-allow-credentials'], undefined)
|
|
682
|
+
.set('Accept', 'application/vnd.api+json')
|
|
683
|
+
|
|
684
|
+
assert.equal(response.status, 200)
|
|
685
|
+
assert.equal(response.headers['access-control-allow-origin'], '*')
|
|
686
|
+
assert.equal(response.headers['access-control-allow-credentials'], undefined)
|
|
688
687
|
// No Vary header when using wildcard without credentials
|
|
689
|
-
assert.equal(response.headers['vary'], undefined)
|
|
690
|
-
})
|
|
691
|
-
|
|
688
|
+
assert.equal(response.headers['vary'], undefined)
|
|
689
|
+
})
|
|
690
|
+
|
|
692
691
|
it('should handle custom allowed headers configuration', async () => {
|
|
693
692
|
// Create API with custom allowed headers
|
|
694
|
-
const headersApp = express()
|
|
693
|
+
const headersApp = express()
|
|
695
694
|
const headersApi = await createBasicApi(knex, {
|
|
696
695
|
apiName: 'cors-headers-test',
|
|
697
696
|
tablePrefix: 'cors_headers',
|
|
698
697
|
includeExpress: true,
|
|
699
698
|
express: { app: headersApp }
|
|
700
|
-
})
|
|
701
|
-
|
|
699
|
+
})
|
|
700
|
+
|
|
702
701
|
await headersApi.use(CorsPlugin, {
|
|
703
702
|
origin: 'https://app.example.com',
|
|
704
703
|
credentials: true,
|
|
705
704
|
allowedHeaders: ['Content-Type', 'X-API-Key', 'X-Session-Token']
|
|
706
|
-
})
|
|
707
|
-
|
|
708
|
-
headersApi.http.express.mount(headersApp)
|
|
709
|
-
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
headersApi.http.express.mount(headersApp)
|
|
708
|
+
|
|
710
709
|
const response = await request(headersApp)
|
|
711
710
|
.options('/api/countries')
|
|
712
711
|
.set('Origin', 'https://app.example.com')
|
|
713
712
|
.set('Access-Control-Request-Method', 'POST')
|
|
714
|
-
.set('Access-Control-Request-Headers', 'X-API-Key, X-Session-Token')
|
|
715
|
-
|
|
716
|
-
assert.equal(response.status, 204)
|
|
717
|
-
assert.equal(response.headers['access-control-allow-headers'], 'Content-Type, X-API-Key, X-Session-Token')
|
|
718
|
-
})
|
|
719
|
-
})
|
|
713
|
+
.set('Access-Control-Request-Headers', 'X-API-Key, X-Session-Token')
|
|
714
|
+
|
|
715
|
+
assert.equal(response.status, 204)
|
|
716
|
+
assert.equal(response.headers['access-control-allow-headers'], 'Content-Type, X-API-Key, X-Session-Token')
|
|
717
|
+
})
|
|
718
|
+
})
|
|
720
719
|
|
|
721
720
|
describe('CORS with PUT Requests', () => {
|
|
722
721
|
it('should handle PUT requests with proper CORS headers', async () => {
|
|
@@ -725,15 +724,15 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
|
|
|
725
724
|
const doc = createJsonApiDocument('countries', {
|
|
726
725
|
name: 'Original Country',
|
|
727
726
|
code: 'OC'
|
|
728
|
-
})
|
|
729
|
-
|
|
727
|
+
})
|
|
728
|
+
|
|
730
729
|
const createResult = await api.resources.countries.post({
|
|
731
730
|
inputRecord: doc,
|
|
732
731
|
simplified: false
|
|
733
|
-
})
|
|
734
|
-
|
|
735
|
-
const countryId = createResult.data.id
|
|
736
|
-
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
const countryId = createResult.data.id
|
|
735
|
+
|
|
737
736
|
// Now test PUT with CORS
|
|
738
737
|
const putDoc = {
|
|
739
738
|
data: {
|
|
@@ -744,92 +743,92 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
|
|
|
744
743
|
code: 'RC'
|
|
745
744
|
}
|
|
746
745
|
}
|
|
747
|
-
}
|
|
748
|
-
|
|
746
|
+
}
|
|
747
|
+
|
|
749
748
|
const response = await request(app)
|
|
750
749
|
.put(`/api/countries/${countryId}`)
|
|
751
750
|
.send(putDoc)
|
|
752
751
|
.set('Origin', 'https://example.com')
|
|
753
752
|
.set('Content-Type', 'application/vnd.api+json')
|
|
754
|
-
.set('Accept', 'application/vnd.api+json')
|
|
755
|
-
|
|
756
|
-
assert.equal(response.status, 204)
|
|
757
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
758
|
-
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
759
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
760
|
-
|
|
753
|
+
.set('Accept', 'application/vnd.api+json')
|
|
754
|
+
|
|
755
|
+
assert.equal(response.status, 204)
|
|
756
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
757
|
+
assert.equal(response.headers['access-control-allow-credentials'], 'true')
|
|
758
|
+
assert.equal(response.headers['vary'], 'Origin')
|
|
759
|
+
|
|
761
760
|
// Note: PUT might not return the full record by default depending on returnFullRecord settings
|
|
762
761
|
// The important part for CORS testing is that the headers are correct, which we've verified above
|
|
763
|
-
})
|
|
764
|
-
|
|
762
|
+
})
|
|
763
|
+
|
|
765
764
|
it('should handle PUT preflight requests', async () => {
|
|
766
765
|
const response = await request(app)
|
|
767
766
|
.options('/api/countries/123')
|
|
768
767
|
.set('Origin', 'https://example.com')
|
|
769
768
|
.set('Access-Control-Request-Method', 'PUT')
|
|
770
|
-
.set('Access-Control-Request-Headers', 'Content-Type')
|
|
771
|
-
|
|
772
|
-
assert.equal(response.status, 204)
|
|
773
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
774
|
-
assert.equal(response.headers['access-control-allow-methods'], 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
|
775
|
-
assert(response.headers['access-control-allow-methods'].includes('PUT'))
|
|
776
|
-
})
|
|
777
|
-
})
|
|
769
|
+
.set('Access-Control-Request-Headers', 'Content-Type')
|
|
770
|
+
|
|
771
|
+
assert.equal(response.status, 204)
|
|
772
|
+
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
773
|
+
assert.equal(response.headers['access-control-allow-methods'], 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
|
774
|
+
assert(response.headers['access-control-allow-methods'].includes('PUT'))
|
|
775
|
+
})
|
|
776
|
+
})
|
|
778
777
|
|
|
779
778
|
describe('CORS Edge Cases', () => {
|
|
780
779
|
it('should handle requests with no exposed headers configured', async () => {
|
|
781
780
|
// Create API with empty exposed headers
|
|
782
|
-
const noExposedApp = express()
|
|
781
|
+
const noExposedApp = express()
|
|
783
782
|
const noExposedApi = await createBasicApi(knex, {
|
|
784
783
|
apiName: 'cors-noexposed-test',
|
|
785
784
|
tablePrefix: 'cors_noexposed',
|
|
786
785
|
includeExpress: true,
|
|
787
786
|
express: { app: noExposedApp }
|
|
788
|
-
})
|
|
789
|
-
|
|
787
|
+
})
|
|
788
|
+
|
|
790
789
|
await noExposedApi.use(CorsPlugin, {
|
|
791
790
|
origin: '*',
|
|
792
791
|
credentials: false,
|
|
793
792
|
exposedHeaders: [] // Empty array
|
|
794
|
-
})
|
|
795
|
-
|
|
796
|
-
noExposedApi.http.express.mount(noExposedApp)
|
|
797
|
-
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
noExposedApi.http.express.mount(noExposedApp)
|
|
796
|
+
|
|
798
797
|
const response = await request(noExposedApp)
|
|
799
798
|
.get('/api/countries')
|
|
800
799
|
.set('Origin', 'https://example.com')
|
|
801
|
-
.set('Accept', 'application/vnd.api+json')
|
|
802
|
-
|
|
803
|
-
assert.equal(response.status, 200)
|
|
800
|
+
.set('Accept', 'application/vnd.api+json')
|
|
801
|
+
|
|
802
|
+
assert.equal(response.status, 200)
|
|
804
803
|
// Should not set exposed headers when array is empty
|
|
805
|
-
assert.equal(response.headers['access-control-expose-headers'], undefined)
|
|
806
|
-
})
|
|
807
|
-
|
|
804
|
+
assert.equal(response.headers['access-control-expose-headers'], undefined)
|
|
805
|
+
})
|
|
806
|
+
|
|
808
807
|
it('should handle optionsSuccessStatus configuration', async () => {
|
|
809
808
|
// Create API with custom options success status
|
|
810
|
-
const statusApp = express()
|
|
809
|
+
const statusApp = express()
|
|
811
810
|
const statusApi = await createBasicApi(knex, {
|
|
812
811
|
apiName: 'cors-status-test',
|
|
813
812
|
tablePrefix: 'cors_status',
|
|
814
813
|
includeExpress: true,
|
|
815
814
|
express: { app: statusApp }
|
|
816
|
-
})
|
|
817
|
-
|
|
815
|
+
})
|
|
816
|
+
|
|
818
817
|
await statusApi.use(CorsPlugin, {
|
|
819
818
|
origin: '*',
|
|
820
819
|
credentials: false,
|
|
821
820
|
optionsSuccessStatus: 200 // Use 200 instead of 204
|
|
822
|
-
})
|
|
823
|
-
|
|
824
|
-
statusApi.http.express.mount(statusApp)
|
|
825
|
-
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
statusApi.http.express.mount(statusApp)
|
|
824
|
+
|
|
826
825
|
const response = await request(statusApp)
|
|
827
826
|
.options('/api/countries')
|
|
828
827
|
.set('Origin', 'https://example.com')
|
|
829
|
-
.set('Access-Control-Request-Method', 'GET')
|
|
830
|
-
|
|
831
|
-
assert.equal(response.status, 200)
|
|
832
|
-
assert.equal(response.headers['access-control-allow-origin'], '*')
|
|
833
|
-
})
|
|
834
|
-
})
|
|
835
|
-
})
|
|
828
|
+
.set('Access-Control-Request-Method', 'GET')
|
|
829
|
+
|
|
830
|
+
assert.equal(response.status, 200)
|
|
831
|
+
assert.equal(response.headers['access-control-allow-origin'], '*')
|
|
832
|
+
})
|
|
833
|
+
})
|
|
834
|
+
})
|