json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
package/tests/cors.test.js
DELETED
|
@@ -1,834 +0,0 @@
|
|
|
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
|
-
createJsonApiDocument,
|
|
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'
|
|
14
|
-
|
|
15
|
-
// Create Knex instance for tests - always use SQLite in-memory
|
|
16
|
-
const knex = knexLib({
|
|
17
|
-
client: 'better-sqlite3',
|
|
18
|
-
connection: {
|
|
19
|
-
filename: ':memory:'
|
|
20
|
-
},
|
|
21
|
-
useNullAsDefault: true
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// API instance and Express app that persist across tests
|
|
25
|
-
let api
|
|
26
|
-
let app
|
|
27
|
-
|
|
28
|
-
describe('CORS Plugin Tests', { timeout: 30000 }, () => {
|
|
29
|
-
describe('CORS with baseUrl', () => {
|
|
30
|
-
let baseUrlApi
|
|
31
|
-
let baseUrlApp
|
|
32
|
-
|
|
33
|
-
before(async () => {
|
|
34
|
-
// Create a new API instance with Express configured with a baseUrl
|
|
35
|
-
baseUrlApp = express()
|
|
36
|
-
// Create API with different name to avoid registry conflict
|
|
37
|
-
const Api = (await import('hooked-api')).Api
|
|
38
|
-
baseUrlApi = new Api({
|
|
39
|
-
name: 'cors-baseurl-test-api',
|
|
40
|
-
log: { level: 'info' }
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
// Install plugins
|
|
44
|
-
await baseUrlApi.use((await import('../plugins/core/rest-api-plugin.js')).RestApiPlugin, {
|
|
45
|
-
simplifiedApi: false
|
|
46
|
-
})
|
|
47
|
-
await baseUrlApi.use((await import('../plugins/core/rest-api-knex-plugin.js')).RestApiKnexPlugin, { knex })
|
|
48
|
-
await baseUrlApi.use((await import('../plugins/core/connectors/express-plugin.js')).ExpressPlugin, {
|
|
49
|
-
app: baseUrlApp,
|
|
50
|
-
mountPath: '/api'
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
// Add basic scope
|
|
54
|
-
await baseUrlApi.addScope('countries', {
|
|
55
|
-
restApi: {
|
|
56
|
-
schema: {
|
|
57
|
-
attributes: {
|
|
58
|
-
name: { type: 'string', required: true },
|
|
59
|
-
code: { type: 'string', required: true }
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
// Install CORS plugin
|
|
66
|
-
console.log('[TEST] vars.transport before CORS install:', baseUrlApi.vars.transport)
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
await baseUrlApi.use(CorsPlugin, {
|
|
70
|
-
origin: '*',
|
|
71
|
-
credentials: true
|
|
72
|
-
})
|
|
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
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Mount the routes
|
|
82
|
-
baseUrlApi.http.express.mount(baseUrlApp)
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('should handle OPTIONS preflight with baseUrl', async () => {
|
|
86
|
-
const response = await request(baseUrlApp)
|
|
87
|
-
.options('/api/countries')
|
|
88
|
-
.set('Origin', 'https://example.com')
|
|
89
|
-
.set('Access-Control-Request-Method', 'POST')
|
|
90
|
-
.set('Access-Control-Request-Headers', 'Content-Type')
|
|
91
|
-
|
|
92
|
-
console.log('[TEST] BaseUrl OPTIONS response:', {
|
|
93
|
-
status: response.status,
|
|
94
|
-
headers: response.headers,
|
|
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
|
-
|
|
103
|
-
it('should handle GET requests with baseUrl', async () => {
|
|
104
|
-
// Just test the GET endpoint without creating data first
|
|
105
|
-
// This tests CORS headers on the response regardless of data
|
|
106
|
-
const response = await request(baseUrlApp)
|
|
107
|
-
.get('/api/countries')
|
|
108
|
-
.set('Origin', 'https://example.com')
|
|
109
|
-
.set('Accept', 'application/vnd.api+json')
|
|
110
|
-
|
|
111
|
-
console.log('[TEST] BaseUrl GET response:', {
|
|
112
|
-
status: response.status,
|
|
113
|
-
headers: response.headers
|
|
114
|
-
})
|
|
115
|
-
|
|
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
|
-
|
|
123
|
-
it('should handle wildcard OPTIONS route with baseUrl', async () => {
|
|
124
|
-
// Test a non-existent endpoint to check if wildcard OPTIONS still works
|
|
125
|
-
const response = await request(baseUrlApp)
|
|
126
|
-
.options('/api/non-existent-endpoint')
|
|
127
|
-
.set('Origin', 'https://example.com')
|
|
128
|
-
.set('Access-Control-Request-Method', 'POST')
|
|
129
|
-
|
|
130
|
-
console.log('[TEST] BaseUrl wildcard OPTIONS response:', {
|
|
131
|
-
status: response.status,
|
|
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
|
-
})
|
|
140
|
-
|
|
141
|
-
// IMPORTANT: before() runs ONCE for the entire test suite
|
|
142
|
-
before(async () => {
|
|
143
|
-
// Initialize API once with Express
|
|
144
|
-
app = express()
|
|
145
|
-
api = await createBasicApi(knex, {
|
|
146
|
-
includeExpress: true,
|
|
147
|
-
express: { app }
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
// Install CORS plugin
|
|
151
|
-
await api.use(CorsPlugin, {
|
|
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
|
-
|
|
162
|
-
// Mount the routes on the Express app
|
|
163
|
-
api.http.express.mount(app)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
// IMPORTANT: after() cleans up resources
|
|
167
|
-
after(async () => {
|
|
168
|
-
// Always destroy knex connection to allow tests to exit
|
|
169
|
-
await knex.destroy()
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
// IMPORTANT: beforeEach() cleans data but does NOT recreate API
|
|
173
|
-
beforeEach(async () => {
|
|
174
|
-
// Clean all tables - list ALL tables your tests use
|
|
175
|
-
await cleanTables(knex, [
|
|
176
|
-
'basic_countries',
|
|
177
|
-
'basic_publishers',
|
|
178
|
-
'basic_authors',
|
|
179
|
-
'basic_books',
|
|
180
|
-
'basic_book_authors'
|
|
181
|
-
])
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
describe('CORS Headers for Regular Requests', () => {
|
|
185
|
-
it('should add CORS headers for GET requests', async () => {
|
|
186
|
-
// Create test data
|
|
187
|
-
const doc = createJsonApiDocument('countries', {
|
|
188
|
-
name: 'Test Country',
|
|
189
|
-
code: 'TC'
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
const createResult = await api.resources.countries.post({
|
|
193
|
-
inputRecord: doc,
|
|
194
|
-
simplified: false
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
// Make HTTP request with origin header
|
|
198
|
-
const response = await request(app)
|
|
199
|
-
.get('/api/countries')
|
|
200
|
-
.set('Origin', 'https://example.com')
|
|
201
|
-
.set('Accept', 'application/vnd.api+json')
|
|
202
|
-
|
|
203
|
-
assert.equal(response.status, 200)
|
|
204
|
-
// When credentials are true, the origin must be specific, not wildcard
|
|
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')
|
|
208
|
-
// Vary header should be set when specific origin is returned
|
|
209
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('should add CORS headers for POST requests', async () => {
|
|
213
|
-
const doc = createJsonApiDocument('countries', {
|
|
214
|
-
name: 'New Country',
|
|
215
|
-
code: 'NC'
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
const response = await request(app)
|
|
219
|
-
.post('/api/countries')
|
|
220
|
-
.send(doc)
|
|
221
|
-
.set('Origin', 'https://example.com')
|
|
222
|
-
.set('Content-Type', 'application/vnd.api+json')
|
|
223
|
-
.set('Accept', 'application/vnd.api+json')
|
|
224
|
-
|
|
225
|
-
assert.equal(response.status, 201)
|
|
226
|
-
// When credentials are true, the origin must be specific, not wildcard
|
|
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')
|
|
230
|
-
// Vary header should be set when specific origin is returned
|
|
231
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('should handle requests without Origin header', async () => {
|
|
235
|
-
// Same-origin request (no Origin header)
|
|
236
|
-
const response = await request(app)
|
|
237
|
-
.get('/api/countries')
|
|
238
|
-
.set('Accept', 'application/vnd.api+json')
|
|
239
|
-
|
|
240
|
-
assert.equal(response.status, 200)
|
|
241
|
-
// Should not set CORS headers for same-origin requests
|
|
242
|
-
assert.equal(response.headers['access-control-allow-origin'], undefined)
|
|
243
|
-
})
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
describe('CORS Preflight Requests', () => {
|
|
247
|
-
it('should handle OPTIONS preflight requests', async () => {
|
|
248
|
-
const response = await request(app)
|
|
249
|
-
.options('/api/countries')
|
|
250
|
-
.set('Origin', 'https://example.com')
|
|
251
|
-
.set('Access-Control-Request-Method', 'POST')
|
|
252
|
-
.set('Access-Control-Request-Headers', 'Content-Type')
|
|
253
|
-
|
|
254
|
-
assert.equal(response.status, 204)
|
|
255
|
-
// When credentials are true, the origin must be specific, not wildcard
|
|
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 () => {
|
|
266
|
-
const response = await request(app)
|
|
267
|
-
.options('/api/countries/1')
|
|
268
|
-
.set('Origin', 'https://example.com')
|
|
269
|
-
.set('Access-Control-Request-Method', 'PATCH')
|
|
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
|
-
})
|
|
276
|
-
|
|
277
|
-
describe('CORS with Specific Origins', () => {
|
|
278
|
-
it('should work with wildcard origin', async () => {
|
|
279
|
-
// Current configuration uses wildcard
|
|
280
|
-
const response = await request(app)
|
|
281
|
-
.get('/api/countries')
|
|
282
|
-
.set('Origin', 'https://any-origin.com')
|
|
283
|
-
.set('Accept', 'application/vnd.api+json')
|
|
284
|
-
|
|
285
|
-
assert.equal(response.status, 200)
|
|
286
|
-
// When credentials are true and origin is sent, specific origin is returned
|
|
287
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://any-origin.com')
|
|
288
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
289
|
-
})
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
describe('CORS Runtime Configuration', () => {
|
|
293
|
-
// Tests for dynamic configuration have been removed
|
|
294
|
-
// The dynamic configuration API is still available for other tests to use
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
describe('CORS with Different HTTP Methods', () => {
|
|
298
|
-
let testCountryId
|
|
299
|
-
|
|
300
|
-
beforeEach(async () => {
|
|
301
|
-
// Create test data for update/delete operations
|
|
302
|
-
const doc = createJsonApiDocument('countries', {
|
|
303
|
-
name: 'Update Test Country',
|
|
304
|
-
code: 'UT'
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
const result = await api.resources.countries.post({
|
|
308
|
-
inputRecord: doc,
|
|
309
|
-
simplified: false
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
testCountryId = result.data.id
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
it('should handle PATCH requests with CORS', async () => {
|
|
316
|
-
const patchDoc = {
|
|
317
|
-
data: {
|
|
318
|
-
type: 'countries',
|
|
319
|
-
id: String(testCountryId),
|
|
320
|
-
attributes: {
|
|
321
|
-
name: 'Updated Country Name'
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const response = await request(app)
|
|
327
|
-
.patch(`/api/countries/${testCountryId}`)
|
|
328
|
-
.send(patchDoc)
|
|
329
|
-
.set('Origin', 'https://example.com')
|
|
330
|
-
.set('Content-Type', 'application/vnd.api+json')
|
|
331
|
-
.set('Accept', 'application/vnd.api+json')
|
|
332
|
-
|
|
333
|
-
assert.equal(response.status, 200)
|
|
334
|
-
// When credentials are true and origin is sent, specific origin is returned
|
|
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 () => {
|
|
340
|
-
const response = await request(app)
|
|
341
|
-
.delete(`/api/countries/${testCountryId}`)
|
|
342
|
-
.set('Origin', 'https://example.com')
|
|
343
|
-
.set('Accept', 'application/vnd.api+json')
|
|
344
|
-
|
|
345
|
-
assert.equal(response.status, 204)
|
|
346
|
-
// When credentials are true and origin is sent, specific origin is returned
|
|
347
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
348
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
it('should handle PUT requests with CORS', async () => {
|
|
352
|
-
const putDoc = {
|
|
353
|
-
data: {
|
|
354
|
-
type: 'countries',
|
|
355
|
-
id: testCountryId,
|
|
356
|
-
attributes: {
|
|
357
|
-
name: 'Completely Replaced Country',
|
|
358
|
-
code: 'RC'
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const response = await request(app)
|
|
364
|
-
.put(`/api/countries/${testCountryId}`)
|
|
365
|
-
.set('Origin', 'https://example.com')
|
|
366
|
-
.set('Accept', 'application/vnd.api+json')
|
|
367
|
-
.set('Content-Type', 'application/vnd.api+json')
|
|
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
|
-
})
|
|
376
|
-
|
|
377
|
-
describe('CORS with Array and Regex Origins', () => {
|
|
378
|
-
it('should support array of allowed origins', async () => {
|
|
379
|
-
// Create a new API instance with array origin configuration
|
|
380
|
-
const arrayOriginApp = express()
|
|
381
|
-
const arrayOriginApi = await createBasicApi(knex, {
|
|
382
|
-
apiName: 'cors-array-origins-test',
|
|
383
|
-
tablePrefix: 'cors_array',
|
|
384
|
-
includeExpress: true,
|
|
385
|
-
express: { app: arrayOriginApp }
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
await arrayOriginApi.use(CorsPlugin, {
|
|
389
|
-
origin: ['https://app1.com', 'https://app2.com', 'https://app3.com'],
|
|
390
|
-
credentials: true
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
// Mount routes after CORS plugin is installed
|
|
394
|
-
arrayOriginApi.http.express.mount(arrayOriginApp)
|
|
395
|
-
|
|
396
|
-
// Test allowed origin
|
|
397
|
-
const response1 = await request(arrayOriginApp)
|
|
398
|
-
.get('/api/countries')
|
|
399
|
-
.set('Origin', 'https://app2.com')
|
|
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
|
-
|
|
405
|
-
// Test disallowed origin
|
|
406
|
-
const response2 = await request(arrayOriginApp)
|
|
407
|
-
.get('/api/countries')
|
|
408
|
-
.set('Origin', 'https://app4.com')
|
|
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
|
-
|
|
415
|
-
it('should support regex origin matching', async () => {
|
|
416
|
-
// Create a new API instance with regex origin configuration
|
|
417
|
-
const regexOriginApp = express()
|
|
418
|
-
const regexOriginApi = await createBasicApi(knex, {
|
|
419
|
-
apiName: 'cors-regex-origins-test',
|
|
420
|
-
tablePrefix: 'cors_regex',
|
|
421
|
-
includeExpress: true,
|
|
422
|
-
express: { app: regexOriginApp }
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
await regexOriginApi.use(CorsPlugin, {
|
|
426
|
-
origin: /^https:\/\/.*\.example\.com$/,
|
|
427
|
-
credentials: true
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
// Mount routes after CORS plugin is installed
|
|
431
|
-
regexOriginApi.http.express.mount(regexOriginApp)
|
|
432
|
-
|
|
433
|
-
// Test matching subdomain
|
|
434
|
-
const response1 = await request(regexOriginApp)
|
|
435
|
-
.get('/api/countries')
|
|
436
|
-
.set('Origin', 'https://app.example.com')
|
|
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
|
-
|
|
442
|
-
// Test another matching subdomain
|
|
443
|
-
const response2 = await request(regexOriginApp)
|
|
444
|
-
.get('/api/countries')
|
|
445
|
-
.set('Origin', 'https://api.example.com')
|
|
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
|
-
|
|
451
|
-
// Test non-matching domain
|
|
452
|
-
const response3 = await request(regexOriginApp)
|
|
453
|
-
.get('/api/countries')
|
|
454
|
-
.set('Origin', 'https://example.org')
|
|
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
|
-
|
|
461
|
-
it('should support function-based origin validation', async () => {
|
|
462
|
-
// Create a new API instance with function origin configuration
|
|
463
|
-
const functionOriginApp = express()
|
|
464
|
-
const functionOriginApi = await createBasicApi(knex, {
|
|
465
|
-
apiName: 'cors-function-origins-test',
|
|
466
|
-
tablePrefix: 'cors_func',
|
|
467
|
-
includeExpress: true,
|
|
468
|
-
express: { app: functionOriginApp }
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
const allowedOrigins = new Set(['https://dynamic1.com', 'https://dynamic2.com'])
|
|
472
|
-
|
|
473
|
-
await functionOriginApi.use(CorsPlugin, {
|
|
474
|
-
origin: (origin) => allowedOrigins.has(origin),
|
|
475
|
-
credentials: true
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
// Mount routes after CORS plugin is installed
|
|
479
|
-
functionOriginApi.http.express.mount(functionOriginApp)
|
|
480
|
-
|
|
481
|
-
// Test allowed origin
|
|
482
|
-
const response1 = await request(functionOriginApp)
|
|
483
|
-
.get('/api/countries')
|
|
484
|
-
.set('Origin', 'https://dynamic1.com')
|
|
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
|
-
|
|
490
|
-
// Test disallowed origin
|
|
491
|
-
const response2 = await request(functionOriginApp)
|
|
492
|
-
.get('/api/countries')
|
|
493
|
-
.set('Origin', 'https://dynamic3.com')
|
|
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
|
-
})
|
|
500
|
-
|
|
501
|
-
describe('CORS Error Handling', () => {
|
|
502
|
-
it('should reject preflight from disallowed origin', async () => {
|
|
503
|
-
// Create a new API instance with specific allowed origin
|
|
504
|
-
const restrictedApp = express()
|
|
505
|
-
const restrictedApi = await createBasicApi(knex, {
|
|
506
|
-
apiName: 'cors-restricted-test',
|
|
507
|
-
tablePrefix: 'cors_restrict',
|
|
508
|
-
includeExpress: true,
|
|
509
|
-
express: { app: restrictedApp }
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
await restrictedApi.use(CorsPlugin, {
|
|
513
|
-
origin: 'https://allowed-only.com',
|
|
514
|
-
credentials: true
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
// Mount routes after CORS plugin is installed
|
|
518
|
-
restrictedApi.http.express.mount(restrictedApp)
|
|
519
|
-
|
|
520
|
-
const response = await request(restrictedApp)
|
|
521
|
-
.options('/api/countries')
|
|
522
|
-
.set('Origin', 'https://not-allowed.com')
|
|
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 () => {
|
|
531
|
-
// Some clients might send lowercase methods
|
|
532
|
-
const response = await request(app)
|
|
533
|
-
.options('/api/countries')
|
|
534
|
-
.set('Origin', 'https://example.com')
|
|
535
|
-
.set('Access-Control-Request-Method', 'post') // lowercase
|
|
536
|
-
|
|
537
|
-
assert.equal(response.status, 204)
|
|
538
|
-
// When credentials are true and origin is sent, specific origin is returned
|
|
539
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
540
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
541
|
-
})
|
|
542
|
-
})
|
|
543
|
-
|
|
544
|
-
describe('CORS Integration with REST API', () => {
|
|
545
|
-
it('should handle CORS for error responses', async () => {
|
|
546
|
-
// Try to get non-existent resource
|
|
547
|
-
const response = await request(app)
|
|
548
|
-
.get('/api/countries/999999')
|
|
549
|
-
.set('Origin', 'https://example.com')
|
|
550
|
-
.set('Accept', 'application/vnd.api+json')
|
|
551
|
-
|
|
552
|
-
assert.equal(response.status, 404)
|
|
553
|
-
// CORS headers should still be set for error responses
|
|
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 () => {
|
|
560
|
-
// Send invalid data
|
|
561
|
-
const invalidDoc = createJsonApiDocument('countries', {
|
|
562
|
-
// Missing required 'name' field
|
|
563
|
-
code: 'XX'
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
const response = await request(app)
|
|
567
|
-
.post('/api/countries')
|
|
568
|
-
.send(invalidDoc)
|
|
569
|
-
.set('Origin', 'https://example.com')
|
|
570
|
-
.set('Content-Type', 'application/vnd.api+json')
|
|
571
|
-
.set('Accept', 'application/vnd.api+json')
|
|
572
|
-
|
|
573
|
-
assert.equal(response.status, 422)
|
|
574
|
-
// CORS headers should be set for validation errors
|
|
575
|
-
assert.equal(response.headers['access-control-allow-origin'], 'https://example.com')
|
|
576
|
-
assert.equal(response.headers['vary'], 'Origin')
|
|
577
|
-
})
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
describe('CORS Configuration Options', () => {
|
|
581
|
-
it('should respect custom max-age configuration', async () => {
|
|
582
|
-
// Create API with custom max-age
|
|
583
|
-
const maxAgeApp = express()
|
|
584
|
-
const maxAgeApi = await createBasicApi(knex, {
|
|
585
|
-
apiName: 'cors-maxage-test',
|
|
586
|
-
tablePrefix: 'cors_maxage',
|
|
587
|
-
includeExpress: true,
|
|
588
|
-
express: { app: maxAgeApp }
|
|
589
|
-
})
|
|
590
|
-
|
|
591
|
-
await maxAgeApi.use(CorsPlugin, {
|
|
592
|
-
origin: '*',
|
|
593
|
-
credentials: false,
|
|
594
|
-
maxAge: 3600 // 1 hour instead of default 24 hours
|
|
595
|
-
})
|
|
596
|
-
|
|
597
|
-
maxAgeApi.http.express.mount(maxAgeApp)
|
|
598
|
-
|
|
599
|
-
const response = await request(maxAgeApp)
|
|
600
|
-
.options('/api/countries')
|
|
601
|
-
.set('Origin', 'https://example.com')
|
|
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
|
-
|
|
608
|
-
it('should respect custom exposed headers configuration', async () => {
|
|
609
|
-
// Create API with custom exposed headers
|
|
610
|
-
const exposedApp = express()
|
|
611
|
-
const exposedApi = await createBasicApi(knex, {
|
|
612
|
-
apiName: 'cors-exposed-test',
|
|
613
|
-
tablePrefix: 'cors_exposed',
|
|
614
|
-
includeExpress: true,
|
|
615
|
-
express: { app: exposedApp }
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
await exposedApi.use(CorsPlugin, {
|
|
619
|
-
origin: '*',
|
|
620
|
-
credentials: false,
|
|
621
|
-
exposedHeaders: ['X-Custom-Header', 'X-Rate-Limit', 'X-Request-Id']
|
|
622
|
-
})
|
|
623
|
-
|
|
624
|
-
exposedApi.http.express.mount(exposedApp)
|
|
625
|
-
|
|
626
|
-
const response = await request(exposedApp)
|
|
627
|
-
.get('/api/countries')
|
|
628
|
-
.set('Origin', 'https://example.com')
|
|
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
|
-
|
|
635
|
-
it('should respect custom allowed methods configuration', async () => {
|
|
636
|
-
// Create API with limited methods
|
|
637
|
-
const methodsApp = express()
|
|
638
|
-
const methodsApi = await createBasicApi(knex, {
|
|
639
|
-
apiName: 'cors-methods-test',
|
|
640
|
-
tablePrefix: 'cors_methods',
|
|
641
|
-
includeExpress: true,
|
|
642
|
-
express: { app: methodsApp }
|
|
643
|
-
})
|
|
644
|
-
|
|
645
|
-
await methodsApi.use(CorsPlugin, {
|
|
646
|
-
origin: '*',
|
|
647
|
-
credentials: false,
|
|
648
|
-
methods: ['GET', 'POST'] // Only allow GET and POST
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
methodsApi.http.express.mount(methodsApp)
|
|
652
|
-
|
|
653
|
-
const response = await request(methodsApp)
|
|
654
|
-
.options('/api/countries')
|
|
655
|
-
.set('Origin', 'https://example.com')
|
|
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
|
-
|
|
662
|
-
it('should handle credentials false configuration', async () => {
|
|
663
|
-
// Create API with credentials disabled
|
|
664
|
-
const noCredApp = express()
|
|
665
|
-
const noCredApi = await createBasicApi(knex, {
|
|
666
|
-
apiName: 'cors-nocred-test',
|
|
667
|
-
tablePrefix: 'cors_nocred',
|
|
668
|
-
includeExpress: true,
|
|
669
|
-
express: { app: noCredApp }
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
await noCredApi.use(CorsPlugin, {
|
|
673
|
-
origin: '*',
|
|
674
|
-
credentials: false
|
|
675
|
-
})
|
|
676
|
-
|
|
677
|
-
noCredApi.http.express.mount(noCredApp)
|
|
678
|
-
|
|
679
|
-
const response = await request(noCredApp)
|
|
680
|
-
.get('/api/countries')
|
|
681
|
-
.set('Origin', 'https://example.com')
|
|
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)
|
|
687
|
-
// No Vary header when using wildcard without credentials
|
|
688
|
-
assert.equal(response.headers['vary'], undefined)
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
it('should handle custom allowed headers configuration', async () => {
|
|
692
|
-
// Create API with custom allowed headers
|
|
693
|
-
const headersApp = express()
|
|
694
|
-
const headersApi = await createBasicApi(knex, {
|
|
695
|
-
apiName: 'cors-headers-test',
|
|
696
|
-
tablePrefix: 'cors_headers',
|
|
697
|
-
includeExpress: true,
|
|
698
|
-
express: { app: headersApp }
|
|
699
|
-
})
|
|
700
|
-
|
|
701
|
-
await headersApi.use(CorsPlugin, {
|
|
702
|
-
origin: 'https://app.example.com',
|
|
703
|
-
credentials: true,
|
|
704
|
-
allowedHeaders: ['Content-Type', 'X-API-Key', 'X-Session-Token']
|
|
705
|
-
})
|
|
706
|
-
|
|
707
|
-
headersApi.http.express.mount(headersApp)
|
|
708
|
-
|
|
709
|
-
const response = await request(headersApp)
|
|
710
|
-
.options('/api/countries')
|
|
711
|
-
.set('Origin', 'https://app.example.com')
|
|
712
|
-
.set('Access-Control-Request-Method', 'POST')
|
|
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
|
-
})
|
|
719
|
-
|
|
720
|
-
describe('CORS with PUT Requests', () => {
|
|
721
|
-
it('should handle PUT requests with proper CORS headers', async () => {
|
|
722
|
-
// The main api already has PUT in its allowed methods
|
|
723
|
-
// Create test data first
|
|
724
|
-
const doc = createJsonApiDocument('countries', {
|
|
725
|
-
name: 'Original Country',
|
|
726
|
-
code: 'OC'
|
|
727
|
-
})
|
|
728
|
-
|
|
729
|
-
const createResult = await api.resources.countries.post({
|
|
730
|
-
inputRecord: doc,
|
|
731
|
-
simplified: false
|
|
732
|
-
})
|
|
733
|
-
|
|
734
|
-
const countryId = createResult.data.id
|
|
735
|
-
|
|
736
|
-
// Now test PUT with CORS
|
|
737
|
-
const putDoc = {
|
|
738
|
-
data: {
|
|
739
|
-
type: 'countries',
|
|
740
|
-
id: String(countryId),
|
|
741
|
-
attributes: {
|
|
742
|
-
name: 'Replaced Country',
|
|
743
|
-
code: 'RC'
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const response = await request(app)
|
|
749
|
-
.put(`/api/countries/${countryId}`)
|
|
750
|
-
.send(putDoc)
|
|
751
|
-
.set('Origin', 'https://example.com')
|
|
752
|
-
.set('Content-Type', 'application/vnd.api+json')
|
|
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
|
-
|
|
760
|
-
// Note: PUT might not return the full record by default depending on returnFullRecord settings
|
|
761
|
-
// The important part for CORS testing is that the headers are correct, which we've verified above
|
|
762
|
-
})
|
|
763
|
-
|
|
764
|
-
it('should handle PUT preflight requests', async () => {
|
|
765
|
-
const response = await request(app)
|
|
766
|
-
.options('/api/countries/123')
|
|
767
|
-
.set('Origin', 'https://example.com')
|
|
768
|
-
.set('Access-Control-Request-Method', 'PUT')
|
|
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
|
-
})
|
|
777
|
-
|
|
778
|
-
describe('CORS Edge Cases', () => {
|
|
779
|
-
it('should handle requests with no exposed headers configured', async () => {
|
|
780
|
-
// Create API with empty exposed headers
|
|
781
|
-
const noExposedApp = express()
|
|
782
|
-
const noExposedApi = await createBasicApi(knex, {
|
|
783
|
-
apiName: 'cors-noexposed-test',
|
|
784
|
-
tablePrefix: 'cors_noexposed',
|
|
785
|
-
includeExpress: true,
|
|
786
|
-
express: { app: noExposedApp }
|
|
787
|
-
})
|
|
788
|
-
|
|
789
|
-
await noExposedApi.use(CorsPlugin, {
|
|
790
|
-
origin: '*',
|
|
791
|
-
credentials: false,
|
|
792
|
-
exposedHeaders: [] // Empty array
|
|
793
|
-
})
|
|
794
|
-
|
|
795
|
-
noExposedApi.http.express.mount(noExposedApp)
|
|
796
|
-
|
|
797
|
-
const response = await request(noExposedApp)
|
|
798
|
-
.get('/api/countries')
|
|
799
|
-
.set('Origin', 'https://example.com')
|
|
800
|
-
.set('Accept', 'application/vnd.api+json')
|
|
801
|
-
|
|
802
|
-
assert.equal(response.status, 200)
|
|
803
|
-
// Should not set exposed headers when array is empty
|
|
804
|
-
assert.equal(response.headers['access-control-expose-headers'], undefined)
|
|
805
|
-
})
|
|
806
|
-
|
|
807
|
-
it('should handle optionsSuccessStatus configuration', async () => {
|
|
808
|
-
// Create API with custom options success status
|
|
809
|
-
const statusApp = express()
|
|
810
|
-
const statusApi = await createBasicApi(knex, {
|
|
811
|
-
apiName: 'cors-status-test',
|
|
812
|
-
tablePrefix: 'cors_status',
|
|
813
|
-
includeExpress: true,
|
|
814
|
-
express: { app: statusApp }
|
|
815
|
-
})
|
|
816
|
-
|
|
817
|
-
await statusApi.use(CorsPlugin, {
|
|
818
|
-
origin: '*',
|
|
819
|
-
credentials: false,
|
|
820
|
-
optionsSuccessStatus: 200 // Use 200 instead of 204
|
|
821
|
-
})
|
|
822
|
-
|
|
823
|
-
statusApi.http.express.mount(statusApp)
|
|
824
|
-
|
|
825
|
-
const response = await request(statusApp)
|
|
826
|
-
.options('/api/countries')
|
|
827
|
-
.set('Origin', 'https://example.com')
|
|
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
|
-
})
|