fix: remove nested .git folders, re-add as normal directories
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
import { describe, test, expect, beforeEach } from "bun:test";
|
||||
import { Hono } from "hono";
|
||||
import { apiResponse } from "../../src/modules/api-utils/apiResponse";
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
|
||||
/**
|
||||
* Tests for the CW callback route handler.
|
||||
*
|
||||
* We import the route handler and mount it on a Hono app to test via
|
||||
* the app.request() convenience method.
|
||||
*/
|
||||
|
||||
// We need to test the internal helper functions. Since they are not
|
||||
// exported, we test them through the route handler's observable behavior.
|
||||
import callbackRoute from "../../src/api/cw/callback";
|
||||
|
||||
describe("CW callback route handler", () => {
|
||||
let app: Hono;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new Hono();
|
||||
// Replicate the error handling from server.ts
|
||||
app.onError((err, c) => {
|
||||
if ((err as any).status) {
|
||||
const body = apiResponse.error(err);
|
||||
return c.json(body, body.status as ContentfulStatusCode);
|
||||
}
|
||||
return c.json(apiResponse.internalError(), 500);
|
||||
});
|
||||
app.route("/", callbackRoute);
|
||||
// Clear the env var before each test
|
||||
delete (process.env as Record<string, any>).CW_CALLBACK_SECRET;
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Secret validation
|
||||
// -------------------------------------------------------------------
|
||||
test("rejects when secret does not match CW_CALLBACK_SECRET", async () => {
|
||||
process.env.CW_CALLBACK_SECRET = "correct-secret";
|
||||
const res = await app.request("/callback/wrong-secret/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(res.status).toBe(401);
|
||||
const body = await res.json();
|
||||
expect(body.message).toContain("Invalid callback secret");
|
||||
});
|
||||
|
||||
test("accepts when secret matches CW_CALLBACK_SECRET", async () => {
|
||||
process.env.CW_CALLBACK_SECRET = "correct-secret";
|
||||
const res = await app.request("/callback/correct-secret/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ Action: "updated", ID: 123 }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
test("accepts any secret when CW_CALLBACK_SECRET is not configured", async () => {
|
||||
const res = await app.request("/callback/any-secret/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ Action: "created" }),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Resource validation
|
||||
// -------------------------------------------------------------------
|
||||
test("accepts 'opportunity' resource", async () => {
|
||||
const res = await app.request("/callback/test/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.resource).toBe("opportunity");
|
||||
});
|
||||
|
||||
test("accepts 'ticket' resource", async () => {
|
||||
const res = await app.request("/callback/test/ticket", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.resource).toBe("ticket");
|
||||
});
|
||||
|
||||
test("accepts 'company' resource", async () => {
|
||||
const res = await app.request("/callback/test/company", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.resource).toBe("company");
|
||||
});
|
||||
|
||||
test("accepts 'activity' resource", async () => {
|
||||
const res = await app.request("/callback/test/activity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.resource).toBe("activity");
|
||||
});
|
||||
|
||||
test("rejects invalid resource type", async () => {
|
||||
const res = await app.request("/callback/test/invalidtype", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
// Zod validation should fail
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Body parsing
|
||||
// -------------------------------------------------------------------
|
||||
test("parses JSON body fields", async () => {
|
||||
const res = await app.request("/callback/test/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "updated",
|
||||
Type: "opportunity",
|
||||
ID: 42,
|
||||
MemberId: "jroberts",
|
||||
MessageId: "msg-123",
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.summary.action).toBe("updated");
|
||||
expect(body.data.summary.type).toBe("opportunity");
|
||||
expect(body.data.summary.id).toBe(42);
|
||||
expect(body.data.summary.memberId).toBe("jroberts");
|
||||
expect(body.data.summary.messageId).toBe("msg-123");
|
||||
});
|
||||
|
||||
test("parses Entity field from JSON string", async () => {
|
||||
const entity = {
|
||||
CompanyName: "Acme Corp",
|
||||
StatusName: "Active",
|
||||
UpdatedBy: "admin",
|
||||
};
|
||||
const res = await app.request("/callback/test/company", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "updated",
|
||||
Entity: JSON.stringify(entity),
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.summary.entitySummary).toBe("Acme Corp");
|
||||
expect(body.data.summary.entityStatus).toBe("Active");
|
||||
expect(body.data.summary.entityUpdatedBy).toBe("admin");
|
||||
});
|
||||
|
||||
test("handles Entity as inline object", async () => {
|
||||
const res = await app.request("/callback/test/company", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
Action: "created",
|
||||
Entity: { CompanyName: "Direct Corp", Status: "New" },
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.summary.entitySummary).toBe("Direct Corp");
|
||||
expect(body.data.summary.entityStatus).toBe("New");
|
||||
});
|
||||
|
||||
test("returns secretValidated field based on env presence", async () => {
|
||||
delete (process.env as Record<string, any>).CW_CALLBACK_SECRET;
|
||||
const res = await app.request("/callback/test/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.data.secretValidated).toBe(false);
|
||||
|
||||
process.env.CW_CALLBACK_SECRET = "secret";
|
||||
const res2 = await app.request("/callback/secret/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const body2 = await res2.json();
|
||||
expect(body2.data.secretValidated).toBe(true);
|
||||
});
|
||||
|
||||
test("returns receivedAt timestamp", async () => {
|
||||
const res = await app.request("/callback/test/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(body.data.receivedAt).toBeDefined();
|
||||
// Should be a valid ISO date string
|
||||
expect(new Date(body.data.receivedAt).toISOString()).toBe(
|
||||
body.data.receivedAt,
|
||||
);
|
||||
});
|
||||
|
||||
test("handles non-JSON body gracefully", async () => {
|
||||
const res = await app.request("/callback/test/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
body: "this is not json",
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.summary).toBeNull();
|
||||
});
|
||||
|
||||
test("handles empty body gracefully", async () => {
|
||||
const res = await app.request("/callback/test/opportunity", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: "",
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.data.summary).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user