Challenge #10: Edge Cases

Now that you’ve created the helper functions you need, dealing with the remaining edge cases should be fairly easy. In this challenge, you’ll take care of all of them.

Instructions

  1. Implement the "fails gracefully if body doesn't exist" test and make it pass.
    • response body: ""
    • error message: "Body missing from ROT-13 service".
  2. Implement the "fails gracefully if body is unparseable" test and make it pass.
    • response body: "xxx"
    • error message: "Unparseable body from ROT-13 service: Unexpected token x in JSON at position 0"
  3. Implement the "fails gracefully if body has unexpected value" test and make it pass.
    • response body: JSON.stringify({ foo: "bar" })
    • error message: "Unexpected body from ROT-13 service: body.transformed must be a string, but it was undefined"
  4. Implement the "doesn't fail when body has more fields than we expect" test and make it pass.
    • response body: JSON.stringify({ transformed: "response", foo: "bar" })
    • should not throw error

Remember to commit your changes when you’re done.

API Documentation

await assert.doesNotThrowAsync(fnAsync);

Assert that an asynchronous function does not throw an error. Use it like this:

await assert.doesNotThrowAsync(
	() => transformAsync(...),    // note: NO await here
);
  • fnAsync (function) - the function to run
const message = error.message

Get the error message from an Error exception.

  • returns message (string) - the error message
const typeError = type.check(variable, type, { name, allowExtraKeys })

Check the type of a variable at runtime. The way to specify the type is a bit complicated, but for the purpose of this challenge, you only need to pass in the RESPONSE_TYPE constant.

  • variable (any) - the variable to check
  • type (special) - the expected type
  • optional name (string) - the name to use for the variable in error messages; defaults to "argument"
  • optional allowExtraKeys (boolean) - whether objects can have more keys than specified in the type; defaults to false
  • returns typeError (string | null) - null if the type matches, or a string with an explanatory error message if the type doesn’t match
const RESPONSE_TYPE = { transformed: String };

This constant is available in the production code. Use it to check the type of the ROT-13 service’s response body.

JavaScript Primers

Hints

"fails gracefully if body doesn't exist":

1
Implement the test.
You can use assertFailureAsync(). Don’t forget to await it.
it("fails gracefully if body doesn't exist", async () => {
	await assertFailureAsync({
		rot13ServiceBody: "",
		message: "Body missing from ROT-13 service",
	});
});
2
The test is ready to run.
It should fail with "Unexpected end of JSON input".

That’s because production code is trying to JSON.parse() an empty string.

3
Check that the response body isn’t empty before parsing it.
It’s in response.body.
async transformAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	const response = await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});

	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	const parsedBody = JSON.parse(response.body);
	return parsedBody.transformed;
}

"fails gracefully if body is unparseable":

4
Implement the test.
it("fails gracefully if body is unparseable", async () => {
	await assertFailureAsync({
		rot13ServiceBody: "xxx",
		message: "Unparseable body from ROT-13 service: Unexpected token x in JSON at position 0",
	});
});
5
The test is ready to run.
It should fail with an unexpected token error.

That’s because JSON.parse() is throwing an exception and the production code isn’t handling it.

6
Catch and rethrow JSON.parse() exceptions.
async transformAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	const response = await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});

	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch (err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	return parsedBody.transformed;
}

"fails gracefully if body has unexpected value":

7
Implement the test.
it("fails gracefully if body has unexpected value", async () => {
	await assertFailureAsync({
		rot13ServiceBody: JSON.stringify({ foo: "bar" }),
		message: "Unexpected body from ROT-13 service: body.transformed must be a string, but it was undefined",
	});
});
8
The test is ready to run.
It should fail, saying it expected an exception.

This is because rot13Client.transformAsync() isn’t checking the type of the parsed body.

9
Check the type of the parsed body.
You can use type.check() to do that.
async transformAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	const response = await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});

	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch (err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body" });
	if (typeError !== null) {
		throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
	}

	return parsedBody.transformed;
}

"doesn't fail when body has more fields than we expect":

