UNPKG

5.44 kBJavaScriptView Raw
1const { getDebug } = require('./debug');
2const responseConfigProps = [
3 'body',
4 'headers',
5 'throws',
6 'status',
7 'redirectUrl',
8];
9
10class 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 // If the response config looks like a status, start to generate a simple response
45 if (typeof this.responseConfig === 'number') {
46 this.debug('building response using status', this.responseConfig);
47 this.responseConfig = {
48 status: this.responseConfig,
49 };
50 // If the response config is not an object, or is an object that doesn't use
51 // any reserved properties, assume it is meant to be the body of the response
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.
77To respond with a JSON object that has status as a property assign the object to body
78e.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 // Set up response headers. The empty object is to cope with
89 // new Headers(undefined) throwing in Chrome
90 // https://code.google.com/p/chromium/issues/detail?id=335871
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 // convert to json if we need to
102 if (
103 this.getOption('sendAsJson') &&
104 this.responseConfig.body != null && //eslint-disable-line
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 // add a Content-Length header if we need to
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 // start to construct the body
129 this.body = this.responseConfig.body;
130 this.convertToJson();
131 this.setContentLength();
132
133 // On the server we need to manually construct the readable stream for the
134 // Response object (on the client this done automatically)
135 if (this.Stream) {
136 this.debug('Creating response stream');
137 const stream = new this.Stream.Readable();
138 if (this.body != null) { //eslint-disable-line
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 // Using a proxy means we can set properties that may not be writable on
151 // the original Response. It also means we can track the resolution of
152 // promises returned by res.json(), res.text() etc
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
191module.exports = (options) => new ResponseBuilder(options).exec();