@logux/server 0.13.0 → 0.14.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/README.md +3 -23
- package/add-http-pages/index.js +24 -0
- package/base-server/index.d.ts +95 -97
- package/base-server/index.js +78 -70
- package/context/index.d.ts +43 -66
- package/context/index.js +18 -16
- package/create-reporter/index.js +55 -76
- package/human-formatter/index.js +52 -82
- package/index.js +1 -9
- package/options-loader/index.js +46 -14
- package/options-loader/index.test.js +3 -3
- package/options-loader/test.env +2 -0
- package/package.json +11 -21
- package/request/index.d.ts +1 -99
- package/request/index.js +2 -40
- package/server/index.d.ts +24 -37
- package/server/index.js +11 -16
- package/server-client/index.d.ts +0 -15
- package/server-client/index.js +21 -9
- package/test-client/index.d.ts +19 -5
- package/test-client/index.js +2 -2
- package/test-server/index.d.ts +20 -10
- package/test-server/index.js +21 -2
- package/bind-backend-proxy/index.js +0 -325
- package/bind-control-server/index.js +0 -136
- package/request/test.test.js +0 -134
- /package/{base-server → add-http-pages}/hello.html +0 -0
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
import { LoguxError } from '@logux/core'
|
|
2
|
-
import JSONStream from 'JSONStream'
|
|
3
|
-
import { nanoid } from 'nanoid'
|
|
4
|
-
import http from 'node:http'
|
|
5
|
-
import https from 'node:https'
|
|
6
|
-
|
|
7
|
-
const VERSION = 4
|
|
8
|
-
|
|
9
|
-
const RESEND_KEYS = [
|
|
10
|
-
'channels',
|
|
11
|
-
'channel',
|
|
12
|
-
'nodes',
|
|
13
|
-
'node',
|
|
14
|
-
'clients',
|
|
15
|
-
'client',
|
|
16
|
-
'users',
|
|
17
|
-
'user'
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
function send(backend, command, events) {
|
|
21
|
-
let body = JSON.stringify({
|
|
22
|
-
commands: [command],
|
|
23
|
-
secret: backend.secret,
|
|
24
|
-
version: VERSION
|
|
25
|
-
})
|
|
26
|
-
let protocol = backend.protocol === 'https:' ? https : http
|
|
27
|
-
let req = protocol.request(
|
|
28
|
-
{
|
|
29
|
-
headers: {
|
|
30
|
-
'Content-Length': Buffer.byteLength(body),
|
|
31
|
-
'Content-Type': 'application/json'
|
|
32
|
-
},
|
|
33
|
-
host: backend.hostname,
|
|
34
|
-
method: 'POST',
|
|
35
|
-
path: backend.pathname + backend.search,
|
|
36
|
-
port: backend.port
|
|
37
|
-
},
|
|
38
|
-
res => {
|
|
39
|
-
if (res.statusCode < 200 || res.statusCode > 299) {
|
|
40
|
-
events.error(new Error(`Backend responded with ${res.statusCode} code`))
|
|
41
|
-
} else {
|
|
42
|
-
let data = false
|
|
43
|
-
res
|
|
44
|
-
.pipe(JSONStream.parse('*'))
|
|
45
|
-
.on('data', answer => {
|
|
46
|
-
if (
|
|
47
|
-
typeof answer !== 'object' ||
|
|
48
|
-
typeof answer.answer !== 'string'
|
|
49
|
-
) {
|
|
50
|
-
events.error(new Error('Wrong back-end answer'))
|
|
51
|
-
} else if (answer.answer === 'error') {
|
|
52
|
-
let err = new Error('Error on back-end server')
|
|
53
|
-
err.stack = answer.details
|
|
54
|
-
events.error(err)
|
|
55
|
-
} else if (events.filter(answer)) {
|
|
56
|
-
if (!events[answer.answer]) {
|
|
57
|
-
events.error(new Error('Unknown back-end answer'))
|
|
58
|
-
} else {
|
|
59
|
-
data = true
|
|
60
|
-
events[answer.answer](answer)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
})
|
|
64
|
-
.on('error', err => {
|
|
65
|
-
events.error(err)
|
|
66
|
-
})
|
|
67
|
-
.on('end', () => {
|
|
68
|
-
if (!data) {
|
|
69
|
-
events.error(new Error('Empty back-end answer'))
|
|
70
|
-
} else if (events.end) {
|
|
71
|
-
events.end()
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
)
|
|
77
|
-
req.on('error', err => {
|
|
78
|
-
events.error(err)
|
|
79
|
-
})
|
|
80
|
-
req.end(body)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function bindBackendProxy(app) {
|
|
84
|
-
if (app.options.controlSecret) {
|
|
85
|
-
app.controls['POST /'] = {
|
|
86
|
-
command({ action, meta }, req) {
|
|
87
|
-
if (!app.types[action.type] && !app.getRegexProcessor(action.type)) {
|
|
88
|
-
meta.status = 'processed'
|
|
89
|
-
}
|
|
90
|
-
meta.backend = req.connection.remoteAddress
|
|
91
|
-
return app.log.add(action, meta)
|
|
92
|
-
},
|
|
93
|
-
isValid({ action, command, meta }) {
|
|
94
|
-
return (
|
|
95
|
-
command === 'action' &&
|
|
96
|
-
typeof action === 'object' &&
|
|
97
|
-
typeof action.type === 'string' &&
|
|
98
|
-
typeof meta === 'object'
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!app.options.backend) return
|
|
105
|
-
if (!app.options.controlSecret) {
|
|
106
|
-
let e = new Error('`backend` requires `controlSecret` option')
|
|
107
|
-
e.code = 'LOGUX_NO_CONTROL_SECRET'
|
|
108
|
-
throw e
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let backend = new URL(app.options.backend)
|
|
112
|
-
backend.secret = app.options.controlSecret
|
|
113
|
-
|
|
114
|
-
let resending = new Map()
|
|
115
|
-
let accessing = new Map()
|
|
116
|
-
let processing = new Map()
|
|
117
|
-
let actions = new Map()
|
|
118
|
-
|
|
119
|
-
function sendAction(action, meta, headers) {
|
|
120
|
-
let resendResolve
|
|
121
|
-
if (action.type !== 'logux/subscribe') {
|
|
122
|
-
resending.set(
|
|
123
|
-
meta.id,
|
|
124
|
-
new Promise(resolve => {
|
|
125
|
-
resendResolve = resolve
|
|
126
|
-
})
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
|
-
let accessReject, accessResolve
|
|
130
|
-
accessing.set(
|
|
131
|
-
meta.id,
|
|
132
|
-
new Promise((resolve, reject) => {
|
|
133
|
-
accessResolve = resolve
|
|
134
|
-
accessReject = reject
|
|
135
|
-
})
|
|
136
|
-
)
|
|
137
|
-
let processReject, processResolve
|
|
138
|
-
processing.set(
|
|
139
|
-
meta.id,
|
|
140
|
-
new Promise((resolve, reject) => {
|
|
141
|
-
processResolve = resolve
|
|
142
|
-
processReject = reject
|
|
143
|
-
})
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
let checked = false
|
|
147
|
-
let processed = false
|
|
148
|
-
let error = false
|
|
149
|
-
function currentReject(e) {
|
|
150
|
-
if (resendResolve) resendResolve()
|
|
151
|
-
if (checked) {
|
|
152
|
-
processReject(e)
|
|
153
|
-
} else {
|
|
154
|
-
accessReject(e)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
let start = Date.now()
|
|
159
|
-
app.emitter.emit('backendSent', action, meta)
|
|
160
|
-
send(
|
|
161
|
-
backend,
|
|
162
|
-
{ action, command: 'action', headers, meta },
|
|
163
|
-
{
|
|
164
|
-
action(data) {
|
|
165
|
-
let promise = app.log.add(data.action, {
|
|
166
|
-
status: 'processed',
|
|
167
|
-
...data.meta
|
|
168
|
-
})
|
|
169
|
-
if (actions.has(meta.id)) {
|
|
170
|
-
actions.get(meta.id).push(promise)
|
|
171
|
-
} else {
|
|
172
|
-
actions.set(meta.id, [promise])
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
approved() {
|
|
176
|
-
if (resendResolve) resendResolve()
|
|
177
|
-
app.emitter.emit('backendGranted', action, meta, Date.now() - start)
|
|
178
|
-
checked = true
|
|
179
|
-
accessResolve(true)
|
|
180
|
-
},
|
|
181
|
-
end() {
|
|
182
|
-
if (!error && (!checked || !processed)) {
|
|
183
|
-
currentReject(new Error('Back-end do not send required answers'))
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
error(e) {
|
|
187
|
-
error = true
|
|
188
|
-
currentReject(e)
|
|
189
|
-
},
|
|
190
|
-
filter({ id }) {
|
|
191
|
-
return id === meta.id
|
|
192
|
-
},
|
|
193
|
-
forbidden() {
|
|
194
|
-
if (resendResolve) resendResolve()
|
|
195
|
-
error = true
|
|
196
|
-
accessResolve(false)
|
|
197
|
-
},
|
|
198
|
-
async processed() {
|
|
199
|
-
if (!checked) {
|
|
200
|
-
error = true
|
|
201
|
-
accessReject(new Error('Processed answer was sent before access'))
|
|
202
|
-
} else {
|
|
203
|
-
processed = true
|
|
204
|
-
await Promise.all(actions.get(meta.id) || [])
|
|
205
|
-
app.emitter.emit(
|
|
206
|
-
'backendProcessed',
|
|
207
|
-
action,
|
|
208
|
-
meta,
|
|
209
|
-
Date.now() - start
|
|
210
|
-
)
|
|
211
|
-
processResolve()
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
resend(answer) {
|
|
215
|
-
if (checked) {
|
|
216
|
-
error = true
|
|
217
|
-
currentReject(new Error('Resend answer was sent after access'))
|
|
218
|
-
} else if (action.type === 'logux/subscribe') {
|
|
219
|
-
error = true
|
|
220
|
-
accessReject(new Error('Resend can be called on subscription'))
|
|
221
|
-
} else {
|
|
222
|
-
let resend = {}
|
|
223
|
-
for (let key of RESEND_KEYS) {
|
|
224
|
-
if (typeof answer[key] !== 'undefined') {
|
|
225
|
-
resend[key] = answer[key]
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
resendResolve(resend)
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
unknownAction() {
|
|
232
|
-
resendResolve()
|
|
233
|
-
error = true
|
|
234
|
-
app.unknownType(action, meta)
|
|
235
|
-
accessResolve(false)
|
|
236
|
-
},
|
|
237
|
-
unknownChannel() {
|
|
238
|
-
error = true
|
|
239
|
-
app.wrongChannel(action, meta)
|
|
240
|
-
accessResolve(false)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
app.auth(
|
|
247
|
-
({ client, cookie, headers, token, userId }) =>
|
|
248
|
-
new Promise((resolve, reject) => {
|
|
249
|
-
let random = nanoid()
|
|
250
|
-
send(
|
|
251
|
-
backend,
|
|
252
|
-
{
|
|
253
|
-
authId: random,
|
|
254
|
-
command: 'auth',
|
|
255
|
-
cookie,
|
|
256
|
-
headers,
|
|
257
|
-
subprotocol: client.node.remoteSubprotocol,
|
|
258
|
-
token,
|
|
259
|
-
userId
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
authenticated({ subprotocol }) {
|
|
263
|
-
if (subprotocol) {
|
|
264
|
-
app.options.subprotocol = subprotocol
|
|
265
|
-
client.node.options.subprotocol = subprotocol
|
|
266
|
-
}
|
|
267
|
-
resolve(true)
|
|
268
|
-
},
|
|
269
|
-
denied() {
|
|
270
|
-
resolve(false)
|
|
271
|
-
},
|
|
272
|
-
error(e) {
|
|
273
|
-
reject(e)
|
|
274
|
-
},
|
|
275
|
-
filter({ authId }) {
|
|
276
|
-
return random === authId
|
|
277
|
-
},
|
|
278
|
-
wrongSubprotocol({ supported }) {
|
|
279
|
-
reject(
|
|
280
|
-
new LoguxError('wrong-subprotocol', {
|
|
281
|
-
supported,
|
|
282
|
-
used: client.node.remoteSubprotocol
|
|
283
|
-
})
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
)
|
|
288
|
-
})
|
|
289
|
-
)
|
|
290
|
-
app.otherType({
|
|
291
|
-
access(ctx, action, meta) {
|
|
292
|
-
sendAction(action, meta, ctx.headers)
|
|
293
|
-
return accessing.get(meta.id)
|
|
294
|
-
},
|
|
295
|
-
finally(ctx, action, meta) {
|
|
296
|
-
actions.delete(meta.id)
|
|
297
|
-
resending.delete(meta.id)
|
|
298
|
-
accessing.delete(meta.id)
|
|
299
|
-
processing.delete(meta.id)
|
|
300
|
-
},
|
|
301
|
-
process(ctx, action, meta) {
|
|
302
|
-
return processing.get(meta.id)
|
|
303
|
-
},
|
|
304
|
-
resend(ctx, action, meta) {
|
|
305
|
-
if (!resending.has(meta.id)) {
|
|
306
|
-
sendAction(action, meta, ctx.headers)
|
|
307
|
-
}
|
|
308
|
-
return resending.get(meta.id)
|
|
309
|
-
}
|
|
310
|
-
})
|
|
311
|
-
app.otherChannel({
|
|
312
|
-
access(ctx, action, meta) {
|
|
313
|
-
sendAction(action, meta, ctx.headers)
|
|
314
|
-
return accessing.get(meta.id)
|
|
315
|
-
},
|
|
316
|
-
finally(ctx, action, meta) {
|
|
317
|
-
actions.delete(meta.id)
|
|
318
|
-
accessing.delete(meta.id)
|
|
319
|
-
processing.delete(meta.id)
|
|
320
|
-
},
|
|
321
|
-
load(ctx, action, meta) {
|
|
322
|
-
return processing.get(meta.id)
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { Netmask } from 'netmask'
|
|
2
|
-
|
|
3
|
-
const MAX_VERSION = 4
|
|
4
|
-
const MIN_VERSION = 4
|
|
5
|
-
const NO_SECRET =
|
|
6
|
-
'Set LOGUX_CONTROL_SECRET environment variable for Logux ' +
|
|
7
|
-
'to have access to this page.\n' +
|
|
8
|
-
'Run `npx nanoid` to generate secure secret.'
|
|
9
|
-
|
|
10
|
-
function isValidBody(body) {
|
|
11
|
-
if (typeof body !== 'object') return false
|
|
12
|
-
if (typeof body.version !== 'number') return false
|
|
13
|
-
if (typeof body.secret !== 'string') return false
|
|
14
|
-
if (!Array.isArray(body.commands)) return false
|
|
15
|
-
if (body.commands.some(i => typeof i !== 'object')) return false
|
|
16
|
-
return true
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function bindControlServer(app, custom) {
|
|
20
|
-
if (app.options.disableHttpServer) {
|
|
21
|
-
if (app.options.controlSecret) {
|
|
22
|
-
let err = new Error(
|
|
23
|
-
'`controlSecret` can not be set together with `disableHttpServer` option'
|
|
24
|
-
)
|
|
25
|
-
err.logux = true
|
|
26
|
-
throw err
|
|
27
|
-
}
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let masks = app.options.controlMask.split(/,\s*/).map(i => new Netmask(i))
|
|
32
|
-
app.httpServer.on('request', async (req, res) => {
|
|
33
|
-
let urlString = req.url
|
|
34
|
-
if (/^\/\w+%3F/.test(urlString)) urlString = decodeURIComponent(urlString)
|
|
35
|
-
let reqUrl = new URL(urlString, 'http://localhost')
|
|
36
|
-
let rule = app.controls[req.method + ' ' + reqUrl.pathname]
|
|
37
|
-
|
|
38
|
-
if (!rule) {
|
|
39
|
-
if (custom) {
|
|
40
|
-
custom(req, res)
|
|
41
|
-
} else {
|
|
42
|
-
let accepts = Object.keys(app.controls)
|
|
43
|
-
if (accepts.some(i => i.endsWith(' ' + reqUrl.pathname))) {
|
|
44
|
-
res.statusCode = 405
|
|
45
|
-
res.end('Wrong method')
|
|
46
|
-
} else {
|
|
47
|
-
res.statusCode = 404
|
|
48
|
-
res.end('Wrong path')
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
} else {
|
|
52
|
-
let ipAddress = req.connection.remoteAddress
|
|
53
|
-
if (!(req.method === 'GET' && rule.safe)) {
|
|
54
|
-
if (masks.every(i => !i.contains(ipAddress))) {
|
|
55
|
-
app.emitter.emit('report', 'wrongControlIp', {
|
|
56
|
-
ipAddress,
|
|
57
|
-
mask: app.options.controlMask
|
|
58
|
-
})
|
|
59
|
-
res.statusCode = 403
|
|
60
|
-
res.end('IP address is not in LOGUX_CONTROL_MASK/controlMask')
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (req.method !== 'GET') {
|
|
65
|
-
let json = ''
|
|
66
|
-
req.on('data', chunk => {
|
|
67
|
-
json += chunk
|
|
68
|
-
})
|
|
69
|
-
req.on('end', async () => {
|
|
70
|
-
let body
|
|
71
|
-
try {
|
|
72
|
-
body = JSON.parse(json)
|
|
73
|
-
} catch {
|
|
74
|
-
res.statusCode = 400
|
|
75
|
-
res.end('Wrong format')
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
if (!isValidBody(body)) {
|
|
79
|
-
res.statusCode = 400
|
|
80
|
-
res.end('Wrong body')
|
|
81
|
-
} else if (body.version < MIN_VERSION || body.version > MAX_VERSION) {
|
|
82
|
-
res.statusCode = 400
|
|
83
|
-
res.end('Back-end protocol version is not supported')
|
|
84
|
-
} else if (app.isBruteforce(req.connection.remoteAddress)) {
|
|
85
|
-
res.statusCode = 429
|
|
86
|
-
res.end('Too many wrong secret attempts')
|
|
87
|
-
} else if (body.secret !== app.options.controlSecret) {
|
|
88
|
-
app.rememberBadAuth(req.connection.remoteAddress)
|
|
89
|
-
app.emitter.emit('report', 'wrongControlSecret', {
|
|
90
|
-
ipAddress,
|
|
91
|
-
wrongSecret: body.secret
|
|
92
|
-
})
|
|
93
|
-
res.statusCode = 403
|
|
94
|
-
res.end('Wrong secret')
|
|
95
|
-
} else {
|
|
96
|
-
for (let i of body.commands) {
|
|
97
|
-
if (!rule.isValid(i)) {
|
|
98
|
-
res.statusCode = 400
|
|
99
|
-
res.end('Wrong command')
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
await Promise.all(body.commands.map(i => rule.command(i, req)))
|
|
104
|
-
res.end()
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
} else {
|
|
108
|
-
if (!rule.safe) {
|
|
109
|
-
if (!app.options.controlSecret) {
|
|
110
|
-
res.statusCode = 500
|
|
111
|
-
res.end(NO_SECRET)
|
|
112
|
-
return
|
|
113
|
-
} else if (app.isBruteforce(req.connection.remoteAddress)) {
|
|
114
|
-
res.statusCode = 429
|
|
115
|
-
res.end('Too many wrong secret attempts')
|
|
116
|
-
return
|
|
117
|
-
} else if (reqUrl.search !== '?' + app.options.controlSecret) {
|
|
118
|
-
app.rememberBadAuth(req.connection.remoteAddress)
|
|
119
|
-
app.emitter.emit('report', 'wrongControlSecret', {
|
|
120
|
-
ipAddress,
|
|
121
|
-
wrongSecret: reqUrl.search.slice(1)
|
|
122
|
-
})
|
|
123
|
-
res.statusCode = 403
|
|
124
|
-
res.end('Wrong secret')
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
let answer = await rule.request(req)
|
|
129
|
-
for (let name in answer.headers) {
|
|
130
|
-
res.setHeader(name, answer.headers[name])
|
|
131
|
-
}
|
|
132
|
-
res.end(answer.body)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
}
|
package/request/test.test.js
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { spy } from 'nanospy'
|
|
2
|
-
import { expect, it } from 'vitest'
|
|
3
|
-
|
|
4
|
-
import { del, get, patch, post, put, request, ResponseError } from '../index.js'
|
|
5
|
-
|
|
6
|
-
it('has error', () => {
|
|
7
|
-
let error = new ResponseError(
|
|
8
|
-
403,
|
|
9
|
-
'/a',
|
|
10
|
-
{ body: 'body', method: 'GET' },
|
|
11
|
-
'answer'
|
|
12
|
-
)
|
|
13
|
-
expect(error.name).toEqual('ResponseError')
|
|
14
|
-
expect(error.statusCode).toEqual(403)
|
|
15
|
-
expect(error.message).toEqual('403 response on GET /a {"body":"body"} answer')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
it('throws an error on non-2xx response', async () => {
|
|
19
|
-
async function fetch() {
|
|
20
|
-
return {
|
|
21
|
-
status: 403,
|
|
22
|
-
async text() {
|
|
23
|
-
return 'answer'
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
await expect(
|
|
28
|
-
request('/a', { body: 'body', method: 'GET' }, fetch)
|
|
29
|
-
).rejects.toThrow(
|
|
30
|
-
new ResponseError(403, '/a', { body: 'body', method: 'GET' }, 'answer')
|
|
31
|
-
)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('parses JSON body', async () => {
|
|
35
|
-
async function fetch() {
|
|
36
|
-
return {
|
|
37
|
-
async json() {
|
|
38
|
-
return { answer: '1' }
|
|
39
|
-
},
|
|
40
|
-
status: 200
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
expect(await request('/a', { method: 'GET' }, fetch)).toEqual({ answer: '1' })
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('has shortcut for GET', async () => {
|
|
47
|
-
let fetch = spy(async () => ({
|
|
48
|
-
async json() {
|
|
49
|
-
return { answer: '1' }
|
|
50
|
-
},
|
|
51
|
-
status: 200
|
|
52
|
-
}))
|
|
53
|
-
|
|
54
|
-
expect(await get('/a', { headers: { a: 1 } }, fetch)).toEqual({ answer: '1' })
|
|
55
|
-
expect(fetch.calls).toEqual([
|
|
56
|
-
[
|
|
57
|
-
'/a',
|
|
58
|
-
{
|
|
59
|
-
headers: { a: 1 },
|
|
60
|
-
method: 'GET'
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
])
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('has shortcut for POST', async () => {
|
|
67
|
-
let fetch = spy(async () => ({ status: 204 }))
|
|
68
|
-
await post('/a', { headers: { a: 1 } }, fetch)
|
|
69
|
-
expect(fetch.calls).toEqual([
|
|
70
|
-
[
|
|
71
|
-
'/a',
|
|
72
|
-
{
|
|
73
|
-
headers: { a: 1 },
|
|
74
|
-
method: 'POST'
|
|
75
|
-
}
|
|
76
|
-
]
|
|
77
|
-
])
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('has shortcut for PUT', async () => {
|
|
81
|
-
let fetch = spy(async () => ({ status: 204 }))
|
|
82
|
-
await put('/a', { headers: { a: 1 } }, fetch)
|
|
83
|
-
expect(fetch.calls).toEqual([
|
|
84
|
-
[
|
|
85
|
-
'/a',
|
|
86
|
-
{
|
|
87
|
-
headers: { a: 1 },
|
|
88
|
-
method: 'PUT'
|
|
89
|
-
}
|
|
90
|
-
]
|
|
91
|
-
])
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('has shortcut for PATCH', async () => {
|
|
95
|
-
let fetch = spy(async () => ({ status: 204 }))
|
|
96
|
-
await patch('/a', { headers: { a: 1 } }, fetch)
|
|
97
|
-
expect(fetch.calls).toEqual([
|
|
98
|
-
[
|
|
99
|
-
'/a',
|
|
100
|
-
{
|
|
101
|
-
headers: { a: 1 },
|
|
102
|
-
method: 'PATCH'
|
|
103
|
-
}
|
|
104
|
-
]
|
|
105
|
-
])
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('has shortcut for DELETE', async () => {
|
|
109
|
-
let fetch = spy(async () => ({ status: 204 }))
|
|
110
|
-
await del('/a', { headers: { a: 1 } }, fetch)
|
|
111
|
-
expect(fetch.calls).toEqual([
|
|
112
|
-
[
|
|
113
|
-
'/a',
|
|
114
|
-
{
|
|
115
|
-
headers: { a: 1 },
|
|
116
|
-
method: 'DELETE'
|
|
117
|
-
}
|
|
118
|
-
]
|
|
119
|
-
])
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('works without option object', async () => {
|
|
123
|
-
async function fetch() {
|
|
124
|
-
return {
|
|
125
|
-
status: 404,
|
|
126
|
-
async text() {
|
|
127
|
-
return 'answer'
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
await expect(get('/a', undefined, fetch)).rejects.toThrow(
|
|
132
|
-
new ResponseError(404, '/a', { method: 'GET' }, 'answer')
|
|
133
|
-
)
|
|
134
|
-
})
|
|
File without changes
|