Skip to content

Commit 65f452b

Browse files
committed
✅ More tests
1 parent 6c396f3 commit 65f452b

File tree

4 files changed

+350
-1
lines changed

4 files changed

+350
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Compatible with MRI Ruby 2.3+, and concordant releases of JRuby, and TruffleRuby
5050
### Federated DVCS
5151

5252
<details>
53-
<summary>Find this repo on other forges (Coming soon!)</summary>
53+
<summary>Find this repo on other forges</summary>
5454

5555
| Federated [DVCS][💎d-in-dvcs] Repository | Status | Issues | PRs | Wiki | CI | Discussions |
5656
|-------------------------------------------------|-----------------------------------------------------------------------|---------------------------|--------------------------|---------------------------|--------------------------|------------------------------|
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe OAuth::TTY::Commands::AuthorizeCommand do
4+
let(:stdout) { StringIO.new }
5+
let(:stdin) { StringIO.new }
6+
let(:stderr) { StringIO.new }
7+
8+
def build_cmd(args = [])
9+
described_class.new(stdout, stdin, stderr, args)
10+
end
11+
12+
describe "#_run happy path with callback confirmed", :check_output do
13+
it "sets version to 1.0a and prompts for verifier, then prints response params" do
14+
consumer = instance_double("OAuth::Consumer")
15+
request_token = instance_double("OAuth::RequestToken")
16+
access_token = instance_double("OAuth::AccessToken", params: {"oauth_token" => "OTK", :symbol_key => "ignored"})
17+
18+
# Provide deterministic nonce/timestamp used by defaults through Command
19+
allow(OAuth::Helper).to receive(:generate_key).and_return("KEY")
20+
allow(OAuth::Helper).to receive(:generate_timestamp).and_return("TS")
21+
22+
expect(OAuth::Consumer).to receive(:new).and_return(consumer)
23+
expect(consumer).to receive(:get_request_token).with({oauth_callback: nil}, {}).and_return(request_token)
24+
25+
expect(request_token).to receive(:callback_confirmed?).and_return(true)
26+
expect(request_token).to receive(:authorize_url).and_return("https://example.com/authorize")
27+
28+
# stdin provides a verifier when version is 1.0a
29+
stdin.write("VERIFIER\n")
30+
stdin.rewind
31+
32+
expect(request_token).to receive(:get_access_token).with({oauth_verifier: "VERIFIER"}).and_return(access_token)
33+
34+
out_before = stdout.string.dup
35+
build_cmd(%w[--consumer-key CK --consumer-secret CS --method GET --uri https://example.com]).run
36+
37+
stdout.rewind
38+
out = stdout.read
39+
expect(out).to include("Server appears to support OAuth 1.0a; enabling support.")
40+
expect(out).to include("Please visit this url to authorize:")
41+
expect(out).to include("https://example.com/authorize")
42+
expect(out).to include("Please enter the verification code provided by the SP (oauth_verifier):")
43+
expect(out).to include("Response:")
44+
# Only non-symbol keys are printed
45+
expect(out).to include(" oauth_token: OTK")
46+
expect(out).not_to include("symbol_key")
47+
end
48+
end
49+
50+
describe "error handling", :check_output do
51+
it "alerts when get_request_token raises OAuth::Unauthorized" do
52+
consumer = instance_double("OAuth::Consumer")
53+
54+
expect(OAuth::Consumer).to receive(:new).and_return(consumer)
55+
Request = Struct.new(:body, :code, :message)
56+
error = OAuth::Unauthorized.new(Request.new("denied", 401, "401 Unauthorized"))
57+
expect(consumer).to receive(:get_request_token).and_raise(error)
58+
59+
build_cmd(%w[--consumer-key CK --consumer-secret CS --uri https://example.com]).send(:get_request_token)
60+
61+
stderr.rewind
62+
err = stderr.read
63+
expect(err).to include("A problem occurred while attempting to authorize:")
64+
expect(err).to include("denied")
65+
end
66+
67+
it "alerts when get_access_token raises OAuth::Unauthorized" do
68+
consumer = instance_double("OAuth::Consumer")
69+
request_token = instance_double("OAuth::RequestToken")
70+
expect(OAuth::Consumer).to receive(:new).and_return(consumer)
71+
expect(consumer).to receive(:get_request_token).and_return(request_token)
72+
73+
Request2 = Struct.new(:body, :code, :message)
74+
error = OAuth::Unauthorized.new(Request2.new("bad_access", 401, "401 Unauthorized"))
75+
expect(request_token).to receive(:callback_confirmed?).and_return(false)
76+
expect(request_token).to receive(:authorize_url).and_return("https://example.com/authorize")
77+
expect(request_token).to receive(:get_access_token).and_raise(error)
78+
79+
build_cmd(%w[--consumer-key CK --consumer-secret CS --uri https://example.com]).run
80+
81+
stderr.rewind
82+
err = stderr.read
83+
expect(err).to include("A problem occurred while attempting to obtain an access token:")
84+
expect(err).to include("bad_access")
85+
end
86+
end
87+
end

spec/oauth/tty/command_spec.rb

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe OAuth::TTY::Command do
4+
let(:stdout) { StringIO.new }
5+
let(:stdin) { StringIO.new }
6+
let(:stderr) { StringIO.new }
7+
8+
# Minimal concrete subclass to enable exercising #run paths
9+
class TestCommand < described_class
10+
attr_writer :required
11+
12+
def initialize(stdout, stdin, stderr, arguments)
13+
super
14+
@required ||= []
15+
end
16+
17+
def required_options
18+
@required
19+
end
20+
21+
def _run
22+
puts "ran" # use provided stdout
23+
end
24+
end
25+
26+
def build_cmd(args = [])
27+
TestCommand.new(stdout, stdin, stderr, args)
28+
end
29+
30+
describe "#run", :check_output do
31+
it "executes _run when required options are present" do
32+
cmd = build_cmd(["--consumer-key", "ck"])
33+
cmd.required = [:oauth_consumer_key]
34+
out_before = stdout.string.dup
35+
cmd.run
36+
stdout.rewind
37+
expect(stdout.read).to eq(out_before + "ran\n")
38+
end
39+
40+
it "prints missing options and help when required options are absent" do
41+
cmd = build_cmd([])
42+
cmd.required = [:oauth_consumer_key]
43+
44+
expect(OAuth::TTY::CLI).to receive(:puts_red).with("Options missing to OAuth CLI: --oauth_consumer_key")
45+
cmd.run
46+
stdout.rewind
47+
expect(stdout.read).to match(/Usage: oauth <command> \[ARGS\]/)
48+
end
49+
end
50+
51+
describe "option parser defaults and flags" do
52+
it "sets sane defaults" do
53+
cmd = build_cmd([])
54+
expect(cmd.send(:options)).to include(
55+
oauth_signature_method: "HMAC-SHA1",
56+
oauth_version: "1.0",
57+
scheme: :header,
58+
method: :post,
59+
params: [],
60+
version: "1.0"
61+
)
62+
# Non-deterministic values are present but not asserted for exact value
63+
expect(cmd.send(:options)).to include(:oauth_nonce, :oauth_timestamp)
64+
end
65+
66+
it "switches scheme based on -B/--body, -H/--header, -Q/--query-string" do
67+
expect(build_cmd(["-B"]).send(:options)[:scheme]).to eq(:body)
68+
expect(build_cmd(["-H"]).send(:options)[:scheme]).to eq(:header)
69+
expect(build_cmd(["-Q"]).send(:options)[:scheme]).to eq(:query_string)
70+
end
71+
72+
it "captures verbosity and xmpp flags" do
73+
expect(build_cmd(["--verbose"]).send(:options)[:verbose]).to be true
74+
expect(build_cmd(["--xmpp"]).send(:options)).to include(xmpp: true)
75+
# Exercise predicate helpers
76+
cmd = build_cmd(["--xmpp", "--verbose"])
77+
expect(cmd.send(:xmpp?)).to be true
78+
expect(cmd.send(:verbose?)).to be true
79+
end
80+
81+
it "collects sign/query related switches" do
82+
cmd = build_cmd(%w[
83+
--method GET
84+
--nonce N
85+
--parameters a:1
86+
--parameters raw_pair
87+
--signature-method PLAINTEXT
88+
--token T
89+
--secret S
90+
--timestamp TS
91+
--realm R
92+
--uri http://example.com/
93+
--version 1.0a
94+
])
95+
expect(cmd.send(:options)).to include(
96+
method: "GET",
97+
oauth_nonce: "N",
98+
oauth_signature_method: "PLAINTEXT",
99+
oauth_token: "T",
100+
oauth_token_secret: "S",
101+
oauth_timestamp: "TS",
102+
realm: "R",
103+
uri: "http://example.com/",
104+
oauth_version: "1.0a"
105+
)
106+
expect(cmd.send(:options)[:params]).to eq(["a:1", "raw_pair"])
107+
end
108+
109+
it "honors --no-version by nulling oauth_version" do
110+
cmd = build_cmd(["--no-version"])
111+
expect(cmd.send(:options)[:oauth_version]).to be_nil
112+
end
113+
114+
it "captures authorization URLs and scope" do
115+
cmd = build_cmd(%w[
116+
--access-token-url https://example.com/access
117+
--authorize-url https://example.com/auth
118+
--callback-url https://example.com/cb
119+
--request-token-url https://example.com/request
120+
--scope email
121+
])
122+
expect(cmd.send(:options)).to include(
123+
access_token_url: "https://example.com/access",
124+
authorize_url: "https://example.com/auth",
125+
oauth_callback: "https://example.com/cb",
126+
request_token_url: "https://example.com/request",
127+
scope: "email"
128+
)
129+
end
130+
end
131+
132+
describe "#parameters" do
133+
it "builds escaped params and merges oauth keys, dropping nil/empty ones" do
134+
cmd = build_cmd(%w[
135+
--consumer-key CK
136+
--token TK
137+
--parameters foo:bar
138+
--parameters baz:qux
139+
--parameters raw=pair
140+
])
141+
params = cmd.send(:parameters)
142+
# CGI.parse returns arrays of values per key
143+
expect(params["foo"]).to eq(["bar"]) # escaped colon-pair
144+
expect(params["baz"]).to eq(["qux"]) # escaped colon-pair
145+
expect(params["raw"]).to eq(["pair"]) # raw pair preserved
146+
# OAuth fields present
147+
expect(params).to include(
148+
"oauth_consumer_key" => "CK",
149+
"oauth_token" => "TK",
150+
"oauth_signature_method" => "HMAC-SHA1"
151+
)
152+
# timestamp, nonce exist but are not asserted exactly
153+
expect(params).to include("oauth_timestamp", "oauth_nonce")
154+
end
155+
end
156+
157+
describe "output helpers", :check_output do
158+
it "writes to stdout and stderr" do
159+
cmd = build_cmd([])
160+
cmd.send(:puts, "out!")
161+
cmd.send(:alert, "err!")
162+
stdout.rewind
163+
stderr.rewind
164+
expect(stdout.read).to eq("out!\n")
165+
expect(stderr.read).to eq("err!\n")
166+
end
167+
end
168+
end
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe OAuth::TTY::Commands::SignCommand do
4+
let(:stdout) { StringIO.new }
5+
let(:stdin) { StringIO.new }
6+
let(:stderr) { StringIO.new }
7+
8+
def build_cmd(args = [])
9+
described_class.new(stdout, stdin, stderr, args)
10+
end
11+
12+
describe "non-verbose output", :check_output do
13+
it "prints only the signature" do
14+
request = double("Request",
15+
oauth_parameters: {},
16+
non_oauth_parameters: {},
17+
oauth_signature: "SIG",
18+
)
19+
20+
expect(OAuth::RequestProxy).to receive(:proxy).and_return(request)
21+
expect(request).to receive(:sign!).with(consumer_secret: "CS", token_secret: "TS")
22+
23+
build_cmd(%w[
24+
--consumer-key CK
25+
--consumer-secret CS
26+
--token TK
27+
--secret TS
28+
--uri https://example.com
29+
]).run
30+
31+
stdout.rewind
32+
expect(stdout.read).to eq("SIG\n")
33+
end
34+
end
35+
36+
describe "verbose output with xmpp", :check_output do
37+
it "prints detailed information and an XMPP stanza" do
38+
request = double(
39+
"Request",
40+
oauth_parameters: {
41+
"oauth_consumer_key" => "CK",
42+
"oauth_token" => "TK",
43+
"oauth_signature_method" => "HMAC-SHA1",
44+
"oauth_timestamp" => "TS",
45+
"oauth_nonce" => "NONCE",
46+
"oauth_version" => "1.0",
47+
},
48+
non_oauth_parameters: {},
49+
method: "POST",
50+
uri: "https://example.com",
51+
normalized_parameters: "a=1&b=2",
52+
signature_base_string: "BASE",
53+
oauth_signature: "SIG",
54+
oauth_consumer_key: "CK",
55+
oauth_token: "TK",
56+
oauth_signature_method: "HMAC-SHA1",
57+
oauth_timestamp: "TS",
58+
oauth_nonce: "NONCE",
59+
oauth_version: "1.0",
60+
)
61+
62+
# Stub out silent verbose interactions with OAuth::Consumer
63+
consumer = instance_double("OAuth::Consumer")
64+
req_token = instance_double("OAuth::RequestToken")
65+
expect(OAuth::Consumer).to receive(:new).and_return(consumer)
66+
expect(consumer).to receive(:get_request_token).and_return(req_token)
67+
allow(req_token).to receive(:callback_confirmed?).and_return(false)
68+
allow(req_token).to receive(:authorize_url).and_return("https://example.com/authorize")
69+
allow(req_token).to receive(:get_access_token).and_return(instance_double("AccessToken"))
70+
71+
expect(OAuth::RequestProxy).to receive(:proxy).and_return(request)
72+
expect(request).to receive(:sign!).with(consumer_secret: "CS", token_secret: "TS")
73+
74+
build_cmd(%w[
75+
--consumer-key CK
76+
--consumer-secret CS
77+
--token TK
78+
--secret TS
79+
--uri https://example.com
80+
--verbose
81+
--xmpp
82+
]).run
83+
84+
stdout.rewind
85+
out = stdout.read
86+
expect(out).to include("OAuth parameters:")
87+
expect(out).to include("XMPP Stanza:")
88+
expect(out).to include("<oauth_consumer_key>CK</oauth_consumer_key>")
89+
expect(out).to include("Escaped signature: ")
90+
# In XMPP mode, it should not print normalized params line
91+
expect(out).not_to include("Normalized params:")
92+
end
93+
end
94+
end

0 commit comments

Comments
 (0)