]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - shared/server-commands/requests/requests.ts
e3f1817f1b45c934599f9905b2a2e8fb7a028905
[github/Chocobozzz/PeerTube.git] / shared / server-commands / requests / requests.ts
1 /* eslint-disable @typescript-eslint/no-floating-promises */
2
3 import { decode } from 'querystring'
4 import request from 'supertest'
5 import { URL } from 'url'
6 import { buildAbsoluteFixturePath, pick } from '@shared/core-utils'
7 import { HttpStatusCode } from '@shared/models'
8
9 export type CommonRequestParams = {
10 url: string
11 path?: string
12 contentType?: string
13 responseType?: string
14 range?: string
15 redirects?: number
16 accept?: string
17 host?: string
18 token?: string
19 headers?: { [ name: string ]: string }
20 type?: string
21 xForwardedFor?: string
22 expectedStatus?: HttpStatusCode
23 }
24
25 function makeRawRequest (options: {
26 url: string
27 token?: string
28 expectedStatus?: HttpStatusCode
29 range?: string
30 query?: { [ id: string ]: string }
31 method?: 'GET' | 'POST'
32 }) {
33 const { host, protocol, pathname } = new URL(options.url)
34
35 const reqOptions = {
36 url: `${protocol}//${host}`,
37 path: pathname,
38 contentType: undefined,
39
40 ...pick(options, [ 'expectedStatus', 'range', 'token', 'query' ])
41 }
42
43 if (options.method === 'POST') {
44 return makePostBodyRequest(reqOptions)
45 }
46
47 return makeGetRequest(reqOptions)
48 }
49
50 function makeGetRequest (options: CommonRequestParams & {
51 query?: any
52 rawQuery?: string
53 }) {
54 const req = request(options.url).get(options.path)
55
56 if (options.query) req.query(options.query)
57 if (options.rawQuery) req.query(options.rawQuery)
58
59 return buildRequest(req, { contentType: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
60 }
61
62 function makeHTMLRequest (url: string, path: string) {
63 return makeGetRequest({
64 url,
65 path,
66 accept: 'text/html',
67 expectedStatus: HttpStatusCode.OK_200
68 })
69 }
70
71 function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
72 return makeGetRequest({
73 url,
74 path,
75 expectedStatus,
76 accept: 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8'
77 })
78 }
79
80 function makeDeleteRequest (options: CommonRequestParams & {
81 query?: any
82 rawQuery?: string
83 }) {
84 const req = request(options.url).delete(options.path)
85
86 if (options.query) req.query(options.query)
87 if (options.rawQuery) req.query(options.rawQuery)
88
89 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
90 }
91
92 function makeUploadRequest (options: CommonRequestParams & {
93 method?: 'POST' | 'PUT'
94
95 fields: { [ fieldName: string ]: any }
96 attaches?: { [ attachName: string ]: any | any[] }
97 }) {
98 let req = options.method === 'PUT'
99 ? request(options.url).put(options.path)
100 : request(options.url).post(options.path)
101
102 req = buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
103
104 buildFields(req, options.fields)
105
106 Object.keys(options.attaches || {}).forEach(attach => {
107 const value = options.attaches[attach]
108 if (!value) return
109
110 if (Array.isArray(value)) {
111 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
112 } else {
113 req.attach(attach, buildAbsoluteFixturePath(value))
114 }
115 })
116
117 return req
118 }
119
120 function makePostBodyRequest (options: CommonRequestParams & {
121 fields?: { [ fieldName: string ]: any }
122 }) {
123 const req = request(options.url).post(options.path)
124 .send(options.fields)
125
126 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
127 }
128
129 function makePutBodyRequest (options: {
130 url: string
131 path: string
132 token?: string
133 fields: { [ fieldName: string ]: any }
134 expectedStatus?: HttpStatusCode
135 }) {
136 const req = request(options.url).put(options.path)
137 .send(options.fields)
138
139 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
140 }
141
142 function decodeQueryString (path: string) {
143 return decode(path.split('?')[1])
144 }
145
146 // ---------------------------------------------------------------------------
147
148 function unwrapBody <T> (test: request.Test): Promise<T> {
149 return test.then(res => res.body)
150 }
151
152 function unwrapText (test: request.Test): Promise<string> {
153 return test.then(res => res.text)
154 }
155
156 function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> {
157 return test.then(res => {
158 if (res.body instanceof Buffer) {
159 try {
160 return JSON.parse(new TextDecoder().decode(res.body))
161 } catch (err) {
162 console.error('Cannot decode JSON.', { res, body: res.body instanceof Buffer ? res.body.toString() : res.body })
163 throw err
164 }
165 }
166
167 if (res.text) {
168 try {
169 return JSON.parse(res.text)
170 } catch (err) {
171 console.error('Cannot decode json', { res, text: res.text })
172 throw err
173 }
174 }
175
176 return res.body
177 })
178 }
179
180 function unwrapTextOrDecode (test: request.Test): Promise<string> {
181 return test.then(res => res.text || new TextDecoder().decode(res.body))
182 }
183
184 // ---------------------------------------------------------------------------
185
186 export {
187 makeHTMLRequest,
188 makeGetRequest,
189 decodeQueryString,
190 makeUploadRequest,
191 makePostBodyRequest,
192 makePutBodyRequest,
193 makeDeleteRequest,
194 makeRawRequest,
195 makeActivityPubGetRequest,
196 unwrapBody,
197 unwrapTextOrDecode,
198 unwrapBodyOrDecodeToJSON,
199 unwrapText
200 }
201
202 // ---------------------------------------------------------------------------
203
204 function buildRequest (req: request.Test, options: CommonRequestParams) {
205 if (options.contentType) req.set('Accept', options.contentType)
206 if (options.responseType) req.responseType(options.responseType)
207 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
208 if (options.range) req.set('Range', options.range)
209 if (options.accept) req.set('Accept', options.accept)
210 if (options.host) req.set('Host', options.host)
211 if (options.redirects) req.redirects(options.redirects)
212 if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor)
213 if (options.type) req.type(options.type)
214
215 Object.keys(options.headers || {}).forEach(name => {
216 req.set(name, options.headers[name])
217 })
218
219 return req.expect(res => {
220 if (options.expectedStatus && res.status !== options.expectedStatus) {
221 const err = new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` +
222 `\nThe server responded: "${res.body?.error ?? res.text}".\n` +
223 'You may take a closer look at the logs. To see how to do so, check out this page: ' +
224 'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs');
225
226 (err as any).res = res
227
228 throw err
229 }
230
231 return res
232 })
233 }
234
235 function buildFields (req: request.Test, fields: { [ fieldName: string ]: any }, namespace?: string) {
236 if (!fields) return
237
238 let formKey: string
239
240 for (const key of Object.keys(fields)) {
241 if (namespace) formKey = `${namespace}[${key}]`
242 else formKey = key
243
244 if (fields[key] === undefined) continue
245
246 if (Array.isArray(fields[key]) && fields[key].length === 0) {
247 req.field(key, [])
248 continue
249 }
250
251 if (fields[key] !== null && typeof fields[key] === 'object') {
252 buildFields(req, fields[key], formKey)
253 } else {
254 req.field(formKey, fields[key])
255 }
256 }
257 }