Challenge #5: Logging

Your existing tests check that the code handles the “happy path” of POST requests, but that’s just the tip of the iceberg. The real work is in handling edge cases. You’ll start by handling the case where the form field is missing.

Instructions

1. Implement the "logs warning when form field not found (and treats request like GET)" test:

  1. Configure the request body to be an empty string.
  2. Assert that HomePageController.postAsync() returns homePageView.homePage().
  3. Assert that it doesn’t call the ROT-13 service.
  4. Assert that it writes the following log message:
    {
    	alert: "monitor",
    	endpoint: "/",
    	method: "POST",
    	message: "form parse error",
    	error: "'text' form field not found",
    	form: {},
    }

2. Revise HomePageController.postAsync() to do the following when the text form field isn’t found:

  1. Log a warning.
  2. Return the home page (with nothing in the text field).
  3. Don’t worry about making the production code clean yet.

Remember to commit your changes when you’re done.

API Documentation

const log = Log.createNull();

Create a Nulled logger.

  • returns log (Log) - the logger
log.monitor(data);

Write data to the log with the monitor alert level.

  • data (object) - an object containing anything you want
const logOutput = log.trackOutput();

Track log output, similar to rot13.trackRequests(). This returns an OutputTracker instance. Every time log writes data, the OutputTracker is updated with an object describing the data that was written. The object is in this format:

{
	alert: "monitor",   // alert level
	...data,            // fields for the data that was written
}

Fields containing Error objects are converted to strings.

  • returns logOutput (OutputTracker) - the output tracker
const config = WwwConfig.createTestInstance({ log, rot13ServicePort, correlationId });

Create a WwwConfig instance with the provided log, ROT-13 service port, and correlation ID. Each parameter is optional.

  • log (Log) - the log
  • rot13ServicePort (number) - the port of the ROT-13 service
  • correlationId (string) - a unique ID representing the user’s request
  • returns config (WwwConfig) - the configured instance
const log = config.log;

Get the configured logger.

  • returns log (WwwConfig) - the logger
const ENDPOINT = "/";

This constant is available in the production code. Use it to log the controller’s endpoint.

TypeScript Types

logOutput.data: LogOutput[]

The logs stored in the output tracker.

type LogOutput = Record<string, any>;

JavaScript Primers

No new concepts.

Hints

1
Start by checking that the code returns the home page when the request body is empty.
You can use your tests’ postAsync() helper function. Don’t forget to await it.
The assertion is the same as for the GET test.
it("logs warning when form field not found (and treats request like GET)", async () => {
	const { response } = await postAsync({ body: "" });
	assert.deepEqual(response, homePageView.homePage());
});
2
You can run the test.
The test will fail, saying Cannot read properties of undefined (reading '0'). (In TypeScript, it will fail because it’s getting the wrong response.)

This is happening because there’s no text field. In JavaScript, this is causing an undefined reference. In TypeScript, it’s causing the code to call the Nulled ROT-13 service and render the default response.

3
Change your production code to not dereference an undefined array.
Check if form[INPUT_FIELD_NAME] is undefined. If it is, return early.
async postAsync(request, config) {
	const form = await request.readBodyAsUrlEncodedFormAsync();
	const textFields = form[INPUT_FIELD_NAME];
	if (textFields === undefined) return homePageView.homePage();

	const userInput = textFields[0];
	const output = await this._rot13Client.transformAsync(
		config.rot13ServicePort,
		userInput,
		config.correlationId,
	);
	return homePageView.homePage(output);
}

TypeScript needs a temporary workaround to satisfy the compiler:

// TypeScript
async postAsync(request: HttpServerRequest, config: WwwConfig): Promise<HttpServerResponse> {
	const form = await request.readBodyAsUrlEncodedFormAsync();
	const textFields = form[INPUT_FIELD_NAME];
	if (textFields === undefined) return homePageView.homePage();

	const userInput = textFields[0] ?? "not implemented";
	const output = await this._rot13Client.transformAsync(
		config.rot13ServicePort,
		userInput,
		config.correlationId,
	);
	return homePageView.homePage(output);
}
4
You’re ready to assert that the ROT-13 service isn’t called.
Get the rot13Requests return value from your postAsync() helper.
Assert that its tracking data is empty.
it("logs warning when form field not found (and treats request like GET)", async () => {
	const { response, rot13Requests } = await postAsync({ body: "" });

	assert.deepEqual(response, homePageView.homePage(), "should render home page");
	assert.deepEqual(rot13Requests.data, [], "shouldn't call ROT-13 service");
});
5
The test will pass the first time. Confirm that it really works.
Add a call to the ROT-13 service to your guard clause, see the test fail, then take it back out.
async postAsync(request, config) {
	const form = await request.readBodyAsUrlEncodedFormAsync();
	const textFields = form[INPUT_FIELD_NAME];
	if (textFields === undefined) {
		await this._rot13Client.transformAsync(42, "test", "foo");
		return homePageView.homePage();
	}

	const userInput = textFields[0];
	const output = await this._rot13Client.transformAsync(
		config.rot13ServicePort,
		userInput,
		config.correlationId,
	);
	return homePageView.homePage(output);
}

(Undo this change once you’ve confirmed the test fails correctly.)

