Skip to content

Commit c1018ce

Browse files
test(backend): add vitest validation tests for all route schemas
1 parent c6039d3 commit c1018ce

1 file changed

Lines changed: 264 additions & 0 deletions

File tree

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import { describe, it, expect } from "vitest";
2+
import { postMessageBody, getConversationQuery, getInboxQuery } from "../schemas/messages.js";
3+
import {
4+
getNotificationsQuery,
5+
postNotificationBody,
6+
patchNotificationReadParams,
7+
} from "../schemas/notifications.js";
8+
import { postMilestonesBody, postCoverLetterBody, postRewriteBody } from "../schemas/ai.js";
9+
import { postGaslessApplyBody } from "../schemas/gasless.js";
10+
import { uploadMilestoneBody } from "../schemas/upload.js";
11+
12+
const VALID_ADDR_A = "G" + "A".repeat(55);
13+
const VALID_ADDR_B = "G" + "B".repeat(55);
14+
const VALID_UUID = "123e4567-e89b-12d3-a456-426614174000";
15+
16+
// ── messages ─────────────────────────────────────────────────────────────────
17+
18+
describe("postMessageBody", () => {
19+
it("accepts valid payload", () => {
20+
const result = postMessageBody.safeParse({
21+
sender_address: VALID_ADDR_A,
22+
recipient_address: VALID_ADDR_B,
23+
content: "hello",
24+
});
25+
expect(result.success).toBe(true);
26+
});
27+
28+
it("rejects missing content", () => {
29+
const result = postMessageBody.safeParse({
30+
sender_address: VALID_ADDR_A,
31+
recipient_address: VALID_ADDR_B,
32+
content: "",
33+
});
34+
expect(result.success).toBe(false);
35+
});
36+
37+
it("rejects self-messaging", () => {
38+
const result = postMessageBody.safeParse({
39+
sender_address: VALID_ADDR_A,
40+
recipient_address: VALID_ADDR_A,
41+
content: "hi",
42+
});
43+
expect(result.success).toBe(false);
44+
});
45+
46+
it("rejects invalid Stellar address format", () => {
47+
const result = postMessageBody.safeParse({
48+
sender_address: "notastellaraddress",
49+
recipient_address: VALID_ADDR_B,
50+
content: "hello",
51+
});
52+
expect(result.success).toBe(false);
53+
});
54+
55+
it("rejects content exceeding 4000 chars", () => {
56+
const result = postMessageBody.safeParse({
57+
sender_address: VALID_ADDR_A,
58+
recipient_address: VALID_ADDR_B,
59+
content: "x".repeat(4001),
60+
});
61+
expect(result.success).toBe(false);
62+
});
63+
});
64+
65+
describe("getConversationQuery", () => {
66+
it("accepts valid a, b addresses", () => {
67+
const result = getConversationQuery.safeParse({ a: VALID_ADDR_A, b: VALID_ADDR_B });
68+
expect(result.success).toBe(true);
69+
});
70+
71+
it("rejects invalid address for a", () => {
72+
const result = getConversationQuery.safeParse({ a: "bad", b: VALID_ADDR_B });
73+
expect(result.success).toBe(false);
74+
});
75+
76+
it("accepts optional since as ISO datetime", () => {
77+
const result = getConversationQuery.safeParse({
78+
a: VALID_ADDR_A,
79+
b: VALID_ADDR_B,
80+
since: "2024-01-01T00:00:00.000Z",
81+
});
82+
expect(result.success).toBe(true);
83+
});
84+
85+
it("rejects since as non-datetime string", () => {
86+
const result = getConversationQuery.safeParse({
87+
a: VALID_ADDR_A,
88+
b: VALID_ADDR_B,
89+
since: "not-a-date",
90+
});
91+
expect(result.success).toBe(false);
92+
});
93+
});
94+
95+
describe("getInboxQuery", () => {
96+
it("accepts valid wallet", () => {
97+
expect(getInboxQuery.safeParse({ wallet: VALID_ADDR_A }).success).toBe(true);
98+
});
99+
100+
it("rejects wallet starting with wrong character", () => {
101+
expect(getInboxQuery.safeParse({ wallet: "XABC" + "D".repeat(52) }).success).toBe(false);
102+
});
103+
104+
it("rejects missing wallet", () => {
105+
expect(getInboxQuery.safeParse({}).success).toBe(false);
106+
});
107+
});
108+
109+
// ── notifications ─────────────────────────────────────────────────────────────
110+
111+
describe("getNotificationsQuery", () => {
112+
it("accepts valid 56-char G-address", () => {
113+
expect(getNotificationsQuery.safeParse({ wallet: VALID_ADDR_A }).success).toBe(true);
114+
});
115+
116+
it("rejects wallet that only starts with G but is too short", () => {
117+
expect(getNotificationsQuery.safeParse({ wallet: "GABC" }).success).toBe(false);
118+
});
119+
});
120+
121+
describe("postNotificationBody", () => {
122+
it("accepts valid payload", () => {
123+
const result = postNotificationBody.safeParse({
124+
wallet_address: VALID_ADDR_A,
125+
type: "payment",
126+
title: "Payment received",
127+
message: "You received 100 XLM",
128+
});
129+
expect(result.success).toBe(true);
130+
});
131+
132+
it("rejects missing type", () => {
133+
const result = postNotificationBody.safeParse({
134+
wallet_address: VALID_ADDR_A,
135+
title: "hello",
136+
message: "world",
137+
});
138+
expect(result.success).toBe(false);
139+
});
140+
141+
it("rejects title exceeding 200 chars", () => {
142+
const result = postNotificationBody.safeParse({
143+
wallet_address: VALID_ADDR_A,
144+
type: "t",
145+
title: "x".repeat(201),
146+
message: "msg",
147+
});
148+
expect(result.success).toBe(false);
149+
});
150+
151+
it("rejects invalid action_url", () => {
152+
const result = postNotificationBody.safeParse({
153+
wallet_address: VALID_ADDR_A,
154+
type: "t",
155+
title: "t",
156+
message: "m",
157+
action_url: "not-a-url",
158+
});
159+
expect(result.success).toBe(false);
160+
});
161+
});
162+
163+
describe("patchNotificationReadParams", () => {
164+
it("accepts valid UUID", () => {
165+
expect(patchNotificationReadParams.safeParse({ id: VALID_UUID }).success).toBe(true);
166+
});
167+
168+
it("rejects non-UUID string", () => {
169+
expect(patchNotificationReadParams.safeParse({ id: "not-a-uuid" }).success).toBe(false);
170+
});
171+
});
172+
173+
// ── AI ────────────────────────────────────────────────────────────────────────
174+
175+
describe("postMilestonesBody", () => {
176+
it("accepts valid payload with userPrompt", () => {
177+
const result = postMilestonesBody.safeParse({ userPrompt: "build a login page" });
178+
expect(result.success).toBe(true);
179+
});
180+
181+
it("rejects missing userPrompt", () => {
182+
expect(postMilestonesBody.safeParse({}).success).toBe(false);
183+
});
184+
185+
it("rejects userPrompt over 2000 chars", () => {
186+
expect(postMilestonesBody.safeParse({ userPrompt: "x".repeat(2001) }).success).toBe(false);
187+
});
188+
189+
it("applies default empty strings for optional fields", () => {
190+
const result = postMilestonesBody.safeParse({ userPrompt: "test" });
191+
expect(result.success).toBe(true);
192+
if (result.success) {
193+
expect(result.data.projectTitle).toBe("");
194+
expect(result.data.milestoneIndex).toBeNull();
195+
}
196+
});
197+
});
198+
199+
describe("postCoverLetterBody", () => {
200+
it("accepts valid payload", () => {
201+
const result = postCoverLetterBody.safeParse({ jobDescription: "Build a REST API" });
202+
expect(result.success).toBe(true);
203+
});
204+
205+
it("rejects missing jobDescription", () => {
206+
expect(postCoverLetterBody.safeParse({ jobTitle: "Dev" }).success).toBe(false);
207+
});
208+
});
209+
210+
describe("postRewriteBody", () => {
211+
it("accepts valid text", () => {
212+
expect(postRewriteBody.safeParse({ text: "Some project description" }).success).toBe(true);
213+
});
214+
215+
it("rejects empty text", () => {
216+
expect(postRewriteBody.safeParse({ text: "" }).success).toBe(false);
217+
});
218+
219+
it("rejects text over 5000 chars", () => {
220+
expect(postRewriteBody.safeParse({ text: "x".repeat(5001) }).success).toBe(false);
221+
});
222+
});
223+
224+
// ── gasless ───────────────────────────────────────────────────────────────────
225+
226+
describe("postGaslessApplyBody", () => {
227+
it("accepts valid XDR string", () => {
228+
const result = postGaslessApplyBody.safeParse({ signedTxXdr: "A".repeat(100) });
229+
expect(result.success).toBe(true);
230+
});
231+
232+
it("rejects signedTxXdr shorter than 48 chars", () => {
233+
expect(postGaslessApplyBody.safeParse({ signedTxXdr: "short" }).success).toBe(false);
234+
});
235+
236+
it("rejects missing signedTxXdr", () => {
237+
expect(postGaslessApplyBody.safeParse({}).success).toBe(false);
238+
});
239+
});
240+
241+
// ── upload ────────────────────────────────────────────────────────────────────
242+
243+
describe("uploadMilestoneBody", () => {
244+
it("accepts valid escrow_id and milestone_index", () => {
245+
const result = uploadMilestoneBody.safeParse({ escrow_id: "esc-123", milestone_index: "2" });
246+
expect(result.success).toBe(true);
247+
if (result.success) {
248+
expect(result.data.milestone_index).toBe(2);
249+
}
250+
});
251+
252+
it("applies defaults when fields are missing", () => {
253+
const result = uploadMilestoneBody.safeParse({});
254+
expect(result.success).toBe(true);
255+
if (result.success) {
256+
expect(result.data.escrow_id).toBe("unknown");
257+
expect(result.data.milestone_index).toBe(0);
258+
}
259+
});
260+
261+
it("rejects negative milestone_index", () => {
262+
expect(uploadMilestoneBody.safeParse({ milestone_index: "-1" }).success).toBe(false);
263+
});
264+
});

0 commit comments

Comments
 (0)