postmark
Version:
Official Node.js client library for the Postmark HTTP API - https://www.postmarkapp.com
257 lines (202 loc) • 10.8 kB
text/typescript
import {expect} from "chai";
import "mocha";
import * as sinon from "sinon";
import {FetchHttpClient} from "../../src/client/HttpClient";
import {ClientOptions} from "../../src/client/models";
import {Errors} from "../../src";
describe("FetchHttpClient", () => {
let sandbox: sinon.SinonSandbox;
const httpClient = new FetchHttpClient();
// Minimal Fetch Response stand-in - only the members the client relies on.
const buildResponse = (status: number, body?: any): any => ({
status,
text: async () => (body === undefined ? "" : JSON.stringify(body)),
});
const stubFetch = () => sandbox.stub(httpClient, "client");
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
describe("successful requests", () => {
it("returns parsed JSON body for 2xx responses", () => {
stubFetch().resolves(buildResponse(200, {To: "receiver@example.com"}));
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "/server", {}, null, {}).then((result: any) => {
expect(result).to.eql({To: "receiver@example.com"});
});
});
it("returns empty object when response body is empty", () => {
stubFetch().resolves(buildResponse(200));
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "/server", {}, null, {}).then((result: any) => {
expect(result).to.eql({});
});
});
});
describe("request composition", () => {
it("builds the URL from host, path and query parameters, skipping null/undefined", async () => {
const stub = stubFetch().resolves(buildResponse(200, {}));
await httpClient.httpRequest(ClientOptions.HttpMethod.GET, "/bounces",
{count: 10, offset: 0, tag: undefined, name: null, subject: "hi there"}, null, {});
expect(stub.firstCall.args[0]).to.equal("https://api.postmarkapp.com/bounces?count=10&offset=0&subject=hi+there");
});
it("prefixes the path with a slash when missing", async () => {
const stub = stubFetch().resolves(buildResponse(200, {}));
await httpClient.httpRequest(ClientOptions.HttpMethod.GET, "server", {}, null, {});
expect(stub.firstCall.args[0]).to.equal("https://api.postmarkapp.com/server");
});
it("serializes the body, forwards the method and headers, and sets an abort signal", async () => {
const stub = stubFetch().resolves(buildResponse(200, {}));
const headers = {"X-Postmark-Server-Token": "abc", "Content-Type": "application/json"};
await httpClient.httpRequest(ClientOptions.HttpMethod.POST, "/email", {}, {To: "a@b.com"}, headers);
const init: any = stub.firstCall.args[1];
expect(init.method).to.equal("POST");
expect(init.body).to.equal(JSON.stringify({To: "a@b.com"}));
expect(init.headers).to.eql(headers);
expect(init.signal).to.be.instanceOf(AbortSignal);
});
it("does not send a body for bodyless requests", async () => {
const stub = stubFetch().resolves(buildResponse(200, {}));
await httpClient.httpRequest(ClientOptions.HttpMethod.GET, "/server", {}, null, {});
const init: any = stub.firstCall.args[1];
expect(init.body).to.equal(undefined);
});
it("expands array query params to repeated keys and serializes Dates as ISO strings", async () => {
const stub = stubFetch().resolves(buildResponse(200, {}));
const since = new Date("2026-01-02T03:04:05.000Z");
await httpClient.httpRequest(ClientOptions.HttpMethod.GET, "/x",
{tags: ["a", "b"], since} as any, null, {});
expect(stub.firstCall.args[0])
.to.equal("https://api.postmarkapp.com/x?tags=a&tags=b&since=2026-01-02T03%3A04%3A05.000Z");
});
});
describe("custom fetch option", () => {
it("uses a caller-supplied fetch implementation (e.g. for proxy support)", async () => {
const customFetch = sinon.stub().resolves(buildResponse(200, {ok: true}));
const clientWithFetch = new FetchHttpClient({fetch: customFetch} as any);
const result: any = await clientWithFetch.httpRequest(ClientOptions.HttpMethod.GET, "/server", {}, null, {});
expect(customFetch.calledOnce).to.be.true;
expect(customFetch.firstCall.args[0]).to.equal("https://api.postmarkapp.com/server");
expect(result).to.eql({ok: true});
});
});
describe("network / timeout errors", () => {
it("default error", () => {
stubFetch().rejects(new Error("test"));
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.PostmarkError);
expect(error.message).to.equal("test");
expect(error.code).to.equal(0);
expect(error.statusCode).to.equal(0);
});
});
it("error with no message in it", () => {
const errorToThrow: any = { stack: 'Hello stack' };
stubFetch().rejects(errorToThrow);
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.an.instanceof(Errors.PostmarkError);
expect(error.name).to.equal("PostmarkError");
expect(error.message).to.equal(JSON.stringify(errorToThrow));
});
});
});
describe("http status code errors", () => {
const stubStatus = (statusNumber: number, body: any = { Message: "Basic error", ErrorCode: 0 }) =>
stubFetch().resolves(buildResponse(statusNumber, body));
it("uses the response Message and ErrorCode for the built error", () => {
stubStatus(500, { Message: "Server exploded", ErrorCode: 100 });
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error.message).to.equal("Server exploded");
expect(error.code).to.equal(100);
expect(error.statusCode).to.equal(500);
});
});
it("falls back to a generic message when the body has none", () => {
stubFetch().resolves(buildResponse(500));
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.InternalServerError);
expect(error.message).to.equal("Request returned status code 500");
});
});
it("401", () => {
stubStatus(401);
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.InvalidAPIKeyError);
});
});
it("404", () => {
stubStatus(404);
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.PostmarkError);
});
});
it("422", () => {
stubStatus(422);
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.ApiInputError);
});
});
it("422 - invalid email request", () => {
stubStatus(422, { Message: "Basic error", ErrorCode: 300 });
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.InvalidEmailRequestError);
});
});
it("422 - inactive recipients", () => {
stubStatus(422, { Message: "Basic error", ErrorCode: 406 });
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.InactiveRecipientsError);
});
});
it("429", () => {
stubStatus(429);
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.RateLimitExceededError);
});
});
it("500", () => {
stubStatus(500);
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.InternalServerError);
});
});
it("503", () => {
stubStatus(503);
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.ServiceUnavailablerError);
});
});
it("unknown status", () => {
stubStatus(-1);
return httpClient.httpRequest(ClientOptions.HttpMethod.GET, "", {}, null, {}).then((result) => {
throw Error(`Should not be here with result: ${result}`);
}, (error) => {
expect(error).to.be.instanceOf(Errors.UnknownError);
});
});
});
});