tastytrade 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/commands/release-pr.md +108 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- data/.github/ISSUE_TEMPLATE/roadmap_task.md +34 -0
- data/.github/dependabot.yml +11 -0
- data/.github/workflows/main.yml +75 -0
- data/.rspec +3 -0
- data/.rubocop.yml +101 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +100 -0
- data/CLAUDE.md +78 -0
- data/CODE_OF_CONDUCT.md +81 -0
- data/CONTRIBUTING.md +89 -0
- data/DISCLAIMER.md +54 -0
- data/LICENSE.txt +24 -0
- data/README.md +235 -0
- data/ROADMAP.md +157 -0
- data/Rakefile +17 -0
- data/SECURITY.md +48 -0
- data/docs/getting_started.md +48 -0
- data/docs/python_sdk_analysis.md +181 -0
- data/exe/tastytrade +8 -0
- data/lib/tastytrade/cli.rb +604 -0
- data/lib/tastytrade/cli_config.rb +79 -0
- data/lib/tastytrade/cli_helpers.rb +178 -0
- data/lib/tastytrade/client.rb +117 -0
- data/lib/tastytrade/keyring_store.rb +72 -0
- data/lib/tastytrade/models/account.rb +129 -0
- data/lib/tastytrade/models/account_balance.rb +75 -0
- data/lib/tastytrade/models/base.rb +47 -0
- data/lib/tastytrade/models/current_position.rb +155 -0
- data/lib/tastytrade/models/user.rb +23 -0
- data/lib/tastytrade/models.rb +7 -0
- data/lib/tastytrade/session.rb +164 -0
- data/lib/tastytrade/session_manager.rb +160 -0
- data/lib/tastytrade/version.rb +5 -0
- data/lib/tastytrade.rb +31 -0
- data/sig/tastytrade.rbs +4 -0
- data/spec/exe/tastytrade_spec.rb +104 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/tastytrade/cli_accounts_spec.rb +166 -0
- data/spec/tastytrade/cli_auth_spec.rb +216 -0
- data/spec/tastytrade/cli_config_spec.rb +180 -0
- data/spec/tastytrade/cli_helpers_spec.rb +248 -0
- data/spec/tastytrade/cli_interactive_spec.rb +54 -0
- data/spec/tastytrade/cli_logout_spec.rb +121 -0
- data/spec/tastytrade/cli_select_spec.rb +174 -0
- data/spec/tastytrade/cli_status_spec.rb +206 -0
- data/spec/tastytrade/client_spec.rb +210 -0
- data/spec/tastytrade/keyring_store_spec.rb +168 -0
- data/spec/tastytrade/models/account_balance_spec.rb +247 -0
- data/spec/tastytrade/models/account_spec.rb +206 -0
- data/spec/tastytrade/models/base_spec.rb +61 -0
- data/spec/tastytrade/models/current_position_spec.rb +444 -0
- data/spec/tastytrade/models/user_spec.rb +58 -0
- data/spec/tastytrade/session_manager_spec.rb +296 -0
- data/spec/tastytrade/session_spec.rb +392 -0
- data/spec/tastytrade_spec.rb +9 -0
- metadata +303 -0
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "tastytrade/cli_helpers"
|
5
|
+
require "tastytrade/cli_config"
|
6
|
+
|
7
|
+
RSpec.describe Tastytrade::CLIHelpers do
|
8
|
+
# Create a test class that includes the module
|
9
|
+
let(:test_class) do
|
10
|
+
Class.new do
|
11
|
+
include Tastytrade::CLIHelpers
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:instance) { test_class.new }
|
16
|
+
|
17
|
+
describe "output helpers" do
|
18
|
+
describe "#error" do
|
19
|
+
it "outputs red error message to stderr" do
|
20
|
+
expect { instance.error("Something went wrong") }
|
21
|
+
.to output(/Error: Something went wrong/).to_stderr
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#warning" do
|
26
|
+
it "outputs yellow warning message to stderr" do
|
27
|
+
expect { instance.warning("Be careful") }
|
28
|
+
.to output(/Warning: Be careful/).to_stderr
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#success" do
|
33
|
+
it "outputs green success message with checkmark" do
|
34
|
+
expect { instance.success("Operation completed") }
|
35
|
+
.to output(/✓ Operation completed/).to_stdout
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#info" do
|
40
|
+
it "outputs cyan info message with arrow" do
|
41
|
+
expect { instance.info("Processing...") }
|
42
|
+
.to output(/→ Processing.../).to_stdout
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#format_currency" do
|
48
|
+
it "formats nil as $0.00" do
|
49
|
+
expect(instance.format_currency(nil)).to eq("$0.00")
|
50
|
+
end
|
51
|
+
|
52
|
+
it "formats zero as $0.00" do
|
53
|
+
expect(instance.format_currency(0)).to eq("$0.00")
|
54
|
+
end
|
55
|
+
|
56
|
+
it "formats positive values" do
|
57
|
+
expect(instance.format_currency(1234.56)).to eq("$1,234.56")
|
58
|
+
end
|
59
|
+
|
60
|
+
it "formats negative values" do
|
61
|
+
expect(instance.format_currency(-1234.56)).to eq("-$1,234.56")
|
62
|
+
end
|
63
|
+
|
64
|
+
it "formats small values" do
|
65
|
+
expect(instance.format_currency(0.99)).to eq("$0.99")
|
66
|
+
end
|
67
|
+
|
68
|
+
it "formats large values with commas" do
|
69
|
+
expect(instance.format_currency(1_234_567.89)).to eq("$1,234,567.89")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#color_value" do
|
74
|
+
it "colors positive values green" do
|
75
|
+
result = instance.color_value(100)
|
76
|
+
expect(result).to include("$100.00")
|
77
|
+
# Pastel adds ANSI codes, so we check for presence of the value
|
78
|
+
expect(instance.pastel.strip(result)).to eq("$100.00")
|
79
|
+
end
|
80
|
+
|
81
|
+
it "colors negative values red" do
|
82
|
+
result = instance.color_value(-100)
|
83
|
+
expect(result).to include("$100.00")
|
84
|
+
expect(instance.pastel.strip(result)).to eq("-$100.00")
|
85
|
+
end
|
86
|
+
|
87
|
+
it "colors zero as dim" do
|
88
|
+
result = instance.color_value(0)
|
89
|
+
expect(instance.pastel.strip(result)).to eq("$0.00")
|
90
|
+
end
|
91
|
+
|
92
|
+
it "can format without currency" do
|
93
|
+
result = instance.color_value(42, format_as_currency: false)
|
94
|
+
expect(instance.pastel.strip(result)).to eq("42")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#pastel" do
|
99
|
+
it "returns a Pastel instance" do
|
100
|
+
expect(instance.pastel).to be_a(Pastel::Delegator)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "memoizes the instance" do
|
104
|
+
pastel1 = instance.pastel
|
105
|
+
pastel2 = instance.pastel
|
106
|
+
expect(pastel1).to be(pastel2)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#prompt" do
|
111
|
+
it "returns a TTY::Prompt instance" do
|
112
|
+
expect(instance.prompt).to be_a(TTY::Prompt)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "memoizes the instance" do
|
116
|
+
prompt1 = instance.prompt
|
117
|
+
prompt2 = instance.prompt
|
118
|
+
expect(prompt1).to be(prompt2)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "#config" do
|
123
|
+
it "returns a Config instance" do
|
124
|
+
expect(instance.config).to be_a(Tastytrade::CLIConfig)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "memoizes the instance" do
|
128
|
+
config1 = instance.config
|
129
|
+
config2 = instance.config
|
130
|
+
expect(config1).to be(config2)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "authentication helpers" do
|
135
|
+
describe "#authenticated?" do
|
136
|
+
context "when no session exists" do
|
137
|
+
it "returns false" do
|
138
|
+
expect(instance.authenticated?).to be false
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe "#require_authentication!" do
|
144
|
+
context "when not authenticated" do
|
145
|
+
it "exits with error message" do
|
146
|
+
expect(instance).to receive(:exit).with(1)
|
147
|
+
expect { instance.require_authentication! }
|
148
|
+
.to output(/You must be logged in/).to_stderr
|
149
|
+
end
|
150
|
+
|
151
|
+
it "suggests login command" do
|
152
|
+
expect(instance).to receive(:exit).with(1)
|
153
|
+
expect { instance.require_authentication! }
|
154
|
+
.to output(/Run 'tastytrade login'/).to_stdout
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe "class methods" do
|
161
|
+
it "sets exit_on_failure? to true" do
|
162
|
+
expect(test_class.exit_on_failure?).to be true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "#current_account" do
|
167
|
+
let(:session) { instance_double(Tastytrade::Session) }
|
168
|
+
let(:config) { instance_double(Tastytrade::CLIConfig) }
|
169
|
+
let(:account) do
|
170
|
+
instance_double(Tastytrade::Models::Account,
|
171
|
+
account_number: "5WX12345",
|
172
|
+
nickname: "Test Account")
|
173
|
+
end
|
174
|
+
|
175
|
+
before do
|
176
|
+
allow(instance).to receive(:current_session).and_return(session)
|
177
|
+
allow(instance).to receive(:config).and_return(config)
|
178
|
+
end
|
179
|
+
|
180
|
+
context "when account number is saved" do
|
181
|
+
before do
|
182
|
+
allow(config).to receive(:get).with("current_account_number").and_return("5WX12345")
|
183
|
+
allow(Tastytrade::Models::Account).to receive(:get)
|
184
|
+
.with(session, "5WX12345").and_return(account)
|
185
|
+
end
|
186
|
+
|
187
|
+
it "returns the account" do
|
188
|
+
expect(instance.current_account).to eq(account)
|
189
|
+
end
|
190
|
+
|
191
|
+
it "caches the account" do
|
192
|
+
expect(Tastytrade::Models::Account).to receive(:get).once
|
193
|
+
2.times { instance.current_account }
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
context "when no account number is saved" do
|
198
|
+
before do
|
199
|
+
allow(config).to receive(:get).with("current_account_number").and_return(nil)
|
200
|
+
end
|
201
|
+
|
202
|
+
it "returns nil" do
|
203
|
+
expect(instance.current_account).to be_nil
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
context "when account fetch fails" do
|
208
|
+
before do
|
209
|
+
allow(config).to receive(:get).with("current_account_number").and_return("5WX12345")
|
210
|
+
allow(Tastytrade::Models::Account).to receive(:get)
|
211
|
+
.and_raise(StandardError, "Network error")
|
212
|
+
end
|
213
|
+
|
214
|
+
it "returns nil" do
|
215
|
+
expect { instance.current_account }.to output(/Failed to load current account/).to_stderr
|
216
|
+
expect(instance.current_account).to be_nil
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
describe "#current_account_number" do
|
222
|
+
let(:config) { instance_double(Tastytrade::CLIConfig) }
|
223
|
+
|
224
|
+
before do
|
225
|
+
allow(instance).to receive(:config).and_return(config)
|
226
|
+
end
|
227
|
+
|
228
|
+
context "when account number is saved" do
|
229
|
+
before do
|
230
|
+
allow(config).to receive(:get).with("current_account_number").and_return("5WX12345")
|
231
|
+
end
|
232
|
+
|
233
|
+
it "returns the account number" do
|
234
|
+
expect(instance.current_account_number).to eq("5WX12345")
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
context "when no account number is saved" do
|
239
|
+
before do
|
240
|
+
allow(config).to receive(:get).with("current_account_number").and_return(nil)
|
241
|
+
end
|
242
|
+
|
243
|
+
it "returns nil" do
|
244
|
+
expect(instance.current_account_number).to be_nil
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "tastytrade/cli"
|
5
|
+
|
6
|
+
RSpec.describe "Tastytrade::CLI interactive command" do
|
7
|
+
let(:cli) { Tastytrade::CLI.new }
|
8
|
+
let(:session) { instance_double(Tastytrade::Session) }
|
9
|
+
let(:prompt) { instance_double(TTY::Prompt) }
|
10
|
+
let(:config) { instance_double(Tastytrade::CLIConfig) }
|
11
|
+
|
12
|
+
before do
|
13
|
+
allow(cli).to receive(:prompt).and_return(prompt)
|
14
|
+
allow(cli).to receive(:config).and_return(config)
|
15
|
+
allow(cli).to receive(:exit)
|
16
|
+
allow(config).to receive(:get).with("current_account_number").and_return(nil)
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#interactive" do
|
20
|
+
context "when authenticated" do
|
21
|
+
before do
|
22
|
+
allow(cli).to receive(:current_session).and_return(session)
|
23
|
+
allow(cli).to receive(:interactive_mode)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "enters interactive mode" do
|
27
|
+
expect(cli).to receive(:interactive_mode)
|
28
|
+
cli.interactive
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "when not authenticated" do
|
33
|
+
before do
|
34
|
+
allow(cli).to receive(:current_session).and_return(nil)
|
35
|
+
allow(cli).to receive(:exit).with(1).and_raise(SystemExit)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "requires authentication" do
|
39
|
+
expect { cli.interactive }.to raise_error(SystemExit)
|
40
|
+
.and output(/You must be logged in/).to_stderr
|
41
|
+
end
|
42
|
+
|
43
|
+
it "suggests login command" do
|
44
|
+
expect { cli.interactive }.to raise_error(SystemExit)
|
45
|
+
.and output(/Run 'tastytrade login'/).to_stdout
|
46
|
+
end
|
47
|
+
|
48
|
+
it "does not enter interactive mode" do
|
49
|
+
expect(cli).not_to receive(:interactive_mode)
|
50
|
+
expect { cli.interactive }.to raise_error(SystemExit)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "tastytrade/cli"
|
5
|
+
|
6
|
+
RSpec.describe "Tastytrade::CLI logout command" do
|
7
|
+
let(:cli) { Tastytrade::CLI.new }
|
8
|
+
let(:config) { instance_double(Tastytrade::CLIConfig) }
|
9
|
+
let(:session_manager) { instance_double(Tastytrade::SessionManager) }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow(cli).to receive(:config).and_return(config)
|
13
|
+
allow(cli).to receive(:exit) # Prevent actual exit during tests
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#logout" do
|
17
|
+
context "with active session" do
|
18
|
+
before do
|
19
|
+
allow(config).to receive(:get).with("current_username").and_return("test@example.com")
|
20
|
+
allow(config).to receive(:get).with("environment").and_return("production")
|
21
|
+
allow(Tastytrade::SessionManager).to receive(:new).with(
|
22
|
+
username: "test@example.com",
|
23
|
+
environment: "production"
|
24
|
+
).and_return(session_manager)
|
25
|
+
end
|
26
|
+
|
27
|
+
context "when logout succeeds" do
|
28
|
+
before do
|
29
|
+
allow(session_manager).to receive(:clear_session!).and_return(true)
|
30
|
+
allow(config).to receive(:delete)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "clears session credentials" do
|
34
|
+
expect(session_manager).to receive(:clear_session!)
|
35
|
+
cli.logout
|
36
|
+
end
|
37
|
+
|
38
|
+
it "removes config entries" do
|
39
|
+
expect(config).to receive(:delete).with("current_username")
|
40
|
+
expect(config).to receive(:delete).with("environment")
|
41
|
+
expect(config).to receive(:delete).with("last_login")
|
42
|
+
cli.logout
|
43
|
+
end
|
44
|
+
|
45
|
+
it "displays success message" do
|
46
|
+
expect { cli.logout }.to output(/Successfully logged out/).to_stdout
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when logout fails" do
|
51
|
+
before do
|
52
|
+
allow(session_manager).to receive(:clear_session!).and_return(false)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "displays error message" do
|
56
|
+
expect(cli).to receive(:exit).with(1)
|
57
|
+
expect { cli.logout }.to output(/Failed to logout completely/).to_stderr
|
58
|
+
end
|
59
|
+
|
60
|
+
it "exits with status 1" do
|
61
|
+
expect(cli).to receive(:exit).with(1)
|
62
|
+
cli.logout
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with sandbox environment" do
|
68
|
+
before do
|
69
|
+
allow(config).to receive(:get).with("current_username").and_return("test@example.com")
|
70
|
+
allow(config).to receive(:get).with("environment").and_return("sandbox")
|
71
|
+
allow(session_manager).to receive(:clear_session!).and_return(true)
|
72
|
+
allow(config).to receive(:delete)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "creates session manager with sandbox environment" do
|
76
|
+
expect(Tastytrade::SessionManager).to receive(:new).with(
|
77
|
+
username: "test@example.com",
|
78
|
+
environment: "sandbox"
|
79
|
+
).and_return(session_manager)
|
80
|
+
cli.logout
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "without active session" do
|
85
|
+
before do
|
86
|
+
allow(config).to receive(:get).with("current_username").and_return(nil)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "displays warning message" do
|
90
|
+
expect { cli.logout }.to output(/No active session found/).to_stderr
|
91
|
+
end
|
92
|
+
|
93
|
+
it "doesn't attempt to clear session" do
|
94
|
+
expect(Tastytrade::SessionManager).not_to receive(:new)
|
95
|
+
cli.logout
|
96
|
+
end
|
97
|
+
|
98
|
+
it "doesn't exit with error" do
|
99
|
+
expect(cli).not_to receive(:exit)
|
100
|
+
cli.logout
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "with missing environment" do
|
105
|
+
before do
|
106
|
+
allow(config).to receive(:get).with("current_username").and_return("test@example.com")
|
107
|
+
allow(config).to receive(:get).with("environment").and_return(nil)
|
108
|
+
allow(session_manager).to receive(:clear_session!).and_return(true)
|
109
|
+
allow(config).to receive(:delete)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "defaults to production environment" do
|
113
|
+
expect(Tastytrade::SessionManager).to receive(:new).with(
|
114
|
+
username: "test@example.com",
|
115
|
+
environment: "production"
|
116
|
+
).and_return(session_manager)
|
117
|
+
cli.logout
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "tastytrade/cli"
|
5
|
+
|
6
|
+
RSpec.describe "Tastytrade::CLI select command" do
|
7
|
+
let(:cli) { Tastytrade::CLI.new }
|
8
|
+
let(:config) { instance_double(Tastytrade::CLIConfig) }
|
9
|
+
let(:prompt) { instance_double(TTY::Prompt) }
|
10
|
+
let(:session) { instance_double(Tastytrade::Session) }
|
11
|
+
let(:account1) do
|
12
|
+
instance_double(Tastytrade::Models::Account,
|
13
|
+
account_number: "5WX12345",
|
14
|
+
nickname: "Main Account",
|
15
|
+
account_type_name: "Margin")
|
16
|
+
end
|
17
|
+
let(:account2) do
|
18
|
+
instance_double(Tastytrade::Models::Account,
|
19
|
+
account_number: "5WX67890",
|
20
|
+
nickname: nil,
|
21
|
+
account_type_name: "Cash")
|
22
|
+
end
|
23
|
+
|
24
|
+
before do
|
25
|
+
allow(cli).to receive(:config).and_return(config)
|
26
|
+
allow(cli).to receive(:prompt).and_return(prompt)
|
27
|
+
allow(cli).to receive(:exit) # Prevent actual exit during tests
|
28
|
+
allow(cli).to receive(:current_session).and_return(session)
|
29
|
+
allow(cli).to receive(:authenticated?).and_return(true)
|
30
|
+
allow(config).to receive(:get).with("current_account_number").and_return(nil)
|
31
|
+
allow(config).to receive(:set)
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#select" do
|
35
|
+
context "when not authenticated" do
|
36
|
+
before do
|
37
|
+
allow(cli).to receive(:authenticated?).and_return(false)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "requires authentication" do
|
41
|
+
expect(cli).to receive(:require_authentication!)
|
42
|
+
allow(Tastytrade::Models::Account).to receive(:get_all).with(session).and_return([])
|
43
|
+
cli.select
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with no accounts" do
|
48
|
+
before do
|
49
|
+
allow(Tastytrade::Models::Account).to receive(:get_all).with(session).and_return([])
|
50
|
+
end
|
51
|
+
|
52
|
+
it "returns early with warning" do
|
53
|
+
expect { cli.select }.to output(/No accounts found/).to_stderr
|
54
|
+
end
|
55
|
+
|
56
|
+
it "doesn't prompt for selection" do
|
57
|
+
expect(prompt).not_to receive(:select)
|
58
|
+
cli.select
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "with single account" do
|
63
|
+
before do
|
64
|
+
allow(Tastytrade::Models::Account).to receive(:get_all).with(session).and_return([account1])
|
65
|
+
end
|
66
|
+
|
67
|
+
it "auto-selects the account" do
|
68
|
+
expect(config).to receive(:set).with("current_account_number", "5WX12345")
|
69
|
+
cli.select
|
70
|
+
end
|
71
|
+
|
72
|
+
it "displays success message" do
|
73
|
+
expect { cli.select }.to output(/Using account: 5WX12345/).to_stdout
|
74
|
+
end
|
75
|
+
|
76
|
+
it "doesn't prompt for selection" do
|
77
|
+
expect(prompt).not_to receive(:select)
|
78
|
+
cli.select
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "with multiple accounts" do
|
83
|
+
before do
|
84
|
+
allow(Tastytrade::Models::Account).to receive(:get_all).with(session).and_return([account1, account2])
|
85
|
+
end
|
86
|
+
|
87
|
+
it "prompts for account selection" do
|
88
|
+
expect(prompt).to receive(:select).with(
|
89
|
+
"Choose an account:",
|
90
|
+
[
|
91
|
+
{ name: "5WX12345 - Main Account (Margin)", value: "5WX12345" },
|
92
|
+
{ name: "5WX67890 (Cash)", value: "5WX67890" }
|
93
|
+
]
|
94
|
+
).and_return("5WX12345")
|
95
|
+
|
96
|
+
cli.select
|
97
|
+
end
|
98
|
+
|
99
|
+
it "saves selected account" do
|
100
|
+
allow(prompt).to receive(:select).and_return("5WX67890")
|
101
|
+
|
102
|
+
expect(config).to receive(:set).with("current_account_number", "5WX67890")
|
103
|
+
cli.select
|
104
|
+
end
|
105
|
+
|
106
|
+
it "displays success message" do
|
107
|
+
allow(prompt).to receive(:select).and_return("5WX12345")
|
108
|
+
|
109
|
+
expect { cli.select }.to output(/Selected account: 5WX12345/).to_stdout
|
110
|
+
end
|
111
|
+
|
112
|
+
context "with current account selected" do
|
113
|
+
before do
|
114
|
+
allow(config).to receive(:get).with("current_account_number").and_return("5WX12345")
|
115
|
+
end
|
116
|
+
|
117
|
+
it "marks current account in choices" do
|
118
|
+
expect(prompt).to receive(:select).with(
|
119
|
+
"Choose an account:",
|
120
|
+
[
|
121
|
+
{ name: "5WX12345 - Main Account (Margin) [current]", value: "5WX12345" },
|
122
|
+
{ name: "5WX67890 (Cash)", value: "5WX67890" }
|
123
|
+
]
|
124
|
+
).and_return("5WX12345")
|
125
|
+
|
126
|
+
cli.select
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "with account without nickname" do
|
131
|
+
it "handles nil nickname properly" do
|
132
|
+
expect(prompt).to receive(:select).with(
|
133
|
+
"Choose an account:",
|
134
|
+
[
|
135
|
+
{ name: "5WX12345 - Main Account (Margin)", value: "5WX12345" },
|
136
|
+
{ name: "5WX67890 (Cash)", value: "5WX67890" }
|
137
|
+
]
|
138
|
+
).and_return("5WX67890")
|
139
|
+
|
140
|
+
cli.select
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context "on API error" do
|
146
|
+
before do
|
147
|
+
allow(Tastytrade::Models::Account).to receive(:get_all).with(session)
|
148
|
+
.and_raise(Tastytrade::Error, "Network error")
|
149
|
+
end
|
150
|
+
|
151
|
+
it "displays error message" do
|
152
|
+
expect(cli).to receive(:exit).with(1)
|
153
|
+
expect { cli.select }.to output(/Failed to fetch accounts: Network error/).to_stderr
|
154
|
+
end
|
155
|
+
|
156
|
+
it "exits with status 1" do
|
157
|
+
expect(cli).to receive(:exit).with(1)
|
158
|
+
cli.select
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context "on unexpected error" do
|
163
|
+
before do
|
164
|
+
allow(Tastytrade::Models::Account).to receive(:get_all).with(session)
|
165
|
+
.and_raise(StandardError, "Unexpected issue")
|
166
|
+
end
|
167
|
+
|
168
|
+
it "displays generic error message" do
|
169
|
+
expect(cli).to receive(:exit).with(1)
|
170
|
+
expect { cli.select }.to output(/Unexpected error: Unexpected issue/).to_stderr
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|