create-request-mock.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import {Socket} from 'net';
  2. import {TLSSocket} from 'tls';
  3. import {IncomingMessage} from 'http';
  4. import queryString from 'querystring';
  5. import {Errorf} from '@e22m4u/js-format';
  6. import {isReadableStream} from './is-readable-stream.js';
  7. import {createCookieString} from './create-cookie-string.js';
  8. import {BUFFER_ENCODING_LIST} from './fetch-request-body.js';
  9. /**
  10. * @typedef {{
  11. * host?: string;
  12. * method?: string;
  13. * secure?: boolean;
  14. * path?: string;
  15. * query?: object;
  16. * cookie?: object;
  17. * headers?: object;
  18. * body?: string;
  19. * stream?: import('stream').Readable;
  20. * encoding?: import('buffer').BufferEncoding;
  21. * }} RequestPatch
  22. */
  23. /**
  24. * Create request mock.
  25. *
  26. * @param {RequestPatch} patch
  27. * @returns {import('http').IncomingMessage}
  28. */
  29. export function createRequestMock(patch) {
  30. if ((patch != null && typeof patch !== 'object') || Array.isArray(patch)) {
  31. throw new Errorf(
  32. 'The first parameter of "createRequestMock" ' +
  33. 'should be an Object, but %v given.',
  34. patch,
  35. );
  36. }
  37. patch = patch || {};
  38. if (patch.host != null && typeof patch.host !== 'string')
  39. throw new Errorf(
  40. 'The parameter "host" of "createRequestMock" ' +
  41. 'should be a String, but %v given.',
  42. patch.host,
  43. );
  44. if (patch.method != null && typeof patch.method !== 'string')
  45. throw new Errorf(
  46. 'The parameter "method" of "createRequestMock" ' +
  47. 'should be a String, but %v given.',
  48. patch.method,
  49. );
  50. if (patch.secure != null && typeof patch.secure !== 'boolean')
  51. throw new Errorf(
  52. 'The parameter "secure" of "createRequestMock" ' +
  53. 'should be a Boolean, but %v given.',
  54. patch.secure,
  55. );
  56. if (patch.path != null && typeof patch.path !== 'string')
  57. throw new Errorf(
  58. 'The parameter "path" of "createRequestMock" ' +
  59. 'should be a String, but %v given.',
  60. patch.path,
  61. );
  62. if (
  63. (patch.query != null &&
  64. typeof patch.query !== 'object' &&
  65. typeof patch.query !== 'string') ||
  66. Array.isArray(patch.query)
  67. ) {
  68. throw new Errorf(
  69. 'The parameter "query" of "createRequestMock" ' +
  70. 'should be a String or Object, but %v given.',
  71. patch.query,
  72. );
  73. }
  74. if (
  75. (patch.cookie != null &&
  76. typeof patch.cookie !== 'string' &&
  77. typeof patch.cookie !== 'object') ||
  78. Array.isArray(patch.cookie)
  79. ) {
  80. throw new Errorf(
  81. 'The parameter "cookie" of "createRequestMock" ' +
  82. 'should be a String or Object, but %v given.',
  83. patch.cookie,
  84. );
  85. }
  86. if (
  87. (patch.headers != null && typeof patch.headers !== 'object') ||
  88. Array.isArray(patch.headers)
  89. ) {
  90. throw new Errorf(
  91. 'The parameter "headers" of "createRequestMock" ' +
  92. 'should be an Object, but %v given.',
  93. patch.headers,
  94. );
  95. }
  96. if (patch.stream != null && !isReadableStream(patch.stream))
  97. throw new Errorf(
  98. 'The parameter "stream" of "createRequestMock" ' +
  99. 'should be a Stream, but %v given.',
  100. patch.stream,
  101. );
  102. if (patch.encoding != null) {
  103. if (typeof patch.encoding !== 'string')
  104. throw new Errorf(
  105. 'The parameter "encoding" of "createRequestMock" ' +
  106. 'should be a String, but %v given.',
  107. patch.encoding,
  108. );
  109. if (!BUFFER_ENCODING_LIST.includes(patch.encoding))
  110. throw new Errorf('Buffer encoding %v is not supported.', patch.encoding);
  111. }
  112. // если передан поток, выполняется
  113. // проверка на несовместимые опции
  114. if (patch.stream) {
  115. if (patch.secure != null)
  116. throw new Errorf(
  117. 'The "createRequestMock" does not allow specifying the ' +
  118. '"stream" and "secure" options simultaneously.',
  119. );
  120. if (patch.body != null)
  121. throw new Errorf(
  122. 'The "createRequestMock" does not allow specifying the ' +
  123. '"stream" and "body" options simultaneously.',
  124. );
  125. if (patch.encoding != null)
  126. throw new Errorf(
  127. 'The "createRequestMock" does not allow specifying the ' +
  128. '"stream" and "encoding" options simultaneously.',
  129. );
  130. }
  131. // если передан поток, он будет использован
  132. // в качестве объекта запроса, в противном
  133. // случае создается новый
  134. const req =
  135. patch.stream ||
  136. createRequestStream(patch.secure, patch.body, patch.encoding);
  137. req.url = createRequestUrl(patch.path || '/', patch.query);
  138. req.headers = createRequestHeaders(
  139. patch.host,
  140. patch.secure,
  141. patch.body,
  142. patch.cookie,
  143. patch.encoding,
  144. patch.headers,
  145. );
  146. req.method = (patch.method || 'get').toUpperCase();
  147. return req;
  148. }
  149. /**
  150. * Create request stream.
  151. *
  152. * @param {boolean|null|undefined} secure
  153. * @param {*} body
  154. * @param {import('buffer').BufferEncoding|null|undefined} encoding
  155. * @returns {import('http').IncomingMessage}
  156. */
  157. function createRequestStream(secure, body, encoding) {
  158. if (encoding != null && typeof encoding !== 'string')
  159. throw new Errorf(
  160. 'The parameter "encoding" of "createRequestStream" ' +
  161. 'should be a String, but %v given.',
  162. encoding,
  163. );
  164. encoding = encoding || 'utf-8';
  165. // для безопасного подключения
  166. // использует обертка TLSSocket
  167. let socket = new Socket();
  168. if (secure) socket = new TLSSocket(socket);
  169. const req = new IncomingMessage(socket);
  170. // тело запроса должно являться
  171. // строкой или бинарными данными
  172. if (body != null) {
  173. if (typeof body === 'string') {
  174. req.push(body, encoding);
  175. } else if (Buffer.isBuffer(body)) {
  176. req.push(body);
  177. } else {
  178. req.push(JSON.stringify(body));
  179. }
  180. }
  181. // передача "null" определяет
  182. // конец данных
  183. req.push(null);
  184. return req;
  185. }
  186. /**
  187. * Create request url.
  188. *
  189. * @param {string} path
  190. * @param {string|object|null|undefined} query
  191. * @returns {string}
  192. */
  193. function createRequestUrl(path, query) {
  194. if (typeof path !== 'string')
  195. throw new Errorf(
  196. 'The parameter "path" of "createRequestUrl" ' +
  197. 'should be a String, but %v given.',
  198. path,
  199. );
  200. if (
  201. (query != null && typeof query !== 'string' && typeof query !== 'object') ||
  202. Array.isArray(query)
  203. ) {
  204. throw new Errorf(
  205. 'The parameter "query" of "createRequestUrl" ' +
  206. 'should be a String or Object, but %v given.',
  207. query,
  208. );
  209. }
  210. let url = ('/' + path).replace('//', '/');
  211. if (typeof query === 'object') {
  212. const qs = queryString.stringify(query);
  213. if (qs) url += `?${qs}`;
  214. } else if (typeof query === 'string') {
  215. url += `?${query.replace(/^\?/, '')}`;
  216. }
  217. return url;
  218. }
  219. /**
  220. * Create request headers.
  221. *
  222. * @param {string|null|undefined} host
  223. * @param {boolean|null|undefined} secure
  224. * @param {*} body
  225. * @param {string|object|null|undefined} cookie
  226. * @param {import('buffer').BufferEncoding|null|undefined} encoding
  227. * @param {object|null|undefined} headers
  228. * @returns {object}
  229. */
  230. function createRequestHeaders(host, secure, body, cookie, encoding, headers) {
  231. if (host != null && typeof host !== 'string')
  232. throw new Errorf(
  233. 'The parameter "host" of "createRequestHeaders" ' +
  234. 'a non-empty String, but %v given.',
  235. host,
  236. );
  237. host = host || 'localhost';
  238. if (secure != null && typeof secure !== 'boolean')
  239. throw new Errorf(
  240. 'The parameter "secure" of "createRequestHeaders" ' +
  241. 'should be a String, but %v given.',
  242. secure,
  243. );
  244. secure = Boolean(secure);
  245. if (
  246. (cookie != null &&
  247. typeof cookie !== 'object' &&
  248. typeof cookie !== 'string') ||
  249. Array.isArray(cookie)
  250. ) {
  251. throw new Errorf(
  252. 'The parameter "cookie" of "createRequestHeaders" ' +
  253. 'should be a String or Object, but %v given.',
  254. cookie,
  255. );
  256. }
  257. if (
  258. (headers != null && typeof headers !== 'object') ||
  259. Array.isArray(headers)
  260. ) {
  261. throw new Errorf(
  262. 'The parameter "headers" of "createRequestHeaders" ' +
  263. 'should be an Object, but %v given.',
  264. headers,
  265. );
  266. }
  267. headers = headers || {};
  268. if (encoding != null && typeof encoding !== 'string')
  269. throw new Errorf(
  270. 'The parameter "encoding" of "createRequestHeaders" ' +
  271. 'should be a String, but %v given.',
  272. encoding,
  273. );
  274. encoding = encoding || 'utf-8';
  275. const obj = {...headers};
  276. obj['host'] = host;
  277. if (secure) obj['x-forwarded-proto'] = 'https';
  278. // формирование заголовка Cookie
  279. // из строки или объекта
  280. if (cookie != null) {
  281. if (typeof cookie === 'string') {
  282. obj['cookie'] = obj['cookie'] ? obj['cookie'] : '';
  283. obj['cookie'] += cookie;
  284. } else if (typeof cookie === 'object') {
  285. obj['cookie'] = obj['cookie'] ? obj['cookie'] : '';
  286. obj['cookie'] += createCookieString(cookie);
  287. }
  288. }
  289. // установка заголовка "content-type"
  290. // в зависимости от тела запроса
  291. if (obj['content-type'] == null) {
  292. if (typeof body === 'string') {
  293. obj['content-type'] = 'text/plain';
  294. } else if (Buffer.isBuffer(body)) {
  295. obj['content-type'] = 'application/octet-stream';
  296. } else if (
  297. typeof body === 'object' ||
  298. typeof body === 'boolean' ||
  299. typeof body === 'number'
  300. ) {
  301. obj['content-type'] = 'application/json';
  302. }
  303. }
  304. // подсчет количества байт тела
  305. // для заголовка "content-length"
  306. if (body != null && obj['content-length'] == null) {
  307. if (typeof body === 'string') {
  308. const length = Buffer.byteLength(body, encoding);
  309. obj['content-length'] = String(length);
  310. } else if (Buffer.isBuffer(body)) {
  311. const length = Buffer.byteLength(body);
  312. obj['content-length'] = String(length);
  313. } else if (
  314. typeof body === 'object' ||
  315. typeof body === 'boolean' ||
  316. typeof body === 'number'
  317. ) {
  318. const json = JSON.stringify(body);
  319. const length = Buffer.byteLength(json, encoding);
  320. obj['content-length'] = String(length);
  321. }
  322. }
  323. return obj;
  324. }