From a1688724ada88a4f6b636f10464918f79cd1f950 Mon Sep 17 00:00:00 2001 From: Andrew Tongen Date: Fri, 10 Jul 2026 17:23:25 -0500 Subject: [PATCH] Repo updates --- .github/workflows/ci.yml | 35 +++++++ .rubocop.yml | 33 +++++-- .rubocop_todo.yml | 98 +------------------ .travis.yml | 7 -- CHANGELOG.md | 35 ++++++- Gemfile | 17 ++-- README.md | 3 +- drip-ruby.gemspec | 8 +- flake.lock | 61 ++++++++++++ flake.nix | 32 ++++++ lib/drip/client.rb | 6 +- lib/drip/client/accounts.rb | 4 +- lib/drip/client/broadcasts.rb | 4 +- lib/drip/client/campaign_subscriptions.rb | 4 +- lib/drip/client/campaigns.rb | 10 +- lib/drip/client/conversions.rb | 4 +- lib/drip/client/forms.rb | 4 +- lib/drip/client/http_client.rb | 42 +++++--- lib/drip/client/orders.rb | 2 +- lib/drip/client/shopper_activity.rb | 14 ++- lib/drip/client/subscribers.rb | 9 +- lib/drip/client/webhooks.rb | 6 +- lib/drip/client/workflow_triggers.rb | 8 +- lib/drip/client/workflows.rb | 10 +- lib/drip/collection.rb | 4 +- lib/drip/resource.rb | 12 +-- lib/drip/resources/tag.rb | 5 - lib/drip/response.rb | 39 ++++---- lib/drip/version.rb | 2 +- test/drip/client/accounts_test.rb | 20 +++- test/drip/client/broadcasts_test.rb | 4 +- .../client/campaign_subscriptions_test.rb | 2 +- test/drip/client/campaigns_test.rb | 10 +- test/drip/client/configuration_test.rb | 2 +- test/drip/client/conversions_test.rb | 4 +- test/drip/client/custom_fields_test.rb | 2 +- test/drip/client/events_test.rb | 2 +- test/drip/client/forms_test.rb | 4 +- test/drip/client/http_client_test.rb | 34 ++++++- test/drip/client/orders_test.rb | 2 +- test/drip/client/shopper_activity_test.rb | 2 +- test/drip/client/subscribers_test.rb | 41 +++++++- test/drip/client/tags_test.rb | 2 +- test/drip/client/users_test.rb | 2 +- test/drip/client/webhooks_test.rb | 2 +- test/drip/client/workflow_triggers_test.rb | 4 +- test/drip/client/workflows_test.rb | 20 +++- test/drip/client_test.rb | 10 +- test/drip/collection_test.rb | 2 +- test/drip/collections/account_test.rb | 2 +- test/drip/collections/broadcasts_test.rb | 2 +- .../campaign_subscriptions_test.rb | 2 +- test/drip/collections/campaigns_test.rb | 2 +- test/drip/collections/errors_test.rb | 2 +- test/drip/collections/orders_test.rb | 2 +- test/drip/collections/purchases_test.rb | 2 +- test/drip/collections/tags_test.rb | 2 +- test/drip/collections/webhooks_test.rb | 2 +- .../collections/workflow_triggers_test.rb | 2 +- test/drip/collections/workflows_test.rb | 2 +- test/drip/collections_test.rb | 2 +- test/drip/request_test.rb | 2 +- test/drip/resource_test.rb | 2 +- test/drip/resources/account_test.rb | 2 +- test/drip/resources/order_test.rb | 2 +- test/drip/resources/subscriber_test.rb | 2 +- test/drip/resources/tag_test.rb | 2 +- test/drip/resources_test.rb | 2 +- test/drip/response_test.rb | 2 +- test/test_helper.rb | 4 +- 70 files changed, 476 insertions(+), 257 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..57a18fe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: ["3.1", "3.2", "3.3", "3.4"] + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - run: bundle exec rake test + + # Runs the suite through the same flake.nix shell used for local development, + # so a broken dev environment fails CI instead of surfacing only when someone + # next runs `nix develop`. No Nix cache action is wired up here: this flake + # doesn't build anything from source, so every package is already served by + # the public cache.nixos.org substituter and there's nothing to gain from one. + nix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: DeterminateSystems/determinate-nix-action@v3 + - run: nix develop --command bundle install + - run: nix develop --command bundle exec rake test + - run: nix develop --command bundle exec rubocop diff --git a/.rubocop.yml b/.rubocop.yml index 10e7036..3d7de94 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,11 +1,15 @@ +plugins: + - rubocop-performance + inherit_from: .rubocop_todo.yml AllCops: - TargetRubyVersion: '2.3' + TargetRubyVersion: '3.1' + NewCops: enable Exclude: - 'test/fixtures/**/*' -Layout/AlignParameters: +Layout/ParameterAlignment: EnforcedStyle: with_fixed_indentation Layout/ClosingParenthesisIndentation: @@ -14,15 +18,32 @@ Layout/ClosingParenthesisIndentation: Layout/DotPosition: EnforcedStyle: trailing -Style/BracesAroundHashParameters: - Enabled: false - Layout/AccessModifierIndentation: EnforcedStyle: outdent -Metrics/LineLength: +Layout/LineLength: Enabled: false +Style/StringLiterals: + Enabled: false + +# This codebase intentionally uses the compact `class Drip::Client::Foo` +# style throughout instead of nested `module`/`class` blocks. +Style/ClassAndModuleChildren: + Enabled: false + +# Methods are documented with "Public:"-style comments above each method +# rather than class/module-level documentation. +Style/Documentation: + Enabled: false + +# Data-literal methods that just enumerate known classes aren't meaningfully +# simplified by splitting them up. +Metrics/MethodLength: + Exclude: + - 'lib/drip/resources.rb' + - 'lib/drip/collections.rb' + Layout/MultilineOperationIndentation: EnforcedStyle: 'indented' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 93346e1..c8f5734 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,103 +1,7 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2019-05-02 16:45:21 -0500 using RuboCop version 0.67.2. +# using RuboCop version 1.88.2. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. - -# Offense count: 2 -# Cop supports --auto-correct. -Layout/EmptyLineAfterGuardClause: - Exclude: - - 'lib/drip/client/subscribers.rb' - - 'lib/drip/response.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. -Lint/UnusedMethodArgument: - Exclude: - - 'lib/drip/client.rb' - - 'lib/drip/collection.rb' - -# Offense count: 2 -Metrics/AbcSize: - Max: 25 - -# Offense count: 8 -# Configuration parameters: CountComments, ExcludedMethods. -# ExcludedMethods: refine -Metrics/BlockLength: - Max: 46 - -# Offense count: 4 -# Configuration parameters: CountComments, ExcludedMethods. -Metrics/MethodLength: - Max: 26 - -# Offense count: 1 -Metrics/PerceivedComplexity: - Max: 8 - -# Offense count: 4 -# Cop supports --auto-correct. -Performance/InefficientHashSearch: - Exclude: - - 'lib/drip/resource.rb' - - 'lib/drip/response.rb' - -# Offense count: 24 -# Cop supports --auto-correct. -# Configuration parameters: AutoCorrect, EnforcedStyle. -# SupportedStyles: nested, compact -Style/ClassAndModuleChildren: - Enabled: false - -# Offense count: 44 -Style/Documentation: - Enabled: false - -# Offense count: 1 -# Cop supports --auto-correct. -Style/Encoding: - Exclude: - - 'drip-ruby.gemspec' - -# Offense count: 2 -# Cop supports --auto-correct. -Style/ExpandPathArguments: - Exclude: - - 'drip-ruby.gemspec' - - 'test/test_helper.rb' - -# Offense count: 1 -# Configuration parameters: MinBodyLength. -Style/GuardClause: - Exclude: - - 'lib/drip/client.rb' - -# Offense count: 2 -# Cop supports --auto-correct. -Style/IfUnlessModifier: - Exclude: - - 'lib/drip/client.rb' - -# Offense count: 2 -Style/MissingRespondToMissing: - Exclude: - - 'lib/drip/resource.rb' - - 'lib/drip/response.rb' - -# Offense count: 12 -# Cop supports --auto-correct. -# Configuration parameters: Strict. -Style/NumericLiterals: - MinDigits: 8 - -# Offense count: 700 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiterals: - Enabled: false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a882f59..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: ruby -sudo: false -rvm: - - 2.7 - - 2.6 - - 2.5 - - jruby-19mode diff --git a/CHANGELOG.md b/CHANGELOG.md index 9301883..81cb252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,43 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] -[master]: https://github.com/DripEmail/drip-ruby/compare/v3.4.3...HEAD +[main]: https://github.com/DripEmail/drip-ruby/compare/v3.5.0...HEAD - Your contribution here! +## [3.5.0] - 2026-07-10 + +[3.5.0]: https://github.com/DripEmail/drip-ruby/compare/v3.4.3...v3.5.0 + +### Security +- `Drip::Client::HTTPClient` no longer forwards the `Authorization`/Basic-auth + credentials when an HTTP redirect points to a different host than the one + originally requested. +- Path segments and query values built from caller-supplied ids (`id`, + `campaign_id`, `workflow_id`, `subscriber_id`, etc.) are now consistently + URL-escaped, matching the existing behavior for emails and tags. + +### Fixed +- `Drip::Client::Orders#create_or_update_order` no longer merges the `email` + option under a symbol key alongside a possible string `"email"` key, which + produced a hash with a duplicate JSON key and a deprecation warning under + recent `json` gem versions. + +### Changed +- Raised `required_ruby_version` to `>= 3.1` and bumped development + dependencies (minitest, mocha, rake, rubocop, rubocop-performance, + shoulda-context, simplecov, webmock) to current versions. +- Replaced Travis CI with a GitHub Actions workflow that runs the test suite + across Ruby 3.1-3.4 (via `ruby/setup-ruby`) plus a job that runs the suite + and rubocop through the project's `flake.nix` shell (via + `DeterminateSystems/determinate-nix-action`), so the documented dev + environment is itself covered by CI. +- Upgraded rubocop from 0.67.2 to 1.x, resolved the accumulated + `.rubocop_todo.yml` offenses, and fixed the underlying code where + reasonable rather than just re-suppressing them. +- Added a `flake.nix` development shell (ruby, bundler, libyaml, pkg-config) + for a reproducible local dev environment. + ## [3.4.3] - 2023-02-15 [3.4.3]: https://github.com/DripEmail/drip-ruby/compare/v3.4.2...v3.4.3 diff --git a/Gemfile b/Gemfile index 75232d0..56937be 100644 --- a/Gemfile +++ b/Gemfile @@ -5,12 +5,13 @@ source 'https://rubygems.org' # Specify your gem's dependencies in drip-ruby.gemspec gemspec -gem "bundler", ">= 1.6", "< 3" +gem "base64" +gem "bundler", ">= 2.0", "< 5" gem "minitest", "~> 5.0" -gem "mocha", "~> 1.1" -gem "rake", "~> 12.0" -gem "rubocop", "~> 0.67.2" -gem "rubocop-performance", "~> 1.1.0" -gem "shoulda-context", "~> 1.0" -gem "simplecov", require: false -gem "webmock", "~> 3.4" +gem "mocha", "~> 2.1" +gem "rake", "~> 13.0" +gem "rubocop", "~> 1.81" +gem "rubocop-performance", "~> 1.26" +gem "shoulda-context", "~> 1.2" +gem "simplecov", "~> 0.22", require: false +gem "webmock", "~> 3.24" diff --git a/README.md b/README.md index 94ddfe6..b660979 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ A Ruby toolkit for the [Drip](https://www.getdrip.com/) API. -[![Build Status](https://travis-ci.org/DripEmail/drip-ruby.svg?branch=master)](https://travis-ci.org/DripEmail/drip-ruby) -[![Code Climate](https://codeclimate.com/github/DripEmail/drip-ruby/badges/gpa.svg)](https://codeclimate.com/github/DripEmail/drip-ruby) +[![CI](https://github.com/DripEmail/drip-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/DripEmail/drip-ruby/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/drip-ruby.svg)](https://badge.fury.io/rb/drip-ruby) diff --git a/drip-ruby.gemspec b/drip-ruby.gemspec index d3bad61..26450ce 100644 --- a/drip-ruby.gemspec +++ b/drip-ruby.gemspec @@ -1,7 +1,6 @@ -# coding: utf-8 # frozen_string_literal: true -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'drip/version' @@ -17,8 +16,9 @@ Gem::Specification.new do |spec| spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } - spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] - spec.required_ruby_version = '>= 2.3' + spec.required_ruby_version = '>= 3.1' + + spec.metadata['rubygems_mfa_required'] = 'true' end diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5f60947 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1783522502, + "narHash": "sha256-iffAls3iaNTyJC2faYcUXSI+Gp02cDjYl+MygxKl2GI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0bb7ec54c8483066ec9d7720e780a5caa71f8612", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..6593936 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + description = "Development environment for drip-ruby"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShell { + packages = [ + pkgs.ruby_3_3 + pkgs.bundler + pkgs.libyaml + pkgs.pkg-config + ]; + + shellHook = '' + export GEM_HOME="$PWD/.bundle/gems" + export BUNDLE_PATH="$GEM_HOME" + export PATH="$GEM_HOME/bin:$PATH" + echo "drip-ruby dev shell ($(ruby -v))" + echo "Run 'bundle install' then 'bundle exec rake test' to get started." + ''; + }; + }); +} diff --git a/lib/drip/client.rb b/lib/drip/client.rb index 695249b..85c900f 100644 --- a/lib/drip/client.rb +++ b/lib/drip/client.rb @@ -49,7 +49,7 @@ class Client @config.public_send(config_key) end - setter_name = "#{config_key}=".to_sym + setter_name = :"#{config_key}=" define_method(setter_name) do |val| warn "[DEPRECATED] Setting configuration on Drip::Client after initialization will be removed in a future version" @config.public_send(setter_name, val) @@ -77,7 +77,7 @@ def content_type JSON_API_CONTENT_TYPE end - Drip::Request::VERB_CLASS_MAPPING.keys.each do |verb| + Drip::Request::VERB_CLASS_MAPPING.each_key do |verb| define_method(verb) do |path, options = {}| warn "[DEPRECATED] Drip::Client##{verb} please use the API endpoint specific methods" make_json_api_request(verb, "v2/#{path}", options) @@ -109,7 +109,7 @@ def make_request(drip_request) end end - def build_response(&block) + def build_response(&) response = yield Drip::Response.new(response.code.to_i, response.body || response.body == "" ? JSON.parse(response.body) : nil) rescue JSON::ParserError diff --git a/lib/drip/client/accounts.rb b/lib/drip/client/accounts.rb index 5a41a80..5cec3dc 100644 --- a/lib/drip/client/accounts.rb +++ b/lib/drip/client/accounts.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "cgi" + module Drip class Client module Accounts @@ -18,7 +20,7 @@ def accounts # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#accounts def account(id) - make_json_api_request :get, "v2/accounts/#{id}" + make_json_api_request :get, "v2/accounts/#{CGI.escape id.to_s}" end end end diff --git a/lib/drip/client/broadcasts.rb b/lib/drip/client/broadcasts.rb index 8b270b3..93b09b5 100644 --- a/lib/drip/client/broadcasts.rb +++ b/lib/drip/client/broadcasts.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "cgi" + module Drip class Client module Broadcasts @@ -24,7 +26,7 @@ def broadcasts(options = {}) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#broadcasts def broadcast(id) - make_json_api_request :get, "v2/#{account_id}/broadcasts/#{id}" + make_json_api_request :get, "v2/#{account_id}/broadcasts/#{CGI.escape id.to_s}" end end end diff --git a/lib/drip/client/campaign_subscriptions.rb b/lib/drip/client/campaign_subscriptions.rb index 101f05c..131a2af 100644 --- a/lib/drip/client/campaign_subscriptions.rb +++ b/lib/drip/client/campaign_subscriptions.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "cgi" + module Drip class Client module CampaignSubscriptions @@ -10,7 +12,7 @@ module CampaignSubscriptions # Returns a Drip::Response. # See https://www.getdrip.com/docs.rest-api#campaign_subscriptions def campaign_subscriptions(subscriber_id) - make_json_api_request :get, "v2/#{account_id}/subscribers/#{subscriber_id}/campaign_subscriptions" + make_json_api_request :get, "v2/#{account_id}/subscribers/#{CGI.escape subscriber_id.to_s}/campaign_subscriptions" end end end diff --git a/lib/drip/client/campaigns.rb b/lib/drip/client/campaigns.rb index f3bb77c..2c304be 100644 --- a/lib/drip/client/campaigns.rb +++ b/lib/drip/client/campaigns.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "cgi" + module Drip class Client module Campaigns @@ -26,7 +28,7 @@ def campaigns(options = {}) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#campaigns def campaign(id) - make_json_api_request :get, "v2/#{account_id}/campaigns/#{id}" + make_json_api_request :get, "v2/#{account_id}/campaigns/#{CGI.escape id.to_s}" end # Public: Activate a campaign. @@ -36,7 +38,7 @@ def campaign(id) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#campaigns def activate_campaign(id) - make_json_api_request :post, "v2/#{account_id}/campaigns/#{id}/activate" + make_json_api_request :post, "v2/#{account_id}/campaigns/#{CGI.escape id.to_s}/activate" end # Public: Pause a campaign. @@ -46,7 +48,7 @@ def activate_campaign(id) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#campaigns def pause_campaign(id) - make_json_api_request :post, "v2/#{account_id}/campaigns/#{id}/pause" + make_json_api_request :post, "v2/#{account_id}/campaigns/#{CGI.escape id.to_s}/pause" end # Public: List everyone subscribed to a campaign. @@ -70,7 +72,7 @@ def pause_campaign(id) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#campaigns def campaign_subscribers(id, options = {}) - make_json_api_request :get, "v2/#{account_id}/campaigns/#{id}/subscribers", options + make_json_api_request :get, "v2/#{account_id}/campaigns/#{CGI.escape id.to_s}/subscribers", options end end end diff --git a/lib/drip/client/conversions.rb b/lib/drip/client/conversions.rb index 1cdff2b..ad12eb9 100644 --- a/lib/drip/client/conversions.rb +++ b/lib/drip/client/conversions.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "cgi" + module Drip class Client module Conversions @@ -22,7 +24,7 @@ def conversions(options = {}) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#conversions def conversion(id) - make_json_api_request :get, "v2/#{account_id}/goals/#{id}" + make_json_api_request :get, "v2/#{account_id}/goals/#{CGI.escape id.to_s}" end end end diff --git a/lib/drip/client/forms.rb b/lib/drip/client/forms.rb index e86cb30..034b65b 100644 --- a/lib/drip/client/forms.rb +++ b/lib/drip/client/forms.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "cgi" + module Drip class Client module Forms @@ -18,7 +20,7 @@ def forms # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#forms def form(id) - make_json_api_request :get, "v2/#{account_id}/forms/#{id}" + make_json_api_request :get, "v2/#{account_id}/forms/#{CGI.escape id.to_s}" end end end diff --git a/lib/drip/client/http_client.rb b/lib/drip/client/http_client.rb index 5675b31..9df8b22 100644 --- a/lib/drip/client/http_client.rb +++ b/lib/drip/client/http_client.rb @@ -13,41 +13,59 @@ def initialize(config) def make_request(drip_request, redirected_url: nil, step: 0) raise TooManyRedirectsError, 'too many HTTP redirects' if step >= REDIRECT_LIMIT - uri = redirected_url || drip_request.url.tap do |orig_url| + uri = redirected_url || request_uri(drip_request) + response = perform_request(drip_request, uri) + + return make_request(drip_request, redirected_url: URI(response["Location"]), step: step + 1) if response.is_a?(Net::HTTPRedirection) + + response + end + + private + + def request_uri(drip_request) + drip_request.url.tap do |orig_url| next if drip_request.http_verb != :get orig_url.query = URI.encode_www_form(drip_request.options) end + end - response = Net::HTTP.start(uri.host, uri.port, connection_options(uri.scheme)) do |http| + def perform_request(drip_request, uri) + Net::HTTP.start(uri.host, uri.port, connection_options(uri.scheme)) do |http| request = drip_request.verb_klass.new uri request.body = drip_request.body - add_standard_headers(request) + add_standard_headers(request, authenticated: same_host?(uri, drip_request.url)) request['Content-Type'] = drip_request.content_type http.request request end - - return make_request(drip_request, redirected_url: URI(response["Location"]), step: step + 1) if response.is_a?(Net::HTTPRedirection) - - response end - private + # Only send credentials to the host the request was originally made to. + # A redirect to a different host must not be able to exfiltrate the API + # key/access token. + def same_host?(uri, original_url) + uri.host == original_url.host + end - def add_standard_headers(request) + def add_standard_headers(request, authenticated:) request['User-Agent'] = "Drip Ruby v#{Drip::VERSION}" request['Accept'] = "*/*" + add_authentication(request) if authenticated + + request['Skip-Analytics'] = true if @config.skip_analytics + request + end + + def add_authentication(request) if @config.access_token request['Authorization'] = "Bearer #{@config.access_token}" else request.basic_auth @config.api_key, "" end - - request['Skip-Analytics'] = true if @config.skip_analytics - request end def connection_options(uri_scheme) diff --git a/lib/drip/client/orders.rb b/lib/drip/client/orders.rb index d78cda0..85f98b3 100644 --- a/lib/drip/client/orders.rb +++ b/lib/drip/client/orders.rb @@ -12,7 +12,7 @@ module Orders # Returns a Drip::Response. # See https://developer.drip.com/#orders def create_or_update_order(email, options = {}) - data = options.merge(email: email) + data = options.merge("email" => email) make_json_api_request :post, "v2/#{account_id}/orders", private_generate_resource("orders", data) end diff --git a/lib/drip/client/shopper_activity.rb b/lib/drip/client/shopper_activity.rb index db183c0..b98ddf6 100644 --- a/lib/drip/client/shopper_activity.rb +++ b/lib/drip/client/shopper_activity.rb @@ -11,7 +11,7 @@ module ShopperActivity # Returns a Drip::Response. # See https://developer.drip.com/#cart-activity def create_cart_activity_event(data = {}) - raise ArgumentError, 'email:, person_id:, or :visitor_uuid parameter required' if !data.key?(:email) && !data.key?(:person_id) && !data.key?(:visitor_uuid) + raise ArgumentError, 'email:, person_id:, or :visitor_uuid parameter required' if missing_cart_identifier?(data) %i[provider action cart_id cart_url].each do |key| raise ArgumentError, "#{key}: parameter required" unless data.key?(key) @@ -47,10 +47,12 @@ def create_order_activity_event(data = {}) # Returns a Drip::Response. # See https://developer.drip.com/#create-or-update-a-batch-of-orders def create_order_activity_events(records = []) + required_keys = %i[provider action order_id] + records.each_with_index do |record, i| raise ArgumentError, "email: or person_id: parameter required in record #{i}" if !record.key?(:email) && !record.key?(:person_id) - %i[provider action order_id].each do |key| + required_keys.each do |key| raise ArgumentError, "#{key}: parameter required in record #{i}" unless record.key?(key) end @@ -85,12 +87,18 @@ def create_product_activity_event(data = {}) # See https://developer.drip.com/#checkout-activity def create_checkout_activity_event(data = {}) %i[provider action checkout_id].each do |key| - raise ArgumentError, "#{key}: parameter required" if !data.key?(key) + raise ArgumentError, "#{key}: parameter required" unless data.key?(key) end data[:occurred_at] = Time.now.iso8601 unless data.key?(:occurred_at) make_json_request :post, "v3/#{account_id}/shopper_activity/checkout", data end end + + private + + def missing_cart_identifier?(data) + !data.key?(:email) && !data.key?(:person_id) && !data.key?(:visitor_uuid) + end end end diff --git a/lib/drip/client/subscribers.rb b/lib/drip/client/subscribers.rb index e1b48f8..c608506 100644 --- a/lib/drip/client/subscribers.rb +++ b/lib/drip/client/subscribers.rb @@ -57,7 +57,8 @@ def create_or_update_subscriber(*args) data = {} data[:email] = args[0] if args[0].is_a? String data.merge!(args.last) if args.last.is_a? Hash - raise ArgumentError, 'email: or id: or bigcommerce_subscriber_id: parameter required' if missing_subscriber_identifier(data) + raise ArgumentError, 'email: or id: or bigcommerce_subscriber_id: parameter required' if missing_subscriber_identifier?(data) + make_json_api_request :post, "v2/#{account_id}/subscribers", private_generate_resource("subscribers", data) end @@ -104,7 +105,7 @@ def unsubscribe_subscribers(subscribers) # See https://www.getdrip.com/docs/rest-api#unsubscribe def unsubscribe(id_or_email, options = {}) url = "v2/#{account_id}/subscribers/#{CGI.escape id_or_email}/remove" - url += options[:campaign_id] ? "?campaign_id=#{options[:campaign_id]}" : "" + url += options[:campaign_id] ? "?campaign_id=#{CGI.escape options[:campaign_id].to_s}" : "" make_json_api_request :post, url end @@ -133,7 +134,7 @@ def unsubscribe(id_or_email, options = {}) # See https://www.getdrip.com/docs/rest-api#subscribe def subscribe(email, campaign_id, options = {}) data = options.merge("email" => email) - url = "v2/#{account_id}/campaigns/#{campaign_id}/subscribers" + url = "v2/#{account_id}/campaigns/#{CGI.escape campaign_id.to_s}/subscribers" make_json_api_request :post, url, private_generate_resource("subscribers", data) end @@ -160,7 +161,7 @@ def unsubscribe_from_all(id_or_email) private - def missing_subscriber_identifier(data) + def missing_subscriber_identifier?(data) external_ids = data[:external_ids] || {} !data.key?(:email) && !data.key?(:id) && !external_ids.key?("bigcommerce_subscriber_id") end diff --git a/lib/drip/client/webhooks.rb b/lib/drip/client/webhooks.rb index 7225859..c5577b9 100644 --- a/lib/drip/client/webhooks.rb +++ b/lib/drip/client/webhooks.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "cgi" + module Drip class Client module Webhooks @@ -17,7 +19,7 @@ def webhooks # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#webhooks def webhook(id) - make_json_api_request :get, "v2/#{account_id}/webhooks/#{id}" + make_json_api_request :get, "v2/#{account_id}/webhooks/#{CGI.escape id.to_s}" end # Public: Create a webhook. @@ -54,7 +56,7 @@ def create_webhook(post_url, include_received_email, events) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#webhooks def delete_webhook(id) - make_json_api_request :delete, "v2/#{account_id}/webhooks/#{id}" + make_json_api_request :delete, "v2/#{account_id}/webhooks/#{CGI.escape id.to_s}" end end end diff --git a/lib/drip/client/workflow_triggers.rb b/lib/drip/client/workflow_triggers.rb index 4883746..8bae44f 100644 --- a/lib/drip/client/workflow_triggers.rb +++ b/lib/drip/client/workflow_triggers.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "cgi" + module Drip class Client module WorkflowTriggers @@ -9,7 +11,7 @@ module WorkflowTriggers # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#workflow_triggers def workflow_triggers(id) - make_json_api_request :get, "v2/#{account_id}/workflows/#{id}/triggers" + make_json_api_request :get, "v2/#{account_id}/workflows/#{CGI.escape id.to_s}/triggers" end # Public: Create a workflow trigger. @@ -24,7 +26,7 @@ def workflow_triggers(id) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#workflows def create_workflow_trigger(id, options = {}) - make_json_api_request :post, "v2/#{account_id}/workflows/#{id}/triggers", private_generate_resource("triggers", options) + make_json_api_request :post, "v2/#{account_id}/workflows/#{CGI.escape id.to_s}/triggers", private_generate_resource("triggers", options) end # Public: Update a workflow trigger. @@ -39,7 +41,7 @@ def create_workflow_trigger(id, options = {}) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#workflows def update_workflow_trigger(id, options = {}) - make_json_api_request :put, "v2/#{account_id}/workflows/#{id}/triggers", private_generate_resource("triggers", options) + make_json_api_request :put, "v2/#{account_id}/workflows/#{CGI.escape id.to_s}/triggers", private_generate_resource("triggers", options) end end end diff --git a/lib/drip/client/workflows.rb b/lib/drip/client/workflows.rb index a507d3f..fa1a514 100644 --- a/lib/drip/client/workflows.rb +++ b/lib/drip/client/workflows.rb @@ -23,7 +23,7 @@ def workflows(options = {}) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#workflows def workflow(id) - make_json_api_request :get, "v2/#{account_id}/workflows/#{id}" + make_json_api_request :get, "v2/#{account_id}/workflows/#{CGI.escape id.to_s}" end # Public: Activate a workflow. @@ -32,7 +32,7 @@ def workflow(id) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#workflows def activate_workflow(id) - make_json_api_request :post, "v2/#{account_id}/workflows/#{id}/activate" + make_json_api_request :post, "v2/#{account_id}/workflows/#{CGI.escape id.to_s}/activate" end # Public: Pause a workflow. @@ -41,7 +41,7 @@ def activate_workflow(id) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#workflows def pause_workflow(id) - make_json_api_request :post, "v2/#{account_id}/workflows/#{id}/pause" + make_json_api_request :post, "v2/#{account_id}/workflows/#{CGI.escape id.to_s}/pause" end # Public: Start someone on a workflow. @@ -65,7 +65,7 @@ def pause_workflow(id) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#workflows def start_subscriber_workflow(id, options = {}) - make_json_api_request :post, "v2/#{account_id}/workflows/#{id}/subscribers", private_generate_resource("subscribers", options) + make_json_api_request :post, "v2/#{account_id}/workflows/#{CGI.escape id.to_s}/subscribers", private_generate_resource("subscribers", options) end # Public: Remove someone from a workflow. @@ -75,7 +75,7 @@ def start_subscriber_workflow(id, options = {}) # Returns a Drip::Response. # See https://www.getdrip.com/docs/rest-api#workflows def remove_subscriber_workflow(workflow_id, id_or_email) - make_json_api_request :delete, "v2/#{account_id}/workflows/#{workflow_id}/subscribers/#{CGI.escape id_or_email}" + make_json_api_request :delete, "v2/#{account_id}/workflows/#{CGI.escape workflow_id.to_s}/subscribers/#{CGI.escape id_or_email}" end end end diff --git a/lib/drip/collection.rb b/lib/drip/collection.rb index 8239633..0bf33c4 100644 --- a/lib/drip/collection.rb +++ b/lib/drip/collection.rb @@ -34,8 +34,8 @@ def singular? items.length < 2 end - def each(&block) - items.each { |item| yield(item) } + def each(&) + items.each(&) end end end diff --git a/lib/drip/resource.rb b/lib/drip/resource.rb index cc8027b..874e3d9 100644 --- a/lib/drip/resource.rb +++ b/lib/drip/resource.rb @@ -19,26 +19,26 @@ def singular? true end - def respond_to?(method_name, include_private = false) - attributes.keys.include?(method_name.to_s) || super + def respond_to_missing?(method_name, include_private = false) + attributes.key?(method_name.to_s) || super end - def method_missing(method_name, *args, &block) - attributes.keys.include?(method_name.to_s) ? attributes[method_name.to_s] : super + def method_missing(method_name, *args, &) + attributes.key?(method_name.to_s) ? attributes[method_name.to_s] : super end private def process(attributes) {}.tap do |attrs| - attributes.keys.each do |key| + attributes.each_key do |key| attrs[key] = process_attribute(key, attributes[key]) end end end def process_attribute(key, raw_value) - if key.to_s =~ /_at$/ # auto-coerce times + if key.to_s.match?(/_at$/) # auto-coerce times raw_value ? Time.parse(raw_value) : nil else raw_value diff --git a/lib/drip/resources/tag.rb b/lib/drip/resources/tag.rb index 28e0c3a..c20e215 100644 --- a/lib/drip/resources/tag.rb +++ b/lib/drip/resources/tag.rb @@ -7,10 +7,5 @@ class Tag < Resource def self.resource_name "tag" end - - def initialize(raw_data = {}) - @raw_attributes = raw_data.dup.freeze - @attributes = @raw_attributes - end end end diff --git a/lib/drip/response.rb b/lib/drip/response.rb index 74229fd..c091155 100644 --- a/lib/drip/response.rb +++ b/lib/drip/response.rb @@ -26,12 +26,13 @@ def success? (200..299).cover?(status) end - def respond_to?(method_name, include_private = false) - member_map.keys.include?(method_name) || super + def respond_to_missing?(method_name, include_private = false) + member_map.key?(method_name) || super end - def method_missing(method_name, *args, &block) - return super unless member_map.keys.include?(method_name) + def method_missing(method_name, *args, &) + return super unless member_map.key?(method_name) + members[member_map[method_name]] end @@ -47,27 +48,29 @@ def parse_meta def parse_members return body unless success? + return {} unless body.is_a?(Hash) + {}.tap do |members| - if body.is_a?(Hash) - body.each do |key, value| - klass = case value - when Array - Drip::Collections.find_class(key) - when String - String - else - Drip::Resources.find_class(key) - end - - members[key] = klass.new(value) - end + body.each do |key, value| + members[key] = member_class(key, value).new(value) end end end + def member_class(key, value) + case value + when Array + Drip::Collections.find_class(key) + when String + String + else + Drip::Resources.find_class(key) + end + end + def member_map @member_map ||= {}.tap do |map| - members.each { |key, _value| map[key.to_sym] = key } + members.each_key { |key| map[key.to_sym] = key } end end end diff --git a/lib/drip/version.rb b/lib/drip/version.rb index c1ce597..39abd0d 100644 --- a/lib/drip/version.rb +++ b/lib/drip/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Drip - VERSION = "3.4.3" + VERSION = "3.5.0" end diff --git a/test/drip/client/accounts_test.rb b/test/drip/client/accounts_test.rb index 91c4b7f..f3aa785 100644 --- a/test/drip/client/accounts_test.rb +++ b/test/drip/client/accounts_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::AccountsTest < Drip::TestCase def setup @@ -26,7 +26,7 @@ def setup setup do @response_status = 200 @response_body = "{}" - @id = 9999999 + @id = 9_999_999 stub_request(:get, "https://api.getdrip.com/v2/accounts/#{@id}"). to_return(status: @response_status, body: @response_body, headers: {}) @@ -37,4 +37,20 @@ def setup assert_equal expected, @client.account(@id) end end + + context "#account with an id that needs escaping" do + setup do + @response_status = 200 + @response_body = "{}" + @id = "abc/123 xyz" + + stub_request(:get, "https://api.getdrip.com/v2/accounts/#{CGI.escape @id}"). + to_return(status: @response_status, body: @response_body, headers: {}) + end + + should "escape the id in the request path" do + expected = Drip::Response.new(@response_status, JSON.parse(@response_body)) + assert_equal expected, @client.account(@id) + end + end end diff --git a/test/drip/client/broadcasts_test.rb b/test/drip/client/broadcasts_test.rb index 1a267c3..e0fbe56 100644 --- a/test/drip/client/broadcasts_test.rb +++ b/test/drip/client/broadcasts_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::BroadcastsTest < Drip::TestCase def setup @@ -26,7 +26,7 @@ def setup setup do @response_status = 200 @response_body = "{}" - @id = 99999 + @id = 99_999 stub_request(:get, "https://api.getdrip.com/v2/12345/broadcasts/#{@id}"). to_return(status: @response_status, body: @response_body, headers: {}) diff --git a/test/drip/client/campaign_subscriptions_test.rb b/test/drip/client/campaign_subscriptions_test.rb index 8bb8d79..a538356 100644 --- a/test/drip/client/campaign_subscriptions_test.rb +++ b/test/drip/client/campaign_subscriptions_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::CampaignSubscriptionsTest < Drip::TestCase def setup diff --git a/test/drip/client/campaigns_test.rb b/test/drip/client/campaigns_test.rb index 3caf2ae..b10f76a 100644 --- a/test/drip/client/campaigns_test.rb +++ b/test/drip/client/campaigns_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::CampaignsTest < Drip::TestCase def setup @@ -26,7 +26,7 @@ def setup setup do @response_status = 200 @response_body = "{}" - @id = 9999999 + @id = 9_999_999 stub_request(:get, "https://api.getdrip.com/v2/12345/campaigns/#{@id}"). to_return(status: @response_status, body: @response_body, headers: {}) @@ -42,7 +42,7 @@ def setup setup do @response_status = 204 @response_body = nil - @id = 9999999 + @id = 9_999_999 stub_request(:post, "https://api.getdrip.com/v2/12345/campaigns/#{@id}/activate"). to_return(status: @response_status, body: @response_body, headers: {}) @@ -58,7 +58,7 @@ def setup setup do @response_status = 204 @response_body = nil - @id = 9999999 + @id = 9_999_999 stub_request(:post, "https://api.getdrip.com/v2/12345/campaigns/#{@id}/pause"). to_return(status: @response_status, body: @response_body, headers: {}) @@ -74,7 +74,7 @@ def setup setup do @response_status = 200 @response_body = "{}" - @id = 9999999 + @id = 9_999_999 stub_request(:get, "https://api.getdrip.com/v2/12345/campaigns/#{@id}/subscribers"). to_return(status: @response_status, body: @response_body, headers: {}) diff --git a/test/drip/client/configuration_test.rb b/test/drip/client/configuration_test.rb index dff823d..d5fd6b9 100644 --- a/test/drip/client/configuration_test.rb +++ b/test/drip/client/configuration_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/client/configuration" class Drip::Client::ConfigurationTest < Drip::TestCase diff --git a/test/drip/client/conversions_test.rb b/test/drip/client/conversions_test.rb index 69704da..c4803fa 100644 --- a/test/drip/client/conversions_test.rb +++ b/test/drip/client/conversions_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::ConversionsTest < Drip::TestCase def setup @@ -26,7 +26,7 @@ def setup setup do @response_status = 200 @response_body = "{}" - @id = 9999999 + @id = 9_999_999 stub_request(:get, "https://api.getdrip.com/v2/12345/goals/#{@id}"). to_return(status: @response_status, body: @response_body, headers: {}) diff --git a/test/drip/client/custom_fields_test.rb b/test/drip/client/custom_fields_test.rb index b158fa3..bbaa9cb 100644 --- a/test/drip/client/custom_fields_test.rb +++ b/test/drip/client/custom_fields_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::CustomFieldsTest < Drip::TestCase def setup diff --git a/test/drip/client/events_test.rb b/test/drip/client/events_test.rb index bb2d499..176eaa8 100644 --- a/test/drip/client/events_test.rb +++ b/test/drip/client/events_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::EventsTest < Drip::TestCase def setup diff --git a/test/drip/client/forms_test.rb b/test/drip/client/forms_test.rb index 8910908..b5673e3 100644 --- a/test/drip/client/forms_test.rb +++ b/test/drip/client/forms_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::FormsTest < Drip::TestCase def setup @@ -26,7 +26,7 @@ def setup setup do @response_status = 200 @response_body = "{}" - @id = 9999999 + @id = 9_999_999 stub_request(:get, "https://api.getdrip.com/v2/12345/forms/#{@id}"). to_return(status: @response_status, body: @response_body, headers: {}) diff --git a/test/drip/client/http_client_test.rb b/test/drip/client/http_client_test.rb index f857148..eb67f71 100644 --- a/test/drip/client/http_client_test.rb +++ b/test/drip/client/http_client_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/client/configuration" require "drip/client/http_client" require "drip/request" @@ -19,7 +19,7 @@ class Drip::Client::HTTPClientTest < Drip::TestCase @client.make_request(Drip::Request.new(:get, URI("https://api.getdrip.com/v2/testpath"))) - header = "Basic #{Base64.encode64(@key + ':')}".strip + header = "Basic #{Base64.encode64("#{@key}:")}".strip assert_requested :get, "https://api.getdrip.com/v2/testpath", headers: { 'Authorization' => header } end end @@ -68,6 +68,36 @@ class Drip::Client::HTTPClientTest < Drip::TestCase end end + context "given a redirect to a different host" do + setup do + @config = Drip::Client::Configuration.new(api_key: "secret-key") + @client = Drip::Client::HTTPClient.new(@config) + end + + should "not forward credentials to the redirect target" do + stub_request(:get, "https://api.getdrip.com/v2/testpath"). + to_return(status: 301, body: "", headers: { "Location" => "https://evil.example.com/steal" }) + stub_request(:get, "https://evil.example.com/steal"). + to_return(status: 200, body: "{}") + + @client.make_request(Drip::Request.new(:get, URI("https://api.getdrip.com/v2/testpath"))) + + assert_requested(:get, "https://api.getdrip.com/v2/testpath") { |req| req.headers.key?("Authorization") } + assert_requested(:get, "https://evil.example.com/steal") { |req| !req.headers.key?("Authorization") } + end + + should "still forward credentials when the redirect stays on the same host" do + stub_request(:get, "https://api.getdrip.com/v2/testpath"). + to_return(status: 301, body: "", headers: { "Location" => "https://api.getdrip.com/v2/othertestpath" }) + stub_request(:get, "https://api.getdrip.com/v2/othertestpath"). + to_return(status: 200, body: "{}") + + @client.make_request(Drip::Request.new(:get, URI("https://api.getdrip.com/v2/testpath"))) + + assert_requested(:get, "https://api.getdrip.com/v2/othertestpath") { |req| req.headers.key?("Authorization") } + end + end + context "given a get request" do setup do @config = Drip::Client::Configuration.new(http_open_timeout: 20) diff --git a/test/drip/client/orders_test.rb b/test/drip/client/orders_test.rb index a5b178f..935107c 100644 --- a/test/drip/client/orders_test.rb +++ b/test/drip/client/orders_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::OrdersTest < Drip::TestCase def setup diff --git a/test/drip/client/shopper_activity_test.rb b/test/drip/client/shopper_activity_test.rb index c5285cd..19d1de2 100644 --- a/test/drip/client/shopper_activity_test.rb +++ b/test/drip/client/shopper_activity_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::ShopperActivityTest < Drip::TestCase def setup diff --git a/test/drip/client/subscribers_test.rb b/test/drip/client/subscribers_test.rb index cbbb79a..cc421d8 100644 --- a/test/drip/client/subscribers_test.rb +++ b/test/drip/client/subscribers_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::SubscribersTest < Drip::TestCase def setup @@ -81,7 +81,7 @@ def setup should "allow request with drip id keyword argument" do expected = Drip::Response.new(@response_status, JSON.parse(@response_body)) - assert_equal expected, @client.create_or_update_subscriber(id: 123456) + assert_equal expected, @client.create_or_update_subscriber(id: 123_456) assert_requested :post, "https://api.getdrip.com/v2/12345/subscribers", body: '{"subscribers":[{"id":123456}]}', times: 1 end @@ -164,6 +164,25 @@ def setup end end + context "#subscribe with a campaign id that needs escaping" do + setup do + @email = "derrick@getdrip.com" + @campaign_id = "abc/123 xyz" + @data = { "time_zone" => "America/Los_Angeles" } + + @response_status = 201 + @response_body = "{}" + + stub_request(:post, "https://api.getdrip.com/v2/12345/campaigns/#{CGI.escape @campaign_id}/subscribers"). + to_return(status: @response_status, body: @response_body, headers: {}) + end + + should "escape the campaign id in the request path" do + expected = Drip::Response.new(@response_status, JSON.parse(@response_body)) + assert_equal expected, @client.subscribe(@email, @campaign_id, @data) + end + end + context "#unsubscribe" do context "if no campaign id is provided" do setup do @@ -199,6 +218,24 @@ def setup assert_equal expected, @client.unsubscribe(@id, campaign_id: @campaign) end end + + context "if a campaign id that needs escaping is provided" do + setup do + @id = "derrick@getdrip.com" + @campaign = "abc/123 xyz" + + @response_status = 201 + @response_body = "{}" + + stub_request(:post, "https://api.getdrip.com/v2/12345/subscribers/#{CGI.escape @id}/remove?campaign_id=#{CGI.escape @campaign}"). + to_return(status: @response_status, body: @response_body, headers: {}) + end + + should "escape the campaign id in the query string" do + expected = Drip::Response.new(@response_status, JSON.parse(@response_body)) + assert_equal expected, @client.unsubscribe(@id, campaign_id: @campaign) + end + end end context "#unsubscribe_from_all" do diff --git a/test/drip/client/tags_test.rb b/test/drip/client/tags_test.rb index 5dce092..69a0e7b 100644 --- a/test/drip/client/tags_test.rb +++ b/test/drip/client/tags_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::TagsTest < Drip::TestCase def setup diff --git a/test/drip/client/users_test.rb b/test/drip/client/users_test.rb index 55848f7..10c7f0b 100644 --- a/test/drip/client/users_test.rb +++ b/test/drip/client/users_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::UsersTest < Drip::TestCase def setup diff --git a/test/drip/client/webhooks_test.rb b/test/drip/client/webhooks_test.rb index 066cc2b..d5794fd 100644 --- a/test/drip/client/webhooks_test.rb +++ b/test/drip/client/webhooks_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::WebhooksTest < Drip::TestCase def setup diff --git a/test/drip/client/workflow_triggers_test.rb b/test/drip/client/workflow_triggers_test.rb index afc6531..03ef162 100644 --- a/test/drip/client/workflow_triggers_test.rb +++ b/test/drip/client/workflow_triggers_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::WorkflowTriggersTest < Drip::TestCase def setup @@ -9,7 +9,7 @@ def setup context "#workflow_triggers" do setup do - @id = 9999999 + @id = 9_999_999 @response_status = 200 @response_body = "{}" diff --git a/test/drip/client/workflows_test.rb b/test/drip/client/workflows_test.rb index 200c55a..8581bad 100644 --- a/test/drip/client/workflows_test.rb +++ b/test/drip/client/workflows_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::Client::WorkflowsTest < Drip::TestCase def setup @@ -38,6 +38,22 @@ def setup end end + context "#workflow with an id that needs escaping" do + setup do + @response_status = 200 + @response_body = "{}" + @id = "abc/123 xyz" + + stub_request(:get, "https://api.getdrip.com/v2/12345/workflows/#{CGI.escape @id}"). + to_return(status: @response_status, body: @response_body, headers: {}) + end + + should "escape the id in the request path" do + expected = Drip::Response.new(@response_status, JSON.parse(@response_body)) + assert_equal expected, @client.workflow(@id) + end + end + context "#activate_workflow" do setup do @response_status = 204 @@ -76,7 +92,7 @@ def setup "email" => "someone@example.com", "time_zone" => "America/Los_Angeles" } - @id = 9999999 + @id = 9_999_999 @payload = { "subscribers" => [@data] }.to_json @response_status = 204 diff --git a/test/drip/client_test.rb b/test/drip/client_test.rb index 6af3a94..f2cc4f6 100644 --- a/test/drip/client_test.rb +++ b/test/drip/client_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../test_helper.rb' +require_relative '../test_helper' require "base64" class Drip::ClientTest < Drip::TestCase @@ -67,7 +67,7 @@ class Drip::ClientTest < Drip::TestCase setup do @client = Drip::Client.new do |config| config.api_key = "Hello world" - config.account_id = 12345 + config.account_id = 12_345 end end @@ -114,7 +114,7 @@ class Drip::ClientTest < Drip::TestCase should "return a resource and note deprecation" do client = Drip::Client.new resource = nil - assert_output(nil, /^\[DEPRECATED\] Drip\:\:Client\#generate_resource is deprecated/) { resource = client.generate_resource("hello", {}) } + assert_output(nil, /^\[DEPRECATED\] Drip::Client\#generate_resource is deprecated/) { resource = client.generate_resource("hello", {}) } assert_equal({ "hello" => [{}] }, resource) end end @@ -124,7 +124,7 @@ class Drip::ClientTest < Drip::TestCase should "return default content type and print warning" do client = Drip::Client.new content_type = nil - assert_output(nil, /^\[DEPRECATED\] Drip\:\:Client\#content_type is deprecated/) { content_type = client.content_type } + assert_output(nil, /^\[DEPRECATED\] Drip::Client\#content_type is deprecated/) { content_type = client.content_type } assert_equal "application/vnd.api+json", content_type end end @@ -137,7 +137,7 @@ class Drip::ClientTest < Drip::TestCase client = Drip::Client.new response = nil - assert_output(nil, /^\[DEPRECATED\] Drip\:\:Client\#get please use the API endpoint specific methods/) { response = client.get("testpath") } + assert_output(nil, /^\[DEPRECATED\] Drip::Client\#get please use the API endpoint specific methods/) { response = client.get("testpath") } assert_equal({}, response.body) end end diff --git a/test/drip/collection_test.rb b/test/drip/collection_test.rb index affc3f3..930023a 100644 --- a/test/drip/collection_test.rb +++ b/test/drip/collection_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../test_helper.rb' +require_relative '../test_helper' class Drip::CollectionTest < Drip::TestCase should "yield resources of the correct type" do diff --git a/test/drip/collections/account_test.rb b/test/drip/collections/account_test.rb index 09dd80b..44fabd4 100644 --- a/test/drip/collections/account_test.rb +++ b/test/drip/collections/account_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/accounts" class Drip::AccountsTest < Drip::TestCase diff --git a/test/drip/collections/broadcasts_test.rb b/test/drip/collections/broadcasts_test.rb index 5f4fa8b..da455ef 100644 --- a/test/drip/collections/broadcasts_test.rb +++ b/test/drip/collections/broadcasts_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/broadcasts" class Drip::BroadcastsTest < Drip::TestCase diff --git a/test/drip/collections/campaign_subscriptions_test.rb b/test/drip/collections/campaign_subscriptions_test.rb index c5737b1..cfc85d8 100644 --- a/test/drip/collections/campaign_subscriptions_test.rb +++ b/test/drip/collections/campaign_subscriptions_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/campaign_subscriptions" class Drip::CampaignSubscriptionsTest < Drip::TestCase diff --git a/test/drip/collections/campaigns_test.rb b/test/drip/collections/campaigns_test.rb index 4535be2..e22e6f2 100644 --- a/test/drip/collections/campaigns_test.rb +++ b/test/drip/collections/campaigns_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/campaigns" class Drip::CampaignsTest < Drip::TestCase diff --git a/test/drip/collections/errors_test.rb b/test/drip/collections/errors_test.rb index 9ed1089..53f4bc1 100644 --- a/test/drip/collections/errors_test.rb +++ b/test/drip/collections/errors_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/errors" class Drip::ErrorsTest < Drip::TestCase diff --git a/test/drip/collections/orders_test.rb b/test/drip/collections/orders_test.rb index c7c689a..5e64a02 100644 --- a/test/drip/collections/orders_test.rb +++ b/test/drip/collections/orders_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/orders" class Drip::OrdersTest < Drip::TestCase diff --git a/test/drip/collections/purchases_test.rb b/test/drip/collections/purchases_test.rb index fef9098..b6f71f2 100644 --- a/test/drip/collections/purchases_test.rb +++ b/test/drip/collections/purchases_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/purchases" class Drip::PurchasesTest < Drip::TestCase diff --git a/test/drip/collections/tags_test.rb b/test/drip/collections/tags_test.rb index 3014076..460f267 100644 --- a/test/drip/collections/tags_test.rb +++ b/test/drip/collections/tags_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/tags" class Drip::TagsTest < Drip::TestCase diff --git a/test/drip/collections/webhooks_test.rb b/test/drip/collections/webhooks_test.rb index 9dd17a3..e892de5 100644 --- a/test/drip/collections/webhooks_test.rb +++ b/test/drip/collections/webhooks_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/webhooks" class Drip::WebhooksTest < Drip::TestCase diff --git a/test/drip/collections/workflow_triggers_test.rb b/test/drip/collections/workflow_triggers_test.rb index 4be0138..c6a9155 100644 --- a/test/drip/collections/workflow_triggers_test.rb +++ b/test/drip/collections/workflow_triggers_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/workflow_triggers" class Drip::WorkflowTriggersTest < Drip::TestCase diff --git a/test/drip/collections/workflows_test.rb b/test/drip/collections/workflows_test.rb index 30dd72e..4788737 100644 --- a/test/drip/collections/workflows_test.rb +++ b/test/drip/collections/workflows_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/collections/workflows" class Drip::WorkflowsTest < Drip::TestCase diff --git a/test/drip/collections_test.rb b/test/drip/collections_test.rb index 778f8b6..5a3df21 100644 --- a/test/drip/collections_test.rb +++ b/test/drip/collections_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../test_helper.rb' +require_relative '../test_helper' class Drip::CollectionsTest < Drip::TestCase should "find collections" do diff --git a/test/drip/request_test.rb b/test/drip/request_test.rb index a0d8084..9039b89 100644 --- a/test/drip/request_test.rb +++ b/test/drip/request_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../test_helper.rb' +require_relative '../test_helper' require "drip/request" class Drip::RequestTest < Drip::TestCase diff --git a/test/drip/resource_test.rb b/test/drip/resource_test.rb index de899f8..2832522 100644 --- a/test/drip/resource_test.rb +++ b/test/drip/resource_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../test_helper.rb' +require_relative '../test_helper' class Drip::ResourceTest < Drip::TestCase class TestResource < Drip::Resource diff --git a/test/drip/resources/account_test.rb b/test/drip/resources/account_test.rb index 5fdf3ca..06c4a4b 100644 --- a/test/drip/resources/account_test.rb +++ b/test/drip/resources/account_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::AccountTest < Drip::TestCase should "have a resource name" do diff --git a/test/drip/resources/order_test.rb b/test/drip/resources/order_test.rb index 5207b9a..869b87a 100644 --- a/test/drip/resources/order_test.rb +++ b/test/drip/resources/order_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::OrderTest < Drip::TestCase should "have a resource name" do diff --git a/test/drip/resources/subscriber_test.rb b/test/drip/resources/subscriber_test.rb index 4f07b12..085b79a 100644 --- a/test/drip/resources/subscriber_test.rb +++ b/test/drip/resources/subscriber_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' class Drip::SubscriberTest < Drip::TestCase should "have a resource name" do diff --git a/test/drip/resources/tag_test.rb b/test/drip/resources/tag_test.rb index 7ad5860..5215255 100644 --- a/test/drip/resources/tag_test.rb +++ b/test/drip/resources/tag_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../../test_helper.rb' +require_relative '../../test_helper' require "drip/resources/tag" class Drip::TagTest < Drip::TestCase diff --git a/test/drip/resources_test.rb b/test/drip/resources_test.rb index 90ab4f2..319e640 100644 --- a/test/drip/resources_test.rb +++ b/test/drip/resources_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../test_helper.rb' +require_relative '../test_helper' class Drip::ResourcesTest < Drip::TestCase should "find resources" do diff --git a/test/drip/response_test.rb b/test/drip/response_test.rb index 8c3f2e0..7228597 100644 --- a/test/drip/response_test.rb +++ b/test/drip/response_test.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require File.dirname(__FILE__) + '/../test_helper.rb' +require_relative '../test_helper' class Drip::ResponseTest < Drip::TestCase context "#success?" do diff --git a/test/test_helper.rb b/test/test_helper.rb index f77517e..fff0ca9 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -3,12 +3,12 @@ require 'simplecov' SimpleCov.start -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +$LOAD_PATH.unshift File.expand_path('../lib', __dir__) require "drip" require "minitest/autorun" require "shoulda-context" -require "mocha/setup" +require "mocha/minitest" require "webmock/minitest" class Drip::TestCase < Minitest::Test