Challenge #6: Normalization
When creating a low-level Nullable infrastructure wrapper, it’s usually better to create a stub than a fake. Stubs are much easier to create than fakes: fakes implement an in-memory version of real-world behavior, and stubs just return hardcoded data.
Although stubs don’t try to match real-world behavior, in those few cases where the stub is emulating real-world behavior, it’s important to ensure that it matches the behavior of the real code as closely as possible. Because the rest of your code will test against the Nulled instance, not a real instance, deviations in behavior could result in code that doesn’t work.
There’s one aspect of the real http
library that the StubbedHttp
classes aren’t emulating: header normalization. The real http
library always converts header names to lowercase. In this challenge, you’ll make your embedded stub match that behavior.
Instructions
1. In the "normalizes header names to lowercase (to match real behavior)"
test:
- Configure the Nulled response with multiple mixed-case headers.
- Assert that the response has lower-case headers.
2. In the production code:
- Modify the embedded stub to make the test pass.
Remember to commit your changes when you’re done.
API Documentation
const objectOut = Object.fromEntries(Object.entries(object).map(transformFn));
Convert an object into another object of the same size by running transformFn()
on each entry.
transformFn()
is a function that takes a [ key, value ]
array as a parameter and returns the new
[ key, value ]
. For example, the following code adds an exclamation mark to each object key:
const original = {
a: "one",
b: "two",
c: "three",
};
const transformed = Object.fromEntries(Object.entries(original).map(([ key, value ]) => {
return [ key + "!", value ];
}
console.log(transformed); // outputs { "a!": "one", "b!": "two", "c!": three }
- transformFn
((element) => elementOut)
- the function to convert array elements - returns objectOut
(object)
- the converted object
const lowercased = string.toLowerCase();
Convert a string to lowercase.
- string
((element) => elementOut)
- the string to convert - returns lowercased
(object)
- the converted string
JavaScript Primers
No new concepts.
Hints
1
The test needs to configure a Nulled response, then assert on it.
This is similar to the previous test, but with more headers.
it("normalizes header names to lowercase (to match real behavior)", async () => {
const client = HttpClient.createNull({
"/endpoint": {
status: 200,
headers: {
MiXeDcAsE: "mixed",
UPPERCASE: "upper",
lowercase: "lower",
},
body: "my body"
},
});
const { response } = await requestAsync({ client, path: "/endpoint" });
assert.deepEqual(response, {
status: 200,
headers: {
mixedcase: "mixed",
uppercase: "upper",
lowercase: "lower",
},
body: "my body",
});
});
2
The test is ready to run.
It should fail, saying that the mixed-case and upper-case keys header names are incorrect.
That’s because the production code isn’t converting the header names to lowercase.
3
The production code needs to convert the strings to lowercase.
You can use Object.fromEntries(Object.entries(...).map(...))
and string.toLowerCase()
to do that.
class StubbedResponse extends EventEmitter {
constructor(response = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status;
this.headers = Object.fromEntries(Object.entries(response.headers).map(([ key, value ]) => {
return [ key.toLowerCase(), value ];
}));
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}
TypeScript still needs its placeholders:
constructor(response: NulledHttpClientResponse = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status ?? 42;
this.headers = Object.fromEntries(Object.entries(response.headers ?? {}).map(([ key, value ]) => {
return [ key.toLowerCase(), value ];
}));
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
4
Refactor and clean up.
This is a matter of taste, with no right answer.
class StubbedResponse extends EventEmitter {
constructor(response = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status; // add "?? 42" for TypeScript
this.headers = normalizeHeaders(response.headers); // add "?? {}" for TypeScript
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}
function normalizeHeaders(headers) {
const originalEntries = Object.entries(headers);
const transformedEntries = originalEntries.map(([ key, value ]) => [ key.toLowerCase(), value ]);
return Object.fromEntries(transformedEntries);
}
Complete Solution
Test code:
it("normalizes header names to lowercase (to match real behavior)", async () => {
const client = HttpClient.createNull({
"/endpoint": {
status: 200,
headers: {
MiXeDcAsE: "mixed",
UPPERCASE: "upper",
lowercase: "lower",
},
body: "my body"
},
});
const { response } = await requestAsync({ client, path: "/endpoint" });
assert.deepEqual(response, {
status: 200,
headers: {
mixedcase: "mixed",
uppercase: "upper",
lowercase: "lower",
},
body: "my body",
});
});
Production code (JavaScript):
class StubbedResponse extends EventEmitter {
constructor(response = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status;
this.headers = normalizeHeaders(response.headers);
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}
function normalizeHeaders(headers) {
const originalEntries = Object.entries(headers);
const transformedEntries = originalEntries.map(([ key, value ]) => [ key.toLowerCase(), value ]);
return Object.fromEntries(transformedEntries);
}
Production code (TypeScript):
class StubbedResponse extends EventEmitter implements NodeHttpResponse {
statusCode: number;
headers: HttpHeaders;
constructor(response: NulledHttpClientResponse = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status ?? 42;
this.headers = normalizeHeaders(response.headers ?? {});
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}
function normalizeHeaders(headers: HttpHeaders) {
const originalEntries = Object.entries(headers);
const transformedEntries = originalEntries.map(([ key, value ]) => [ key.toLowerCase(), value ]);
return Object.fromEntries(transformedEntries);
}