mirror of
https://github.com/mosh-hamedani/helpdesk.git
synced 2026-05-21 11:58:19 +02:00
Fix broken tests
This commit is contained in:
parent
af3c419c6c
commit
a7d4ed6a60
|
|
@ -65,6 +65,15 @@ The client proxies `/api/*` requests to the server via Vite config (target is co
|
||||||
- To add a new background job: create a queue with `boss.createQueue()`, register a worker with `boss.work()` in `startQueue()`, and export a `send*Job()` function
|
- To add a new background job: create a queue with `boss.createQueue()`, register a worker with `boss.work()` in `startQueue()`, and export a `send*Job()` function
|
||||||
- **Existing queues**:
|
- **Existing queues**:
|
||||||
- `classify-ticket` — classifies inbound tickets via GPT (retryLimit: 3, retryDelay: 30s, exponential backoff)
|
- `classify-ticket` — classifies inbound tickets via GPT (retryLimit: 3, retryDelay: 30s, exponential backoff)
|
||||||
|
- `auto-resolve-ticket` — attempts to auto-resolve tickets via GPT; if unsuccessful, transitions status to `open`
|
||||||
|
|
||||||
|
## Ticket Lifecycle
|
||||||
|
|
||||||
|
- Inbound emails arrive via the `/api/webhooks/inbound-email` endpoint (SendGrid multipart format) and are created with status `new`
|
||||||
|
- The system enqueues `classify-ticket` and `auto-resolve-ticket` background jobs automatically
|
||||||
|
- Status flow: `new` → `processing` (AI working) → `open` (if not auto-resolved) or `resolved` (if auto-resolved)
|
||||||
|
- `new` and `processing` tickets are system-managed and never shown in the agent UI — agents only see `open`, `resolved`, and `closed` tickets
|
||||||
|
- The `/api/tickets` endpoint excludes `new` and `processing` tickets by default (no `status` filter param)
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,15 +107,15 @@ describe("TicketsPage", () => {
|
||||||
expect(screen.getByText("Closed")).toBeInTheDocument();
|
expect(screen.getByText("Closed")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display category with underscores replaced by spaces", async () => {
|
it("should display category using category labels", async () => {
|
||||||
mockedAxios.get.mockResolvedValue(mockResponse());
|
mockedAxios.get.mockResolvedValue(mockResponse());
|
||||||
renderWithQuery(<TicketsPage />);
|
renderWithQuery(<TicketsPage />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText("technical question")).toBeInTheDocument();
|
expect(screen.getByText("Technical")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByText("refund request")).toBeInTheDocument();
|
expect(screen.getByText("Refund")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show dash for null category", async () => {
|
it("should show dash for null category", async () => {
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ export async function logout(page: Page) {
|
||||||
*/
|
*/
|
||||||
export async function expectLoginPage(page: Page) {
|
export async function expectLoginPage(page: Page) {
|
||||||
await expect(page).toHaveURL("/login");
|
await expect(page).toHaveURL("/login");
|
||||||
await expect(page.getByText("Helpdesk")).toBeVisible();
|
await expect(page.getByText("Welcome back")).toBeVisible();
|
||||||
await expect(page.getByText(/sign in to your account/i)).toBeVisible();
|
await expect(page.getByText(/sign in to your helpdesk account/i)).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ test.describe("Authentication", () => {
|
||||||
|
|
||||||
test("should display login form with all elements", async ({ page }) => {
|
test("should display login form with all elements", async ({ page }) => {
|
||||||
// Check page title and description
|
// Check page title and description
|
||||||
await expect(page.getByText("Helpdesk")).toBeVisible();
|
await expect(page.getByText("Welcome back")).toBeVisible();
|
||||||
await expect(page.getByText(/sign in to your account/i)).toBeVisible();
|
await expect(page.getByText(/sign in to your helpdesk account/i)).toBeVisible();
|
||||||
|
|
||||||
// Check form fields
|
// Check form fields
|
||||||
await expect(page.getByLabel("Email")).toBeVisible();
|
await expect(page.getByLabel("Email")).toBeVisible();
|
||||||
|
|
@ -129,18 +129,27 @@ test.describe("Authentication", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show loading state during login", async ({ page }) => {
|
test("should show loading state during login", async ({ page }) => {
|
||||||
|
// Delay the auth response so we can observe the loading state
|
||||||
|
let resolveDelay!: () => void;
|
||||||
|
const delay = new Promise<void>((resolve) => {
|
||||||
|
resolveDelay = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/api/auth/sign-in/email", async (route) => {
|
||||||
|
await delay;
|
||||||
|
await route.continue();
|
||||||
|
});
|
||||||
|
|
||||||
await page.getByLabel("Email").fill(TEST_USERS.admin.email);
|
await page.getByLabel("Email").fill(TEST_USERS.admin.email);
|
||||||
await page.getByLabel("Password").fill(TEST_USERS.admin.password);
|
await page.getByLabel("Password").fill(TEST_USERS.admin.password);
|
||||||
|
await page.getByRole("button", { name: /sign in/i }).click();
|
||||||
// Start login
|
|
||||||
const loginPromise = page
|
|
||||||
.getByRole("button", { name: /sign in/i })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Check loading state appears (button disabled with loading text)
|
// Check loading state appears (button disabled with loading text)
|
||||||
await expect(page.getByText(/signing in.../i)).toBeVisible();
|
await expect(page.getByText(/signing in.../i)).toBeVisible();
|
||||||
|
|
||||||
await loginPromise;
|
// Allow the request to complete
|
||||||
|
resolveDelay();
|
||||||
|
await page.unrouteAll({ behavior: "ignoreErrors" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should redirect to home if already authenticated", async ({
|
test("should redirect to home if already authenticated", async ({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect } from "@playwright/test";
|
import { test, expect, type APIRequestContext } from "@playwright/test";
|
||||||
import { loginAsAdmin } from "../fixtures/auth";
|
import { loginAsAdmin } from "../fixtures/auth";
|
||||||
import type { InboundEmailInput } from "core/schemas/tickets.ts";
|
import type { InboundEmailInput } from "core/schemas/tickets.ts";
|
||||||
|
|
||||||
|
|
@ -9,14 +9,22 @@ const API_BASE_URL = process.env.BETTER_AUTH_URL!;
|
||||||
* Creates a ticket via the inbound email webhook and returns the ticket object.
|
* Creates a ticket via the inbound email webhook and returns the ticket object.
|
||||||
*/
|
*/
|
||||||
async function createTicketViaWebhook(
|
async function createTicketViaWebhook(
|
||||||
request: Parameters<Parameters<typeof test>[1]>[0]["request"],
|
request: APIRequestContext,
|
||||||
payload: InboundEmailInput
|
payload: InboundEmailInput
|
||||||
) {
|
) {
|
||||||
|
const from = payload.fromName?.trim()
|
||||||
|
? `${payload.fromName} <${payload.from}>`
|
||||||
|
: payload.from;
|
||||||
const response = await request.post(
|
const response = await request.post(
|
||||||
`${API_BASE_URL}/api/webhooks/inbound-email`,
|
`${API_BASE_URL}/api/webhooks/inbound-email`,
|
||||||
{
|
{
|
||||||
headers: { "x-webhook-secret": WEBHOOK_SECRET },
|
headers: { "x-webhook-secret": WEBHOOK_SECRET },
|
||||||
data: payload,
|
multipart: {
|
||||||
|
from,
|
||||||
|
subject: payload.subject,
|
||||||
|
text: payload.body,
|
||||||
|
...(payload.bodyHtml ? { html: payload.bodyHtml } : {}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
expect(response.status()).toBe(201);
|
expect(response.status()).toBe(201);
|
||||||
|
|
@ -76,6 +84,9 @@ test.describe("Ticket Detail Page", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
await loginAsAdmin(page);
|
await loginAsAdmin(page);
|
||||||
|
await page.request.patch(`${API_BASE_URL}/api/tickets/${ticket.id}`, {
|
||||||
|
data: { status: "open" },
|
||||||
|
});
|
||||||
await page.goto(`/tickets/${ticket.id}`);
|
await page.goto(`/tickets/${ticket.id}`);
|
||||||
|
|
||||||
// Update status
|
// Update status
|
||||||
|
|
@ -107,10 +118,7 @@ test.describe("Ticket Detail Page", () => {
|
||||||
resp.request().method() === "PATCH" &&
|
resp.request().method() === "PATCH" &&
|
||||||
resp.status() === 200
|
resp.status() === 200
|
||||||
);
|
);
|
||||||
await page
|
await page.getByRole("combobox").nth(2).click();
|
||||||
.getByRole("combobox")
|
|
||||||
.filter({ hasText: "Unassigned" })
|
|
||||||
.click();
|
|
||||||
await page.getByRole("option", { name: /^Admin$/ }).click();
|
await page.getByRole("option", { name: /^Admin$/ }).click();
|
||||||
await assignPatch;
|
await assignPatch;
|
||||||
|
|
||||||
|
|
@ -170,6 +178,9 @@ test.describe("Ticket Detail Page", () => {
|
||||||
const ticket = await createTicketViaWebhook(request, payload);
|
const ticket = await createTicketViaWebhook(request, payload);
|
||||||
|
|
||||||
await loginAsAdmin(page);
|
await loginAsAdmin(page);
|
||||||
|
await page.request.patch(`${API_BASE_URL}/api/tickets/${ticket.id}`, {
|
||||||
|
data: { status: "open" },
|
||||||
|
});
|
||||||
|
|
||||||
// Navigate from list to detail
|
// Navigate from list to detail
|
||||||
await page.goto("/tickets");
|
await page.goto("/tickets");
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,21 @@ async function createTicketViaWebhook(
|
||||||
request: any,
|
request: any,
|
||||||
payload: Partial<InboundEmailInput> & { from: string; fromName: string; subject: string; body: string }
|
payload: Partial<InboundEmailInput> & { from: string; fromName: string; subject: string; body: string }
|
||||||
) {
|
) {
|
||||||
|
const from = payload.fromName?.trim()
|
||||||
|
? `${payload.fromName} <${payload.from}>`
|
||||||
|
: payload.from;
|
||||||
const response = await request.post(
|
const response = await request.post(
|
||||||
`${API_BASE_URL}/api/webhooks/inbound-email`,
|
`${API_BASE_URL}/api/webhooks/inbound-email`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: {
|
||||||
|
from,
|
||||||
|
subject: payload.subject,
|
||||||
|
text: payload.body,
|
||||||
|
...(payload.bodyHtml ? { html: payload.bodyHtml } : {}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -73,13 +81,17 @@ test.describe("Tickets Page", () => {
|
||||||
const ticket = await createTicketViaWebhook(request, payload);
|
const ticket = await createTicketViaWebhook(request, payload);
|
||||||
|
|
||||||
await loginAsAdmin(page);
|
await loginAsAdmin(page);
|
||||||
|
await page.request.patch(`${API_BASE_URL}/api/tickets/${ticket.id}`, {
|
||||||
|
data: { status: "open" },
|
||||||
|
});
|
||||||
|
|
||||||
await page.goto("/tickets");
|
await page.goto("/tickets");
|
||||||
|
|
||||||
const row = page.getByRole("row").filter({ hasText: ticket.subject });
|
const row = page.getByRole("row").filter({ hasText: ticket.subject });
|
||||||
await expect(row).toBeVisible();
|
await expect(row).toBeVisible();
|
||||||
await expect(row.getByText(payload.fromName)).toBeVisible();
|
await expect(row.getByText(payload.fromName)).toBeVisible();
|
||||||
await expect(row.getByText(payload.from)).toBeVisible();
|
await expect(row.getByText(payload.from)).toBeVisible();
|
||||||
await expect(row.locator("text=open").first()).toBeVisible();
|
await expect(row.locator("text=Open").first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should show newly created ticket after page reload", async ({ page, request }) => {
|
test("should show newly created ticket after page reload", async ({ page, request }) => {
|
||||||
|
|
@ -89,6 +101,9 @@ test.describe("Tickets Page", () => {
|
||||||
const uniqueId = `refresh-${Date.now()}`;
|
const uniqueId = `refresh-${Date.now()}`;
|
||||||
const payload = createTestPayload(uniqueId);
|
const payload = createTestPayload(uniqueId);
|
||||||
const ticket = await createTicketViaWebhook(request, payload);
|
const ticket = await createTicketViaWebhook(request, payload);
|
||||||
|
await page.request.patch(`${API_BASE_URL}/api/tickets/${ticket.id}`, {
|
||||||
|
data: { status: "open" },
|
||||||
|
});
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,22 @@ import type { InboundEmailInput } from "core/schemas/tickets.ts";
|
||||||
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
|
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
|
||||||
const API_BASE_URL = process.env.BETTER_AUTH_URL!;
|
const API_BASE_URL = process.env.BETTER_AUTH_URL!;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an InboundEmailInput to multipart form fields matching SendGrid's format
|
||||||
|
*/
|
||||||
|
function toMultipart(payload: InboundEmailInput): Record<string, string> {
|
||||||
|
const from = payload.fromName?.trim()
|
||||||
|
? `${payload.fromName} <${payload.from}>`
|
||||||
|
: (payload.from ?? "");
|
||||||
|
const result: Record<string, string> = {
|
||||||
|
from,
|
||||||
|
subject: payload.subject,
|
||||||
|
text: payload.body,
|
||||||
|
};
|
||||||
|
if (payload.bodyHtml) result.html = payload.bodyHtml;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to create a valid inbound email payload
|
* Helper to create a valid inbound email payload
|
||||||
*/
|
*/
|
||||||
|
|
@ -30,7 +46,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
const response = await request.post(
|
const response = await request.post(
|
||||||
`${API_BASE_URL}/api/webhooks/inbound-email`,
|
`${API_BASE_URL}/api/webhooks/inbound-email`,
|
||||||
{
|
{
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -52,7 +68,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": "wrong-secret",
|
"x-webhook-secret": "wrong-secret",
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -71,7 +87,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
const response = await request.post(
|
const response = await request.post(
|
||||||
`${API_BASE_URL}/api/webhooks/inbound-email?secret=wrong-secret`,
|
`${API_BASE_URL}/api/webhooks/inbound-email?secret=wrong-secret`,
|
||||||
{
|
{
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -93,7 +109,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -110,7 +126,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
const response = await request.post(
|
const response = await request.post(
|
||||||
`${API_BASE_URL}/api/webhooks/inbound-email?secret=${WEBHOOK_SECRET}`,
|
`${API_BASE_URL}/api/webhooks/inbound-email?secret=${WEBHOOK_SECRET}`,
|
||||||
{
|
{
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -132,7 +148,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -156,7 +172,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -164,7 +180,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
expect(response.status()).toBe(400);
|
expect(response.status()).toBe(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should reject request with empty fromName", async ({ request }) => {
|
test("should use email as sender name when from field has no name", async ({ request }) => {
|
||||||
const payload = createValidPayload({ fromName: "" });
|
const payload = createValidPayload({ fromName: "" });
|
||||||
|
|
||||||
const response = await request.post(
|
const response = await request.post(
|
||||||
|
|
@ -173,18 +189,16 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status()).toBe(400);
|
expect(response.status()).toBe(201);
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
expect(body).toHaveProperty("error");
|
expect(body.ticket.senderName).toBe(payload.from);
|
||||||
expect(body.error).toMatch(/sender name is required/i);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should reject request with whitespace-only fromName", async ({
|
test("should use email as sender name when from field has whitespace-only name", async ({
|
||||||
request,
|
request,
|
||||||
}) => {
|
}) => {
|
||||||
const payload = createValidPayload({ fromName: " " });
|
const payload = createValidPayload({ fromName: " " });
|
||||||
|
|
@ -195,15 +209,13 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status()).toBe(400);
|
expect(response.status()).toBe(201);
|
||||||
const body = await response.json();
|
const body = await response.json();
|
||||||
expect(body).toHaveProperty("error");
|
expect(body.ticket.senderName).toBe(payload.from);
|
||||||
expect(body.error).toMatch(/sender name is required/i);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should reject request with empty subject", async ({ request }) => {
|
test("should reject request with empty subject", async ({ request }) => {
|
||||||
|
|
@ -215,7 +227,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -237,7 +249,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -257,7 +269,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
failOnStatusCode: false,
|
failOnStatusCode: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -280,7 +292,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -309,7 +321,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -324,7 +336,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
expect(ticket.body).toBe(payload.body);
|
expect(ticket.body).toBe(payload.body);
|
||||||
expect(ticket.senderName).toBe(payload.fromName);
|
expect(ticket.senderName).toBe(payload.fromName);
|
||||||
expect(ticket.senderEmail).toBe(payload.from);
|
expect(ticket.senderEmail).toBe(payload.from);
|
||||||
expect(ticket.status).toBe("open");
|
expect(ticket.status).toBe("new");
|
||||||
expect(ticket.category).toBeNull();
|
expect(ticket.category).toBeNull();
|
||||||
expect(ticket.bodyHtml).toBeNull(); // Not provided in payload
|
expect(ticket.bodyHtml).toBeNull(); // Not provided in payload
|
||||||
|
|
||||||
|
|
@ -353,7 +365,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: payload,
|
multipart: toMultipart(payload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -387,7 +399,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: firstPayload,
|
multipart: toMultipart(firstPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -409,7 +421,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: secondPayload,
|
multipart: toMultipart(secondPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -444,7 +456,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: firstPayload,
|
multipart: toMultipart(firstPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -466,7 +478,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: replyPayload,
|
multipart: toMultipart(replyPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -497,7 +509,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: firstPayload,
|
multipart: toMultipart(firstPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -519,7 +531,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: forwardPayload,
|
multipart: toMultipart(forwardPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -549,7 +561,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: firstPayload,
|
multipart: toMultipart(firstPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -569,7 +581,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: replyPayload,
|
multipart: toMultipart(replyPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -598,7 +610,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: firstPayload,
|
multipart: toMultipart(firstPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -618,7 +630,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: lowercasePayload,
|
multipart: toMultipart(lowercasePayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -647,7 +659,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: firstPayload,
|
multipart: toMultipart(firstPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -668,7 +680,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: secondPayload,
|
multipart: toMultipart(secondPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -699,7 +711,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: firstPayload,
|
multipart: toMultipart(firstPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -720,7 +732,7 @@ test.describe("Webhook: Inbound Email", () => {
|
||||||
headers: {
|
headers: {
|
||||||
"x-webhook-secret": WEBHOOK_SECRET,
|
"x-webhook-secret": WEBHOOK_SECRET,
|
||||||
},
|
},
|
||||||
data: secondPayload,
|
multipart: toMultipart(secondPayload),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ router.post("/inbound-email", requireWebhookSecret, upload.any(), async (req, re
|
||||||
const existingTicket = await prisma.ticket.findFirst({
|
const existingTicket = await prisma.ticket.findFirst({
|
||||||
where: {
|
where: {
|
||||||
senderEmail: data.from,
|
senderEmail: data.from,
|
||||||
status: "open",
|
status: { notIn: ["resolved", "closed"] },
|
||||||
subject: { equals: normalizedSubject, mode: "insensitive" },
|
subject: { equals: normalizedSubject, mode: "insensitive" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue