Challenge #5: Configurable Endpoints

Your high-level infrastructure will often support more than one HTTP endpoint. In this challenge, you’ll update your embedded stub to support custom responses for each endpoint.

Instructions

1. Modify the "can be configured with a different response per endpoint" test:

  1. Replace the Nulled HttpClient with the following endpoint-specific configuration:
    const client = HttpClient.createNull({
    	"/endpoint/1": { status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
    	"/endpoint/2": { status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
    });
  2. Assert that each endpoint returns the corresponding response.

2. In the production code:

  1. Modify the embedded stub to make the test pass.

Remember to commit your changes when you’re done.

API Documentation

const { response } = await requestAsync({ client, path });

Available in the tests. A helper method for calling httpClient.requestAsync(). Its main benefit is that it provides defaults for parameters your test doesn’t care about.

  • optional client (HttpClient) - the HttpClient to use; defaults to HttpClient.create()
  • optional path (HttpClient) - the path to use; defaults to "/irrelevant/path"
  • returns response ({ status, headers, body }) - the HTTP response
  • returns status (number) - the response status code
  • returns headers (object) - the response headers
  • returns body (string) - the response body

JavaScript Primers

Hints

1
Update the test.
It’s just a bigger version of the existing test. Remember to update the assertions.
You’ll need to rename the destructured variables.
it("can be configured with a different response per endpoint", async () => {
	const client = HttpClient.createNull({
		"/endpoint/1": { status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
		"/endpoint/2": { status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
	});

	const { response: response1 } = await requestAsync({ client, path: "/endpoint/1" });
	const { response: response2 } = await requestAsync({ client, path: "/endpoint/2" });

	assert.deepEqual(response1, {
		status: 200,
		headers: { myheader1: "myValue1" },
		body: "my body 1",
	});
	assert.deepEqual(response2, {
		status: 300,
		headers: { myheader2: "myValue2" },
		body: "my body 2",
	});
});

The TypeScript type declaration in HttpClient.createNull() was a placeholder, and can now be simplified:

static createNull(responses?: NulledHttpClientResponses): HttpClient {
	return new HttpClient(new StubbedHttp(responses));
}
2
The test is ready to run.
It should fail, saying that the status, headers, and body are undefined. (In TypeScript, they’ll have placeholder values.)

That’s because you’ve introduced the endpoints, so the top-level object doesn’t have status, headers, and body properties any more. Instead, the top-level object has /endpoint/1 and /endpoint/2 properties.

3
How can you make the production code pass?
StubbedResponse needs to have access to the correct response.
What is the “correct” response?
It’s the one that matches the path being requested.
How do you know which path is being requested?
Trace through the production code to find out.
The path is being passed to this._http.request().
The corresponding stub is StubbedHttp.request().

You can make the production code pass by modifying StubbedHttp.request(). It will use the path parameter to pull the correct response out of the configured responses.

4
Modify the embedded stub to get the correct response.
Modify StubbedHttp.request() to get the path.
You can do that with object destructuring.
When you have the path, you can use it to dereference the configured responses.
class StubbedHttp {

	constructor(responses) {
		this._responses = responses;
	}

	request({ path }) {
		return new StubbedRequest(this._responses[path]);
	}

}

In TypeScript, you’ll need to change the type of _responses. You’ll also need to declare that the response isn’t an array. (You’ll add support for arrays later.)

class StubbedHttp {

	constructor(private readonly _responses?: NulledHttpClientResponses) {
	}

	request({ path }: { path: string }) {
		return new StubbedRequest(this._responses[path] as NulledHttpClientResponse);
	}

}

TypeScript will still complain about a possibly undefined value, but that’s discussed in the next hint.

5
In TypeScript, why doesn’t the code compile? (If you’re using JavaScript, skip this hint.)
It should be complaining that this._responses is possibly undefined.
Where does this._responses come from?

The code isn’t compiling because it isn’t handling the case where no responses have been provided.

6
In JavaScript, what happens if you run the tests? (If you’re using TypeScript, skip this hint.)
They should fail, complaining about undefined while reading /irrelevant/path. Why do they fail?
Look at the test names.
It’s the other nulled instance tests that are failing.
Look at the stack trace.
The error is occurring on this line: return new StubbedRequest(this._responses[path]);.

The tests are failing because they don’t provide responses.

7
Fix the tests (in JavaScript) or compile error (in TypeScript).
You need to provide a default response.
You can do that with a default parameter.
class StubbedHttp {

	constructor(responses = {}) {
		this._responses = responses;
	}

	request({ path }) {
		return new StubbedRequest(this._responses[path]);
	}

}

In TypeScript, it looks like this:

class StubbedHttp {

	constructor(private readonly _responses: NulledHttpClientResponses = {}) {
	}

	request({ path }: { path: string }) {
		return new StubbedRequest(this._responses[path] as NulledHttpClientResponse);
	}

}

Complete Solution

Test code:
it("can be configured with a different response per endpoint", async () => {
	const client = HttpClient.createNull({
		"/endpoint/1": { status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
		"/endpoint/2": { status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
	});

	const { response: response1 } = await requestAsync({ client, path: "/endpoint/1" });
	const { response: response2 } = await requestAsync({ client, path: "/endpoint/2" });

	assert.deepEqual(response1, {
		status: 200,
		headers: { myheader1: "myValue1" },
		body: "my body 1",
	});
	assert.deepEqual(response2, {
		status: 300,
		headers: { myheader2: "myValue2" },
		body: "my body 2",
	});
});
Production code (JavaScript):
class StubbedHttp {

	constructor(responses = {}) {
		this._responses = responses;
	}

	request({ path }) {
		return new StubbedRequest(this._responses[path]);
	}

}
Production code (TypeScript):
class StubbedHttp {

	constructor(private readonly _responses: NulledHttpClientResponses = {}) {
	}

	request({ path }: { path: string }) {
		return new StubbedRequest(this._responses[path] as NulledHttpClientResponse);
	}

}

Next challenge

Return to module overview