UNPKG

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