diff --git a/src/handlers/next/process-async-saleor-webhook.ts b/src/handlers/next/process-async-saleor-webhook.ts index 6d90232..b793d06 100644 --- a/src/handlers/next/process-async-saleor-webhook.ts +++ b/src/handlers/next/process-async-saleor-webhook.ts @@ -21,7 +21,8 @@ export type SaleorWebhookError = | "NOT_REGISTERED" | "SIGNATURE_VERIFICATION_FAILED" | "WRONG_METHOD" - | "CANT_BE_PARSED"; + | "CANT_BE_PARSED" + | "CONFIGURATION_ERROR"; export class WebhookError extends Error { errorType: SaleorWebhookError = "OTHER"; @@ -116,7 +117,7 @@ export const processAsyncSaleorWebhook: ProcessAsyncSaleorWebhook = async ({ parsedBody = JSON.parse(rawBody); } catch { debug("Request body cannot be parsed"); - throw new WebhookError("Request body cant be parsed", "CANT_BE_PARSED"); + throw new WebhookError("Request body can't be parsed", "CANT_BE_PARSED"); } // Check if domain is installed in the app diff --git a/src/handlers/next/saleor-async-webhook.test.ts b/src/handlers/next/saleor-async-webhook.test.ts index 0e4f315..b61bda5 100644 --- a/src/handlers/next/saleor-async-webhook.test.ts +++ b/src/handlers/next/saleor-async-webhook.test.ts @@ -1,7 +1,9 @@ +import { ASTNode } from "graphql"; import { createMocks } from "node-mocks-http"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { APL } from "../../APL"; +import { processAsyncSaleorWebhook } from "./process-async-saleor-webhook"; import { NextWebhookApiHandler, SaleorAsyncWebhook } from "./saleor-async-webhook"; const webhookPath = "api/webhooks/product-updated"; @@ -23,10 +25,40 @@ describe("SaleorAsyncWebhook", () => { isConfigured: vi.fn(), }; - const saleorAsyncWebhook = new SaleorAsyncWebhook({ + afterEach(async () => { + vi.restoreAllMocks(); + }); + + const validAsyncWebhookConfiguration = { apl: mockAPL, asyncEvent: "PRODUCT_UPDATED", webhookPath, + query: "subscription { event { ... on ProductUpdated { product { id }}}}", + }; + + const saleorAsyncWebhook = new SaleorAsyncWebhook(validAsyncWebhookConfiguration); + + it("throw CONFIGURATION_ERROR if query and subscriptionQueryAst are both absent", async () => { + expect(() => { + // eslint-disable-next-line no-new + new SaleorAsyncWebhook({ + ...validAsyncWebhookConfiguration, + // @ts-ignore: We make type error for test purpose + query: undefined, + subscriptionQueryAst: undefined, + }); + }).toThrowError(); + }); + + it("constructor passes if subscriptionQueryAst is provided", async () => { + expect(() => { + // eslint-disable-next-line no-new + new SaleorAsyncWebhook({ + ...validAsyncWebhookConfiguration, + query: undefined, + subscriptionQueryAst: {} as ASTNode, + }); + }).not.toThrowError(); }); it("targetUrl should return full path to the webhook route based on given baseUrl", async () => { @@ -39,18 +71,19 @@ describe("SaleorAsyncWebhook", () => { isActive: true, name: "PRODUCT_UPDATED webhook", targetUrl: "http://example.com/api/webhooks/product-updated", + query: "subscription { event { ... on ProductUpdated { product { id }}}}", }); }); it("Test createHandler which return success", async () => { // prepare mocked context returned by mocked process function - vi.mock("./process-async-saleor-webhook", () => ({ - processAsyncSaleorWebhook: vi.fn().mockResolvedValue({ - baseUrl: "example.com", - event: "product_updated", - payload: { data: "test_payload" }, - authData: { domain: "example.com", token: "token" }, - }), + vi.mock("./process-async-saleor-webhook"); + + vi.mocked(processAsyncSaleorWebhook).mockImplementationOnce(async () => ({ + baseUrl: "example.com", + event: "product_updated", + payload: { data: "test_payload" }, + authData: { domain: "example.com", token: "token" }, })); // Test handler - will throw error if mocked context is not passed to it diff --git a/src/handlers/next/saleor-async-webhook.ts b/src/handlers/next/saleor-async-webhook.ts index 6c33a05..340e129 100644 --- a/src/handlers/next/saleor-async-webhook.ts +++ b/src/handlers/next/saleor-async-webhook.ts @@ -15,16 +15,26 @@ import { const debug = createDebug("SaleorAsyncWebhook"); -export interface WebhookManifestConfiguration { +interface WebhookManifestConfigurationBase { name?: string; webhookPath: string; - subscriptionQueryAst?: ASTNode; - query?: string; asyncEvent: WebhookEvent; isActive?: boolean; apl: APL; } +interface WebhookManifestConfigurationWithAst extends WebhookManifestConfigurationBase { + subscriptionQueryAst: ASTNode; +} + +interface WebhookManifestConfigurationWithQuery extends WebhookManifestConfigurationBase { + query: string; +} + +type WebhookManifestConfiguration = + | WebhookManifestConfigurationWithAst + | WebhookManifestConfigurationWithQuery; + export const ErrorCodeMap: Record = { OTHER: 500, MISSING_HOST_HEADER: 400, @@ -38,6 +48,7 @@ export const ErrorCodeMap: Record = { SIGNATURE_VERIFICATION_FAILED: 401, WRONG_METHOD: 405, CANT_BE_PARSED: 400, + CONFIGURATION_ERROR: 500, }; export type NextWebhookApiHandler = ( @@ -61,18 +72,22 @@ export class SaleorAsyncWebhook { apl: APL; - constructor({ - name, - webhookPath, - subscriptionQueryAst, - query, - asyncEvent, - apl, - isActive = true, - }: WebhookManifestConfiguration) { + constructor(configuration: WebhookManifestConfiguration) { + const { name, webhookPath, asyncEvent, apl, isActive = true } = configuration; this.name = name || `${asyncEvent} webhook`; - this.subscriptionQueryAst = subscriptionQueryAst; - this.query = query; + if ("query" in configuration) { + this.query = configuration.query; + } + if ("subscriptionQueryAst" in configuration) { + this.subscriptionQueryAst = configuration.subscriptionQueryAst; + } + if (!this.subscriptionQueryAst && !this.query) { + throw new WebhookError( + "Need to specify `subscriptionQueryAst` or `query` to create webhook subscription", + "CONFIGURATION_ERROR" + ); + } + this.webhookPath = webhookPath; this.asyncEvent = asyncEvent; this.isActive = isActive;