webhookdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (364) hide show
  1. checksums.yaml +7 -0
  2. data/data/messages/layouts/blank.email.liquid +10 -0
  3. data/data/messages/layouts/minimal.email.liquid +28 -0
  4. data/data/messages/layouts/standard.email.liquid +28 -0
  5. data/data/messages/partials/button.liquid +15 -0
  6. data/data/messages/partials/environment_banner.liquid +9 -0
  7. data/data/messages/partials/footer.liquid +22 -0
  8. data/data/messages/partials/greeting.liquid +3 -0
  9. data/data/messages/partials/logo_header.liquid +18 -0
  10. data/data/messages/partials/signoff.liquid +1 -0
  11. data/data/messages/styles/v1.liquid +346 -0
  12. data/data/messages/templates/errors/icalendar_fetch.email.liquid +29 -0
  13. data/data/messages/templates/invite.email.liquid +15 -0
  14. data/data/messages/templates/new_customer.email.liquid +24 -0
  15. data/data/messages/templates/org_database_migration_finished.email.liquid +7 -0
  16. data/data/messages/templates/org_database_migration_started.email.liquid +9 -0
  17. data/data/messages/templates/specs/_field_partial.liquid +1 -0
  18. data/data/messages/templates/specs/basic.email.liquid +2 -0
  19. data/data/messages/templates/specs/basic.fake.liquid +1 -0
  20. data/data/messages/templates/specs/with_field.email.liquid +2 -0
  21. data/data/messages/templates/specs/with_field.fake.liquid +1 -0
  22. data/data/messages/templates/specs/with_include.email.liquid +2 -0
  23. data/data/messages/templates/specs/with_partial.email.liquid +1 -0
  24. data/data/messages/templates/verification.email.liquid +14 -0
  25. data/data/messages/templates/verification.sms.liquid +1 -0
  26. data/data/messages/web/install-customer-login.liquid +48 -0
  27. data/data/messages/web/install-error.liquid +17 -0
  28. data/data/messages/web/install-success.liquid +35 -0
  29. data/data/messages/web/install.liquid +20 -0
  30. data/data/messages/web/partials/footer.liquid +4 -0
  31. data/data/messages/web/partials/form_error.liquid +1 -0
  32. data/data/messages/web/partials/header.liquid +3 -0
  33. data/data/messages/web/styles.liquid +134 -0
  34. data/data/windows_tz.txt +461 -0
  35. data/db/migrations/001_testing_pixies.rb +13 -0
  36. data/db/migrations/002_initial.rb +132 -0
  37. data/db/migrations/003_ux_overhaul.rb +20 -0
  38. data/db/migrations/004_incremental_backfill.rb +9 -0
  39. data/db/migrations/005_log_webhooks.rb +24 -0
  40. data/db/migrations/006_generalize_roles.rb +29 -0
  41. data/db/migrations/007_org_dns.rb +12 -0
  42. data/db/migrations/008_webhook_subscriptions.rb +19 -0
  43. data/db/migrations/009_nonunique_stripe_subscription_customer.rb +16 -0
  44. data/db/migrations/010_drop_integration_soft_delete.rb +14 -0
  45. data/db/migrations/011_webhook_subscriptions_created_at.rb +10 -0
  46. data/db/migrations/012_webhook_subscriptions_created_by.rb +9 -0
  47. data/db/migrations/013_default_org_membership.rb +30 -0
  48. data/db/migrations/014_webhook_subscription_deliveries.rb +26 -0
  49. data/db/migrations/015_dependent_integrations.rb +9 -0
  50. data/db/migrations/016_encrypted_columns.rb +9 -0
  51. data/db/migrations/017_skip_verification.rb +9 -0
  52. data/db/migrations/018_sync_targets.rb +25 -0
  53. data/db/migrations/019_org_schema.rb +9 -0
  54. data/db/migrations/020_org_database_migrations.rb +25 -0
  55. data/db/migrations/021_no_default_org_schema.rb +14 -0
  56. data/db/migrations/022_database_document.rb +15 -0
  57. data/db/migrations/023_sync_target_schema.rb +9 -0
  58. data/db/migrations/024_org_semaphore_jobs.rb +9 -0
  59. data/db/migrations/025_integration_backfill_cursor.rb +9 -0
  60. data/db/migrations/026_undo_integration_backfill_cursor.rb +9 -0
  61. data/db/migrations/027_sync_target_http_sync.rb +12 -0
  62. data/db/migrations/028_logged_webhook_path.rb +24 -0
  63. data/db/migrations/029_encrypt_columns.rb +97 -0
  64. data/db/migrations/030_org_sync_target_timeout.rb +9 -0
  65. data/db/migrations/031_org_max_query_rows.rb +9 -0
  66. data/db/migrations/032_remove_db_defaults.rb +12 -0
  67. data/db/migrations/033_backfill_jobs.rb +26 -0
  68. data/db/migrations/034_backfill_job_criteria.rb +9 -0
  69. data/db/migrations/035_synchronous_backfill.rb +9 -0
  70. data/db/migrations/036_oauth.rb +26 -0
  71. data/db/migrations/037_oauth_used.rb +9 -0
  72. data/lib/amigo/durable_job.rb +416 -0
  73. data/lib/pry/clipboard.rb +111 -0
  74. data/lib/sequel/advisory_lock.rb +65 -0
  75. data/lib/webhookdb/admin.rb +4 -0
  76. data/lib/webhookdb/admin_api/auth.rb +36 -0
  77. data/lib/webhookdb/admin_api/customers.rb +63 -0
  78. data/lib/webhookdb/admin_api/database_documents.rb +20 -0
  79. data/lib/webhookdb/admin_api/entities.rb +66 -0
  80. data/lib/webhookdb/admin_api/message_deliveries.rb +61 -0
  81. data/lib/webhookdb/admin_api/roles.rb +15 -0
  82. data/lib/webhookdb/admin_api.rb +34 -0
  83. data/lib/webhookdb/aggregate_result.rb +63 -0
  84. data/lib/webhookdb/api/auth.rb +122 -0
  85. data/lib/webhookdb/api/connstr_auth.rb +36 -0
  86. data/lib/webhookdb/api/db.rb +188 -0
  87. data/lib/webhookdb/api/demo.rb +14 -0
  88. data/lib/webhookdb/api/entities.rb +198 -0
  89. data/lib/webhookdb/api/helpers.rb +253 -0
  90. data/lib/webhookdb/api/install.rb +296 -0
  91. data/lib/webhookdb/api/me.rb +53 -0
  92. data/lib/webhookdb/api/organizations.rb +254 -0
  93. data/lib/webhookdb/api/replay.rb +64 -0
  94. data/lib/webhookdb/api/service_integrations.rb +402 -0
  95. data/lib/webhookdb/api/services.rb +27 -0
  96. data/lib/webhookdb/api/stripe.rb +22 -0
  97. data/lib/webhookdb/api/subscriptions.rb +67 -0
  98. data/lib/webhookdb/api/sync_targets.rb +232 -0
  99. data/lib/webhookdb/api/system.rb +37 -0
  100. data/lib/webhookdb/api/webhook_subscriptions.rb +96 -0
  101. data/lib/webhookdb/api.rb +92 -0
  102. data/lib/webhookdb/apps.rb +93 -0
  103. data/lib/webhookdb/async/audit_logger.rb +38 -0
  104. data/lib/webhookdb/async/autoscaler.rb +84 -0
  105. data/lib/webhookdb/async/job.rb +18 -0
  106. data/lib/webhookdb/async/job_logger.rb +45 -0
  107. data/lib/webhookdb/async/scheduled_job.rb +18 -0
  108. data/lib/webhookdb/async.rb +142 -0
  109. data/lib/webhookdb/aws.rb +98 -0
  110. data/lib/webhookdb/backfill_job.rb +107 -0
  111. data/lib/webhookdb/backfiller.rb +107 -0
  112. data/lib/webhookdb/cloudflare.rb +39 -0
  113. data/lib/webhookdb/connection_cache.rb +177 -0
  114. data/lib/webhookdb/console.rb +71 -0
  115. data/lib/webhookdb/convertkit.rb +14 -0
  116. data/lib/webhookdb/crypto.rb +66 -0
  117. data/lib/webhookdb/customer/reset_code.rb +94 -0
  118. data/lib/webhookdb/customer.rb +347 -0
  119. data/lib/webhookdb/database_document.rb +72 -0
  120. data/lib/webhookdb/db_adapter/column_types.rb +37 -0
  121. data/lib/webhookdb/db_adapter/default_sql.rb +187 -0
  122. data/lib/webhookdb/db_adapter/pg.rb +96 -0
  123. data/lib/webhookdb/db_adapter/snowflake.rb +137 -0
  124. data/lib/webhookdb/db_adapter.rb +208 -0
  125. data/lib/webhookdb/dbutil.rb +92 -0
  126. data/lib/webhookdb/demo_mode.rb +100 -0
  127. data/lib/webhookdb/developer_alert.rb +51 -0
  128. data/lib/webhookdb/email_octopus.rb +21 -0
  129. data/lib/webhookdb/enumerable.rb +18 -0
  130. data/lib/webhookdb/fixtures/backfill_jobs.rb +72 -0
  131. data/lib/webhookdb/fixtures/customers.rb +65 -0
  132. data/lib/webhookdb/fixtures/database_documents.rb +27 -0
  133. data/lib/webhookdb/fixtures/faker.rb +41 -0
  134. data/lib/webhookdb/fixtures/logged_webhooks.rb +56 -0
  135. data/lib/webhookdb/fixtures/message_deliveries.rb +59 -0
  136. data/lib/webhookdb/fixtures/oauth_sessions.rb +24 -0
  137. data/lib/webhookdb/fixtures/organization_database_migrations.rb +37 -0
  138. data/lib/webhookdb/fixtures/organization_memberships.rb +54 -0
  139. data/lib/webhookdb/fixtures/organizations.rb +32 -0
  140. data/lib/webhookdb/fixtures/reset_codes.rb +23 -0
  141. data/lib/webhookdb/fixtures/service_integrations.rb +42 -0
  142. data/lib/webhookdb/fixtures/subscriptions.rb +33 -0
  143. data/lib/webhookdb/fixtures/sync_targets.rb +32 -0
  144. data/lib/webhookdb/fixtures/webhook_subscriptions.rb +35 -0
  145. data/lib/webhookdb/fixtures.rb +15 -0
  146. data/lib/webhookdb/formatting.rb +56 -0
  147. data/lib/webhookdb/front.rb +49 -0
  148. data/lib/webhookdb/github.rb +22 -0
  149. data/lib/webhookdb/google_calendar.rb +29 -0
  150. data/lib/webhookdb/heroku.rb +21 -0
  151. data/lib/webhookdb/http.rb +114 -0
  152. data/lib/webhookdb/icalendar.rb +17 -0
  153. data/lib/webhookdb/id.rb +17 -0
  154. data/lib/webhookdb/idempotency.rb +90 -0
  155. data/lib/webhookdb/increase.rb +42 -0
  156. data/lib/webhookdb/intercom.rb +23 -0
  157. data/lib/webhookdb/jobs/amigo_test_jobs.rb +118 -0
  158. data/lib/webhookdb/jobs/backfill.rb +32 -0
  159. data/lib/webhookdb/jobs/create_mirror_table.rb +18 -0
  160. data/lib/webhookdb/jobs/create_stripe_customer.rb +17 -0
  161. data/lib/webhookdb/jobs/customer_created_notify_internal.rb +22 -0
  162. data/lib/webhookdb/jobs/demo_mode_sync_data.rb +19 -0
  163. data/lib/webhookdb/jobs/deprecated_jobs.rb +19 -0
  164. data/lib/webhookdb/jobs/developer_alert_handle.rb +14 -0
  165. data/lib/webhookdb/jobs/durable_job_recheck_poller.rb +17 -0
  166. data/lib/webhookdb/jobs/emailer.rb +15 -0
  167. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +25 -0
  168. data/lib/webhookdb/jobs/icalendar_sync.rb +23 -0
  169. data/lib/webhookdb/jobs/logged_webhook_replay.rb +17 -0
  170. data/lib/webhookdb/jobs/logged_webhook_resilient_replay.rb +15 -0
  171. data/lib/webhookdb/jobs/message_dispatched.rb +16 -0
  172. data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +21 -0
  173. data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +21 -0
  174. data/lib/webhookdb/jobs/organization_database_migration_run.rb +24 -0
  175. data/lib/webhookdb/jobs/prepare_database_connections.rb +22 -0
  176. data/lib/webhookdb/jobs/process_webhook.rb +47 -0
  177. data/lib/webhookdb/jobs/renew_watch_channel.rb +24 -0
  178. data/lib/webhookdb/jobs/replication_migration.rb +24 -0
  179. data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +23 -0
  180. data/lib/webhookdb/jobs/scheduled_backfills.rb +77 -0
  181. data/lib/webhookdb/jobs/send_invite.rb +15 -0
  182. data/lib/webhookdb/jobs/send_test_webhook.rb +25 -0
  183. data/lib/webhookdb/jobs/send_webhook.rb +20 -0
  184. data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +16 -0
  185. data/lib/webhookdb/jobs/sync_target_run_sync.rb +38 -0
  186. data/lib/webhookdb/jobs/trim_logged_webhooks.rb +15 -0
  187. data/lib/webhookdb/jobs/webhook_resource_notify_integrations.rb +30 -0
  188. data/lib/webhookdb/jobs/webhook_subscription_delivery_attempt.rb +29 -0
  189. data/lib/webhookdb/jobs.rb +4 -0
  190. data/lib/webhookdb/json.rb +113 -0
  191. data/lib/webhookdb/liquid/expose.rb +27 -0
  192. data/lib/webhookdb/liquid/filters.rb +16 -0
  193. data/lib/webhookdb/liquid/liquification.rb +26 -0
  194. data/lib/webhookdb/liquid/partial.rb +12 -0
  195. data/lib/webhookdb/logged_webhook/resilient.rb +95 -0
  196. data/lib/webhookdb/logged_webhook.rb +194 -0
  197. data/lib/webhookdb/message/body.rb +25 -0
  198. data/lib/webhookdb/message/delivery.rb +127 -0
  199. data/lib/webhookdb/message/email_transport.rb +133 -0
  200. data/lib/webhookdb/message/fake_transport.rb +54 -0
  201. data/lib/webhookdb/message/liquid_drops.rb +29 -0
  202. data/lib/webhookdb/message/template.rb +89 -0
  203. data/lib/webhookdb/message/transport.rb +43 -0
  204. data/lib/webhookdb/message.rb +150 -0
  205. data/lib/webhookdb/messages/error_icalendar_fetch.rb +42 -0
  206. data/lib/webhookdb/messages/invite.rb +23 -0
  207. data/lib/webhookdb/messages/new_customer.rb +14 -0
  208. data/lib/webhookdb/messages/org_database_migration_finished.rb +23 -0
  209. data/lib/webhookdb/messages/org_database_migration_started.rb +24 -0
  210. data/lib/webhookdb/messages/specs.rb +57 -0
  211. data/lib/webhookdb/messages/verification.rb +23 -0
  212. data/lib/webhookdb/method_utilities.rb +82 -0
  213. data/lib/webhookdb/microsoft_calendar.rb +36 -0
  214. data/lib/webhookdb/nextpax.rb +14 -0
  215. data/lib/webhookdb/oauth/front.rb +58 -0
  216. data/lib/webhookdb/oauth/intercom.rb +58 -0
  217. data/lib/webhookdb/oauth/session.rb +24 -0
  218. data/lib/webhookdb/oauth.rb +80 -0
  219. data/lib/webhookdb/organization/alerting.rb +35 -0
  220. data/lib/webhookdb/organization/database_migration.rb +151 -0
  221. data/lib/webhookdb/organization/db_builder.rb +429 -0
  222. data/lib/webhookdb/organization.rb +506 -0
  223. data/lib/webhookdb/organization_membership.rb +58 -0
  224. data/lib/webhookdb/phone_number.rb +38 -0
  225. data/lib/webhookdb/plaid.rb +23 -0
  226. data/lib/webhookdb/platform.rb +27 -0
  227. data/lib/webhookdb/plivo.rb +52 -0
  228. data/lib/webhookdb/postgres/maintenance.rb +166 -0
  229. data/lib/webhookdb/postgres/model.rb +82 -0
  230. data/lib/webhookdb/postgres/model_utilities.rb +382 -0
  231. data/lib/webhookdb/postgres/testing_pixie.rb +16 -0
  232. data/lib/webhookdb/postgres/validations.rb +46 -0
  233. data/lib/webhookdb/postgres.rb +176 -0
  234. data/lib/webhookdb/postmark.rb +20 -0
  235. data/lib/webhookdb/redis.rb +35 -0
  236. data/lib/webhookdb/replicator/atom_single_feed_v1.rb +116 -0
  237. data/lib/webhookdb/replicator/aws_pricing_v1.rb +488 -0
  238. data/lib/webhookdb/replicator/base.rb +1185 -0
  239. data/lib/webhookdb/replicator/column.rb +482 -0
  240. data/lib/webhookdb/replicator/convertkit_broadcast_v1.rb +69 -0
  241. data/lib/webhookdb/replicator/convertkit_subscriber_v1.rb +200 -0
  242. data/lib/webhookdb/replicator/convertkit_tag_v1.rb +66 -0
  243. data/lib/webhookdb/replicator/convertkit_v1_mixin.rb +65 -0
  244. data/lib/webhookdb/replicator/docgen.rb +167 -0
  245. data/lib/webhookdb/replicator/email_octopus_campaign_v1.rb +84 -0
  246. data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +159 -0
  247. data/lib/webhookdb/replicator/email_octopus_event_v1.rb +244 -0
  248. data/lib/webhookdb/replicator/email_octopus_list_v1.rb +101 -0
  249. data/lib/webhookdb/replicator/fake.rb +453 -0
  250. data/lib/webhookdb/replicator/front_conversation_v1.rb +45 -0
  251. data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +55 -0
  252. data/lib/webhookdb/replicator/front_message_v1.rb +45 -0
  253. data/lib/webhookdb/replicator/front_v1_mixin.rb +22 -0
  254. data/lib/webhookdb/replicator/github_issue_comment_v1.rb +58 -0
  255. data/lib/webhookdb/replicator/github_issue_v1.rb +83 -0
  256. data/lib/webhookdb/replicator/github_pull_v1.rb +84 -0
  257. data/lib/webhookdb/replicator/github_release_v1.rb +47 -0
  258. data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +250 -0
  259. data/lib/webhookdb/replicator/github_repository_event_v1.rb +45 -0
  260. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +465 -0
  261. data/lib/webhookdb/replicator/icalendar_event_v1.rb +334 -0
  262. data/lib/webhookdb/replicator/increase_account_number_v1.rb +77 -0
  263. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +61 -0
  264. data/lib/webhookdb/replicator/increase_account_v1.rb +63 -0
  265. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +78 -0
  266. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +64 -0
  267. data/lib/webhookdb/replicator/increase_limit_v1.rb +78 -0
  268. data/lib/webhookdb/replicator/increase_transaction_v1.rb +74 -0
  269. data/lib/webhookdb/replicator/increase_v1_mixin.rb +121 -0
  270. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +61 -0
  271. data/lib/webhookdb/replicator/intercom_contact_v1.rb +36 -0
  272. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +38 -0
  273. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +69 -0
  274. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +105 -0
  275. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +65 -0
  276. data/lib/webhookdb/replicator/plivo_sms_inbound_v1.rb +102 -0
  277. data/lib/webhookdb/replicator/postmark_inbound_message_v1.rb +94 -0
  278. data/lib/webhookdb/replicator/postmark_outbound_message_event_v1.rb +107 -0
  279. data/lib/webhookdb/replicator/schema_modification.rb +42 -0
  280. data/lib/webhookdb/replicator/shopify_customer_v1.rb +58 -0
  281. data/lib/webhookdb/replicator/shopify_order_v1.rb +64 -0
  282. data/lib/webhookdb/replicator/shopify_v1_mixin.rb +161 -0
  283. data/lib/webhookdb/replicator/signalwire_message_v1.rb +169 -0
  284. data/lib/webhookdb/replicator/sponsy_customer_v1.rb +54 -0
  285. data/lib/webhookdb/replicator/sponsy_placement_v1.rb +34 -0
  286. data/lib/webhookdb/replicator/sponsy_publication_v1.rb +125 -0
  287. data/lib/webhookdb/replicator/sponsy_slot_v1.rb +41 -0
  288. data/lib/webhookdb/replicator/sponsy_status_v1.rb +35 -0
  289. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +165 -0
  290. data/lib/webhookdb/replicator/state_machine_step.rb +69 -0
  291. data/lib/webhookdb/replicator/stripe_charge_v1.rb +77 -0
  292. data/lib/webhookdb/replicator/stripe_coupon_v1.rb +62 -0
  293. data/lib/webhookdb/replicator/stripe_customer_v1.rb +60 -0
  294. data/lib/webhookdb/replicator/stripe_dispute_v1.rb +77 -0
  295. data/lib/webhookdb/replicator/stripe_invoice_item_v1.rb +82 -0
  296. data/lib/webhookdb/replicator/stripe_invoice_v1.rb +116 -0
  297. data/lib/webhookdb/replicator/stripe_payout_v1.rb +67 -0
  298. data/lib/webhookdb/replicator/stripe_price_v1.rb +60 -0
  299. data/lib/webhookdb/replicator/stripe_product_v1.rb +60 -0
  300. data/lib/webhookdb/replicator/stripe_refund_v1.rb +101 -0
  301. data/lib/webhookdb/replicator/stripe_subscription_item_v1.rb +56 -0
  302. data/lib/webhookdb/replicator/stripe_subscription_v1.rb +75 -0
  303. data/lib/webhookdb/replicator/stripe_v1_mixin.rb +116 -0
  304. data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +141 -0
  305. data/lib/webhookdb/replicator/transistor_episode_v1.rb +169 -0
  306. data/lib/webhookdb/replicator/transistor_show_v1.rb +68 -0
  307. data/lib/webhookdb/replicator/transistor_v1_mixin.rb +65 -0
  308. data/lib/webhookdb/replicator/twilio_sms_v1.rb +156 -0
  309. data/lib/webhookdb/replicator/webhook_request.rb +5 -0
  310. data/lib/webhookdb/replicator/webhookdb_customer_v1.rb +74 -0
  311. data/lib/webhookdb/replicator.rb +224 -0
  312. data/lib/webhookdb/role.rb +42 -0
  313. data/lib/webhookdb/sentry.rb +35 -0
  314. data/lib/webhookdb/service/auth.rb +138 -0
  315. data/lib/webhookdb/service/collection.rb +91 -0
  316. data/lib/webhookdb/service/entities.rb +97 -0
  317. data/lib/webhookdb/service/helpers.rb +270 -0
  318. data/lib/webhookdb/service/middleware.rb +124 -0
  319. data/lib/webhookdb/service/types.rb +30 -0
  320. data/lib/webhookdb/service/validators.rb +32 -0
  321. data/lib/webhookdb/service/view_api.rb +63 -0
  322. data/lib/webhookdb/service.rb +219 -0
  323. data/lib/webhookdb/service_integration.rb +332 -0
  324. data/lib/webhookdb/shopify.rb +35 -0
  325. data/lib/webhookdb/signalwire.rb +13 -0
  326. data/lib/webhookdb/slack.rb +68 -0
  327. data/lib/webhookdb/snowflake.rb +90 -0
  328. data/lib/webhookdb/spec_helpers/async.rb +122 -0
  329. data/lib/webhookdb/spec_helpers/citest.rb +88 -0
  330. data/lib/webhookdb/spec_helpers/integration.rb +121 -0
  331. data/lib/webhookdb/spec_helpers/message.rb +41 -0
  332. data/lib/webhookdb/spec_helpers/postgres.rb +220 -0
  333. data/lib/webhookdb/spec_helpers/service.rb +432 -0
  334. data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +56 -0
  335. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +915 -0
  336. data/lib/webhookdb/spec_helpers/whdb.rb +139 -0
  337. data/lib/webhookdb/spec_helpers.rb +63 -0
  338. data/lib/webhookdb/sponsy.rb +14 -0
  339. data/lib/webhookdb/stripe.rb +37 -0
  340. data/lib/webhookdb/subscription.rb +203 -0
  341. data/lib/webhookdb/sync_target.rb +491 -0
  342. data/lib/webhookdb/tasks/admin.rb +49 -0
  343. data/lib/webhookdb/tasks/annotate.rb +36 -0
  344. data/lib/webhookdb/tasks/db.rb +82 -0
  345. data/lib/webhookdb/tasks/docs.rb +42 -0
  346. data/lib/webhookdb/tasks/fixture.rb +35 -0
  347. data/lib/webhookdb/tasks/message.rb +50 -0
  348. data/lib/webhookdb/tasks/regress.rb +87 -0
  349. data/lib/webhookdb/tasks/release.rb +27 -0
  350. data/lib/webhookdb/tasks/sidekiq.rb +23 -0
  351. data/lib/webhookdb/tasks/specs.rb +64 -0
  352. data/lib/webhookdb/theranest.rb +15 -0
  353. data/lib/webhookdb/transistor.rb +13 -0
  354. data/lib/webhookdb/twilio.rb +13 -0
  355. data/lib/webhookdb/typed_struct.rb +44 -0
  356. data/lib/webhookdb/version.rb +5 -0
  357. data/lib/webhookdb/webhook_response.rb +50 -0
  358. data/lib/webhookdb/webhook_subscription/delivery.rb +82 -0
  359. data/lib/webhookdb/webhook_subscription.rb +226 -0
  360. data/lib/webhookdb/windows_tz.rb +32 -0
  361. data/lib/webhookdb/xml.rb +92 -0
  362. data/lib/webhookdb.rb +224 -0
  363. data/lib/webterm/apps.rb +45 -0
  364. metadata +1129 -0
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/formatting"
4
+ require "webhookdb/id"
5
+ require "webhookdb/postgres/model"
6
+ require "sequel/plugins/soft_deletes"
7
+
8
+ class Webhookdb::ServiceIntegration < Webhookdb::Postgres::Model(:service_integrations)
9
+ class TableRenameError < Webhookdb::InvalidInput; end
10
+
11
+ # We limit the information that a user can access through the CLI to these fields.
12
+ # Blank string returns all info.
13
+ INTEGRATION_INFO_FIELDS = ["id", "service", "table", "url", "webhook_secret", ""].freeze
14
+
15
+ plugin :timestamps
16
+ plugin :column_encryption do |enc|
17
+ enc.column :data_encryption_secret
18
+ enc.column :webhook_secret
19
+ enc.column :backfill_key
20
+ enc.column :backfill_secret
21
+ end
22
+
23
+ many_to_one :organization, class: "Webhookdb::Organization"
24
+ one_to_many :webhook_subscriptions, class: "Webhookdb::WebhookSubscription"
25
+ one_to_many :all_webhook_subscriptions,
26
+ class: "Webhookdb::WebhookSubscription",
27
+ readonly: true,
28
+ dataset: (
29
+ lambda do |r|
30
+ r.associated_dataset.where(
31
+ Sequel[organization_id:] | Sequel[service_integration_id: id],
32
+ )
33
+ end),
34
+ eager_loader: (
35
+ lambda do |eo|
36
+ sint_ids = eo[:id_map].keys
37
+ org_ids_for_sints = eo[:rows].to_h { |r| [r.id, r.organization_id] }
38
+ all_subs = Webhookdb::WebhookSubscription.
39
+ left_join(:service_integrations, {id: :service_integration_id}).
40
+ select(Sequel[:webhook_subscriptions][Sequel.lit("*")]).
41
+ where(
42
+ Sequel[Sequel[:webhook_subscriptions][:organization_id] => org_ids_for_sints.values.uniq] |
43
+ Sequel[Sequel[:webhook_subscriptions][:service_integration_id] => sint_ids],
44
+ ).all
45
+ subs_by_sint = {}
46
+ subs_by_org = {}
47
+ all_subs.each do |sub|
48
+ if (orgid = sub[:organization_id])
49
+ subs = subs_by_org[orgid] ||= []
50
+ else
51
+ sint_id = sub[:service_integration_id]
52
+ subs = subs_by_sint[sint_id] ||= []
53
+ end
54
+ subs << sub
55
+ end
56
+ eo[:rows].each do |sint|
57
+ subs = subs_by_sint.fetch(sint.id, [])
58
+ subs.concat(subs_by_org.fetch(sint.organization_id, []))
59
+ sint.associations[:all_webhook_subscriptions] = subs
60
+ end
61
+ end)
62
+
63
+ many_to_one :depends_on, class: self
64
+ one_to_many :dependents, key: :depends_on_id, class: self
65
+ one_to_many :sync_targets, class: "Webhookdb::SyncTarget"
66
+
67
+ def self.create_disambiguated(service_name, **kwargs)
68
+ kwargs[:table_name] ||= "#{service_name}_#{SecureRandom.hex(2)}"
69
+ return self.create(service_name:, **kwargs)
70
+ end
71
+
72
+ def can_be_modified_by?(customer)
73
+ return customer.verified_member_of?(self.organization)
74
+ end
75
+
76
+ # @return [Webhookdb::Replicator::Base]
77
+ def replicator
78
+ return Webhookdb::Replicator.create(self)
79
+ end
80
+
81
+ def log_tags
82
+ return {
83
+ service_integration_id: self.id,
84
+ service_integration_name: self.service_name,
85
+ service_integration_table: self.table_name,
86
+ **self.organization.log_tags,
87
+ }
88
+ end
89
+
90
+ def authed_api_path
91
+ return "/v1/organizations/#{self.organization_id}/service_integrations/#{self.opaque_id}"
92
+ end
93
+
94
+ def unauthed_webhook_path
95
+ return "/v1/service_integrations/#{self.opaque_id}"
96
+ end
97
+
98
+ def unauthed_webhook_endpoint
99
+ return Webhookdb.api_url + self.unauthed_webhook_path
100
+ end
101
+
102
+ def plan_supports_integration?
103
+ # if the sint's organization has an active subscription, return true
104
+ return true if self.organization.active_subscription?
105
+ # if there is no active subscription, check whether the integration is one of the first two
106
+ # created by the organization
107
+ limit = Webhookdb::Subscription.max_free_integrations
108
+ free_integrations = Webhookdb::ServiceIntegration.
109
+ where(organization: self.organization).order(:created_at, :id).limit(limit).all
110
+ free_integrations.each do |sint|
111
+ return true if sint.id == self.id
112
+ end
113
+ # if not, the integration is not supported
114
+ return false
115
+ end
116
+
117
+ # Return service integrations that can be used as the dependency
118
+ # for this integration.
119
+ # @return [Array<Webhookdb::ServiceIntegration>]
120
+ def dependency_candidates
121
+ dep_descr = self.replicator.descriptor.dependency_descriptor
122
+ return [] if dep_descr.nil?
123
+ return self.organization.service_integrations.
124
+ select { |si| si.service_name == dep_descr.name }
125
+ end
126
+
127
+ def recursive_dependents
128
+ return self.dependents + self.dependents.flat_map(&:recursive_dependents)
129
+ end
130
+
131
+ def destroy_self_and_all_dependents
132
+ self.dependents.each(&:destroy_self_and_all_dependents)
133
+
134
+ begin
135
+ self.replicator.admin_dataset(timeout: :fast) { |ds| ds.db << "DROP TABLE #{self.table_name}" }
136
+ rescue Sequel::DatabaseError => e
137
+ raise unless e.wrapped_exception.is_a?(PG::UndefinedTable)
138
+ end
139
+ self.destroy
140
+ end
141
+
142
+ class Stats
143
+ attr_reader :message, :data
144
+
145
+ def initialize(message, data)
146
+ @message = message
147
+ @data = data
148
+ end
149
+
150
+ def display_headers
151
+ return [
152
+ [:count_last_7_days_formatted, "Count Last 7 Days"],
153
+ [:success_last_7_days_formatted, "Successful Last 7 Days"],
154
+ [:success_last_7_days_percent_formatted, "Successful Last 7 Days %"],
155
+ [:rejected_last_7_days_formatted, "Rejected Last 7 Days"],
156
+ [:rejected_last_7_days_percent_formatted, "Rejected Last 7 Days %"],
157
+ [:successful_of_last_10_formatted, "Successful Of Last 10 Webhooks"],
158
+ [:rejected_of_last_10_formatted, "Rejected Of Last 10 Webhooks"],
159
+ ]
160
+ end
161
+
162
+ def as_json(*_o)
163
+ return @data.merge(message: @message, display_headers: self.display_headers)
164
+ end
165
+ end
166
+
167
+ # @return [Webhookdb::ServiceIntegration::Stats]
168
+ def stats
169
+ all_logged_webhooks = Webhookdb::LoggedWebhook.where(
170
+ service_integration_opaque_id: self.opaque_id,
171
+ ).where { inserted_at > 7.days.ago }
172
+
173
+ if all_logged_webhooks.empty?
174
+ return Stats.new(
175
+ "We have no record of receiving webhooks for that integration in the past seven days.",
176
+ {},
177
+ )
178
+ end
179
+
180
+ # rubocop:disable Naming/VariableNumber
181
+ count_last_7_days = all_logged_webhooks.count
182
+ rejected_last_7_days = all_logged_webhooks.where { response_status >= 400 }.count
183
+ success_last_7_days = (count_last_7_days - rejected_last_7_days)
184
+ rejected_last_7_days_percent = (rejected_last_7_days.to_f / count_last_7_days)
185
+ success_last_7_days_percent = (success_last_7_days.to_f / count_last_7_days)
186
+ last_10 = Webhookdb::LoggedWebhook.order_by(Sequel.desc(:inserted_at)).limit(10).select_map(:response_status)
187
+ last_10_success, last_10_rejected = last_10.partition { |rs| rs < 400 }
188
+
189
+ data = {
190
+ count_last_7_days:,
191
+ count_last_7_days_formatted: count_last_7_days.to_s,
192
+ success_last_7_days:,
193
+ success_last_7_days_formatted: success_last_7_days.to_s,
194
+ success_last_7_days_percent:,
195
+ success_last_7_days_percent_formatted: "%.1f%%" % (success_last_7_days_percent * 100),
196
+ rejected_last_7_days:,
197
+ rejected_last_7_days_formatted: rejected_last_7_days.to_s,
198
+ rejected_last_7_days_percent:,
199
+ rejected_last_7_days_percent_formatted: "%.1f%%" % (rejected_last_7_days_percent * 100),
200
+ successful_of_last_10: last_10_success.size,
201
+ successful_of_last_10_formatted: last_10_success.size.to_s,
202
+ rejected_of_last_10: last_10_rejected.size,
203
+ rejected_of_last_10_formatted: last_10_rejected.size.to_s,
204
+ }
205
+ # rubocop:enable Naming/VariableNumber
206
+ return Stats.new("", data)
207
+ end
208
+
209
+ def rename_table(to:)
210
+ Webhookdb::Organization::DatabaseMigration.guard_ongoing!(self.organization)
211
+ unless Webhookdb::DBAdapter::VALID_IDENTIFIER.match?(to)
212
+ msg = "Sorry, this is not a valid table name. " + Webhookdb::DBAdapter::INVALID_IDENTIFIER_MESSAGE
213
+ msg += " And we see you what you did there ;)" if to.include?(";") && to.downcase.include?("drop")
214
+ raise TableRenameError, msg
215
+ end
216
+ self.db.transaction do
217
+ begin
218
+ self.organization.admin_connection { |db| db << "ALTER TABLE #{self.table_name} RENAME TO #{to}" }
219
+ rescue Sequel::DatabaseError => e
220
+ case e.wrapped_exception
221
+ when PG::DuplicateTable
222
+ raise TableRenameError,
223
+ "There is already a table named \"#{to}\". Run `webhookdb db tables` to see available tables."
224
+ when PG::SyntaxError
225
+ raise TableRenameError,
226
+ "Please try again with double quotes around '#{to}' since it contains invalid identifier characters."
227
+ else
228
+ raise e
229
+ end
230
+ end
231
+ self.update(table_name: to)
232
+ end
233
+ end
234
+
235
+ def requires_sequence?
236
+ return self.replicator.requires_sequence?
237
+ end
238
+
239
+ def sequence_name
240
+ return "replicator_seq_org_#{self.organization_id}_#{self.service_name}_#{self.id}_seq"
241
+ end
242
+
243
+ def ensure_sequence(skip_check: false)
244
+ self.db << self.ensure_sequence_sql(skip_check:)
245
+ end
246
+
247
+ def ensure_sequence_sql(skip_check: false)
248
+ raise Webhookdb::InvalidPrecondition, "#{self.service_name} does not require sequence" if
249
+ !skip_check && !self.requires_sequence?
250
+ return "CREATE SEQUENCE IF NOT EXISTS #{self.sequence_name}"
251
+ end
252
+
253
+ def sequence_nextval
254
+ return self.db.select(Sequel.function(:nextval, self.sequence_name)).single_value
255
+ end
256
+
257
+ #
258
+ # :Sequel Hooks:
259
+ #
260
+
261
+ def before_create
262
+ self[:opaque_id] ||= Webhookdb::Id.new_opaque_id("svi")
263
+ end
264
+
265
+ # @!attribute organization
266
+ # @return [Webhookdb::Organization]
267
+
268
+ # @!attribute table_name
269
+ # @return [String] Name of the table
270
+
271
+ # @!attribute service_name
272
+ # @return [String] Lookup name of the service
273
+
274
+ # @!attribute opaque_id
275
+ # @return [String]
276
+
277
+ # @!attribute api_url
278
+ # @return [String] Root Url of the api to backfill from
279
+
280
+ # @!attribute backfill_key
281
+ # @return [String] Key for backfilling.
282
+
283
+ # @!attribute backfill_secret
284
+ # @return [String] Password/secret for backfilling.
285
+
286
+ # @!attribute webhook_secret
287
+ # @return [String] Secret used to sign webhooks.
288
+
289
+ # @!attribute depends_on
290
+ # @return [Webhookdb::ServiceIntegration]
291
+
292
+ # @!attribute data_encryption_secret
293
+ # @return [String] The encryption key used to encrypt data for this organization.
294
+ # Note that this field is itself encrypted using Sequel encryption;
295
+ # its decrypted value is meant to be used as the data encryption key.
296
+
297
+ # @!attribute skip_webhook_verification
298
+ # @return [Boolean] Set this to disable webhook verification on this integration.
299
+ # Useful when replaying logged webhooks.
300
+ end
301
+
302
+ # Table: service_integrations
303
+ # -----------------------------------------------------------------------------------------------------------------------------------------------------------
304
+ # Columns:
305
+ # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
306
+ # created_at | timestamp with time zone | NOT NULL DEFAULT now()
307
+ # updated_at | timestamp with time zone |
308
+ # organization_id | integer | NOT NULL
309
+ # api_url | text | NOT NULL DEFAULT ''::text
310
+ # opaque_id | text | NOT NULL
311
+ # service_name | text | NOT NULL
312
+ # webhook_secret | text |
313
+ # table_name | text | NOT NULL
314
+ # backfill_key | text |
315
+ # backfill_secret | text |
316
+ # last_backfilled_at | timestamp with time zone |
317
+ # depends_on_id | integer |
318
+ # data_encryption_secret | text |
319
+ # skip_webhook_verification | boolean | NOT NULL DEFAULT false
320
+ # Indexes:
321
+ # service_integrations_pkey | PRIMARY KEY btree (id)
322
+ # service_integrations_opaque_id_key | UNIQUE btree (opaque_id)
323
+ # unique_tablename_in_org | UNIQUE btree (organization_id, table_name)
324
+ # Foreign key constraints:
325
+ # service_integrations_depends_on_id_fkey | (depends_on_id) REFERENCES service_integrations(id) ON DELETE RESTRICT
326
+ # service_integrations_organization_id_fkey | (organization_id) REFERENCES organizations(id)
327
+ # Referenced By:
328
+ # backfill_jobs | backfill_jobs_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id) ON DELETE CASCADE
329
+ # service_integrations | service_integrations_depends_on_id_fkey | (depends_on_id) REFERENCES service_integrations(id) ON DELETE RESTRICT
330
+ # sync_targets | sync_targets_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id) ON DELETE CASCADE
331
+ # webhook_subscriptions | webhook_subscriptions_service_integration_id_fkey | (service_integration_id) REFERENCES service_integrations(id)
332
+ # -----------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/security_utils"
4
+
5
+ class Webhookdb::Shopify
6
+ include Appydays::Configurable
7
+
8
+ configurable(:shopify) do
9
+ setting :http_timeout, 30
10
+ end
11
+
12
+ # This function is used in the backfill process to parse out the
13
+ # pagination_token from the responses
14
+ def self.parse_link_header(header)
15
+ parts = header.split(",")
16
+
17
+ parts.to_h do |part, _|
18
+ section = part.split(";")
19
+ name = section[1][/rel="(.*)"/, 1].to_sym
20
+ url = section[0][/<(.*)>/, 1]
21
+ # results = section[2][/results="(.*)"/, 1] == 'true'
22
+
23
+ [name, url]
24
+ end
25
+ end
26
+
27
+ # Compare the computed HMAC digest based on the shared secret and the
28
+ # request contents to the reported HMAC in the headers
29
+ #
30
+ # see https://shopify.dev/tutorials/manage-webhooks#verifying-webhooks
31
+ def self.verify_webhook(data, hmac_header, webhook_secret)
32
+ calculated_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", webhook_secret, data))
33
+ return ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
34
+ end
35
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appydays/configurable"
4
+ require "appydays/loggable"
5
+
6
+ module Webhookdb::Signalwire
7
+ include Appydays::Configurable
8
+ include Appydays::Loggable
9
+
10
+ configurable(:signalwire) do
11
+ setting :http_timeout, 30
12
+ end
13
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "appydays/configurable"
4
+ require "slack-notifier"
5
+
6
+ require "webhookdb"
7
+
8
+ class Webhookdb::Slack
9
+ include Appydays::Configurable
10
+ extend Webhookdb::MethodUtilities
11
+
12
+ # Set this during testing
13
+ singleton_attr_accessor :http_client
14
+ @http_client = nil
15
+
16
+ configurable(:slack) do
17
+ setting :webhook_url, "http://unconfigured-slack-webhook"
18
+ setting :channel_override, nil
19
+ setting :suppress_all, false
20
+ end
21
+
22
+ def self.new_notifier(opts={})
23
+ opts[:channel] ||= "#eng-naboo"
24
+ opts[:username] ||= "Unknown"
25
+ opts[:icon_emoji] ||= ":question:"
26
+ opts[:channel] = self.channel_override if self.channel_override
27
+ if (force_chan = opts.delete(:force_channel))
28
+ opts[:channel] = force_chan
29
+ end
30
+ return ::Slack::Notifier.new(self.webhook_url) do
31
+ defaults opts
32
+ if Webhookdb::Slack.suppress_all
33
+ http_client NoOpHttpClient.new
34
+ elsif Webhookdb::Slack.http_client
35
+ http_client Webhookdb::Slack.http_client
36
+ end
37
+ end
38
+ end
39
+
40
+ def self.ignore_channel_not_found
41
+ yield()
42
+ rescue ::Slack::Notifier::APIError => e
43
+ return if e.message.include?("channel_not_found")
44
+ return if e.message.include?("channel_is_archived")
45
+ raise e
46
+ end
47
+
48
+ def self.post_many(channels, notifier_options: {}, payload: {})
49
+ channels.each do |chan|
50
+ notifier = self.new_notifier(notifier_options.merge(channel: chan))
51
+ self.ignore_channel_not_found do
52
+ notifier.post(payload)
53
+ end
54
+ end
55
+ end
56
+
57
+ class NoOpHttpClient
58
+ attr_reader :posts
59
+
60
+ def initialize
61
+ @posts = []
62
+ end
63
+
64
+ def post(uri, params={})
65
+ self.posts << [uri, params]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "oj"
4
+ require "open3"
5
+
6
+ class Webhookdb::Snowflake
7
+ include Appydays::Configurable
8
+ include Appydays::Loggable
9
+
10
+ configurable(:snowflake) do
11
+ setting :run_tests, false
12
+ setting :test_url, "snowflake://user:pwd@host/dbname"
13
+ setting :snowsql, "snowsql"
14
+ end
15
+
16
+ # Given a Snowflake URL, return the command line args.
17
+ # Args for the commandline can be traditional URL pieces (host -> account, user/password, etc),
18
+ # or passed as query params.
19
+ # Rules are:
20
+ # - Any query param exactly matching accountname/username/dbname/schemaname/rolename/warehouse
21
+ # is used.
22
+ # - Any query param matching account/user/db/schema/role is used.
23
+ # - URI hostname is used as accountname, basic auth user as username, and uri path as dbname.
24
+ # - Password is pulled from query param 'password' or uri basic auth password.
25
+ def self.parse_url_to_cli_args(url, format: "json")
26
+ uri = URI(url)
27
+ params = Rack::Utils.parse_query(uri.query)
28
+ password = params["password"] || uri.password
29
+ raise ArgumentError, "must provide password in uri basic auth or query params" if password.blank?
30
+ cli = [
31
+ self.snowsql,
32
+ "-o", "friendly=false",
33
+ "-o", "output_format=#{format}",
34
+ "-o", "timing=false",
35
+ "--accountname", params["accountname"] || params["account"] || uri.hostname || "",
36
+ "--username", params["username"] || params["user"] || uri.user || "",
37
+ "--dbname", params["dbname"] || params["db"] || uri.path&.delete_prefix("/") || "",
38
+ ]
39
+ raise ArgumentError, "url requires account (host), user, and db (or uri path): #{url}" if cli.include?("")
40
+
41
+ if (schemaname = params["schemaname"] || params["schema"]).present?
42
+ cli.push("--schemaname", schemaname)
43
+ end
44
+ if (rolename = params["rolename"] || params["role"]).present?
45
+ cli.push("--rolename", rolename)
46
+ end
47
+ cli.push("--warehouse", params["warehouse"]) if params["warehouse"].present?
48
+ return cli, {"SNOWSQL_PWD" => password}
49
+ end
50
+
51
+ def self.run_cli(url, query, parse: false, format: "json")
52
+ args, env = self.parse_url_to_cli_args(url, format:)
53
+ args.push("-q", query)
54
+ stdout, stderr, status = Open3.capture3(env, *args)
55
+
56
+ if stderr.blank? && status.success?
57
+ result = if parse.respond_to?(:call)
58
+ parse.call(stdout)
59
+ elsif parse && format == "json"
60
+ self._parse_json(stdout)
61
+ else
62
+ stdout
63
+ end
64
+ return result
65
+ end
66
+
67
+ self.logger.error("snowflake_error", stdout:, stderr:, status:, cli_args: args)
68
+ msg = "status: #{status}, stderr: #{stderr}, stdout: #{stdout}, query: #{query}"
69
+ raise Webhookdb::InvalidPostcondition, "snowflake failed: #{msg}"
70
+ end
71
+
72
+ # We can't parse newline delimited json easily, so split it ourselves and parse each document.
73
+ def self._parse_json(stdout)
74
+ docs = stdout.split("]\n[")
75
+ result = docs.each_with_index.map do |j, i|
76
+ if docs.size > 1
77
+ if i.zero?
78
+ j += "]"
79
+ else
80
+ j = "[" + j
81
+ end
82
+ end
83
+ Oj.load(j.strip)
84
+ end
85
+ return result
86
+ rescue StandardError => e
87
+ msg = "error: #{e}, stdout: #{stdout}"
88
+ raise Webhookdb::InvalidPostcondition, "Error parsing snowsql output: #{msg}"
89
+ end
90
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq/testing"
4
+
5
+ require "webhookdb/async"
6
+ require "webhookdb/slack"
7
+ require "webhookdb/spec_helpers"
8
+
9
+ module Webhookdb::SpecHelpers::Async
10
+ def self.included(context)
11
+ Sidekiq::Testing.inline!
12
+ Amigo::QueueBackoffJob.reset
13
+
14
+ context.before(:each) do |example|
15
+ if (sidekiq_mode = example.metadata[:sidekiq])
16
+ Sidekiq::Testing.send(:"#{sidekiq_mode}!")
17
+ else
18
+ Sidekiq::Testing.inline!
19
+ end
20
+ Webhookdb::Postgres.do_not_defer_events = true if example.metadata[:do_not_defer_events]
21
+ if example.metadata[:slack]
22
+ Webhookdb::Slack.http_client = Webhookdb::Slack::NoOpHttpClient.new
23
+ Webhookdb::Slack.suppress_all = false
24
+ end
25
+ if example.metadata[:sentry]
26
+ Webhookdb::Sentry.dsn = "http://public:secret@not-really-sentry.nope/someproject"
27
+ Webhookdb::Sentry.run_after_configured_hooks
28
+ end
29
+ end
30
+
31
+ context.after(:each) do |example|
32
+ Webhookdb::Postgres.do_not_defer_events = false if example.metadata[:do_not_defer_events]
33
+ if example.metadata[:slack]
34
+ Webhookdb::Slack.http_client = nil
35
+ Webhookdb::Slack.reset_configuration
36
+ end
37
+ Webhookdb::Sentry.reset_configuration if example.metadata[:sentry]
38
+ Sidekiq::Queues.clear_all if example.metadata[:sidekiq] && Sidekiq::Testing.fake?
39
+ end
40
+ super
41
+ end
42
+
43
+ module_function def job_hash(cls, **more)
44
+ params = {"class" => cls.to_s}
45
+ params.merge!(more.stringify_keys)
46
+ return hash_including(params)
47
+ end
48
+
49
+ RSpec::Matchers.define(:have_queue) do |passed_name|
50
+ match do |sk|
51
+ raise "Sidekiq::Testing must be in fake mode" unless Sidekiq::Testing.fake?
52
+ raise ArgumentError, "argument must be Sidekiq, got #{sk.inspect}" unless sk == Sidekiq
53
+ @name = passed_name || "default"
54
+ q = Sidekiq::Queues[@name]
55
+ if @size
56
+ break true if @size.zero? && q.empty?
57
+ if q.size != @size
58
+ @_err = "has size #{q.size}, expected #{@size}"
59
+ break false
60
+ end
61
+ end
62
+ if q.empty?
63
+ @_err = "is empty"
64
+ break false
65
+ end
66
+ (@matchers || []).each do |m|
67
+ expect(q).to include(m)
68
+ end
69
+ end
70
+
71
+ failure_message do |*|
72
+ msg = "failed to match Sidekiq queue %s:" % @name
73
+ msg += " " + @_err if @_err
74
+ lines = [msg]
75
+ Sidekiq::Queues.jobs_by_queue.each do |n, jobs|
76
+ lines << " #{n}"
77
+ jobs.each do |j|
78
+ lines << " #{j}"
79
+ end
80
+ end
81
+ lines.join("\n")
82
+ end
83
+
84
+ chain :named do |n|
85
+ @name = n
86
+ end
87
+
88
+ chain :including do |*matchers|
89
+ @matchers ||= []
90
+ @matchers.concat(*matchers)
91
+ end
92
+
93
+ chain :consisting_of do |*matchers|
94
+ @matchers = matchers
95
+ @size = matchers.size
96
+ end
97
+
98
+ chain :of_size do |n|
99
+ @size = n
100
+ end
101
+ end
102
+
103
+ RSpec::Matchers.define(:have_empty_queues) do |*|
104
+ match do |sk|
105
+ raise "Sidekiq::Testing must be in fake mode" unless Sidekiq::Testing.fake?
106
+ raise ArgumentError, "argument must be Sidekiq, got #{sk.inspect}" unless sk == Sidekiq
107
+ @nonempty = Sidekiq::Queues.jobs_by_queue.select { |_n, jobs| jobs.size.positive? }
108
+ @nonempty.empty?
109
+ end
110
+
111
+ failure_message do |*|
112
+ lines = ["Sidekiq queues have jobs:"]
113
+ Sidekiq::Queues.jobs_by_queue.each do |n, jobs|
114
+ lines << " #{n}"
115
+ jobs.each do |j|
116
+ lines << " #{j}"
117
+ end
118
+ end
119
+ lines.join("\n")
120
+ end
121
+ end
122
+ end