Skip to content

Commit aeca102

Browse files
authored
Merge pull request Stellar-Mail#1263 from ALIPHATICHYD/feature/228-schema-validation-tests
Add unit tests for schema validation (Stellar-Mail#228)
2 parents 6d972ed + e654e44 commit aeca102

2 files changed

Lines changed: 270 additions & 0 deletions

File tree

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
truncateHash,
4+
formatLatency,
5+
formatPostageStatus,
6+
isValidMockHash,
7+
isValidDiagnosticId,
8+
formatProofSummary,
9+
validateProofRecord,
10+
} from "../proofFormatting";
11+
import { demoProofRecords } from "../fixtures/proofRecordFixtures";
12+
import type { ProofRecord } from "../types/proofRecord";
13+
14+
describe("truncateHash", () => {
15+
it("truncates standard hex hash with default prefix/suffix lengths", () => {
16+
const hash = "0xabcdef1234567890abcdef1234567890abcdef";
17+
expect(truncateHash(hash)).toBe("0xabcdef\u2026cdef");
18+
});
19+
20+
it("handles hashes without 0x prefix by adding it in the result", () => {
21+
const hash = "abcdef1234567890abcdef1234567890abcdef";
22+
expect(truncateHash(hash)).toBe("0xabcdef\u2026cdef");
23+
});
24+
25+
it("returns original hash if body length is less than or equal to prefix + suffix", () => {
26+
const shortHash = "0x123456";
27+
expect(truncateHash(shortHash, 4, 4)).toBe("0x123456");
28+
});
29+
30+
it("supports custom prefix and suffix lengths", () => {
31+
const hash = "0xabcdef1234567890";
32+
expect(truncateHash(hash, 4, 2)).toBe("0xabcd\u202690");
33+
});
34+
});
35+
36+
describe("formatLatency", () => {
37+
it("normalises latency strings to lowercase and trims whitespace", () => {
38+
expect(formatLatency(" 42MS ")).toBe("42ms");
39+
expect(formatLatency("15ms")).toBe("15ms");
40+
});
41+
});
42+
43+
describe("formatPostageStatus", () => {
44+
it("returns human-readable labels for each postage status", () => {
45+
expect(formatPostageStatus("pending")).toBe("Pending");
46+
expect(formatPostageStatus("settled")).toBe("Settled");
47+
expect(formatPostageStatus("refunded")).toBe("Refunded");
48+
});
49+
});
50+
51+
describe("isValidMockHash", () => {
52+
it("returns true for valid hex strings starting with 0x", () => {
53+
expect(isValidMockHash("0x1234567890abcdef")).toBe(true);
54+
expect(isValidMockHash("0xABCDEF")).toBe(true);
55+
expect(isValidMockHash(" 0x12ab ")).toBe(true); // trims whitespace
56+
});
57+
58+
it("returns false for non-hex, empty, or missing 0x prefix", () => {
59+
expect(isValidMockHash("1234567890abcdef")).toBe(false);
60+
expect(isValidMockHash("0x123g")).toBe(false);
61+
expect(isValidMockHash("")).toBe(false);
62+
});
63+
});
64+
65+
describe("isValidDiagnosticId", () => {
66+
it("returns true for valid UUID format", () => {
67+
expect(isValidDiagnosticId("12345678-abcd-1234-abcd-1234567890ab")).toBe(true);
68+
expect(isValidDiagnosticId(" 12345678-abcd-1234-abcd-1234567890ab ")).toBe(true); // trims
69+
});
70+
71+
it("returns false for invalid UUID formatting", () => {
72+
expect(isValidDiagnosticId("12345678-abcd-1234-abcd")).toBe(false);
73+
expect(isValidDiagnosticId("invalid-uuid-string")).toBe(false);
74+
expect(isValidDiagnosticId("")).toBe(false);
75+
});
76+
});
77+
78+
describe("formatProofSummary", () => {
79+
it("builds a formatted one-line summary", () => {
80+
const record: ProofRecord = demoProofRecords[0];
81+
const summary = formatProofSummary(record);
82+
// msg=0xabc…1234 | pay=0xdef…5678 | settled | 42ms
83+
expect(summary).toContain("msg=");
84+
expect(summary).toContain("pay=");
85+
expect(summary).toContain("Settled");
86+
expect(summary).toContain("42ms");
87+
});
88+
});
89+
90+
describe("validateProofRecord", () => {
91+
it("returns empty array for valid proof record fixture", () => {
92+
const errors = validateProofRecord(demoProofRecords[0]);
93+
expect(errors).toEqual([]);
94+
});
95+
96+
it("identifies invalid messageHash path", () => {
97+
const invalidRecord: Partial<ProofRecord> = {
98+
...demoProofRecords[0],
99+
messageHash: "invalid_hash",
100+
};
101+
const errors = validateProofRecord(invalidRecord);
102+
expect(errors).toContainEqual({
103+
field: "messageHash",
104+
message: "Must be a hex string starting with 0x.",
105+
});
106+
});
107+
108+
it("identifies invalid paymentHash path", () => {
109+
const invalidRecord: Partial<ProofRecord> = {
110+
...demoProofRecords[0],
111+
paymentHash: "invalid_hash",
112+
};
113+
const errors = validateProofRecord(invalidRecord);
114+
expect(errors).toContainEqual({
115+
field: "paymentHash",
116+
message: "Must be a hex string starting with 0x.",
117+
});
118+
});
119+
120+
it("identifies invalid diagnosticId path", () => {
121+
const invalidRecord: Partial<ProofRecord> = {
122+
...demoProofRecords[0],
123+
diagnosticId: "not-a-uuid",
124+
};
125+
const errors = validateProofRecord(invalidRecord);
126+
expect(errors).toContainEqual({
127+
field: "diagnosticId",
128+
message: "Must be a valid UUID (8-4-4-4-12).",
129+
});
130+
});
131+
132+
it("identifies missing/invalid contractAddress path", () => {
133+
const invalidRecord: Partial<ProofRecord> = {
134+
...demoProofRecords[0],
135+
contractAddress: "short",
136+
};
137+
const errors = validateProofRecord(invalidRecord);
138+
expect(errors).toContainEqual({
139+
field: "contractAddress",
140+
message: "Contract address is required.",
141+
});
142+
});
143+
144+
it("identifies missing signature path", () => {
145+
const invalidRecord: Partial<ProofRecord> = {
146+
...demoProofRecords[0],
147+
signature: " ",
148+
};
149+
const errors = validateProofRecord(invalidRecord);
150+
expect(errors).toContainEqual({
151+
field: "signature",
152+
message: "Signature is required.",
153+
});
154+
});
155+
156+
it("identifies invalid latency path", () => {
157+
const invalidRecord: Partial<ProofRecord> = {
158+
...demoProofRecords[0],
159+
latency: "42",
160+
};
161+
const errors = validateProofRecord(invalidRecord);
162+
expect(errors).toContainEqual({
163+
field: "latency",
164+
message: 'Latency must be in the format "42ms".',
165+
});
166+
});
167+
168+
it("identifies invalid postageStatus path", () => {
169+
const invalidRecord: Partial<ProofRecord> = {
170+
...demoProofRecords[0],
171+
postageStatus: "unknown-status" as any,
172+
};
173+
const errors = validateProofRecord(invalidRecord);
174+
expect(errors).toContainEqual({
175+
field: "postageStatus",
176+
message: "Must be pending, settled, or refunded.",
177+
});
178+
});
179+
});

