From 7899732d4498874542798e5b6d60f570d7f9ea8c Mon Sep 17 00:00:00 2001 From: Karl Falconer Date: Wed, 19 Aug 2026 08:53:49 -0400 Subject: [PATCH] Modernize dependencies, add MIT license, and automate releases Bring the gem's dependency chain up to date and add the plumbing needed to publish it. Dependencies: - Require omniauth ~> 2.0 and omniauth-oauth2 ~> 1.9 (oauth2 2.0.25), up from an unpinned omniauth-oauth2 that resolved to omniauth 1.9 / oauth2 1.4. This is breaking for host applications, hence the 0.1.0 bump. - Pin the token endpoint to auth_scheme: :request_body. oauth2 2.0 changed its default to :basic_auth, which would have moved client_id/client_secret out of the token request body and changed what ShipBob receives. - Ruby >= 3.2, Bundler 4, rake ~> 13.0, rspec ~> 3.13. The 3.2 floor comes from multi_xml and public_suffix in the resolved set. - Move development dependencies from the gemspec to the Gemfile and drop bundler as a development dependency. Strategy and specs: - Build the channel endpoint from the api_url option added in #10, via a channel_url helper that tolerates a trailing slash on the configured value. - Grow the suite from 3 to 19 examples covering the request phase, the token exchange, the redirect_uri override from #7 and the auth hash, against both the production and sandbox hosts. - Log channel lookup failures instead of discarding them silently. Packaging and release: - Add LICENSE.txt (MIT) and declare it in the gemspec. - Stop packaging bin/ as gem executables and keep development files out of the built gem. - Replace Travis CI with GitHub Actions, testing Ruby 3.2 through 3.4. - Add a Release workflow: pushing a v* tag reruns the matrix, verifies the tag against version.rb, publishes via RubyGems trusted publishing and opens a GitHub release from CHANGELOG.md. - Fix bin/console requiring a non-existent omniauth/fdc file. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 32 ++++ .github/workflows/release.yml | 77 +++++++++ .ruby-version | 2 +- .travis.yml | 7 - CHANGELOG.md | 48 ++++++ Gemfile | 11 +- Gemfile.lock | 158 ++++++++++++++----- LICENSE.txt | 21 +++ README.md | 54 ++++++- bin/console | 7 +- lib/omniauth-shipbob/version.rb | 2 +- lib/omniauth/strategies/shipbob.rb | 30 ++-- omniauth-shipbob.gemspec | 41 +++-- spec/omniauth/strategies/shipbob_spec.rb | 191 ++++++++++++++++++++--- spec/spec_helper.rb | 8 + 15 files changed, 582 insertions(+), 107 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..aad81f2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + # Lets the release workflow gate publishing on this same matrix. + workflow_call: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: ["3.2", "3.3", "3.4"] + + name: Ruby ${{ matrix.ruby }} + steps: + - uses: actions/checkout@v5 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Run the test suite + run: bundle exec rake spec + + - name: Build the gem + run: bundle exec rake build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..53eb34a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: Release + +# Cut a release by bumping lib/omniauth-shipbob/version.rb, committing, then: +# git tag -a v0.1.0 -m "v0.1.0" && git push origin master --tags +on: + push: + tags: ["v*"] + +jobs: + # Never publish a version that has not passed the matrix. + test: + uses: ./.github/workflows/ci.yml + + push: + name: Push gem to RubyGems.org + needs: test + runs-on: ubuntu-latest + + # Publishing uses RubyGems trusted publishing (OIDC), so there is no API key + # secret to rotate and it works with the gemspec's `rubygems_mfa_required`. + # + # One-time setup on rubygems.org: open the gem -> "Trusted publishers" -> + # "Create", with owner `dropstream`, repository `omniauth-shipbob`, + # workflow `release.yml`, and environment `release`. + environment: release + + permissions: + id-token: write # mandatory for trusted publishing + contents: write # `rake release` pushes the tag; `gh` creates the release + + steps: + - uses: actions/checkout@v5 + with: + # rubygems/release-gem installs its own short-lived git credentials. + persist-credentials: false + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + # `rake release` builds whatever version.rb says, regardless of the tag. If the + # two disagree it would publish one version and tag another, so stop here first. + - name: Verify the tag matches the gem version + run: | + gem_version="$(ruby -r./lib/omniauth-shipbob/version -e 'print Omniauth::Shipbob::VERSION')" + tag_version="${GITHUB_REF_NAME#v}" + if [ "$gem_version" != "$tag_version" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match lib/omniauth-shipbob/version.rb (${gem_version})." + exit 1 + fi + echo "Releasing omniauth-shipbob ${gem_version}" + + # Builds the gem and runs `gem push` against the gemspec's allowed_push_host. + # The tag already exists, so bundler skips its own tag-and-push step. + - name: Push to RubyGems.org + uses: rubygems/release-gem@v1 + + - name: Create the GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + notes="$(awk -v v="${GITHUB_REF_NAME#v}" ' + $0 ~ "^## \\[" v "\\]" { flag = 1; next } + flag && /^## \[/ { exit } + flag { print } + ' CHANGELOG.md)" + + if [ -n "$(printf '%s' "$notes" | tr -d '[:space:]')" ]; then + printf '%s\n' "$notes" > release-notes.md + gh release create "$GITHUB_REF_NAME" --verify-tag \ + --title "$GITHUB_REF_NAME" --notes-file release-notes.md + else + echo "No CHANGELOG.md section for ${GITHUB_REF_NAME}; generating notes from commits." + gh release create "$GITHUB_REF_NAME" --verify-tag \ + --title "$GITHUB_REF_NAME" --generate-notes + fi diff --git a/.ruby-version b/.ruby-version index 79a6144..7921bd0 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.4 +3.4.8 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c22db40..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -sudo: false -language: ruby -cache: bundler -rvm: - - 2.4.4 -before_install: gem install bundler -v 1.16.6 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..659215e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [0.1.0] - 2026-08-19 + +### Changed + +- **Breaking:** requires `omniauth ~> 2.0` and `omniauth-oauth2 ~> 1.9` (previously + unpinned, which resolved to `omniauth 1.9` / `oauth2 1.4`). Host applications must + upgrade to OmniAuth 2.x; Rails apps also need + [`omniauth-rails_csrf_protection`](https://github.com/cookpad/omniauth-rails_csrf_protection) + because OmniAuth 2 only accepts `POST` for the request phase. +- Pinned the token endpoint to `auth_scheme: :request_body`. `oauth2 2.0` changed its + default to `:basic_auth`, which would have moved `client_id`/`client_secret` out of + the token request body and changed what ShipBob receives. +- Requires Ruby >= 3.2 (the modern `oauth2` dependency chain requires it). +- Development toolchain: Bundler 4.x, `rake ~> 13.0`, `rspec ~> 3.13`. +- Replaced Travis CI with GitHub Actions, testing Ruby 3.2 through 3.4. +- Releases are now cut by pushing a `v*` tag: the `Release` workflow reruns the matrix, + checks the tag against `version.rb`, publishes to RubyGems.org via trusted publishing + (OIDC, no stored API key), and opens a GitHub release from this changelog. + +### Added + +- `LICENSE.txt` (MIT), now declared in the gemspec. +- `channel_url`, which builds the channel endpoint from `api_url` and tolerates a + trailing slash on the configured value. +- Integration specs covering the request phase, the token exchange, the `redirect_uri` + override and the auth hash (including `channel_id`), against both the production and + sandbox hosts. + +### Fixed + +- Channel lookup failures are now logged instead of being silently discarded. +- `bin/console` required a non-existent `omniauth/fdc` file. +- Development scripts in `bin/` are no longer packaged as gem executables. + +## [0.0.3] + +- `api_url` is now a strategy option, defaulting to `https://api.shipbob.com/1.0`, so + the channel lookup can point at ShipBob's sandbox (#10). +- `callback_url` is overridden so query params from a configured `redirect_uri` are not + sent to ShipBob (#7). + +## [0.0.2] + +- Initial published strategy. diff --git a/Gemfile b/Gemfile index a13c42e..af362bc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,13 @@ +# frozen_string_literal: true + source "https://rubygems.org" -# Specify your gem's dependencies in omniauth-fdc.gemspec +# Specify your gem's dependencies in omniauth-shipbob.gemspec gemspec + +group :development, :test do + gem "rack-test", "~> 2.2" + gem "rake", "~> 13.0" + gem "rspec", "~> 3.13" + gem "webmock", "~> 3.25" +end diff --git a/Gemfile.lock b/Gemfile.lock index 2a85cf6..2a3fbb3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,56 +1,140 @@ PATH remote: . specs: - omniauth-shipbob (0.0.1) - omniauth-oauth2 + omniauth-shipbob (0.1.0) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.9) GEM remote: https://rubygems.org/ specs: - diff-lcs (1.3) - faraday (1.0.1) - multipart-post (>= 1.2, < 3) - hashie (4.1.0) - jwt (2.2.1) - multi_json (1.14.1) - multi_xml (0.6.0) - multipart-post (2.1.1) - oauth2 (1.4.4) - faraday (>= 0.8, < 2.0) - jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + anonymous_loader (0.1.3) + version_gem (~> 1.1, >= 1.1.14) + auth-sanitizer (0.2.3) + version_gem (~> 1.1, >= 1.1.14) + base64 (0.3.0) + bigdecimal (4.1.2) + crack (1.0.1) + bigdecimal + rexml + diff-lcs (1.6.2) + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.4) + net-http (~> 0.5) + hashdiff (1.2.1) + hashie (5.1.0) + logger + json (2.21.2) + jwt (3.2.0) + base64 + logger (1.7.0) + multi_xml (0.9.1) + bigdecimal (>= 3.1, < 5) + net-http (0.9.1) + uri (>= 0.11.1) + oauth2 (2.0.25) + anonymous_loader (~> 0.1, >= 0.1.3) + auth-sanitizer (~> 0.2, >= 0.2.3) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - omniauth (1.9.1) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.7) + version_gem (~> 1.1, >= 1.1.14) + omniauth (2.1.4) hashie (>= 3.4.6) - rack (>= 1.6.2, < 3) - omniauth-oauth2 (1.6.0) - oauth2 (~> 1.1) - omniauth (~> 1.9) - rack (2.2.2) - rake (10.5.0) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.0) - rspec-support (~> 3.9.0) - rspec-expectations (3.9.0) + logger + rack (>= 2.2.3) + rack-protection + omniauth-oauth2 (1.9.0) + oauth2 (>= 2.0.2, < 3) + omniauth (~> 2.0) + public_suffix (7.0.5) + rack (3.2.7) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-test (2.2.0) + rack (>= 1.3) + rake (13.4.2) + rexml (3.4.4) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-support (3.9.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + snaky_hash (2.0.7) + hashie (>= 0.1.0, < 6) + version_gem (~> 1.1, >= 1.1.14) + uri (1.1.1) + version_gem (1.1.15) + webmock (3.26.2) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) PLATFORMS ruby DEPENDENCIES - bundler (~> 1.16) omniauth-shipbob! - rake (~> 10.0) - rspec (~> 3.0) + rack-test (~> 2.2) + rake (~> 13.0) + rspec (~> 3.13) + webmock (~> 3.25) + +CHECKSUMS + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + anonymous_loader (0.1.3) sha256=084a18e2439144d955447dc11dfc982f41fcd1583ad32d4d55151325dc44cb55 + auth-sanitizer (0.2.3) sha256=db10aac92cfbe4c64ab637eebcbe1d67395d1694798041362173370f59933e3c + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.18) sha256=02d9a17429de1847b4e0c9f27a9ee4b20c0a74c0a641b4e77195d6019e3618ac + crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 + hashie (5.1.0) sha256=c266471896f323c446ea8207f8ffac985d2718df0a0ba98651a3057096ca3870 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + multi_xml (0.9.1) sha256=7ce766b59c17241ed62976caeae1fae9b2431b263398c35396239a68c4a64e57 + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + oauth2 (2.0.25) sha256=2f736a2f93c2caa67c1b08dc3c9889bb907d62643f3183fefaa1723cba6a82ac + omniauth (2.1.4) sha256=42a05b0496f0d22e1dd85d42aaf602f064e36bb47a6826a27ab55e5ba608763c + omniauth-oauth2 (1.9.0) sha256=ed15f6d9d20991807ce114cc5b9c1453bce3645b64e51c68c90cff5ff153fee8 + omniauth-shipbob (0.1.0) + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7 + rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + snaky_hash (2.0.7) sha256=7d02c70012a3f932e48860cd024577908300c9aa615e0cb9b450aaa749cbcb4d + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + version_gem (1.1.15) sha256=a73241587b29252e3567e0c818ded80730eb754d43b508f6ce4db22ca9ba27d0 + webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90 BUNDLED WITH - 1.17.3 + 4.0.18 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e586716 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019-2026 Dropstream + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index c50876a..46fdf9c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Omniauth::Shipbob -This is an OmniAuth strategy for authenticating to ShipBob. +This is an OmniAuth strategy for authenticating to ShipBob. + +Requires Ruby >= 3.2, OmniAuth 2.x and `omniauth-oauth2` 1.9.x. ## Installation @@ -20,6 +22,14 @@ Or install it yourself as: ## Usage +OmniAuth 2 only accepts `POST` for the request phase. In a Rails app, add +[`omniauth-rails_csrf_protection`](https://github.com/cookpad/omniauth-rails_csrf_protection) +to your Gemfile and link to the provider with `button_to` or `link_to ..., method: :post`: + +```ruby +gem 'omniauth-rails_csrf_protection' +``` + ```ruby Rails.application.config.middleware.use OmniAuth::Builder do provider :shipbob, @@ -43,6 +53,11 @@ You can configure integration_name through the authorize_params hash: end ``` +### Configure the API endpoint + +`api_url` sets the host the strategy calls to look up the channel id that lands in +`credentials['channel_id']`. It defaults to `https://api.shipbob.com/1.0`. + ### Configure for Sandbox ENV Configure these options to use ShipBob's [sandbox](https://developer.shipbob.com/sandbox-simulations) environment. @@ -61,10 +76,41 @@ end ## Development -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. + +To install this gem onto your local machine, run `bundle exec rake install`. -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +## Releasing + +Releases are cut by pushing a tag; GitHub Actions builds and publishes the gem to +[rubygems.org](https://rubygems.org) via +[RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/), so no API +key is stored in the repo and the gemspec can keep `rubygems_mfa_required`. + +1. Bump `VERSION` in `lib/omniauth-shipbob/version.rb` and add a `CHANGELOG.md` section + for it (the release notes are taken from that section). +2. Commit both on `master`. +3. Tag and push: + + ```sh + git tag -a v0.1.0 -m "v0.1.0" + git push origin master --tags + ``` + +The `Release` workflow then runs the full test matrix, refuses to continue if the tag and +`version.rb` disagree, pushes the gem, and opens a GitHub release. + +### One-time RubyGems setup + +Before the first automated release, register this repo as a trusted publisher: on +rubygems.org open the gem → **Trusted publishers** → **Create**, with owner +`dropstream`, repository `omniauth-shipbob`, workflow `release.yml`, and environment +`release`. ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/omniauth-fdc. +Bug reports and pull requests are welcome on GitHub at https://github.com/dropstream/omniauth-shipbob. + +## License + +Available as open source under the terms of the [MIT License](LICENSE.txt). diff --git a/bin/console b/bin/console index e543eb7..2fac1b5 100755 --- a/bin/console +++ b/bin/console @@ -1,14 +1,11 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" -require "omniauth/fdc" +require "omniauth-shipbob" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start - require "irb" IRB.start(__FILE__) diff --git a/lib/omniauth-shipbob/version.rb b/lib/omniauth-shipbob/version.rb index 2c6ae5e..8d2b1dc 100644 --- a/lib/omniauth-shipbob/version.rb +++ b/lib/omniauth-shipbob/version.rb @@ -1,5 +1,5 @@ module Omniauth module Shipbob - VERSION = "0.0.3" + VERSION = "0.1.0" end end diff --git a/lib/omniauth/strategies/shipbob.rb b/lib/omniauth/strategies/shipbob.rb index e863ab1..989f1bd 100644 --- a/lib/omniauth/strategies/shipbob.rb +++ b/lib/omniauth/strategies/shipbob.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'omniauth-oauth2' require 'json' @@ -5,16 +6,20 @@ module OmniAuth module Strategies class Shipbob < OmniAuth::Strategies::OAuth2 + DEFAULT_API_URL = 'https://api.shipbob.com/1.0' option :client_options, { :authorize_url => '/connect/integrate', :token_url => '/connect/token', - :site => 'https://auth.shipbob.com' + :site => 'https://auth.shipbob.com', + # oauth2 2.x defaults to :basic_auth; ShipBob expects the client + # credentials in the token request body, which was the oauth2 1.x default. + :auth_scheme => :request_body } - + option :authorize_params, { :response_mode => 'form_post' } - option :api_url, 'https://api.shipbob.com/1.0' - + option :api_url, DEFAULT_API_URL + credentials do hash = {"token" => access_token.token} hash["refresh_token"] = access_token.refresh_token if access_token.expires? && access_token.refresh_token @@ -23,20 +28,25 @@ class Shipbob < OmniAuth::Strategies::OAuth2 hash['channel_id'] = get_channel_id(access_token) hash end - + def get_channel_id(token) log :info, 'Calling API to get Channel Id.' - response = token.get("#{options.api_url}/channel", :headers => { 'Content-Type' => 'application/json' }) + response = token.get(channel_url, :headers => { 'Content-Type' => 'application/json' }) JSON.parse(response.body).dig(0, "id") - rescue => e - nil + rescue StandardError => e + log :error, "Failed to fetch ShipBob Channel Id: #{e.class}: #{e.message}" + nil end - + + # The channel endpoint on the configured API host, e.g. ShipBob's sandbox. + def channel_url + "#{options.api_url.to_s.chomp('/')}/channel" + end + # override callback_url to avoid sending query params along with specified redirect_uri def callback_url full_host + script_name + callback_path end - end end end diff --git a/omniauth-shipbob.gemspec b/omniauth-shipbob.gemspec index 24c050c..9064d54 100644 --- a/omniauth-shipbob.gemspec +++ b/omniauth-shipbob.gemspec @@ -1,5 +1,6 @@ +# 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 "omniauth-shipbob/version" @@ -9,31 +10,27 @@ Gem::Specification.new do |spec| spec.authors = ["Venkata Kishor"] spec.email = ["kishore.venkat@getdropstream.com"] - spec.summary = %q{OmniAuth strategy for ShipBob} - spec.description = %q{In this gem you will find an OmniAuth ShipBob strategy} + spec.summary = "OmniAuth strategy for ShipBob" + spec.description = "In this gem you will find an OmniAuth ShipBob strategy" spec.homepage = "https://github.com/dropstream/omniauth-shipbob" + spec.license = "MIT" - # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' - # to allow pushing to a single host or delete this section to allow pushing to any host. - if spec.respond_to?(:metadata) - spec.metadata["allowed_push_host"] = "https://rubygems.org" - else - raise "RubyGems 2.0 or newer is required to protect against " \ - "public gem pushes." - end + spec.required_ruby_version = ">= 3.2" + + spec.metadata["allowed_push_host"] = "https://rubygems.org" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata["rubygems_mfa_required"] = "true" - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + # Ship only the library itself; development scaffolding stays out of the gem. + spec.files = Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{\A(?:spec|bin|pkg|\.github)/}) || + f.match(%r{\A(?:\.gitignore|\.rspec|\.ruby-version|\.ruby-gemset|Gemfile|Gemfile\.lock|Rakefile)\z}) + end end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_runtime_dependency 'omniauth-oauth2' - - spec.add_development_dependency "bundler", "~> 1.16" - spec.add_development_dependency "rake", "~> 10.0" - spec.add_development_dependency "rspec", "~> 3.0" + spec.add_dependency "omniauth", "~> 2.0" + spec.add_dependency "omniauth-oauth2", "~> 1.9" end diff --git a/spec/omniauth/strategies/shipbob_spec.rb b/spec/omniauth/strategies/shipbob_spec.rb index 884a2d5..e572c8f 100644 --- a/spec/omniauth/strategies/shipbob_spec.rb +++ b/spec/omniauth/strategies/shipbob_spec.rb @@ -1,38 +1,191 @@ +# frozen_string_literal: true + RSpec.describe OmniAuth::Strategies::Shipbob do + let(:inner_app) { ->(_env) { [200, {}, ["Hello."]] } } + let(:strategy_options) { {} } + let(:strategy) { described_class.new(inner_app, "client_id", "client_secret", **strategy_options) } + def app - lambda do |_env| - [200, {}, ["Hello."]] - end + described_class.new(inner_app, "client_id", "client_secret", **strategy_options) end - let(:strategy) { Class.new(OmniAuth::Strategies::Shipbob) } describe "#client_options" do - subject { strategy.new(app).options.client_options } + subject(:client_options) { strategy.options.client_options } + + it "points at the ShipBob auth site" do + expect(client_options.site).to eq("https://auth.shipbob.com") + end + + it "has the correct authorize url" do + expect(client_options.authorize_url).to eq("/connect/integrate") + end - it 'should have correct authorize url' do - expect(subject.authorize_url).to eq('/connect/integrate') + it "has the correct token url" do + expect(client_options.token_url).to eq("/connect/token") end - it 'should have correct token url' do - expect(subject.token_url).to eq('/connect/token') + # oauth2 >= 2.0 defaults to :basic_auth, which would move the client + # credentials out of the token request body. + it "sends client credentials in the request body" do + expect(client_options.auth_scheme).to eq(:request_body) end end - describe '#options' do - subject { strategy.new(app).options } + describe "#api_url" do + it "defaults to the production API" do + expect(strategy.options.api_url).to eq("https://api.shipbob.com/1.0") + end + + context "when api_url is set" do + let(:strategy_options) { {api_url: "https://sandbox-api.shipbob.com/2.0"} } + + it "uses the configured value" do + expect(strategy.options.api_url).to eq("https://sandbox-api.shipbob.com/2.0") + end + end + end - it 'should have default api_url' do - expect(subject.api_url).to eq('https://api.shipbob.com/1.0') + describe "#channel_url" do + it "is built from the default api_url" do + expect(strategy.channel_url).to eq("https://api.shipbob.com/1.0/channel") end - context 'when api_url is set' do - let(:staging_api_url) { 'https://example.com/1.0' } + context "when api_url is set" do + let(:strategy_options) { {api_url: "https://sandbox-api.shipbob.com/2.0"} } - subject { strategy.new(app, { api_url: staging_api_url }).options } - - it 'should have correct api_url' do - expect(subject.api_url).to eq(staging_api_url) + it "is built from the configured api_url" do + expect(strategy.channel_url).to eq("https://sandbox-api.shipbob.com/2.0/channel") end end + + context "when api_url has a trailing slash" do + let(:strategy_options) { {api_url: "https://sandbox-api.shipbob.com/2.0/"} } + + it "does not double up the separator" do + expect(strategy.channel_url).to eq("https://sandbox-api.shipbob.com/2.0/channel") + end + end + end + + describe "the request phase" do + around do |example| + previous = OmniAuth.config.request_validation_phase + OmniAuth.config.request_validation_phase = proc {} + example.run + OmniAuth.config.request_validation_phase = previous + end + + it "redirects to ShipBob with response_mode=form_post" do + post "/auth/shipbob", {}, "rack.session" => {} + + expect(last_response.status).to eq(302) + location = URI.parse(last_response.headers["Location"]) + expect("#{location.scheme}://#{location.host}#{location.path}") + .to eq("https://auth.shipbob.com/connect/integrate") + expect(URI.decode_www_form(location.query).to_h) + .to include("response_mode" => "form_post", "client_id" => "client_id") + end + + context "when configured for the sandbox" do + let(:strategy_options) { {client_options: {site: "https://authstage.shipbob.com"}} } + + it "redirects to the configured auth site" do + post "/auth/shipbob", {}, "rack.session" => {} + + expect(last_response.headers["Location"]) + .to start_with("https://authstage.shipbob.com/connect/integrate") + end + end + end + + describe "the callback phase" do + let(:session) { {"omniauth.state" => "abc123"} } + let(:token_url) { "#{strategy.options.client_options[:site]}/connect/token" } + + before do + stub_request(:post, token_url).to_return( + status: 200, + body: {access_token: "token123", token_type: "Bearer"}.to_json, + headers: {"Content-Type" => "application/json"} + ) + + stub_request(:get, strategy.channel_url).to_return( + status: 200, + body: [{id: 42, name: "Dropstream"}].to_json, + headers: {"Content-Type" => "application/json"} + ) + end + + def complete_callback(query = "") + post "/auth/shipbob/callback#{query}", + {"code" => "auth_code", "state" => "abc123"}, + "rack.session" => session + end + + it "exchanges the code for a token with credentials in the body" do + complete_callback + + expect(WebMock).to have_requested(:post, token_url) + .with { |req| URI.decode_www_form(req.body).to_h.values_at("client_id", "client_secret") == %w[client_id client_secret] } + end + + # PR #7: the redirect_uri sent to ShipBob must not carry the callback's query params. + it "sends a redirect_uri without query params" do + complete_callback("?utm_source=shipbob") + + expect(WebMock).to have_requested(:post, token_url) + .with { |req| URI.decode_www_form(req.body).to_h["redirect_uri"] == "http://example.org/auth/shipbob/callback" } + end + + it "builds an auth hash carrying the token and channel id" do + complete_callback + + credentials = last_request.env["omniauth.auth"]["credentials"] + expect(credentials["token"]).to eq("token123") + expect(credentials["channel_id"]).to eq(42) + expect(credentials["expires"]).to be(false) + end + + context "when configured for the sandbox" do + let(:strategy_options) do + { + api_url: "https://sandbox-api.shipbob.com/2.0", + client_options: {site: "https://authstage.shipbob.com"} + } + end + + it "exchanges the token and fetches the channel against the sandbox hosts" do + complete_callback + + expect(WebMock).to have_requested(:post, "https://authstage.shipbob.com/connect/token") + expect(WebMock).to have_requested(:get, "https://sandbox-api.shipbob.com/2.0/channel") + expect(last_request.env["omniauth.auth"]["credentials"]["channel_id"]).to eq(42) + end + end + end + + describe "#get_channel_id" do + let(:access_token) { OAuth2::AccessToken.new(strategy.client, "token123") } + + it "returns the id of the first channel" do + stub_request(:get, strategy.channel_url) + .with(headers: {"Authorization" => "Bearer token123"}) + .to_return(status: 200, body: [{id: 7}].to_json, headers: {"Content-Type" => "application/json"}) + + expect(strategy.get_channel_id(access_token)).to eq(7) + end + + it "returns nil when the channel list is empty" do + stub_request(:get, strategy.channel_url) + .to_return(status: 200, body: "[]", headers: {"Content-Type" => "application/json"}) + + expect(strategy.get_channel_id(access_token)).to be_nil + end + + it "returns nil when the API errors" do + stub_request(:get, strategy.channel_url).to_return(status: 500, body: "boom") + + expect(strategy.get_channel_id(access_token)).to be_nil + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 935d848..fc55e33 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,15 @@ +# frozen_string_literal: true + require "bundler/setup" +require "rack/test" +require "webmock/rspec" require "omniauth-shipbob" +OmniAuth.config.logger = Logger.new(File::NULL) + RSpec.configure do |config| + config.include Rack::Test::Methods + # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status"