10
Implement the test. This test should check that there isn’t an error.
You can use assert.doesNotThrowAsync() to do that. Don’t forget to await it.
it("doesn't fail when body has more fields than we expect", async () => {
	const rot13ServiceBody = JSON.stringify({ transformed: "response", foo: "bar" });
	await assert.doesNotThrowAsync(
		() => transformAsync({ rot13ServiceBody }),
	);
});
11
The test is ready to run.
It should fail, saying it got an exception.

This is because type.check() is finding parameters that aren’t specified in the type.

12
Loosen the strictness of the parsed body’s type checking.
You can use type.check()’s allowExtraKeys parameter to do that.
async transformAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	const response = await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});

	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch (err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
	if (typeError !== null) {
		throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
	}

	return parsedBody.transformed;
}

Refactor:

13
rot13Client.transformAsync() has gotten a bit unwieldy. Refactor it to make it more clear.
Private methods (#) may be helpful.
There’s no one right answer. This was my solution:
async transformAsync(port, text, correlationId) {
	const response = await this.#performRequestAsync(port, text, correlationId);
	return this.#parseResponse(response, port);
}

async #performRequestAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	return await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});
}

#parseResponse(response, port) {
	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch(err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
	if (typeError !== null) {
		throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
	}

	return parsedBody.transformed;
}

TypeScript requires type declarations for the extracted functions:

async #performRequestAsync(port: number, text: string, correlationId: string) {
	this._listener.emit({ port, text, correlationId });

	return await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});
}

#parseResponse(response: HttpClientResponse, port: number) {
	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch(err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
	if (typeError !== null) {
		throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
	}

	return parsedBody.transformed;
}

Complete Solution

Test code:
it("fails gracefully if body doesn't exist", async () => {
	await assertFailureAsync({
		rot13ServiceBody: "",
		message: "Body missing from ROT-13 service",
	});
});

it("fails gracefully if body is unparseable", async () => {
	await assertFailureAsync({
		rot13ServiceBody: "xxx",
		message: "Unparseable body from ROT-13 service: Unexpected token x in JSON at position 0",
	});
});

it("fails gracefully if body has unexpected value", async () => {
	await assertFailureAsync({
		rot13ServiceBody: JSON.stringify({ foo: "bar" }),
		message: "Unexpected body from ROT-13 service: body.transformed must be a string, but it was undefined",
	});
});

it("doesn't fail when body has more fields than we expect", async () => {
	const rot13ServiceBody = JSON.stringify({ transformed: "response", foo: "bar" });
	await assert.doesNotThrowAsync(
		() => transformAsync({ rot13ServiceBody }),
	);
});
Production code (JavaScript):
async transformAsync(port, text, correlationId) {
	const response = await this.#performRequestAsync(port, text, correlationId);
	return this.#parseResponse(response, port);
}

async #performRequestAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	return await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});
}

#parseResponse(response, port) {
	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch(err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
	if (typeError !== null) {
		throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
	}

	return parsedBody.transformed;
}
Production code (TypeScript):
async transformAsync(
	port: number,
	text: string,
	correlationId: string,
): Promise<string> {
	const response = await this.#performRequestAsync(port, text, correlationId);
	return this.#parseResponse(response, port);
}

async #performRequestAsync(port: number, text: string, correlationId: string) {
	this._listener.emit({ port, text, correlationId });

	return await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: "/rot13/transform",
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});
}

#parseResponse(response: HttpClientResponse, port: number) {
	if (response.status !== 200) {
		throwError("Unexpected status from ROT-13 service", port, response);
	}
	if (response.body === "") {
		throwError("Body missing from ROT-13 service", port, response);
	}

	let parsedBody;
	try {
		parsedBody = JSON.parse(response.body);
	}
	catch(err) {
		throwError(`Unparseable body from ROT-13 service: ${err.message}`, port, response);
	}

	const typeError = type.check(parsedBody, RESPONSE_TYPE, { name: "body", allowExtraKeys: true });
	if (typeError !== null) {
		throwError(`Unexpected body from ROT-13 service: ${typeError}`, port, response);
	}

	return parsedBody.transformed;
}

Next challenge

Return to module overview