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:
- Configure the request body to be an empty string.
- Assert that
HomePageController.postAsync()
returnshomePageView.homePage()
. - Assert that it doesn’t call the ROT-13 service.
- 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:
- Log a warning.
- Return the home page (with nothing in the text field).
- 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);
}