SQLEditor.utils.test.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. import { safeSql } from '@supabase/pg-meta'
  2. import { stripIndent } from 'common-tags'
  3. import { describe, expect, it, test } from 'vitest'
  4. import {
  5. appendEnableRLSStatements,
  6. checkAlterDatabaseConnection,
  7. checkDestructiveQuery,
  8. checkIfAppendLimitRequired,
  9. filterTablesCoveredByEnsureRLSTrigger,
  10. getCreateTablesMissingRLS,
  11. hasActiveEnsureRLSTrigger,
  12. isUpdateWithoutWhere,
  13. suffixWithLimit,
  14. } from './SQLEditor.utils'
  15. import type { DatabaseEventTrigger } from '@/data/database-event-triggers/database-event-triggers-query'
  16. const buildTrigger = (overrides: Partial<DatabaseEventTrigger> = {}): DatabaseEventTrigger => ({
  17. oid: 1,
  18. name: 'ensure_rls',
  19. event: 'ddl_command_end',
  20. enabled_mode: 'ORIGIN',
  21. tags: ['CREATE TABLE'],
  22. function_name: 'rls_auto_enable',
  23. function_schema: 'public',
  24. owner: 'postgres',
  25. function_definition: null,
  26. ...overrides,
  27. })
  28. describe('SQLEditor.utils.ts:checkIfAppendLimitRequired', () => {
  29. test('Should return false if limit passed is <= 0', () => {
  30. const sql = 'select * from countries;'
  31. const limit = -1
  32. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  33. expect(appendAutoLimit).toBe(false)
  34. })
  35. test('Should return true if limit passed is > 0', () => {
  36. const sql = 'select * from countries;'
  37. const limit = 100
  38. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  39. expect(appendAutoLimit).toBe(true)
  40. })
  41. test('Should return false if query already has a limit', () => {
  42. const sql = 'select * from countries limit 10;'
  43. const limit = 100
  44. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  45. expect(appendAutoLimit).toBe(false)
  46. })
  47. test('Should return false if query already has a limit (check for case-insensitiveness)', () => {
  48. const sql = 'SELECT * FROM countries LIMIT 10;'
  49. const limit = 100
  50. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  51. expect(appendAutoLimit).toBe(false)
  52. })
  53. test('Should return false if query already has a limit and offset', () => {
  54. const sql = 'select * from countries limit 10 offset 0;'
  55. const limit = 100
  56. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  57. expect(appendAutoLimit).toBe(false)
  58. })
  59. test('Should return false if query already has a limit and offset (flip order of limit and offset)', () => {
  60. const sql = 'select * from countries offset 0 limit 1;'
  61. const limit = 100
  62. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  63. expect(appendAutoLimit).toBe(false)
  64. })
  65. test('Should return false if query already has a limit, even if no value provided for limit', () => {
  66. const sql = 'select * from countries limit'
  67. const limit = 100
  68. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  69. expect(appendAutoLimit).toBe(false)
  70. })
  71. test('Should return false if query uses `FETCH FIRST` instead of limit ', () => {
  72. const sql = 'select * from countries FETCH FIRST 5 rows only'
  73. const limit = 100
  74. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  75. expect(appendAutoLimit).toBe(false)
  76. })
  77. test('Should return false if query uses `fetch first` instead of limit ', () => {
  78. const sql = 'select * from countries fetch first 5 rows only'
  79. const limit = 100
  80. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  81. expect(appendAutoLimit).toBe(false)
  82. })
  83. test('Should return false if query uses `fetch first` (with random spaces) instead of limit ', () => {
  84. const sql = 'select * from countries FETCH FIRST 5 rows only'
  85. const limit = 100
  86. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  87. expect(appendAutoLimit).toBe(false)
  88. })
  89. test('Should return false if query is not a select statement', () => {
  90. const sql = 'create table test (id int8 primary key, name varchar);'
  91. const limit = 100
  92. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  93. expect(appendAutoLimit).toBe(false)
  94. })
  95. test('Should return false if there are multiple queries I', () => {
  96. const sql1 = `
  97. select * from countries;
  98. select * from cities;
  99. `.trim()
  100. const limit = 100
  101. const { appendAutoLimit } = checkIfAppendLimitRequired(sql1, limit)
  102. expect(appendAutoLimit).toBe(false)
  103. })
  104. test('Should return false if there are multiple queries II', () => {
  105. const sql1 = `
  106. select * from countries;
  107. select * from cities
  108. `.trim()
  109. const limit = 100
  110. const { appendAutoLimit } = checkIfAppendLimitRequired(sql1, limit)
  111. expect(appendAutoLimit).toBe(false)
  112. })
  113. // [Joshen] Opting to just avoid appending in this case to prevent making the logic overly complex atm
  114. test('Should return false if query has with a comment I', () => {
  115. const sql = `
  116. -- This is a comment
  117. select * from cities
  118. `.trim()
  119. const limit = 100
  120. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  121. expect(appendAutoLimit).toBe(false)
  122. })
  123. test('Should return false if query has with a comment II', () => {
  124. const sql = `
  125. select * from cities
  126. -- This is a comment
  127. `.trim()
  128. const limit = 100
  129. const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
  130. expect(appendAutoLimit).toBe(false)
  131. })
  132. })
  133. // [Joshen] These will just need to test the cases when appendAutoLimit returns true then
  134. describe('SQLEditor.utils.ts:suffixWithLimit', () => {
  135. test('Should add the limit param properly if query ends without a semi colon', () => {
  136. const sql = safeSql`select * from countries`
  137. const limit = 100
  138. const formattedSql = suffixWithLimit(sql, limit)
  139. expect(formattedSql).toBe('select * from countries limit 100;')
  140. })
  141. test('Should add the limit param properly if query ends with a semi colon', () => {
  142. const sql = safeSql`select * from countries;`
  143. const limit = 100
  144. const formattedSql = suffixWithLimit(sql, limit)
  145. expect(formattedSql).toBe('select * from countries limit 100;')
  146. })
  147. test('Should add the limit param properly if query ends with multiple semi colon', () => {
  148. const sql = safeSql`select * from countries;;;;;;;`
  149. const limit = 100
  150. const formattedSql = suffixWithLimit(sql, limit)
  151. expect(formattedSql).toBe('select * from countries limit 100;')
  152. })
  153. })
  154. describe(`SQLEditor.utils.ts:checkDestructiveQuery`, () => {
  155. it('drop statement matches', () => {
  156. const match = checkDestructiveQuery('drop table films, distributors;')
  157. expect(match).toBe(true)
  158. })
  159. it('truncate statement matches', () => {
  160. const match = checkDestructiveQuery('truncate films;')
  161. expect(match).toBe(true)
  162. })
  163. it('delete statement matches', () => {
  164. const match = checkDestructiveQuery("delete from films where kind <> 'Musical';")
  165. expect(match).toBe(true)
  166. })
  167. it('delete statement after another statement matches', () => {
  168. const match = checkDestructiveQuery(stripIndent`
  169. select * from films;
  170. delete from films where kind <> 'Musical';
  171. `)
  172. expect(match).toBe(true)
  173. })
  174. it("rls policy containing delete doesn't match", () => {
  175. const match = checkDestructiveQuery(stripIndent`
  176. create policy "Users can delete their own files"
  177. on storage.objects for delete to authenticated using (
  178. bucket id = 'files' and (select auth.uid()) = owner
  179. );
  180. `)
  181. expect(match).toBe(false)
  182. })
  183. it('capitalized statement matches', () => {
  184. const match = checkDestructiveQuery("DELETE FROM films WHERE kind <> 'Musical';")
  185. expect(match).toBe(true)
  186. })
  187. it("comment containing keyword doesn't match", () => {
  188. const match = checkDestructiveQuery(stripIndent`
  189. -- Going to drop this in here, might delete later
  190. select * from films;
  191. `)
  192. expect(match).toBe(false)
  193. })
  194. })
  195. describe('SQLEditor.utils:updateWithoutWhere', () => {
  196. it('contains an update query with a where clause', () => {
  197. const match = isUpdateWithoutWhere(stripIndent`
  198. UPDATE public.countries SET name = 'New Name' WHERE id = 1;
  199. `)
  200. expect(match).toBe(false)
  201. })
  202. it('contains an update query without a where clause', () => {
  203. const match = isUpdateWithoutWhere(stripIndent`
  204. UPDATE public.countries SET name = 'New Name';
  205. `)
  206. expect(match).toBe(true)
  207. })
  208. it('contains an update query, with quoted identifiers with a where clause', () => {
  209. const match = isUpdateWithoutWhere(stripIndent`
  210. UPDATE "public"."countries" SET name = 'New Name' WHERE id = 1;
  211. `)
  212. expect(match).toBe(false)
  213. })
  214. it('contains an update query, with quoted identifiers without a where clause', () => {
  215. const match = isUpdateWithoutWhere(stripIndent`
  216. UPDATE "public"."countries" SET name = 'New Name';
  217. `)
  218. expect(match).toBe(true)
  219. })
  220. it('catches update on a single quoted table name without a where clause', () => {
  221. const match = isUpdateWithoutWhere(`UPDATE "messages" SET id = 1;`)
  222. expect(match).toBe(true)
  223. })
  224. it('does not flag update on a single quoted table name with a where clause', () => {
  225. const match = isUpdateWithoutWhere(`UPDATE "messages" SET id = 1 WHERE id = 2;`)
  226. expect(match).toBe(false)
  227. })
  228. it('catches update on a quoted schema with a bareword table without a where clause', () => {
  229. const match = isUpdateWithoutWhere(`UPDATE "public".messages SET id = 1;`)
  230. expect(match).toBe(true)
  231. })
  232. it('catches update on a bareword schema with a quoted table without a where clause', () => {
  233. const match = isUpdateWithoutWhere(`UPDATE public."messages" SET id = 1;`)
  234. expect(match).toBe(true)
  235. })
  236. it('catches update on a quoted table name containing a space without a where clause', () => {
  237. const match = isUpdateWithoutWhere(`UPDATE "my table" SET id = 1;`)
  238. expect(match).toBe(true)
  239. })
  240. it('catches update on a quoted table name containing escaped quotes without a where clause', () => {
  241. const match = isUpdateWithoutWhere(`UPDATE "weird""name" SET id = 1;`)
  242. expect(match).toBe(true)
  243. })
  244. it('catches update where a quoted identifier contains the word where', () => {
  245. const match = isUpdateWithoutWhere(`UPDATE "where table" SET id = 1;`)
  246. expect(match).toBe(true)
  247. })
  248. it('catches update where a string literal contains the word where', () => {
  249. const match = isUpdateWithoutWhere(`UPDATE messages SET name = 'where x';`)
  250. expect(match).toBe(true)
  251. })
  252. it('does not flag update where the only "where" sits inside a string literal but a real where clause exists', () => {
  253. const match = isUpdateWithoutWhere(`UPDATE messages SET name = 'where x' WHERE id = 1;`)
  254. expect(match).toBe(false)
  255. })
  256. it('contains both an update query and a delete query, triggers destructive', () => {
  257. const match = checkDestructiveQuery(stripIndent`
  258. delete from countries; update countries set name = 'hello';
  259. `)
  260. expect(match).toBe(true)
  261. })
  262. it('contains both an update query and a delete query, triggers no where', () => {
  263. const match = isUpdateWithoutWhere(stripIndent`
  264. delete from countries; update countries set name = 'hello';
  265. `)
  266. expect(match).toBe(true)
  267. })
  268. it('contains both an update query and a delete query, triggers no where', () => {
  269. const match = isUpdateWithoutWhere(stripIndent`
  270. delete from countries; update countries set name = 'hello';
  271. `)
  272. expect(match).toBe(true)
  273. })
  274. it('should catch potential destructive queries', () => {
  275. const DESTRUCTIVE_QUERIES = [
  276. `ALTER TABLE test DROP COLUMN test;`,
  277. `DELETE FROM test;`,
  278. `DROP TABLE test;`,
  279. `TRUNCATE TABLE test;`,
  280. ]
  281. DESTRUCTIVE_QUERIES.forEach((query) => {
  282. expect(checkDestructiveQuery(query), `Query ${query} should be destructive`).toBe(true)
  283. })
  284. })
  285. })
  286. describe('SQLEditor.utils:getCreateTablesMissingRLS', () => {
  287. it('flags a basic CREATE TABLE without RLS', () => {
  288. const result = getCreateTablesMissingRLS('create table foo (id int8 primary key);')
  289. expect(result).toEqual([{ schema: undefined, tableName: 'foo' }])
  290. })
  291. it('flags CREATE TABLE IF NOT EXISTS', () => {
  292. const result = getCreateTablesMissingRLS(
  293. 'create table if not exists foo (id int8 primary key);'
  294. )
  295. expect(result).toHaveLength(1)
  296. expect(result[0].tableName).toBe('foo')
  297. })
  298. it('flags schema-qualified CREATE TABLE', () => {
  299. const result = getCreateTablesMissingRLS('create table public.foo (id int8 primary key);')
  300. expect(result).toEqual([{ schema: 'public', tableName: 'foo' }])
  301. })
  302. it('flags quoted identifiers', () => {
  303. const result = getCreateTablesMissingRLS(
  304. 'create table "public"."user_table" (id int8 primary key);'
  305. )
  306. expect(result).toEqual([{ schema: 'public', tableName: 'user_table' }])
  307. })
  308. it('flags quoted identifiers containing spaces', () => {
  309. const result = getCreateTablesMissingRLS(
  310. 'create table "public"."My Table" (id int8 primary key);'
  311. )
  312. expect(result).toEqual([{ schema: 'public', tableName: 'My Table' }])
  313. })
  314. it('matches RLS to a table whose name contains spaces', () => {
  315. const sql = stripIndent`
  316. create table "My Table" (id int8 primary key);
  317. alter table "My Table" enable row level security;
  318. `
  319. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  320. })
  321. it('does not flag when ENABLE ROW LEVEL SECURITY is in the same SQL', () => {
  322. const sql = stripIndent`
  323. create table foo (id int8 primary key);
  324. alter table foo enable row level security;
  325. `
  326. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  327. })
  328. it('does not flag when ENABLE RLS shorthand is in the same SQL', () => {
  329. const sql = stripIndent`
  330. create table foo (id int8 primary key);
  331. alter table foo enable rls;
  332. `
  333. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  334. })
  335. it('matches RLS to the right table when multiple tables created', () => {
  336. const sql = stripIndent`
  337. create table foo (id int8 primary key);
  338. create table bar (id int8 primary key);
  339. alter table foo enable row level security;
  340. `
  341. const result = getCreateTablesMissingRLS(sql)
  342. expect(result).toHaveLength(1)
  343. expect(result[0].tableName).toBe('bar')
  344. })
  345. it('does not flag when CREATE TABLE is inside a comment', () => {
  346. const sql = stripIndent`
  347. -- create table foo (id int8 primary key);
  348. select 1;
  349. `
  350. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  351. })
  352. it('does not flag when there is no CREATE TABLE at all', () => {
  353. expect(getCreateTablesMissingRLS('select * from foo;')).toEqual([])
  354. })
  355. it('schema-qualified RLS matches schema-qualified CREATE', () => {
  356. const sql = stripIndent`
  357. create table public.foo (id int8 primary key);
  358. alter table public.foo enable row level security;
  359. `
  360. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  361. })
  362. it('does not flag when ALTER TABLE IF EXISTS enables RLS', () => {
  363. const sql = stripIndent`
  364. CREATE TABLE IF NOT EXISTS public."Conversations" (id int8 primary key);
  365. ALTER TABLE IF EXISTS public."Conversations" ENABLE ROW LEVEL SECURITY;
  366. GRANT ALL ON TABLE public."Conversations" TO postgres, anon, authenticated, service_role;
  367. `
  368. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  369. })
  370. it('flags CREATE TEMP TABLE', () => {
  371. const result = getCreateTablesMissingRLS('create temp table foo (id int8 primary key);')
  372. expect(result).toHaveLength(1)
  373. expect(result[0].tableName).toBe('foo')
  374. })
  375. it('does not flag `select ... into var` inside a plpgsql function body', () => {
  376. // Regression: the SELECT..INTO detector used to match variable assignments
  377. // inside $$...$$ function bodies and surface them as \"new tables\".
  378. const sql = stripIndent`
  379. create or replace function schema_checks()
  380. returns jsonb
  381. language plpgsql
  382. as $$
  383. declare
  384. ret jsonb;
  385. begin
  386. select jsonb_build_object('value', 'ok')
  387. into ret;
  388. return ret;
  389. end;
  390. $$;
  391. `
  392. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  393. })
  394. it('does not flag `select ... into var` inside a DO block', () => {
  395. const sql = stripIndent`
  396. do $$
  397. declare
  398. result int;
  399. begin
  400. select count(*) into result from information_schema.tables;
  401. end
  402. $$;
  403. `
  404. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  405. })
  406. it('does not flag CREATE TABLE text that appears inside a function body', () => {
  407. const sql = stripIndent`
  408. create or replace function noop()
  409. returns void
  410. language plpgsql
  411. as $$
  412. begin
  413. -- create table foo (id int);
  414. perform 1;
  415. end;
  416. $$;
  417. `
  418. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  419. })
  420. it('flags top-level CREATE TABLE alongside a function with INTO assignments', () => {
  421. const sql = stripIndent`
  422. create table public.foo (id int8 primary key);
  423. create or replace function bar()
  424. returns int
  425. language plpgsql
  426. as $$
  427. declare
  428. v int;
  429. begin
  430. select 1 into v;
  431. return v;
  432. end;
  433. $$;
  434. `
  435. const result = getCreateTablesMissingRLS(sql)
  436. expect(result).toEqual([{ schema: 'public', tableName: 'foo' }])
  437. })
  438. it('does not flag CREATE TABLE inside nested dollar-quoted dynamic SQL', () => {
  439. // Regression: the `$sql$...$sql$` block inside the outer `$fn$...$fn$`
  440. // body was previously pairing with the outer tag, letting the inner
  441. // semicolon split the statement and exposing `create table fake` to the
  442. // RLS warning.
  443. const sql = stripIndent`
  444. create function f()
  445. returns void
  446. language plpgsql
  447. as $fn$
  448. begin
  449. execute $sql$create table fake(id int);$sql$;
  450. end;
  451. $fn$;
  452. `
  453. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  454. })
  455. it('handles custom dollar-quote tags (e.g. $body$...$body$)', () => {
  456. const sql = stripIndent`
  457. create or replace function f()
  458. returns int
  459. language plpgsql
  460. as $body$
  461. declare
  462. v int;
  463. begin
  464. select 1 into v;
  465. return v;
  466. end;
  467. $body$;
  468. `
  469. expect(getCreateTablesMissingRLS(sql)).toEqual([])
  470. })
  471. it('does not collide quoted identifiers that differ only by case', () => {
  472. // "MyTable" and "mytable" are distinct tables in Postgres, so the ALTER
  473. // here targets a different table than the CREATE — the warning must fire.
  474. const sql = stripIndent`
  475. create table "MyTable" (id int8 primary key);
  476. alter table "mytable" enable row level security;
  477. `
  478. const result = getCreateTablesMissingRLS(sql)
  479. expect(result).toHaveLength(1)
  480. expect(result[0].tableName).toBe('MyTable')
  481. })
  482. })
  483. describe('SQLEditor.utils:appendEnableRLSStatements', () => {
  484. it('appends a single ALTER TABLE ENABLE RLS statement', () => {
  485. const result = appendEnableRLSStatements('create table foo (id int8 primary key);', [
  486. { tableName: 'foo' },
  487. ])
  488. expect(result).toContain('ALTER TABLE foo ENABLE ROW LEVEL SECURITY;')
  489. })
  490. it('appends one ALTER per table', () => {
  491. const result = appendEnableRLSStatements(
  492. 'create table foo (id int8); create table bar (id int8);',
  493. [{ tableName: 'foo' }, { tableName: 'bar' }]
  494. )
  495. expect(result).toContain('ALTER TABLE foo ENABLE ROW LEVEL SECURITY;')
  496. expect(result).toContain('ALTER TABLE bar ENABLE ROW LEVEL SECURITY;')
  497. })
  498. it('schema-qualifies the table when schema is provided', () => {
  499. const result = appendEnableRLSStatements('create table public.foo (id int8);', [
  500. { schema: 'public', tableName: 'foo' },
  501. ])
  502. expect(result).toContain('ALTER TABLE public.foo ENABLE ROW LEVEL SECURITY;')
  503. })
  504. it('quotes identifiers that are not simple', () => {
  505. const result = appendEnableRLSStatements('create table "My Table" (id int8);', [
  506. { tableName: 'My Table' },
  507. ])
  508. expect(result).toContain('ALTER TABLE "My Table" ENABLE ROW LEVEL SECURITY;')
  509. })
  510. it('quotes mixed-case identifiers so Postgres does not fold them to lowercase', () => {
  511. const result = appendEnableRLSStatements('create table "MyTable" (id int8);', [
  512. { tableName: 'MyTable' },
  513. ])
  514. expect(result).toContain('ALTER TABLE "MyTable" ENABLE ROW LEVEL SECURITY;')
  515. })
  516. it('quotes mixed-case schema and table identifiers', () => {
  517. const result = appendEnableRLSStatements('create table "MySchema"."MyTable" (id int8);', [
  518. { schema: 'MySchema', tableName: 'MyTable' },
  519. ])
  520. expect(result).toContain('ALTER TABLE "MySchema"."MyTable" ENABLE ROW LEVEL SECURITY;')
  521. })
  522. it('returns the original SQL unchanged when there are no tables', () => {
  523. const sql = 'select 1;'
  524. expect(appendEnableRLSStatements(sql, [])).toBe(sql)
  525. })
  526. it('puts the terminator on its own line when SQL ends with a line comment', () => {
  527. // Without this, the appended ';' would be swallowed by the line comment and
  528. // the following ALTER TABLE would be parsed as part of the CREATE TABLE.
  529. const sql = stripIndent`
  530. create table foo (id int)
  531. -- forgot the semicolon
  532. `
  533. const result = appendEnableRLSStatements(sql, [{ tableName: 'foo' }])
  534. expect(result).toMatch(/-- forgot the semicolon\n;\n/)
  535. expect(result).toContain('ALTER TABLE foo ENABLE ROW LEVEL SECURITY;')
  536. })
  537. })
  538. describe('SQLEditor.utils:hasActiveEnsureRLSTrigger', () => {
  539. it('returns false for undefined triggers', () => {
  540. expect(hasActiveEnsureRLSTrigger(undefined)).toBe(false)
  541. })
  542. it('returns false for an empty list', () => {
  543. expect(hasActiveEnsureRLSTrigger([])).toBe(false)
  544. })
  545. it('returns true when a trigger named "ensure_rls" is active', () => {
  546. expect(hasActiveEnsureRLSTrigger([buildTrigger()])).toBe(true)
  547. })
  548. it('returns true when a trigger uses the rls_auto_enable function (renamed trigger)', () => {
  549. expect(
  550. hasActiveEnsureRLSTrigger([
  551. buildTrigger({ name: 'something_else', function_name: 'rls_auto_enable' }),
  552. ])
  553. ).toBe(true)
  554. })
  555. it('returns false when the matching trigger is DISABLED', () => {
  556. expect(hasActiveEnsureRLSTrigger([buildTrigger({ enabled_mode: 'DISABLED' })])).toBe(false)
  557. })
  558. it('ignores unrelated triggers', () => {
  559. expect(
  560. hasActiveEnsureRLSTrigger([buildTrigger({ name: 'audit_log', function_name: 'log_changes' })])
  561. ).toBe(false)
  562. })
  563. })
  564. describe('SQLEditor.utils:filterTablesCoveredByEnsureRLSTrigger', () => {
  565. it('returns the input unchanged when the trigger is not present', () => {
  566. const tables = [{ tableName: 'foo' }, { schema: 'private', tableName: 'bar' }]
  567. expect(filterTablesCoveredByEnsureRLSTrigger(tables, false)).toEqual(tables)
  568. })
  569. it('drops public-schema tables when the trigger is present', () => {
  570. const tables = [
  571. { schema: 'public', tableName: 'foo' },
  572. { tableName: 'bar' }, // no schema → defaults to public
  573. ]
  574. expect(filterTablesCoveredByEnsureRLSTrigger(tables, true)).toEqual([])
  575. })
  576. it('keeps tables in non-public schemas when the trigger is present', () => {
  577. const tables = [
  578. { schema: 'public', tableName: 'foo' },
  579. { schema: 'private', tableName: 'bar' },
  580. { schema: 'app', tableName: 'baz' },
  581. ]
  582. expect(filterTablesCoveredByEnsureRLSTrigger(tables, true)).toEqual([
  583. { schema: 'private', tableName: 'bar' },
  584. { schema: 'app', tableName: 'baz' },
  585. ])
  586. })
  587. it('matches the public schema case-insensitively', () => {
  588. const tables = [{ schema: 'PUBLIC', tableName: 'foo' }]
  589. expect(filterTablesCoveredByEnsureRLSTrigger(tables, true)).toEqual([])
  590. })
  591. })
  592. describe('SQLEditor.utils:checkAlterDatabaseConnection', () => {
  593. it('detects connection limit 0', () => {
  594. const match = checkAlterDatabaseConnection('alter database postgres connection limit 0;')
  595. expect(match).toBe(true)
  596. })
  597. it('detects allow_connections false', () => {
  598. const match = checkAlterDatabaseConnection('alter database postgres allow_connections false;')
  599. expect(match).toBe(true)
  600. })
  601. it('detects case-insensitive match', () => {
  602. const match = checkAlterDatabaseConnection('ALTER DATABASE postgres CONNECTION LIMIT 0;')
  603. expect(match).toBe(true)
  604. })
  605. it('detects statement among multiple statements', () => {
  606. const match = checkAlterDatabaseConnection(stripIndent`
  607. select * from countries;
  608. alter database postgres connection limit 0;
  609. `)
  610. expect(match).toBe(true)
  611. })
  612. it('does not flag unrelated alter database statement', () => {
  613. const match = checkAlterDatabaseConnection(
  614. 'alter database postgres set statement_timeout = 60000;'
  615. )
  616. expect(match).toBe(false)
  617. })
  618. it('does not flag non-alter statements', () => {
  619. const match = checkAlterDatabaseConnection('select * from countries;')
  620. expect(match).toBe(false)
  621. })
  622. it('ignores statements inside comments', () => {
  623. const match = checkAlterDatabaseConnection(stripIndent`
  624. -- alter database postgres connection limit 0;
  625. select 1;
  626. `)
  627. expect(match).toBe(false)
  628. })
  629. it('detects both dangerous statements in same query', () => {
  630. const match = checkAlterDatabaseConnection(stripIndent`
  631. alter database postgres connection limit 0;
  632. alter database postgres allow_connections false;
  633. `)
  634. expect(match).toBe(true)
  635. })
  636. })