From 6b97c0e716c2a2a207beb95ec2b8807516506dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Thu, 25 Jan 2024 10:24:22 -0500 Subject: [PATCH] [PM-5840] add duck duck go forwarder (#7674) --- .../username/forwarders/duck-duck-go.spec.ts | 120 ++++++++++++++++++ .../username/forwarders/duck-duck-go.ts | 52 ++++++++ 2 files changed, 172 insertions(+) create mode 100644 libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts create mode 100644 libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts new file mode 100644 index 0000000000..7b5765f9a7 --- /dev/null +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.spec.ts @@ -0,0 +1,120 @@ +/** + * include Request in test environment. + * @jest-environment ../../../../shared/test.environment.ts + */ +import { Forwarders } from "../options/constants"; + +import { DuckDuckGoForwarder } from "./duck-duck-go"; +import { mockApiService, mockI18nService } from "./mocks.jest"; + +describe("DuckDuckGo 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 DuckDuckGoForwarder(apiService, i18nService); + + await expect( + async () => + await forwarder.generate(null, { + token, + }), + ).rejects.toEqual("forwaderInvalidToken"); + + expect(apiService.nativeFetch).not.toHaveBeenCalled(); + expect(i18nService.t).toHaveBeenCalledWith( + "forwaderInvalidToken", + Forwarders.DuckDuckGo.name, + ); + }); + + it.each([ + ["jane.doe@duck.com", 201, "jane.doe"], + ["john.doe@duck.com", 201, "john.doe"], + ["jane.doe@duck.com", 200, "jane.doe"], + ["john.doe@duck.com", 200, "john.doe"], + ])( + "returns the generated email address (= %p) if the request is successful (status = %p)", + async (email, status, address) => { + const apiService = mockApiService(status, { address }); + const i18nService = mockI18nService(); + + const forwarder = new DuckDuckGoForwarder(apiService, i18nService); + + const result = await forwarder.generate(null, { + token: "token", + }); + + 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 DuckDuckGoForwarder(apiService, i18nService); + + await expect( + async () => + await forwarder.generate(null, { + token: "token", + }), + ).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).toHaveBeenCalledWith( + "forwaderInvalidToken", + Forwarders.DuckDuckGo.name, + ); + }); + + it("throws an unknown error if the request is successful but an address isn't present", async () => { + const apiService = mockApiService(200, {}); + const i18nService = mockI18nService(); + + const forwarder = new DuckDuckGoForwarder(apiService, i18nService); + + await expect( + async () => + await forwarder.generate(null, { + token: "token", + }), + ).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).toHaveBeenCalledWith( + "forwarderUnknownError", + Forwarders.DuckDuckGo.name, + ); + }); + + it.each([100, 202, 300, 418, 500, 600])( + "throws an unknown error if the request returns any other status code (= %i)", + async (statusCode) => { + const apiService = mockApiService(statusCode, {}); + const i18nService = mockI18nService(); + + const forwarder = new DuckDuckGoForwarder(apiService, i18nService); + + await expect( + async () => + await forwarder.generate(null, { + token: "token", + }), + ).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).toHaveBeenCalledWith( + "forwarderUnknownError", + Forwarders.DuckDuckGo.name, + ); + }, + ); + }); +}); diff --git a/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts new file mode 100644 index 0000000000..8078230b3a --- /dev/null +++ b/libs/common/src/tools/generator/username/forwarders/duck-duck-go.ts @@ -0,0 +1,52 @@ +import { ApiService } from "../../../../abstractions/api.service"; +import { I18nService } from "../../../../platform/abstractions/i18n.service"; +import { Forwarders } from "../options/constants"; +import { ApiOptions, Forwarder } from "../options/forwarder-options"; + +/** Generates a forwarding address for DuckDuckGo */ +export class DuckDuckGoForwarder 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: ApiOptions): Promise { + if (!options.token || options.token === "") { + const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name); + throw error; + } + + const url = "https://quack.duckduckgo.com/api/email/addresses"; + const request = new Request(url, { + redirect: "manual", + cache: "no-store", + method: "POST", + headers: new Headers({ + Authorization: "Bearer " + options.token, + "Content-Type": "application/json", + }), + }); + + const response = await this.apiService.nativeFetch(request); + if (response.status === 200 || response.status === 201) { + const json = await response.json(); + if (json.address) { + return `${json.address}@duck.com`; + } else { + const error = this.i18nService.t("forwarderUnknownError", Forwarders.DuckDuckGo.name); + throw error; + } + } else if (response.status === 401) { + const error = this.i18nService.t("forwaderInvalidToken", Forwarders.DuckDuckGo.name); + throw error; + } else { + const error = this.i18nService.t("forwarderUnknownError", Forwarders.DuckDuckGo.name); + throw error; + } + } +}