wire.test.ts258 lines · main
1import assert from 'node:assert/strict';
2import { test } from 'node:test';
3
4import { schemaSnapshotSchema, validateSchemaSnapshot } from './index.js';
5
6const okColumn = {
7 sqlType: 'text',
8 nullable: false,
9 primaryKey: true,
10 unique: false,
11};
12
13test('accepts a legitimate snapshot', () => {
14 const ok = validateSchemaSnapshot({
15 version: 1,
16 tables: {
17 notes: {
18 columns: {
19 id: okColumn,
20 body: { sqlType: 'text', nullable: false, primaryKey: false, unique: false },
21 createdAt: {
22 sqlType: 'timestamptz',
23 nullable: false,
24 primaryKey: false,
25 unique: false,
26 default: 'now()',
27 },
28 },
29 indexes: [],
30 },
31 posts: {
32 columns: {
33 id: okColumn,
34 userId: {
35 sqlType: 'text',
36 nullable: false,
37 primaryKey: false,
38 unique: false,
39 references: { table: 'notes', column: 'id', onDelete: 'cascade' },
40 },
41 },
42 indexes: [{ columns: ['userId'], unique: false }],
43 },
44 },
45 });
46 assert.equal(ok.version, 1);
47 assert.equal(Object.keys(ok.tables).length, 2);
48});
49
50test('rejects table name with quote-injection', () => {
51 assert.throws(() =>
52 validateSchemaSnapshot({
53 version: 1,
54 tables: {
55 'x" (id text); DROP SCHEMA "proj_other" CASCADE; CREATE TABLE "y': {
56 columns: { id: okColumn },
57 indexes: [],
58 },
59 },
60 }),
61 );
62});
63
64test('rejects table name with reserved _briven_ prefix', () => {
65 assert.throws(() =>
66 validateSchemaSnapshot({
67 version: 1,
68 tables: {
69 _briven_meta: { columns: { id: okColumn }, indexes: [] },
70 },
71 }),
72 );
73});
74
75test('rejects column name with quote-injection', () => {
76 assert.throws(() =>
77 validateSchemaSnapshot({
78 version: 1,
79 tables: {
80 notes: {
81 columns: { 'id" text); DROP TABLE "users': okColumn },
82 indexes: [],
83 },
84 },
85 }),
86 );
87});
88
89test('rejects sqlType outside allowlist', () => {
90 assert.throws(() =>
91 validateSchemaSnapshot({
92 version: 1,
93 tables: {
94 notes: {
95 columns: {
96 id: {
97 sqlType: 'text PRIMARY KEY); DROP SCHEMA other CASCADE; --',
98 nullable: false,
99 primaryKey: true,
100 unique: false,
101 },
102 },
103 indexes: [],
104 },
105 },
106 }),
107 );
108});
109
110test('accepts sqlType with parametric forms (varchar, vector)', () => {
111 const ok = validateSchemaSnapshot({
112 version: 1,
113 tables: {
114 docs: {
115 columns: {
116 id: okColumn,
117 title: { sqlType: 'varchar(255)', nullable: false, primaryKey: false, unique: false },
118 embedding: { sqlType: 'vector(1536)', nullable: true, primaryKey: false, unique: false },
119 },
120 indexes: [],
121 },
122 },
123 });
124 assert.equal(ok.tables.docs?.columns.title?.sqlType, 'varchar(255)');
125});
126
127test('rejects default that breaks out of literal context', () => {
128 assert.throws(() =>
129 validateSchemaSnapshot({
130 version: 1,
131 tables: {
132 notes: {
133 columns: {
134 id: okColumn,
135 body: {
136 sqlType: 'text',
137 nullable: false,
138 primaryKey: false,
139 unique: false,
140 default: "''); DROP TABLE users; --",
141 },
142 },
143 indexes: [],
144 },
145 },
146 }),
147 );
148});
149
150test('accepts safe default forms', () => {
151 for (const def of [
152 'now()',
153 'gen_random_uuid()',
154 'true',
155 'false',
156 'null',
157 '0',
158 '-1.5',
159 "'hello'",
160 'current_timestamp',
161 ]) {
162 validateSchemaSnapshot({
163 version: 1,
164 tables: {
165 t: {
166 columns: {
167 id: okColumn,
168 v: {
169 sqlType: 'text',
170 nullable: true,
171 primaryKey: false,
172 unique: false,
173 default: def,
174 },
175 },
176 indexes: [],
177 },
178 },
179 });
180 }
181});
182
183test('rejects FK references with invalid identifier', () => {
184 assert.throws(() =>
185 validateSchemaSnapshot({
186 version: 1,
187 tables: {
188 posts: {
189 columns: {
190 id: okColumn,
191 userId: {
192 sqlType: 'text',
193 nullable: false,
194 primaryKey: false,
195 unique: false,
196 references: { table: 'users"; DROP TABLE x; --', column: 'id' },
197 },
198 },
199 indexes: [],
200 },
201 },
202 }),
203 );
204});
205
206test('rejects onDelete outside enum', () => {
207 assert.throws(() =>
208 validateSchemaSnapshot({
209 version: 1,
210 tables: {
211 posts: {
212 columns: {
213 id: okColumn,
214 userId: {
215 sqlType: 'text',
216 nullable: false,
217 primaryKey: false,
218 unique: false,
219 references: {
220 table: 'users',
221 column: 'id',
222 onDelete: 'CASCADE; DROP TABLE x; --' as 'cascade',
223 },
224 },
225 },
226 indexes: [],
227 },
228 },
229 }),
230 );
231});
232
233test('rejects index columns with invalid identifier', () => {
234 assert.throws(() =>
235 validateSchemaSnapshot({
236 version: 1,
237 tables: {
238 notes: {
239 columns: { id: okColumn },
240 indexes: [{ columns: ['id"); DROP TABLE x; --'], unique: false }],
241 },
242 },
243 }),
244 );
245});
246
247test('rejects version other than 1', () => {
248 assert.throws(() =>
249 validateSchemaSnapshot({
250 version: 2 as 1,
251 tables: {},
252 }),
253 );
254});
255
256test('schemaSnapshotSchema is exported as a Zod schema', () => {
257 assert.equal(typeof schemaSnapshotSchema.parse, 'function');
258});