From 4a86a5448fc523a3b8bd36df7f86b85c06bdde2b Mon Sep 17 00:00:00 2001 From: Peter Boling Date: Tue, 24 Sep 2024 14:59:15 -0600 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20and=20modernize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/FUNDING.yml | 13 + .github/dependabot.yml | 13 + .github/workflows/ancient.yml | 50 +++ .github/workflows/codeql-analysis.yml | 70 ++++ .github/workflows/coverage.yml | 115 ++++++ .github/workflows/dependency-review.yml | 20 ++ .github/workflows/heads.yml | 62 ++++ .github/workflows/style.yml | 46 +++ .github/workflows/supported.yml | 58 +++ .github/workflows/unsupported.yml | 51 +++ .rubocop_gradual.lock | 107 ++++++ CHANGELOG.md | 21 ++ CONTRIBUTING.md | 55 +++ Gemfile | 6 + Gemfile.lock | 159 ++++++++- README | 64 ---- README.md | 248 +++++++++++++ Rakefile | 46 ++- bin/bundle | 118 ++++++ bin/checksums | 67 ++++ bin/console | 14 + bin/rake | 27 ++ bin/rspec | 27 ++ bin/rubocop | 27 ++ bin/setup | 8 + exe/rots | 102 +++--- gemfiles/ancient.gemfile | 27 ++ gemfiles/coverage.gemfile | 9 + gemfiles/style.gemfile | 9 + gemfiles/vanilla.gemfile | 9 + lib/rots.rb | 1 - lib/rots/identity_page_app.rb | 44 +-- lib/rots/mocks.rb | 8 + lib/rots/mocks/client_app.rb | 53 +++ lib/rots/mocks/mock_fetcher.rb | 34 ++ lib/rots/mocks/rots_server.rb | 49 +++ lib/rots/server_app.rb | 108 +++--- lib/rots/test.rb | 2 + lib/rots/test/rack_test_helpers.rb | 21 ++ lib/rots/test/request_helper.rb | 78 ++++ lib/rots/test_helper.rb | 19 - rots.gemspec | 49 ++- spec/config/byebug.rb | 3 + spec/config/rspec/rots.rb | 3 + spec/config/rspec/rspec_block_is_expected.rb | 2 + spec/config/rspec/rspec_core.rb | 9 + spec/config/rspec/version_gem.rb | 1 + spec/rots/mocks/integration_spec.rb | 356 +++++++++++++++++++ spec/rots/version_spec.rb | 7 + spec/server_app_spec.rb | 113 +++--- spec/spec_helper.rb | 93 ++--- 51 files changed, 2338 insertions(+), 363 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ancient.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/coverage.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/heads.yml create mode 100644 .github/workflows/style.yml create mode 100644 .github/workflows/supported.yml create mode 100644 .github/workflows/unsupported.yml create mode 100644 .rubocop_gradual.lock create mode 100644 CHANGELOG.md create mode 100755 CONTRIBUTING.md delete mode 100644 README create mode 100644 README.md create mode 100755 bin/bundle create mode 100755 bin/checksums create mode 100755 bin/console create mode 100755 bin/rake create mode 100755 bin/rspec create mode 100755 bin/rubocop create mode 100755 bin/setup create mode 100644 gemfiles/ancient.gemfile create mode 100644 gemfiles/coverage.gemfile create mode 100644 gemfiles/style.gemfile create mode 100644 gemfiles/vanilla.gemfile create mode 100644 lib/rots/mocks.rb create mode 100644 lib/rots/mocks/client_app.rb create mode 100644 lib/rots/mocks/mock_fetcher.rb create mode 100644 lib/rots/mocks/rots_server.rb create mode 100644 lib/rots/test.rb create mode 100644 lib/rots/test/rack_test_helpers.rb create mode 100644 lib/rots/test/request_helper.rb delete mode 100644 lib/rots/test_helper.rb create mode 100644 spec/config/byebug.rb create mode 100644 spec/config/rspec/rots.rb create mode 100644 spec/config/rspec/rspec_block_is_expected.rb create mode 100644 spec/config/rspec/rspec_core.rb create mode 100644 spec/config/rspec/version_gem.rb create mode 100644 spec/rots/mocks/integration_spec.rb create mode 100644 spec/rots/version_spec.rb diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..5f7648c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +buy_me_a_coffee: pboling +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +github: [pboling] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +issuehunt: pboling # Replace with a single IssueHunt username +ko_fi: pboling # Replace with a single Ko-fi username +liberapay: pboling # Replace with a single Liberapay username +open_collective: # Replace with a single Open Collective username +patreon: galtzo # Replace with a single Patreon username +polar: pboling +thanks_dev: gh/pboling +tidelift: rubygems/rots # Replace with a single Tidelift platform-name/package-name e.g., npm/babel diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..46f1c90 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: bundler + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "rubocop-lts" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/ancient.yml b/.github/workflows/ancient.yml new file mode 100644 index 0000000..d1e973b --- /dev/null +++ b/.github/workflows/ancient.yml @@ -0,0 +1,50 @@ +name: Ancient (EOL) Rubies + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }}${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + gemfile: + # Skips the gemspec, to avoid the modern development tools which can't be installed on ancient Rubies. + - ancient + ruby: + - "2.3" + - "2.4" + - "2.5" + - "2.6" + - "2.7" + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rake test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..502c2a0 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, "*-stable" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, "*-stable" ] + schedule: + - cron: '35 1 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'ruby' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..cc7d4cb --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,115 @@ +name: Code Coverage + +env: + K_SOUP_COV_MIN_BRANCH: 57 + K_SOUP_COV_MIN_LINE: 81 + K_SOUP_COV_MIN_HARD: true + K_SOUP_COV_DO: true + K_SOUP_COV_COMMAND_NAME: "RSpec Coverage" + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs with Coverage - Ruby ${{ matrix.ruby }} ${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + experimental: [false] + rubygems: + - latest + bundler: + - latest + gemfile: + - coverage + ruby: + - "3.3" + + steps: + - uses: amancevice/setup-code-climate@v2 + name: CodeClimate Install + if: ${{ github.event_name != 'pull_request' }} + with: + cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + + - name: CodeClimate Pre-build Notification + run: cc-test-reporter before-build + if: ${{ github.event_name != 'pull_request' }} + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Run RSpec tests + run: | + bundle exec rspec + + - name: CodeClimate Post-build Notification + run: cc-test-reporter after-build + if: ${{ github.event_name != 'pull_request' }} + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Code Coverage Summary Report + uses: irongut/CodeCoverageSummary@v1.3.0 + if: ${{ github.event_name == 'pull_request' }} + with: + filename: ./coverage/coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '81 57' + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event_name == 'pull_request' }} + with: + recreate: true + path: code-coverage-results.md + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Upload results to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..0d4a013 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/heads.yml b/.github/workflows/heads.yml new file mode 100644 index 0000000..150222b --- /dev/null +++ b/.github/workflows/heads.yml @@ -0,0 +1,62 @@ +name: Ruby HEAD Support + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }}${{ matrix.name_extra || '' }} + runs-on: ubuntu-latest + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + experimental: [true] + rubygems: + - latest + bundler: + - latest + gemfile: + - vanilla + ruby: + - "ruby-head" + # NOTE: jruby-head is still @ Ruby 3.1 compat + - "jruby-head" + # NOTE: truffleruby-head is still @ Ruby 3.1 compat + - "truffleruby-head" + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rake test diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000..5ee257c --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,46 @@ +name: Ruby - Style + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +jobs: + rubocop: + name: RuboCop - Ruby ${{ matrix.ruby }}${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + strategy: + fail-fast: false + matrix: + experimental: [false] + rubygems: + - latest + bundler: + - latest + gemfile: + - style + ruby: + - "3.3" + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true + - name: Run RuboCop + run: bundle exec rake rubocop_gradual:check diff --git a/.github/workflows/supported.yml b/.github/workflows/supported.yml new file mode 100644 index 0000000..cbcb6ed --- /dev/null +++ b/.github/workflows/supported.yml @@ -0,0 +1,58 @@ +name: Supported Ruby Matrix + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }}${{ matrix.name_extra || '' }} + runs-on: ubuntu-latest + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + #- Ruby 3.3 tests are run by coverage.yml + - ruby: "3.2" + rubygems: latest + bundler: latest + gemfile: vanilla + - ruby: "3.1" + rubygems: latest + bundler: latest + gemfile: vanilla + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rake test diff --git a/.github/workflows/unsupported.yml b/.github/workflows/unsupported.yml new file mode 100644 index 0000000..b2b7e48 --- /dev/null +++ b/.github/workflows/unsupported.yml @@ -0,0 +1,51 @@ +name: Unsupported (EOL) Ruby Matrix + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }}${{ matrix.name_extra || '' }} + runs-on: ubuntu-20.04 + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + - ruby: "3.0" + rubygems: "3.3.27" + bundler: none + gemfile: vanilla + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rake test diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock new file mode 100644 index 0000000..74d0f9f --- /dev/null +++ b/.rubocop_gradual.lock @@ -0,0 +1,107 @@ +{ + "lib/rots/server_app.rb:4029472662": [ + [66, 7, 44, "Style/SafeNavigation: Use safe navigation (`&.`) instead of checking if an object exists before calling the method.", 3155555354] + ], + "rots.gemspec:1942548291": [ + [54, 3, 27, "Gemspec/DependencyVersion: Dependency version specification is required.", 3231645354], + [55, 3, 31, "Gemspec/DependencyVersion: Dependency version specification is required.", 2029420852], + [56, 3, 30, "Gemspec/DependencyVersion: Dependency version specification is required.", 2950165766], + [57, 3, 31, "Gemspec/DependencyVersion: Dependency version specification is required.", 856856768], + [62, 3, 31, "Gemspec/DependencyVersion: Dependency version specification is required.", 4231415917], + [64, 3, 30, "Gemspec/DependencyVersion: Dependency version specification is required.", 2088193405] + ], + "spec/rots/mocks/integration_spec.rb:2999786939": [ + [9, 16, 13, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 3245556507], + [66, 25, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [67, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [68, 25, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [69, 31, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [76, 25, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [77, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [78, 25, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [79, 31, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [86, 25, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [87, 28, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [88, 25, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [89, 31, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [92, 13, 23, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 3708407610], + [99, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [100, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [101, 35, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [102, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [109, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [110, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [111, 35, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [112, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [123, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [124, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [125, 35, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [126, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [127, 37, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [133, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [134, 41, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [140, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [141, 30, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [142, 35, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [143, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [144, 37, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [155, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [156, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [157, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [158, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [169, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [170, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [171, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [172, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [188, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [189, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [190, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [191, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [205, 20, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [212, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [213, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [214, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [215, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [230, 20, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [237, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [238, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [239, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [240, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [250, 20, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [256, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [264, 20, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [270, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [281, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [282, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [283, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [284, 32, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [294, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [295, 29, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [296, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [297, 33, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [306, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [307, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [308, 26, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [316, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [328, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [329, 54, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [331, 18, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [334, 27, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [335, 31, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526], + [344, 59, 9, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 570307526] + ], + "spec/server_app_spec.rb:3966116987": [ + [1, 1, 30, "RSpec/SpecFilePathFormat: Spec path should end with `rots/server_app*_spec.rb`.", 3141558351], + [3, 5, 64, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 3037133032], + [4, 39, 15, "RSpec/DescribedClass: Use `described_class` instead of `Rots::ServerApp`.", 1679027035], + [16, 40, 15, "RSpec/DescribedClass: Use `described_class` instead of `Rots::ServerApp`.", 1679027035], + [35, 38, 8, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 1967475860], + [42, 11, 49, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 407156958], + [43, 42, 8, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 1967475860], + [54, 38, 8, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 1967475860], + [62, 38, 8, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 1967475860], + [70, 38, 8, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 1967475860], + [75, 11, 32, "RSpec/MultipleExpectations: Example has too many expectations [5/1].", 4289101259], + [76, 38, 8, "RSpec/InstanceVariable: Avoid instance variables - use let, a method call, or a local variable (if possible).", 1967475860] + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c773e79 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog +All notable changes to this project will be documented in this file. + +Since version 3.0.0, the format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added +### Changed +### Fixed +### Removed + +## 1.0.0 - 2024-09-24 +COVERAGE: 96.33% -- 236/245 lines in 11 files +BRANCH COVERAGE: 77.78% -- 35/45 branches in 11 files +37.84% documented +### Fixed +- More tests & switch to RSpec +- Modernized code (require_relative) +- Refactored +- GitHub Actions for CI diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 0000000..df27f90 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +## Contributing + +Bug reports and pull requests are welcome on GitHub at [https://github.com/oauth-xx/rots][🚎src-main] +. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to +the [code of conduct][🤝conduct]. + +To submit a patch, please fork the project and create a patch with tests. +Once you're happy with it send a pull request. + +## Release + +### One-time, Per-developer, Setup + +**IMPORTANT**: Your public key for signing gems will need to be picked up by the line in the +`gemspec` defining the `spec.cert_chain` (check the relevant ENV variables there), +in order to sign the new release. +See: [RubyGems Security Guide][🔒️rubygems-security-guide] + +### To release a new version: + +1. Run `bin/setup && bin/rake` as a tests, coverage, & linting sanity check +2. Update the version number in `version.rb` +3. Run `bin/setup && bin/rake` again as a secondary check, and to update `Gemfile.lock` +4. Run `git commit -am "🔖 Prepare release v"` to commit the changes +5. Run `git push` to trigger the final CI pipeline before release, & merge PRs + - NOTE: Remember to [check the build][🧪build]! +6. Run `export GIT_TRUNK_BRANCH_NAME="$(git remote show origin | grep 'HEAD branch' | cut -d ' ' -f5)" && echo $GIT_TRUNK_BRANCH_NAME` +7. Run `git checkout $GIT_TRUNK_BRANCH_NAME` +8. Run `git pull origin $GIT_TRUNK_BRANCH_NAME` to ensure you will release the latest trunk code +9. Set `SOURCE_DATE_EPOCH` so `rake build` and `rake release` use same timestamp, and generate same checksums + - Run `export SOURCE_DATE_EPOCH=$EPOCHSECONDS && echo $SOURCE_DATE_EPOCH` + - If the echo above has no output, then it didn't work. + - Note that you'll need the `zsh/datetime` module, if running `zsh`. + - In `bash` you can use `date +%s` instead, i.e. `export SOURCE_DATE_EPOCH=$(date +%s) && echo $SOURCE_DATE_EPOCH` +10. Run `bundle exec rake build` +11. Run `bin/checksums` (more [context][🔒️rubygems-checksums-pr]) to create SHA-256 and SHA-512 checksums + - Checksums will be committed automatically by the script, but not pushed +12. 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][💎rubygems] + +## Contributors + +[![Contributors][🖐contributors-img]][🖐contributors] + +Made with [contributors-img][🖐contrib-rocks]. + +[🧪build]: https://github.com/oauth-xx/rots/actions +[🤝conduct]: https://github.com/oauth-xx/rots/blob/main/CODE_OF_CONDUCT.md +[🖐contrib-rocks]: https://contrib.rocks +[🖐contributors]: https://github.com/oauth-xx/rots/graphs/contributors +[🖐contributors-img]: https://contrib.rocks/image?repo=oauth-xx/rots +[💎rubygems]: https://rubygems.org +[🔒️rubygems-security-guide]: https://guides.rubygems.org/security/#building-gems +[🔒️rubygems-checksums-pr]: https://github.com/rubygems/guides/pull/325 +[🚎src-main]: https://github.com/oauth-xx/rots diff --git a/Gemfile b/Gemfile index e72315e..e34022d 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,9 @@ source "https://rubygems.org" # Specify your gem's dependencies in ranked-model.gemspec gemspec + +gem "byebug", ">= 2.0.3" +gem "rack-openid2" # , path: "/Users/pboling/src/forks/rack-openid" +gem "ruby-openid2" # , path: "/Users/pboling/src/forks/ruby-openid" +gem "minitest" +gem "rack-test" diff --git a/Gemfile.lock b/Gemfile.lock index 8088da0..574717f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - rots (0.2.2) + rots (1.0.0) date net-http openssl @@ -9,31 +9,70 @@ PATH psych (~> 5.1) rack (>= 2) rackup (>= 2) - ruby-openid2 (~> 3.0) + ruby-openid2 (~> 3.0, >= 3.0.3) stringio + version_gem (~> 1.1, >= 1.1.4) webrick yaml (~> 0.3) GEM remote: https://rubygems.org/ specs: + ansi (1.5.0) + ast (2.4.2) + backports (3.25.0) + byebug (11.1.3) date (3.3.4) diff-lcs (1.5.1) + diffy (3.4.2) + docile (1.4.1) + json (2.7.2) + kettle-soup-cover (1.0.4) + simplecov (~> 0.22) + simplecov-cobertura (~> 2.1) + simplecov-console (~> 0.9, >= 0.9.1) + simplecov-html (~> 0.12) + simplecov-lcov (~> 0.8) + simplecov-rcov (~> 0.3, >= 0.3.3) + simplecov_json_formatter (~> 0.1, >= 0.1.4) + version_gem (~> 1.1, >= 1.1.4) + language_server-protocol (3.17.0.3) + lint_roller (1.1.0) + logger (1.6.1) + minitest (5.25.1) net-http (0.4.1) uri openssl (3.2.0) optparse (0.5.0) + ostruct (0.6.0) + parallel (1.26.3) + parser (3.3.5.0) + ast (~> 2.4.1) + racc psych (5.1.2) stringio + racc (1.8.1) rack (3.1.7) + rack-openid2 (2.0.1) + rack (>= 2.2) + ruby-openid2 (>= 3.0) + version_gem (~> 1.1, >= 1.1.4) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) rackup (2.1.0) rack (>= 3) webrick (~> 1.8) + rainbow (3.1.1) rake (13.2.1) + regexp_parser (2.9.2) + rexml (3.3.7) rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) + rspec-block_is_expected (1.0.6) rspec-core (3.13.1) rspec-support (~> 3.13.0) rspec-expectations (3.13.3) @@ -43,21 +82,129 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.1) - ruby-openid2 (3.0.0) + rubocop (1.65.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.4, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.31.1, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.32.3) + parser (>= 3.3.1.0) + rubocop-gradual (0.3.6) + diff-lcs (>= 1.2.0, < 2.0) + diffy (~> 3.0) + parallel (~> 1.10) + rainbow (>= 2.2.2, < 4.0) + rubocop (~> 1.0) + rubocop-lts (10.1.1) + rubocop-ruby2_3 (>= 2.0.3, < 3) + standard-rubocop-lts (>= 1.0.3, < 3) + version_gem (>= 1.1.2, < 3) + rubocop-md (1.2.3) + rubocop (>= 1.45) + rubocop-packaging (0.5.2) + rubocop (>= 1.33, < 2.0) + rubocop-performance (1.21.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) + rubocop-rspec (3.0.5) + rubocop (~> 1.61) + rubocop-ruby2_3 (2.0.5) + rubocop-gradual (~> 0.3, >= 0.3.1) + rubocop-md (~> 1.2) + rubocop-rake (~> 0.6) + rubocop-shopify (~> 2.14) + rubocop-thread_safety (~> 0.5, >= 0.5.1) + standard-rubocop-lts (~> 1.0, >= 1.0.7) + version_gem (>= 1.1.3, < 3) + rubocop-shopify (2.15.1) + rubocop (~> 1.51) + rubocop-thread_safety (0.5.1) + rubocop (>= 0.90.0) + ruby-openid2 (3.0.3) + logger (~> 1.6, >= 1.6.1) + net-http (~> 0.4, >= 0.4.1) + rexml (~> 3.3, >= 3.3.7) version_gem (~> 1.1, >= 1.1.4) + ruby-progressbar (1.13.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-cobertura (2.1.0) + rexml + simplecov (~> 0.19) + simplecov-console (0.9.2) + ansi + simplecov + terminal-table + simplecov-html (0.13.1) + simplecov-lcov (0.8.0) + simplecov-rcov (0.3.7) + simplecov (>= 0.4.1) + simplecov_json_formatter (0.1.4) + standard (1.40.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.65.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.4) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.4.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.21.0) + standard-rubocop-lts (1.0.10) + rspec-block_is_expected (~> 1.0, >= 1.0.5) + standard (>= 1.35.1, < 2) + standard-custom (>= 1.0.2, < 2) + standard-performance (>= 1.3.1, < 2) + version_gem (>= 1.1.4, < 3) stringio (3.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) uri (0.13.1) version_gem (1.1.4) - webrick (1.8.1) + webrick (1.8.2) yaml (0.3.0) + yard (0.9.37) + yard-junk (0.0.10) + backports (>= 3.18) + ostruct + rainbow + yard PLATFORMS + arm64-darwin-23 ruby DEPENDENCIES - rake (>= 13) + byebug (>= 2.0.3) + kettle-soup-cover (~> 1.0, >= 1.0.2) + minitest + rack-openid2 + rack-session (>= 1) + rack-test + rake (>= 10) rots! rspec (~> 3.13) + rspec-block_is_expected (~> 1.0, >= 1.0.6) + rubocop-lts (~> 10.1) + rubocop-packaging (~> 0.5, >= 0.5.2) + rubocop-rspec (~> 3.0) + ruby-openid2 + standard (~> 1.40) + yard (~> 0.9, >= 0.9.34) + yard-junk (~> 0.0.10) BUNDLED WITH - 2.4.22 + 2.5.18 diff --git a/README b/README deleted file mode 100644 index 5369cc6..0000000 --- a/README +++ /dev/null @@ -1,64 +0,0 @@ -= Ruby OpenID Test Server (ROTS), a dummy OpenID server that makes consumer tests dead easy. - -ROTS is a minimal implementation of an OpenID server, developed on top of the Rack middleware, this -server provides an easy to use interface to make testing OpenID consumers really easy. - -== No more mocks - -Have you always wanted to test the authentication of an OpenID consumer implementation, but find your self -in a point where is to hard to mock? A lot of people have been there. - -With ROTS, you only need to specify an identity url provided by the dummy server, passing with it a flag -saying that you want the authentication to be successful. It handles SREG extensions as well. - -== How does it works - -When you install the ROTS gem, a binary called rots is provided for starting the server (for more -info about what options you have when executing this file, check the -h option). - -By default, rots will have a test user called "John Doe", with an OpenID identity "john.doe". -If you want to use your own test user name, you can specify a config file to rots. The -default configuration file looks like this: - -# Default configuration file -identity: john.doe -sreg: - nickname: jdoe - fullname: John Doe - email: jhon@doe.com - dob: 1985-09-21 - gender: M - -You can specify a new config file using the option --config. - -== Getting Started - -The best way to get started, is running the rots server, and then starting to execute your OpenID consumer tests/specs. You just have to specify the identity url of your test user, if you want the OpenID response be successful just add the openid.success=true flag to the user identity url. If you don't specify the flag it -will return a cancel response instead. - -Example: - -it "should authenticate with OpenID" do - post("/consumer_openid_login", 'identity_url' => 'http://localhost:1132/john.doe?openid.success=true') -end - -== Copyright - -Copyright (C) 2009 Roman Gonzalez - -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 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 new file mode 100644 index 0000000..5526969 --- /dev/null +++ b/README.md @@ -0,0 +1,248 @@ +# ROTS - Ruby OpenID Test Server + +
+ +
+ +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) +[![Version](https://img.shields.io/gem/v/rots.svg)](https://rubygems.org/gems/rots) +[![Downloads Today](https://img.shields.io/gem/rd/rots.svg)](https://github.com/oauth-xx/rots) +[![CodeCov][🖇codecov-img♻️]][🖇codecov] +[![CI Supported Build][🚎s-wfi]][🚎s-wf] +[![CI Unsupported Build][🚎us-wfi]][🚎us-wf] +[![CI Style Build][🚎st-wfi]][🚎st-wf] +[![CI Coverage Build][🚎cov-wfi]][🚎cov-wf] +[![CI Heads Build][🚎hd-wfi]][🚎hd-wf] +[![CI Ancient Build][🚎an-wfi]][🚎an-wf] + +[🖇codecov-img♻️]: https://codecov.io/gh/oauth-xx/rots/graph/badge.svg?token=selEoMrZzA +[🖇codecov]: https://codecov.io/gh/oauth-xx/rots +[🚎s-wf]: https://github.com/oauth-xx/rots/actions/workflows/supported.yml +[🚎s-wfi]: https://github.com/oauth-xx/rots/actions/workflows/supported.yml/badge.svg +[🚎us-wf]: https://github.com/oauth-xx/rots/actions/workflows/unsupported.yml +[🚎us-wfi]: https://github.com/oauth-xx/rots/actions/workflows/unsupported.yml/badge.svg +[🚎st-wf]: https://github.com/oauth-xx/rots/actions/workflows/style.yml +[🚎st-wfi]: https://github.com/oauth-xx/rots/actions/workflows/style.yml/badge.svg +[🚎cov-wf]: https://github.com/oauth-xx/rots/actions/workflows/coverage.yml +[🚎cov-wfi]: https://github.com/oauth-xx/rots/actions/workflows/coverage.yml/badge.svg +[🚎hd-wf]: https://github.com/oauth-xx/rots/actions/workflows/heads.yml +[🚎hd-wfi]: https://github.com/oauth-xx/rots/actions/workflows/heads.yml/badge.svg +[🚎an-wf]: https://github.com/oauth-xx/rots/actions/workflows/ancient.yml +[🚎an-wfi]: https://github.com/oauth-xx/rots/actions/workflows/ancient.yml/badge.svg + +
+ +----- + +
+ +[![Liberapay Patrons][⛳liberapay-img]][⛳liberapay] +[![Sponsor Me on Github][🖇sponsor-img]][🖇sponsor] +[![Polar Shield][🖇polar-img]][🖇polar] +[![Donate to my FLOSS or refugee efforts at ko-fi.com][🖇kofi-img]][🖇kofi] +[![Donate to my FLOSS or refugee efforts using Patreon][🖇patreon-img]][🖇patreon] + +[⛳liberapay-img]: https://img.shields.io/liberapay/patrons/pboling.svg?logo=liberapay +[⛳liberapay]: https://liberapay.com/pboling/donate +[🖇sponsor-img]: https://img.shields.io/badge/Sponsor_Me!-pboling.svg?style=social&logo=github +[🖇sponsor]: https://github.com/sponsors/pboling +[🖇polar-img]: https://polar.sh/embed/seeks-funding-shield.svg?org=pboling +[🖇polar]: https://polar.sh/pboling +[🖇kofi-img]: https://img.shields.io/badge/buy%20me%20coffee-donate-yellow.svg +[🖇kofi]: https://ko-fi.com/O5O86SNP4 +[🖇patreon-img]: https://img.shields.io/badge/patreon-donate-yellow.svg +[🖇patreon]: https://patreon.com/galtzo + + + + + +
+
+ +Ruby OpenID Test Server (ROTS) is a dummy OpenID server that makes consumer tests dead easy. + +ROTS is a minimal implementation of an OpenID server, developed on top of the Rack middleware, this +server provides an easy to use interface to make testing OpenID consumers really easy. + +## No more mocks + +Have you always wanted to test the authentication of an OpenID consumer implementation, but find your self +in a point where is to hard to mock? A lot of people have been there. + +With ROTS, you only need to specify an identity url provided by the dummy server, passing with it a flag +saying that you want the authentication to be successful. It handles SREG extensions as well. + +### Or do use mocks, maybe? + +You can also require a library of mocks and request helpers which might be useful in your tests (perhaps in your `spec_helper.rb`): + +```ruby +require "rots/mocks" +require "rots/test" + +OpenID.fetcher = Rots::Mocks::Fetcher.new(Rots::Mocks::RotsServer.new) + +RSpec.configure do |config| + config.include(Rots::Test::RackTestHelpers) +end +``` + +Helpers are written with minitest syntax, +but RSpec supports that, so it should work in both RSpec and MiniTest, +and you don't need to switch your other tests to use minitest syntax. +Use them interchangeably, as needed. +```ruby +RSpec.configure do |config| + config.expect_with(:rspec, :minitest) +end +``` + +And in another file (see `spec/rots/mocks/integration_spec.rb` for more): + +```ruby +RSpec.describe("openid") do + subject(:app) { Rots::Mocks::ClientApp.new(**options) } + + let(:options) { {identifier: "#{Rots::Mocks::RotsServer::SERVER_URL}/john.doe?openid.success=true"} } + + it "with_get" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end +end +``` + +## How does it work? + +When you install the ROTS gem, a binary called rots is provided for starting the server (for more +info about what options you have when executing this file, check the -h option). + +By default, rots will have a test user called "John Doe", with an OpenID identity "john.doe". +If you want to use your own test user name, you can specify a config file to rots. The +default configuration file looks like this: + +### Default configuration file +```yaml +identity: john.doe +sreg: + nickname: jdoe + fullname: John Doe + email: jhon@doe.com + dob: 1985-09-21 + gender: M +``` + +You can specify a new config file using the option `--config`. + +## Getting Started + +The best way to get started, is running the rots server, and then starting to execute your OpenID consumer tests/specs. You just have to specify the identity url of your test user, if you want the OpenID response be successful just add the openid.success=true flag to the user identity url. If you don't specify the flag it +will return a cancel response instead. + +Example: +```ruby +it "should authenticate with OpenID" do + post("/consumer_openid_login", "identity_url" => "http://localhost:1132/john.doe?openid.success=true") +end +``` + +## 🤝 Contributing + +See [CONTRIBUTING.md][🤝contributing] + +[🤝contributing]: CONTRIBUTING.md + +### Code Coverage + +If you need some ideas of where to help, you could work on adding more code coverage. + +[![Coverage Graph][🔑codecov-g]][🖇codecov] + +[🔑codecov-g]: https://codecov.io/gh/oauth-xx/rots/graphs/tree.svg?token=selEoMrZzA + +## 🌈 Contributors + +[![Contributors][🖐contributors-img]][🖐contributors] + +Made with [contributors-img][🖐contrib-rocks]. + +[🖐contrib-rocks]: https://contrib.rocks +[🖐contributors]: https://github.com/oauth-xx/rots/graphs/contributors +[🖐contributors-img]: https://contrib.rocks/image?repo=oauth-xx/rots + +## Star History + + + + + + Star History Chart + + + +## 🪇 Code of Conduct + +Everyone interacting in this project's codebases, issue trackers, +chat rooms and mailing lists is expected to follow the [code of conduct][🪇conduct]. + +[🪇conduct]: CODE_OF_CONDUCT.md + +## 📌 Versioning + +This Library adheres to [Semantic Versioning 2.0.0][📌semver]. +Violations of this scheme should be reported as bugs. +Specifically, if a minor or patch version is released that breaks backward compatibility, +a new version should be immediately released that restores compatibility. +Breaking changes to the public API will only be introduced with new major versions. + +To get a better understanding of how SemVer is intended to work over a project's lifetime, +read this article from the creator of SemVer: + +- ["Major Version Numbers are Not Sacred"][📌major-versions-not-sacred] + +As a result of this policy, you can (and should) specify a dependency on these libraries using +the [Pessimistic Version Constraint][📌pvc] with two digits of precision. + +For example: + +```ruby +spec.add_dependency("rots", "~> 1.0") +``` + +See [CHANGELOG.md][📌changelog] for list of releases. + +[comment]: <> ( 📌 VERSIONING LINKS ) + +[📌pvc]: http://guides.rubygems.org/patterns/#pessimistic-version-constraint +[📌semver]: http://semver.org/ +[📌major-versions-not-sacred]: https://tom.preston-werner.com/2022/05/23/major-version-numbers-are-not-sacred.html +[📌changelog]: CHANGELOG.md + +## 📄 License + +The gem is available as open source under the terms of +the [MIT License][📄license] [![License: MIT][📄license-img]][📄license-ref]. +See [LICENSE.txt][📄license] for the official [Copyright Notice][📄copyright-notice-explainer]. + +[comment]: <> ( 📄 LEGAL LINKS ) + +[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year +[📄license]: LICENSE.txt +[📄license-ref]: https://opensource.org/licenses/MIT +[📄license-img]: https://img.shields.io/badge/License-MIT-green.svg + +### © Copyright + +* Copyright (c) 2013 - 2014, 2016 - 2020, 2023 - 2024 [Peter H. Boling][peterboling] of [Rails Bling][railsbling] + +[railsbling]: http://www.railsbling.com +[peterboling]: http://www.peterboling.com +[bundle-group-pattern]: https://gist.github.com/pboling/4564780 +[documentation]: http://rdoc.info/github/oauth-xx/rots/frames +[homepage]: https://github.com/oauth-xx/rots diff --git a/Rakefile b/Rakefile index 9be7e5a..5ceef63 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,43 @@ -require 'bundler' -Bundler::GemHelper.install_tasks +require "bundler/gem_tasks" -require 'rspec/core/rake_task' -RSpec::Core::RakeTask.new('spec') +begin + require "rspec/core/rake_task" + RSpec::Core::RakeTask.new(:spec) + desc("alias test task to spec") + task(test: :spec) +rescue LoadError + task(:spec) do + warn("rspec is disabled") + end +end -task :default => :spec +begin + require "yard-junk/rake" + + YardJunk::Rake.define_task +rescue LoadError + task("yard:junk") do + warn("yard:junk is disabled") + end +end + +begin + require "yard" + + YARD::Rake::YardocTask.new(:yard) +rescue LoadError + task(:yard) do + warn("yard is disabled") + end +end + +begin + require "rubocop/lts" + Rubocop::Lts.install_tasks +rescue LoadError + task(:rubocop_gradual) do + warn("RuboCop (Gradual) is disabled") + end +end + +task default: %i[spec rubocop_gradual yard yard:junk] diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..5fa2fb6 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,118 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__) + end + + def env_var_version + ENV.fetch("BUNDLER_VERSION", nil) + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/o + + bundler_version = Regexp.last_match(1) + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV.fetch("BUNDLE_GEMFILE", nil) + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/o + + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= # rubocop:disable ThreadSafety/InstanceVariableInClassMethod + env_var_version || cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement if Gem.rubygems_version >= Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem("bundler", bundler_requirement) + end + return if gem_error.nil? + + require_error = activation_error_handling do + require "bundler/version" + end + if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + return + end + + warn("Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`") + exit(42) + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +load Gem.bin_path("bundler", "bundle") if m.invoked_as_script? diff --git a/bin/checksums b/bin/checksums new file mode 100755 index 0000000..5498152 --- /dev/null +++ b/bin/checksums @@ -0,0 +1,67 @@ +#!/usr/bin/env ruby + +# Script from https://github.com/rubygems/guides/pull/325 +require "digest/sha2" + +# Final clause of Regex `(?=\.gem)` is a positive lookahead assertion +# See: https://learnbyexample.github.io/Ruby_Regexp/lookarounds.html#positive-lookarounds +# Used to pattern match against a gem package name, which always ends with .gem. +# The positive lookahead ensures it is present, and prevents it from being captured. +VERSION_REGEX = /((\d+\.\d+\.\d+)([-.][0-9A-Za-z-]+)*)(?=\.gem)/ + +gem_path_parts = ARGV.first&.split("/") + +if gem_path_parts&.any? + gem_name = gem_path_parts.last + gem_pkg = File.join(gem_path_parts) + puts "Looking for: #{gem_pkg.inspect}" + gems = Dir[gem_pkg] + puts "Found: #{gems.inspect}" +else + gem_pkgs = File.join("pkg", "*.gem") + puts "Looking for: #{gem_pkgs.inspect}" + gems = Dir[gem_pkgs] + raise "Unable to find gems #{gem_pkgs}" if gems.empty? + + # Sort by newest last + # [ "my_gem-2.3.9.gem", "my_gem-2.3.11.pre.alpha.4.gem", "my_gem-2.3.15.gem", ... ] + gems.sort_by! { |gem| Gem::Version.new(gem[VERSION_REGEX]) } + gem_pkg = gems.last + gem_path_parts = gem_pkg.split("/") + gem_name = gem_path_parts.last + puts "Found: #{gems.length} gems; latest is #{gem_name}" +end + +checksum512 = Digest::SHA512.new.hexdigest(File.read(gem_pkg)) +checksum512_path = "checksums/#{gem_name}.sha512" +File.write(checksum512_path, checksum512) + +checksum256 = Digest::SHA256.new.hexdigest(File.read(gem_pkg)) +checksum256_path = "checksums/#{gem_name}.sha256" +File.write(checksum256_path, checksum256) + +version = gem_name[VERSION_REGEX] + +git_cmd = <<~GIT_MSG + git add checksums/* && \ + git commit -m "🔒️ Checksums for v#{version}" +GIT_MSG + +puts <<~RESULTS + [ GEM: #{gem_name} ] + [ VERSION: #{version} ] + [ GEM PKG LOCATION: #{gem_pkg} ] + [ CHECKSUM SHA-256: #{checksum256} ] + [ CHECKSUM SHA-512: #{checksum512} ] + [ CHECKSUM SHA-256 PATH: #{checksum256_path} ] + [ CHECKSUM SHA-512 PATH: #{checksum512_path} ] + + ... Running ... + + #{git_cmd} +RESULTS + +# This will replace the current process with the git process, and exit. +# Any command placed after this will not be run: +# See: https://www.akshaykhot.com/call-shell-commands-in-ruby +exec(git_cmd) diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..f3ba582 --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "rots" + +# 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/bin/rake b/bin/rake new file mode 100755 index 0000000..4eb7d7b --- /dev/null +++ b/bin/rake @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rake' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rake", "rake") diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 0000000..cb53ebe --- /dev/null +++ b/bin/rspec @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rspec' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000..369a05b --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rubocop' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/exe/rots b/exe/rots index 61fe581..1207b63 100755 --- a/exe/rots +++ b/exe/rots @@ -4,62 +4,73 @@ require "rots" server_options = { - :debugger => false, - :port => 1123, - :verbose => true, - :storage => File.join('.', 'tmp', 'rots'), - :config => <<-DEFAULT_CONFIG -# Default configuration file -identity: john.doe -sreg: - nickname: jdoe - fullname: John Doe - email: jhon@doe.com - dob: 1985-09-21 - gender: M - + debugger: false, + port: 1123, + verbose: true, + storage: File.join(".", "tmp", "rots"), + config: <<~DEFAULT_CONFIG, + # Default configuration file + identity: john.doe + sreg: + nickname: jdoe + fullname: John Doe + email: jhon@doe.com + dob: 1985-09-21 + gender: M + DEFAULT_CONFIG } opts = OptionParser.new do |opts| opts.banner = "Usage: rots [options]" - - opts.separator "" - opts.separator "Options:" - - opts.on("-p", "--port PORT", - "use PORT (default: 1123)") do |port| + + opts.separator("") + opts.separator("Options:") + + opts.on( + "-p", + "--port PORT", + "use PORT (default: 1123)", + ) do |port| server_options[:port] = port end - - opts.on("-s", "--storage PATH", - "use PATH as the OpenID Server storage path (default: ./tmp/rots)") do |storage_path| + + opts.on( + "-s", + "--storage PATH", + "use PATH as the OpenID Server storage path (default: ./tmp/rots)", + ) do |storage_path| server_options[:storage] = storage_path end - - opts.on("-c", "--config FILE.yaml", - "server configuration YAML file") do |config_path| - abort "\x1B[31mConfiguration file #{config_path} not found\x1B[0m" unless File.exists?(config_path) + + opts.on( + "-c", + "--config FILE.yaml", + "server configuration YAML file", + ) do |config_path| + abort("\x1B[31mConfiguration file #{config_path} not found\x1B[0m") unless File.exist?(config_path) server_options[:config] = File.new(config_path) end - - opts.on("-s", "--silent", - "If specified, the server will be in silent mode") do + + opts.on( + "-s", + "--silent", + "If specified, the server will be in silent mode", + ) do server_options[:verbose] = false end - + opts.on("-d", "--debugger") do server_options[:debugger] = true end - - opts.separator "" - opts.separator "Common options:" - + + opts.separator("") + opts.separator("Common options:") + opts.on_tail("-h", "--help", "Show this help message") do puts opts exit end - end opts.parse!(ARGV) @@ -68,21 +79,20 @@ config = YAML.load(server_options[:config], permitted_classes: [Date]) require "ruby-debug" if server_options[:debugger] -server = Rack::Builder.new do - use Rack::Lint +server = Rack::Builder.new do + use(Rack::Lint) if server_options[:verbose] - use Rack::CommonLogger, STDOUT - use Rack::ShowExceptions + use(Rack::CommonLogger, $stdout) + use(Rack::ShowExceptions) end - map ("/%s" % config['identity']) do - run Rots::IdentityPageApp.new(config, server_options) + map("/%s" % config["identity"]) do + run(Rots::IdentityPageApp.new(config, server_options)) end - map "/server" do - run Rots::ServerApp.new(config, server_options) + map("/server") do + run(Rots::ServerApp.new(config, server_options)) end end puts "\x1B[32mRunning Ruby OpenID Test Server (ROTS) on port #{server_options[:port]}\x1B[0m" if server_options[:verbose] -Rackup::Server.start app: server, Port: server_options[:port] - +Rackup::Server.start(app: server, Port: server_options[:port]) diff --git a/gemfiles/ancient.gemfile b/gemfiles/ancient.gemfile new file mode 100644 index 0000000..09aafb9 --- /dev/null +++ b/gemfiles/ancient.gemfile @@ -0,0 +1,27 @@ +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Root Gemfile is only for local development only. It is not loaded on CI. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/*.gemfile + +# In the ancient gemfile we also do not load the gemspec's dependencies +# because they target Ruby 2.7+ +# Thus we load the runtime dependencies of the gem here. +gem "version_gem", "~> 1.1", ">= 1.1.4" +gem "rack-openid2", ">= 2" +gem "rack-session", ">= 1" + +gem "minitest", ">= 5", "< 6" +gem "rake" +gem "rspec" +gem "rspec-block_is_expected" + +# For debugging, casecmp is only available in Ruby 2.4+ +if RUBY_VERSION > "2.4" && ENV.fetch("DEBUG", "false").casecmp?("true") + gem "byebug" +end + +# this gem +gem "rots", path: "../" diff --git a/gemfiles/coverage.gemfile b/gemfiles/coverage.gemfile new file mode 100644 index 0000000..b45c4e8 --- /dev/null +++ b/gemfiles/coverage.gemfile @@ -0,0 +1,9 @@ +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Root Gemfile is only for local development only. It is not loaded on CI. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/*.gemfile + +gemspec path: "../" diff --git a/gemfiles/style.gemfile b/gemfiles/style.gemfile new file mode 100644 index 0000000..b45c4e8 --- /dev/null +++ b/gemfiles/style.gemfile @@ -0,0 +1,9 @@ +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Root Gemfile is only for local development only. It is not loaded on CI. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/*.gemfile + +gemspec path: "../" diff --git a/gemfiles/vanilla.gemfile b/gemfiles/vanilla.gemfile new file mode 100644 index 0000000..b45c4e8 --- /dev/null +++ b/gemfiles/vanilla.gemfile @@ -0,0 +1,9 @@ +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Root Gemfile is only for local development only. It is not loaded on CI. +# On CI we only need the gemspecs' dependencies (including development dependencies). +# Exceptions, if any, will be found in gemfiles/*.gemfile + +gemspec path: "../" diff --git a/lib/rots.rb b/lib/rots.rb index 7792edd..09a6aef 100644 --- a/lib/rots.rb +++ b/lib/rots.rb @@ -17,7 +17,6 @@ require_relative "rots/version" require_relative "rots/server_app" require_relative "rots/identity_page_app" -require_relative "rots/test_helper" # Namespace for this gem module Rots diff --git a/lib/rots/identity_page_app.rb b/lib/rots/identity_page_app.rb index bf07a5c..106e1af 100644 --- a/lib/rots/identity_page_app.rb +++ b/lib/rots/identity_page_app.rb @@ -1,37 +1,37 @@ # external libraries -require 'rack/request' -require 'rack/response' -require 'rack/utils' -require 'openid' +require "rack/request" +require "rack/response" +require "rack/utils" +require "openid" -class Rots::IdentityPageApp - +class Rots::IdentityPageApp def initialize(config, server_options) @server_options = server_options @config = config end - + def call(env) @request = Rack::Request.new(env) Rack::Response.new do |response| - response.write <<-HERE - - - - - - -

This is #{@config['identity']} identity page

- - + response.write(<<~HERE) + + + + + + +

This is #{@config["identity"]} identity page

+ + HERE end.finish end - + def op_endpoint - "http://%s:%d/server/%s" % [@request.host, - @request.port, - (@request.params['openid.success'] ? '?openid.success=true' : '')] + "http://%s:%d/server/%s" % [ + @request.host, + @request.port, + (@request.params["openid.success"] ? "?openid.success=true" : ""), + ] end - end diff --git a/lib/rots/mocks.rb b/lib/rots/mocks.rb new file mode 100644 index 0000000..04d4081 --- /dev/null +++ b/lib/rots/mocks.rb @@ -0,0 +1,8 @@ +# Not in the runtime dependencies. +# If you use the mocks, you need to add `rack-openid2` to your Gemfile. +require "rack/openid" # rack-openid2 + +# this gem's test support files: +require_relative "mocks/mock_fetcher" +require_relative "mocks/rots_server" +require_relative "mocks/client_app" diff --git a/lib/rots/mocks/client_app.rb b/lib/rots/mocks/client_app.rb new file mode 100644 index 0000000..5cf3be1 --- /dev/null +++ b/lib/rots/mocks/client_app.rb @@ -0,0 +1,53 @@ +require "rack/openid" # rack-openid2 +require "rack/session" + +module Rots + module Mocks + class ClientApp + extend Forwardable + + attr_reader :app, :options + + def_delegator :@app, :call + + def initialize(**options) + @options = options.dup + + @options[:identifier] ||= "#{Rots::Mocks::RotsServer::SERVER_URL}/john.doe?openid.success=true" + + @app = Rack::Session::Pool.new(Rack::OpenID.new(rack_app)) + end + + private + + def rack_app + # block passed to new is evaluated with instance_eval, + # which searches `binding` for local variables, + # while `self` is searched for instance variables and methods. + # A local pointer in `binding` to @options makes it accessible. + options = @options + lambda { |env| + if (resp = env[Rack::OpenID::RESPONSE]) + headers = { + "X-Path" => env["PATH_INFO"], + "X-Method" => env["REQUEST_METHOD"], + "X-Query-String" => env["QUERY_STRING"], + } + if resp.status == :success + [200, headers, [resp.status.to_s]] + elsif resp.status == :setup_needed + headers["Location"] = Rots::Mocks::RotsServer::SERVER_URL # TODO update Rots to properly send user_setup_url. This should come from resp. + [307, headers, [resp.status.to_s]] + else + [400, headers, [resp.status.to_s]] + end + elsif env["MOCK_HTTP_BASIC_AUTH"] + [401, {Rack::OpenID::AUTHENTICATE_HEADER => 'Realm="Example"'}, []] + else + [401, {Rack::OpenID::AUTHENTICATE_HEADER => Rack::OpenID.build_header(options)}, []] + end + } + end + end + end +end diff --git a/lib/rots/mocks/mock_fetcher.rb b/lib/rots/mocks/mock_fetcher.rb new file mode 100644 index 0000000..bd335d0 --- /dev/null +++ b/lib/rots/mocks/mock_fetcher.rb @@ -0,0 +1,34 @@ +require "openid" # ruby-openid2 +require "rack/utils" +require "net/http" # stdlib in Ruby < 3, gem after +require "net/protocol" + +module Rots + module Mocks + class Fetcher + def initialize(app) + @app = app + end + + def fetch(url, body = nil, headers = nil, limit = nil) + opts = (headers || {}).dup + opts[:input] = body + opts[:method] = "POST" if body + env = Rack::MockRequest.env_for(url, opts) + + status, headers, body = @app.call(env) + + buf = [] + buf << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}" + headers.each { |header, value| buf << "#{header}: #{value}" } + buf << "" + body.each { |part| buf << part } + + io = Net::BufferedIO.new(StringIO.new(buf.join("\n"))) + res = Net::HTTPResponse.read_new(io) + res.reading_body(io, true) {} + OpenID::HTTPResponse._from_net_response(res, url) + end + end + end +end diff --git a/lib/rots/mocks/rots_server.rb b/lib/rots/mocks/rots_server.rb new file mode 100644 index 0000000..d1f8e70 --- /dev/null +++ b/lib/rots/mocks/rots_server.rb @@ -0,0 +1,49 @@ +module Rots + module Mocks + class RotsServer + extend Forwardable + SERVER_URL = "http://localhost:9292" + DEFAULT_CONFIG = { + "identity" => "john.doe", + "sreg" => { + "nickname" => "jdoe", + "fullname" => "John Doe", + "email" => "jhon@doe.com", + "dob" => Date.parse("1985-09-21"), + "gender" => "M", + }.freeze, + }.freeze + + attr_reader :app, :config + + def_delegator :@app, :call + + # @param config [Hash, nil] - the configuration of the app's authorizable identity + def initialize(config = nil) + @config ||= DEFAULT_CONFIG + raise ArgumentError, "config must be a Hash" unless self.config.is_a?(Hash) + + @app = rack_app + end + + private + + def rack_app + # block passed to new is evaluated with instance_eval, + # which searches `binding` for local variables, + # while `self` is searched for instance variables and methods. + # A local pointer in `binding` to @config makes it accessible. + config = @config + Rack::Builder.new do + map("/%s" % config["identity"]) do + run(Rots::IdentityPageApp.new(config, {})) + end + + map("/server") do + run(Rots::ServerApp.new(config, storage: Dir.tmpdir)) + end + end + end + end + end +end diff --git a/lib/rots/server_app.rb b/lib/rots/server_app.rb index 97e8e7f..769865b 100644 --- a/lib/rots/server_app.rb +++ b/lib/rots/server_app.rb @@ -1,28 +1,28 @@ # stdlib -require 'fileutils' +require "fileutils" # external libraries -require 'openid' -require 'openid/extension' -require 'openid/extensions/sreg' -require 'openid/store/filesystem' -require 'openid/util' -require 'rack/request' -require 'rack/utils' +require "openid" +require "openid/extension" +require "openid/extensions/sreg" +require "openid/store/filesystem" +require "openid/util" +require "rack/request" +require "rack/utils" module Rots - class ServerApp - - attr_accessor :request,:openid_request, - :response, :openid_response, - :server - + attr_accessor :request, + :openid_request, + :response, + :openid_response, + :server + def initialize(config, server_options) @server_options = server_options - @sreg_fields = config['sreg'] + @sreg_fields = config["sreg"] end - + def call(env) on_openid_request(env) do if !is_checkid_request? @@ -35,34 +35,37 @@ def call(env) end end end - + protected - + def on_openid_request(env) create_wrappers(env) if @openid_request.nil? - [200, {Rack::CONTENT_TYPE => 'text/html'}, - ["

ROTS => This is an OpenID endpoint

"] ] + [ + 200, + {Rack::CONTENT_TYPE => "text/html"}, + ["

ROTS => This is an OpenID endpoint

"], + ] else yield end end - + def create_wrappers(env) @request = Rack::Request.new(env) - @server = OpenID::Server::Server.new(storage, op_endpoint) + @server = OpenID::Server::Server.new(storage, op_endpoint) @openid_request = @server.decode_request(@request.params) @openid_sreg_request = OpenID::SReg::Request.from_openid_request(@openid_request) unless @openid_request.nil? end - + def is_checkid_request? @openid_request.is_a?(OpenID::Server::CheckIDRequest) end - + def is_checkid_immediate? @openid_request && @openid_request.immediate end - + def process_immediate_checkid_request if checkid_immediate_is_valid? return_successful_openid_response @@ -70,7 +73,7 @@ def process_immediate_checkid_request return_setup_needed_openid_response end end - + def process_checkid_request if checkid_request_is_valid? return_successful_openid_response @@ -78,15 +81,15 @@ def process_checkid_request return_cancel_openid_response end end - + def checkid_request_is_valid? - @request.params['openid.success'] == 'true' + @request.params["openid.success"] == "true" end def checkid_immediate_is_valid? - @request.params['openid.success'] == 'true' + @request.params["openid.success"] == "true" end - + def return_successful_openid_response @openid_response = @openid_request.answer(true) process_sreg_extension @@ -94,19 +97,19 @@ def return_successful_openid_response @server.signatory.sign(@openid_response) if @openid_response.needs_signing reply_consumer end - + def process_sreg_extension return if @openid_sreg_request.nil? response = OpenID::SReg::Response.extract_response(@openid_sreg_request, @sreg_fields) @openid_response.add_extension(response) end - + def return_cancel_openid_response redirect(@openid_request.cancel_url) end - + def return_setup_needed_openid_response - setup_needed_args = @request.params.merge('openid.mode' => 'setup_needed', 'user_setup_url' => '') + setup_needed_args = @request.params.merge("openid.mode" => "setup_needed", "user_setup_url" => "") url = OpenID::Util.append_args(@openid_request.return_to, setup_needed_args) redirect(url) end @@ -117,43 +120,48 @@ def reply_consumer when OpenID::Server::HTTP_OK success(web_response.body) when OpenID::Server::HTTP_REDIRECT - redirect(web_response.headers['location']) + redirect(web_response.headers["location"]) else bad_request - end + end end def redirect(uri) - [ 303, {Rack::CONTENT_LENGTH=>'0', Rack::CONTENT_TYPE=>'text/plain', - 'Location' => uri}, - [] ] + [ + 303, + { + Rack::CONTENT_LENGTH => "0", + Rack::CONTENT_TYPE => "text/plain", + "Location" => uri, + }, + [], + ] end - def bad_request() - [ 400, {Rack::CONTENT_TYPE=>'text/plain', Rack::CONTENT_LENGTH=>'0'}, - [] ] + def bad_request + [ + 400, + {Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "0"}, + [], + ] end - + def storage # create the folder if it doesn't exist FileUtils.mkdir_p(@server_options[:storage]) unless File.exist?(@server_options[:storage]) OpenID::Store::Filesystem.new(@server_options[:storage]) end - - def success(text="") + + def success(text = "") Rack::Response.new(text).finish end - + def op_endpoint if @request.url =~ /(.*\?openid.success=true)/ $1 elsif @request.url =~ /([^?]*)/ $1 - else - nil end end - end - end diff --git a/lib/rots/test.rb b/lib/rots/test.rb new file mode 100644 index 0000000..c64c289 --- /dev/null +++ b/lib/rots/test.rb @@ -0,0 +1,2 @@ +require_relative "test/rack_test_helpers" +require_relative "test/request_helper" diff --git a/lib/rots/test/rack_test_helpers.rb b/lib/rots/test/rack_test_helpers.rb new file mode 100644 index 0000000..93f012d --- /dev/null +++ b/lib/rots/test/rack_test_helpers.rb @@ -0,0 +1,21 @@ +module Rots + module Test + module RackTestHelpers + def mock_openid_request(app, *args) + env = Rack::MockRequest.env_for(*args) + @response = Rack::MockResponse.new(*app.call(env)) + end + + def follow_openid_redirect!(app) + assert(@response) + assert_equal(303, @response.status) + + env = Rack::MockRequest.env_for(@response.headers["Location"]) + _status, headers, _body = Rots::Mocks::RotsServer.new.call(env) + + uri = URI(headers["Location"]) + mock_openid_request(app, "#{uri.path}?#{uri.query}") + end + end + end +end diff --git a/lib/rots/test/request_helper.rb b/lib/rots/test/request_helper.rb new file mode 100644 index 0000000..6ae7a95 --- /dev/null +++ b/lib/rots/test/request_helper.rb @@ -0,0 +1,78 @@ +require "rack/utils" + +module Rots + module Test + module RequestHelper + def openid_request(openid_request_uri) + openid_response = Net::HTTP.get_response(URI.parse(openid_request_uri)) + openid_response_uri = URI(openid_response["Location"]) + openid_response_qs = Rack::Utils.parse_query(openid_response_uri.query) + + { + url: openid_response_uri.to_s, + query_params: openid_response_qs, + } + end + + def checkid_setup(request, params = {}, with_associate = true) + assoc_handle = make_association(request) if with_associate + send_checkid(request, :setup, params, assoc_handle) + end + + def checkid_immediate(request, params = {}, with_associate = true) + assoc_handle = make_association(request) if with_associate + send_checkid(request, :immediate, params, assoc_handle) + end + + def openid_params(response) + uri = URI(response.headers["Location"]) + Rack::Utils.parse_query(uri.query) + end + + protected + + def send_checkid(request, mode, params = {}, assoc_handle = nil) + params = send(:"checkid_#{mode}_params", params) + params.merge("openid.assoc_handle" => assoc_handle) if assoc_handle + qs = "/?" + Rack::Utils.build_query(params) + request.get(qs) + end + + def make_association(request) + associate_qs = Rack::Utils.build_query(associate_params) + response = request.post("/", input: associate_qs) + parse_assoc_handle_from(response) + end + + def parse_assoc_handle_from(response) + response.body.split("\n")[0].match(/^assoc_handle:(.*)$/).captures[0] + end + + def checkid_setup_params(params = {}) + { + "openid.ns" => "http://specs.openid.net/auth/2.0", + "openid.mode" => "checkid_setup", + "openid.claimed_id" => "john.doe", + "openid.identity" => "john.doe", + "openid.return_to" => "http://www.google.com", + # need to specify the openid_handle by hand + }.merge!(params) + end + + def checkid_immediate_params(params = {}) + checkid_setup_params({"openid.mode" => "checkid_immediate"}.merge!(params)) + end + + def associate_params + { + "openid.ns" => "http://specs.openid.net/auth/2.0", + "openid.mode" => "associate", + "openid.session_type" => "DH-SHA1", + "openid.assoc_type" => "HMAC-SHA1", + "openid.dh_consumer_public" => + "U672/RsDUNxAFFAXA+ShVh5LMD2CRdsoqdqhDCPUzfCNy2f44uTWuid/MZuGfJmiVA7QmxqM3GSb8EVq3SGK8eGEwwyzUtatqHidx72rfwAav5AUrZTnwSPQJyiCFrKNGmNhXdRJzcfzSkgaC3hVz2kpADzEevIExG6agns1sYY=", + } + end + end + end +end diff --git a/lib/rots/test_helper.rb b/lib/rots/test_helper.rb deleted file mode 100644 index 8e5e571..0000000 --- a/lib/rots/test_helper.rb +++ /dev/null @@ -1,19 +0,0 @@ -# stdlib in Ruby < 3, gem after -require "net/http" - -# external libraries -require "openid/consumer" -require "openid/consumer/checkid_request.rb" - -module Rots - module TestHelper - def openid_request(openid_request_uri) - openid_response = Net::HTTP.get_response(URI.parse(openid_request_uri)) - openid_response_uri = URI(openid_response['Location']) - openid_response_qs = Rack::Utils.parse_query(openid_response_uri.query) - - { :url => openid_response_uri.to_s, - :query_params => openid_response_qs } - end - end -end diff --git a/rots.gemspec b/rots.gemspec index 4f7dc74..941f390 100644 --- a/rots.gemspec +++ b/rots.gemspec @@ -22,12 +22,26 @@ Gem::Specification.new do |spec| the success of the response will depend on a parameter given on the URL of the authentication request. EOF + spec.metadata["homepage_uri"] = "https://railsbling.com/tags/rots/" + spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/v#{spec.version}" + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/v#{spec.version}/CHANGELOG.md" + spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues" + spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" + spec.metadata["wiki_uri"] = "#{spec.homepage}/wiki" + spec.metadata["funding_uri"] = "https://liberapay.com/pboling" + spec.metadata["rubygems_mfa_required"] = "true" + spec.files = Dir[ # Splats (alphabetical) "{exe,lib}/**/*", # Files (alphabetical) "AUTHORS", - "README", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + "LICENSE.txt", + "README.md", + "SECURITY.md" ] # bin/ is scripts, in any available language, for development of this specific gem # exe/ is for ruby scripts that will ship with this gem to be used by other tools @@ -44,24 +58,31 @@ Gem::Specification.new do |spec| spec.add_dependency("psych", "~> 5.1") spec.add_dependency("rack", ">= 2") spec.add_dependency("rackup", ">= 2") - spec.add_dependency("ruby-openid2", "~> 3.0", ">= 3.0.2") + spec.add_dependency("ruby-openid2", "~> 3.0", ">= 3.0.3") spec.add_dependency("stringio") spec.add_dependency("version_gem", "~> 1.1", ">= 1.1.4") spec.add_dependency("webrick") spec.add_dependency("yaml", "~> 0.3") - spec.add_dependency "date" - spec.add_dependency "net-http" - spec.add_dependency "openssl" - spec.add_dependency "optparse" - spec.add_dependency "rack", ">= 2" - spec.add_dependency "rackup", ">= 2" - spec.add_dependency "ruby-openid2", "~> 3.0" - spec.add_dependency "stringio" - spec.add_dependency "webrick" - spec.add_dependency "yaml", "~> 0.3" - spec.add_dependency "psych", "~> 5.1" + spec.add_development_dependency("rack-openid2", ">= 2") + spec.add_development_dependency("rack-session", ">= 1") + + # Documentation + spec.add_development_dependency("yard", "~> 0.9", ">= 0.9.34") + spec.add_development_dependency("yard-junk", "~> 0.0.10") + # Coverage + spec.add_development_dependency("kettle-soup-cover", "~> 1.0", ">= 1.0.2") + + # Unit tests + spec.add_development_dependency("minitest", ">= 5", "< 6") # Use assert_nil if expecting nil + spec.add_development_dependency("rake", ">= 10") spec.add_development_dependency("rspec", "~> 3.13") - spec.add_development_dependency("rake", ">= 13") + spec.add_development_dependency("rspec-block_is_expected", "~> 1.0", ">= 1.0.6") + + # Linting + spec.add_development_dependency("rubocop-lts", "~> 10.1") # Lint & Style Support for Ruby 2.3+ + spec.add_development_dependency("rubocop-packaging", "~> 0.5", ">= 0.5.2") + spec.add_development_dependency("rubocop-rspec", "~> 3.0") + spec.add_development_dependency("standard", "~> 1.40") end diff --git a/spec/config/byebug.rb b/spec/config/byebug.rb new file mode 100644 index 0000000..81c8b1f --- /dev/null +++ b/spec/config/byebug.rb @@ -0,0 +1,3 @@ +if VersionGem::Ruby.gte_minimum_version?("2.7") + require "byebug" if ENV.fetch("DEBUG", "false").casecmp?("true") +end diff --git a/spec/config/rspec/rots.rb b/spec/config/rspec/rots.rb new file mode 100644 index 0000000..c8b14ee --- /dev/null +++ b/spec/config/rspec/rots.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include Rots::Test::RequestHelper +end diff --git a/spec/config/rspec/rspec_block_is_expected.rb b/spec/config/rspec/rspec_block_is_expected.rb new file mode 100644 index 0000000..ab140fa --- /dev/null +++ b/spec/config/rspec/rspec_block_is_expected.rb @@ -0,0 +1,2 @@ +require "rspec/block_is_expected" +require "rspec/block_is_expected/matchers/not" diff --git a/spec/config/rspec/rspec_core.rb b/spec/config/rspec/rspec_core.rb new file mode 100644 index 0000000..f543e7a --- /dev/null +++ b/spec/config/rspec/rspec_core.rb @@ -0,0 +1,9 @@ +RSpec.configure do |config| + # Enable flags like --only-failures and --next-failure + config.example_status_persistence_file_path = ".rspec_status" + + # Disable RSpec exposing methods globally on `Module` and `main` + config.disable_monkey_patching! + + config.expect_with :rspec, :minitest +end diff --git a/spec/config/rspec/version_gem.rb b/spec/config/rspec/version_gem.rb new file mode 100644 index 0000000..1517e9c --- /dev/null +++ b/spec/config/rspec/version_gem.rb @@ -0,0 +1 @@ +require "version_gem/rspec" diff --git a/spec/rots/mocks/integration_spec.rb b/spec/rots/mocks/integration_spec.rb new file mode 100644 index 0000000..8b7bff6 --- /dev/null +++ b/spec/rots/mocks/integration_spec.rb @@ -0,0 +1,356 @@ +require "rots/mocks" +require "rots/test" + +OpenID.fetcher = Rots::Mocks::Fetcher.new(Rots::Mocks::RotsServer.new) + +# This is mostly a copy of the specs from rack-openid2 +# This is to ensure that this library does not accidentally +# break the underlying functionality of rack-openid2. +RSpec.describe "integration" do + describe "headers" do + it "builds header" do + assert_equal 'OpenID identity="http://example.com/"', + Rack::OpenID.build_header(identity: "http://example.com/") + assert_equal 'OpenID identity="http://example.com/?foo=bar"', + Rack::OpenID.build_header(identity: "http://example.com/?foo=bar") + + header = Rack::OpenID.build_header(identity: "http://example.com/", return_to: "http://example.org/") + + assert_match(/OpenID /, header) + assert_match(/identity="http:\/\/example\.com\/"/, header) + assert_match(/return_to="http:\/\/example\.org\/"/, header) + + header = Rack::OpenID.build_header(identity: "http://example.com/", required: ["nickname", "email"]) + + assert_match(/OpenID /, header) + assert_match(/identity="http:\/\/example\.com\/"/, header) + assert_match(/required="nickname,email"/, header) + end + + it "parses header" do + assert_equal( + {"identity" => "http://example.com/"}, + Rack::OpenID.parse_header('OpenID identity="http://example.com/"'), + ) + assert_equal( + {"identity" => "http://example.com/?foo=bar"}, + Rack::OpenID.parse_header('OpenID identity="http://example.com/?foo=bar"'), + ) + assert_equal( + {"identity" => "http://example.com/", "return_to" => "http://example.org/"}, + Rack::OpenID.parse_header('OpenID identity="http://example.com/", return_to="http://example.org/"'), + ) + assert_equal( + {"identity" => "http://example.com/", "required" => ["nickname", "email"]}, + Rack::OpenID.parse_header('OpenID identity="http://example.com/", required="nickname,email"'), + ) + + # ensure we don't break standard HTTP basic auth + assert_empty( + Rack::OpenID.parse_header('Realm="Example"'), + ) + end + end + + describe "openid" do + include Rots::Test::RackTestHelpers + + subject(:app) { Rots::Mocks::ClientApp.new(**options) } + + let(:options) { {} } + + it "with_get" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end + + it "with_deprecated_identity" do + mock_openid_request(app, "/", method: "GET", identity: "#{Rots::Mocks::RotsServer::SERVER_URL}/john.doe?openid.success=true") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end + + it "with_post_method" do + mock_openid_request(app, "/", method: "POST") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "POST", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end + + context "with_custom_return_to" do + let(:options) { {return_to: "http://example.org/complete"} } + + it "succeeds wth GET" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/complete", @response.headers["X-Path"] + assert_equal "success", @response.body + end + + it "succeeds with POST" do + mock_openid_request(app, "/", method: "POST") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/complete", @response.headers["X-Path"] + assert_equal "success", @response.body + end + end + + context "with nested_params_custom_return_to" do + let(:options) { {return_to: "http://example.org/complete?user[remember_me]=true"} } + + it "succeeds with GET" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/complete", @response.headers["X-Path"] + assert_equal "success", @response.body + assert_match(/remember_me/, @response.headers["X-Query-String"]) + end + + it "succeeds with POST" do + mock_openid_request(app, "/", method: "POST") + + assert_equal 303, @response.status + env = Rack::MockRequest.env_for(@response.headers["Location"]) + _status, headers, _body = Rots::Mocks::RotsServer.new.call(env) + + _uri, input = headers["Location"].split("?", 2) + mock_openid_request(app, "http://example.org/complete?user[remember_me]=true", method: "POST", input: input) + + assert_equal 200, @response.status + assert_equal "POST", @response.headers["X-Method"] + assert_equal "/complete", @response.headers["X-Path"] + assert_equal "success", @response.body + assert_match(/remember_me/, @response.headers["X-Query-String"]) + end + end + + context "with custom return method" do + let(:options) { {method: "put"} } + + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "PUT", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end + end + + context "with simple registration fields" do + let(:options) { {required: ["nickname", "email"], optional: "fullname"} } + + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end + end + + context "with attribute exchange" do + let(:options) { + { + required: ["http://axschema.org/namePerson/friendly", "http://axschema.org/contact/email"], + optional: "http://axschema.org/namePerson", + } + } + + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end + end + + context "with oauth" do + let(:options) { + { + "oauth[consumer]": "www.example.com", + "oauth[scope]": ["http://docs.google.com/feeds/", "http://spreadsheets.google.com/feeds/"], + } + } + + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + location = @response.headers["Location"] + + assert_match(/openid.oauth.consumer/, location) + assert_match(/openid.oauth.scope/, location) + + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end + end + + context "with page" do + let(:options) { + { + "pape[preferred_auth_policies]": ["test_policy1", "test_policy2"], + "pape[max_auth_age]": 600, + } + } + + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + + location = @response.headers["Location"] + + assert_match(/pape\.preferred_auth_policies=test_policy1\+test_policy2/, location) + assert_match(/pape\.max_auth_age=600/, location) + + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "success", @response.body + end + end + + context "with realm wildcard" do + let(:options) { {realm_domain: "*.example.org"} } + + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + + location = @response.headers["Location"] + + assert_match(/openid.realm=http%3A%2F%2F%2A.example.org/, location) + + follow_openid_redirect!(app) + + assert_equal 200, @response.status + end + end + + context "with inferred realm" do + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + + location = @response.headers["Location"] + + assert_match(/openid.realm=http%3A%2F%2Fexample.org/, location) + + follow_openid_redirect!(app) + + assert_equal 200, @response.status + end + end + + context "with missing id" do + let(:options) { {identifier: "#{Rots::Mocks::RotsServer::SERVER_URL}/john.doe"} } + + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 400, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "cancel", @response.body + end + end + + context "with timeout" do + let(:options) { {identifier: Rots::Mocks::RotsServer::SERVER_URL} } + + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + + assert_equal 400, @response.status + assert_equal "GET", @response.headers["X-Method"] + assert_equal "/", @response.headers["X-Path"] + assert_equal "missing", @response.body + end + end + + context "with sanitized query string" do + it "succeeds" do + mock_openid_request(app, "/", method: "GET") + follow_openid_redirect!(app) + + assert_equal 200, @response.status + assert_equal "/", @response.headers["X-Path"] + assert_equal "", @response.headers["X-Query-String"] + end + end + + context "with passthrough standard http basic auth" do + it "succeeds" do + mock_openid_request(app, "/", :method => "GET", "MOCK_HTTP_BASIC_AUTH" => "1") + + assert_equal 401, @response.status + end + end + + describe "simple auth" do + include Rots::Test::RackTestHelpers + + it "can login" do + app = simple_app("#{Rots::Mocks::RotsServer::SERVER_URL}/john.doe?openid.success=true") + mock_openid_request(app, "/dashboard") + follow_openid_redirect!(app) + + assert_equal 303, @response.status + assert_equal "http://example.org/dashboard", @response.headers["Location"] + + cookie = @response.headers["Set-Cookie"].split(";").first + mock_openid_request(app, "/dashboard", "HTTP_COOKIE" => cookie) + + assert_equal 200, @response.status + assert_equal "Hello", @response.body + end + + it "fails login" do + app = simple_app("#{Rots::Mocks::RotsServer::SERVER_URL}/john.doe") + + mock_openid_request(app, "/dashboard") + follow_openid_redirect!(app) + + assert_match Rots::Mocks::RotsServer::SERVER_URL, @response.headers["Location"] + end + + private + + def simple_app(identifier) + rack_app = lambda { |env| [200, {"Content-Type" => "text/html"}, ["Hello"]] } + rack_app = Rack::OpenID::SimpleAuth.new(rack_app, identifier) + Rack::Session::Pool.new(rack_app) + end + end + end +end diff --git a/spec/rots/version_spec.rb b/spec/rots/version_spec.rb new file mode 100644 index 0000000..24bc040 --- /dev/null +++ b/spec/rots/version_spec.rb @@ -0,0 +1,7 @@ +RSpec.describe Rots::Version do + it_behaves_like "a Version module", described_class + + it "is greater than 1.0.0" do + expect(Gem::Version.new(described_class) >= Gem::Version.new("1.0.0")).to be(true) + end +end diff --git a/spec/server_app_spec.rb b/spec/server_app_spec.rb index 5d85c90..2436d3e 100644 --- a/spec/server_app_spec.rb +++ b/spec/server_app_spec.rb @@ -1,114 +1,93 @@ -require File.join(File.dirname(__FILE__), 'spec_helper') - -# This is just a comment test - -describe Rots::ServerApp do - +RSpec.describe Rots::ServerApp do describe "when the request is not an OpenID request" do - - it "should return a helpful message saying that is an OpenID endpoint" do - request = Rack::MockRequest.new(Rots::ServerApp.new({'sreg' => {}}, - {:storage => File.join(*%w(. tmp rots)) })) + it "returns a helpful message saying that is an OpenID endpoint" do + request = Rack::MockRequest.new(Rots::ServerApp.new( + {"sreg" => {}}, + {storage: File.join(*%w(. tmp rots))}, + )) response = request.get("/") expect(response).to be_ok expect(response.body).to eq("

ROTS => This is an OpenID endpoint

") end - end describe "when the request is an OpenID request" do - - before(:each) do - @request = Rack::MockRequest.new(Rots::ServerApp.new({ - 'identity' => 'john.doe', - 'sreg' => { - 'email' => "john@doe.com", - 'nickname' => 'johndoe', - 'fullname' => "John Doe", - 'dob' => "1985-09-21", - 'gender' => "M" - }}, - {:storage => File.join(*%w(. tmp rots))} + before do + @request = Rack::MockRequest.new(Rots::ServerApp.new( + { + "identity" => "john.doe", + "sreg" => { + "email" => "john@doe.com", + "nickname" => "johndoe", + "fullname" => "John Doe", + "dob" => "1985-09-21", + "gender" => "M", + }, + }, + {storage: File.join(*%w(. tmp rots))}, )) end - describe "and it is a check_id request" do - describe "and is immediate" do - describe "with a success flag" do - - it "should return an openid.mode equal to id_res" do - response = checkid_setup(@request, 'openid.success' => 'true') + it "returns an openid.mode equal to id_res" do + response = checkid_setup(@request, "openid.success" => "true") params = openid_params(response) - expect(params['openid.mode']).to eq('id_res') + expect(params["openid.mode"]).to eq("id_res") end - end describe "without a success flag" do - - it "should return an openid.mode equal to setup_needed" do + it "returns an openid.mode equal to setup_needed" do response = checkid_immediate(@request) params = openid_params(response) - expect(params['openid.mode']).to eq('setup_needed') - expect(params['user_setup_url']).to eq('') + expect(params["openid.mode"]).to eq("setup_needed") + expect(params["user_setup_url"]).to eq("") end - end - end describe "and is not immediate" do - describe "with a success flag" do - - it "should return an openid.mode equal to id_res" do - response = checkid_setup(@request, 'openid.success' => 'true') + it "returns an openid.mode equal to id_res" do + response = checkid_setup(@request, "openid.success" => "true") params = openid_params(response) - expect(params['openid.mode']).to eq('id_res') + expect(params["openid.mode"]).to eq("id_res") end - end describe "without a success flag" do - - it "should return an openid.mode equal to cancel" do + it "returns an openid.mode equal to cancel" do response = checkid_setup(@request) params = openid_params(response) - expect(params['openid.mode']).to eq('cancel') + expect(params["openid.mode"]).to eq("cancel") end - end - + describe "using SREG extension with a success flag" do - - it "should return an openid.mode equal to id_res" do - response = checkid_setup(@request, 'openid.success' => 'true') + it "returns an openid.mode equal to id_res" do + response = checkid_setup(@request, "openid.success" => "true") params = openid_params(response) - expect(params['openid.mode']).to eq('id_res') + expect(params["openid.mode"]).to eq("id_res") end - - it "should return all the sreg fields" do + + it "returns all the sreg fields" do response = checkid_setup(@request, { - 'openid.success' => true, - 'openid.ns.sreg' => OpenID::SReg::NS_URI, - 'openid.sreg.required' => 'email,nickname,fullname', - 'openid.sreg.optional' => 'dob,gender' + "openid.success" => true, + "openid.ns.sreg" => OpenID::SReg::NS_URI, + "openid.sreg.required" => "email,nickname,fullname", + "openid.sreg.optional" => "dob,gender", }) params = openid_params(response) - expect(params['openid.sreg.email']).to eq("john@doe.com") - expect(params['openid.sreg.nickname']).to eq('johndoe') - expect(params['openid.sreg.fullname']).to eq("John Doe") - expect(params['openid.sreg.dob']).to eq("1985-09-21") - expect(params['openid.sreg.gender']).to eq("M") + expect(params["openid.sreg.email"]).to eq("john@doe.com") + expect(params["openid.sreg.nickname"]).to eq("johndoe") + expect(params["openid.sreg.fullname"]).to eq("John Doe") + expect(params["openid.sreg.dob"]).to eq("1985-09-21") + expect(params["openid.sreg.gender"]).to eq("M") end - end - end end end - -end \ No newline at end of file +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index af09281..df2c885 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,73 +1,28 @@ -$:.unshift(File.dirname(__FILE__), '..', 'lib') -require 'rubygems' -require 'bundler/setup' -require 'rack/mock' -require "rots" +# External library dependencies +require "rack/openid" # rack-openid2 +require "rack/openid/simple_auth" # Not loaded by rack-openid2, by default +require "version_gem/ruby" +require "rack/mock" +require "minitest/assertions" -module Rots::RequestHelper - - def checkid_setup(request, params={}, with_associate=true) - assoc_handle = make_association(request) if with_associate - send_checkid(request, :setup, params, assoc_handle) - end - - def checkid_immediate(request, params={}, with_associate=true) - assoc_handle = make_association(request) if with_associate - send_checkid(request, :immediate, params, assoc_handle) - end - - def openid_params(response) - uri = URI(response.headers['Location']) - Rack::Utils.parse_query(uri.query) - end - - protected - - def send_checkid(request, mode, params={}, assoc_handle = nil) - params = self.send(:"checkid_#{mode}_params", params) - params.merge('openid.assoc_handle' => assoc_handle) if assoc_handle - qs = "/?" + Rack::Utils.build_query(params) - request.get(qs) - end +# RSpec Configs +require "config/byebug" +require "config/rspec/rspec_block_is_expected" +require "config/rspec/rspec_core" +require "config/rspec/version_gem" - def make_association(request) - associate_qs = Rack::Utils.build_query(associate_params) - response = request.post('/', :input => associate_qs) - parse_assoc_handle_from(response) - end - - def parse_assoc_handle_from(response) - response.body.split("\n")[0].match(/^assoc_handle:(.*)$/).captures[0] - end - - def checkid_setup_params(params = {}) - { - "openid.ns" => "http://specs.openid.net/auth/2.0", - "openid.mode" => "checkid_setup", - "openid.claimed_id" => 'john.doe', - "openid.identity" => 'john.doe', - "openid.return_to" => "http://www.google.com" - # need to specify the openid_handle by hand - }.merge!(params) - end - - def checkid_immediate_params(params = {}) - checkid_setup_params({'openid.mode' => 'checkid_immediate'}.merge!(params)) - end - - def associate_params - { - "openid.ns" => "http://specs.openid.net/auth/2.0", - "openid.mode" => "associate", - "openid.session_type" => "DH-SHA1", - "openid.assoc_type" => "HMAC-SHA1", - "openid.dh_consumer_public" => - "U672/RsDUNxAFFAXA+ShVh5LMD2CRdsoqdqhDCPUzfCNy2f44uTWuid/MZuGfJmiVA7QmxqM3GSb8EVq3SGK8eGEwwyzUtatqHidx72rfwAav5AUrZTnwSPQJyiCFrKNGmNhXdRJzcfzSkgaC3hVz2kpADzEevIExG6agns1sYY=" - } - end - +# Last thing before loading this gem is to setup code coverage +begin + # This does not require "simplecov", but + require "kettle-soup-cover" + # this next line has a side-effect of running `.simplecov` + require "simplecov" if defined?(Kettle::Soup::Cover) && Kettle::Soup::Cover::DO_COV +rescue LoadError + nil end -RSpec.configure do |config| - config.include Rots::RequestHelper -end \ No newline at end of file +# this gem: +require "rots" +# this gem's library files of test helpers: +require "rots/test/request_helper" +require "config/rspec/rots"