src/features/demo-admin-dashboard/__tests__/seedDatasetValidation.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,95 @@ describe("validateInboxSeedDataset", () => {
8787
const issues = validateInboxSeedDataset(dataset);
8888
expect(issues.some((i) => i.id?.includes("empty"))).toBe(true);
8989
});
90+
91+
it("reports warning for message with no subject", () => {
92+
const dataset: DemoDataset = {
93+
...inboxSeedDataset,
94+
messages: [{ ...inboxSeedMessages[0], subject: "" }],
95+
};
96+
const issues = validateInboxSeedDataset(dataset);
97+
const subjectIssue = issues.find((i) => i.fieldPath === "messages[0].subject");
98+
expect(subjectIssue).toBeDefined();
99+
expect(subjectIssue!.severity).toBe("warning");
100+
expect(subjectIssue!.message).toContain("has no subject");
101+
});
102+
103+
it("reports error for invalid message date format", () => {
104+
const dataset: DemoDataset = {
105+
...inboxSeedDataset,
106+
messages: [{ ...inboxSeedMessages[0], date: "invalid-date-format" }],
107+
};
108+
const issues = validateInboxSeedDataset(dataset);
109+
const dateIssue = issues.find((i) => i.fieldPath === "messages[0].date");
110+
expect(dateIssue).toBeDefined();
111+
expect(dateIssue!.severity).toBe("error");
112+
expect(dateIssue!.message).toContain("invalid date");
113+
});
114+
115+
it("reports warning for unsafe recipient domain", () => {
116+
const dataset: DemoDataset = {
117+
...inboxSeedDataset,
118+
messages: [{ ...inboxSeedMessages[0], recipients: ["eve@unverified.com"] }],
119+
};
120+
const issues = validateInboxSeedDataset(dataset);
121+
const recipientIssue = issues.find((i) => i.fieldPath === "messages[0].recipients[0]");
122+
expect(recipientIssue).toBeDefined();
123+
expect(recipientIssue!.severity).toBe("warning");
124+
expect(recipientIssue!.message).toContain("uses an unsafe domain");
125+
});
126+
127+
it("reports error for potential secret pattern in message body", () => {
128+
const dataset: DemoDataset = {
129+
...inboxSeedDataset,
130+
messages: [
131+
{
132+
...inboxSeedMessages[0],
133+
body: "Here is my Stellar secret key: SAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
134+
},
135+
],
136+
};
137+
const issues = validateInboxSeedDataset(dataset);
138+
const bodyIssue = issues.find((i) => i.fieldPath === "messages[0].body");
139+
expect(bodyIssue).toBeDefined();
140+
expect(bodyIssue!.severity).toBe("error");
141+
expect(bodyIssue!.message).toContain("may contain a secret");
142+
});
143+
144+
it("reports error for invalid proof record timestamp", () => {
145+
const dataset: DemoDataset = {
146+
...inboxSeedDataset,
147+
messages: [
148+
{
149+
...inboxSeedMessages[0],
150+
proofRecord: {
151+
...inboxSeedMessages[0].proofRecord!,
152+
timestamp: "not-iso-date",
153+
},
154+
},
155+
],
156+
};
157+
const issues = validateInboxSeedDataset(dataset);
158+
const proofTimeIssue = issues.find((i) => i.fieldPath === "messages[0].proofRecord.timestamp");
159+
expect(proofTimeIssue).toBeDefined();
160+
expect(proofTimeIssue!.severity).toBe("error");
161+
expect(proofTimeIssue!.message).toContain("is not ISO 8601");
162+
});
163+
164+
it("reports warning for unsafe sender domain in senders list", () => {
165+
const dataset: DemoDataset = {
166+
...inboxSeedDataset,
167+
senders: [
168+
{
169+
address: "unsafe@malicious.domain.com",
170+
name: "Unsafe Sender",
171+
isTrusted: false,
172+
},
173+
],
174+
};
175+
const issues = validateInboxSeedDataset(dataset);
176+
const senderIssue = issues.find((i) => i.fieldPath === "senders[0].address");
177+
expect(senderIssue).toBeDefined();
178+
expect(senderIssue!.severity).toBe("warning");
179+
expect(senderIssue!.message).toContain("uses an unsafe domain");
180+
});
90181
});

0 commit comments

Comments
 (0)