1 | const { getDebug } = require('./debug');
|
2 | const responseConfigProps = [
|
3 | 'body',
|
4 | 'headers',
|
5 | 'throws',
|
6 | 'status',
|
7 | 'redirectUrl',
|
8 | ];
|
9 |
|
10 | class ResponseBuilder {
|
11 | constructor(options) {
|
12 | this.debug = getDebug('ResponseBuilder()');
|
13 | this.debug('Response builder created with options', options);
|
14 | Object.assign(this, options);
|
15 | }
|
16 |
|
17 | exec() {
|
18 | this.debug('building response');
|
19 | this.normalizeResponseConfig();
|
20 | this.constructFetchOpts();
|
21 | this.constructResponseBody();
|
22 | return this.buildObservableResponse(
|
23 | new this.fetchMock.config.Response(this.body, this.options)
|
24 | );
|
25 | }
|
26 |
|
27 | sendAsObject() {
|
28 | if (responseConfigProps.some((prop) => this.responseConfig[prop])) {
|
29 | if (
|
30 | Object.keys(this.responseConfig).every((key) =>
|
31 | responseConfigProps.includes(key)
|
32 | )
|
33 | ) {
|
34 | return false;
|
35 | } else {
|
36 | return true;
|
37 | }
|
38 | } else {
|
39 | return true;
|
40 | }
|
41 | }
|
42 |
|
43 | normalizeResponseConfig() {
|
44 |
|
45 | if (typeof this.responseConfig === 'number') {
|
46 | this.debug('building response using status', this.responseConfig);
|
47 | this.responseConfig = {
|
48 | status: this.responseConfig,
|
49 | };
|
50 |
|
51 |
|
52 | } else if (typeof this.responseConfig === 'string' || this.sendAsObject()) {
|
53 | this.debug('building text response from', this.responseConfig);
|
54 | this.responseConfig = {
|
55 | body: this.responseConfig,
|
56 | };
|
57 | }
|
58 | }
|
59 |
|
60 | validateStatus(status) {
|
61 | if (!status) {
|
62 | this.debug('No status provided. Defaulting to 200');
|
63 | return 200;
|
64 | }
|
65 |
|
66 | if (
|
67 | (typeof status === 'number' &&
|
68 | parseInt(status, 10) !== status &&
|
69 | status >= 200) ||
|
70 | status < 600
|
71 | ) {
|
72 | this.debug('Valid status provided', status);
|
73 | return status;
|
74 | }
|
75 |
|
76 | throw new TypeError(`fetch-mock: Invalid status ${status} passed on response object.
|
77 | To respond with a JSON object that has status as a property assign the object to body
|
78 | e.g. {"body": {"status: "registered"}}`);
|
79 | }
|
80 |
|
81 | constructFetchOpts() {
|
82 | this.options = this.responseConfig.options || {};
|
83 | this.options.url = this.responseConfig.redirectUrl || this.url;
|
84 | this.options.status = this.validateStatus(this.responseConfig.status);
|
85 | this.options.statusText = this.fetchMock.statusTextMap[
|
86 | String(this.options.status)
|
87 | ];
|
88 |
|
89 |
|
90 |
|
91 | this.options.headers = new this.fetchMock.config.Headers(
|
92 | this.responseConfig.headers || {}
|
93 | );
|
94 | }
|
95 |
|
96 | getOption(name) {
|
97 | return this.fetchMock.getOption(name, this.route);
|
98 | }
|
99 |
|
100 | convertToJson() {
|
101 |
|
102 | if (
|
103 | this.getOption('sendAsJson') &&
|
104 | this.responseConfig.body != null &&
|
105 | typeof this.body === 'object'
|
106 | ) {
|
107 | this.debug('Stringifying JSON response body');
|
108 | this.body = JSON.stringify(this.body);
|
109 | if (!this.options.headers.has('Content-Type')) {
|
110 | this.options.headers.set('Content-Type', 'application/json');
|
111 | }
|
112 | }
|
113 | }
|
114 |
|
115 | setContentLength() {
|
116 |
|
117 | if (
|
118 | this.getOption('includeContentLength') &&
|
119 | typeof this.body === 'string' &&
|
120 | !this.options.headers.has('Content-Length')
|
121 | ) {
|
122 | this.debug('Setting content-length header:', this.body.length.toString());
|
123 | this.options.headers.set('Content-Length', this.body.length.toString());
|
124 | }
|
125 | }
|
126 |
|
127 | constructResponseBody() {
|
128 |
|
129 | this.body = this.responseConfig.body;
|
130 | this.convertToJson();
|
131 | this.setContentLength();
|
132 |
|
133 |
|
134 |
|
135 | if (this.Stream) {
|
136 | this.debug('Creating response stream');
|
137 | const stream = new this.Stream.Readable();
|
138 | if (this.body != null) {
|
139 | stream.push(this.body, 'utf-8');
|
140 | }
|
141 | stream.push(null);
|
142 | this.body = stream;
|
143 | }
|
144 | this.body = this.body;
|
145 | }
|
146 |
|
147 | buildObservableResponse(response) {
|
148 | const fetchMock = this.fetchMock;
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | this.debug('Wrappipng Response in ES proxy for observability');
|
154 | return new Proxy(response, {
|
155 | get: (originalResponse, name) => {
|
156 | if (this.responseConfig.redirectUrl) {
|
157 | if (name === 'url') {
|
158 | this.debug(
|
159 | 'Retrieving redirect url',
|
160 | this.responseConfig.redirectUrl
|
161 | );
|
162 | return this.responseConfig.redirectUrl;
|
163 | }
|
164 |
|
165 | if (name === 'redirected') {
|
166 | this.debug('Retrieving redirected status', true);
|
167 | return true;
|
168 | }
|
169 | }
|
170 |
|
171 | if (typeof originalResponse[name] === 'function') {
|
172 | this.debug('Wrapping body promises in ES proxies for observability');
|
173 | return new Proxy(originalResponse[name], {
|
174 | apply: (func, thisArg, args) => {
|
175 | this.debug(`Calling res.${name}`);
|
176 | const result = func.apply(response, args);
|
177 | if (result.then) {
|
178 | fetchMock._holdingPromises.push(result.catch(() => null));
|
179 | }
|
180 | return result;
|
181 | },
|
182 | });
|
183 | }
|
184 |
|
185 | return originalResponse[name];
|
186 | },
|
187 | });
|
188 | }
|
189 | }
|
190 |
|
191 | module.exports = (options) => new ResponseBuilder(options).exec();
|