diff --git a/.envrc b/.envrc
index 800bc2d..673e034 100644
--- a/.envrc
+++ b/.envrc
@@ -21,8 +21,8 @@ export K_SOUP_COV_DO=true # Means you want code coverage
export K_SOUP_COV_COMMAND_NAME="Test Coverage"
# Available formats are html, xml, rcov, lcov, json, tty
export K_SOUP_COV_FORMATTERS="html,xml,rcov,lcov,json,tty"
-export K_SOUP_COV_MIN_BRANCH=80 # Means you want to enforce X% branch coverage
-export K_SOUP_COV_MIN_LINE=96 # Means you want to enforce X% line coverage
+export K_SOUP_COV_MIN_BRANCH=96 # Means you want to enforce X% branch coverage
+export K_SOUP_COV_MIN_LINE=100 # Means you want to enforce X% line coverage
export K_SOUP_COV_MIN_HARD=true # Means you want the build to fail if the coverage thresholds are not met
export K_SOUP_COV_MULTI_FORMATTERS=true
export K_SOUP_COV_OPEN_BIN= # Means don't try to open coverage results in browser
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index c9d6a2e..b725611 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -6,7 +6,7 @@ permissions:
id-token: write
env:
- K_SOUP_COV_MIN_BRANCH: 100
+ K_SOUP_COV_MIN_BRANCH: 96
K_SOUP_COV_MIN_LINE: 100
K_SOUP_COV_MIN_HARD: true
K_SOUP_COV_FORMATTERS: "xml,rcov,lcov,tty"
@@ -115,7 +115,7 @@ jobs:
hide_complexity: true
indicators: true
output: both
- thresholds: '100 100'
+ thresholds: '100 96'
continue-on-error: ${{ matrix.experimental != 'false' }}
- name: Add Coverage PR Comment
diff --git a/.github/workflows/current.yml b/.github/workflows/current.yml
index f8a9e64..54beab4 100644
--- a/.github/workflows/current.yml
+++ b/.github/workflows/current.yml
@@ -63,11 +63,11 @@ jobs:
steps:
- name: Checkout
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
uses: actions/checkout@v5
- name: Setup Ruby & RubyGems
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
@@ -79,11 +79,37 @@ jobs:
# We need to do this first to get appraisal installed.
# NOTE: This does not use the primary Gemfile at all.
- name: Install Root Appraisal
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle
- - name: Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+
+ - name: "[Attempt 1] Install Root Appraisal"
+ id: bundleAttempt1
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
+ run: bundle
+ # Continue to the next step on failure
+ continue-on-error: true
+
+ # Effectively an automatic retry of the previous step.
+ - name: "[Attempt 2] Install Root Appraisal"
+ id: bundleAttempt2
+ # If bundleAttempt1 failed, try again here; Otherwise skip.
+ if: ${{ steps.bundleAttempt1.outcome == 'failure' && !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
+ run: bundle
+
+ - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
+ id: bundleAppraisalAttempt1
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
+ run: bundle exec appraisal ${{ matrix.appraisal }} bundle
+ # Continue to the next step on failure
+ continue-on-error: true
+
+ # Effectively an automatic retry of the previous step.
+ - name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
+ id: bundleAppraisalAttempt2
+ # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip.
+ if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle
- - name: Tests for ${{ matrix.ruby }}@${{ matrix.appraisal }} via ${{ matrix.exec_cmd }}
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+
+ - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }}
diff --git a/.github/workflows/dep-heads.yml b/.github/workflows/dep-heads.yml
index f691568..524cb0c 100644
--- a/.github/workflows/dep-heads.yml
+++ b/.github/workflows/dep-heads.yml
@@ -67,11 +67,11 @@ jobs:
steps:
- name: Checkout
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
uses: actions/checkout@v5
- name: Setup Ruby & RubyGems
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
@@ -82,24 +82,38 @@ jobs:
# Raw `bundle` will use the BUNDLE_GEMFILE set to matrix.gemfile (i.e. Appraisal.root)
# We need to do this first to get appraisal installed.
# NOTE: This does not use the primary Gemfile at all.
- - name: "Install Root Appraisal"
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ - name: Install Root Appraisal
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle
- - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ - name: "[Attempt 1] Install Root Appraisal"
id: bundleAttempt1
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
+ run: bundle
+ # Continue to the next step on failure
+ continue-on-error: true
+
+ # Effectively an automatic retry of the previous step.
+ - name: "[Attempt 2] Install Root Appraisal"
+ id: bundleAttempt2
+ # If bundleAttempt1 failed, try again here; Otherwise skip.
+ if: ${{ steps.bundleAttempt1.outcome == 'failure' && !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
+ run: bundle
+
+ - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
+ id: bundleAppraisalAttempt1
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle
# Continue to the next step on failure
continue-on-error: true
# Effectively an automatic retry of the previous step.
- name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
- # If bundleAttempt1 failed, try again here; Otherwise skip.
- if: ${{ steps.bundleAttempt1.outcome == 'failure' && !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
- id: bundleAttempt2
+ id: bundleAppraisalAttempt2
+ # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip.
+ if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle
- - name: Tests for ${{ matrix.ruby }}@${{ matrix.appraisal }} via ${{ matrix.exec_cmd }}
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }}
diff --git a/.github/workflows/heads.yml b/.github/workflows/heads.yml
index f8c92d1..7321a15 100644
--- a/.github/workflows/heads.yml
+++ b/.github/workflows/heads.yml
@@ -64,11 +64,11 @@ jobs:
steps:
- name: Checkout
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
uses: actions/checkout@v5
- name: Setup Ruby & RubyGems
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
@@ -79,24 +79,38 @@ jobs:
# Raw `bundle` will use the BUNDLE_GEMFILE set to matrix.gemfile (i.e. Appraisal.root)
# We need to do this first to get appraisal installed.
# NOTE: This does not use the primary Gemfile at all.
- - name: "Install Root Appraisal"
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ - name: Install Root Appraisal
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle
- - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ - name: "[Attempt 1] Install Root Appraisal"
id: bundleAttempt1
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
+ run: bundle
+ # Continue to the next step on failure
+ continue-on-error: true
+
+ # Effectively an automatic retry of the previous step.
+ - name: "[Attempt 2] Install Root Appraisal"
+ id: bundleAttempt2
+ # If bundleAttempt1 failed, try again here; Otherwise skip.
+ if: ${{ steps.bundleAttempt1.outcome == 'failure' && !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
+ run: bundle
+
+ - name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
+ id: bundleAppraisalAttempt1
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle
# Continue to the next step on failure
continue-on-error: true
# Effectively an automatic retry of the previous step.
- name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
- # If bundleAttempt1 failed, try again here; Otherwise skip.
- if: ${{ steps.bundleAttempt1.outcome == 'failure' && !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
- id: bundleAttempt2
+ id: bundleAppraisalAttempt2
+ # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip.
+ if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle
- - name: Tests for ${{ matrix.ruby }}@${{ matrix.appraisal }} via ${{ matrix.exec_cmd }}
- if: ${{ !(env.ACT && startsWith(matrix.ruby, 'jruby')) }}
+ - name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }}
diff --git a/.github/workflows/truffle.yml b/.github/workflows/truffle.yml
index db65188..b44416e 100644
--- a/.github/workflows/truffle.yml
+++ b/.github/workflows/truffle.yml
@@ -47,9 +47,11 @@ jobs:
steps:
- name: Checkout
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
uses: actions/checkout@v5
- name: Setup Ruby & RubyGems
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
@@ -61,10 +63,12 @@ jobs:
# We need to do this first to get appraisal installed.
# NOTE: This does not use the primary Gemfile at all.
- name: Install Root Appraisal
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle
- name: "[Attempt 1] Install Root Appraisal"
id: bundleAttempt1
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle
# Continue to the next step on failure
continue-on-error: true
@@ -73,11 +77,12 @@ jobs:
- name: "[Attempt 2] Install Root Appraisal"
id: bundleAttempt2
# If bundleAttempt1 failed, try again here; Otherwise skip.
- if: steps.bundleAttempt1.outcome == 'failure'
+ if: ${{ steps.bundleAttempt1.outcome == 'failure' && !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle
- name: "[Attempt 1] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
id: bundleAppraisalAttempt1
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle
# Continue to the next step on failure
continue-on-error: true
@@ -85,9 +90,10 @@ jobs:
# Effectively an automatic retry of the previous step.
- name: "[Attempt 2] Appraisal for ${{ matrix.ruby }}@${{ matrix.appraisal }}"
id: bundleAppraisalAttempt2
- # If bundleAttempt1 failed, try again here; Otherwise skip.
- if: steps.bundleAppraisalAttempt1.outcome == 'failure'
+ # If bundleAppraisalAttempt1 failed, try again here; Otherwise skip.
+ if: ${{ steps.bundleAppraisalAttempt1.outcome == 'failure' && !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle
- name: Tests for ${{ matrix.ruby }} via ${{ matrix.exec_cmd }}
+ if: ${{ !(env.ACT && (startsWith(matrix.ruby, 'jruby')) || startsWith(matrix.ruby, 'truffleruby')) }}
run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3390138..bf3b74a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -22,7 +22,7 @@ variables:
K_SOUP_COV_DEBUG: true
K_SOUP_COV_DO: true
K_SOUP_COV_HARD: true
- K_SOUP_COV_MIN_BRANCH: 100
+ K_SOUP_COV_MIN_BRANCH: 96
K_SOUP_COV_MIN_LINE: 100
K_SOUP_COV_VERBOSE: true
K_SOUP_COV_FORMATTERS: "tty"
diff --git a/.idea/misc.xml b/.idea/misc.xml
index b4353a6..b43a841 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 4c6280e..7ddfc9e 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -7,6 +7,6 @@
-
+
\ No newline at end of file
diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock
index cc33db0..123cf76 100644
--- a/.rubocop_gradual.lock
+++ b/.rubocop_gradual.lock
@@ -2,44 +2,15 @@
"lib/oauth/tty/cli.rb:904168046": [
[6, 7, 77, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 2883780555]
],
- "lib/oauth/tty/command.rb:3092098200": [
- [126, 23, 4, "Security/Open: The use of `Kernel#open` is a serious security risk.", 2087926481]
- ],
- "oauth-tty.gemspec:3319279878": [
- [129, 3, 40, "Gemspec/DependencyVersion: Dependency version specification is required.", 2300588954],
- [131, 3, 44, "Gemspec/DependencyVersion: Dependency version specification is required.", 1905290578],
- [132, 3, 46, "Gemspec/DependencyVersion: Dependency version specification is required.", 4289565910]
+ "oauth-tty.gemspec:3045486337": [
+ [128, 3, 40, "Gemspec/DependencyVersion: Dependency version specification is required.", 2300588954],
+ [130, 3, 44, "Gemspec/DependencyVersion: Dependency version specification is required.", 1905290578],
+ [131, 3, 46, "Gemspec/DependencyVersion: Dependency version specification is required.", 4289565910]
],
"spec/oauth/backwards_compatibility_spec.rb:4041711732": [
[3, 16, 25, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 3956042931]
],
- "spec/oauth/tty/authorize_command_spec.rb:3270431476": [
- [3, 1, 53, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth/tty/commands/authorize_command*_spec.rb`.", 2667806271],
- [14, 34, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
- [15, 39, 21, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 3390344648],
- [16, 38, 20, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 1228090493],
- [19, 7, 64, "RSpec/ReceiveMessages: Use `receive_messages` instead of multiple stubs on lines [20].", 1559313276],
- [20, 7, 69, "RSpec/ReceiveMessages: Use `receive_messages` instead of multiple stubs on lines [19].", 3030878101],
- [22, 7, 23, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 4174421602],
- [23, 7, 16, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 3492346277],
- [25, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
- [26, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
- [32, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
- [52, 34, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
- [54, 7, 23, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 4174421602],
- [55, 7, 44, "RSpec/LeakyConstantDeclaration: Stub constant instead of declaring explicitly.", 2395720961],
- [57, 7, 16, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 3492346277],
- [68, 34, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
- [69, 39, 21, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 3390344648],
- [70, 7, 23, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 4174421602],
- [71, 7, 16, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 3492346277],
- [73, 7, 45, "RSpec/LeakyConstantDeclaration: Stub constant instead of declaring explicitly.", 1997245299],
- [75, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
- [76, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
- [77, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262]
- ],
"spec/oauth/tty/cli_spec.rb:361981118": [
- [3, 1, 30, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth/tty/cli*_spec.rb`.", 2849860169],
[109, 34, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
[110, 38, 20, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 1228090493],
[111, 34, 10, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 4294324198],
@@ -65,25 +36,40 @@
[212, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
[213, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262]
],
- "spec/oauth/tty/command_spec.rb:513689811": [
- [3, 1, 34, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth/tty/command*_spec.rb`.", 667110152],
+ "spec/oauth/tty/command_spec.rb:2516268945": [
[9, 3, 275, "RSpec/LeakyConstantDeclaration: Stub class constant instead of declaring explicitly.", 2810654211]
],
- "spec/oauth/tty/sign_command_spec.rb:622094174": [
- [3, 1, 48, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth/tty/commands/sign_command*_spec.rb`.", 3251857167],
- [21, 7, 27, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 340848961],
- [69, 34, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
- [70, 35, 21, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 3390344648],
- [71, 7, 23, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 4174421602],
- [72, 7, 16, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 3492346277],
- [73, 7, 67, "RSpec/ReceiveMessages: Use `receive_messages` instead of multiple stubs on lines [74, 75].", 42619244],
- [74, 7, 87, "RSpec/ReceiveMessages: Use `receive_messages` instead of multiple stubs on lines [73, 75].", 1827647110],
- [75, 7, 89, "RSpec/ReceiveMessages: Use `receive_messages` instead of multiple stubs on lines [73, 74].", 1585010367],
- [75, 81, 13, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 4026407386],
- [77, 7, 27, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 340848961]
+ "spec/oauth/tty/commands/authorize_command_spec.rb:3270431476": [
+ [14, 34, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
+ [15, 39, 21, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 3390344648],
+ [16, 38, 20, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 1228090493],
+ [19, 7, 64, "RSpec/ReceiveMessages: Use `receive_messages` instead of multiple stubs on lines [20].", 1559313276],
+ [20, 7, 69, "RSpec/ReceiveMessages: Use `receive_messages` instead of multiple stubs on lines [19].", 3030878101],
+ [22, 7, 23, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 4174421602],
+ [23, 7, 16, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 3492346277],
+ [25, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
+ [26, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
+ [32, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
+ [52, 34, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
+ [54, 7, 23, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 4174421602],
+ [55, 7, 44, "RSpec/LeakyConstantDeclaration: Stub constant instead of declaring explicitly.", 2395720961],
+ [57, 7, 16, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 3492346277],
+ [68, 34, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
+ [69, 39, 21, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 3390344648],
+ [70, 7, 23, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 4174421602],
+ [71, 7, 16, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 3492346277],
+ [73, 7, 45, "RSpec/LeakyConstantDeclaration: Stub constant instead of declaring explicitly.", 1997245299],
+ [75, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
+ [76, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262],
+ [77, 7, 21, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 2407753262]
+ ],
+ "spec/oauth/tty/commands/query_command_spec.rb:1247725853": [
+ [20, 32, 17, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 285748316],
+ [23, 36, 20, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 1228090493],
+ [26, 37, 19, "RSpec/VerifiedDoubleReference: Use a constant class reference for verified doubles. String references are not verifying unless the class is loaded.", 511534081],
+ [55, 5, 20, "RSpec/StubbedMock: Prefer `allow` over `expect` when configuring a response.", 4235470523]
],
"spec/oauth/tty_spec.rb:1891755344": [
- [3, 1, 25, "RSpec/EmptyExampleGroup: Empty example group detected.", 208109039],
- [3, 1, 25, "RSpec/SpecFilePathFormat: Spec path should end with `o_auth/tty*_spec.rb`.", 208109039]
+ [3, 1, 25, "RSpec/EmptyExampleGroup: Empty example group detected.", 208109039]
]
}
diff --git a/.rubocop_rspec.yml b/.rubocop_rspec.yml
index df5911b..45f84ad 100644
--- a/.rubocop_rspec.yml
+++ b/.rubocop_rspec.yml
@@ -18,7 +18,7 @@ RSpec/InstanceVariable:
RSpec/NestedGroups:
Enabled: false
-
+
RSpec/ExpectInHook:
Enabled: false
@@ -28,3 +28,7 @@ RSpec/DescribeClass:
RSpec/MultipleMemoizedHelpers:
Enabled: false
+
+RSpec/SpecFilePathFormat:
+ CustomTransform:
+ "OAuth": "oauth"
diff --git a/Appraisals b/Appraisals
index 411ade4..381bd92 100644
--- a/Appraisals
+++ b/Appraisals
@@ -32,7 +32,7 @@ end
# Split into discrete appraisals if one of them needs a dependency locked discretely.
appraise "head" do
# Why is gem "cgi" here? See: https://github.com/vcr/vcr/issues/1057
- # gem "cgi", ">= 0.5"
+ gem "cgi", ">= 0.5"
gem "oauth", github: "ruby-oauth/oauth", branch: "main"
gem "benchmark", "~> 0.4", ">= 0.4.1"
eval_gemfile "modular/x_std_libs.gemfile"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fac49b6..18d32f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,8 @@ Please file a bug if you notice a violation of semantic versioning.
- (test) many new tests (@pboling)
- (docs) CITATION.cff
- support window increased, down to Ruby 2.3 (@pboling)
+- (test) added specs for oauth.opts usage (@pboling)
+- (test) added specs for all commands (@pboling)
### Changed
@@ -42,6 +44,10 @@ Please file a bug if you notice a violation of semantic versioning.
### Fixed
+- Fixed issues in option parsing by implementing Command#parse_options (@pboling)
+ - Use Shellwords for proper tokenization
+ - Verified options file loading and CLI flag precedence
+
### Security
## [1.0.5] - 2022-09-20
diff --git a/Gemfile.lock b/Gemfile.lock
index a1ed51a..1372c10 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -12,7 +12,7 @@ GIT
GIT
remote: https://github.com/ruby-oauth/oauth
- revision: 2ae700b99c264baf2b179dfb79749cefc36915db
+ revision: 0430b449f51e3006fdfeea62c080bdfc85307d5f
branch: main
specs:
oauth (1.1.1)
@@ -45,7 +45,6 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
concurrent-ruby (1.3.5)
- cookiejar (0.3.4)
crack (1.0.0)
bigdecimal
rexml
@@ -87,19 +86,9 @@ GEM
dry-inflector (~> 1.0)
dry-logic (~> 1.4)
zeitwerk (~> 2.6)
- em-http-request (1.1.7)
- addressable (>= 2.3.4)
- cookiejar (!= 0.3.1)
- em-socksify (>= 0.3)
- eventmachine (>= 1.0.3)
- http_parser.rb (>= 0.6.0)
- em-socksify (0.3.3)
- base64
- eventmachine (>= 1.0.0.beta.4)
erb (5.0.2)
ethon (0.15.0)
ffi (>= 1.15.0)
- eventmachine (1.2.7)
ffi (1.17.2-aarch64-linux-gnu)
ffi (1.17.2-aarch64-linux-musl)
ffi (1.17.2-arm-linux-gnu)
@@ -118,14 +107,13 @@ GEM
http-accept (1.7.0)
http-cookie (1.0.8)
domain_name (~> 0.5)
- http_parser.rb (0.8.0)
io-console (0.8.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
json (2.14.1)
- kettle-dev (1.1.26)
+ kettle-dev (1.1.27)
kettle-soup-cover (1.0.10)
simplecov (~> 0.22)
simplecov-cobertura (~> 3.0)
@@ -385,7 +373,6 @@ DEPENDENCIES
benchmark (~> 0.4, >= 0.4.1)
bundler-audit (~> 0.9.2)
debug (>= 1.1)
- em-http-request (~> 1.1.7)
erb (~> 5.0)
gem_bench (~> 2.0, >= 2.0.5)
gitmoji-regex (~> 1.0, >= 1.0.3)
diff --git a/README.md b/README.md
index 36baf72..c5dd3b0 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,10 @@
-[![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]][🖼️galtzo-discord] [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]][🖼️ruby-lang] [![oauth-tty Logo by Aboling0, CC BY-SA 4.0][🖼️oauth-tty-i]][🖼️oauth-tty]
+[![Galtzo FLOSS Logo by Aboling0, CC BY-SA 4.0][🖼️galtzo-i]][🖼️galtzo-discord] [![ruby-lang Logo, Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5][🖼️ruby-lang-i]][🖼️ruby-lang] [![oauth Logo by Chris Messina, CC BY-SA 3.0][🖼️oauth-tty-i]][🖼️oauth-tty]
[🖼️galtzo-i]: https://logos.galtzo.com/assets/images/galtzo-floss/avatar-192px.svg
[🖼️galtzo-discord]: https://discord.gg/3qme4XHNKN
[🖼️ruby-lang-i]: https://logos.galtzo.com/assets/images/ruby-lang/avatar-192px.svg
[🖼️ruby-lang]: https://www.ruby-lang.org/
-[🖼️oauth-tty-i]: https://logos.galtzo.com/assets/images/ruby-oauth/oauth-tty/avatar-192px.svg
+[🖼️oauth-tty-i]: https://logos.galtzo.com/assets/images/oauth/avatar-192px.svg
[🖼️oauth-tty]: https://github.com/ruby-oauth/oauth-tty
# 🖥️ OAuth::TTY
@@ -21,7 +21,21 @@
## 🌻 Synopsis
+OAuth 1.0a is an industry-standard protocol for authorization.
+It is an update to the original OAuth 1.0 protocol, and is used by many popular services.
+This is a RubyGem for implementing OAuth 1.0 or 1.0a _clients_ and _servers_ in Ruby applications.
+See the sibling `oauth2` gem for OAuth 2.0, 2.1, & OIDC clients in Ruby.
+
+All dependencies of this gem are signed, so it can be installed with a `HighSecurity` profile.
+
+* [OAuth 1.0 Spec][oauth1-spec]
+* [oauth gem][sibling-gem] for OAuth 1.0 / 1.0a client and server implementations in Ruby.
+* [oauth2 sibling gem][sibling2-gem] for OAuth 2.0 / 2.1, & OIDC client implementations in Ruby.
+
+[oauth1-spec]: http://oauth.net/core/1.0/
+[sibling-gem]: https://gitlab.com/ruby-oauth/oauth
+[sibling2-gem]: https://gitlab.com/ruby-oauth/oauth2
## 💡 Info you can shake a stick at
@@ -138,12 +152,141 @@ NOTE: Be prepared to track down certs for signed gems and add them the same way
## ⚙️ Configuration
+The oauth-tty gem is a thin CLI over the oauth gem. You supply your consumer credentials, token credentials (when applicable), a target URI, and optional parameters, and the tool signs requests or helps you complete an OAuth 1.0/1.0a 3-legged flow.
+
+What you can configure
+- Locations for OAuth parameters:
+ - --header (default): send OAuth params in Authorization header
+ - --body: send OAuth params in the request body
+ - --query-string: send OAuth params on the query string
+- HTTP method: --method GET|POST|PUT|DELETE|… (default: POST)
+- Signature method: --signature-method HMAC-SHA1|RSA-SHA1|PLAINTEXT (default: HMAC-SHA1)
+- OAuth version: --version 1.0 (default: 1.0) or --no-version to omit
+- Nonce/timestamp: auto-generated by default; can be overridden via --nonce and --timestamp
+- Verbose output: --verbose prints the full signing breakdown, headers, and signature base string
+- XMPP mode: --xmpp emits OAuth as an XMPP stanza instead of HTTP artifacts
+
+Required inputs (by command)
+- sign, query: --consumer-key, --consumer-secret, --token, --secret, and --uri
+- authorize: --consumer-key, --consumer-secret, the OAuth endpoints below, and --uri (service resource you’re authorizing for)
+
+Authorization endpoints (for oauth authorize)
+- --request-token-url URL
+- --authorize-url URL
+- --access-token-url URL
+- Optional: --callback-url URL (for 1.0a), --scope SCOPE (provider-specific)
+
+Providing options
+- CLI flags (preferred for quick usage)
+- Options file: use -O or --options to read additional arguments from a file. The file is tokenized by whitespace; put the same flags you’d pass on the command line, spread across lines as needed.
+
+Example options file (oauth.opts)
+```text
+--consumer-key ck_123
+--consumer-secret cs_456
+--token at_789
+--secret ats_abc
+--method GET
+--uri https://api.example.com/v1/profile
+--parameters foo:bar
+--parameters "status=active"
+--header
+```
+Run with: oauth sign -O oauth.opts
+Defaults at a glance
+- scheme: header
+- method: POST
+- signature method: HMAC-SHA1
+- oauth_version: 1.0 (omit with --no-version)
+- nonce, timestamp: auto-generated each run
+
+Tips
+- For parameters you can use either key:value or already-escaped pairs like key=value. Repeat --parameters to add multiple pairs.
+- When using --body, only methods that support bodies should be used (e.g., POST/PUT/PATCH).
+- Some providers require exact parameter ordering and inclusion; use --verbose to see normalized parameters and the signature base string.
## 🔧 Basic Usage
In a shell run `oauth` to start the console.
+Quick help and version
+```console
+oauth --help
+oauth --version # or oauth -v
+```
+
+Sign a request (minimal)
+Print just the OAuth signature value for a GET request:
+```console
+oauth sign \
+ --consumer-key ck \
+ --consumer-secret cs \
+ --token at \
+ --secret ats \
+ --method GET \
+ --uri "https://api.example.com/v1/resource?limit=10"
+```
+
+Sign a request (verbose, header output)
+```console
+oauth sign \
+ --consumer-key ck \
+ --consumer-secret cs \
+ --token at \
+ --secret ats \
+ --method POST \
+ --uri https://api.example.com/v1/resource \
+ --parameters "status=active" \
+ --header \
+ --verbose
+```
+This prints OAuth parameters, normalized parameters, signature base string, Authorization header, and both raw and escaped signatures.
+
+Query a protected resource
+Performs the signed HTTP request and prints the HTTP status and body.
+```console
+oauth query \
+ --consumer-key ck \
+ --consumer-secret cs \
+ --token at \
+ --secret ats \
+ --method GET \
+ --uri https://api.example.com/v1/profile \
+ --parameters "fields=id,name"
+```
+Notes:
+- The CLI will append any --parameters to the request URI’s query string and sign the request.
+- Use --body or --header/--query-string to influence where OAuth params go; query also constructs OAuth via the consumer internally.
+
+Start an OAuth 1.0a authorization flow
+Guides you to obtain an access token and token secret from a provider.
+```console
+oauth authorize \
+ --consumer-key ck \
+ --consumer-secret cs \
+ --request-token-url https://provider.example.com/oauth/request_token \
+ --authorize-url https://provider.example.com/oauth/authorize \
+ --access-token-url https://provider.example.com/oauth/access_token \
+ --callback-url https://yourapp.example.com/oauth/callback
+```
+What happens:
+- You’ll be shown an authorization URL to open in a browser.
+- After approving, the provider shows a verifier (PIN). Paste it back into the prompt.
+- The tool prints the access token and secret under “Response:”. Save those and use them with sign/query.
+
+Using an options file
+```console
+oauth sign -O oauth.opts
+```
+You can still add/override flags after -O; later flags win.
+
+For more examples
+- Run any command without args to see its specific help.
+- Browse the specs under spec/oauth/tty for additional scenarios and edge cases.
+
+In a shell run `oauth` to start the console.
+
For now, please see the tests for other usage.
## 🦷 FLOSS Funding
@@ -519,7 +662,7 @@ Thanks for RTFM. ☺️
[📌gitmoji]:https://gitmoji.dev
[📌gitmoji-img]:https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
-[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.225-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
+[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.007-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
[🔐security]: SECURITY.md
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
diff --git a/Rakefile b/Rakefile
index b6d8ffc..0355728 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# kettle-dev Rakefile v1.1.26 - 2025-09-20
+# kettle-dev Rakefile v1.1.27 - 2025-09-20
# Ruby 2.3 (Safe Navigation) or higher required
#
# MIT License (see License.txt)
diff --git a/gemfiles/head.gemfile b/gemfiles/head.gemfile
index 1cce6a3..252ea6d 100644
--- a/gemfiles/head.gemfile
+++ b/gemfiles/head.gemfile
@@ -2,6 +2,7 @@
source "https://rubygems.org"
+gem "cgi", ">= 0.5"
gem "oauth", branch: "main", git: "https://github.com/ruby-oauth/oauth"
gem "benchmark", "~> 0.4", ">= 0.4.1"
diff --git a/lib/oauth/tty/command.rb b/lib/oauth/tty/command.rb
index 71b437d..a810758 100644
--- a/lib/oauth/tty/command.rb
+++ b/lib/oauth/tty/command.rb
@@ -88,6 +88,25 @@ def option_parser
end
end
+ # Parse an array of CLI-like arguments into an options hash without mutating current state
+ # This is used by the -O/--options FILE feature to load args from a file and merge them
+ def parse_options(arguments)
+ original_options = @options
+ begin
+ temp_options = {}
+ @options = temp_options
+ _option_parser_defaults
+ OptionParser.new do |opts|
+ _option_parser_common(opts)
+ _option_parser_sign_and_query(opts)
+ _option_parser_authorization(opts)
+ end.parse!(arguments)
+ temp_options
+ ensure
+ @options = original_options
+ end
+ end
+
def _option_parser_defaults
options[:oauth_nonce] = OAuth::Helper.generate_key
options[:oauth_signature_method] = "HMAC-SHA1"
@@ -123,7 +142,8 @@ def _option_parser_common(opts)
end
opts.on("-O", "--options FILE", "Read options from a file") do |v|
- arguments = open(v).readlines.map { |l| l.chomp.split }.flatten
+ require "shellwords"
+ arguments = File.open(v).readlines.flat_map { |l| Shellwords.shellsplit(l.chomp) }
options2 = parse_options(arguments)
options.merge!(options2)
end
diff --git a/oauth-tty.gemspec b/oauth-tty.gemspec
index 69772fa..00326a8 100644
--- a/oauth-tty.gemspec
+++ b/oauth-tty.gemspec
@@ -18,8 +18,8 @@ Gem::Specification.new do |spec|
spec.authors = ["Thiago Pinto", "Peter Boling"]
spec.email = ["floss@galtzo.com", "oauth-ruby@googlegroups.com"]
- spec.summary = "🖥️ OAuth 1.0 TTY CLI"
- spec.description = "🖥️ OAuth 1.0 TTY Command Line Interface"
+ spec.summary = "🖥️ OAuth 1.0 / 1.0a TTY CLI"
+ spec.description = "🖥️ OAuth 1.0 / 1.0a TTY Command Line Interface"
spec.homepage = "https://github.com/ruby-oauth/oauth-tty"
spec.licenses = ["MIT"]
spec.required_ruby_version = ">= 2.3.0"
@@ -124,7 +124,6 @@ Gem::Specification.new do |spec|
# Testing
spec.add_development_dependency("appraisal2", "~> 3.0") # ruby >= 1.8.7, for testing against multiple versions of dependencies
- spec.add_development_dependency("em-http-request", "~> 1.1.7")
spec.add_development_dependency("kettle-test", "~> 1.0") # ruby >= 2.3
spec.add_development_dependency("mocha")
spec.add_development_dependency("rack", "~> 2.0")
diff --git a/spec/oauth/tty/command_spec.rb b/spec/oauth/tty/command_spec.rb
index ddb7855..c2a1e7e 100644
--- a/spec/oauth/tty/command_spec.rb
+++ b/spec/oauth/tty/command_spec.rb
@@ -186,4 +186,9 @@ def build_cmd(args = [])
expect(stderr.read).to eq("err!\n")
end
end
+
+ it "has no required options by default" do
+ command = described_class.new(stdout, stdin, stderr, [])
+ expect(command.required_options).to eq([])
+ end
end
diff --git a/spec/oauth/tty/authorize_command_spec.rb b/spec/oauth/tty/commands/authorize_command_spec.rb
similarity index 100%
rename from spec/oauth/tty/authorize_command_spec.rb
rename to spec/oauth/tty/commands/authorize_command_spec.rb
diff --git a/spec/oauth/tty/commands/help_command_spec.rb b/spec/oauth/tty/commands/help_command_spec.rb
new file mode 100644
index 0000000..c026940
--- /dev/null
+++ b/spec/oauth/tty/commands/help_command_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+RSpec.describe OAuth::TTY::Commands::HelpCommand, :check_output do
+ let(:stdout) { StringIO.new }
+ let(:stdin) { StringIO.new }
+ let(:stderr) { StringIO.new }
+
+ def run_cli(command, argv = [])
+ cli = OAuth::TTY::CLI.new(stdout, stdin, stderr, command, argv.dup)
+ cli.run
+ stdout.rewind
+ out = stdout.read
+ stdout.truncate(0)
+ stdout.rewind
+ out
+ end
+
+ it "prints usage and lists available commands" do
+ out = run_cli("help", [])
+
+ expect(out).to include("Usage: oauth COMMAND")
+ expect(out).to include("authorize")
+ expect(out).to include("query")
+ expect(out).to include("sign")
+ expect(out).to include("version")
+ expect(out).to include("help")
+
+ # no errors expected
+ expect(stderr.string).to eq("")
+ end
+end
diff --git a/spec/oauth/tty/commands/query_command_spec.rb b/spec/oauth/tty/commands/query_command_spec.rb
new file mode 100644
index 0000000..b316b4d
--- /dev/null
+++ b/spec/oauth/tty/commands/query_command_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+RSpec.describe OAuth::TTY::Commands::QueryCommand, :check_output do
+ let(:stdout) { StringIO.new }
+ let(:stdin) { StringIO.new }
+ let(:stderr) { StringIO.new }
+
+ def run_cli(command, argv)
+ cli = OAuth::TTY::CLI.new(stdout, stdin, stderr, command, argv.dup)
+ cli.run
+ stdout.rewind
+ out = stdout.read
+ stdout.truncate(0)
+ stdout.rewind
+ out
+ end
+
+ it "appends parameters to the URI, performs the request, and prints status and body" do
+ # Stub network objects so we don't make real HTTP calls
+ consumer = instance_double("OAuth::Consumer")
+ allow(OAuth::Consumer).to receive(:new).and_return(consumer)
+
+ access_token = instance_double("OAuth::AccessToken")
+ allow(OAuth::AccessToken).to receive(:new).with(consumer, "at_789", "ats_abc").and_return(access_token)
+
+ fake_response = instance_double("Net::HTTPResponse", code: "200", message: "OK", body: "Hello world")
+
+ # Build args
+ uri = "https://api.example.com/v1/profile"
+ argv = [
+ "--consumer-key",
+ "ck_123",
+ "--consumer-secret",
+ "cs_456",
+ "--token",
+ "at_789",
+ "--secret",
+ "ats_abc",
+ "--method",
+ "GET",
+ "--nonce",
+ "abc123",
+ "--timestamp",
+ "1699999999",
+ "--uri",
+ uri,
+ "--parameters",
+ "foo:bar",
+ "--parameters",
+ "status=active",
+ ]
+
+ expected_url = "#{uri}?oauth_consumer_key=ck_123&oauth_nonce=abc123&oauth_timestamp=1699999999&oauth_token=at_789&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&foo=bar&status=active"
+
+ expect(access_token).to receive(:request).with(:get, expected_url).and_return(fake_response)
+
+ out = run_cli("query", argv)
+
+ # First line prints the final URL used
+ # Second line prints status line
+ # Third line prints response body
+ expect(out).to include(expected_url)
+ expect(out).to include("200 OK")
+ expect(out).to include("Hello world")
+
+ # no errors expected
+ expect(stderr.string).to eq("")
+ end
+end
diff --git a/spec/oauth/tty/commands/sign_command_spec.rb b/spec/oauth/tty/commands/sign_command_spec.rb
new file mode 100644
index 0000000..d625146
--- /dev/null
+++ b/spec/oauth/tty/commands/sign_command_spec.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+RSpec.describe OAuth::TTY::Commands::SignCommand, :check_output do
+ let(:stdout) { StringIO.new }
+ # Use a path relative to the project spec/ root so specs are idempotent regardless of CWD
+ let(:fixture_path) { File.expand_path("../../../support/fixtures/oauth.opts", __dir__) }
+ let(:stdin) { StringIO.new }
+ let(:stderr) { StringIO.new }
+
+ # Helper to run CLI and capture stdout string
+ def run_cli(command, argv)
+ cli = OAuth::TTY::CLI.new(stdout, stdin, stderr, command, argv.dup)
+ cli.run
+ stdout.rewind
+ out = stdout.read
+ stdout.truncate(0)
+ stdout.rewind
+ out
+ end
+
+ it "loads options from a file with -O and produces same signature as inline args (quoted values supported)" do
+ require "shellwords"
+ # Signature via -O file
+ sig_from_file = run_cli("sign", ["-O", fixture_path])
+
+ # Signature via equivalent inline args (tokens parsed like a shell)
+ inline_args = File.readlines(fixture_path, chomp: true).flat_map { |l| Shellwords.shellsplit(l) }
+ sig_inline = run_cli("sign", inline_args)
+
+ expect(sig_from_file).to eq(sig_inline)
+ # Basic sanity: looks like a base64 signature string
+ expect(sig_from_file.strip).to match(/^[A-Za-z0-9+\/]+=*\n?$/)
+ end
+
+ it "allows later flags to override values loaded from the options file (later flags win)" do
+ require "shellwords"
+ # Compute expected signature when overriding the method to POST
+ inline_args = File.readlines(fixture_path, chomp: true).flat_map { |l| Shellwords.shellsplit(l) }
+ overridden_inline = inline_args.dup
+ # Replace method to POST (remove any existing --method value then add POST at the end)
+ # Simpler approach: just append --method POST to override prior value
+ overridden_inline += ["--method", "POST"]
+
+ expected_sig = run_cli("sign", overridden_inline)
+
+ # Now via -O file + later CLI flags
+ sig_from_file_then_override = run_cli("sign", ["-O", fixture_path, "--method", "POST"])
+
+ expect(sig_from_file_then_override).to eq(expected_sig)
+ end
+
+ it "prints non-OAuth parameters block and a trailing blank line in verbose mode" do
+ args = [
+ "--consumer-key",
+ "ck_123",
+ "--consumer-secret",
+ "cs_456",
+ "--token",
+ "at_789",
+ "--secret",
+ "ats_abc",
+ "--nonce",
+ "4d7b2e0f9a",
+ "--timestamp",
+ "1699999999",
+ "--method",
+ "GET",
+ "--uri",
+ "https://api.example.com/v1/profile",
+ "--parameters",
+ "foo:bar",
+ "--parameters",
+ "status=active",
+ "--header",
+ "--verbose",
+ ]
+
+ out = run_cli("sign", args)
+
+ expect(out).to include("OAuth parameters:")
+ # Ensure the non-oauth Parameters section is printed
+ expect(out).to include("Parameters:")
+ expect(out).to include(" foo: bar")
+ expect(out).to include(" status: active")
+ # Ensure there is a blank line after the non-oauth parameters block
+ expect(out).to match(/Parameters:\n(?: .*\n)+\n/)
+ end
+
+ it "in XMPP verbose mode prints stanza and note, and omits normalized params" do
+ args = [
+ "--consumer-key",
+ "ck_123",
+ "--consumer-secret",
+ "cs_456",
+ "--token",
+ "at_789",
+ "--secret",
+ "ats_abc",
+ "--nonce",
+ "4d7b2e0f9a",
+ "--timestamp",
+ "1699999999",
+ "--uri",
+ "https://api.example.com/v1/xmpp",
+ "--parameters",
+ "foo:bar",
+ "--verbose",
+ "--xmpp",
+ ]
+
+ out = run_cli("sign", args)
+
+ # In XMPP mode, normalized params line is suppressed
+ expect(out).not_to include("Normalized params:")
+
+ expect(out).to include("Signature base string:")
+ expect(out).to include("XMPP Stanza:")
+ expect(out).to include("")
+ expect(out).to include("Note: You may want to use bare JIDs in your URI.")
+
+ # Still prints signature summaries
+ expect(out).to include("Signature:")
+ expect(out).to include("Escaped signature:")
+ end
+end
diff --git a/spec/oauth/tty/commands/version_command_spec.rb b/spec/oauth/tty/commands/version_command_spec.rb
new file mode 100644
index 0000000..c9188f5
--- /dev/null
+++ b/spec/oauth/tty/commands/version_command_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.describe OAuth::TTY::Commands::VersionCommand, :check_output do
+ let(:stdout) { StringIO.new }
+ let(:stdin) { StringIO.new }
+ let(:stderr) { StringIO.new }
+
+ def run_cli(command, argv = [])
+ cli = OAuth::TTY::CLI.new(stdout, stdin, stderr, command, argv.dup)
+ cli.run
+ stdout.rewind
+ out = stdout.read
+ stdout.truncate(0)
+ stdout.rewind
+ out
+ end
+
+ it "prints versions for oauth and oauth-tty gems" do
+ out = run_cli("version")
+
+ expect(out).to include("OAuth Gem #{OAuth::Version::VERSION}")
+ expect(out).to include("OAuth TTY Gem #{OAuth::TTY::Version::VERSION}")
+
+ # no errors expected
+ expect(stderr.string).to eq("")
+ end
+end
diff --git a/spec/oauth/tty/sign_command_spec.rb b/spec/oauth/tty/sign_command_spec.rb
deleted file mode 100644
index 5a8aa5a..0000000
--- a/spec/oauth/tty/sign_command_spec.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.describe OAuth::TTY::Commands::SignCommand do
- let(:stdout) { StringIO.new }
- let(:stdin) { StringIO.new }
- let(:stderr) { StringIO.new }
-
- def build_cmd(args = [])
- described_class.new(stdout, stdin, stderr, args)
- end
-
- describe "non-verbose output", :check_output do
- it "prints only the signature" do
- request = double(
- "Request",
- oauth_parameters: {},
- non_oauth_parameters: {},
- oauth_signature: "SIG",
- )
-
- expect(OAuth::RequestProxy).to receive(:proxy).and_return(request)
- expect(request).to receive(:sign!).with(consumer_secret: "CS", token_secret: "TS")
-
- build_cmd(%w[
- --consumer-key
- CK
- --consumer-secret
- CS
- --token
- TK
- --secret
- TS
- --uri
- https://example.com
- ]).run
-
- stdout.rewind
- expect(stdout.read).to eq("SIG\n")
- end
- end
-
- describe "verbose output with xmpp", :check_output do
- it "prints detailed information and an XMPP stanza" do
- request = double(
- "Request",
- oauth_parameters: {
- "oauth_consumer_key" => "CK",
- "oauth_token" => "TK",
- "oauth_signature_method" => "HMAC-SHA1",
- "oauth_timestamp" => "TS",
- "oauth_nonce" => "NONCE",
- "oauth_version" => "1.0",
- },
- non_oauth_parameters: {},
- method: "POST",
- uri: "https://example.com",
- normalized_parameters: "a=1&b=2",
- signature_base_string: "BASE",
- oauth_signature: "SIG",
- oauth_consumer_key: "CK",
- oauth_token: "TK",
- oauth_signature_method: "HMAC-SHA1",
- oauth_timestamp: "TS",
- oauth_nonce: "NONCE",
- oauth_version: "1.0",
- )
-
- # Stub out silent verbose interactions with OAuth::Consumer
- consumer = instance_double("OAuth::Consumer")
- req_token = instance_double("OAuth::RequestToken")
- expect(OAuth::Consumer).to receive(:new).and_return(consumer)
- expect(consumer).to receive(:get_request_token).and_return(req_token)
- allow(req_token).to receive(:callback_confirmed?).and_return(false)
- allow(req_token).to receive(:authorize_url).and_return("https://example.com/authorize")
- allow(req_token).to receive(:get_access_token).and_return(instance_double("AccessToken"))
-
- expect(OAuth::RequestProxy).to receive(:proxy).and_return(request)
- expect(request).to receive(:sign!).with(consumer_secret: "CS", token_secret: "TS")
-
- build_cmd(%w[
- --consumer-key
- CK
- --consumer-secret
- CS
- --token
- TK
- --secret
- TS
- --uri
- https://example.com
- --verbose
- --xmpp
- ]).run
-
- stdout.rewind
- out = stdout.read
- expect(out).to include("OAuth parameters:")
- expect(out).to include("XMPP Stanza:")
- expect(out).to include("CK")
- expect(out).to include("Escaped signature: ")
- # In XMPP mode, it should not print normalized params line
- expect(out).not_to include("Normalized params:")
- end
- end
-end
diff --git a/spec/support/fixtures/oauth.opts b/spec/support/fixtures/oauth.opts
new file mode 100644
index 0000000..9277049
--- /dev/null
+++ b/spec/support/fixtures/oauth.opts
@@ -0,0 +1,11 @@
+--consumer-key ck_123
+--consumer-secret cs_456
+--token at_789
+--secret ats_abc
+--nonce 4d7b2e0f9a
+--timestamp 1699999999
+--method GET
+--uri https://api.example.com/v1/profile
+--parameters foo:bar
+--parameters "status=active"
+--header