Skip to content

Commit

Permalink
Merge pull request #16 from ideal-postcodes/configure_agent
Browse files Browse the repository at this point in the history
Configure agent
  • Loading branch information
cblanc authored Jul 18, 2019
2 parents fa8e280 + c8f9307 commit 024f567
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 19 deletions.
57 changes: 43 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ Our JavaScript client implements a common interface which is implemented at [`@i

### Configuration & Usage

- [Install](#install)
- [Instantiate](#instantiate) and [Use](#use) client
- [Catch Errors](#catch-errors)
- [Configure Agent](#configure-agent)
- [Proxy HTTP Requests](#proxy-requests)

#### Install

```bash
Expand Down Expand Up @@ -78,26 +84,49 @@ try {
}
```

#### Configure HTTP Agent

`core-node` uses [got](https://github.com/sindresorhus/got) as its underlying HTTP client. The Ideal Postcodes API client can also be optionally configured with a [got](https://github.com/sindresorhus/got) options object which is fed to [got](https://github.com/sindresorhus/got) on every request.

Be aware this options object will overwrite any existing [got](https://github.com/sindresorhus/got) HTTP request parameters.

```javascript
const client = new Client({ api_key: "iddqd" }, {
cache: new Map, // Instantiate a cache: https://github.com/sindresorhus/got#cache-1
hooks: { // Hook into HTTP responses: https://github.com/sindresorhus/got#hooksafterresponse
afterResponse: response => {
log(response);
return response;
}
},
});
```

#### Proxy HTTP Requests

You can [proxy requests](https://github.com/sindresorhus/got#proxies) by configuring the underlying [got](https://github.com/sindresorhus/got) HTTP client.

```javascript
const tunnel = require("tunnel");

const client = new Client(config, {
agent: tunnel.httpOverHttp({
proxy: {
host: "localhost"
}
})
});
```

---

### Quickstart

The client exposes a number of simple methods to get at the most common tasks when interacting with the API. Below is a (incomplete) list of commonly used methods.

- [Links](#links)
- [Other JavaScript Clients](#other-javascript-clients)
- [Documentation](#documentation)
- [Configuration & Usage](#configuration--usage)
- [Install](#install)
- [Instantiate](#instantiate)
- [Use](#use)
- [Catch Errors](#catch-errors)
- [Quickstart](#quickstart)
- [Lookup a Postcode](#lookup-a-postcode)
- [Search for an Address](#search-for-an-address)
- [Search for an Address by UDPRN](#search-for-an-address-by-udprn)
- [Test](#test)
- [Licence](#licence)
- [Lookup a Postcode](#lookup-a-postcode)
- [Search for an Address](#search-for-an-address)
- [Search for an Address by UDPRN](#search-for-an-address-by-udprn)

For a complete list of client methods, including low level resource methods, please see the [core-interface documentation](https://core-interface.ideal-postcodes.dev/#documentation)

Expand Down
16 changes: 14 additions & 2 deletions lib/agent.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import got, { GotInstance, Response } from "got";
import got, { GotInstance, Response, GotJSONOptions } from "got";
import {
Agent as IAgent,
HttpRequest,
Expand All @@ -15,6 +15,14 @@ interface StringMap {
[key: string]: string;
}

/**
* GotConfig
*
* An optional configuration object which is passed to the underlying got http
* client
*/
export type GotConfig = Partial<GotJSONOptions>;

// Converts a Got header object to one that can be used by the client
export const toHeader = (gotHeaders: GotHeaders): StringMap => {
return Object.keys(gotHeaders).reduce(
Expand Down Expand Up @@ -55,9 +63,11 @@ const handleError = (error: Error): Promise<never> => {

export class Agent implements IAgent {
public got: GotInstance;
public gotConfig: GotConfig;

constructor() {
constructor(gotConfig: GotConfig = {}) {
this.got = got;
this.gotConfig = gotConfig;
}

private requestWithBody(httpRequest: HttpRequest): Promise<HttpResponse> {
Expand All @@ -70,6 +80,7 @@ export class Agent implements IAgent {
throwHttpErrors: false,
body,
timeout,
...this.gotConfig,
})
.then(response => toHttpResponse(httpRequest, response))
.catch(handleError);
Expand All @@ -84,6 +95,7 @@ export class Agent implements IAgent {
timeout,
throwHttpErrors: false,
json: true,
...this.gotConfig,
})
.then(response => toHttpResponse(httpRequest, response))
.catch(handleError);
Expand Down
14 changes: 11 additions & 3 deletions lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
TIMEOUT,
STRICT_AUTHORISATION,
} from "@ideal-postcodes/core-interface";
import { Agent } from "./agent";
import { Agent, GotConfig } from "./agent";

const userAgent = `IdealPostcodes ideal-postcodes/core-node`;

Expand All @@ -16,8 +16,16 @@ interface Config extends Partial<CoreConfig> {
}

export class Client extends CoreInterface {
constructor(config: Config) {
const agent = new Agent();
/**
* Client constructor extends CoreInterface by also accepting an optional got
* configuration object as the second argument.
*
* got is the underlying HTTP client that powers core-node. Be careful when
* configuring gotConfig so as not to manually override critical request
* attributes like method, query, header, etc.
*/
constructor(config: Config, gotConfig: GotConfig = {}) {
const agent = new Agent(gotConfig);
const header = { "User-Agent": userAgent };
const tls = config.tls === undefined ? TLS : config.tls;
const baseUrl = config.baseUrl === undefined ? API_URL : config.baseUrl;
Expand Down
88 changes: 88 additions & 0 deletions test/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ describe("Agent", () => {
agent = new Agent();
});

describe("Agent class", () => {
it("allows for optional got configuration", () => {
const a = new Agent();
assert.deepEqual(a.gotConfig, {});
});

it("assigns GOT config", () => {
const retry = 2;
const a = new Agent({ retry });
assert.deepEqual({ retry }, a.gotConfig);
});
});

describe("toHeader", () => {
it("coerces a Got header object into an object of strings", () => {
const gotHeader = {
Expand Down Expand Up @@ -129,6 +142,81 @@ describe("Agent", () => {
timeout: 1000,
} as any);
});

describe("GOT Configuration", () => {
const timeout = 2000;
const retry = 2;

beforeEach(() => {
agent = new Agent({ timeout, retry });
});

it("overrides HTTP configuration for GET requests", async () => {
const method: HttpVerb = "GET";
const query = { foo: "bar" };
const header = { baz: "quux" };
const url = "http://www.foo.com/";
const SUCCESS = 200;

const response: unknown = {
statusCode: SUCCESS,
headers: header,
body: Buffer.from("{}"),
url,
};

const stub = sinon
.stub(agent, "got")
.resolves(response as Response<Buffer>);

await agent.http({ method, timeout: 1000, url, header, query });

sinon.assert.calledOnce(stub);
sinon.assert.calledWithExactly(stub, url, {
method,
headers: header,
throwHttpErrors: false,
query,
json: true,
timeout,
retry,
} as any);
});

it("overrides HTTP configuration for POST requests", async () => {
const method: HttpVerb = "POST";
const query = { foo: "bar" };
const header = { baz: "quux" };
const url = "http://www.foo.com/";
const SUCCESS = 200;
const body = { foo: "bar" };

const response: unknown = {
statusCode: SUCCESS,
headers: header,
body: Buffer.from("{}"),
url,
};

const stub = sinon
.stub(agent, "got")
.resolves(response as Response<Buffer>);

await agent.http({ body, method, timeout: 1000, url, header, query });

sinon.assert.calledOnce(stub);
sinon.assert.calledWithExactly(stub, url, {
method,
throwHttpErrors: false,
json: true,
body,
headers: header,
query,
retry,
timeout,
} as any);
});
});
});

describe("Error handling", () => {
Expand Down
9 changes: 9 additions & 0 deletions test/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("Client", () => {
assert.equal(client.strictAuthorisation, STRICT_AUTHORISATION);
assert.equal(client.timeout, TIMEOUT);
});

it("allows default config values to be overwritten", () => {
const options = {
tls: false,
Expand All @@ -44,9 +45,17 @@ describe("Client", () => {
options.strictAuthorisation
);
assert.equal(customClient.timeout, options.timeout);
assert.deepEqual((customClient.agent as any).gotConfig, {});
});

it("assigns user agent header", () => {
assert.match(client.header["User-Agent"], /Core-Node/i);
});

it("assigns got config", () => {
const retry = 2;
const customClient = new Client({ api_key }, { retry });
assert.deepEqual((customClient.agent as any).gotConfig, { retry });
});
});
});

0 comments on commit 024f567

Please sign in to comment.