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,296 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "tastytrade/session_manager"
|
5
|
+
require "tastytrade/keyring_store"
|
6
|
+
require "tastytrade/cli_config"
|
7
|
+
|
8
|
+
RSpec.describe Tastytrade::SessionManager do
|
9
|
+
let(:username) { "test@example.com" }
|
10
|
+
let(:environment) { "production" }
|
11
|
+
let(:manager) { described_class.new(username: username, environment: environment) }
|
12
|
+
let(:session) { instance_double(Tastytrade::Session) }
|
13
|
+
let(:config) { instance_double(Tastytrade::CLIConfig) }
|
14
|
+
|
15
|
+
before do
|
16
|
+
allow(Tastytrade::CLIConfig).to receive(:new).and_return(config)
|
17
|
+
allow(config).to receive(:set)
|
18
|
+
allow(config).to receive(:delete)
|
19
|
+
allow(Tastytrade::KeyringStore).to receive(:available?).and_return(true)
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#initialize" do
|
23
|
+
it "sets username and environment" do
|
24
|
+
expect(manager.username).to eq(username)
|
25
|
+
expect(manager.environment).to eq(environment)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "defaults to production environment" do
|
29
|
+
manager = described_class.new(username: username)
|
30
|
+
expect(manager.environment).to eq("production")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#save_session" do
|
35
|
+
let(:session_token) { "session_token_123" }
|
36
|
+
let(:remember_token) { "remember_token_456" }
|
37
|
+
|
38
|
+
before do
|
39
|
+
allow(session).to receive(:session_token).and_return(session_token)
|
40
|
+
allow(session).to receive(:remember_token).and_return(remember_token)
|
41
|
+
allow(session).to receive(:session_expiration).and_return(nil)
|
42
|
+
allow(Tastytrade::KeyringStore).to receive(:set).and_return(true)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "saves session token" do
|
46
|
+
expect(Tastytrade::KeyringStore).to receive(:set)
|
47
|
+
.with("token_test@example.com_production", session_token)
|
48
|
+
|
49
|
+
manager.save_session(session)
|
50
|
+
end
|
51
|
+
|
52
|
+
context "with session expiration" do
|
53
|
+
let(:expiration_time) { Time.now + 3600 }
|
54
|
+
|
55
|
+
before do
|
56
|
+
allow(session).to receive(:session_expiration).and_return(expiration_time)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "saves session expiration" do
|
60
|
+
expect(Tastytrade::KeyringStore).to receive(:set)
|
61
|
+
.with("session_expiration_test@example.com_production", expiration_time.iso8601)
|
62
|
+
|
63
|
+
manager.save_session(session)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it "saves config data" do
|
68
|
+
expect(config).to receive(:set).with("current_username", username)
|
69
|
+
expect(config).to receive(:set).with("environment", environment)
|
70
|
+
expect(config).to receive(:set).with("last_login", anything)
|
71
|
+
|
72
|
+
manager.save_session(session)
|
73
|
+
end
|
74
|
+
|
75
|
+
context "with remember option" do
|
76
|
+
it "saves remember token and password" do
|
77
|
+
expect(Tastytrade::KeyringStore).to receive(:set)
|
78
|
+
.with("remember_test@example.com_production", remember_token)
|
79
|
+
expect(Tastytrade::KeyringStore).to receive(:set)
|
80
|
+
.with("password_test@example.com_production", "secret123")
|
81
|
+
|
82
|
+
manager.save_session(session, password: "secret123", remember: true)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "doesn't save password if keyring unavailable" do
|
86
|
+
allow(Tastytrade::KeyringStore).to receive(:available?).and_return(false)
|
87
|
+
|
88
|
+
expect(Tastytrade::KeyringStore).not_to receive(:set)
|
89
|
+
.with("password_test@example.com_production", anything)
|
90
|
+
|
91
|
+
manager.save_session(session, password: "secret123", remember: true)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context "without remember option" do
|
96
|
+
it "doesn't save remember token or password" do
|
97
|
+
expect(Tastytrade::KeyringStore).not_to receive(:set)
|
98
|
+
.with("remember_test@example.com_production", anything)
|
99
|
+
expect(Tastytrade::KeyringStore).not_to receive(:set)
|
100
|
+
.with("password_test@example.com_production", anything)
|
101
|
+
|
102
|
+
manager.save_session(session, password: "secret123", remember: false)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
it "handles errors gracefully" do
|
107
|
+
allow(Tastytrade::KeyringStore).to receive(:set).and_raise(StandardError, "Save error")
|
108
|
+
|
109
|
+
expect { manager.save_session(session) }.to output(/Failed to save session/).to_stderr
|
110
|
+
expect(manager.save_session(session)).to be false
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "#load_session" do
|
115
|
+
context "with saved token" do
|
116
|
+
before do
|
117
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
118
|
+
.with("token_test@example.com_production").and_return("saved_token")
|
119
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
120
|
+
.with("remember_test@example.com_production").and_return("saved_remember")
|
121
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
122
|
+
.with("session_expiration_test@example.com_production").and_return(nil)
|
123
|
+
end
|
124
|
+
|
125
|
+
it "returns session data hash" do
|
126
|
+
result = manager.load_session
|
127
|
+
|
128
|
+
expect(result).to eq({
|
129
|
+
session_token: "saved_token",
|
130
|
+
remember_token: "saved_remember",
|
131
|
+
session_expiration: nil,
|
132
|
+
username: username,
|
133
|
+
environment: environment
|
134
|
+
})
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "without saved token" do
|
139
|
+
before do
|
140
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
141
|
+
.with("token_test@example.com_production").and_return(nil)
|
142
|
+
end
|
143
|
+
|
144
|
+
it "returns nil" do
|
145
|
+
expect(manager.load_session).to be_nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "#restore_session" do
|
151
|
+
let(:new_session) { instance_double(Tastytrade::Session) }
|
152
|
+
|
153
|
+
before do
|
154
|
+
allow(Tastytrade::Session).to receive(:new).and_return(new_session)
|
155
|
+
allow(new_session).to receive(:login).and_return(new_session)
|
156
|
+
end
|
157
|
+
|
158
|
+
context "with saved remember token" do
|
159
|
+
before do
|
160
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
161
|
+
.with("password_test@example.com_production").and_return(nil)
|
162
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
163
|
+
.with("remember_test@example.com_production").and_return("saved_remember")
|
164
|
+
end
|
165
|
+
|
166
|
+
it "creates session with remember token" do
|
167
|
+
expect(Tastytrade::Session).to receive(:new).with(
|
168
|
+
username: username,
|
169
|
+
password: nil,
|
170
|
+
remember_token: "saved_remember",
|
171
|
+
is_test: false
|
172
|
+
)
|
173
|
+
|
174
|
+
manager.restore_session
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context "with saved password" do
|
179
|
+
before do
|
180
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
181
|
+
.with("remember_test@example.com_production").and_return(nil)
|
182
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
183
|
+
.with("password_test@example.com_production").and_return("saved_password")
|
184
|
+
end
|
185
|
+
|
186
|
+
it "creates session with password" do
|
187
|
+
expect(Tastytrade::Session).to receive(:new).with(
|
188
|
+
username: username,
|
189
|
+
password: "saved_password",
|
190
|
+
remember_token: nil,
|
191
|
+
is_test: false
|
192
|
+
)
|
193
|
+
|
194
|
+
manager.restore_session
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "with sandbox environment" do
|
199
|
+
let(:environment) { "sandbox" }
|
200
|
+
|
201
|
+
before do
|
202
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
203
|
+
.with("password_test@example.com_sandbox").and_return("saved_password")
|
204
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
205
|
+
.with("remember_test@example.com_sandbox").and_return(nil)
|
206
|
+
end
|
207
|
+
|
208
|
+
it "sets is_test flag" do
|
209
|
+
expect(Tastytrade::Session).to receive(:new).with(
|
210
|
+
hash_including(is_test: true)
|
211
|
+
)
|
212
|
+
|
213
|
+
manager.restore_session
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
context "without saved credentials" do
|
218
|
+
before do
|
219
|
+
allow(Tastytrade::KeyringStore).to receive(:get).and_return(nil)
|
220
|
+
end
|
221
|
+
|
222
|
+
it "returns nil" do
|
223
|
+
expect(manager.restore_session).to be_nil
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
it "handles errors gracefully" do
|
228
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
229
|
+
.with("password_test@example.com_production").and_return("password")
|
230
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
231
|
+
.with("remember_test@example.com_production").and_return(nil)
|
232
|
+
allow(new_session).to receive(:login).and_raise(StandardError, "Login error")
|
233
|
+
|
234
|
+
expect { manager.restore_session }.to output(/Failed to restore session/).to_stderr
|
235
|
+
expect(manager.restore_session).to be_nil
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe "#clear_session!" do
|
240
|
+
it "deletes all stored credentials" do
|
241
|
+
expect(Tastytrade::KeyringStore).to receive(:delete)
|
242
|
+
.with("token_test@example.com_production")
|
243
|
+
expect(Tastytrade::KeyringStore).to receive(:delete)
|
244
|
+
.with("remember_test@example.com_production")
|
245
|
+
expect(Tastytrade::KeyringStore).to receive(:delete)
|
246
|
+
.with("password_test@example.com_production")
|
247
|
+
expect(Tastytrade::KeyringStore).to receive(:delete)
|
248
|
+
.with("session_expiration_test@example.com_production")
|
249
|
+
|
250
|
+
manager.clear_session!
|
251
|
+
end
|
252
|
+
|
253
|
+
it "clears config data" do
|
254
|
+
expect(config).to receive(:delete).with("current_username")
|
255
|
+
expect(config).to receive(:delete).with("last_login")
|
256
|
+
|
257
|
+
manager.clear_session!
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
describe "#saved_credentials?" do
|
262
|
+
context "with saved password" do
|
263
|
+
before do
|
264
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
265
|
+
.with("password_test@example.com_production").and_return("password")
|
266
|
+
end
|
267
|
+
|
268
|
+
it "returns true" do
|
269
|
+
expect(manager.saved_credentials?).to be true
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
context "with saved remember token" do
|
274
|
+
before do
|
275
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
276
|
+
.with("password_test@example.com_production").and_return(nil)
|
277
|
+
allow(Tastytrade::KeyringStore).to receive(:get)
|
278
|
+
.with("remember_test@example.com_production").and_return("token")
|
279
|
+
end
|
280
|
+
|
281
|
+
it "returns true" do
|
282
|
+
expect(manager.saved_credentials?).to be true
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
context "without saved credentials" do
|
287
|
+
before do
|
288
|
+
allow(Tastytrade::KeyringStore).to receive(:get).and_return(nil)
|
289
|
+
end
|
290
|
+
|
291
|
+
it "returns false" do
|
292
|
+
expect(manager.saved_credentials?).to be false
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
@@ -0,0 +1,392 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Tastytrade::Session do
|
6
|
+
let(:username) { "testuser" }
|
7
|
+
let(:password) { "testpass" }
|
8
|
+
let(:client) { instance_double(Tastytrade::Client) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
allow(Tastytrade::Client).to receive(:new).and_return(client)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#initialize" do
|
15
|
+
it "creates session with default settings" do
|
16
|
+
session = described_class.new(username: username, password: password)
|
17
|
+
|
18
|
+
expect(session.is_test).to be false
|
19
|
+
expect(session.user).to be_nil
|
20
|
+
expect(session.session_token).to be_nil
|
21
|
+
end
|
22
|
+
|
23
|
+
it "creates session with test environment" do
|
24
|
+
session = described_class.new(username: username, password: password, is_test: true)
|
25
|
+
|
26
|
+
expect(session.is_test).to be true
|
27
|
+
expect(Tastytrade::Client).to have_received(:new).with(base_url: Tastytrade::CERT_URL, timeout: 30)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates session with remember_me" do
|
31
|
+
session = described_class.new(username: username, password: password, remember_me: true)
|
32
|
+
|
33
|
+
expect(session.remember_token).to be_nil # Not set until login
|
34
|
+
end
|
35
|
+
|
36
|
+
it "creates session with remember_token" do
|
37
|
+
remember_token = "existing-remember-token"
|
38
|
+
session = described_class.new(username: username, remember_token: remember_token)
|
39
|
+
|
40
|
+
expect(session.remember_token).to eq(remember_token)
|
41
|
+
expect(session.instance_variable_get(:@password)).to be_nil
|
42
|
+
end
|
43
|
+
|
44
|
+
it "creates session with timeout" do
|
45
|
+
session = described_class.new(username: username, password: password, timeout: 60)
|
46
|
+
|
47
|
+
expect(Tastytrade::Client).to have_received(:new).with(base_url: Tastytrade::API_URL, timeout: 60)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#login" do
|
52
|
+
let(:session) { described_class.new(username: username, password: password) }
|
53
|
+
let(:login_response) do
|
54
|
+
{
|
55
|
+
"data" => {
|
56
|
+
"user" => {
|
57
|
+
"email" => "test@example.com",
|
58
|
+
"username" => "testuser",
|
59
|
+
"external-id" => "ext-123"
|
60
|
+
},
|
61
|
+
"session-token" => "test-session-token"
|
62
|
+
}
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
it "authenticates and sets user data" do
|
67
|
+
expect(client).to receive(:post).with("/sessions", {
|
68
|
+
"login" => username,
|
69
|
+
"password" => password,
|
70
|
+
"remember-me" => false
|
71
|
+
}).and_return(login_response)
|
72
|
+
|
73
|
+
result = session.login
|
74
|
+
|
75
|
+
expect(result).to eq(session) # Returns self for chaining
|
76
|
+
expect(session.user).to be_a(Tastytrade::Models::User)
|
77
|
+
expect(session.user.email).to eq("test@example.com")
|
78
|
+
expect(session.session_token).to eq("test-session-token")
|
79
|
+
end
|
80
|
+
|
81
|
+
context "with remember_me enabled" do
|
82
|
+
let(:session) { described_class.new(username: username, password: password, remember_me: true) }
|
83
|
+
let(:login_response) do
|
84
|
+
super().tap do |response|
|
85
|
+
response["data"]["remember-token"] = "test-remember-token"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it "stores remember token" do
|
90
|
+
expect(client).to receive(:post).with("/sessions", {
|
91
|
+
"login" => username,
|
92
|
+
"password" => password,
|
93
|
+
"remember-me" => true
|
94
|
+
}).and_return(login_response)
|
95
|
+
|
96
|
+
session.login
|
97
|
+
|
98
|
+
expect(session.remember_token).to eq("test-remember-token")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "with remember_token authentication" do
|
103
|
+
let(:remember_token) { "existing-remember-token" }
|
104
|
+
let(:session) { described_class.new(username: username, remember_token: remember_token) }
|
105
|
+
|
106
|
+
it "authenticates using remember token" do
|
107
|
+
expect(client).to receive(:post).with("/sessions", {
|
108
|
+
"login" => username,
|
109
|
+
"remember-token" => remember_token,
|
110
|
+
"remember-me" => false
|
111
|
+
}).and_return(login_response)
|
112
|
+
|
113
|
+
session.login
|
114
|
+
|
115
|
+
expect(session.session_token).to eq("test-session-token")
|
116
|
+
end
|
117
|
+
|
118
|
+
it "does not send password when using remember token" do
|
119
|
+
expect(client).to receive(:post) do |_path, body|
|
120
|
+
expect(body).not_to have_key("password")
|
121
|
+
login_response
|
122
|
+
end
|
123
|
+
|
124
|
+
session.login
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "with session expiration" do
|
129
|
+
let(:login_response_with_expiration) do
|
130
|
+
login_response.tap do |response|
|
131
|
+
response["data"]["session-expiration"] = "2024-01-01T12:00:00Z"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it "parses and stores session expiration" do
|
136
|
+
expect(client).to receive(:post).and_return(login_response_with_expiration)
|
137
|
+
|
138
|
+
session.login
|
139
|
+
|
140
|
+
expect(session.session_expiration).to be_a(Time)
|
141
|
+
expect(session.session_expiration.iso8601).to eq("2024-01-01T12:00:00Z")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#validate" do
|
147
|
+
let(:session) { described_class.new(username: username, password: password) }
|
148
|
+
let(:user) { instance_double(Tastytrade::Models::User, email: "test@example.com") }
|
149
|
+
|
150
|
+
before do
|
151
|
+
session.instance_variable_set(:@user, user)
|
152
|
+
session.instance_variable_set(:@session_token, "token")
|
153
|
+
end
|
154
|
+
|
155
|
+
it "returns true for valid session" do
|
156
|
+
expect(client).to receive(:get).with("/sessions/validate", {}, { "Authorization" => "token" })
|
157
|
+
.and_return({ "data" => { "email" => "test@example.com" } })
|
158
|
+
|
159
|
+
expect(session.validate).to be true
|
160
|
+
end
|
161
|
+
|
162
|
+
it "returns false for invalid session" do
|
163
|
+
expect(client).to receive(:get).with("/sessions/validate", {}, { "Authorization" => "token" })
|
164
|
+
.and_return({ "data" => { "email" => "different@example.com" } })
|
165
|
+
|
166
|
+
expect(session.validate).to be false
|
167
|
+
end
|
168
|
+
|
169
|
+
it "returns false on error" do
|
170
|
+
expect(client).to receive(:get).and_raise(Tastytrade::Error, "Unauthorized")
|
171
|
+
|
172
|
+
expect(session.validate).to be false
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "#destroy" do
|
177
|
+
let(:session) { described_class.new(username: username, password: password) }
|
178
|
+
|
179
|
+
before do
|
180
|
+
session.instance_variable_set(:@session_token, "token")
|
181
|
+
session.instance_variable_set(:@user, "user")
|
182
|
+
session.instance_variable_set(:@remember_token, "remember")
|
183
|
+
end
|
184
|
+
|
185
|
+
it "sends DELETE request and clears session data" do
|
186
|
+
expect(client).to receive(:delete).with("/sessions", { "Authorization" => "token" })
|
187
|
+
|
188
|
+
session.destroy
|
189
|
+
|
190
|
+
expect(session.session_token).to be_nil
|
191
|
+
expect(session.user).to be_nil
|
192
|
+
expect(session.remember_token).to be_nil
|
193
|
+
end
|
194
|
+
|
195
|
+
it "does nothing if not authenticated" do
|
196
|
+
session.instance_variable_set(:@session_token, nil)
|
197
|
+
|
198
|
+
expect(client).not_to receive(:delete)
|
199
|
+
|
200
|
+
session.destroy
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe "HTTP methods" do
|
205
|
+
let(:session) { described_class.new(username: username, password: password) }
|
206
|
+
let(:auth_headers) { { "Authorization" => "token" } }
|
207
|
+
|
208
|
+
before do
|
209
|
+
session.instance_variable_set(:@session_token, "token")
|
210
|
+
end
|
211
|
+
|
212
|
+
describe "#get" do
|
213
|
+
it "makes authenticated GET request" do
|
214
|
+
expect(client).to receive(:get).with("/test", { foo: "bar" }, auth_headers)
|
215
|
+
.and_return({ "data" => "result" })
|
216
|
+
|
217
|
+
result = session.get("/test", { foo: "bar" })
|
218
|
+
|
219
|
+
expect(result).to eq({ "data" => "result" })
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe "#post" do
|
224
|
+
it "makes authenticated POST request" do
|
225
|
+
body = { key: "value" }
|
226
|
+
expect(client).to receive(:post).with("/test", body, auth_headers)
|
227
|
+
.and_return({ "data" => "result" })
|
228
|
+
|
229
|
+
result = session.post("/test", body)
|
230
|
+
|
231
|
+
expect(result).to eq({ "data" => "result" })
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
describe "#put" do
|
236
|
+
it "makes authenticated PUT request" do
|
237
|
+
body = { key: "value" }
|
238
|
+
expect(client).to receive(:put).with("/test", body, auth_headers)
|
239
|
+
.and_return({ "data" => "result" })
|
240
|
+
|
241
|
+
result = session.put("/test", body)
|
242
|
+
|
243
|
+
expect(result).to eq({ "data" => "result" })
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
describe "#delete" do
|
248
|
+
it "makes authenticated DELETE request" do
|
249
|
+
expect(client).to receive(:delete).with("/test", auth_headers)
|
250
|
+
.and_return({ "data" => "result" })
|
251
|
+
|
252
|
+
result = session.delete("/test")
|
253
|
+
|
254
|
+
expect(result).to eq({ "data" => "result" })
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
context "when not authenticated" do
|
259
|
+
before do
|
260
|
+
session.instance_variable_set(:@session_token, nil)
|
261
|
+
end
|
262
|
+
|
263
|
+
it "raises error on GET" do
|
264
|
+
expect { session.get("/test") }.to raise_error(Tastytrade::Error, "Not authenticated")
|
265
|
+
end
|
266
|
+
|
267
|
+
it "raises error on POST" do
|
268
|
+
expect { session.post("/test") }.to raise_error(Tastytrade::Error, "Not authenticated")
|
269
|
+
end
|
270
|
+
|
271
|
+
it "raises error on PUT" do
|
272
|
+
expect { session.put("/test") }.to raise_error(Tastytrade::Error, "Not authenticated")
|
273
|
+
end
|
274
|
+
|
275
|
+
it "raises error on DELETE" do
|
276
|
+
expect { session.delete("/test") }.to raise_error(Tastytrade::Error, "Not authenticated")
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
describe "#authenticated?" do
|
282
|
+
let(:session) { described_class.new(username: username, password: password) }
|
283
|
+
|
284
|
+
it "returns false when no session token" do
|
285
|
+
expect(session.authenticated?).to be false
|
286
|
+
end
|
287
|
+
|
288
|
+
it "returns true when session token exists" do
|
289
|
+
session.instance_variable_set(:@session_token, "token")
|
290
|
+
expect(session.authenticated?).to be true
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
describe "#expired?" do
|
295
|
+
let(:session) { described_class.new(username: username, password: password) }
|
296
|
+
|
297
|
+
it "returns false when no expiration is set" do
|
298
|
+
expect(session.expired?).to be false
|
299
|
+
end
|
300
|
+
|
301
|
+
it "returns false when session is not expired" do
|
302
|
+
future_time = Time.now + 3600 # 1 hour from now
|
303
|
+
session.instance_variable_set(:@session_expiration, future_time)
|
304
|
+
|
305
|
+
expect(session.expired?).to be false
|
306
|
+
end
|
307
|
+
|
308
|
+
it "returns true when session is expired" do
|
309
|
+
past_time = Time.now - 3600 # 1 hour ago
|
310
|
+
session.instance_variable_set(:@session_expiration, past_time)
|
311
|
+
|
312
|
+
expect(session.expired?).to be true
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
describe "#time_until_expiry" do
|
317
|
+
let(:session) { described_class.new(username: username, password: password) }
|
318
|
+
|
319
|
+
it "returns nil when no expiration is set" do
|
320
|
+
expect(session.time_until_expiry).to be_nil
|
321
|
+
end
|
322
|
+
|
323
|
+
it "returns positive seconds when session is not expired" do
|
324
|
+
future_time = Time.now + 3600 # 1 hour from now
|
325
|
+
session.instance_variable_set(:@session_expiration, future_time)
|
326
|
+
|
327
|
+
time_left = session.time_until_expiry
|
328
|
+
expect(time_left).to be > 3590 # Allow for small time difference
|
329
|
+
expect(time_left).to be <= 3600
|
330
|
+
end
|
331
|
+
|
332
|
+
it "returns negative seconds when session is expired" do
|
333
|
+
past_time = Time.now - 3600 # 1 hour ago
|
334
|
+
session.instance_variable_set(:@session_expiration, past_time)
|
335
|
+
|
336
|
+
time_left = session.time_until_expiry
|
337
|
+
expect(time_left).to be < -3590
|
338
|
+
expect(time_left).to be >= -3610 # Allow for small timing differences
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
describe "#refresh_session" do
|
343
|
+
let(:session) { described_class.new(username: username, password: password, remember_me: true) }
|
344
|
+
let(:refresh_response) do
|
345
|
+
{
|
346
|
+
"data" => {
|
347
|
+
"user" => {
|
348
|
+
"email" => "test@example.com",
|
349
|
+
"username" => "testuser"
|
350
|
+
},
|
351
|
+
"session-token" => "new-session-token",
|
352
|
+
"session-expiration" => "2024-01-01T12:00:00Z"
|
353
|
+
}
|
354
|
+
}
|
355
|
+
end
|
356
|
+
|
357
|
+
context "with remember token" do
|
358
|
+
before do
|
359
|
+
session.instance_variable_set(:@remember_token, "valid-remember-token")
|
360
|
+
end
|
361
|
+
|
362
|
+
it "refreshes session using remember token" do
|
363
|
+
expect(client).to receive(:post).with("/sessions", {
|
364
|
+
"login" => username,
|
365
|
+
"remember-token" => "valid-remember-token",
|
366
|
+
"remember-me" => true
|
367
|
+
}).and_return(refresh_response)
|
368
|
+
|
369
|
+
result = session.refresh_session
|
370
|
+
|
371
|
+
expect(result).to eq(session)
|
372
|
+
expect(session.session_token).to eq("new-session-token")
|
373
|
+
expect(session.instance_variable_get(:@password)).to be_nil
|
374
|
+
end
|
375
|
+
|
376
|
+
it "updates session expiration on refresh" do
|
377
|
+
expect(client).to receive(:post).and_return(refresh_response)
|
378
|
+
|
379
|
+
session.refresh_session
|
380
|
+
|
381
|
+
expect(session.session_expiration).to be_a(Time)
|
382
|
+
expect(session.session_expiration.iso8601).to eq("2024-01-01T12:00:00Z")
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
context "without remember token" do
|
387
|
+
it "raises error when no remember token available" do
|
388
|
+
expect { session.refresh_session }.to raise_error(Tastytrade::Error, "No remember token available")
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|