diff --git a/src/APL/has-auth-data.ts b/src/APL/has-auth-data.ts index 3edc3d3..0901c66 100644 --- a/src/APL/has-auth-data.ts +++ b/src/APL/has-auth-data.ts @@ -5,6 +5,10 @@ import { hasProp } from "../has-prop"; */ export const hasAuthData = (data: unknown) => hasProp(data, "domain") && + data.domain && hasProp(data, "token") && + data.token && hasProp(data, "appId") && - hasProp(data, "saleorApiUrl"); + data.appId && + hasProp(data, "saleorApiUrl") && + data.saleorApiUrl; diff --git a/src/APL/saleor-cloud-apl.test.ts b/src/APL/saleor-cloud-apl.test.ts new file mode 100644 index 0000000..2579f4f --- /dev/null +++ b/src/APL/saleor-cloud-apl.test.ts @@ -0,0 +1,120 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { AuthData } from "./apl"; +import { SaleorCloudAPL, SaleorCloudAPLConfig } from "./saleor-cloud-apl"; + +const fetchMock = vi.fn(); + +vi.stubGlobal("fetch", fetchMock); + +const aplConfig: SaleorCloudAPLConfig = { + resourceUrl: "https://example.com", + token: "token", +}; + +const stubAuthData: AuthData = { + domain: "example.com", + token: "example-token", + saleorApiUrl: "https://example.com/graphql/", + appId: "42", + jwks: "{}", +}; + +describe("APL", () => { + afterEach(() => { + vi.resetModules(); + vi.restoreAllMocks(); + }); + + describe("SaleorCloudAPL", () => { + describe("set", () => { + it("Successful save of the auth data", async () => { + fetchMock.mockResolvedValue({ + status: 200, + json: async () => ({ result: "ok" }), + ok: true, + }); + + const apl = new SaleorCloudAPL(aplConfig); + await apl.set(stubAuthData); + + expect(fetchMock).toBeCalledWith( + "https://example.com", + + { + body: JSON.stringify({ + saleor_app_id: "42", + saleor_api_url: "https://example.com/graphql/", + jwks: "{}", + domain: "example.com", + token: "example-token", + }), + headers: { + "Content-Type": "application/json", + Authorization: "Bearer token", + }, + method: "POST", + } + ); + }); + + it("Raise error when register service returns non 200 response", async () => { + fetchMock.mockResolvedValue({ + status: 500, + ok: false, + }); + + const apl = new SaleorCloudAPL(aplConfig); + + await expect(apl.set(stubAuthData)).rejects.toThrow( + "Fetch returned with non 200 status code 500" + ); + }); + }); + + describe("get", () => { + describe("Read existing auth data", () => { + it("Read existing auth data", async () => { + fetchMock.mockResolvedValue({ + status: 200, + ok: true, + json: async () => ({ + saleor_app_id: stubAuthData.appId, + saleor_api_url: stubAuthData.saleorApiUrl, + jwks: stubAuthData.jwks, + domain: stubAuthData.domain, + token: stubAuthData.token, + }), + }); + + const apl = new SaleorCloudAPL(aplConfig); + + expect(await apl.get(stubAuthData.saleorApiUrl)).toStrictEqual(stubAuthData); + + expect(fetchMock).toBeCalledWith( + "https://example.com/aHR0cHM6Ly9leGFtcGxlLmNvbS9ncmFwaHFsLw", // base64 encoded api url + { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer token", + }, + method: "GET", + } + ); + }); + + it("Return undefined when unknown domain requested", async () => { + fetchMock.mockResolvedValue({ + status: 404, + ok: false, + json: async () => undefined, + }); + + const apl = new SaleorCloudAPL(aplConfig); + + expect(await apl.get("http://unknown-domain.example.com/graphql/")).toBe(undefined); + }); + }); + }); + }); +}); diff --git a/src/APL/saleor-cloud-apl.ts b/src/APL/saleor-cloud-apl.ts index a04298b..e2192aa 100644 --- a/src/APL/saleor-cloud-apl.ts +++ b/src/APL/saleor-cloud-apl.ts @@ -10,6 +10,11 @@ export type SaleorCloudAPLConfig = { }; const validateResponseStatus = (response: Response) => { + if (response.status === 404) { + debug("Auth data not found"); + + throw new Error("Auth data not found"); + } if (!response.ok) { debug("Response failed with status %s", response.status); @@ -56,8 +61,8 @@ export class SaleorCloudAPL implements APL { } private getUrlForDomain(saleorApiUrl: string) { - // API URL has to be base64 encoded - return `${this.resourceUrl}/${btoa(saleorApiUrl)}`; + // API URL has to be base64url encoded + return `${this.resourceUrl}/${Buffer.from(saleorApiUrl).toString("base64url")}`; } async get(saleorApiUrl: string): Promise { @@ -68,10 +73,20 @@ export class SaleorCloudAPL implements APL { headers: { "Content-Type": "application/json", ...this.headers }, }).catch((error) => { debug("Failed to reach API call: %s", error?.message ?? "Unknown error"); - throw new Error(`Attempt in fetch the data resulted with error: ${error}`); + return undefined; }); - validateResponseStatus(response); + if (!response) { + debug("No response from the API"); + return undefined; + } + + try { + validateResponseStatus(response); + } catch { + debug("Response status not valid"); + return undefined; + } const parsedResponse = (await response.json().catch((e) => { debug("Failed to parse response: %s", e?.message ?? "Unknown error");