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,915 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/spec_helpers/whdb"
4
+
5
+ # The basics: these shared examples are among the most commonly used.
6
+
7
+ RSpec.shared_examples "a replicator" do |name|
8
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
9
+ let(:svc) { Webhookdb::Replicator.create(sint) }
10
+ let(:body) { raise NotImplementedError }
11
+ let(:expected_data) { body }
12
+ let(:supports_row_diff) { true }
13
+ let(:expected_row) { nil }
14
+ Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
15
+
16
+ before(:each) do
17
+ sint.organization.prepare_database_connections
18
+ end
19
+
20
+ after(:each) do
21
+ sint.organization.remove_related_database
22
+ end
23
+
24
+ it "knows the expression used to conditionally update" do
25
+ expect(svc._update_where_expr).to be_a(Sequel::SQL::Expression)
26
+ end
27
+
28
+ it "has a timestamp column" do
29
+ expect(svc.timestamp_column).to be_a(Webhookdb::DBAdapter::Column)
30
+ end
31
+
32
+ it "can create its table in its org db" do
33
+ svc.create_table
34
+ svc.readonly_dataset do |ds|
35
+ expect(ds.db).to be_table_exists(svc.qualified_table_sequel_identifier)
36
+ end
37
+ expect(sint.db).to_not be_table_exists(svc.qualified_table_sequel_identifier)
38
+ end
39
+
40
+ it "can insert into its table" do
41
+ svc.create_table
42
+ upsert_webhook(svc, body:)
43
+ svc.readonly_dataset do |ds|
44
+ expect(ds.all).to have_length(1)
45
+ if expected_row
46
+ expect(ds.first).to match(expected_row)
47
+ else
48
+ expect(ds.first[:data]).to eq(expected_data)
49
+ end
50
+ end
51
+ end
52
+
53
+ it "can insert into a custom table when the org has a replication schema set" do
54
+ svc.service_integration.organization.migrate_replication_schema("xyz")
55
+ svc.create_table
56
+ upsert_webhook(svc, body:)
57
+ svc.admin_dataset do |ds|
58
+ expect(ds.all).to have_length(1)
59
+ if expected_row
60
+ expect(ds.first).to match(expected_row)
61
+ else
62
+ expect(ds.first[:data]).to eq(expected_data)
63
+ end
64
+ # this is how a fully qualified table is represented (schema->table, table->column)
65
+ expect(ds.opts[:from].first).to have_attributes(table: "xyz", column: svc.service_integration.table_name.to_sym)
66
+ end
67
+ svc.readonly_dataset do |ds|
68
+ # Need to make sure readonly user has schema privs
69
+ expect(ds.all).to have_length(1)
70
+ end
71
+ end
72
+
73
+ it "emits the rowupsert event if the row has changed", :async, :do_not_defer_events, sidekiq: :fake do
74
+ Webhookdb::Fixtures.webhook_subscription(service_integration: sint).create
75
+ svc.create_table
76
+ upsert_webhook(svc, body:)
77
+ expect(Sidekiq).to have_queue.consisting_of(
78
+ job_hash(
79
+ Webhookdb::Jobs::SendWebhook,
80
+ args: contain_exactly(
81
+ hash_including(
82
+ "id",
83
+ "name" => "webhookdb.serviceintegration.rowupsert",
84
+ "payload" => [
85
+ sint.id,
86
+ hash_including("external_id", "external_id_column", "row" => hash_including("data")),
87
+ ],
88
+ ),
89
+ ),
90
+ ),
91
+ )
92
+ end
93
+
94
+ it "does not emit the rowupsert event if the row has not changed", :async, :do_not_defer_events, sidekiq: :fake do
95
+ if supports_row_diff
96
+ Webhookdb::Fixtures.webhook_subscription(service_integration: sint).create
97
+ expect(Webhookdb::Jobs::SendWebhook).to receive(:perform_async).once
98
+ svc.create_table
99
+ upsert_webhook(svc, body:) # Upsert and make sure the next does not run
100
+ expect do
101
+ upsert_webhook(svc, body:)
102
+ end.to_not publish("webhookdb.serviceintegration.rowupsert")
103
+ expect(Sidekiq).to have_empty_queues
104
+ end
105
+ end
106
+
107
+ it "does not emit the rowupsert event if there are no subscriptions", :async, :do_not_defer_events do
108
+ # No subscription is created so should not publish
109
+ svc.create_table
110
+ expect do
111
+ upsert_webhook(svc, body:)
112
+ end.to_not publish("webhookdb.serviceintegration.rowupsert")
113
+ end
114
+
115
+ it "can serve a webhook response" do
116
+ create_all_dependencies(sint)
117
+ request = fake_request
118
+ whresp = svc.webhook_response(request)
119
+ expect(whresp).to be_a(Webhookdb::WebhookResponse)
120
+ status, headers, body = whresp.to_rack
121
+ expect(status).to be_a(Integer)
122
+ expect(headers).to be_a(Hash)
123
+ expect(headers).to include("Content-Type")
124
+ expect(body).to be_a(String)
125
+ end
126
+
127
+ it "clears setup information" do
128
+ sint.update(webhook_secret: "wh_sek")
129
+ svc.clear_webhook_information
130
+ expect(sint).to have_attributes(webhook_secret: "")
131
+ end
132
+
133
+ it "clears backfill information" do
134
+ sint.update(api_url: "example.api.com", backfill_key: "bf_key", backfill_secret: "bf_sek")
135
+ svc.clear_backfill_information
136
+ expect(sint).to have_attributes(api_url: "")
137
+ expect(sint).to have_attributes(backfill_key: "")
138
+ expect(sint).to have_attributes(backfill_secret: "")
139
+ end
140
+
141
+ # rubocop:disable Lint/RescueException
142
+ def expect_implemented
143
+ # Same as expect { x }.to_not raise_error(NotImplementedError)
144
+ yield
145
+ # No error is good.
146
+ rescue Exception => e
147
+ # Any other error except NotImplementedError is fine.
148
+ # For example we may error verifying credentials; that's fine.
149
+ raise "method is unimplemented" if e.is_a?(NotImplementedError)
150
+ end
151
+ # rubocop:enable Lint/RescueException
152
+
153
+ it "adheres to whether it supports webhooks and backfilling" do
154
+ if svc.descriptor.supports_webhooks_and_backfill?
155
+ expect_implemented { svc.calculate_webhook_state_machine }
156
+ expect_implemented { svc.calculate_backfill_state_machine }
157
+ elsif svc.descriptor.webhooks_only?
158
+ expect_implemented { svc.calculate_webhook_state_machine }
159
+ expect { svc.calculate_backfill_state_machine }.to raise_error(NotImplementedError)
160
+ elsif svc.descriptor.backfill_only?
161
+ expect { svc.calculate_webhook_state_machine }.to raise_error(NotImplementedError)
162
+ expect_implemented { svc.calculate_backfill_state_machine }
163
+ else
164
+ raise TypeError, "invalid ingest behavior"
165
+ end
166
+ end
167
+
168
+ it "implements `on_dependency_webhook_upsert` if dependency is present" do
169
+ if svc.descriptor.dependency_descriptor.present?
170
+ expect_implemented do
171
+ svc.on_dependency_webhook_upsert(svc, {}, changed: true)
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ RSpec.shared_examples "a replicator with dependents" do |service_name, dependent_service_name|
178
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
179
+ let(:svc) { Webhookdb::Replicator.create(sint) }
180
+ let(:body) { raise NotImplementedError }
181
+ let(:expected_insert) { raise NotImplementedError }
182
+ let(:can_track_row_changes) { true }
183
+ Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
184
+
185
+ before(:each) do
186
+ sint.organization.prepare_database_connections
187
+ end
188
+
189
+ after(:each) do
190
+ sint.organization.remove_related_database
191
+ end
192
+
193
+ it "calls on_dependency_webhook_upsert on dependencies with whether the row has changed" do
194
+ svc.create_table
195
+ Webhookdb::Fixtures.service_integration(service_name: dependent_service_name).
196
+ depending_on(svc.service_integration).
197
+ create
198
+
199
+ calls = []
200
+ svc.service_integration.dependents.each do |dep|
201
+ dep_svc = dep.replicator
202
+ expect(dep).to receive(:replicator).at_least(:once).and_return(dep_svc)
203
+ expect(dep_svc).to receive(:on_dependency_webhook_upsert).twice do |inst, payload, changed:|
204
+ calls << 0
205
+ expect(inst).to eq(svc)
206
+ expect(payload).to match(expected_insert)
207
+ if can_track_row_changes
208
+ expect(changed).to(calls.length == 1 ? be_truthy : be_falsey)
209
+ else
210
+ expect(changed).to be_truthy
211
+ end
212
+ end
213
+ end
214
+ upsert_webhook(svc, body:)
215
+ expect(calls).to have_length(1)
216
+ upsert_webhook(svc, body:)
217
+ expect(calls).to have_length(2)
218
+ end
219
+ end
220
+
221
+ RSpec.shared_examples "a replicator dependent on another" do |service_name, dependency_service_name|
222
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name:) }
223
+ let(:svc) { Webhookdb::Replicator.create(sint) }
224
+
225
+ it "can list and describe the replicators used as dependencies" do
226
+ this_descriptor = Webhookdb::Replicator.registered!(service_name)
227
+ dep_descriptor = Webhookdb::Replicator.registered!(dependency_service_name)
228
+ expect(this_descriptor.dependency_descriptor).to eq(dep_descriptor)
229
+ expect(sint.dependency_candidates).to be_empty
230
+ Webhookdb::Fixtures.service_integration(service_name: dependency_service_name).create
231
+ expect(sint.dependency_candidates).to be_empty
232
+ dep = create_dependency(sint)
233
+ expect(sint.dependency_candidates).to contain_exactly(be === dep)
234
+ end
235
+
236
+ it "errors if there are no dependency candidates" do
237
+ step = sint.replicator.send(sint.replicator.preferred_create_state_machine_method)
238
+ expect(step).to have_attributes(
239
+ output: match(no_dependencies_message),
240
+ )
241
+ end
242
+
243
+ it "asks for the dependency as the first step of its state machine" do
244
+ create_dependency(sint)
245
+ sint.depends_on = nil
246
+ step = sint.replicator.send(sint.replicator.preferred_create_state_machine_method)
247
+ expect(step).to have_attributes(
248
+ output: match("Enter the number for the"),
249
+ )
250
+ end
251
+ end
252
+
253
+ RSpec.shared_examples "a replicator that prevents overwriting new data with old" do |name|
254
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
255
+ let(:svc) { Webhookdb::Replicator.create(sint) }
256
+ let(:old_body) { raise NotImplementedError }
257
+ let(:new_body) { raise NotImplementedError }
258
+ let(:expected_old_data) { old_body }
259
+ let(:expected_new_data) { new_body }
260
+ let(:expected_old_row) { nil }
261
+ let(:expected_new_row) { nil }
262
+ Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
263
+
264
+ before(:each) do
265
+ sint.organization.prepare_database_connections
266
+ end
267
+
268
+ after(:each) do
269
+ sint.organization.remove_related_database
270
+ end
271
+
272
+ it "will override older rows with newer ones" do
273
+ svc.create_table
274
+ svc.readonly_dataset do |ds|
275
+ upsert_webhook(svc, body: old_body)
276
+ expect(ds.all).to have_length(1)
277
+ if expected_old_row
278
+ expect(ds.first).to match(expected_old_row)
279
+ else
280
+ expect(ds.first[:data]).to eq(expected_old_data)
281
+ end
282
+
283
+ upsert_webhook(svc, body: new_body)
284
+ expect(ds.all).to have_length(1)
285
+ if expected_new_row
286
+ expect(ds.first).to match(expected_new_row)
287
+ else
288
+ expect(ds.first[:data]).to eq(expected_new_data)
289
+ end
290
+ end
291
+ end
292
+
293
+ it "will not override newer rows with older ones" do
294
+ svc.create_table
295
+
296
+ svc.readonly_dataset do |ds|
297
+ upsert_webhook(svc, body: new_body)
298
+ expect(ds.all).to have_length(1)
299
+ if expected_new_row
300
+ expect(ds.first).to match(expected_new_row)
301
+ else
302
+ expect(ds.first[:data]).to eq(expected_new_data)
303
+ end
304
+
305
+ upsert_webhook(svc, body: old_body)
306
+ expect(ds.all).to have_length(1)
307
+ if expected_new_row
308
+ expect(ds.first).to match(expected_new_row)
309
+ else
310
+ expect(ds.first[:data]).to eq(expected_new_data)
311
+ end
312
+ end
313
+ end
314
+ end
315
+
316
+ RSpec.shared_examples "a replicator that can backfill" do |name|
317
+ let(:api_url) { "https://fake-url.com" }
318
+ let(:sint) do
319
+ Webhookdb::Fixtures.service_integration.create(
320
+ service_name: name,
321
+ backfill_key: "bfkey",
322
+ backfill_secret: "bfsek",
323
+ api_url:,
324
+ )
325
+ end
326
+ let(:svc) { Webhookdb::Replicator.create(sint) }
327
+ let(:backfiller_class) { Webhookdb::Backfiller }
328
+ let(:expected_items_count) { raise NotImplementedError, "how many items do we insert?" }
329
+ let(:has_no_logical_empty_state) { false }
330
+
331
+ def insert_required_data_callback
332
+ # For instances where our custom backfillers use info from rows in the dependency table to make requests.
333
+ # The function should take a replicator of the dependency.
334
+ # Something like: `return ->(dep_svc) { insert_some_info }`
335
+ return ->(*) { return }
336
+ end
337
+
338
+ def stub_service_requests
339
+ raise NotImplementedError, "return stub_request for service"
340
+ end
341
+
342
+ def stub_empty_requests
343
+ raise NotImplementedError, "return stub_request that returns no items (or a response with no key if appropriate)"
344
+ end
345
+
346
+ def stub_service_request_error
347
+ raise NotImplementedError, "return error stub request, usually 4xx"
348
+ end
349
+
350
+ def reset_backfill_credentials
351
+ svc.service_integration.backfill_key = ""
352
+ svc.service_integration.backfill_secret = ""
353
+ end
354
+
355
+ before(:each) do
356
+ sint.organization.prepare_database_connections
357
+ end
358
+
359
+ after(:each) do
360
+ sint.organization.remove_related_database
361
+ end
362
+
363
+ it "upsert records for pages of results and updates the backfill job" do
364
+ create_all_dependencies(sint)
365
+ setup_dependencies(sint, insert_required_data_callback)
366
+ responses = stub_service_requests
367
+ bfjob = backfill(sint)
368
+ expect(responses).to all(have_been_made)
369
+ svc.readonly_dataset { |ds| expect(ds.all).to have_length(expected_items_count) }
370
+ expect(bfjob.refresh).to have_attributes(
371
+ started_at: be_present,
372
+ finished_at: be_present,
373
+ )
374
+ end
375
+
376
+ it "handles empty responses" do
377
+ # APIs fall into two sets: those that return consistent shapes no matter the set of data available,
378
+ # and those that remove keys when data is unavailable (there is maybe a third that uses 'nil' instead of '[]'?).
379
+ # When we have APIs in the second group, we need to test them against missing keys;
380
+ # APIs in the first group can reuse structured responses. That is, we do not need every replicator
381
+ # to work against an empty response shape just because we can.
382
+ next if has_no_logical_empty_state
383
+ create_all_dependencies(sint)
384
+ setup_dependencies(sint, insert_required_data_callback)
385
+ responses = stub_empty_requests
386
+ backfill(sint)
387
+ expect(responses).to all(have_been_made)
388
+ svc.readonly_dataset { |ds| expect(ds.all).to be_empty }
389
+ end
390
+
391
+ it "retries the page fetch" do
392
+ create_all_dependencies(sint)
393
+ setup_dependencies(sint, insert_required_data_callback)
394
+ backfillers = svc._backfillers
395
+ expect(sint).to receive(:replicator).and_return(svc)
396
+ expect(svc).to receive(:_backfillers).and_return(backfillers)
397
+ expect(Webhookdb::Backfiller).to receive(:do_retry_wait).
398
+ exactly(backfillers.size * 2).times # Each backfiller sleeps twice
399
+ # rubocop:disable RSpec/IteratedExpectation
400
+ backfillers.each do |bf|
401
+ expect(bf).to receive(:fetch_backfill_page).and_raise(Webhookdb::Http::BaseError)
402
+ expect(bf).to receive(:fetch_backfill_page).and_raise(Webhookdb::Http::BaseError)
403
+ expect(bf).to receive(:fetch_backfill_page).at_least(:once).and_call_original
404
+ end
405
+ # rubocop:enable RSpec/IteratedExpectation
406
+ responses = stub_service_requests
407
+ backfill(svc)
408
+ expect(responses).to all(have_been_made)
409
+ svc.readonly_dataset { |ds| expect(ds.all).to have_length(expected_items_count) }
410
+ end
411
+
412
+ it "errors if backfill credentials are not present" do
413
+ reset_backfill_credentials
414
+ # `depends_on` is nil because we haven't created dependencies in this test
415
+ expect { backfill(sint) }.to raise_error(Webhookdb::Replicator::CredentialsMissing)
416
+ end
417
+
418
+ it "errors if fetching page errors" do
419
+ create_all_dependencies(sint)
420
+ setup_dependencies(sint, insert_required_data_callback)
421
+ expect(Webhookdb::Backfiller).to receive(:do_retry_wait).twice # Mock out the sleep
422
+ response = stub_service_request_error
423
+ expect { backfill(sint) }.to raise_error(Webhookdb::Http::Error)
424
+ expect(response).to have_been_made.at_least_once
425
+ end
426
+ end
427
+
428
+ # These shared examples test the way a replicator synthesizes and retrieves information from the API.
429
+
430
+ RSpec.shared_examples "a replicator that may have a minimal body" do |name|
431
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
432
+ let(:svc) { Webhookdb::Replicator.create(sint) }
433
+ let(:body) { raise NotImplementedError }
434
+ let(:other_bodies) { [] }
435
+ Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
436
+
437
+ before(:each) do
438
+ sint.organization.prepare_database_connections
439
+ end
440
+
441
+ after(:each) do
442
+ sint.organization.remove_related_database
443
+ end
444
+
445
+ it "can insert minimal examples into its table" do
446
+ svc.create_table
447
+ all_bodies = [body] + other_bodies
448
+ all_bodies.each { |b| upsert_webhook(svc, body: b) }
449
+ svc.readonly_dataset do |ds|
450
+ expect(ds.all).to have_length(all_bodies.length)
451
+ expect(ds.first[:data]).to be_present
452
+ end
453
+ end
454
+ end
455
+
456
+ RSpec.shared_examples "a replicator that deals with resources and wrapped events" do |name|
457
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
458
+ let(:svc) { Webhookdb::Replicator.create(sint) }
459
+ let(:resource_json) { raise NotImplementedError }
460
+ let(:resource_in_envelope_json) { raise NotImplementedError }
461
+ let(:resource_headers) { nil }
462
+ let(:resource_in_envelope_headers) { nil }
463
+ Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
464
+
465
+ before(:each) do
466
+ sint.organization.prepare_database_connections
467
+ end
468
+
469
+ after(:each) do
470
+ sint.organization.remove_related_database
471
+ end
472
+
473
+ it "puts the raw resource in the data column" do
474
+ svc.create_table
475
+ upsert_webhook(svc, body: resource_json, headers: resource_headers)
476
+ svc.readonly_dataset do |ds|
477
+ expect(ds.all).to have_length(1)
478
+ expect(ds.first[:data]).to eq(resource_json)
479
+ end
480
+ end
481
+
482
+ it "puts the enveloped resource in the data column" do
483
+ svc.create_table
484
+ upsert_webhook(svc, body: resource_in_envelope_json, headers: resource_in_envelope_headers)
485
+ svc.readonly_dataset do |ds|
486
+ expect(ds.all).to have_length(1)
487
+ expect(ds.first[:data]).to eq(resource_json)
488
+ end
489
+ end
490
+ end
491
+
492
+ RSpec.shared_examples "a replicator that uses enrichments" do |name, stores_enrichment_column: true|
493
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
494
+ let(:svc) { Webhookdb::Replicator.create(sint) }
495
+ let(:body) { raise NotImplementedError }
496
+ # Needed if stores_enrichment_column is true
497
+ let(:expected_enrichment_data) { raise NotImplementedError }
498
+ Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
499
+
500
+ before(:each) do
501
+ sint.organization.prepare_database_connections
502
+ svc.create_table
503
+ end
504
+
505
+ after(:each) do
506
+ sint.organization.remove_related_database
507
+ end
508
+
509
+ # noinspection RubyUnusedLocalVariable
510
+ def stub_service_request
511
+ raise NotImplementedError,
512
+ "return the stub_request for an enrichment if _fetch_enrichment requires HTTP request, else return nil"
513
+ end
514
+
515
+ def stub_service_request_error
516
+ raise NotImplementedError,
517
+ "return an erroring stub_request for an enrichment " \
518
+ "if _fetch_enrichment requires HTTP request, else return nil"
519
+ end
520
+
521
+ def assert_is_enriched(_row)
522
+ raise NotImplementedError, 'something like: expect(row[:data]["enrichment"]).to eq({"extra" => "abc"})'
523
+ end
524
+
525
+ if stores_enrichment_column
526
+ it "adds enrichment column to main table" do
527
+ req = stub_service_request
528
+ upsert_webhook(svc, body:)
529
+ expect(req).to have_been_made unless req.nil?
530
+ row = svc.readonly_dataset(&:first)
531
+ expect(row[:enrichment]).to eq(expected_enrichment_data)
532
+ end
533
+ end
534
+
535
+ it "can use enriched data when inserting" do
536
+ req = stub_service_request
537
+ upsert_webhook(svc, body:)
538
+ expect(req).to have_been_made unless req.nil?
539
+ row = svc.readonly_dataset(&:first)
540
+ assert_is_enriched(row)
541
+ end
542
+
543
+ it "errors if fetching enrichment errors" do
544
+ req = stub_service_request_error
545
+ unless req.nil?
546
+ expect { upsert_webhook(svc, body:) }.to raise_error(Webhookdb::Http::Error)
547
+ expect(req).to have_been_made
548
+ end
549
+ end
550
+ end
551
+
552
+ RSpec.shared_examples "a replicator that upserts webhooks only under specific conditions" do |name|
553
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
554
+ let(:svc) { Webhookdb::Replicator.create(sint) }
555
+ let(:incorrect_webhook) { raise NotImplementedError }
556
+ Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
557
+
558
+ before(:each) do
559
+ sint.organization.prepare_database_connections
560
+ end
561
+
562
+ after(:each) do
563
+ sint.organization.remove_related_database
564
+ end
565
+
566
+ it "won't insert webhook if resource_and_event returns nil" do
567
+ svc.create_table
568
+ upsert_webhook(svc, body: incorrect_webhook)
569
+ svc.readonly_dataset do |ds|
570
+ expect(ds.all).to have_length(0)
571
+ end
572
+ end
573
+ end
574
+
575
+ # These shared examples can be used to test replicators that support webhooks.
576
+
577
+ RSpec.shared_examples "a webhook validating replicator that uses credentials from a dependency" do |name|
578
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
579
+
580
+ before(:each) do
581
+ create_all_dependencies(sint)
582
+ end
583
+
584
+ def make_request_valid(_req) = raise NotImplementedError
585
+ def make_request_invalid(_req) = raise NotImplementedError
586
+
587
+ it "returns a validated webhook response the request is valid using credentials from the auth integration" do
588
+ request = fake_request
589
+ make_request_valid(request)
590
+ expect(sint.replicator.webhook_response(request)).to have_attributes(status: be >= 200)
591
+ end
592
+
593
+ it "returns an invalid webhook response if the request is is not valid" do
594
+ request = fake_request
595
+ make_request_invalid(request)
596
+ expect(sint.replicator.webhook_response(request)).to have_attributes(status: be_between(400, 499))
597
+ end
598
+ end
599
+
600
+ RSpec.shared_examples "a replicator that processes webhooks synchronously" do |name|
601
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
602
+ let(:svc) { Webhookdb::Replicator.create(sint) }
603
+ let(:expected_synchronous_response) { raise NotImplementedError }
604
+ Webhookdb::SpecHelpers::Whdb.setup_upsert_webhook_example(self)
605
+
606
+ it "is set to process webhooks synchronously" do
607
+ expect(svc).to be_process_webhooks_synchronously
608
+ end
609
+
610
+ it "returns expected response from `synchronous_processing_response`" do
611
+ sint.organization.prepare_database_connections
612
+ svc.create_table
613
+ inserting = upsert_webhook(svc)
614
+ synch_resp = svc.synchronous_processing_response_body(upserted: inserting, request: webhook_request)
615
+ expected = expected_synchronous_response
616
+ expect(expected).to be_a(String)
617
+ expect(synch_resp).to eq(expected)
618
+ end
619
+ end
620
+
621
+ # These shared examples test the intricacies of backfill logic.
622
+
623
+ RSpec.shared_examples "a backfill replicator that requires credentials from a dependency" do |name|
624
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
625
+ let(:error_message) { raise NotImplementedError }
626
+
627
+ before(:each) do
628
+ create_all_dependencies(sint)
629
+ end
630
+
631
+ def strip_auth(_sint)
632
+ raise NotImplementedError
633
+ end
634
+
635
+ it "raises if credentials are not set" do
636
+ strip_auth(sint)
637
+ expect do
638
+ backfill(sint)
639
+ end.to raise_error(Webhookdb::Replicator::CredentialsMissing).with_message(error_message)
640
+ end
641
+ end
642
+
643
+ RSpec.shared_examples "a replicator that can backfill incrementally" do |name|
644
+ let(:last_backfilled) { raise NotImplementedError, "what should the last_backfilled_at value be to start?" }
645
+ let(:api_url) { "https://fake-url.com" }
646
+ let(:sint) do
647
+ Webhookdb::Fixtures.service_integration.create(
648
+ service_name: name,
649
+ backfill_key: "bfkey",
650
+ backfill_secret: "bfsek",
651
+ api_url:,
652
+ last_backfilled_at: last_backfilled,
653
+ )
654
+ end
655
+ let(:svc) { Webhookdb::Replicator.create(sint) }
656
+ let(:expected_new_items_count) { raise NotImplementedError, "how many newer items do we insert?" }
657
+ let(:expected_old_items_count) { raise NotImplementedError, "how many older items do we insert?" }
658
+
659
+ def insert_required_data_callback
660
+ # See backfiller example
661
+ return ->(*) { return }
662
+ end
663
+
664
+ def stub_service_requests(partial:)
665
+ msg = if partial
666
+ "return only the stub_requests called in an incremental situation"
667
+ else
668
+ "return all stub_requests for a full backfill"
669
+ end
670
+ raise NotImplementedError, msg
671
+ end
672
+
673
+ before(:each) do
674
+ sint.organization.prepare_database_connections
675
+ end
676
+
677
+ after(:each) do
678
+ sint.organization.remove_related_database
679
+ end
680
+
681
+ it "upserts records created since last backfill if incremental is true" do
682
+ create_all_dependencies(sint)
683
+ setup_dependencies(sint, insert_required_data_callback)
684
+ responses = stub_service_requests(partial: true)
685
+ backfill(sint, incremental: true)
686
+ expect(responses).to all(have_been_made)
687
+ svc.readonly_dataset { |ds| expect(ds.all).to have_length(expected_new_items_count) }
688
+ end
689
+
690
+ it "upserts all records if incremental is false" do
691
+ create_all_dependencies(sint)
692
+ setup_dependencies(sint, insert_required_data_callback)
693
+ responses = stub_service_requests(partial: false)
694
+ backfill(sint, incremental: false)
695
+ expect(responses).to all(have_been_made)
696
+ svc.readonly_dataset { |ds| expect(ds.all).to have_length(expected_new_items_count + expected_old_items_count) }
697
+ end
698
+
699
+ it "upserts all records if last_backfilled_at == nil" do
700
+ sint.update(last_backfilled_at: nil)
701
+ create_all_dependencies(sint)
702
+ setup_dependencies(sint, insert_required_data_callback)
703
+ responses = stub_service_requests(partial: false)
704
+ backfill(sint)
705
+ expect(responses).to all(have_been_made)
706
+ svc.readonly_dataset { |ds| expect(ds.all).to have_length(expected_new_items_count + expected_old_items_count) }
707
+ end
708
+ end
709
+
710
+ RSpec.shared_examples "a replicator that verifies backfill secrets" do
711
+ let(:correct_creds_sint) { raise NotImplementedError, "what sint should we use to test correct creds?" }
712
+ let(:incorrect_creds_sint) { raise NotImplementedError, "what sint should we use to test incorrect creds?" }
713
+
714
+ def stub_service_request
715
+ raise NotImplementedError, "return stub_request for service"
716
+ end
717
+
718
+ def stub_service_request_error
719
+ raise NotImplementedError, "return 401 error stub request"
720
+ end
721
+
722
+ it "returns a positive result if backfill info is correct" do
723
+ res = stub_service_request
724
+ svc = Webhookdb::Replicator.create(correct_creds_sint)
725
+ result = svc.verify_backfill_credentials
726
+ expect(res).to have_been_made
727
+ expect(result).to have_attributes(verified: true, message: "")
728
+ end
729
+
730
+ it "if backfill info is incorrect for some other reason, return the a negative result and error message" do
731
+ res = stub_service_request_error
732
+ svc = Webhookdb::Replicator.create(incorrect_creds_sint)
733
+ result = svc.verify_backfill_credentials
734
+ expect(res).to have_been_made
735
+ expect(result).to have_attributes(verified: false, message: be_a(String).and(be_present))
736
+ end
737
+
738
+ let(:failed_step_matchers) do
739
+ {output: include("It looks like "), prompt_is_secret: true}
740
+ end
741
+
742
+ it "returns a failed backfill message if the credentials aren't verified when building the state machine" do
743
+ res = stub_service_request_error
744
+ svc = Webhookdb::Replicator.create(incorrect_creds_sint)
745
+ result = svc.calculate_backfill_state_machine
746
+ expect(res).to have_been_made
747
+ expect(result).to have_attributes(needs_input: true, **failed_step_matchers)
748
+ end
749
+ end
750
+
751
+ RSpec.shared_examples "a replicator with a custom backfill not supported message" do |name|
752
+ it "has a custom message" do
753
+ sint = Webhookdb::Fixtures.service_integration.create(service_name: name)
754
+ expect(sint.replicator.backfill_not_supported_message).to_not include("You may be looking for one of the following")
755
+ end
756
+ end
757
+
758
+ RSpec.shared_examples "a backfill replicator that marks missing rows as deleted" do |name|
759
+ let(:deleted_column_name) { raise NotImplementedError }
760
+ let(:api_url) { "https://fake-url.com" }
761
+ let(:sint) do
762
+ Webhookdb::Fixtures.service_integration.create(
763
+ service_name: name,
764
+ backfill_key: "bfkey",
765
+ backfill_secret: "bfsek",
766
+ api_url:,
767
+ )
768
+ end
769
+ let(:svc) { Webhookdb::Replicator.create(sint) }
770
+ let(:undeleted_count_after_first_backfill) { 2 }
771
+ let(:undeleted_count_after_second_backfill) { 1 }
772
+
773
+ def insert_required_data_callback
774
+ # See backfiller example
775
+ return ->(*) { return }
776
+ end
777
+
778
+ def stub_service_requests
779
+ raise NotImplementedError, "return all stub_requests for two backfill calls"
780
+ end
781
+
782
+ before(:each) do
783
+ sint.organization.prepare_database_connections
784
+ create_all_dependencies(sint)
785
+ setup_dependencies(sint, insert_required_data_callback)
786
+ end
787
+
788
+ after(:each) do
789
+ sint.organization.remove_related_database
790
+ end
791
+
792
+ it "marks the deleted timestamp column as deleted" do
793
+ responses = stub_service_requests
794
+ backfill(sint)
795
+ first_backfill_items = svc.readonly_dataset { |ds| ds.where(deleted_column_name => nil).all }
796
+ expect(first_backfill_items).to have_length(undeleted_count_after_first_backfill)
797
+ backfill(sint)
798
+ second_backfill_items = svc.readonly_dataset { |ds| ds.where(deleted_column_name => nil).all }
799
+ expect(second_backfill_items).to have_length(undeleted_count_after_second_backfill)
800
+ expect(responses).to all(have_been_made.twice)
801
+ end
802
+
803
+ it "does not modify the deleted timestamp column once set" do
804
+ responses = stub_service_requests
805
+ backfill(sint)
806
+ ts = Time.parse("1999-04-20T12:00:00Z")
807
+ svc.admin_dataset { |ds| ds.update(deleted_column_name => ts) }
808
+ backfill(sint)
809
+ expect(responses).to all(have_been_made.twice)
810
+ svc.admin_dataset do |ds|
811
+ expect(ds.all).to all(include(deleted_column_name => match_time(ts)))
812
+ end
813
+ end
814
+ end
815
+
816
+ RSpec.shared_examples "a replicator that ignores HTTP errors during backfill" do |name|
817
+ let(:api_url) { "https://fake-url.com" }
818
+ let(:sint) do
819
+ Webhookdb::Fixtures.service_integration.create(
820
+ service_name: name,
821
+ backfill_key: "bfkey",
822
+ backfill_secret: "bfsek",
823
+ api_url:,
824
+ )
825
+ end
826
+ let(:svc) { Webhookdb::Replicator.create(sint) }
827
+ let(:backfiller_class) { Webhookdb::Backfiller }
828
+
829
+ def insert_required_data_callback
830
+ # For instances where our custom backfillers use info from rows in the dependency table to make requests.
831
+ # The function should take the chain of replicator dependencies.
832
+ # Something like: `return ->(direct_dep_replicator, grandparent_dep_replicator) { insert_some_info }`
833
+ return ->(*) { return }
834
+ end
835
+
836
+ def stub_error_requests
837
+ raise NotImplementedError, "return request stubs for all ignored error responses"
838
+ end
839
+
840
+ before(:each) do
841
+ sint.organization.prepare_database_connections
842
+ end
843
+
844
+ after(:each) do
845
+ sint.organization.remove_related_database
846
+ end
847
+
848
+ it "does not error for any of the configured responses" do
849
+ allow(Webhookdb::Backfiller).to receive(:do_retry_wait).at_least(:once)
850
+ create_all_dependencies(sint)
851
+ setup_dependencies(sint, insert_required_data_callback)
852
+ responses = stub_error_requests
853
+ Array.new(responses.size) { backfill(sint) }
854
+ expect(responses).to all(have_been_made.at_least_times(1))
855
+ svc.readonly_dataset { |ds| expect(ds.all).to be_empty }
856
+ end
857
+ end
858
+
859
+ RSpec.shared_examples "a replicator backfilling against the table of its dependency" do |name|
860
+ let(:sint) { Webhookdb::Fixtures.service_integration.create(service_name: name) }
861
+ let(:svc) { Webhookdb::Replicator.create(sint) }
862
+ let(:dep_svc) { @dep_svc }
863
+ let(:external_id_col) { raise NotImplementedError }
864
+
865
+ before(:each) do
866
+ sint.organization.prepare_database_connections
867
+ create_all_dependencies(sint)
868
+ @dep_svc = setup_dependencies(sint).first
869
+ end
870
+
871
+ after(:each) do
872
+ sint.organization.remove_related_database
873
+ end
874
+
875
+ def create_dependency_row(_external_id, _timestamp)
876
+ raise NotImplementedError, "upsert a row"
877
+ end
878
+
879
+ it "upserts records created since last backfill if incremental is true" do
880
+ dep_svc.admin_dataset do |ds|
881
+ ds.insert(create_dependency_row("dep1", 1.hours.ago))
882
+ ds.insert(create_dependency_row("dep2", 2.hours.ago))
883
+ ds.insert(create_dependency_row("dep3", 3.hours.ago))
884
+ end
885
+ sint.update(last_backfilled_at: 2.5.hours.ago)
886
+ backfill(sint, incremental: true)
887
+ expect(svc.readonly_dataset(&:all)).to contain_exactly(
888
+ include(external_id_col => "dep1"),
889
+ include(external_id_col => "dep2"),
890
+ # dep3 is too old so wasn't seen
891
+ )
892
+ end
893
+
894
+ it "upserts all records if incremental is false" do
895
+ dep_svc.admin_dataset do |ds|
896
+ ds.insert(create_dependency_row("dep1", 1.hours.ago))
897
+ ds.insert(create_dependency_row("dep2", 2.hours.ago))
898
+ ds.insert(create_dependency_row("dep3", 3.hours.ago))
899
+ end
900
+ sint.update(last_backfilled_at: 2.5.hours.ago)
901
+ backfill(sint, incremental: false)
902
+ expect(svc.readonly_dataset(&:all)).to have_length(3)
903
+ end
904
+
905
+ it "upserts all records if last_backfilled_at is nil" do
906
+ dep_svc.admin_dataset do |ds|
907
+ ds.insert(create_dependency_row("dep1", 1.hours.ago))
908
+ ds.insert(create_dependency_row("dep2", 2.hours.ago))
909
+ ds.insert(create_dependency_row("dep3", 3.hours.ago))
910
+ end
911
+ sint.update(last_backfilled_at: nil)
912
+ backfill(sint, incremental: true)
913
+ expect(svc.readonly_dataset(&:all)).to have_length(3)
914
+ end
915
+ end