CronJobs.utils.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import { describe, expect, it } from 'vitest'
  2. import { cronPattern, secondsPattern } from './CronJobs.constants'
  3. import { formatCronJobColumns, parseCronJobCommand } from './CronJobs.utils'
  4. describe('parseCronJobCommand', () => {
  5. it('should return a default object when the command is null', () => {
  6. expect(parseCronJobCommand('', 'random_project_ref')).toStrictEqual({
  7. httpBody: '',
  8. method: 'POST',
  9. snippet: '',
  10. timeoutMs: 1000,
  11. type: 'sql_snippet',
  12. })
  13. })
  14. it('should return a default object when the command is random', () => {
  15. const command = 'some random text'
  16. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  17. snippet: 'some random text',
  18. type: 'sql_snippet',
  19. })
  20. })
  21. it('should return a sql function command when the command is SELECT auth.jwt ()', () => {
  22. const command = 'SELECT auth.jwt ()'
  23. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  24. type: 'sql_function',
  25. schema: 'auth',
  26. functionName: 'jwt',
  27. snippet: command,
  28. })
  29. })
  30. it('should return a sql function command when the command is SELECT auth.jwt () and ends with ;', () => {
  31. const command = 'SELECT auth.jwt ();'
  32. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  33. type: 'sql_function',
  34. schema: 'auth',
  35. functionName: 'jwt',
  36. snippet: command,
  37. })
  38. })
  39. it('should return a sql function command when the function name contains an underscore', () => {
  40. const command = 'SELECT random_schema.function_1()'
  41. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  42. type: 'sql_function',
  43. schema: 'random_schema',
  44. functionName: 'function_1',
  45. snippet: command,
  46. })
  47. })
  48. it('should return a sql function command for lowercase select', () => {
  49. const command = 'select lowercase.issue ()'
  50. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  51. type: 'sql_function',
  52. schema: 'lowercase',
  53. functionName: 'issue',
  54. snippet: command,
  55. })
  56. })
  57. it('should return a sql snippet command when the command is SELECT public.test_fn(1, 2)', () => {
  58. const command = 'SELECT public.test_fn(1, 2)'
  59. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  60. type: 'sql_snippet',
  61. snippet: command,
  62. })
  63. })
  64. it('should return a sql snippet command when the command is using a SQL function from the search path', () => {
  65. const command = 'SELECT test_cron_function()'
  66. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  67. type: 'sql_snippet',
  68. snippet: command,
  69. })
  70. })
  71. it('should return a sql snippet command when the command is SELECT .()', () => {
  72. const command = 'SELECT .()'
  73. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  74. type: 'sql_snippet',
  75. snippet: command,
  76. })
  77. })
  78. it('should return a sql snippet command when the command is SELECT schema.()', () => {
  79. const command = 'SELECT schema.()'
  80. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  81. type: 'sql_snippet',
  82. snippet: command,
  83. })
  84. })
  85. it('should return a edge function config when the command posts to its own supabase.co project', () => {
  86. const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object('Authorization', 'Bearer something'), body:='', timeout_milliseconds:=5000 );`
  87. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  88. edgeFunctionName: 'https://random_project_ref.supabase.co/functions/v1/_',
  89. method: 'POST',
  90. httpHeaders: [
  91. {
  92. name: 'Authorization',
  93. value: 'Bearer something',
  94. },
  95. ],
  96. httpBody: '',
  97. timeoutMs: 5000,
  98. type: 'edge_function',
  99. snippet: command,
  100. })
  101. })
  102. it('should return a edge function config when the body is missing', () => {
  103. const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 );`
  104. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  105. edgeFunctionName: 'https://random_project_ref.supabase.co/functions/v1/_',
  106. method: 'POST',
  107. httpHeaders: [
  108. {
  109. name: 'Authorization',
  110. value: 'Bearer something',
  111. },
  112. ],
  113. httpBody: '',
  114. timeoutMs: 5000,
  115. type: 'edge_function',
  116. snippet: command,
  117. })
  118. })
  119. it("should return an HTTP request config when there's a query parameter or hash in the URL (also handles edge function)", () => {
  120. const command = `select net.http_post( url:='https://random_project_ref.supabase.co/functions/v1/_?first=1#second=2', headers:=jsonb_build_object('Authorization', 'Bearer something'), timeout_milliseconds:=5000 )`
  121. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  122. endpoint: 'https://random_project_ref.supabase.co/functions/v1/_?first=1#second=2',
  123. method: 'POST',
  124. httpHeaders: [
  125. {
  126. name: 'Authorization',
  127. value: 'Bearer something',
  128. },
  129. ],
  130. httpBody: '',
  131. timeoutMs: 5000,
  132. type: 'http_request',
  133. snippet: command,
  134. })
  135. })
  136. it('should return an HTTP request config when the command posts to another supabase.co project', () => {
  137. const command = `select net.http_post( url:='https://another_project_ref.supabase.co/functions/v1/_', headers:=jsonb_build_object(), body:='', timeout_milliseconds:=5000 );`
  138. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  139. endpoint: 'https://another_project_ref.supabase.co/functions/v1/_',
  140. method: 'POST',
  141. httpHeaders: [],
  142. httpBody: '',
  143. timeoutMs: 5000,
  144. type: 'http_request',
  145. snippet: command,
  146. })
  147. })
  148. it('should return an HTTP request config with POST method, empty headers and a body as string', () => {
  149. const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:=jsonb_build_object(), body:='hello', timeout_milliseconds:=5000 );`
  150. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  151. endpoint: 'https://example.com/api/endpoint',
  152. method: 'POST',
  153. httpHeaders: [],
  154. httpBody: 'hello',
  155. timeoutMs: 5000,
  156. type: 'http_request',
  157. snippet: command,
  158. })
  159. })
  160. it('should return an HTTP request config with POST method, some headers and empty body', () => {
  161. const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:=jsonb_build_object('fst', '1', 'snd', 'O''Reilly'), body:='', timeout_milliseconds:=1000 );`
  162. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  163. endpoint: 'https://example.com/api/endpoint',
  164. method: 'POST',
  165. httpHeaders: [
  166. { name: 'fst', value: '1' },
  167. { name: 'snd', value: "O'Reilly" },
  168. ],
  169. httpBody: '',
  170. timeoutMs: 1000,
  171. type: 'http_request',
  172. snippet: command,
  173. })
  174. })
  175. it('should return an HTTP request config with GET method and empty body', () => {
  176. const command = `select net.http_get( url:='https://example.com/api/endpoint', headers:=jsonb_build_object(), timeout_milliseconds:=5000 );`
  177. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  178. endpoint: 'https://example.com/api/endpoint',
  179. method: 'GET',
  180. httpHeaders: [],
  181. httpBody: '',
  182. timeoutMs: 5000,
  183. type: 'http_request',
  184. snippet: command,
  185. })
  186. })
  187. it('should return an HTTP request config with POST method and a body as JSON object', () => {
  188. const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:=jsonb_build_object(), body:='{"key": "value"}', timeout_milliseconds:=5000 );`
  189. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  190. endpoint: 'https://example.com/api/endpoint',
  191. method: 'POST',
  192. httpHeaders: [],
  193. httpBody: '{"key": "value"}',
  194. timeoutMs: 5000,
  195. type: 'http_request',
  196. snippet: command,
  197. })
  198. })
  199. it('should return an HTTP request config with POST method, plain JSON headers and plain JSON body', () => {
  200. const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:='{"fst": "1", "snd": "2"}',body:='{"key": "value"}',timeout_milliseconds:=5000);`
  201. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  202. endpoint: 'https://example.com/api/endpoint',
  203. method: 'POST',
  204. httpHeaders: [
  205. { name: 'fst', value: '1' },
  206. { name: 'snd', value: '2' },
  207. ],
  208. httpBody: '{"key": "value"}',
  209. timeoutMs: 5000,
  210. type: 'http_request',
  211. snippet: command,
  212. })
  213. })
  214. it('should return an HTTP request config with POST method, plain JSON headers and plain JSON body with escaped SQL strings and ::jsonb typecasting', () => {
  215. const command = `select net.http_post( url:='https://example.com/api/endpoint', headers:='{"X-Name":"O''Reilly"}'::jsonb,body:='{"message":"hello there","name":"O''Reilly"}'::jsonb,timeout_milliseconds:=5000);`
  216. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  217. endpoint: 'https://example.com/api/endpoint',
  218. method: 'POST',
  219. httpHeaders: [{ name: 'X-Name', value: "O'Reilly" }],
  220. httpBody: `{"message":"hello there","name":"O'Reilly"}`,
  221. timeoutMs: 5000,
  222. type: 'http_request',
  223. snippet: command,
  224. })
  225. })
  226. it('should return an HTTP request config with POST method, escape-string headers and body with backslashes', () => {
  227. const command = String.raw`select net.http_post( url:='https://example.com/api/endpoint', headers:=E'{"Content-Type":"application/json","X-Regex":"^\\\\d+$"}'::jsonb,body:=E'{"path":"C:\\\\tmp","regex":"^\\\\d+$"}',timeout_milliseconds:=1000);`
  228. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  229. endpoint: 'https://example.com/api/endpoint',
  230. method: 'POST',
  231. httpHeaders: [
  232. { name: 'Content-Type', value: 'application/json' },
  233. { name: 'X-Regex', value: String.raw`^\d+$` },
  234. ],
  235. httpBody: String.raw`{"path":"C:\\tmp","regex":"^\\d+$"}`,
  236. timeoutMs: 1000,
  237. type: 'http_request',
  238. snippet: command,
  239. })
  240. })
  241. it('should parse a POST body without swallowing later quoted arguments', () => {
  242. const command = `select net.http_post( url:='https://example.com/api/endpoint', body:='{"payload":"ok"}'::jsonb, headers:='{"Authorization":"Bearer demo"}'::jsonb, timeout_milliseconds:=5000 );`
  243. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  244. endpoint: 'https://example.com/api/endpoint',
  245. method: 'POST',
  246. httpHeaders: [{ name: 'Authorization', value: 'Bearer demo' }],
  247. httpBody: '{"payload":"ok"}',
  248. timeoutMs: 5000,
  249. type: 'http_request',
  250. snippet: command,
  251. })
  252. })
  253. it('should return SQL snippet type if the command is an HTTP request that cannot be parsed properly due to positional notation', () => {
  254. const command = `SELECT net.http_post( 'https://webhook.site/dacc2028-a588-462c-9597-c8968e61d0fa', '{"message":"Hello from Briven"}'::jsonb, '{}'::jsonb, '{"Content-Type":"application/json"}'::jsonb );`
  255. expect(parseCronJobCommand(command, 'random_project_ref')).toStrictEqual({
  256. type: 'sql_snippet',
  257. snippet: command,
  258. })
  259. })
  260. // Array of test cases for secondsPattern
  261. const secondsPatternTests = [
  262. { description: '10 seconds', command: '10 seconds' },
  263. { description: '30 seconds', command: '30 seconds' },
  264. ]
  265. // Run tests for secondsPattern
  266. secondsPatternTests.forEach(({ description, command }) => {
  267. it(`should match the regex for a secondsPattern with "${description}"`, () => {
  268. expect(command).toMatch(secondsPattern)
  269. })
  270. })
  271. // Array of test cases for cronPattern
  272. const cronPatternTests = [
  273. { description: 'every hour', command: '0 * * * *' },
  274. { description: 'every day at midnight', command: '0 0 * * *' },
  275. { description: 'every Monday at 9 AM', command: '0 9 * * 1' },
  276. { description: 'every 15th of the month at noon', command: '0 12 15 * *' },
  277. { description: 'every January 1st at 3 AM', command: '0 3 1 1 *' },
  278. { description: 'every 30 minutes', command: '30 * * * *' },
  279. { description: 'every weekday at 6 PM', command: '0 18 * * 1-5' },
  280. { description: 'every weekend at 10 AM', command: '0 10 * * 0,6' },
  281. { description: 'every quarter hour', command: '*/15 * * * *' },
  282. { description: 'twice daily at 8 AM and 8 PM', command: '0 8,20 * * *' },
  283. { description: 'last day of every month at midnight (pg_cron $ syntax)', command: '0 0 $ * *' },
  284. { description: 'last day of every month at noon (pg_cron $ syntax)', command: '0 12 $ * *' },
  285. ]
  286. const cronPatternRejectTests = [
  287. { description: '$ in minute field', command: '$ * * * *' },
  288. { description: '$ in hour field', command: '* $ * * *' },
  289. { description: '$ in month field', command: '* * * $ *' },
  290. { description: '$ in day-of-week field', command: '* * * * $' },
  291. ]
  292. cronPatternRejectTests.forEach(({ description, command }) => {
  293. it(`should not match the regex for a cronPattern with "${description}"`, () => {
  294. expect(command).not.toMatch(cronPattern)
  295. })
  296. })
  297. // Replace the single cronPattern test with forEach
  298. cronPatternTests.forEach(({ description, command }) => {
  299. it(`should match the regex for a cronPattern with "${description}"`, () => {
  300. expect(command).toMatch(cronPattern)
  301. })
  302. })
  303. })
  304. describe('formatCronJobColumns', () => {
  305. it('enables resizing for informational columns and keeps utility columns fixed', () => {
  306. const columns = formatCronJobColumns({
  307. onSelectEdit: () => undefined,
  308. onSelectDelete: () => undefined,
  309. })
  310. const columnsByKey = Object.fromEntries(columns.map((column) => [String(column.key), column]))
  311. expect(columnsByKey.jobname.resizable).toBe(true)
  312. expect(columnsByKey.jobname.minWidth).toBeGreaterThan(0)
  313. expect(columnsByKey.schedule.resizable).toBe(true)
  314. expect(columnsByKey.latest_run.resizable).toBe(true)
  315. expect(columnsByKey.next_run.resizable).toBe(true)
  316. expect(columnsByKey.command.resizable).toBe(true)
  317. expect(columnsByKey.active.resizable).toBe(false)
  318. expect(columnsByKey.actions.resizable).toBe(false)
  319. })
  320. })