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.
- checksums.yaml +7 -0
- data/data/messages/layouts/blank.email.liquid +10 -0
- data/data/messages/layouts/minimal.email.liquid +28 -0
- data/data/messages/layouts/standard.email.liquid +28 -0
- data/data/messages/partials/button.liquid +15 -0
- data/data/messages/partials/environment_banner.liquid +9 -0
- data/data/messages/partials/footer.liquid +22 -0
- data/data/messages/partials/greeting.liquid +3 -0
- data/data/messages/partials/logo_header.liquid +18 -0
- data/data/messages/partials/signoff.liquid +1 -0
- data/data/messages/styles/v1.liquid +346 -0
- data/data/messages/templates/errors/icalendar_fetch.email.liquid +29 -0
- data/data/messages/templates/invite.email.liquid +15 -0
- data/data/messages/templates/new_customer.email.liquid +24 -0
- data/data/messages/templates/org_database_migration_finished.email.liquid +7 -0
- data/data/messages/templates/org_database_migration_started.email.liquid +9 -0
- data/data/messages/templates/specs/_field_partial.liquid +1 -0
- data/data/messages/templates/specs/basic.email.liquid +2 -0
- data/data/messages/templates/specs/basic.fake.liquid +1 -0
- data/data/messages/templates/specs/with_field.email.liquid +2 -0
- data/data/messages/templates/specs/with_field.fake.liquid +1 -0
- data/data/messages/templates/specs/with_include.email.liquid +2 -0
- data/data/messages/templates/specs/with_partial.email.liquid +1 -0
- data/data/messages/templates/verification.email.liquid +14 -0
- data/data/messages/templates/verification.sms.liquid +1 -0
- data/data/messages/web/install-customer-login.liquid +48 -0
- data/data/messages/web/install-error.liquid +17 -0
- data/data/messages/web/install-success.liquid +35 -0
- data/data/messages/web/install.liquid +20 -0
- data/data/messages/web/partials/footer.liquid +4 -0
- data/data/messages/web/partials/form_error.liquid +1 -0
- data/data/messages/web/partials/header.liquid +3 -0
- data/data/messages/web/styles.liquid +134 -0
- data/data/windows_tz.txt +461 -0
- data/db/migrations/001_testing_pixies.rb +13 -0
- data/db/migrations/002_initial.rb +132 -0
- data/db/migrations/003_ux_overhaul.rb +20 -0
- data/db/migrations/004_incremental_backfill.rb +9 -0
- data/db/migrations/005_log_webhooks.rb +24 -0
- data/db/migrations/006_generalize_roles.rb +29 -0
- data/db/migrations/007_org_dns.rb +12 -0
- data/db/migrations/008_webhook_subscriptions.rb +19 -0
- data/db/migrations/009_nonunique_stripe_subscription_customer.rb +16 -0
- data/db/migrations/010_drop_integration_soft_delete.rb +14 -0
- data/db/migrations/011_webhook_subscriptions_created_at.rb +10 -0
- data/db/migrations/012_webhook_subscriptions_created_by.rb +9 -0
- data/db/migrations/013_default_org_membership.rb +30 -0
- data/db/migrations/014_webhook_subscription_deliveries.rb +26 -0
- data/db/migrations/015_dependent_integrations.rb +9 -0
- data/db/migrations/016_encrypted_columns.rb +9 -0
- data/db/migrations/017_skip_verification.rb +9 -0
- data/db/migrations/018_sync_targets.rb +25 -0
- data/db/migrations/019_org_schema.rb +9 -0
- data/db/migrations/020_org_database_migrations.rb +25 -0
- data/db/migrations/021_no_default_org_schema.rb +14 -0
- data/db/migrations/022_database_document.rb +15 -0
- data/db/migrations/023_sync_target_schema.rb +9 -0
- data/db/migrations/024_org_semaphore_jobs.rb +9 -0
- data/db/migrations/025_integration_backfill_cursor.rb +9 -0
- data/db/migrations/026_undo_integration_backfill_cursor.rb +9 -0
- data/db/migrations/027_sync_target_http_sync.rb +12 -0
- data/db/migrations/028_logged_webhook_path.rb +24 -0
- data/db/migrations/029_encrypt_columns.rb +97 -0
- data/db/migrations/030_org_sync_target_timeout.rb +9 -0
- data/db/migrations/031_org_max_query_rows.rb +9 -0
- data/db/migrations/032_remove_db_defaults.rb +12 -0
- data/db/migrations/033_backfill_jobs.rb +26 -0
- data/db/migrations/034_backfill_job_criteria.rb +9 -0
- data/db/migrations/035_synchronous_backfill.rb +9 -0
- data/db/migrations/036_oauth.rb +26 -0
- data/db/migrations/037_oauth_used.rb +9 -0
- data/lib/amigo/durable_job.rb +416 -0
- data/lib/pry/clipboard.rb +111 -0
- data/lib/sequel/advisory_lock.rb +65 -0
- data/lib/webhookdb/admin.rb +4 -0
- data/lib/webhookdb/admin_api/auth.rb +36 -0
- data/lib/webhookdb/admin_api/customers.rb +63 -0
- data/lib/webhookdb/admin_api/database_documents.rb +20 -0
- data/lib/webhookdb/admin_api/entities.rb +66 -0
- data/lib/webhookdb/admin_api/message_deliveries.rb +61 -0
- data/lib/webhookdb/admin_api/roles.rb +15 -0
- data/lib/webhookdb/admin_api.rb +34 -0
- data/lib/webhookdb/aggregate_result.rb +63 -0
- data/lib/webhookdb/api/auth.rb +122 -0
- data/lib/webhookdb/api/connstr_auth.rb +36 -0
- data/lib/webhookdb/api/db.rb +188 -0
- data/lib/webhookdb/api/demo.rb +14 -0
- data/lib/webhookdb/api/entities.rb +198 -0
- data/lib/webhookdb/api/helpers.rb +253 -0
- data/lib/webhookdb/api/install.rb +296 -0
- data/lib/webhookdb/api/me.rb +53 -0
- data/lib/webhookdb/api/organizations.rb +254 -0
- data/lib/webhookdb/api/replay.rb +64 -0
- data/lib/webhookdb/api/service_integrations.rb +402 -0
- data/lib/webhookdb/api/services.rb +27 -0
- data/lib/webhookdb/api/stripe.rb +22 -0
- data/lib/webhookdb/api/subscriptions.rb +67 -0
- data/lib/webhookdb/api/sync_targets.rb +232 -0
- data/lib/webhookdb/api/system.rb +37 -0
- data/lib/webhookdb/api/webhook_subscriptions.rb +96 -0
- data/lib/webhookdb/api.rb +92 -0
- data/lib/webhookdb/apps.rb +93 -0
- data/lib/webhookdb/async/audit_logger.rb +38 -0
- data/lib/webhookdb/async/autoscaler.rb +84 -0
- data/lib/webhookdb/async/job.rb +18 -0
- data/lib/webhookdb/async/job_logger.rb +45 -0
- data/lib/webhookdb/async/scheduled_job.rb +18 -0
- data/lib/webhookdb/async.rb +142 -0
- data/lib/webhookdb/aws.rb +98 -0
- data/lib/webhookdb/backfill_job.rb +107 -0
- data/lib/webhookdb/backfiller.rb +107 -0
- data/lib/webhookdb/cloudflare.rb +39 -0
- data/lib/webhookdb/connection_cache.rb +177 -0
- data/lib/webhookdb/console.rb +71 -0
- data/lib/webhookdb/convertkit.rb +14 -0
- data/lib/webhookdb/crypto.rb +66 -0
- data/lib/webhookdb/customer/reset_code.rb +94 -0
- data/lib/webhookdb/customer.rb +347 -0
- data/lib/webhookdb/database_document.rb +72 -0
- data/lib/webhookdb/db_adapter/column_types.rb +37 -0
- data/lib/webhookdb/db_adapter/default_sql.rb +187 -0
- data/lib/webhookdb/db_adapter/pg.rb +96 -0
- data/lib/webhookdb/db_adapter/snowflake.rb +137 -0
- data/lib/webhookdb/db_adapter.rb +208 -0
- data/lib/webhookdb/dbutil.rb +92 -0
- data/lib/webhookdb/demo_mode.rb +100 -0
- data/lib/webhookdb/developer_alert.rb +51 -0
- data/lib/webhookdb/email_octopus.rb +21 -0
- data/lib/webhookdb/enumerable.rb +18 -0
- data/lib/webhookdb/fixtures/backfill_jobs.rb +72 -0
- data/lib/webhookdb/fixtures/customers.rb +65 -0
- data/lib/webhookdb/fixtures/database_documents.rb +27 -0
- data/lib/webhookdb/fixtures/faker.rb +41 -0
- data/lib/webhookdb/fixtures/logged_webhooks.rb +56 -0
- data/lib/webhookdb/fixtures/message_deliveries.rb +59 -0
- data/lib/webhookdb/fixtures/oauth_sessions.rb +24 -0
- data/lib/webhookdb/fixtures/organization_database_migrations.rb +37 -0
- data/lib/webhookdb/fixtures/organization_memberships.rb +54 -0
- data/lib/webhookdb/fixtures/organizations.rb +32 -0
- data/lib/webhookdb/fixtures/reset_codes.rb +23 -0
- data/lib/webhookdb/fixtures/service_integrations.rb +42 -0
- data/lib/webhookdb/fixtures/subscriptions.rb +33 -0
- data/lib/webhookdb/fixtures/sync_targets.rb +32 -0
- data/lib/webhookdb/fixtures/webhook_subscriptions.rb +35 -0
- data/lib/webhookdb/fixtures.rb +15 -0
- data/lib/webhookdb/formatting.rb +56 -0
- data/lib/webhookdb/front.rb +49 -0
- data/lib/webhookdb/github.rb +22 -0
- data/lib/webhookdb/google_calendar.rb +29 -0
- data/lib/webhookdb/heroku.rb +21 -0
- data/lib/webhookdb/http.rb +114 -0
- data/lib/webhookdb/icalendar.rb +17 -0
- data/lib/webhookdb/id.rb +17 -0
- data/lib/webhookdb/idempotency.rb +90 -0
- data/lib/webhookdb/increase.rb +42 -0
- data/lib/webhookdb/intercom.rb +23 -0
- data/lib/webhookdb/jobs/amigo_test_jobs.rb +118 -0
- data/lib/webhookdb/jobs/backfill.rb +32 -0
- data/lib/webhookdb/jobs/create_mirror_table.rb +18 -0
- data/lib/webhookdb/jobs/create_stripe_customer.rb +17 -0
- data/lib/webhookdb/jobs/customer_created_notify_internal.rb +22 -0
- data/lib/webhookdb/jobs/demo_mode_sync_data.rb +19 -0
- data/lib/webhookdb/jobs/deprecated_jobs.rb +19 -0
- data/lib/webhookdb/jobs/developer_alert_handle.rb +14 -0
- data/lib/webhookdb/jobs/durable_job_recheck_poller.rb +17 -0
- data/lib/webhookdb/jobs/emailer.rb +15 -0
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +25 -0
- data/lib/webhookdb/jobs/icalendar_sync.rb +23 -0
- data/lib/webhookdb/jobs/logged_webhook_replay.rb +17 -0
- data/lib/webhookdb/jobs/logged_webhook_resilient_replay.rb +15 -0
- data/lib/webhookdb/jobs/message_dispatched.rb +16 -0
- data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +21 -0
- data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +21 -0
- data/lib/webhookdb/jobs/organization_database_migration_run.rb +24 -0
- data/lib/webhookdb/jobs/prepare_database_connections.rb +22 -0
- data/lib/webhookdb/jobs/process_webhook.rb +47 -0
- data/lib/webhookdb/jobs/renew_watch_channel.rb +24 -0
- data/lib/webhookdb/jobs/replication_migration.rb +24 -0
- data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +23 -0
- data/lib/webhookdb/jobs/scheduled_backfills.rb +77 -0
- data/lib/webhookdb/jobs/send_invite.rb +15 -0
- data/lib/webhookdb/jobs/send_test_webhook.rb +25 -0
- data/lib/webhookdb/jobs/send_webhook.rb +20 -0
- data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +16 -0
- data/lib/webhookdb/jobs/sync_target_run_sync.rb +38 -0
- data/lib/webhookdb/jobs/trim_logged_webhooks.rb +15 -0
- data/lib/webhookdb/jobs/webhook_resource_notify_integrations.rb +30 -0
- data/lib/webhookdb/jobs/webhook_subscription_delivery_attempt.rb +29 -0
- data/lib/webhookdb/jobs.rb +4 -0
- data/lib/webhookdb/json.rb +113 -0
- data/lib/webhookdb/liquid/expose.rb +27 -0
- data/lib/webhookdb/liquid/filters.rb +16 -0
- data/lib/webhookdb/liquid/liquification.rb +26 -0
- data/lib/webhookdb/liquid/partial.rb +12 -0
- data/lib/webhookdb/logged_webhook/resilient.rb +95 -0
- data/lib/webhookdb/logged_webhook.rb +194 -0
- data/lib/webhookdb/message/body.rb +25 -0
- data/lib/webhookdb/message/delivery.rb +127 -0
- data/lib/webhookdb/message/email_transport.rb +133 -0
- data/lib/webhookdb/message/fake_transport.rb +54 -0
- data/lib/webhookdb/message/liquid_drops.rb +29 -0
- data/lib/webhookdb/message/template.rb +89 -0
- data/lib/webhookdb/message/transport.rb +43 -0
- data/lib/webhookdb/message.rb +150 -0
- data/lib/webhookdb/messages/error_icalendar_fetch.rb +42 -0
- data/lib/webhookdb/messages/invite.rb +23 -0
- data/lib/webhookdb/messages/new_customer.rb +14 -0
- data/lib/webhookdb/messages/org_database_migration_finished.rb +23 -0
- data/lib/webhookdb/messages/org_database_migration_started.rb +24 -0
- data/lib/webhookdb/messages/specs.rb +57 -0
- data/lib/webhookdb/messages/verification.rb +23 -0
- data/lib/webhookdb/method_utilities.rb +82 -0
- data/lib/webhookdb/microsoft_calendar.rb +36 -0
- data/lib/webhookdb/nextpax.rb +14 -0
- data/lib/webhookdb/oauth/front.rb +58 -0
- data/lib/webhookdb/oauth/intercom.rb +58 -0
- data/lib/webhookdb/oauth/session.rb +24 -0
- data/lib/webhookdb/oauth.rb +80 -0
- data/lib/webhookdb/organization/alerting.rb +35 -0
- data/lib/webhookdb/organization/database_migration.rb +151 -0
- data/lib/webhookdb/organization/db_builder.rb +429 -0
- data/lib/webhookdb/organization.rb +506 -0
- data/lib/webhookdb/organization_membership.rb +58 -0
- data/lib/webhookdb/phone_number.rb +38 -0
- data/lib/webhookdb/plaid.rb +23 -0
- data/lib/webhookdb/platform.rb +27 -0
- data/lib/webhookdb/plivo.rb +52 -0
- data/lib/webhookdb/postgres/maintenance.rb +166 -0
- data/lib/webhookdb/postgres/model.rb +82 -0
- data/lib/webhookdb/postgres/model_utilities.rb +382 -0
- data/lib/webhookdb/postgres/testing_pixie.rb +16 -0
- data/lib/webhookdb/postgres/validations.rb +46 -0
- data/lib/webhookdb/postgres.rb +176 -0
- data/lib/webhookdb/postmark.rb +20 -0
- data/lib/webhookdb/redis.rb +35 -0
- data/lib/webhookdb/replicator/atom_single_feed_v1.rb +116 -0
- data/lib/webhookdb/replicator/aws_pricing_v1.rb +488 -0
- data/lib/webhookdb/replicator/base.rb +1185 -0
- data/lib/webhookdb/replicator/column.rb +482 -0
- data/lib/webhookdb/replicator/convertkit_broadcast_v1.rb +69 -0
- data/lib/webhookdb/replicator/convertkit_subscriber_v1.rb +200 -0
- data/lib/webhookdb/replicator/convertkit_tag_v1.rb +66 -0
- data/lib/webhookdb/replicator/convertkit_v1_mixin.rb +65 -0
- data/lib/webhookdb/replicator/docgen.rb +167 -0
- data/lib/webhookdb/replicator/email_octopus_campaign_v1.rb +84 -0
- data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +159 -0
- data/lib/webhookdb/replicator/email_octopus_event_v1.rb +244 -0
- data/lib/webhookdb/replicator/email_octopus_list_v1.rb +101 -0
- data/lib/webhookdb/replicator/fake.rb +453 -0
- data/lib/webhookdb/replicator/front_conversation_v1.rb +45 -0
- data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +55 -0
- data/lib/webhookdb/replicator/front_message_v1.rb +45 -0
- data/lib/webhookdb/replicator/front_v1_mixin.rb +22 -0
- data/lib/webhookdb/replicator/github_issue_comment_v1.rb +58 -0
- data/lib/webhookdb/replicator/github_issue_v1.rb +83 -0
- data/lib/webhookdb/replicator/github_pull_v1.rb +84 -0
- data/lib/webhookdb/replicator/github_release_v1.rb +47 -0
- data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +250 -0
- data/lib/webhookdb/replicator/github_repository_event_v1.rb +45 -0
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +465 -0
- data/lib/webhookdb/replicator/icalendar_event_v1.rb +334 -0
- data/lib/webhookdb/replicator/increase_account_number_v1.rb +77 -0
- data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +61 -0
- data/lib/webhookdb/replicator/increase_account_v1.rb +63 -0
- data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +78 -0
- data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +64 -0
- data/lib/webhookdb/replicator/increase_limit_v1.rb +78 -0
- data/lib/webhookdb/replicator/increase_transaction_v1.rb +74 -0
- data/lib/webhookdb/replicator/increase_v1_mixin.rb +121 -0
- data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +61 -0
- data/lib/webhookdb/replicator/intercom_contact_v1.rb +36 -0
- data/lib/webhookdb/replicator/intercom_conversation_v1.rb +38 -0
- data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +69 -0
- data/lib/webhookdb/replicator/intercom_v1_mixin.rb +105 -0
- data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +65 -0
- data/lib/webhookdb/replicator/plivo_sms_inbound_v1.rb +102 -0
- data/lib/webhookdb/replicator/postmark_inbound_message_v1.rb +94 -0
- data/lib/webhookdb/replicator/postmark_outbound_message_event_v1.rb +107 -0
- data/lib/webhookdb/replicator/schema_modification.rb +42 -0
- data/lib/webhookdb/replicator/shopify_customer_v1.rb +58 -0
- data/lib/webhookdb/replicator/shopify_order_v1.rb +64 -0
- data/lib/webhookdb/replicator/shopify_v1_mixin.rb +161 -0
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +169 -0
- data/lib/webhookdb/replicator/sponsy_customer_v1.rb +54 -0
- data/lib/webhookdb/replicator/sponsy_placement_v1.rb +34 -0
- data/lib/webhookdb/replicator/sponsy_publication_v1.rb +125 -0
- data/lib/webhookdb/replicator/sponsy_slot_v1.rb +41 -0
- data/lib/webhookdb/replicator/sponsy_status_v1.rb +35 -0
- data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +165 -0
- data/lib/webhookdb/replicator/state_machine_step.rb +69 -0
- data/lib/webhookdb/replicator/stripe_charge_v1.rb +77 -0
- data/lib/webhookdb/replicator/stripe_coupon_v1.rb +62 -0
- data/lib/webhookdb/replicator/stripe_customer_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_dispute_v1.rb +77 -0
- data/lib/webhookdb/replicator/stripe_invoice_item_v1.rb +82 -0
- data/lib/webhookdb/replicator/stripe_invoice_v1.rb +116 -0
- data/lib/webhookdb/replicator/stripe_payout_v1.rb +67 -0
- data/lib/webhookdb/replicator/stripe_price_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_product_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_refund_v1.rb +101 -0
- data/lib/webhookdb/replicator/stripe_subscription_item_v1.rb +56 -0
- data/lib/webhookdb/replicator/stripe_subscription_v1.rb +75 -0
- data/lib/webhookdb/replicator/stripe_v1_mixin.rb +116 -0
- data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +141 -0
- data/lib/webhookdb/replicator/transistor_episode_v1.rb +169 -0
- data/lib/webhookdb/replicator/transistor_show_v1.rb +68 -0
- data/lib/webhookdb/replicator/transistor_v1_mixin.rb +65 -0
- data/lib/webhookdb/replicator/twilio_sms_v1.rb +156 -0
- data/lib/webhookdb/replicator/webhook_request.rb +5 -0
- data/lib/webhookdb/replicator/webhookdb_customer_v1.rb +74 -0
- data/lib/webhookdb/replicator.rb +224 -0
- data/lib/webhookdb/role.rb +42 -0
- data/lib/webhookdb/sentry.rb +35 -0
- data/lib/webhookdb/service/auth.rb +138 -0
- data/lib/webhookdb/service/collection.rb +91 -0
- data/lib/webhookdb/service/entities.rb +97 -0
- data/lib/webhookdb/service/helpers.rb +270 -0
- data/lib/webhookdb/service/middleware.rb +124 -0
- data/lib/webhookdb/service/types.rb +30 -0
- data/lib/webhookdb/service/validators.rb +32 -0
- data/lib/webhookdb/service/view_api.rb +63 -0
- data/lib/webhookdb/service.rb +219 -0
- data/lib/webhookdb/service_integration.rb +332 -0
- data/lib/webhookdb/shopify.rb +35 -0
- data/lib/webhookdb/signalwire.rb +13 -0
- data/lib/webhookdb/slack.rb +68 -0
- data/lib/webhookdb/snowflake.rb +90 -0
- data/lib/webhookdb/spec_helpers/async.rb +122 -0
- data/lib/webhookdb/spec_helpers/citest.rb +88 -0
- data/lib/webhookdb/spec_helpers/integration.rb +121 -0
- data/lib/webhookdb/spec_helpers/message.rb +41 -0
- data/lib/webhookdb/spec_helpers/postgres.rb +220 -0
- data/lib/webhookdb/spec_helpers/service.rb +432 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +56 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +915 -0
- data/lib/webhookdb/spec_helpers/whdb.rb +139 -0
- data/lib/webhookdb/spec_helpers.rb +63 -0
- data/lib/webhookdb/sponsy.rb +14 -0
- data/lib/webhookdb/stripe.rb +37 -0
- data/lib/webhookdb/subscription.rb +203 -0
- data/lib/webhookdb/sync_target.rb +491 -0
- data/lib/webhookdb/tasks/admin.rb +49 -0
- data/lib/webhookdb/tasks/annotate.rb +36 -0
- data/lib/webhookdb/tasks/db.rb +82 -0
- data/lib/webhookdb/tasks/docs.rb +42 -0
- data/lib/webhookdb/tasks/fixture.rb +35 -0
- data/lib/webhookdb/tasks/message.rb +50 -0
- data/lib/webhookdb/tasks/regress.rb +87 -0
- data/lib/webhookdb/tasks/release.rb +27 -0
- data/lib/webhookdb/tasks/sidekiq.rb +23 -0
- data/lib/webhookdb/tasks/specs.rb +64 -0
- data/lib/webhookdb/theranest.rb +15 -0
- data/lib/webhookdb/transistor.rb +13 -0
- data/lib/webhookdb/twilio.rb +13 -0
- data/lib/webhookdb/typed_struct.rb +44 -0
- data/lib/webhookdb/version.rb +5 -0
- data/lib/webhookdb/webhook_response.rb +50 -0
- data/lib/webhookdb/webhook_subscription/delivery.rb +82 -0
- data/lib/webhookdb/webhook_subscription.rb +226 -0
- data/lib/webhookdb/windows_tz.rb +32 -0
- data/lib/webhookdb/xml.rb +92 -0
- data/lib/webhookdb.rb +224 -0
- data/lib/webterm/apps.rb +45 -0
- 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
|