6
Before you can test the logging, you’ll need to modify the tests’ postAsync() helper function to support checking log output.
Create the logger, track its output, add it to the config, and return the tracker.
You can create the logger with Log.createNull(). You can track its output with log.trackOutput(). You can add it to the config with WwwConfig.createTestInstance().
async function postAsync({
	body = `text=${IRRELEVANT_INPUT}`,
	rot13ServicePort = IRRELEVANT_PORT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13Response = "irrelevant ROT-13 response",
} = {}) {
	const rot13Client = Rot13Client.createNull([{ response: rot13Response }]);
	const rot13Requests = rot13Client.trackRequests();

	const log = Log.createNull();
	const logOutput = log.trackOutput();

	const clock = Clock.createNull();
	const controller = new HomePageController(rot13Client, clock);

	const request = HttpServerRequest.createNull({ body });
	const config = WwwConfig.createTestInstance({ log, rot13ServicePort, correlationId });

	const response = await controller.postAsync(request, config);

	return { response, rot13Requests, logOutput };
}
7
You’re ready to check the log output in your test.
Get the logOutput from the postAsync() helper and assert on logOutput.data.
it("logs warning when form field not found (and treats request like GET)", async () => {
	const { response, rot13Requests, logOutput } = await postAsync({ body: "" });

	assert.deepEqual(logOutput.data, [{
		alert: "monitor",
		endpoint: "/",
		method: "POST",
		message: "form parse error",
		error: "'text' form field not found",
		form: {},
	}], "should log a warning");

	assert.deepEqual(response, homePageView.homePage(), "should render home page");
	assert.deepEqual(rot13Requests.data, [], "shouldn't call ROT-13 service");
});
8
The test is ready to run again.
It should fail, saying that there was no log output.

This is because your production code isn’t writing to the log.

9
Make your production code write to the log.
You can do that by calling config.log.monitor().
async postAsync(request, config) {
	const form = await request.readBodyAsUrlEncodedFormAsync();
	const textFields = form[INPUT_FIELD_NAME];
	if (textFields === undefined) {
		config.log.monitor({
			endpoint: ENDPOINT,
			method: "POST",
			message: "form parse error",
			error: `'${INPUT_FIELD_NAME}' form field not found`,
			form,
		});
		return homePageView.homePage();
	}

	const userInput = textFields[0];  // Add '?? "not implemented"' for TypeScript
	const output = await this._rot13Client.transformAsync(
		config.rot13ServicePort,
		userInput,
		config.correlationId,
	);
	return homePageView.homePage(output);
}

Complete Solution

Test code:
it("logs warning when form field not found (and treats request like GET)", async () => {
	const { response, rot13Requests, logOutput } = await postAsync({ body: "" });

	assert.deepEqual(logOutput.data, [{
		alert: "monitor",
		endpoint: "/",
		method: "POST",
		message: "form parse error",
		error: "'text' form field not found",
		form: {},
	}], "should log a warning");

	assert.deepEqual(response, homePageView.homePage(), "should render home page");
	assert.deepEqual(rot13Requests.data, [], "shouldn't call ROT-13 service");
});

async function postAsync({
	body = `text=${IRRELEVANT_INPUT}`,
	rot13ServicePort = IRRELEVANT_PORT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13Response = "irrelevant ROT-13 response",
} = {}) {
	const rot13Client = Rot13Client.createNull([{ response: rot13Response }]);
	const rot13Requests = rot13Client.trackRequests();

	const log = Log.createNull();
	const logOutput = log.trackOutput();

	const clock = Clock.createNull();
	const controller = new HomePageController(rot13Client, clock);

	const request = HttpServerRequest.createNull({ body });
	const config = WwwConfig.createTestInstance({ log, rot13ServicePort, correlationId });

	const response = await controller.postAsync(request, config);

	return { response, rot13Requests, logOutput };
}
Production code (JavaScript):
async postAsync(request, config) {
	const form = await request.readBodyAsUrlEncodedFormAsync();
	const textFields = form[INPUT_FIELD_NAME];
	if (textFields === undefined) {
		config.log.monitor({
			endpoint: ENDPOINT,
			method: "POST",
			message: "form parse error",
			error: `'${INPUT_FIELD_NAME}' form field not found`,
			form,
		});
		return homePageView.homePage();
	}

	const userInput = textFields[0];
	const output = await this._rot13Client.transformAsync(
		config.rot13ServicePort,
		userInput,
		config.correlationId,
	);
	return homePageView.homePage(output);
}
Production code (TypeScript):
async postAsync(request: HttpServerRequest, config: WwwConfig): Promise<HttpServerResponse> {
	const form = await request.readBodyAsUrlEncodedFormAsync();
	const textFields = form[INPUT_FIELD_NAME];
	if (textFields === undefined) {
		config.log.monitor({
			endpoint: ENDPOINT,
			method: "POST",
			message: "form parse error",
			error: "'text' form field not found",
			form,
		});
		return homePageView.homePage();
	}

	const userInput = textFields[0] ?? "not implemented";
	const output = await this._rot13Client.transformAsync(
		config.rot13ServicePort,
		userInput,
		config.correlationId,
	);

	return homePageView.homePage(output);
}

Next challenge

Return to module overview