wire.test.ts258 lines · main
| 1 | import assert from 'node:assert/strict'; |
| 2 | import { test } from 'node:test'; |
| 3 | |
| 4 | import { schemaSnapshotSchema, validateSchemaSnapshot } from './index.js'; |
| 5 | |
| 6 | const okColumn = { |
| 7 | sqlType: 'text', |
| 8 | nullable: false, |
| 9 | primaryKey: true, |
| 10 | unique: false, |
| 11 | }; |
| 12 | |
| 13 | test('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 | |
| 50 | test('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 | |
| 64 | test('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 | |
| 75 | test('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 | |
| 89 | test('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 | |
| 110 | test('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 | |
| 127 | test('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 | |
| 150 | test('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 | |
| 183 | test('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 | |
| 206 | test('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 | |
| 233 | test('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 | |
| 247 | test('rejects version other than 1', () => { |
| 248 | assert.throws(() => |
| 249 | validateSchemaSnapshot({ |
| 250 | version: 2 as 1, |
| 251 | tables: {}, |
| 252 | }), |
| 253 | ); |
| 254 | }); |
| 255 | |
| 256 | test('schemaSnapshotSchema is exported as a Zod schema', () => { |
| 257 | assert.equal(typeof schemaSnapshotSchema.parse, 'function'); |
| 258 | }); |