From 08fac996ffc807fcff521e9d8b1c888eef8666ba Mon Sep 17 00:00:00 2001 From: willymwai Date: Fri, 20 Jun 2025 12:07:01 +0300 Subject: [PATCH] feat: add webhook_secret handling and validations for FlutterwaveProvider --- .../payment_providers/flutterwave_provider.rb | 4 +- spec/factories/payment_providers.rb | 5 +- .../flutterwave/handle_event_job_spec.rb | 8 +- .../flutterwave_provider_spec.rb | 109 ++++++++++++++++++ .../handle_incoming_webhook_service_spec.rb | 4 +- 5 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 spec/models/payment_providers/flutterwave_provider_spec.rb diff --git a/app/models/payment_providers/flutterwave_provider.rb b/app/models/payment_providers/flutterwave_provider.rb index 9b4a8044038..5fa734936ad 100644 --- a/app/models/payment_providers/flutterwave_provider.rb +++ b/app/models/payment_providers/flutterwave_provider.rb @@ -10,12 +10,10 @@ class FlutterwaveProvider < BaseProvider PROCESSING_STATUSES = %w[pending].freeze SUCCESS_STATUSES = %w[successful].freeze FAILED_STATUSES = %w[failed cancelled].freeze - validates :secret_key, presence: true validates :success_redirect_url, url: true, allow_nil: true, length: {maximum: 1024} - secrets_accessors :secret_key - settings_accessors :webhook_secret + secrets_accessors :secret_key, :webhook_secret before_create :generate_webhook_secret diff --git a/spec/factories/payment_providers.rb b/spec/factories/payment_providers.rb index 5e7d31de6cb..cffd0a0aa2a 100644 --- a/spec/factories/payment_providers.rb +++ b/spec/factories/payment_providers.rb @@ -106,13 +106,12 @@ type { "PaymentProviders::FlutterwaveProvider" } name { "Flutterwave" } code { "flutterwave_#{SecureRandom.uuid}" } - secrets do - {secret_key:}.to_json + {secret_key:, webhook_secret:}.to_json end settings do - {success_redirect_url:, webhook_secret:} + {success_redirect_url:} end transient do diff --git a/spec/jobs/payment_providers/flutterwave/handle_event_job_spec.rb b/spec/jobs/payment_providers/flutterwave/handle_event_job_spec.rb index 5fbb6e99f69..6626463ea83 100644 --- a/spec/jobs/payment_providers/flutterwave/handle_event_job_spec.rb +++ b/spec/jobs/payment_providers/flutterwave/handle_event_job_spec.rb @@ -27,7 +27,9 @@ after { ENV.delete("SIDEKIQ_PAYMENTS") } it "uses the payments queue" do - expect(described_class.queue_name).to eq("payments") + expect { + described_class.perform_later(organization:, event: "test") + }.to have_enqueued_job.on_queue("payments") end end @@ -35,7 +37,9 @@ before { ENV.delete("SIDEKIQ_PAYMENTS") } it "uses the providers queue" do - expect(described_class.queue_name).to eq("providers") + expect { + described_class.perform_later(organization:, event: "test") + }.to have_enqueued_job.on_queue("providers") end end end diff --git a/spec/models/payment_providers/flutterwave_provider_spec.rb b/spec/models/payment_providers/flutterwave_provider_spec.rb new file mode 100644 index 00000000000..a7b84890eb7 --- /dev/null +++ b/spec/models/payment_providers/flutterwave_provider_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe PaymentProviders::FlutterwaveProvider, type: :model do + subject(:flutterwave_provider) { build(:flutterwave_provider) } + + describe "validations" do + it { is_expected.to validate_presence_of(:secret_key) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to allow_value(nil).for(:success_redirect_url) } + it { is_expected.to allow_value("https://example.com/success").for(:success_redirect_url) } + it { is_expected.not_to allow_value("invalid-url").for(:success_redirect_url) } + it { is_expected.not_to allow_value("a" * 1025).for(:success_redirect_url) } + + it "validates uniqueness of the code" do + expect(flutterwave_provider).to validate_uniqueness_of(:code).scoped_to(:organization_id) + end + end + + describe "constants" do + it "defines success redirect URL" do + expect(described_class::SUCCESS_REDIRECT_URL).to eq("https://www.flutterwave.com/ng") + end + + it "defines API URL" do + expect(described_class::API_URL).to eq("https://api.flutterwave.com/v3") + end + + it "defines processing statuses" do + expect(described_class::PROCESSING_STATUSES).to eq(%w[pending]) + end + + it "defines success statuses" do + expect(described_class::SUCCESS_STATUSES).to eq(%w[successful]) + end + + it "defines failed statuses" do + expect(described_class::FAILED_STATUSES).to eq(%w[failed cancelled]) + end + end + + describe "#payment_type" do + it "returns flutterwave" do + expect(flutterwave_provider.payment_type).to eq("flutterwave") + end + end + + describe "#api_url" do + it "returns the API URL" do + expect(flutterwave_provider.api_url).to eq("https://api.flutterwave.com/v3") + end + end + + describe "webhook_secret generation" do + context "when creating a new provider" do + it "generates a webhook secret" do + provider = create(:flutterwave_provider) + expect(provider.webhook_secret).to be_present + expect(provider.webhook_secret.length).to eq(64) + end + + it "generates different secrets for different providers" do + provider1 = create(:flutterwave_provider) + provider2 = create(:flutterwave_provider) + expect(provider1.webhook_secret).not_to eq(provider2.webhook_secret) + end + + it "does not override existing webhook secret" do + existing_secret = "existing_secret" + provider = described_class.new( + organization: create(:organization), + name: "Test Provider", + code: "test_provider", + secret_key: "test_key", + webhook_secret: existing_secret + ) + provider.save! + expect(provider.reload.webhook_secret).to eq(existing_secret) + end + end + end + + describe "FlutterwavePayment" do + it "defines a data structure for payments" do + payment = described_class::FlutterwavePayment.new( + id: "12345", + status: "successful", + metadata: {amount: 1000} + ) + + expect(payment.id).to eq("12345") + expect(payment.status).to eq("successful") + expect(payment.metadata).to eq({amount: 1000}) + end + end + + describe "secrets accessors" do + it "provides access to secret_key through secrets" do + provider = create(:flutterwave_provider, secret_key: "test_secret_key") + expect(provider.secret_key).to eq("test_secret_key") + end + + it "provides access to webhook_secret through secrets" do + provider = create(:flutterwave_provider) + expect(provider.webhook_secret).to be_present + end + end +end diff --git a/spec/services/payment_providers/flutterwave/handle_incoming_webhook_service_spec.rb b/spec/services/payment_providers/flutterwave/handle_incoming_webhook_service_spec.rb index 6c45791671f..26ca5df96ac 100644 --- a/spec/services/payment_providers/flutterwave/handle_incoming_webhook_service_spec.rb +++ b/spec/services/payment_providers/flutterwave/handle_incoming_webhook_service_spec.rb @@ -63,7 +63,9 @@ let(:secret) { nil } before do - flutterwave_provider.update!(settings: flutterwave_provider.settings.merge("webhook_secret" => nil)) + secrets = JSON.parse(flutterwave_provider.secrets || "{}") + secrets.delete("webhook_secret") + flutterwave_provider.update!(secrets: secrets.to_json) end it "returns service failure" do