ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(api): add validation util unit tests

byarielm.fyi 13b830c7 cde8671b

verified
+205
+205
packages/api/src/utils/validation.utils.test.ts
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { z } from "zod"; 3 + import { 4 + createArraySchema, 5 + ValidationSchemas, 6 + validateInput, 7 + validateArrayInput, 8 + } from "./validation.utils"; 9 + import { ValidationError } from "../errors"; 10 + 11 + describe("Validation Utils", () => { 12 + describe("createArraySchema", () => { 13 + it("creates schema that accepts valid arrays", () => { 14 + const schema = createArraySchema(z.string(), 5, "tags"); 15 + const result = schema.safeParse(["a", "b", "c"]); 16 + expect(result.success).toBe(true); 17 + }); 18 + 19 + it("rejects empty arrays", () => { 20 + const schema = createArraySchema(z.string(), 5, "tags"); 21 + const result = schema.safeParse([]); 22 + expect(result.success).toBe(false); 23 + if (!result.success) { 24 + expect(result.error.issues[0].message).toContain( 25 + "tags array is required and must not be empty", 26 + ); 27 + } 28 + }); 29 + 30 + it("rejects arrays over max length", () => { 31 + const schema = createArraySchema(z.string(), 3, "items"); 32 + const result = schema.safeParse(["a", "b", "c", "d"]); 33 + expect(result.success).toBe(false); 34 + if (!result.success) { 35 + expect(result.error.issues[0].message).toContain( 36 + "Maximum 3 items per batch", 37 + ); 38 + } 39 + }); 40 + 41 + it("rejects items that dont match item schema", () => { 42 + const schema = createArraySchema(z.number(), 5, "numbers"); 43 + const result = schema.safeParse(["not-a-number"]); 44 + expect(result.success).toBe(false); 45 + }); 46 + 47 + it("uses default field name when not provided", () => { 48 + const schema = createArraySchema(z.string(), 5); 49 + const result = schema.safeParse([]); 50 + expect(result.success).toBe(false); 51 + if (!result.success) { 52 + expect(result.error.issues[0].message).toContain("items"); 53 + } 54 + }); 55 + 56 + it("accepts single-element arrays", () => { 57 + const schema = createArraySchema(z.string(), 5, "tags"); 58 + const result = schema.safeParse(["only-one"]); 59 + expect(result.success).toBe(true); 60 + }); 61 + 62 + it("accepts arrays at exactly max length", () => { 63 + const schema = createArraySchema(z.string(), 3, "tags"); 64 + const result = schema.safeParse(["a", "b", "c"]); 65 + expect(result.success).toBe(true); 66 + }); 67 + }); 68 + 69 + describe("ValidationSchemas", () => { 70 + describe("didsArray", () => { 71 + it("accepts valid DID arrays (up to 100)", () => { 72 + const result = ValidationSchemas.didsArray.safeParse([ 73 + "did:plc:abc123", 74 + "did:web:example.com", 75 + ]); 76 + expect(result.success).toBe(true); 77 + }); 78 + 79 + it("rejects empty arrays", () => { 80 + const result = ValidationSchemas.didsArray.safeParse([]); 81 + expect(result.success).toBe(false); 82 + }); 83 + 84 + it("rejects arrays over 100 items", () => { 85 + const tooMany = Array.from({ length: 101 }, (_, i) => `did:plc:${i}`); 86 + const result = ValidationSchemas.didsArray.safeParse(tooMany); 87 + expect(result.success).toBe(false); 88 + }); 89 + }); 90 + 91 + describe("usernamesArray", () => { 92 + it("accepts valid username arrays (up to 50)", () => { 93 + const result = ValidationSchemas.usernamesArray.safeParse([ 94 + "user1", 95 + "user2", 96 + ]); 97 + expect(result.success).toBe(true); 98 + }); 99 + 100 + it("rejects arrays over 50 items", () => { 101 + const tooMany = Array.from({ length: 51 }, (_, i) => `user${i}`); 102 + const result = ValidationSchemas.usernamesArray.safeParse(tooMany); 103 + expect(result.success).toBe(false); 104 + }); 105 + }); 106 + 107 + describe("stringArray", () => { 108 + it("creates array schema with custom max", () => { 109 + const schema = ValidationSchemas.stringArray(10, "tags"); 110 + const result = schema.safeParse(["a", "b"]); 111 + expect(result.success).toBe(true); 112 + }); 113 + 114 + it("enforces custom max length", () => { 115 + const schema = ValidationSchemas.stringArray(2, "tags"); 116 + const result = schema.safeParse(["a", "b", "c"]); 117 + expect(result.success).toBe(false); 118 + }); 119 + }); 120 + }); 121 + 122 + describe("validateInput", () => { 123 + it("returns parsed data for valid input", () => { 124 + const schema = z.object({ name: z.string() }); 125 + const result = validateInput(schema, { name: "test" }); 126 + expect(result).toEqual({ name: "test" }); 127 + }); 128 + 129 + it("throws ValidationError for invalid input", () => { 130 + const schema = z.object({ name: z.string() }); 131 + expect(() => validateInput(schema, { name: 123 })).toThrow( 132 + ValidationError, 133 + ); 134 + }); 135 + 136 + it("provides first error message in ValidationError", () => { 137 + const schema = z.object({ name: z.string(), age: z.number() }); 138 + try { 139 + validateInput(schema, { name: 123, age: "not-a-number" }); 140 + expect.fail("Should have thrown"); 141 + } catch (error) { 142 + expect(error).toBeInstanceOf(ValidationError); 143 + expect((error as ValidationError).message).toBeTruthy(); 144 + } 145 + }); 146 + 147 + it("strips extra fields with Zod strict mode", () => { 148 + const schema = z.object({ name: z.string() }); 149 + // Default Zod strips extra fields 150 + const result = validateInput(schema, { 151 + name: "test", 152 + extra: "field", 153 + }); 154 + expect(result).toEqual({ name: "test" }); 155 + }); 156 + 157 + it("validates primitive types", () => { 158 + expect(validateInput(z.string(), "hello")).toBe("hello"); 159 + expect(validateInput(z.number(), 42)).toBe(42); 160 + expect(validateInput(z.boolean(), true)).toBe(true); 161 + }); 162 + 163 + it("throws ValidationError for null when not nullable", () => { 164 + const schema = z.string(); 165 + expect(() => validateInput(schema, null)).toThrow(ValidationError); 166 + }); 167 + }); 168 + 169 + describe("validateArrayInput", () => { 170 + const schema = ValidationSchemas.usernamesArray; 171 + 172 + it("parses body JSON and validates the named field", () => { 173 + const body = JSON.stringify({ usernames: ["user1", "user2"] }); 174 + const result = validateArrayInput<string>(body, "usernames", schema); 175 + expect(result).toEqual(["user1", "user2"]); 176 + }); 177 + 178 + it("throws ValidationError when field is missing", () => { 179 + const body = JSON.stringify({ other: ["value"] }); 180 + expect(() => validateArrayInput<string>(body, "usernames", schema)).toThrow( 181 + ValidationError, 182 + ); 183 + }); 184 + 185 + it("throws SyntaxError for invalid JSON body", () => { 186 + expect(() => 187 + validateArrayInput<string>("not-json", "usernames", schema), 188 + ).toThrow(SyntaxError); 189 + }); 190 + 191 + it("uses empty object for null body", () => { 192 + expect(() => 193 + validateArrayInput<string>(null, "usernames", schema), 194 + ).toThrow(ValidationError); 195 + }); 196 + 197 + it("validates array constraints from schema", () => { 198 + const tooMany = Array.from({ length: 51 }, (_, i) => `user${i}`); 199 + const body = JSON.stringify({ usernames: tooMany }); 200 + expect(() => 201 + validateArrayInput<string>(body, "usernames", schema), 202 + ).toThrow(ValidationError); 203 + }); 204 + }); 205 + });