dreame-action/test/activity-route.test.ts

187 lines
5.4 KiB
TypeScript
Raw Normal View History

2026-06-19 19:35:50 +08:00
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();
});
});