mockaton 8.12.4 → 8.12.6
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/package.json +1 -1
- package/src/Api.js +2 -2
- package/src/Commander.js +2 -2
- package/src/Dashboard.js +30 -27
- package/src/MockBroker.js +2 -2
- package/src/Watcher.js +2 -2
- package/src/Mockaton.test.js +0 -659
- package/src/utils/http-cors.test.js +0 -225
- package/src/utils/validate.test.js +0 -47
package/package.json
CHANGED
package/src/Api.js
CHANGED
|
@@ -51,7 +51,7 @@ export const apiPatchRequests = new Map([
|
|
|
51
51
|
])
|
|
52
52
|
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/** # GET */
|
|
55
55
|
|
|
56
56
|
function serveDashboard(_, response) {
|
|
57
57
|
sendFile(response, join(import.meta.dirname, 'Dashboard.html'))
|
|
@@ -97,7 +97,7 @@ function longPollAR_Events(req, response) {
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
/** # PATCH */
|
|
101
101
|
|
|
102
102
|
function reinitialize(_, response) {
|
|
103
103
|
mockBrokersCollection.init()
|
package/src/Commander.js
CHANGED
|
@@ -92,9 +92,9 @@ export class Commander {
|
|
|
92
92
|
return this.#patch(API.reset)
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
getAR_EventsCount(nAR_EventReceived) {
|
|
95
|
+
getAR_EventsCount(nAR_EventReceived, abortSignal) {
|
|
96
96
|
return fetch(API.arEvents, {
|
|
97
|
-
signal: AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000),
|
|
97
|
+
signal: AbortSignal.any([abortSignal, AbortSignal.timeout(LONG_POLL_SERVER_TIMEOUT + 1000)]),
|
|
98
98
|
headers: {
|
|
99
99
|
[DF.lastReceived_nAR]: nAR_EventReceived
|
|
100
100
|
}
|
package/src/Dashboard.js
CHANGED
|
@@ -68,11 +68,7 @@ let globalDelay = 1200
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
init()
|
|
71
|
-
|
|
72
|
-
document.addEventListener('visibilitychange', () => {
|
|
73
|
-
if (!document.hidden)
|
|
74
|
-
pollAR_Events()
|
|
75
|
-
})
|
|
71
|
+
initLongPoll()
|
|
76
72
|
|
|
77
73
|
function init() {
|
|
78
74
|
return Promise.all([
|
|
@@ -97,7 +93,8 @@ function App([brokersByMethod, cookies, comments, delay, collectProxied, fallbac
|
|
|
97
93
|
r(StaticFilesList, { staticFiles })))
|
|
98
94
|
}
|
|
99
95
|
|
|
100
|
-
|
|
96
|
+
|
|
97
|
+
/** # Header */
|
|
101
98
|
|
|
102
99
|
function Header({ cookies, comments, delay, fallbackAddress, collectProxied }) {
|
|
103
100
|
return (
|
|
@@ -240,8 +237,7 @@ function ResetButton() {
|
|
|
240
237
|
}
|
|
241
238
|
|
|
242
239
|
|
|
243
|
-
|
|
244
|
-
// MockList ===============
|
|
240
|
+
/** # MockList */
|
|
245
241
|
|
|
246
242
|
function MockList({ brokersByMethod, canProxy }) {
|
|
247
243
|
const hasMocks = Object.keys(brokersByMethod).length
|
|
@@ -256,7 +252,6 @@ function MockList({ brokersByMethod, canProxy }) {
|
|
|
256
252
|
r(PayloadViewer)))
|
|
257
253
|
}
|
|
258
254
|
|
|
259
|
-
|
|
260
255
|
function SectionByMethod({ method, brokers, canProxy }) {
|
|
261
256
|
return (
|
|
262
257
|
r('tbody', null,
|
|
@@ -273,7 +268,6 @@ function SectionByMethod({ method, brokers, canProxy }) {
|
|
|
273
268
|
r('td', null, r(ProxyToggler, { broker, disabled: !canProxy }))))))
|
|
274
269
|
}
|
|
275
270
|
|
|
276
|
-
|
|
277
271
|
function PreviewLink({ method, urlMask }) {
|
|
278
272
|
async function onClick(event) {
|
|
279
273
|
event.preventDefault()
|
|
@@ -294,7 +288,6 @@ function PreviewLink({ method, urlMask }) {
|
|
|
294
288
|
}, urlMask))
|
|
295
289
|
}
|
|
296
290
|
|
|
297
|
-
|
|
298
291
|
function MockSelector({ broker }) {
|
|
299
292
|
function onChange() {
|
|
300
293
|
const { urlMask, method } = parseFilename(this.value)
|
|
@@ -349,7 +342,6 @@ function DelayRouteToggler({ broker }) {
|
|
|
349
342
|
TimerIcon()))
|
|
350
343
|
}
|
|
351
344
|
|
|
352
|
-
|
|
353
345
|
function InternalServerErrorToggler({ broker }) {
|
|
354
346
|
function onChange() {
|
|
355
347
|
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
@@ -375,7 +367,6 @@ function InternalServerErrorToggler({ broker }) {
|
|
|
375
367
|
r('span', null, '500')))
|
|
376
368
|
}
|
|
377
369
|
|
|
378
|
-
|
|
379
370
|
function ProxyToggler({ broker, disabled }) {
|
|
380
371
|
function onChange() {
|
|
381
372
|
const { urlMask, method } = parseFilename(broker.mocks[0])
|
|
@@ -399,8 +390,7 @@ function ProxyToggler({ broker, disabled }) {
|
|
|
399
390
|
}
|
|
400
391
|
|
|
401
392
|
|
|
402
|
-
|
|
403
|
-
// Payload Preview ===============
|
|
393
|
+
/** # Payload Preview */
|
|
404
394
|
|
|
405
395
|
const payloadViewerTitleRef = useRef()
|
|
406
396
|
const payloadViewerRef = useRef()
|
|
@@ -497,8 +487,7 @@ function mockSelectorFor(method, urlMask) {
|
|
|
497
487
|
}
|
|
498
488
|
|
|
499
489
|
|
|
500
|
-
|
|
501
|
-
// StaticFilesList ===============
|
|
490
|
+
/** # StaticFilesList */
|
|
502
491
|
|
|
503
492
|
function StaticFilesList({ staticFiles }) {
|
|
504
493
|
if (!staticFiles.length)
|
|
@@ -515,7 +504,7 @@ function StaticFilesList({ staticFiles }) {
|
|
|
515
504
|
}
|
|
516
505
|
|
|
517
506
|
|
|
518
|
-
|
|
507
|
+
/** # Misc */
|
|
519
508
|
|
|
520
509
|
function onError(error) {
|
|
521
510
|
if (error?.message === 'Failed to fetch')
|
|
@@ -537,16 +526,29 @@ function CloudIcon() {
|
|
|
537
526
|
}
|
|
538
527
|
|
|
539
528
|
|
|
540
|
-
|
|
529
|
+
/** # Poll AR Events - Add or Remove mock */
|
|
530
|
+
|
|
531
|
+
function initLongPoll() {
|
|
532
|
+
pollAR_Events.oldAR_EventsCount = 0
|
|
533
|
+
pollAR_Events.isPolling = false
|
|
534
|
+
pollAR_Events.controller = new AbortController()
|
|
535
|
+
pollAR_Events()
|
|
536
|
+
document.addEventListener('visibilitychange', () => {
|
|
537
|
+
if (document.hidden) {
|
|
538
|
+
pollAR_Events.controller.abort('_was_hidden_')
|
|
539
|
+
pollAR_Events.controller = new AbortController()
|
|
540
|
+
}
|
|
541
|
+
else
|
|
542
|
+
pollAR_Events()
|
|
543
|
+
})
|
|
544
|
+
}
|
|
541
545
|
|
|
542
|
-
pollAR_Events.isPolling = false
|
|
543
|
-
pollAR_Events.oldAR_EventsCount = 0
|
|
544
546
|
async function pollAR_Events() {
|
|
545
|
-
if (pollAR_Events.isPolling
|
|
547
|
+
if (pollAR_Events.isPolling)
|
|
546
548
|
return
|
|
547
549
|
try {
|
|
548
550
|
pollAR_Events.isPolling = true
|
|
549
|
-
const response = await mockaton.getAR_EventsCount(pollAR_Events.oldAR_EventsCount)
|
|
551
|
+
const response = await mockaton.getAR_EventsCount(pollAR_Events.oldAR_EventsCount, pollAR_Events.controller.signal)
|
|
550
552
|
if (response.ok) {
|
|
551
553
|
const nAR_Events = await response.json()
|
|
552
554
|
if (pollAR_Events.oldAR_EventsCount !== nAR_Events) { // because it could be < or >
|
|
@@ -559,21 +561,22 @@ async function pollAR_Events() {
|
|
|
559
561
|
else
|
|
560
562
|
throw response.status
|
|
561
563
|
}
|
|
562
|
-
catch (
|
|
564
|
+
catch (error) {
|
|
563
565
|
pollAR_Events.isPolling = false
|
|
564
|
-
|
|
566
|
+
if (error !== '_was_hidden_')
|
|
567
|
+
setTimeout(pollAR_Events, 3000)
|
|
565
568
|
}
|
|
566
569
|
}
|
|
567
570
|
|
|
568
571
|
|
|
569
|
-
|
|
572
|
+
/** # Utils */
|
|
570
573
|
|
|
571
574
|
function cssClass(...args) {
|
|
572
575
|
return args.filter(Boolean).join(' ')
|
|
573
576
|
}
|
|
574
577
|
|
|
575
578
|
|
|
576
|
-
|
|
579
|
+
/** ## React-compatible simplified implementations */
|
|
577
580
|
|
|
578
581
|
function createElement(elem, props = null, ...children) {
|
|
579
582
|
if (typeof elem === 'function')
|
package/src/MockBroker.js
CHANGED
|
@@ -2,8 +2,8 @@ import { includesComment, extractComments, parseFilename } from './Filename.js'
|
|
|
2
2
|
import { DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** MockBroker is a state for a particular route. It knows the available mock files
|
|
6
|
+
* that can be served for the route, the currently selected file, and if it’s delayed. */
|
|
7
7
|
export class MockBroker {
|
|
8
8
|
constructor(file) {
|
|
9
9
|
this.urlMaskMatches = new UrlMatcher(file).urlMaskMatches
|
package/src/Watcher.js
CHANGED
|
@@ -7,7 +7,7 @@ import { isFile } from './utils/fs.js'
|
|
|
7
7
|
import * as mockBrokerCollection from './mockBrokersCollection.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/** # AR = Add or Remove Mock */
|
|
11
11
|
export const arEvents = new class extends EventEmitter {
|
|
12
12
|
count = 0
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ export const arEvents = new class extends EventEmitter {
|
|
|
16
16
|
super.emit('AR')
|
|
17
17
|
}
|
|
18
18
|
subscribe(listener) {
|
|
19
|
-
this.
|
|
19
|
+
this.once('AR', listener)
|
|
20
20
|
}
|
|
21
21
|
unsubscribe(listener) {
|
|
22
22
|
this.removeListener('AR', listener)
|
package/src/Mockaton.test.js
DELETED
|
@@ -1,659 +0,0 @@
|
|
|
1
|
-
import { tmpdir } from 'node:os'
|
|
2
|
-
import { promisify } from 'node:util'
|
|
3
|
-
import { describe, it } from 'node:test'
|
|
4
|
-
import { createServer } from 'node:http'
|
|
5
|
-
import { dirname, join } from 'node:path'
|
|
6
|
-
import { randomUUID } from 'node:crypto'
|
|
7
|
-
import { equal, deepEqual, match } from 'node:assert/strict'
|
|
8
|
-
import { writeFileSync, mkdtempSync, mkdirSync, unlinkSync, readFileSync } from 'node:fs'
|
|
9
|
-
|
|
10
|
-
import { config } from './config.js'
|
|
11
|
-
import { mimeFor } from './utils/mime.js'
|
|
12
|
-
import { Mockaton } from './Mockaton.js'
|
|
13
|
-
import { readBody } from './utils/http-request.js'
|
|
14
|
-
import { Commander } from './Commander.js'
|
|
15
|
-
import { CorsHeader } from './utils/http-cors.js'
|
|
16
|
-
import { parseFilename } from './Filename.js'
|
|
17
|
-
import { listFilesRecursively } from './utils/fs.js'
|
|
18
|
-
import { API, DEFAULT_500_COMMENT, DEFAULT_MOCK_COMMENT } from './ApiConstants.js'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const tmpDir = mkdtempSync(tmpdir() + '/mocks') + '/'
|
|
22
|
-
const staticTmpDir = mkdtempSync(tmpdir() + '/static') + '/'
|
|
23
|
-
|
|
24
|
-
const fixtureCustomMime = [
|
|
25
|
-
'/api/custom-mime',
|
|
26
|
-
'api/custom-mime.GET.200.my_custom_extension',
|
|
27
|
-
'Custom Extension and MIME'
|
|
28
|
-
]
|
|
29
|
-
const fixtureNonDefaultInName = [
|
|
30
|
-
'/api/the-route',
|
|
31
|
-
'api/the-route.GET.200.json',
|
|
32
|
-
'default my route body content'
|
|
33
|
-
]
|
|
34
|
-
const fixtureDefaultInName = [
|
|
35
|
-
'/api/the-route',
|
|
36
|
-
'api/the-route(default).GET.200.json',
|
|
37
|
-
'default my route body content'
|
|
38
|
-
]
|
|
39
|
-
const fixtureDelayed = [
|
|
40
|
-
'/api/delayed',
|
|
41
|
-
'api/delayed.GET.200.json',
|
|
42
|
-
'Route_To_Be_Delayed'
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
/* Only fixtures with PUT */
|
|
46
|
-
const fixtureForRegisteringPutA = [
|
|
47
|
-
'/api/register',
|
|
48
|
-
'api/register(a).PUT.200.json',
|
|
49
|
-
'fixture_for_registering_a'
|
|
50
|
-
]
|
|
51
|
-
const fixtureForRegisteringPutB = [
|
|
52
|
-
'/api/register',
|
|
53
|
-
'api/register(b).PUT.200.json',
|
|
54
|
-
'fixture_for_registering_b'
|
|
55
|
-
]
|
|
56
|
-
const fixtureForRegisteringPutA500 = [
|
|
57
|
-
'/api/register',
|
|
58
|
-
'api/register.PUT.500.json',
|
|
59
|
-
'fixture_for_registering_500'
|
|
60
|
-
]
|
|
61
|
-
const fixtureForUnregisteringPutC = [
|
|
62
|
-
'/api/unregister',
|
|
63
|
-
'api/unregister.PUT.200.json',
|
|
64
|
-
'fixture_for_unregistering'
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const fixtures = [
|
|
69
|
-
[
|
|
70
|
-
'/api',
|
|
71
|
-
'api/.GET.200.json',
|
|
72
|
-
'index-like route for /api, which could just be the extension convention'
|
|
73
|
-
],
|
|
74
|
-
|
|
75
|
-
// Exact route paths
|
|
76
|
-
fixtureDefaultInName,
|
|
77
|
-
fixtureDelayed,
|
|
78
|
-
[
|
|
79
|
-
'/api/the-route',
|
|
80
|
-
'api/the-route(default).GET.200.json',
|
|
81
|
-
'default my route body content'
|
|
82
|
-
],
|
|
83
|
-
[
|
|
84
|
-
'/api/the-mime',
|
|
85
|
-
'api/the-mime.GET.200.txt',
|
|
86
|
-
'determines the content type'
|
|
87
|
-
], [
|
|
88
|
-
'/api/the-method-and-status',
|
|
89
|
-
'api/the-method-and-status.POST.201.json',
|
|
90
|
-
'obeys the HTTP method and response status'
|
|
91
|
-
], [
|
|
92
|
-
'/api/the-comment',
|
|
93
|
-
'api/the-comment(this is the actual comment).GET.200(another comment).txt',
|
|
94
|
-
''
|
|
95
|
-
], [
|
|
96
|
-
'/api/alternative',
|
|
97
|
-
'api/alternative(comment-1).GET.200.json',
|
|
98
|
-
'With_Comment_1'
|
|
99
|
-
], [
|
|
100
|
-
'/api/dot.in.path',
|
|
101
|
-
'api/dot.in.path.GET.200.json',
|
|
102
|
-
'Dot_in_Path'
|
|
103
|
-
], [
|
|
104
|
-
'/api/space & colon:',
|
|
105
|
-
'api/space & colon:.GET.200.json',
|
|
106
|
-
'Decodes URI'
|
|
107
|
-
],
|
|
108
|
-
|
|
109
|
-
[
|
|
110
|
-
'/api/uncommon-method',
|
|
111
|
-
'/api/uncommon-method.ACL.200.json',
|
|
112
|
-
'node.js doesn’t support arbitrary HTTP methods, but it does support a few non-standard ones'
|
|
113
|
-
],
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Dynamic Params
|
|
117
|
-
[
|
|
118
|
-
'/api/user/1234',
|
|
119
|
-
'api/user/[id]/.GET.200.json',
|
|
120
|
-
'variable at end'
|
|
121
|
-
], [
|
|
122
|
-
'/api/user/1234/suffix',
|
|
123
|
-
'api/user/[id]/suffix.GET.200.json',
|
|
124
|
-
'sandwich a variable that another route has at the end'
|
|
125
|
-
], [
|
|
126
|
-
'/api/user/exact-route',
|
|
127
|
-
'api/user/exact-route.GET.200.json',
|
|
128
|
-
'ensure dynamic params do not take precedence over exact routes'
|
|
129
|
-
],
|
|
130
|
-
|
|
131
|
-
// Query String
|
|
132
|
-
// TODO ignore on Windows (because of ?)
|
|
133
|
-
[
|
|
134
|
-
'/api/my-query-string?foo=[foo]&bar=[bar]',
|
|
135
|
-
'api/my-query-string?foo=[foo]&bar=[bar].GET.200.json',
|
|
136
|
-
'two query string params'
|
|
137
|
-
], [
|
|
138
|
-
'/api/company-a',
|
|
139
|
-
'api/company-a/[id]?limit=[limit].GET.200.json',
|
|
140
|
-
'without pretty-param nor query-params'
|
|
141
|
-
], [
|
|
142
|
-
'/api/company-b/',
|
|
143
|
-
'api/company-b/[id]?limit=[limit].GET.200.json',
|
|
144
|
-
'without pretty-param nor query-params with trailing slash'
|
|
145
|
-
], [
|
|
146
|
-
'/api/company-c/1234',
|
|
147
|
-
'api/company-c/[id]?limit=[limit].GET.200.json',
|
|
148
|
-
'with pretty-param and without query-params'
|
|
149
|
-
], [
|
|
150
|
-
'/api/company-d/1234/?',
|
|
151
|
-
'api/company-d/[id]?limit=[limit].GET.200.json',
|
|
152
|
-
'with pretty-param and without query-params, but with trailing slash and "?"'
|
|
153
|
-
], [
|
|
154
|
-
'/api/company-e/1234/?limit=4',
|
|
155
|
-
'api/company-e/[id]?limit=[limit].GET.200.json',
|
|
156
|
-
'with pretty-param and query-params'
|
|
157
|
-
],
|
|
158
|
-
fixtureCustomMime
|
|
159
|
-
]
|
|
160
|
-
for (const [, file, body] of [fixtureNonDefaultInName, ...fixtures])
|
|
161
|
-
write(file, file.endsWith('.json') ? JSON.stringify(body) : body)
|
|
162
|
-
|
|
163
|
-
write('api/.GET.500.txt', 'keeps non-autogenerated 500')
|
|
164
|
-
write('api/alternative(comment-2).GET.200.json', JSON.stringify({ comment: 2 }))
|
|
165
|
-
write('api/my-route(comment-2).GET.200.json', JSON.stringify({ comment: 2 }))
|
|
166
|
-
write('api/ignored.GET.200.json~', '')
|
|
167
|
-
|
|
168
|
-
// JavaScript to JSON
|
|
169
|
-
write('/api/object.GET.200.js', 'export default { JSON_FROM_JS: true }')
|
|
170
|
-
|
|
171
|
-
const staticFiles = [
|
|
172
|
-
['index.html', '<h1>Static</h1>'],
|
|
173
|
-
['assets/app.js', 'const app = 1'],
|
|
174
|
-
['another-entry/index.html', '<h1>Another</h1>']
|
|
175
|
-
]
|
|
176
|
-
writeStatic('ignored.js~', 'ignored_file_body')
|
|
177
|
-
for (const [file, body] of staticFiles)
|
|
178
|
-
writeStatic(file, body)
|
|
179
|
-
|
|
180
|
-
const server = Mockaton({
|
|
181
|
-
mocksDir: tmpDir,
|
|
182
|
-
staticDir: staticTmpDir,
|
|
183
|
-
delay: 80,
|
|
184
|
-
onReady: () => {},
|
|
185
|
-
cookies: {
|
|
186
|
-
userA: 'CookieA',
|
|
187
|
-
userB: 'CookieB'
|
|
188
|
-
},
|
|
189
|
-
extraHeaders: ['Server', 'MockatonTester'],
|
|
190
|
-
extraMimes: {
|
|
191
|
-
my_custom_extension: 'my_custom_mime'
|
|
192
|
-
},
|
|
193
|
-
corsOrigins: ['http://example.com'],
|
|
194
|
-
corsExposedHeaders: ['Content-Encoding']
|
|
195
|
-
})
|
|
196
|
-
server.on('listening', runTests)
|
|
197
|
-
|
|
198
|
-
function mockatonAddr() {
|
|
199
|
-
const { address, port } = server.address()
|
|
200
|
-
return `http://${address}:${port}`
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function request(path, options = {}) {
|
|
204
|
-
return fetch(`${mockatonAddr()}${path}`, options)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
let commander
|
|
208
|
-
async function runTests() {
|
|
209
|
-
commander = new Commander(mockatonAddr())
|
|
210
|
-
|
|
211
|
-
await testItRendersDashboard()
|
|
212
|
-
await test404()
|
|
213
|
-
|
|
214
|
-
for (const [url, file, body] of fixtures)
|
|
215
|
-
await testMockDispatching(url, file, body)
|
|
216
|
-
|
|
217
|
-
await testDefaultMock()
|
|
218
|
-
|
|
219
|
-
await testItUpdatesRouteDelay(...fixtureDelayed)
|
|
220
|
-
await testBadRequestWhenUpdatingNonExistingMockAlternative()
|
|
221
|
-
|
|
222
|
-
await testAutogenerates500(
|
|
223
|
-
'/api/alternative',
|
|
224
|
-
`api/alternative${DEFAULT_500_COMMENT}.GET.500.empty`)
|
|
225
|
-
|
|
226
|
-
await testPreservesExiting500(
|
|
227
|
-
'/api',
|
|
228
|
-
'api/.GET.500.txt',
|
|
229
|
-
'keeps non-autogenerated 500')
|
|
230
|
-
|
|
231
|
-
await commander.reset()
|
|
232
|
-
await testItUpdatesTheCurrentSelectedMock(
|
|
233
|
-
'/api/alternative',
|
|
234
|
-
'api/alternative(comment-2).GET.200.json',
|
|
235
|
-
200,
|
|
236
|
-
JSON.stringify({ comment: 2 }))
|
|
237
|
-
|
|
238
|
-
await commander.reset()
|
|
239
|
-
await testExtractsAllComments([
|
|
240
|
-
'(comment-1)',
|
|
241
|
-
'(comment-2)',
|
|
242
|
-
DEFAULT_500_COMMENT,
|
|
243
|
-
'(this is the actual comment)',
|
|
244
|
-
'(another comment)',
|
|
245
|
-
DEFAULT_MOCK_COMMENT
|
|
246
|
-
])
|
|
247
|
-
await testItBulkSelectsByComment('(comment-2)', [
|
|
248
|
-
['/api/alternative', 'api/alternative(comment-2).GET.200.json', { comment: 2 }],
|
|
249
|
-
['/api/my-route', 'api/my-route(comment-2).GET.200.json', { comment: 2 }]
|
|
250
|
-
])
|
|
251
|
-
await testItBulkSelectsByComment('mment-1', [ // partial match within parentheses
|
|
252
|
-
['/api/alternative', 'api/alternative(comment-1).GET.200.json', 'With_Comment_1']
|
|
253
|
-
])
|
|
254
|
-
await commander.reset()
|
|
255
|
-
|
|
256
|
-
for (const [url, file, body] of fixtures)
|
|
257
|
-
await testMockDispatching(url, file, body)
|
|
258
|
-
|
|
259
|
-
await testMockDispatching('/api/object', 'api/object.GET.200.js', { JSON_FROM_JS: true }, mimeFor('.json'))
|
|
260
|
-
await testMockDispatching(...fixtureCustomMime, 'my_custom_mime')
|
|
261
|
-
await testJsFunctionMocks()
|
|
262
|
-
|
|
263
|
-
await testItUpdatesCookie()
|
|
264
|
-
await testStaticFileServing()
|
|
265
|
-
await testStaticFileList()
|
|
266
|
-
await testInvalidFilenamesAreIgnored()
|
|
267
|
-
await testEnableFallbackSoRoutesWithoutMocksGetRelayed()
|
|
268
|
-
await testValidatesProxyFallbackURL()
|
|
269
|
-
await testCorsAllowed()
|
|
270
|
-
testWindowsPaths()
|
|
271
|
-
|
|
272
|
-
await testRegistering()
|
|
273
|
-
|
|
274
|
-
server.close()
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
async function testItRendersDashboard() {
|
|
279
|
-
const res = await request(API.dashboard)
|
|
280
|
-
const body = await res.text()
|
|
281
|
-
await describe('Dashboard', () =>
|
|
282
|
-
it('Renders HTML', () => match(body, new RegExp('<!DOCTYPE html>'))))
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function test404() {
|
|
286
|
-
await it('Sends 404 when there is no mock', async () => {
|
|
287
|
-
const res = await request('/api/non-existing')
|
|
288
|
-
equal(res.status, 404)
|
|
289
|
-
})
|
|
290
|
-
await it('Sends 404 when there’s no mock at all for a method', async () => {
|
|
291
|
-
const res = await request('/api/non-existing-too', { method: 'DELETE' })
|
|
292
|
-
equal(res.status, 404)
|
|
293
|
-
})
|
|
294
|
-
await it('Ignores files ending in ~ by default, e.g. JetBrains temp files', async () => {
|
|
295
|
-
const res = await request('/api/ignored')
|
|
296
|
-
equal(res.status, 404)
|
|
297
|
-
})
|
|
298
|
-
await it('Ignores static files ending in ~ by default, e.g. JetBrains temp files', async () => {
|
|
299
|
-
const res = await request('/ignored.js~')
|
|
300
|
-
equal(res.status, 404)
|
|
301
|
-
})
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function testMockDispatching(url, file, expectedBody, forcedMime = undefined) {
|
|
305
|
-
const { urlMask, method, status } = parseFilename(file)
|
|
306
|
-
const mime = forcedMime || mimeFor(file)
|
|
307
|
-
const res = await request(url, { method })
|
|
308
|
-
const body = mime === 'application/json'
|
|
309
|
-
? await res.json()
|
|
310
|
-
: await res.text()
|
|
311
|
-
await describe('URL Mask: ' + urlMask, () => {
|
|
312
|
-
it('file: ' + file, () => deepEqual(body, expectedBody))
|
|
313
|
-
it('mime: ' + mime, () => equal(res.headers.get('content-type'), mime))
|
|
314
|
-
it('status: ' + status, () => equal(res.status, status))
|
|
315
|
-
it('cookie: ' + mime, () => equal(res.headers.get('set-cookie'), 'CookieA'))
|
|
316
|
-
it('extra header', () => equal(res.headers.get('server'), 'MockatonTester'))
|
|
317
|
-
})
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async function testDefaultMock() {
|
|
321
|
-
await testMockDispatching(...fixtureDefaultInName)
|
|
322
|
-
await it('sorts mocks list with the user specified default first for dashboard display', async () => {
|
|
323
|
-
const body = await (await commander.listMocks()).json()
|
|
324
|
-
const { mocks } = body['GET'][fixtureDefaultInName[0]]
|
|
325
|
-
equal(mocks[0], fixtureDefaultInName[1])
|
|
326
|
-
equal(mocks[1], fixtureNonDefaultInName[1])
|
|
327
|
-
})
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
async function testRegistering() {
|
|
331
|
-
await describe('Registering', async () => {
|
|
332
|
-
const temp500 = `api/register${DEFAULT_500_COMMENT}.PUT.500.empty`
|
|
333
|
-
|
|
334
|
-
await it('registering new route creates temp 500 as well and re-registering is a noop', async () => {
|
|
335
|
-
write(fixtureForRegisteringPutA[1], '')
|
|
336
|
-
await sleep()
|
|
337
|
-
write(fixtureForRegisteringPutB[1], '')
|
|
338
|
-
await sleep()
|
|
339
|
-
write(fixtureForRegisteringPutA[1], '')
|
|
340
|
-
await sleep()
|
|
341
|
-
const collection = await (await commander.listMocks()).json()
|
|
342
|
-
deepEqual(collection['PUT'][fixtureForRegisteringPutA[0]].mocks, [
|
|
343
|
-
fixtureForRegisteringPutA[1],
|
|
344
|
-
fixtureForRegisteringPutB[1],
|
|
345
|
-
temp500
|
|
346
|
-
])
|
|
347
|
-
})
|
|
348
|
-
await it('registering a 500 removes the temp 500 (and selects the new 500)', async () => {
|
|
349
|
-
await commander.select(temp500)
|
|
350
|
-
write(fixtureForRegisteringPutA500[1], '')
|
|
351
|
-
await sleep()
|
|
352
|
-
const collection = await (await commander.listMocks()).json()
|
|
353
|
-
const { mocks, currentMock } = collection['PUT'][fixtureForRegisteringPutA[0]]
|
|
354
|
-
deepEqual(mocks, [
|
|
355
|
-
fixtureForRegisteringPutA[1],
|
|
356
|
-
fixtureForRegisteringPutB[1],
|
|
357
|
-
fixtureForRegisteringPutA500[1]
|
|
358
|
-
])
|
|
359
|
-
deepEqual(currentMock, {
|
|
360
|
-
file: fixtureForRegisteringPutA[1],
|
|
361
|
-
delayed: false
|
|
362
|
-
})
|
|
363
|
-
})
|
|
364
|
-
await it('unregisters selected', async () => {
|
|
365
|
-
await commander.select(fixtureForRegisteringPutA[1])
|
|
366
|
-
remove(fixtureForRegisteringPutA[1])
|
|
367
|
-
await sleep()
|
|
368
|
-
const collection = await (await commander.listMocks()).json()
|
|
369
|
-
const { mocks, currentMock } = collection['PUT'][fixtureForRegisteringPutA[0]]
|
|
370
|
-
deepEqual(mocks, [
|
|
371
|
-
fixtureForRegisteringPutB[1],
|
|
372
|
-
fixtureForRegisteringPutA500[1]
|
|
373
|
-
])
|
|
374
|
-
deepEqual(currentMock, {
|
|
375
|
-
file: fixtureForRegisteringPutB[1],
|
|
376
|
-
delayed: false
|
|
377
|
-
})
|
|
378
|
-
})
|
|
379
|
-
await it('unregistering the last mock removes broker', async () => {
|
|
380
|
-
write(fixtureForUnregisteringPutC[1], '') // Register another PUT so it doesn't delete PUT from collection
|
|
381
|
-
await sleep()
|
|
382
|
-
remove(fixtureForUnregisteringPutC[1])
|
|
383
|
-
await sleep()
|
|
384
|
-
const collection = await (await commander.listMocks()).json()
|
|
385
|
-
equal(collection['PUT'][fixtureForUnregisteringPutC[0]], undefined)
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
await it('unregistering the last PUT mock removes PUT from collection', async () => {
|
|
389
|
-
remove(fixtureForRegisteringPutB[1])
|
|
390
|
-
remove(fixtureForRegisteringPutA500[1])
|
|
391
|
-
await sleep()
|
|
392
|
-
const collection = await (await commander.listMocks()).json()
|
|
393
|
-
equal(collection['PUT'], undefined)
|
|
394
|
-
})
|
|
395
|
-
})
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
async function testItUpdatesTheCurrentSelectedMock(url, file, expectedStatus, expectedBody) {
|
|
400
|
-
await commander.select(file)
|
|
401
|
-
const res = await request(url)
|
|
402
|
-
const body = await res.text()
|
|
403
|
-
await describe('url: ' + url, () => {
|
|
404
|
-
it('body is: ' + expectedBody, () => equal(body, expectedBody))
|
|
405
|
-
it('status is: ' + expectedStatus, () => equal(res.status, expectedStatus))
|
|
406
|
-
})
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function testItUpdatesRouteDelay(url, file, expectedBody) {
|
|
410
|
-
const { method } = parseFilename(file)
|
|
411
|
-
await commander.setRouteIsDelayed(method, url, true)
|
|
412
|
-
const now = new Date()
|
|
413
|
-
const res = await request(url)
|
|
414
|
-
const body = await res.text()
|
|
415
|
-
await describe('url: ' + url, () => {
|
|
416
|
-
it('body is: ' + expectedBody, () => equal(body, JSON.stringify(expectedBody)))
|
|
417
|
-
it('delay', () => equal((new Date()).getTime() - now.getTime() > config.delay, true))
|
|
418
|
-
// TODO flaky test ^
|
|
419
|
-
})
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async function testBadRequestWhenUpdatingNonExistingMockAlternative() {
|
|
423
|
-
await it('There are mocks for /api/the-route but not this one', async () => {
|
|
424
|
-
const missingFile = 'api/the-route(non-existing-variant).GET.200.json'
|
|
425
|
-
const res = await commander.select(missingFile)
|
|
426
|
-
equal(res.status, 422)
|
|
427
|
-
equal(await res.text(), `Missing Mock: ${missingFile}`)
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
async function testAutogenerates500(url, file) {
|
|
432
|
-
await commander.select(file)
|
|
433
|
-
const res = await request(url)
|
|
434
|
-
const body = await res.text()
|
|
435
|
-
await describe('autogenerated in-memory 500', () => {
|
|
436
|
-
it('body is empty', () => equal(body, ''))
|
|
437
|
-
it('status is: 500', () => equal(res.status, 500))
|
|
438
|
-
it('mime is empty', () => equal(res.headers.get('content-type'), ''))
|
|
439
|
-
})
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
async function testPreservesExiting500(url, file, expectedBody) {
|
|
443
|
-
await commander.select(file)
|
|
444
|
-
const res = await request(url)
|
|
445
|
-
const body = await res.text()
|
|
446
|
-
await describe('preserves existing 500', () => {
|
|
447
|
-
it('body is empty', () => equal(body, expectedBody))
|
|
448
|
-
it('status is: 500', () => equal(res.status, 500))
|
|
449
|
-
})
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
async function testExtractsAllComments(expected) {
|
|
453
|
-
const res = await commander.listComments()
|
|
454
|
-
const body = await res.json()
|
|
455
|
-
await it('Extracts all comments without duplicates', () =>
|
|
456
|
-
deepEqual(body, expected))
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function testItBulkSelectsByComment(comment, tests) {
|
|
460
|
-
await commander.bulkSelectByComment(comment)
|
|
461
|
-
for (const [url, file, body] of tests)
|
|
462
|
-
await testMockDispatching(url, file, body)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
async function testItUpdatesCookie() {
|
|
467
|
-
await describe('Cookie', () => {
|
|
468
|
-
it('Defaults to the first key:value', async () => {
|
|
469
|
-
const res = await commander.listCookies()
|
|
470
|
-
deepEqual(await res.json(), [
|
|
471
|
-
['userA', true],
|
|
472
|
-
['userB', false]
|
|
473
|
-
])
|
|
474
|
-
})
|
|
475
|
-
|
|
476
|
-
it('Updates selected cookie', async () => {
|
|
477
|
-
await commander.selectCookie('userB')
|
|
478
|
-
const res = await commander.listCookies()
|
|
479
|
-
deepEqual(await res.json(), [
|
|
480
|
-
['userA', false],
|
|
481
|
-
['userB', true]
|
|
482
|
-
])
|
|
483
|
-
})
|
|
484
|
-
|
|
485
|
-
it('422 when trying to select non-existing cookie', async () => {
|
|
486
|
-
const res = await commander.selectCookie('non-existing-cookie-key')
|
|
487
|
-
equal(res.status, 422)
|
|
488
|
-
})
|
|
489
|
-
})
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async function testJsFunctionMocks() {
|
|
493
|
-
await describe('JS Function Mocks', async () => {
|
|
494
|
-
write('api/js-func.POST.200.js', `
|
|
495
|
-
export default function (req, response) {
|
|
496
|
-
response.setHeader('content-type', 'custom-mime')
|
|
497
|
-
return 'SOME_STRING'
|
|
498
|
-
}`)
|
|
499
|
-
await commander.reset() // for registering the file
|
|
500
|
-
await testMockDispatching('/api/js-func',
|
|
501
|
-
'api/js-func.POST.200.js',
|
|
502
|
-
'SOME_STRING',
|
|
503
|
-
'custom-mime')
|
|
504
|
-
})
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
async function testStaticFileServing() {
|
|
509
|
-
await describe('Static File Serving', () => {
|
|
510
|
-
it('404 path traversal', async () => {
|
|
511
|
-
const res = await request('/../../../../../../../../../../../%2E%2E/etc/passwd')
|
|
512
|
-
equal(res.status, 404)
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
it('Defaults to index.html', async () => {
|
|
516
|
-
const res = await request('/')
|
|
517
|
-
const body = await res.text()
|
|
518
|
-
equal(body, '<h1>Static</h1>')
|
|
519
|
-
equal(res.status, 200)
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
it('Defaults to in subdirs index.html', async () => {
|
|
523
|
-
const res = await request('/another-entry')
|
|
524
|
-
const body = await res.text()
|
|
525
|
-
equal(body, '<h1>Another</h1>')
|
|
526
|
-
equal(res.status, 200)
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
it('Serves exacts paths', async () => {
|
|
530
|
-
const res = await request('/assets/app.js')
|
|
531
|
-
const body = await res.text()
|
|
532
|
-
equal(body, 'const app = 1')
|
|
533
|
-
equal(res.status, 200)
|
|
534
|
-
})
|
|
535
|
-
})
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
async function testStaticFileList() {
|
|
539
|
-
await it('Static File List', async () => {
|
|
540
|
-
const res = await commander.listStaticFiles()
|
|
541
|
-
deepEqual((await res.json()).sort(), staticFiles.map(([file]) => file).sort())
|
|
542
|
-
})
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
async function testInvalidFilenamesAreIgnored() {
|
|
546
|
-
await it('Invalid filenames get skipped, so they don’t crash the server', async (t) => {
|
|
547
|
-
const consoleErrorSpy = t.mock.method(console, 'error')
|
|
548
|
-
consoleErrorSpy.mock.mockImplementation(() => {}) // so they don’t render in the test report
|
|
549
|
-
|
|
550
|
-
write('api/_INVALID_FILENAME_CONVENTION_.json', '')
|
|
551
|
-
write('api/bad-filename-method._INVALID_METHOD_.200.json', '')
|
|
552
|
-
write('api/bad-filename-status.GET._INVALID_STATUS_.json', '')
|
|
553
|
-
await commander.reset()
|
|
554
|
-
equal(consoleErrorSpy.mock.calls[0].arguments[0], 'Invalid Filename Convention')
|
|
555
|
-
equal(consoleErrorSpy.mock.calls[1].arguments[0], 'Unrecognized HTTP Method: "_INVALID_METHOD_"')
|
|
556
|
-
equal(consoleErrorSpy.mock.calls[2].arguments[0], 'Invalid HTTP Response Status: "NaN"')
|
|
557
|
-
})
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
async function testEnableFallbackSoRoutesWithoutMocksGetRelayed() {
|
|
561
|
-
await describe('Fallback', async () => {
|
|
562
|
-
const fallbackServer = createServer(async (req, response) => {
|
|
563
|
-
response.writeHead(423, {
|
|
564
|
-
'custom_header': 'my_custom_header',
|
|
565
|
-
'content-type': mimeFor('.txt'),
|
|
566
|
-
'set-cookie': [
|
|
567
|
-
'cookieA=A',
|
|
568
|
-
'cookieB=B'
|
|
569
|
-
]
|
|
570
|
-
})
|
|
571
|
-
response.end(await readBody(req)) // echoes they req body payload
|
|
572
|
-
})
|
|
573
|
-
await promisify(fallbackServer.listen).bind(fallbackServer, 0, '127.0.0.1')()
|
|
574
|
-
|
|
575
|
-
await commander.setProxyFallback(`http://localhost:${fallbackServer.address().port}`)
|
|
576
|
-
await commander.setCollectProxied(true)
|
|
577
|
-
await it('Relays to fallback server and saves the mock', async () => {
|
|
578
|
-
const reqBodyPayload = 'text_req_body'
|
|
579
|
-
|
|
580
|
-
const res = await request(`/api/non-existing-mock/${randomUUID()}`, {
|
|
581
|
-
method: 'POST',
|
|
582
|
-
body: reqBodyPayload
|
|
583
|
-
})
|
|
584
|
-
equal(res.status, 423)
|
|
585
|
-
equal(res.headers.get('custom_header'), 'my_custom_header')
|
|
586
|
-
equal(res.headers.get('set-cookie'), ['cookieA=A', 'cookieB=B'].join(', '))
|
|
587
|
-
equal(await res.text(), reqBodyPayload)
|
|
588
|
-
|
|
589
|
-
const savedBody = readFileSync(join(tmpDir, 'api/non-existing-mock/[id].POST.423.txt'), 'utf8')
|
|
590
|
-
equal(savedBody, reqBodyPayload)
|
|
591
|
-
|
|
592
|
-
fallbackServer.close()
|
|
593
|
-
})
|
|
594
|
-
})
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
async function testValidatesProxyFallbackURL() {
|
|
598
|
-
await it('422 when value is not a valid URL', async () => {
|
|
599
|
-
const res = await commander.setProxyFallback('bad url')
|
|
600
|
-
equal(res.status, 422)
|
|
601
|
-
})
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
async function testCorsAllowed() {
|
|
605
|
-
await it('cors preflight', async () => {
|
|
606
|
-
await commander.setCorsAllowed(true)
|
|
607
|
-
const res = await request('/does-not-matter', {
|
|
608
|
-
method: 'OPTIONS',
|
|
609
|
-
headers: {
|
|
610
|
-
[CorsHeader.Origin]: 'http://example.com',
|
|
611
|
-
[CorsHeader.AcRequestMethod]: 'GET'
|
|
612
|
-
}
|
|
613
|
-
})
|
|
614
|
-
equal(res.status, 204)
|
|
615
|
-
equal(res.headers.get(CorsHeader.AcAllowOrigin), 'http://example.com')
|
|
616
|
-
equal(res.headers.get(CorsHeader.AcAllowMethods), 'GET')
|
|
617
|
-
})
|
|
618
|
-
await it('cors actual response', async () => {
|
|
619
|
-
const res = await request(fixtureDefaultInName[0], {
|
|
620
|
-
headers: {
|
|
621
|
-
[CorsHeader.Origin]: 'http://example.com'
|
|
622
|
-
}
|
|
623
|
-
})
|
|
624
|
-
equal(res.status, 200)
|
|
625
|
-
equal(res.headers.get(CorsHeader.AcAllowOrigin), 'http://example.com')
|
|
626
|
-
equal(res.headers.get(CorsHeader.AcExposeHeaders), 'Content-Encoding')
|
|
627
|
-
})
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function testWindowsPaths() {
|
|
631
|
-
it('normalizes backslashes with forward ones', () => {
|
|
632
|
-
const files = listFilesRecursively(config.mocksDir)
|
|
633
|
-
equal(files[0], 'api/.GET.200.json')
|
|
634
|
-
})
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
// Utils
|
|
639
|
-
|
|
640
|
-
function write(filename, data) {
|
|
641
|
-
_write(tmpDir + filename, data)
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
function remove(filename) {
|
|
645
|
-
unlinkSync(tmpDir + filename)
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function writeStatic(filename, data) {
|
|
649
|
-
_write(staticTmpDir + filename, data)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function _write(absPath, data) {
|
|
653
|
-
mkdirSync(dirname(absPath), { recursive: true })
|
|
654
|
-
writeFileSync(absPath, data, 'utf8')
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
async function sleep(ms = 50) {
|
|
658
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
659
|
-
}
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { equal } from 'node:assert/strict'
|
|
2
|
-
import { promisify } from 'node:util'
|
|
3
|
-
import { createServer } from 'node:http'
|
|
4
|
-
import { describe, it, after } from 'node:test'
|
|
5
|
-
import { isPreflight, setCorsHeaders, CorsHeader as CH } from './http-cors.js'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function headerIs(response, header, value) {
|
|
9
|
-
equal(response.headers.get(header), value)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const FooDotCom = 'http://foo.com'
|
|
13
|
-
const AllowedDotCom = 'http://allowed.com'
|
|
14
|
-
const NotAllowedDotCom = 'http://not-allowed.com'
|
|
15
|
-
|
|
16
|
-
await describe('CORS', async () => {
|
|
17
|
-
let corsConfig = {}
|
|
18
|
-
|
|
19
|
-
const server = createServer((req, response) => {
|
|
20
|
-
setCorsHeaders(req, response, corsConfig)
|
|
21
|
-
if (isPreflight(req)) {
|
|
22
|
-
response.statusCode = 204
|
|
23
|
-
response.end()
|
|
24
|
-
}
|
|
25
|
-
else
|
|
26
|
-
response.end('NON_PREFLIGHT')
|
|
27
|
-
})
|
|
28
|
-
await promisify(server.listen).bind(server, 0, '127.0.0.1')()
|
|
29
|
-
after(() => {
|
|
30
|
-
server.close()
|
|
31
|
-
})
|
|
32
|
-
function preflight(headers, method = 'OPTIONS') {
|
|
33
|
-
const { address, port } = server.address()
|
|
34
|
-
return fetch(`http://${address}:${port}/`, { method, headers })
|
|
35
|
-
}
|
|
36
|
-
function request(headers, method) {
|
|
37
|
-
const { address, port } = server.address()
|
|
38
|
-
return fetch(`http://${address}:${port}/`, { method, headers })
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await describe('Identifies Preflight Requests', async () => {
|
|
42
|
-
const requiredRequestHeaders = {
|
|
43
|
-
[CH.Origin]: 'http://localhost:9999',
|
|
44
|
-
[CH.AcRequestMethod]: 'POST'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
await it('Ignores non-OPTIONS requests', async () => {
|
|
48
|
-
const res = await request(requiredRequestHeaders, 'POST')
|
|
49
|
-
equal(await res.text(), 'NON_PREFLIGHT')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
await it(`Ignores non-parseable req ${CH.Origin} header`, async () => {
|
|
53
|
-
const headers = {
|
|
54
|
-
...requiredRequestHeaders,
|
|
55
|
-
[CH.Origin]: 'non-url'
|
|
56
|
-
}
|
|
57
|
-
const res = await preflight(headers)
|
|
58
|
-
equal(await res.text(), 'NON_PREFLIGHT')
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
await it(`Ignores missing method in ${CH.AcRequestMethod} header`, async () => {
|
|
62
|
-
const headers = { ...requiredRequestHeaders }
|
|
63
|
-
delete headers[CH.AcRequestMethod]
|
|
64
|
-
const res = await preflight(headers)
|
|
65
|
-
equal(await res.text(), 'NON_PREFLIGHT')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
await it(`Ignores non-standard method in ${CH.AcRequestMethod} header`, async () => {
|
|
69
|
-
const headers = {
|
|
70
|
-
...requiredRequestHeaders,
|
|
71
|
-
[CH.AcRequestMethod]: 'NON_STANDARD'
|
|
72
|
-
}
|
|
73
|
-
const res = await preflight(headers)
|
|
74
|
-
equal(await res.text(), 'NON_PREFLIGHT')
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
await it('204 valid preflights', async () => {
|
|
78
|
-
const res = await preflight(requiredRequestHeaders)
|
|
79
|
-
equal(res.status, 204)
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
await describe('Preflight Response Headers', async () => {
|
|
84
|
-
await it('no origins allowed', async () => {
|
|
85
|
-
corsConfig = {
|
|
86
|
-
corsOrigins: [],
|
|
87
|
-
corsMethods: ['GET']
|
|
88
|
-
}
|
|
89
|
-
const p = await preflight({
|
|
90
|
-
[CH.Origin]: FooDotCom,
|
|
91
|
-
[CH.AcRequestMethod]: 'GET'
|
|
92
|
-
})
|
|
93
|
-
headerIs(p, CH.AcAllowOrigin, null)
|
|
94
|
-
headerIs(p, CH.AcAllowMethods, null)
|
|
95
|
-
headerIs(p, CH.AcAllowCredentials, null)
|
|
96
|
-
headerIs(p, CH.AcAllowHeaders, null)
|
|
97
|
-
headerIs(p, CH.AcMaxAge, null)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
await it('not in allowed origins', async () => {
|
|
101
|
-
corsConfig = {
|
|
102
|
-
corsOrigins: [AllowedDotCom],
|
|
103
|
-
corsMethods: ['GET']
|
|
104
|
-
}
|
|
105
|
-
const p = await preflight({
|
|
106
|
-
[CH.Origin]: NotAllowedDotCom,
|
|
107
|
-
[CH.AcRequestMethod]: 'GET'
|
|
108
|
-
})
|
|
109
|
-
headerIs(p, CH.AcAllowOrigin, null)
|
|
110
|
-
headerIs(p, CH.AcAllowMethods, null)
|
|
111
|
-
headerIs(p, CH.AcAllowCredentials, null)
|
|
112
|
-
headerIs(p, CH.AcAllowHeaders, null)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
await it('origin and method match', async () => {
|
|
116
|
-
corsConfig = {
|
|
117
|
-
corsOrigins: [AllowedDotCom],
|
|
118
|
-
corsMethods: ['GET']
|
|
119
|
-
}
|
|
120
|
-
const p = await preflight({
|
|
121
|
-
[CH.Origin]: AllowedDotCom,
|
|
122
|
-
[CH.AcRequestMethod]: 'GET'
|
|
123
|
-
})
|
|
124
|
-
headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
|
|
125
|
-
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
126
|
-
headerIs(p, CH.AcAllowCredentials, null)
|
|
127
|
-
headerIs(p, CH.AcAllowHeaders, null)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
await it('origin matches from multiple', async () => {
|
|
131
|
-
corsConfig = {
|
|
132
|
-
corsOrigins: [AllowedDotCom, FooDotCom],
|
|
133
|
-
corsMethods: ['GET']
|
|
134
|
-
}
|
|
135
|
-
const p = await preflight({
|
|
136
|
-
[CH.Origin]: AllowedDotCom,
|
|
137
|
-
[CH.AcRequestMethod]: 'GET'
|
|
138
|
-
})
|
|
139
|
-
headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
|
|
140
|
-
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
141
|
-
headerIs(p, CH.AcAllowCredentials, null)
|
|
142
|
-
headerIs(p, CH.AcAllowHeaders, null)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
await it('wildcard origin', async () => {
|
|
146
|
-
corsConfig = {
|
|
147
|
-
corsOrigins: ['*'],
|
|
148
|
-
corsMethods: ['GET']
|
|
149
|
-
}
|
|
150
|
-
const p = await preflight({
|
|
151
|
-
[CH.Origin]: FooDotCom,
|
|
152
|
-
[CH.AcRequestMethod]: 'GET'
|
|
153
|
-
})
|
|
154
|
-
headerIs(p, CH.AcAllowOrigin, FooDotCom)
|
|
155
|
-
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
156
|
-
headerIs(p, CH.AcAllowCredentials, null)
|
|
157
|
-
headerIs(p, CH.AcAllowHeaders, null)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
await it(`wildcard and credentials`, async () => {
|
|
161
|
-
corsConfig = {
|
|
162
|
-
corsOrigins: ['*'],
|
|
163
|
-
corsMethods: ['GET'],
|
|
164
|
-
corsCredentials: true
|
|
165
|
-
}
|
|
166
|
-
const p = await preflight({
|
|
167
|
-
[CH.Origin]: FooDotCom,
|
|
168
|
-
[CH.AcRequestMethod]: 'GET'
|
|
169
|
-
})
|
|
170
|
-
headerIs(p, CH.AcAllowOrigin, FooDotCom)
|
|
171
|
-
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
172
|
-
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
173
|
-
headerIs(p, CH.AcAllowHeaders, null)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
await it(`wildcard, credentials, and headers`, async () => {
|
|
177
|
-
corsConfig = {
|
|
178
|
-
corsOrigins: ['*'],
|
|
179
|
-
corsMethods: ['GET'],
|
|
180
|
-
corsCredentials: true,
|
|
181
|
-
corsHeaders: ['content-type', 'my-header']
|
|
182
|
-
}
|
|
183
|
-
const p = await preflight({
|
|
184
|
-
[CH.Origin]: FooDotCom,
|
|
185
|
-
[CH.AcRequestMethod]: 'GET'
|
|
186
|
-
})
|
|
187
|
-
headerIs(p, CH.AcAllowOrigin, FooDotCom)
|
|
188
|
-
headerIs(p, CH.AcAllowMethods, 'GET')
|
|
189
|
-
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
190
|
-
headerIs(p, CH.AcAllowHeaders, 'content-type,my-header')
|
|
191
|
-
})
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
await describe('Non-Preflight (Actual Response) Headers', async () => {
|
|
195
|
-
await it('no origins allowed', async () => {
|
|
196
|
-
corsConfig = {
|
|
197
|
-
corsOrigins: [],
|
|
198
|
-
corsMethods: ['GET']
|
|
199
|
-
}
|
|
200
|
-
const p = await request({
|
|
201
|
-
[CH.Origin]: NotAllowedDotCom
|
|
202
|
-
})
|
|
203
|
-
equal(p.status, 200)
|
|
204
|
-
headerIs(p, CH.AcAllowOrigin, null)
|
|
205
|
-
headerIs(p, CH.AcAllowCredentials, null)
|
|
206
|
-
headerIs(p, CH.AcExposeHeaders, null)
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
await it('origin allowed', async () => {
|
|
210
|
-
corsConfig = {
|
|
211
|
-
corsOrigins: [AllowedDotCom],
|
|
212
|
-
corsMethods: ['GET'],
|
|
213
|
-
corsCredentials: true,
|
|
214
|
-
corsExposedHeaders: ['x-h1', 'x-h2']
|
|
215
|
-
}
|
|
216
|
-
const p = await request({
|
|
217
|
-
[CH.Origin]: AllowedDotCom
|
|
218
|
-
})
|
|
219
|
-
equal(p.status, 200)
|
|
220
|
-
headerIs(p, CH.AcAllowOrigin, AllowedDotCom)
|
|
221
|
-
headerIs(p, CH.AcAllowCredentials, 'true')
|
|
222
|
-
headerIs(p, CH.AcExposeHeaders, 'x-h1,x-h2')
|
|
223
|
-
})
|
|
224
|
-
})
|
|
225
|
-
})
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import { doesNotThrow, throws } from 'node:assert/strict'
|
|
3
|
-
import { validate, is, optional } from './validate.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
describe('validate', () => {
|
|
7
|
-
describe('optional', () => {
|
|
8
|
-
it('accepts undefined', () =>
|
|
9
|
-
doesNotThrow(() =>
|
|
10
|
-
validate({}, { field: optional(Number.isInteger) })))
|
|
11
|
-
|
|
12
|
-
it('accepts falsy value regardless of type', () =>
|
|
13
|
-
doesNotThrow(() =>
|
|
14
|
-
validate({ field: 0 }, { field: optional(Array.isArray) })))
|
|
15
|
-
|
|
16
|
-
it('accepts when tester func returns truthy', () =>
|
|
17
|
-
doesNotThrow(() =>
|
|
18
|
-
validate({ field: [] }, { field: optional(Array.isArray) })))
|
|
19
|
-
|
|
20
|
-
it('rejects when tester func returns falsy', () =>
|
|
21
|
-
throws(() =>
|
|
22
|
-
validate({ field: 1 }, { field: optional(Array.isArray) }),
|
|
23
|
-
/field=1 is invalid/))
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
describe('is', () => {
|
|
27
|
-
it('rejects mismatched type', () =>
|
|
28
|
-
throws(() =>
|
|
29
|
-
validate({ field: 1 }, { field: is(String) }),
|
|
30
|
-
/field=1 is invalid/))
|
|
31
|
-
|
|
32
|
-
it('accepts matched type', () =>
|
|
33
|
-
doesNotThrow(() =>
|
|
34
|
-
validate({ field: '' }, { field: is(String) })))
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
describe('custom tester func', () => {
|
|
38
|
-
it('rejects mismatched type', () =>
|
|
39
|
-
throws(() =>
|
|
40
|
-
validate({ field: 0.1 }, { field: Number.isInteger }),
|
|
41
|
-
/field=0.1 is invalid/))
|
|
42
|
-
|
|
43
|
-
it('accepts matched type', () =>
|
|
44
|
-
doesNotThrow(() =>
|
|
45
|
-
validate({ field: 1 }, { field: Number.isInteger })))
|
|
46
|
-
})
|
|
47
|
-
})
|