4 changed files with 318 additions and 10 deletions
@ -0,0 +1,206 @@
@@ -0,0 +1,206 @@
|
||||
/** |
||||
* include Request in test environment. |
||||
* @jest-environment ../../../../shared/test.environment.ts |
||||
*/ |
||||
import { Forwarders } from "../options/constants"; |
||||
|
||||
import { AddyIoForwarder } from "./addy-io"; |
||||
import { mockApiService, mockI18nService } from "./mocks.jest"; |
||||
|
||||
describe("Addy.io Forwarder", () => { |
||||
describe("generate(string | null, SelfHostedApiOptions & EmailDomainOptions)", () => { |
||||
it.each([null, ""])("throws an error if the token is missing (token = %p)", async (token) => { |
||||
const apiService = mockApiService(200, {}); |
||||
const i18nService = mockI18nService(); |
||||
|
||||
const forwarder = new AddyIoForwarder(apiService, i18nService); |
||||
|
||||
await expect( |
||||
async () => |
||||
await forwarder.generate(null, { |
||||
token, |
||||
domain: "example.com", |
||||
baseUrl: "https://api.example.com", |
||||
}), |
||||
).rejects.toEqual("forwaderInvalidToken"); |
||||
|
||||
expect(apiService.nativeFetch).not.toHaveBeenCalled(); |
||||
expect(i18nService.t).toHaveBeenCalledWith("forwaderInvalidToken", Forwarders.AddyIo.name); |
||||
}); |
||||
|
||||
it.each([null, ""])( |
||||
"throws an error if the domain is missing (domain = %p)", |
||||
async (domain) => { |
||||
const apiService = mockApiService(200, {}); |
||||
const i18nService = mockI18nService(); |
||||
|
||||
const forwarder = new AddyIoForwarder(apiService, i18nService); |
||||
|
||||
await expect( |
||||
async () => |
||||
await forwarder.generate(null, { |
||||
token: "token", |
||||
domain, |
||||
baseUrl: "https://api.example.com", |
||||
}), |
||||
).rejects.toEqual("forwarderNoDomain"); |
||||
|
||||
expect(apiService.nativeFetch).not.toHaveBeenCalled(); |
||||
expect(i18nService.t).toHaveBeenCalledWith("forwarderNoDomain", Forwarders.AddyIo.name); |
||||
}, |
||||
); |
||||
|
||||
it.each([null, ""])( |
||||
"throws an error if the baseUrl is missing (baseUrl = %p)", |
||||
async (baseUrl) => { |
||||
const apiService = mockApiService(200, {}); |
||||
const i18nService = mockI18nService(); |
||||
|
||||
const forwarder = new AddyIoForwarder(apiService, i18nService); |
||||
|
||||
await expect( |
||||
async () => |
||||
await forwarder.generate(null, { |
||||
token: "token", |
||||
domain: "example.com", |
||||
baseUrl, |
||||
}), |
||||
).rejects.toEqual("forwarderNoUrl"); |
||||
|
||||
expect(apiService.nativeFetch).not.toHaveBeenCalled(); |
||||
expect(i18nService.t).toHaveBeenCalledWith("forwarderNoUrl", Forwarders.AddyIo.name); |
||||
}, |
||||
); |
||||
|
||||
it.each([ |
||||
["forwarderGeneratedByWithWebsite", "provided", "bitwarden.com", "bitwarden.com"], |
||||
["forwarderGeneratedByWithWebsite", "provided", "httpbin.org", "httpbin.org"], |
||||
["forwarderGeneratedBy", "not provided", null, ""], |
||||
["forwarderGeneratedBy", "not provided", "", ""], |
||||
])( |
||||
"describes the website with %p when the website is %s (= %p)", |
||||
async (translationKey, _ignored, website, expectedWebsite) => { |
||||
const apiService = mockApiService(200, {}); |
||||
const i18nService = mockI18nService(); |
||||
|
||||
const forwarder = new AddyIoForwarder(apiService, i18nService); |
||||
|
||||
await forwarder.generate(website, { |
||||
token: "token", |
||||
domain: "example.com", |
||||
baseUrl: "https://api.example.com", |
||||
}); |
||||
|
||||
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
|
||||
expect(i18nService.t).toHaveBeenCalledWith(translationKey, expectedWebsite); |
||||
}, |
||||
); |
||||
|
||||
it.each([ |
||||
["jane.doe@example.com", 201], |
||||
["john.doe@example.com", 201], |
||||
["jane.doe@example.com", 200], |
||||
["john.doe@example.com", 200], |
||||
])( |
||||
"returns the generated email address (= %p) if the request is successful (status = %p)", |
||||
async (email, status) => { |
||||
const apiService = mockApiService(status, { data: { email } }); |
||||
const i18nService = mockI18nService(); |
||||
|
||||
const forwarder = new AddyIoForwarder(apiService, i18nService); |
||||
|
||||
const result = await forwarder.generate(null, { |
||||
token: "token", |
||||
domain: "example.com", |
||||
baseUrl: "https://api.example.com", |
||||
}); |
||||
|
||||
expect(result).toEqual(email); |
||||
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); |
||||
}, |
||||
); |
||||
|
||||
it("throws an invalid token error if the request fails with a 401", async () => { |
||||
const apiService = mockApiService(401, {}); |
||||
const i18nService = mockI18nService(); |
||||
|
||||
const forwarder = new AddyIoForwarder(apiService, i18nService); |
||||
|
||||
await expect( |
||||
async () => |
||||
await forwarder.generate(null, { |
||||
token: "token", |
||||
domain: "example.com", |
||||
baseUrl: "https://api.example.com", |
||||
}), |
||||
).rejects.toEqual("forwaderInvalidToken"); |
||||
|
||||
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); |
||||
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
|
||||
expect(i18nService.t).toHaveBeenNthCalledWith( |
||||
2, |
||||
"forwaderInvalidToken", |
||||
Forwarders.AddyIo.name, |
||||
); |
||||
}); |
||||
|
||||
it("throws an unknown error if the request fails and no status is provided", async () => { |
||||
const apiService = mockApiService(500, {}); |
||||
const i18nService = mockI18nService(); |
||||
|
||||
const forwarder = new AddyIoForwarder(apiService, i18nService); |
||||
|
||||
await expect( |
||||
async () => |
||||
await forwarder.generate(null, { |
||||
token: "token", |
||||
domain: "example.com", |
||||
baseUrl: "https://api.example.com", |
||||
}), |
||||
).rejects.toEqual("forwarderUnknownError"); |
||||
|
||||
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); |
||||
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
|
||||
expect(i18nService.t).toHaveBeenNthCalledWith( |
||||
2, |
||||
"forwarderUnknownError", |
||||
Forwarders.AddyIo.name, |
||||
); |
||||
}); |
||||
|
||||
it.each([ |
||||
[100, "Continue"], |
||||
[202, "Accepted"], |
||||
[300, "Multiple Choices"], |
||||
[418, "I'm a teapot"], |
||||
[500, "Internal Server Error"], |
||||
[600, "Unknown Status"], |
||||
])( |
||||
"throws an error with the status text if the request returns any other status code (= %i) and a status (= %p) is provided", |
||||
async (statusCode, statusText) => { |
||||
const apiService = mockApiService(statusCode, {}, statusText); |
||||
const i18nService = mockI18nService(); |
||||
|
||||
const forwarder = new AddyIoForwarder(apiService, i18nService); |
||||
|
||||
await expect( |
||||
async () => |
||||
await forwarder.generate(null, { |
||||
token: "token", |
||||
domain: "example.com", |
||||
baseUrl: "https://api.example.com", |
||||
}), |
||||
).rejects.toEqual("forwarderError"); |
||||
|
||||
expect(apiService.nativeFetch).toHaveBeenCalledWith(expect.any(Request)); |
||||
// counting instances is terribly flaky over changes, but jest doesn't have a better way to do this
|
||||
expect(i18nService.t).toHaveBeenNthCalledWith( |
||||
2, |
||||
"forwarderError", |
||||
Forwarders.AddyIo.name, |
||||
statusText, |
||||
); |
||||
}, |
||||
); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service"; |
||||
import { I18nService } from "../../../../platform/abstractions/i18n.service"; |
||||
import { Forwarders } from "../options/constants"; |
||||
import { EmailDomainOptions, Forwarder, SelfHostedApiOptions } from "../options/forwarder-options"; |
||||
|
||||
/** Generates a forwarding address for addy.io (formerly anon addy) */ |
||||
export class AddyIoForwarder implements Forwarder { |
||||
/** Instantiates the forwarder |
||||
* @param apiService used for ajax requests to the forwarding service |
||||
* @param i18nService used to look up error strings |
||||
*/ |
||||
constructor( |
||||
private apiService: ApiService, |
||||
private i18nService: I18nService, |
||||
) {} |
||||
|
||||
/** {@link Forwarder.generate} */ |
||||
async generate( |
||||
website: string | null, |
||||
options: SelfHostedApiOptions & EmailDomainOptions, |
||||
): Promise<string> { |
||||
if (!options.token || options.token === "") { |
||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name); |
||||
throw error; |
||||
} |
||||
if (!options.domain || options.domain === "") { |
||||
const error = this.i18nService.t("forwarderNoDomain", Forwarders.AddyIo.name); |
||||
throw error; |
||||
} |
||||
if (!options.baseUrl || options.baseUrl === "") { |
||||
const error = this.i18nService.t("forwarderNoUrl", Forwarders.AddyIo.name); |
||||
throw error; |
||||
} |
||||
|
||||
const descriptionId = |
||||
website && website !== "" ? "forwarderGeneratedByWithWebsite" : "forwarderGeneratedBy"; |
||||
const description = this.i18nService.t(descriptionId, website ?? ""); |
||||
|
||||
const url = options.baseUrl + "/api/v1/aliases"; |
||||
const request = new Request(url, { |
||||
redirect: "manual", |
||||
cache: "no-store", |
||||
method: "POST", |
||||
headers: new Headers({ |
||||
Authorization: "Bearer " + options.token, |
||||
"Content-Type": "application/json", |
||||
"X-Requested-With": "XMLHttpRequest", |
||||
}), |
||||
body: JSON.stringify({ |
||||
domain: options.domain, |
||||
description, |
||||
}), |
||||
}); |
||||
|
||||
const response = await this.apiService.nativeFetch(request); |
||||
if (response.status === 200 || response.status === 201) { |
||||
const json = await response.json(); |
||||
return json?.data?.email; |
||||
} else if (response.status === 401) { |
||||
const error = this.i18nService.t("forwaderInvalidToken", Forwarders.AddyIo.name); |
||||
throw error; |
||||
} else if (response?.statusText) { |
||||
const error = this.i18nService.t( |
||||
"forwarderError", |
||||
Forwarders.AddyIo.name, |
||||
response.statusText, |
||||
); |
||||
throw error; |
||||
} else { |
||||
const error = this.i18nService.t("forwarderUnknownError", Forwarders.AddyIo.name); |
||||
throw error; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
import { ApiService } from "../../../../abstractions/api.service"; |
||||
import { I18nService } from "../../../../platform/abstractions/i18n.service"; |
||||
|
||||
/** a mock {@link ApiService} that returns a fetch-like response with a given status and body */ |
||||
export function mockApiService(status: number, body: any, statusText?: string) { |
||||
return { |
||||
nativeFetch: jest.fn().mockImplementation((r: Request) => { |
||||
return { |
||||
status, |
||||
statusText, |
||||
json: jest.fn().mockImplementation(() => Promise.resolve(body)), |
||||
}; |
||||
}), |
||||
} as unknown as ApiService; |
||||
} |
||||
|
||||
/** a mock {@link I18nService} that returns the translation key */ |
||||
export function mockI18nService() { |
||||
return { |
||||
t: jest.fn().mockImplementation((key: string) => key), |
||||
} as unknown as I18nService; |
||||
} |
||||
@ -1,22 +1,28 @@
@@ -1,22 +1,28 @@
|
||||
import JSDOMEnvironment from "jest-environment-jsdom"; |
||||
|
||||
/** |
||||
* https://github.com/jsdom/jsdom/issues/3363#issuecomment-1467894943
|
||||
* Adds nodes structuredClone implementation to the global object of jsdom. |
||||
* use by either adding this file to the testEnvironment property of jest config |
||||
* or by adding the following to the top spec file: |
||||
* Maps Node's APIs to the jsdom global object to work around |
||||
* missing methods in Jest's 'jsdom' test environment. |
||||
* |
||||
* ``` |
||||
* /** |
||||
* * @jest-environment ../shared/test.environment.ts |
||||
* *\/ |
||||
* ``` |
||||
* @remarks To use this test environment, reference this file |
||||
* in the `testEnvironment` property of the Jest configuration |
||||
* or adding a `@jest-environment path/to/test.environment.ts` |
||||
* directive to your test file. Consult the Jest documentation |
||||
* for more information. |
||||
* |
||||
* @see https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string
|
||||
*/ |
||||
export default class FixJSDOMEnvironment extends JSDOMEnvironment { |
||||
constructor(...args: ConstructorParameters<typeof JSDOMEnvironment>) { |
||||
super(...args); |
||||
|
||||
// FIXME https://github.com/jsdom/jsdom/issues/3363
|
||||
// FIXME https://github.com/jsdom/jsdom/issues/3363#issuecomment-1467894943
|
||||
this.global.structuredClone = structuredClone; |
||||
|
||||
// FIXME https://github.com/jsdom/jsdom/issues/1724#issuecomment-1446858041
|
||||
this.global.fetch = fetch; |
||||
this.global.Headers = Headers; |
||||
this.global.Request = Request; |
||||
this.global.Response = Response; |
||||
} |
||||
} |
||||
|
||||
Loading…
Reference in new issue