1import { expect, test, uid } from "../../../helpers/fixtures";
2import { expectDenied } from "../../../helpers/api";
3import { PASSWORD, personas } from "../../../helpers/personas";
4import { loginContext } from "../../../helpers/portal";
5import { createThrowawayAgent } from "../../../helpers/settings";
6
7const NEW_TICKET = "opendesk.opendesk.doctype.od_ticket.api.new";
8const RATE_LIMIT = 300;
9
10function ticketDoc() {
11 const subject = `E2E api ${uid()}`;
12 return { subject, description: `<p>${subject}</p>` };
13}
14
15test("malformed attachments are refused instead of crashing", async ({ apiAs }) => {
16 const customer = await apiAs("customer");
17
18 for (const attachments of [[{}], [null], ["abc"], [{ file_url: "/files/x.png" }], [{ x: 1 }]]) {
19 const response = await customer.raw(NEW_TICKET, { doc: ticketDoc(), attachments });
20 expect(response.status(), JSON.stringify(attachments)).toBe(417);
21 }
22});
23
24test("an image pasted twice leaves no upload of it orphaned", async ({ baseURL, api }) => {
25 const customer = await loginContext(baseURL!, personas.customer.email, PASSWORD);
26 // identical bytes share one URL; the first upload is an abandoned draft's
27 const buffer = Buffer.from(`inline ${uid()}`);
28 const uploads = [];
29 for (let i = 0; i < 3; i++) {
30 const response = await customer.post("/api/method/upload_file", {
31 multipart: { file: { name: "inline.png", mimeType: "image/png", buffer }, is_private: "1" },
32 });
33 uploads.push((await response.json()).message);
34 }
35 expect(new Set(uploads.map((file) => file.file_url)).size).toBe(1);
36
37 const img = `<img src="${uploads[0].file_url}">`;
38 const response = await customer.post(`/api/method/${NEW_TICKET}`, {
39 data: { doc: { ...ticketDoc(), description: `<p>${img}${img}</p>` } },
40 });
41 const ticket = (await response.json()).message;
42
43 const files = await api.list("File", {
44 filters: { name: ["in", uploads.map((file) => file.name)] },
45 fields: ["attached_to_doctype", "attached_to_name"],
46 });
47 expect(files).toEqual(
48 Array(3).fill({ attached_to_doctype: "OD Ticket", attached_to_name: String(ticket.name) })
49 );
50});
51
52test("a flood of ticket creates from one user is rate limited", async ({ baseURL, api, apiAs }) => {
53 const email = `e2e-flood-${uid()}@example.com`;
54 await createThrowawayAgent(api, email, PASSWORD);
55 const flooder = await loginContext(baseURL!, email, PASSWORD);
56
57 // malformed, so the flood is counted without creating tickets
58 const statuses = [];
59 for (let i = 0; i < RATE_LIMIT + 5; i++) {
60 const data = { doc: ticketDoc(), attachments: [{}] };
61 statuses.push((await flooder.post(`/api/method/${NEW_TICKET}`, { data })).status());
62 }
63
64 expect(statuses.filter((status) => status === 429)).toHaveLength(5);
65 // others keep their own budget
66 await (await apiAs("customer")).call(NEW_TICKET, { doc: ticketDoc() });
67});
68
69test("a customer cannot rate a ticket while raising it", async ({ api, apiAs }) => {
70 const doc = { ...ticketDoc(), feedback_rating: 1, feedback_extra: "great" };
71
72 await expectDenied((await apiAs("customer")).raw(NEW_TICKET, { doc }));
73 expect(await api.exists("OD Ticket", { subject: doc.subject })).toBeUndefined();
74});
75
76test("a customer's value for a read-only field is replaced by its default", async ({ api, apiAs }) => {
77 // same field as portal-new.spec.ts
78 if (!(await api.exists("Custom Field", { name: "OD Ticket-e2e_frozen" }))) {
79 await api.insert("Custom Field", {
80 dt: "OD Ticket",
81 fieldname: "e2e_frozen",
82 label: "E2E frozen",
83 fieldtype: "Select",
84 options: "No\nYes",
85 default: "No",
86 read_only: 1,
87 insert_after: "description",
88 });
89 }
90
91 const ticket = await (await apiAs("customer")).call(NEW_TICKET, {
92 doc: { ...ticketDoc(), e2e_frozen: "Yes" },
93 });
94
95 expect((await api.get("OD Ticket", ticket.name)).e2e_frozen).toBe("No");
96});
97