UNPKG

10.6 kBJavaScriptView Raw
1/**
2 * Kettle Test Utilities - HTTP
3 *
4 * Contains facilities for
5 * - issuing plain HTTP requests encoded declaratively as Infusion components
6 * - parsing and capturing cookies returned by these responses
7 * - assembling sequences of asynchronous fixtures suitable for execution by the IoC testing framework
8 *
9 * Copyright 2013-2015 Raising the Floor (International)
10 * Copyright 2013-2018 OCAD University
11 *
12 * Licensed under the New BSD license. You may not use this file except in
13 * compliance with this License.
14 *
15 * You may obtain a copy of the License at
16 * https://github.com/gpii/universal/LICENSE.txt
17 */
18
19"use strict";
20
21var fluid = require("infusion"),
22 kettle = fluid.registerNamespace("kettle"),
23 http = require("http"),
24 jqUnit = fluid.require("node-jqunit", require, "jqUnit");
25
26fluid.require("ws", require, "kettle.npm.ws");
27
28fluid.registerNamespace("kettle.test");
29
30/*
31 * Definitions for HTTP-based test fixtures - request classes and utilities
32 */
33
34fluid.defaults("kettle.test.cookieJar", {
35 gradeNames: ["fluid.component"],
36 members: {
37 cookie: "",
38 parser: "@expand:kettle.npm.cookieParser({that}.options.secret)"
39 }
40});
41
42fluid.defaults("kettle.test.request", {
43 gradeNames: ["fluid.component"],
44 invokers: {
45 send: "fluid.notImplemented"
46 },
47 hostname: "localhost",
48 port: 8081,
49 path: "/",
50 failOnError: true,
51 storeCookies: false,
52 termMap: {}
53});
54
55// A component which issues an HTTP request, collects its response body and parses
56// any cookies returned into a cookieJar component if one is available
57fluid.defaults("kettle.test.request.http", {
58 gradeNames: ["kettle.test.request"],
59 events: { // this will fire with the signature (data, that, {cookies, signedCookies})
60 onComplete: null,
61 // pass an options object structured as follows
62 // when firing
63 // var optsObject = {
64 // cookieJar: cookieJar,
65 // callback: callback,
66 // payload: payload,
67 // directOptions: directOptions
68 // };
69 // that.events.send.fire(optsObject);
70 send: null
71 },
72 listeners: {
73 "send.prepareRequest": {
74 funcName: "kettle.test.request.http.prepareRequest",
75 args: ["{that}", "{arguments}.0"],
76 priority: "before:sendPayload"
77 },
78 "send.sendPayload": {
79 funcName: "kettle.test.request.http.sendPayload",
80 args: ["{that}", "{arguments}.0"]
81 }
82 },
83 invokers: {
84 send: {
85 funcName: "kettle.test.request.http.send",
86 args: [
87 "{that}",
88 "{cookieJar}",
89 "{that}.events.onComplete.fire",
90 "{arguments}.0",
91 "{arguments}.1"
92 ]
93 }
94 }
95});
96
97// A variety of HTTP request that stores received cookies in a "jar" higher in the component tree
98fluid.defaults("kettle.test.request.httpCookie", {
99 gradeNames: ["kettle.test.request.http"],
100 storeCookies: true
101});
102
103kettle.test.request.http.prepareRequest = function (that, optsObject) {
104
105 var cookieJar = optsObject.cookieJar,
106 callback = optsObject.callback,
107 directOptions = optsObject.directOptions;
108
109 if (that.nativeRequest) {
110 fluid.fail("You cannot reuse a kettle.test.request.http object once it has been sent - please construct a fresh component for this request");
111 }
112
113 var requestOptions = kettle.dataSource.URL.prepareRequestOptions(that.options, cookieJar, directOptions, kettle.dataSource.URL.requestOptions, that.resolveUrl);
114
115 fluid.log("Sending a " + (requestOptions.method || "GET") + " request to: ", requestOptions.path, " on port " + requestOptions.port);
116
117 var req = that.nativeRequest = http.request(requestOptions, function (res) {
118 that.nativeResponse = res;
119 var data = "";
120 res.setEncoding("utf8");
121
122 res.on("data", function (chunk) {
123 data += chunk;
124 });
125
126 res.on("close", function (err) {
127 if (err) {
128 fluid.fail("Error making request to " + requestOptions.path + ": " + err.message);
129 }
130 });
131
132 res.on("end", function () {
133 var cookie = res.headers["set-cookie"];
134 var pseudoReq = {};
135 if (cookie && that.options.storeCookies) {
136 if (fluid.isDestroyed(cookieJar)) {
137 fluid.fail("Stored cookie in destroyed jar");
138 }
139 cookieJar.cookie = cookie;
140 // Use connect's cookie parser with set secret to parse the
141 // cookies from the kettle.server.
142 pseudoReq = {
143 headers: {
144 cookie: cookie[0]
145 }
146 };
147 // pseudoReq will get its cookies and signedCookies fields
148 // populated by the cookie parser.
149 cookieJar.parser(pseudoReq, {}, fluid.identity);
150 }
151 callback(data, that, pseudoReq);
152 });
153 });
154
155 // See http://stackoverflow.com/questions/6658761/how-to-destroy-a-node-js-http-request-connection-nicely and
156 // https://github.com/joyent/node/blob/master/lib/_http_client.js#L136 - probably unnecessary in modern node
157 req.shouldKeepAlive = false;
158
159 req.on("error", function (err) {
160 if (that.options.failOnError) {
161 jqUnit.fail("Error making request to " + requestOptions.path + ": " + err.message);
162 } else {
163 callback(JSON.stringify({
164 isError: true,
165 message: err.toString(),
166 statusCode: 500
167 }));
168 }
169 });
170};
171
172kettle.test.request.http.sendPayload = function (that, optsObject) {
173
174 var req = that.nativeRequest,
175 payload = optsObject.payload;
176 if (payload) {
177 payload = typeof payload === "string" ? payload : JSON.stringify(payload);
178 req.setHeader("Content-Type", req.getHeader("Content-Type") || "application/json");
179 req.setHeader("Content-Length", payload.length);
180 }
181
182 if (payload) {
183 req.write(payload);
184 }
185
186 req.end();
187};
188
189// Backwards-compatible forwarder to new event-based
190// workflow introduced in KETTLE-66
191kettle.test.request.http.send = function (that, cookieJar, callback, payload, directOptions) {
192
193 var optsObject = {
194 cookieJar: cookieJar,
195 callback: callback,
196 payload: payload,
197 directOptions: directOptions
198 };
199
200 that.events.send.fire(optsObject);
201};
202
203/** HTTP RESPONSE ASSERTION FUNCTIONS
204 * These all accept an argument "options" which contains the following core fields - some of these functions may also
205 * accept some extra members of options:
206 * message {String} The assertion message
207 * request {Component} The HTTP request component which has fired
208 * string {String} The received response body
209 * statusCode {Number} The expected status code (defaults to 200 if missing)
210 */
211
212/** Asserts that the status code held in options.request is equal to options.statusCode, or the supplied default
213 * @param options {Options} containing the core fields listed above
214 */
215
216kettle.test.assertResponseStatusCode = function (options, defaultCode) {
217 var statusCode = options.statusCode || defaultCode;
218 jqUnit.assertEquals(options.message + " statusCode", statusCode, options.request.nativeResponse.statusCode);
219};
220
221/** Asserts that a successful response with a JSON payload body has been received
222 * @param {Object} options - The "core fields" for response assertion as described above
223 * In addition to the core fields, options contains:
224 * @param {Object} options.expected - The expected response body as JSON
225 */
226kettle.test.assertJSONResponse = function (options) {
227 var data;
228 try {
229 data = kettle.JSON.parse(options.string);
230 } catch (e) {
231 throw kettle.upgradeError(e, "\nwhile parsing HTTP response as JSON");
232 }
233 jqUnit.assertDeepEq(options.message, options.expected, data);
234 kettle.test.assertResponseStatusCode(options, 200);
235};
236
237/** Asserts that a successful response with a plain text body or JSON body has been received
238 * @param {Object} options - The "core fields" for response assertion as described above
239 * In addition to the core fields, options contains:
240 * @param {Object} options.expected - The expected response body as JSON or plainText
241 * @param {String} options.expectedSubstring - If set, as well as `plainText`, will for a substring within the response rather than its entirety
242 * @param {Boolean} options.plainText - If `false`, will be forwarded to `kettle.test.assertJSONResponse`, otherwise, the response body will be tested as plain text
243 */
244kettle.test.assertResponse = function (options) {
245 if (options.plainText) {
246 if (options.expectedSubstring) {
247 jqUnit.assertTrue(options.message, options.string.indexOf(options.expectedSubstring) !== -1);
248 } else {
249 jqUnit.assertEquals(options.message, options.expected, options.string);
250 }
251 kettle.test.assertResponseStatusCode(options, 200);
252 } else {
253 kettle.test.assertJSONResponse(options);
254 }
255};
256
257/** Asserts that an error response of a particular structure has been received.
258 * @param {Object} options The structure to be checked
259 * @param {String} options.message - The assertion message
260 * @param {String|Array<String>} options.errorTexts - Strings which are expected to appear within the text of the "message" field of the response
261 * @param {String} options.string - The received response body
262 * @param {Component} options.request - The http request component which has fired
263 * @param {Number} options.statusCode - The expected status code (defaults to 500 if missing)
264 * @param {Boolean} options.plainText - If a plain text (and not a JSON) response is expected
265 */
266kettle.test.assertErrorResponse = function (options) {
267 var data = options.plainText ? {message: options.string} : kettle.JSON.parse(options.string);
268 if (!options.plainText) {
269 jqUnit.assertEquals(options.message + " isError field set", true, data.isError);
270 }
271 var errorTexts = fluid.makeArray(options.errorTexts);
272 fluid.each(errorTexts, function (errorText) {
273 jqUnit.assertTrue(options.message + " - message text \"" + data.message + "\" must contain " + errorText, data.message.indexOf(errorText) >= 0);
274 });
275 kettle.test.assertResponseStatusCode(options, 500);
276};