187 lines
5.4 KiB
TypeScript
187 lines
5.4 KiB
TypeScript
|
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||
|
|
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||
|
|
|
||
|
|
const mocks = vi.hoisted(() => ({
|
||
|
|
checkout: vi.fn(),
|
||
|
|
unauthenticatedAdmin: vi.fn(),
|
||
|
|
runActivityCheck: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("../app/shopify.server", () => ({
|
||
|
|
authenticate: {
|
||
|
|
public: {
|
||
|
|
checkout: mocks.checkout,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
unauthenticated: {
|
||
|
|
admin: mocks.unauthenticatedAdmin,
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("../app/services/activity.server", () => ({
|
||
|
|
runActivityCheck: mocks.runActivityCheck,
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("../app/services/config.server", () => ({
|
||
|
|
getActivityConfig: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("../app/services/dreame-client.server", () => ({
|
||
|
|
callPush: vi.fn(),
|
||
|
|
callStatus: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("../app/services/idempotency.server", () => ({
|
||
|
|
findSuccessfulPush: vi.fn(),
|
||
|
|
recordSuccessfulPush: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("../app/services/logger.server", () => ({
|
||
|
|
logActivityPush: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock("../app/services/order.server", () => ({
|
||
|
|
getOrderDetails: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
function cors(response: Response): Response {
|
||
|
|
response.headers.set("X-Cors-Applied", "true");
|
||
|
|
return response;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function postAction(body: unknown): Promise<Response> {
|
||
|
|
const { action } = await import("../app/routes/api.action.activity.check");
|
||
|
|
return action({
|
||
|
|
request: new Request("https://app.example.com/api/action/activity/check", {
|
||
|
|
method: "POST",
|
||
|
|
headers: {
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
},
|
||
|
|
body: JSON.stringify(body),
|
||
|
|
}),
|
||
|
|
context: {},
|
||
|
|
params: {},
|
||
|
|
} as ActionFunctionArgs) as Promise<Response>;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("activity check route", () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
mocks.checkout.mockResolvedValue({
|
||
|
|
sessionToken: {
|
||
|
|
dest: "https://demo.myshopify.com",
|
||
|
|
},
|
||
|
|
cors,
|
||
|
|
});
|
||
|
|
mocks.unauthenticatedAdmin.mockResolvedValue({
|
||
|
|
admin: {
|
||
|
|
graphql: vi.fn(),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
mocks.runActivityCheck.mockResolvedValue({
|
||
|
|
success: true,
|
||
|
|
redirectUrl: "https://activity.example.com",
|
||
|
|
buttonText: "Claim",
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test("wraps success responses with Shopify cors and no-store cache headers", async () => {
|
||
|
|
const response = await postAction({
|
||
|
|
orderId: "gid://shopify/Order/123",
|
||
|
|
orderNumber: "#1001",
|
||
|
|
locale: "en",
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(200);
|
||
|
|
expect(response.headers.get("X-Cors-Applied")).toBe("true");
|
||
|
|
expect(response.headers.get("Cache-Control")).toContain("no-store");
|
||
|
|
await expect(response.json()).resolves.toEqual({
|
||
|
|
success: true,
|
||
|
|
redirectUrl: "https://activity.example.com",
|
||
|
|
buttonText: "Claim",
|
||
|
|
});
|
||
|
|
expect(mocks.runActivityCheck).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
shop: "demo.myshopify.com",
|
||
|
|
orderId: "gid://shopify/Order/123",
|
||
|
|
}),
|
||
|
|
expect.any(Object),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("rejects checkout tokens without a myshopify shop context", async () => {
|
||
|
|
mocks.checkout.mockResolvedValue({
|
||
|
|
sessionToken: {
|
||
|
|
dest: "https://not-shop.example.com",
|
||
|
|
},
|
||
|
|
cors,
|
||
|
|
});
|
||
|
|
|
||
|
|
const response = await postAction({
|
||
|
|
orderId: "gid://shopify/Order/123",
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(401);
|
||
|
|
expect(response.headers.get("X-Cors-Applied")).toBe("true");
|
||
|
|
expect(response.headers.get("Cache-Control")).toContain("no-store");
|
||
|
|
await expect(response.json()).resolves.toEqual({
|
||
|
|
success: false,
|
||
|
|
error: {
|
||
|
|
code: "INVALID_SHOP_CONTEXT",
|
||
|
|
message: "A valid Shopify shop context is required.",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect(mocks.unauthenticatedAdmin).not.toHaveBeenCalled();
|
||
|
|
expect(mocks.runActivityCheck).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("returns a bad request when orderId is missing", async () => {
|
||
|
|
const response = await postAction({
|
||
|
|
orderNumber: "#1001",
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(400);
|
||
|
|
expect(response.headers.get("X-Cors-Applied")).toBe("true");
|
||
|
|
expect(response.headers.get("Cache-Control")).toContain("no-store");
|
||
|
|
await expect(response.json()).resolves.toEqual({
|
||
|
|
success: false,
|
||
|
|
error: {
|
||
|
|
code: "ORDER_ID_REQUIRED",
|
||
|
|
message: "orderId is required.",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
expect(mocks.runActivityCheck).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("preserves Shopify authentication failures as HTTP errors", async () => {
|
||
|
|
mocks.checkout.mockRejectedValue(new Response("Unauthorized", { status: 401 }));
|
||
|
|
|
||
|
|
const response = await postAction({
|
||
|
|
orderId: "gid://shopify/Order/123",
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(response.status).toBe(401);
|
||
|
|
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
|
||
|
|
expect(response.headers.get("Cache-Control")).toContain("no-store");
|
||
|
|
expect(await response.text()).toBe("Unauthorized");
|
||
|
|
expect(mocks.unauthenticatedAdmin).not.toHaveBeenCalled();
|
||
|
|
expect(mocks.runActivityCheck).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("uses Shopify checkout authentication for OPTIONS responses", async () => {
|
||
|
|
const { loader } = await import("../app/routes/api.action.activity.check");
|
||
|
|
const response = await loader({
|
||
|
|
request: new Request("https://app.example.com/api/action/activity/check", {
|
||
|
|
method: "OPTIONS",
|
||
|
|
}),
|
||
|
|
context: {},
|
||
|
|
params: {},
|
||
|
|
} as LoaderFunctionArgs);
|
||
|
|
|
||
|
|
expect(response.status).toBe(204);
|
||
|
|
expect(response.headers.get("X-Cors-Applied")).toBe("true");
|
||
|
|
expect(response.headers.get("Cache-Control")).toContain("no-store");
|
||
|
|
expect(mocks.checkout).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
});
|