webhookdb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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