subscriptionQueryAst or query argument has to be specified for the event subscription (#107)

This commit is contained in:
Krzysztof Wolski 2022-11-05 10:35:01 +01:00 committed by GitHub
parent 33d7666c7d
commit 83ed6719d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 74 additions and 25 deletions

View file

@ -21,7 +21,8 @@ export type SaleorWebhookError =
| "NOT_REGISTERED" | "NOT_REGISTERED"
| "SIGNATURE_VERIFICATION_FAILED" | "SIGNATURE_VERIFICATION_FAILED"
| "WRONG_METHOD" | "WRONG_METHOD"
| "CANT_BE_PARSED"; | "CANT_BE_PARSED"
| "CONFIGURATION_ERROR";
export class WebhookError extends Error { export class WebhookError extends Error {
errorType: SaleorWebhookError = "OTHER"; errorType: SaleorWebhookError = "OTHER";
@ -116,7 +117,7 @@ export const processAsyncSaleorWebhook: ProcessAsyncSaleorWebhook = async <T>({
parsedBody = JSON.parse(rawBody); parsedBody = JSON.parse(rawBody);
} catch { } catch {
debug("Request body cannot be parsed"); 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 // Check if domain is installed in the app

View file

@ -1,7 +1,9 @@
import { ASTNode } from "graphql";
import { createMocks } from "node-mocks-http"; 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 { APL } from "../../APL";
import { processAsyncSaleorWebhook } from "./process-async-saleor-webhook";
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "./saleor-async-webhook"; import { NextWebhookApiHandler, SaleorAsyncWebhook } from "./saleor-async-webhook";
const webhookPath = "api/webhooks/product-updated"; const webhookPath = "api/webhooks/product-updated";
@ -23,10 +25,40 @@ describe("SaleorAsyncWebhook", () => {
isConfigured: vi.fn(), isConfigured: vi.fn(),
}; };
const saleorAsyncWebhook = new SaleorAsyncWebhook({ afterEach(async () => {
vi.restoreAllMocks();
});
const validAsyncWebhookConfiguration = {
apl: mockAPL, apl: mockAPL,
asyncEvent: "PRODUCT_UPDATED", asyncEvent: "PRODUCT_UPDATED",
webhookPath, 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 () => { it("targetUrl should return full path to the webhook route based on given baseUrl", async () => {
@ -39,18 +71,19 @@ describe("SaleorAsyncWebhook", () => {
isActive: true, isActive: true,
name: "PRODUCT_UPDATED webhook", name: "PRODUCT_UPDATED webhook",
targetUrl: "http://example.com/api/webhooks/product-updated", targetUrl: "http://example.com/api/webhooks/product-updated",
query: "subscription { event { ... on ProductUpdated { product { id }}}}",
}); });
}); });
it("Test createHandler which return success", async () => { it("Test createHandler which return success", async () => {
// prepare mocked context returned by mocked process function // prepare mocked context returned by mocked process function
vi.mock("./process-async-saleor-webhook", () => ({ vi.mock("./process-async-saleor-webhook");
processAsyncSaleorWebhook: vi.fn().mockResolvedValue({
baseUrl: "example.com", vi.mocked(processAsyncSaleorWebhook).mockImplementationOnce(async () => ({
event: "product_updated", baseUrl: "example.com",
payload: { data: "test_payload" }, event: "product_updated",
authData: { domain: "example.com", token: "token" }, payload: { data: "test_payload" },
}), authData: { domain: "example.com", token: "token" },
})); }));
// Test handler - will throw error if mocked context is not passed to it // Test handler - will throw error if mocked context is not passed to it

View file

@ -15,16 +15,26 @@ import {
const debug = createDebug("SaleorAsyncWebhook"); const debug = createDebug("SaleorAsyncWebhook");
export interface WebhookManifestConfiguration { interface WebhookManifestConfigurationBase {
name?: string; name?: string;
webhookPath: string; webhookPath: string;
subscriptionQueryAst?: ASTNode;
query?: string;
asyncEvent: WebhookEvent; asyncEvent: WebhookEvent;
isActive?: boolean; isActive?: boolean;
apl: APL; apl: APL;
} }
interface WebhookManifestConfigurationWithAst extends WebhookManifestConfigurationBase {
subscriptionQueryAst: ASTNode;
}
interface WebhookManifestConfigurationWithQuery extends WebhookManifestConfigurationBase {
query: string;
}
type WebhookManifestConfiguration =
| WebhookManifestConfigurationWithAst
| WebhookManifestConfigurationWithQuery;
export const ErrorCodeMap: Record<SaleorWebhookError, number> = { export const ErrorCodeMap: Record<SaleorWebhookError, number> = {
OTHER: 500, OTHER: 500,
MISSING_HOST_HEADER: 400, MISSING_HOST_HEADER: 400,
@ -38,6 +48,7 @@ export const ErrorCodeMap: Record<SaleorWebhookError, number> = {
SIGNATURE_VERIFICATION_FAILED: 401, SIGNATURE_VERIFICATION_FAILED: 401,
WRONG_METHOD: 405, WRONG_METHOD: 405,
CANT_BE_PARSED: 400, CANT_BE_PARSED: 400,
CONFIGURATION_ERROR: 500,
}; };
export type NextWebhookApiHandler<TPayload = unknown, TResp = unknown> = ( export type NextWebhookApiHandler<TPayload = unknown, TResp = unknown> = (
@ -61,18 +72,22 @@ export class SaleorAsyncWebhook<TPayload = unknown> {
apl: APL; apl: APL;
constructor({ constructor(configuration: WebhookManifestConfiguration) {
name, const { name, webhookPath, asyncEvent, apl, isActive = true } = configuration;
webhookPath,
subscriptionQueryAst,
query,
asyncEvent,
apl,
isActive = true,
}: WebhookManifestConfiguration) {
this.name = name || `${asyncEvent} webhook`; this.name = name || `${asyncEvent} webhook`;
this.subscriptionQueryAst = subscriptionQueryAst; if ("query" in configuration) {
this.query = query; 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.webhookPath = webhookPath;
this.asyncEvent = asyncEvent; this.asyncEvent = asyncEvent;
this.isActive = isActive; this.isActive = isActive;