Challenge #10: Set the Response

There’s one hardcoded value still remaining: the spy server’s response. In this challenge, you’ll add a method for setting the response.

Instructions

  1. Add a spyServer.setResponse() method with the following signature:
    spyServer.setResponse({
    	status,   // the status to use for the next server response
    	header,   // the headers to use for the next server response
    	body,     // the body to use for the next server response
    }
  2. Modify spyServer.reset() to call setResponse with the following defaults:
    spyServer.setResponse({
    	status: 501,    // not implemented
    	header: {},
    	body: "SpyServer response not specified",
    });
  3. Modify the spy server’s response to use the values set in spyServer.setResponse(). The test should fail.
  4. Update the test to call spyServer.setResponse() as follows. The test should pass.
    spyServer.setResponse({
    	status: 999,
    	headers: {
    		myResponseHeader: "myResponseValue",
    	},
    	body: "my response body"
    });

Remember to commit your changes when you’re done.

API Documentation

Object.entries(object).forEach(fn);

Run fn() on every key/value pair in object. The function parameter is a [ key, value ] array. Use it with array destructuring like this:

const myObject = {
	a: "one",
	b: "two",
};

Object.entries(myObject).forEach(([ key, value ] => {
	console.log(`${key} --> ${value}`);
});

// outputs:
// a --> one
// b --> two
  • fn (([ key, value ]) => void) - the function to call on each key/value pair

JavaScript Primers

Hints

1
Start by introducing the spyServer.setResponse() method.
Store the response in an instance variable for later.
class SpyServer {

	constructor() {
		this.reset();
	}

	reset() {
		this.lastRequest = null;
	}

	setResponse(response) {
		this._response = response;
	}

	// ...
}

TypeScript requires type declarations and an interface:

interface SpyServerResponse {
	status: number,
	headers: HttpHeaders,
	body: string,
}

class SpyServer {

	_server?: http.Server;
	_response?: SpyServerResponse;
	lastRequest!: SpyServerRequest | null;

	constructor() {
		this.reset();
	}

	reset() {
		this.lastRequest = null;
	}

	setResponse(response: SpyServerResponse) {
		this._response = response;
	}

	// ...
}
2
Update spyServer.reset() to call setResponse().
reset() {
	this.lastRequest = null;
	this.setResponse({
		status: 501,
		headers: {},
		body: "SpyServer response not specified",
	});
}

In TypeScript, because reset() is called from the constructor, this allows you to declare that the _response variable will never be undefined:

class SpyServer {

	_server?: http.Server;
	_response!: SpyServerResponse;
	lastRequest!: SpyServerRequest | null;

	// ...
}
3
Modify the spy server to use the values from setResponse().
The headers are a bit more complicated, so do the status and body first.
The code to change is in the serverRequest.on("end", ...) event handler.
async startAsync() {
	this._server = http.createServer();

	await new Promise((resolve, reject) => {
		this._server.listen(PORT);
		this._server.on("listening", () => resolve());
	});

	this._server.on("request", (serverRequest, serverResponse) => {
		let body = "";
		serverRequest.on("data", (chunk) => {
			body += chunk;
		});

		serverRequest.on("end", () => {
			this.lastRequest = {
				method: serverRequest.method,
				path: serverRequest.url,
				headers: serverRequest.headers,
				body,
			};  // add 'as SpyServerRequest' in TypeScript

			serverResponse.statusCode = this._response.status;
			serverResponse.setHeader("myResponseHeader", "myResponseValue");
			serverResponse.end(this._response.body);
		});
	});
}
4
The test should fail.

That’s because the test isn’t setting a response. You’ll get to it when you’re done modifying the spy server.

5
Modify the spy server to use the headers from setResponse().
You’ll need to loop over all the headers and call serverResponse.setHeader() for each one.
You can use Object.entries().forEach() for that.
async startAsync() {
	this._server = http.createServer();

	await new Promise((resolve, reject) => {
		this._server.listen(PORT);
		this._server.on("listening", () => resolve());
	});

	this._server.on("request", (serverRequest, serverResponse) => {
		let body = "";
		serverRequest.on("data", (chunk) => {
			body += chunk;
		});

		serverRequest.on("end", () => {
			this.lastRequest = {
				method: serverRequest.method,
				path: serverRequest.url,
				headers: serverRequest.headers,
				body,
			};  // add 'as SpyServerRequest' in TypeScript

			serverResponse.statusCode = this._response.status;
			Object.entries(this._response.headers).forEach(([ key, value ]) => {
				serverResponse.setHeader(key, value);
			});
			serverResponse.end(this._response.body);
		});
	});
}

(The forEach() arrow function uses array destructuring.)

6
You’re ready to fix the test.
All you have to do is call spyServer.setResponse().
You need to set the response before making the request.
it("performs request", async () => {
	spyServer.setResponse({
		status: 999,
		headers: {
			myResponseHeader: "myResponseValue",
		},
		body: "my response body"
	});

	const clientRequest = http.request({
		host: HOST,
		port: PORT,
		method: "POST",
		path: "/my/path",
		headers: {
			myRequestHeader: "myRequestValue",
		},
	});
	clientRequest.end("my request body");

	const response = await new Promise((resolve, reject) => {  // add <HttpClientResponse> in TypeScript
		clientRequest.on("response", (clientResponse) => {
			let body = "";
			clientResponse.on("data", (chunk) => {
				body += chunk;
			});

			clientResponse.on("end", () => {
				resolve({
					status: clientResponse.statusCode,  // add 'as number' in TypeScript
					headers: clientResponse.headers,    // add 'as HttpHeaders' in TypeScript
					body,
				});
			});
		});
	});

	assert.deepEqual(spyServer.lastRequest, {
		method: "POST",
		path: "/my/path",
		headers: {
			myrequestheader: "myRequestValue",
			host: `${HOST}:${PORT}`,
			connection: "close",
			"content-length": "15",
		},
		body: "my request body",
	});

	delete response.headers.date;
	assert.deepEqual(response, {
		status: 999,
		headers: {
			myresponseheader: "myResponseValue",
			connection: "close",
			"content-length": "16",
		},
		body: "my response body",
	});
});

Complete Solution

Test code (JavaScript):
it("performs request", async () => {
	spyServer.setResponse({
		status: 999,
		headers: {
			myResponseHeader: "myResponseValue",
		},
		body: "my response body"
	});

	const clientRequest = http.request({
		host: HOST,
		port: PORT,
		method: "POST",
		path: "/my/path",
		headers: {
			myRequestHeader: "myRequestValue",
		},
	});
	clientRequest.end("my request body");

	const response = await new Promise((resolve, reject) => {
		clientRequest.on("response", (clientResponse) => {
			let body = "";
			clientResponse.on("data", (chunk) => {
				body += chunk;
			});

			clientResponse.on("end", () => {
				resolve({
					status: clientResponse.statusCode,
					headers: clientResponse.headers,
					body,
				});
			});
		});
	});

	assert.deepEqual(spyServer.lastRequest, {
		method: "POST",
		path: "/my/path",
		headers: {
			myrequestheader: "myRequestValue",
			host: `${HOST}:${PORT}`,
			connection: "close",
			"content-length": "15",
		},
		body: "my request body",
	});

	delete response.headers.date;
	assert.deepEqual(response, {
		status: 999,
		headers: {
			myresponseheader: "myResponseValue",
			connection: "close",
			"content-length": "16",
		},
		body: "my response body",
	});
});

// ...

class SpyServer {

	constructor() {
		this.reset();
	}

	reset() {
		this.lastRequest = null;
		this.setResponse({
			status: 501,
			headers: {},
			body: "SpyServer response not specified",
		});
	}

	setResponse(response) {
		this._response = response;
	}

	async startAsync() {
		this._server = http.createServer();

		await new Promise((resolve, reject) => {
			this._server.listen(PORT);
			this._server.on("listening", () => resolve());
		});

		this._server.on("request", (serverRequest, serverResponse) => {
			let body = "";
			serverRequest.on("data", (chunk) => {
				body += chunk;
			});

			serverRequest.on("end", () => {
				this.lastRequest = {
					method: serverRequest.method,
					path: serverRequest.url,
					headers: serverRequest.headers,
					body,
				};

				serverResponse.statusCode = this._response.status;
				Object.entries(this._response.headers).forEach(([ key, value ]) => {
					serverResponse.setHeader(key, value);
				});
				serverResponse.end(this._response.body);
			});
		});
	}

	async stopAsync() {
		await new Promise((resolve, reject) => {
			this._server.close();
			this._server.on("close", () => resolve());
		});
	}

}
Test code (TypeScript):
it("performs request", async () => {
	spyServer.setResponse({
		status: 999,
		headers: {
			myResponseHeader: "myResponseValue",
		},
		body: "my response body"
	});

	const clientRequest = http.request({
		host: HOST,
		port: PORT,
		method: "POST",
		path: "/my/path",
		headers: {
			myRequestHeader: "myRequestValue",
		},
	});
	clientRequest.end("my request body");

	const response = await new Promise<HttpClientResponse>((resolve, reject) => {
		clientRequest.on("response", (clientResponse) => {
			let body = "";
			clientResponse.on("data", (chunk) => {
				body += chunk;
			});

			clientResponse.on("end", () => {
				resolve({
					status: clientResponse.statusCode as number,
					headers: clientResponse.headers as HttpHeaders,
					body,
				});
			});
		});
	});

	assert.deepEqual(spyServer.lastRequest, {
		method: "POST",
		path: "/my/path",
		headers: {
			myrequestheader: "myRequestValue",
			host: `${HOST}:${PORT}`,
			connection: "close",
			"content-length": "15",
		},
		body: "my request body",
	});

	delete response.headers.date;
	assert.deepEqual(response, {
		status: 999,
		headers: {
			myresponseheader: "myResponseValue",
			connection: "close",
			"content-length": "16",
		},
		body: "my response body",
	});
});

// ...

interface SpyServerRequest {
	method: string,
	path: string,
	headers: NodeIncomingHttpHeaders,
	body: string,
}

interface SpyServerResponse {
	status: number,
	headers: HttpHeaders,
	body: string,
}

class SpyServer {

	_server?: http.Server;
	_response!: SpyServerResponse;
	lastRequest!: SpyServerRequest | null;

	constructor() {
		this.reset();
	}

	reset() {
		this.lastRequest = null;
		this.setResponse({
			status: 501,
			headers: {},
			body: "SpyServer response not specified",
		});
	}

	setResponse(response: SpyServerResponse) {
		this._response = response;
	}

	async startAsync() {
		await new Promise<void>((resolve, reject) => {
			this._server = http.createServer();

			this._server.listen(PORT);
			this._server.on("listening", () => resolve());

			this._server.on("request", (serverRequest, serverResponse) => {
				let body = "";
				serverRequest.on("data", (chunk) => {
					body += chunk;
				});

				serverRequest.on("end", () => {
					this.lastRequest = {
						method: serverRequest.method,
						path: serverRequest.url,
						headers: serverRequest.headers,
						body,
					} as SpyServerRequest;

					serverResponse.statusCode = this._response.status;
					Object.entries(this._response.headers).forEach(([ key, value ]) => {
						serverResponse.setHeader(key, value);
					});
					serverResponse.end(this._response.body);
				});
			});
		});
	}

	async stopAsync() {
		await new Promise<void>((resolve, reject) => {
			if (this._server === undefined) return reject(new Error("SpyServer has not been started"));

			this._server.close();
			this._server.on("close", () => resolve());
		});
	}

}

No production code yet.

Next challenge

Return to module overview