tastytrade 0.3.0 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +11 -1
- data/lib/tastytrade/version.rb +1 -1
- data/spec/spec_helper.rb +0 -72
- data/spec/tastytrade/models/account_order_management_spec.rb +3 -3
- metadata +1 -17
- data/vcr_implementation_plan.md +0 -403
- data/vcr_implementation_research.md +0 -330
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc1c01296e2566f4fc37858644a653d88592959a377bf2170872e469a7364875
|
4
|
+
data.tar.gz: 1358a0e80d2cb413336edd44b0a38fdeecb21a57c6dafd23358b183f570a426f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a2f378a230782d89ebaa843c155f6086c2d31bd1c21976179b8da38d7712533e3632a33d2ac5cdb41d88e5d24fa3fba1228e0c748f4efa2f6d41a9ce04af9854
|
7
|
+
data.tar.gz: ddbdcd65ef55e82501617202fdf9f3b6dbeb32f87b439fcbe04a357988c13097041662550450f57e31e608434e63ec4d9148ea09c50b09401298daa218ced886
|
data/CHANGELOG.md
CHANGED
@@ -25,6 +25,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
25
25
|
### Security
|
26
26
|
- Nothing yet
|
27
27
|
|
28
|
+
## [0.3.1] - 2025-08-08
|
29
|
+
|
30
|
+
### Removed
|
31
|
+
- VCR documentation files (vcr_implementation_plan.md, vcr_implementation_research.md) that were inadvertently included in v0.3.0
|
32
|
+
- VCR gem dependency from development dependencies
|
33
|
+
- VCR configuration and setup code from spec_helper.rb
|
34
|
+
- VCR-related test tags from spec files
|
35
|
+
|
36
|
+
### Fixed
|
37
|
+
- Cleaned up test suite to remove unused VCR references
|
38
|
+
|
28
39
|
## [0.3.0] - 2025-08-08
|
29
40
|
|
30
41
|
### Added
|
@@ -76,7 +87,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
76
87
|
- `order replace` command with interactive price/quantity modification
|
77
88
|
- Partial fill tracking with filled/remaining quantity calculations
|
78
89
|
- Order status color coding in CLI output
|
79
|
-
- VCR test configuration with sensitive data filtering
|
80
90
|
- Comprehensive test coverage for all order management features
|
81
91
|
- Integration tests for complete order lifecycle (place, list, modify, cancel)
|
82
92
|
- Renamed existing `order` command to `place` for clarity
|
data/lib/tastytrade/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -11,78 +11,6 @@ end
|
|
11
11
|
require "bundler/setup"
|
12
12
|
require "tastytrade"
|
13
13
|
require "webmock/rspec"
|
14
|
-
require "vcr"
|
15
|
-
|
16
|
-
VCR.configure do |config|
|
17
|
-
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
|
18
|
-
config.hook_into :webmock
|
19
|
-
config.configure_rspec_metadata!
|
20
|
-
|
21
|
-
# Filter out sensitive data
|
22
|
-
config.filter_sensitive_data("<AUTH_TOKEN>") do |interaction|
|
23
|
-
if interaction.request.headers["Authorization"]
|
24
|
-
interaction.request.headers["Authorization"].first
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
config.filter_sensitive_data("<SESSION_TOKEN>") do |interaction|
|
29
|
-
if interaction.response.headers["Content-Type"] &&
|
30
|
-
interaction.response.headers["Content-Type"].first.include?("application/json")
|
31
|
-
begin
|
32
|
-
body = JSON.parse(interaction.response.body)
|
33
|
-
body.dig("data", "session-token") || body.dig("data", "session_token")
|
34
|
-
rescue JSON::ParserError
|
35
|
-
nil
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
config.filter_sensitive_data("<ACCOUNT_NUMBER>") do |interaction|
|
41
|
-
# Filter account numbers from URLs
|
42
|
-
interaction.request.uri.match(%r{/accounts/([^/]+)})&.captures&.first
|
43
|
-
end
|
44
|
-
|
45
|
-
config.filter_sensitive_data("<USERNAME>") do |interaction|
|
46
|
-
if interaction.request.body && interaction.request.body.include?("username")
|
47
|
-
begin
|
48
|
-
body = JSON.parse(interaction.request.body)
|
49
|
-
body["username"]
|
50
|
-
rescue JSON::ParserError
|
51
|
-
nil
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
config.filter_sensitive_data("<PASSWORD>") do |interaction|
|
57
|
-
if interaction.request.body && interaction.request.body.include?("password")
|
58
|
-
begin
|
59
|
-
body = JSON.parse(interaction.request.body)
|
60
|
-
body["password"]
|
61
|
-
rescue JSON::ParserError
|
62
|
-
nil
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Filter personal information from response bodies
|
68
|
-
config.filter_sensitive_data("<EMAIL>") do |interaction|
|
69
|
-
if interaction.response.body
|
70
|
-
begin
|
71
|
-
body = JSON.parse(interaction.response.body)
|
72
|
-
body.dig("data", "email")
|
73
|
-
rescue JSON::ParserError
|
74
|
-
nil
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
# Default cassette options
|
80
|
-
config.default_cassette_options = {
|
81
|
-
record: :new_episodes,
|
82
|
-
match_requests_on: [:method, :uri, :body],
|
83
|
-
allow_playback_repeats: true
|
84
|
-
}
|
85
|
-
end
|
86
14
|
|
87
15
|
RSpec.configure do |config|
|
88
16
|
# Enable flags like --only-failures and --next-failure
|
@@ -54,7 +54,7 @@ RSpec.describe Tastytrade::Models::Account, "#get_live_orders" do
|
|
54
54
|
end
|
55
55
|
|
56
56
|
describe "without filters" do
|
57
|
-
it "retrieves all live orders"
|
57
|
+
it "retrieves all live orders" do
|
58
58
|
allow(session).to receive(:get)
|
59
59
|
.with("/accounts/5WV12345/orders/live/", {})
|
60
60
|
.and_return(live_orders_response)
|
@@ -122,7 +122,7 @@ RSpec.describe Tastytrade::Models::Account, "#cancel_order" do
|
|
122
122
|
let(:order_id) { "12345" }
|
123
123
|
|
124
124
|
describe "successful cancellation" do
|
125
|
-
it "sends DELETE request and returns nil"
|
125
|
+
it "sends DELETE request and returns nil" do
|
126
126
|
expect(session).to receive(:delete)
|
127
127
|
.with("/accounts/5WV12345/orders/12345/")
|
128
128
|
.and_return(nil)
|
@@ -210,7 +210,7 @@ RSpec.describe Tastytrade::Models::Account, "#replace_order" do
|
|
210
210
|
end
|
211
211
|
|
212
212
|
describe "successful replacement" do
|
213
|
-
it "sends PUT request and returns OrderResponse"
|
213
|
+
it "sends PUT request and returns OrderResponse" do
|
214
214
|
expect(new_order).to receive(:to_api_params).and_return(order_params)
|
215
215
|
expect(session).to receive(:put)
|
216
216
|
.with("/accounts/5WV12345/orders/12345/", order_params)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tastytrade
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Hamamura
|
@@ -163,20 +163,6 @@ dependencies:
|
|
163
163
|
- - "~>"
|
164
164
|
- !ruby/object:Gem::Version
|
165
165
|
version: '0.22'
|
166
|
-
- !ruby/object:Gem::Dependency
|
167
|
-
name: vcr
|
168
|
-
requirement: !ruby/object:Gem::Requirement
|
169
|
-
requirements:
|
170
|
-
- - "~>"
|
171
|
-
- !ruby/object:Gem::Version
|
172
|
-
version: '6.3'
|
173
|
-
type: :development
|
174
|
-
prerelease: false
|
175
|
-
version_requirements: !ruby/object:Gem::Requirement
|
176
|
-
requirements:
|
177
|
-
- - "~>"
|
178
|
-
- !ruby/object:Gem::Version
|
179
|
-
version: '6.3'
|
180
166
|
- !ruby/object:Gem::Dependency
|
181
167
|
name: webmock
|
182
168
|
requirement: !ruby/object:Gem::Requirement
|
@@ -302,8 +288,6 @@ files:
|
|
302
288
|
- spec/tastytrade/session_manager_spec.rb
|
303
289
|
- spec/tastytrade/session_spec.rb
|
304
290
|
- spec/tastytrade_spec.rb
|
305
|
-
- vcr_implementation_plan.md
|
306
|
-
- vcr_implementation_research.md
|
307
291
|
homepage: https://github.com/ryanhamamura/tastytrade
|
308
292
|
licenses:
|
309
293
|
- MIT
|
data/vcr_implementation_plan.md
DELETED
@@ -1,403 +0,0 @@
|
|
1
|
-
# VCR Implementation Plan for Tastytrade Gem
|
2
|
-
|
3
|
-
## Executive Summary
|
4
|
-
Implement VCR cassettes to replace existing mocks, accounting for Tastytrade sandbox's market-hours-only order routing behavior.
|
5
|
-
|
6
|
-
## Phase 1: Foundation Setup (Week 1)
|
7
|
-
|
8
|
-
### 1.1 Credential Management
|
9
|
-
```bash
|
10
|
-
# Create .env.test (add to .gitignore)
|
11
|
-
TASTYTRADE_SANDBOX_USERNAME=your_username
|
12
|
-
TASTYTRADE_SANDBOX_PASSWORD=your_password
|
13
|
-
TASTYTRADE_SANDBOX_ACCOUNT=your_account
|
14
|
-
TASTYTRADE_ENVIRONMENT=sandbox
|
15
|
-
```
|
16
|
-
|
17
|
-
### 1.2 Enhanced VCR Configuration
|
18
|
-
```ruby
|
19
|
-
# spec/spec_helper.rb
|
20
|
-
require 'dotenv'
|
21
|
-
Dotenv.load('.env.test') if File.exist?('.env.test')
|
22
|
-
|
23
|
-
VCR.configure do |config|
|
24
|
-
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
|
25
|
-
config.hook_into :webmock
|
26
|
-
config.configure_rspec_metadata!
|
27
|
-
|
28
|
-
# Recording mode management
|
29
|
-
vcr_mode = ENV['VCR_MODE'] =~ /rec/i ? :all : :once
|
30
|
-
vcr_mode = :none if ENV['CI'] # Prevent recording in CI
|
31
|
-
|
32
|
-
config.default_cassette_options = {
|
33
|
-
record: vcr_mode,
|
34
|
-
match_requests_on: [:method, :uri, :body],
|
35
|
-
allow_playback_repeats: true,
|
36
|
-
record_on_error: false
|
37
|
-
}
|
38
|
-
|
39
|
-
# Enhanced sensitive data filtering
|
40
|
-
config.filter_sensitive_data('<SANDBOX_USERNAME>') { ENV['TASTYTRADE_SANDBOX_USERNAME'] }
|
41
|
-
config.filter_sensitive_data('<SANDBOX_PASSWORD>') { ENV['TASTYTRADE_SANDBOX_PASSWORD'] }
|
42
|
-
config.filter_sensitive_data('<SANDBOX_ACCOUNT>') { ENV['TASTYTRADE_SANDBOX_ACCOUNT'] }
|
43
|
-
|
44
|
-
# Filter dynamic/sensitive data from responses
|
45
|
-
config.before_record do |interaction|
|
46
|
-
if interaction.response.body
|
47
|
-
body = interaction.response.body
|
48
|
-
|
49
|
-
# Filter account numbers (format: 5WX12345)
|
50
|
-
body.gsub!(/5W[A-Z0-9]{6,8}/, '<ACCOUNT_NUMBER>')
|
51
|
-
|
52
|
-
# Filter session tokens
|
53
|
-
body.gsub!(/"session-token":"[^"]+/, '"session-token":"<SESSION_TOKEN>')
|
54
|
-
body.gsub!(/"remember-token":"[^"]+/, '"remember-token":"<REMEMBER_TOKEN>')
|
55
|
-
|
56
|
-
# Filter personal information
|
57
|
-
body.gsub!(/"email":"[^"]+/, '"email":"<EMAIL>')
|
58
|
-
body.gsub!(/"external-id":"[^"]+/, '"external-id":"<EXTERNAL_ID>')
|
59
|
-
|
60
|
-
# Add recording metadata
|
61
|
-
interaction.response.headers['X-VCR-Recorded-At'] = Time.current.iso8601
|
62
|
-
interaction.response.headers['X-VCR-Market-Status'] = market_open? ? 'open' : 'closed'
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
# Ignore certain dynamic parameters
|
67
|
-
config.before_playback do |interaction|
|
68
|
-
# Normalize timestamps in URLs if needed
|
69
|
-
interaction.request.uri.gsub!(/timestamp=\d+/, 'timestamp=NORMALIZED')
|
70
|
-
end
|
71
|
-
end
|
72
|
-
```
|
73
|
-
|
74
|
-
### 1.3 Market Hours Helper
|
75
|
-
```ruby
|
76
|
-
# spec/support/market_hours_helper.rb
|
77
|
-
module MarketHoursHelper
|
78
|
-
def market_open?(time = Time.current)
|
79
|
-
# Convert to ET (Eastern Time)
|
80
|
-
et_time = time.in_time_zone('America/New_York')
|
81
|
-
|
82
|
-
# Check if weekend
|
83
|
-
return false if et_time.saturday? || et_time.sunday?
|
84
|
-
|
85
|
-
# Check if US market holiday (simplified - expand as needed)
|
86
|
-
holidays = [
|
87
|
-
Date.new(et_time.year, 1, 1), # New Year's Day
|
88
|
-
Date.new(et_time.year, 7, 4), # Independence Day
|
89
|
-
Date.new(et_time.year, 12, 25), # Christmas
|
90
|
-
# Add more holidays as needed
|
91
|
-
]
|
92
|
-
return false if holidays.include?(et_time.to_date)
|
93
|
-
|
94
|
-
# Check market hours (9:30 AM - 4:00 PM ET)
|
95
|
-
market_open = et_time.change(hour: 9, min: 30)
|
96
|
-
market_close = et_time.change(hour: 16, min: 0)
|
97
|
-
|
98
|
-
et_time >= market_open && et_time <= market_close
|
99
|
-
end
|
100
|
-
|
101
|
-
def skip_outside_market_hours
|
102
|
-
unless market_open? || VCR.current_cassette
|
103
|
-
skip "Test requires market hours or existing cassette"
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def next_market_open_time
|
108
|
-
time = Time.current.in_time_zone('America/New_York')
|
109
|
-
|
110
|
-
# If it's before 9:30 AM on a weekday, return today's open
|
111
|
-
if !time.saturday? && !time.sunday? && time.hour < 9 || (time.hour == 9 && time.min < 30)
|
112
|
-
return time.change(hour: 9, min: 30)
|
113
|
-
end
|
114
|
-
|
115
|
-
# Otherwise, find next weekday
|
116
|
-
loop do
|
117
|
-
time = time.tomorrow
|
118
|
-
break if !time.saturday? && !time.sunday?
|
119
|
-
end
|
120
|
-
|
121
|
-
time.change(hour: 9, min: 30)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
RSpec.configure do |config|
|
126
|
-
config.include MarketHoursHelper
|
127
|
-
end
|
128
|
-
```
|
129
|
-
|
130
|
-
## Phase 2: Proof of Concept - Session Class (Week 1)
|
131
|
-
|
132
|
-
### 2.1 Convert Session Tests
|
133
|
-
```ruby
|
134
|
-
# spec/tastytrade/session_vcr_spec.rb
|
135
|
-
require 'spec_helper'
|
136
|
-
|
137
|
-
RSpec.describe Tastytrade::Session, :vcr do
|
138
|
-
let(:sandbox_credentials) do
|
139
|
-
{
|
140
|
-
username: ENV['TASTYTRADE_SANDBOX_USERNAME'],
|
141
|
-
password: ENV['TASTYTRADE_SANDBOX_PASSWORD'],
|
142
|
-
is_test: true
|
143
|
-
}
|
144
|
-
end
|
145
|
-
|
146
|
-
describe "#login" do
|
147
|
-
context "with valid credentials" do
|
148
|
-
it "authenticates successfully" do
|
149
|
-
session = described_class.new(**sandbox_credentials).login
|
150
|
-
|
151
|
-
expect(session.session_token).not_to be_nil
|
152
|
-
expect(session.user).to be_a(Tastytrade::Models::User)
|
153
|
-
expect(session.user.email).not_to be_nil
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
context "with invalid credentials" do
|
158
|
-
it "raises InvalidCredentialsError" do
|
159
|
-
invalid_creds = sandbox_credentials.merge(password: 'wrong_password')
|
160
|
-
|
161
|
-
expect {
|
162
|
-
described_class.new(**invalid_creds).login
|
163
|
-
}.to raise_error(Tastytrade::InvalidCredentialsError)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
describe "#validate" do
|
169
|
-
let(:session) { described_class.new(**sandbox_credentials).login }
|
170
|
-
|
171
|
-
it "validates active session" do
|
172
|
-
expect(session.validate).to be true
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
describe "#destroy" do
|
177
|
-
let(:session) { described_class.new(**sandbox_credentials).login }
|
178
|
-
|
179
|
-
it "destroys the session" do
|
180
|
-
session.destroy
|
181
|
-
expect(session.session_token).to be_nil
|
182
|
-
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
```
|
186
|
-
|
187
|
-
### 2.2 Recording Schedule
|
188
|
-
```markdown
|
189
|
-
# Recording Schedule
|
190
|
-
|
191
|
-
## Initial Recording Session
|
192
|
-
- **Date**: [Schedule a weekday]
|
193
|
-
- **Time**: 10:00 AM - 3:00 PM ET (avoiding market open/close volatility)
|
194
|
-
- **Checklist**:
|
195
|
-
- [ ] Verify sandbox credentials work
|
196
|
-
- [ ] Market is open
|
197
|
-
- [ ] No US market holidays
|
198
|
-
- [ ] VCR_MODE=rec environment variable set
|
199
|
-
|
200
|
-
## Recording Commands
|
201
|
-
```bash
|
202
|
-
# Record all cassettes
|
203
|
-
VCR_MODE=rec bundle exec rspec spec/tastytrade/session_vcr_spec.rb
|
204
|
-
|
205
|
-
# Verify cassettes work
|
206
|
-
bundle exec rspec spec/tastytrade/session_vcr_spec.rb
|
207
|
-
```
|
208
|
-
|
209
|
-
## Phase 3: Order-Related Tests (Week 2)
|
210
|
-
|
211
|
-
### 3.1 Order Placement Tests with Market Hours
|
212
|
-
```ruby
|
213
|
-
# spec/tastytrade/models/account_place_order_vcr_spec.rb
|
214
|
-
RSpec.describe "Order Placement", :vcr do
|
215
|
-
include MarketHoursHelper
|
216
|
-
|
217
|
-
let(:session) { sandbox_session } # From helper
|
218
|
-
let(:account) { session.accounts.first }
|
219
|
-
|
220
|
-
describe "placing orders" do
|
221
|
-
context "during market hours" do
|
222
|
-
before { skip_outside_market_hours }
|
223
|
-
|
224
|
-
it "places a limit order" do
|
225
|
-
order = Tastytrade::Order.new(
|
226
|
-
type: Tastytrade::OrderType::LIMIT,
|
227
|
-
time_in_force: Tastytrade::OrderTimeInForce::DAY,
|
228
|
-
legs: build_spy_leg(100, "Buy to Open"),
|
229
|
-
price: 430.00
|
230
|
-
)
|
231
|
-
|
232
|
-
response = account.place_order(session, order)
|
233
|
-
|
234
|
-
expect(response).to be_a(Tastytrade::Models::OrderResponse)
|
235
|
-
expect(response.order_id).not_to be_nil
|
236
|
-
end
|
237
|
-
end
|
238
|
-
|
239
|
-
context "order validation (market independent)" do
|
240
|
-
it "validates order without placing" do
|
241
|
-
order = Tastytrade::Order.new(
|
242
|
-
type: Tastytrade::OrderType::LIMIT,
|
243
|
-
time_in_force: Tastytrade::OrderTimeInForce::DAY,
|
244
|
-
legs: build_spy_leg(100, "Buy to Open"),
|
245
|
-
price: 430.00
|
246
|
-
)
|
247
|
-
|
248
|
-
# Dry run doesn't require market hours
|
249
|
-
response = account.place_order(session, order, dry_run: true)
|
250
|
-
|
251
|
-
expect(response.buying_power_effect).not_to be_nil
|
252
|
-
end
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
private
|
257
|
-
|
258
|
-
def build_spy_leg(quantity, action)
|
259
|
-
Tastytrade::OrderLeg.new(
|
260
|
-
action: action,
|
261
|
-
symbol: "SPY",
|
262
|
-
quantity: quantity
|
263
|
-
)
|
264
|
-
end
|
265
|
-
end
|
266
|
-
```
|
267
|
-
|
268
|
-
## Phase 4: Gradual Migration (Weeks 3-4)
|
269
|
-
|
270
|
-
### 4.1 Parallel Testing Strategy
|
271
|
-
```ruby
|
272
|
-
# spec/spec_helper.rb
|
273
|
-
RSpec.configure do |config|
|
274
|
-
# Run VCR tests only when marked
|
275
|
-
config.around(:each) do |example|
|
276
|
-
if example.metadata[:vcr]
|
277
|
-
example.run
|
278
|
-
elsif example.metadata[:use_mocks]
|
279
|
-
# Keep existing mock behavior
|
280
|
-
VCR.turned_off { example.run }
|
281
|
-
else
|
282
|
-
# Default to mocks for now
|
283
|
-
VCR.turned_off { example.run }
|
284
|
-
end
|
285
|
-
end
|
286
|
-
end
|
287
|
-
```
|
288
|
-
|
289
|
-
### 4.2 Migration Checklist
|
290
|
-
- [ ] Session tests converted
|
291
|
-
- [ ] Client HTTP tests converted
|
292
|
-
- [ ] Account model tests converted
|
293
|
-
- [ ] Balance retrieval tests converted
|
294
|
-
- [ ] Order placement tests converted
|
295
|
-
- [ ] Order history tests converted
|
296
|
-
- [ ] Position tests converted
|
297
|
-
- [ ] CLI tests converted (mock the API client layer)
|
298
|
-
|
299
|
-
## Phase 5: CI/CD Integration (Week 4)
|
300
|
-
|
301
|
-
### 5.1 GitHub Actions Configuration
|
302
|
-
```yaml
|
303
|
-
# .github/workflows/test.yml
|
304
|
-
name: Tests
|
305
|
-
on: [push, pull_request]
|
306
|
-
|
307
|
-
jobs:
|
308
|
-
test:
|
309
|
-
runs-on: ubuntu-latest
|
310
|
-
env:
|
311
|
-
VCR_MODE: none # Never record in CI
|
312
|
-
TASTYTRADE_SANDBOX_USERNAME: ${{ secrets.TASTYTRADE_SANDBOX_USERNAME }}
|
313
|
-
TASTYTRADE_SANDBOX_PASSWORD: ${{ secrets.TASTYTRADE_SANDBOX_PASSWORD }}
|
314
|
-
TASTYTRADE_SANDBOX_ACCOUNT: ${{ secrets.TASTYTRADE_SANDBOX_ACCOUNT }}
|
315
|
-
|
316
|
-
steps:
|
317
|
-
- uses: actions/checkout@v4
|
318
|
-
with:
|
319
|
-
fetch-depth: 1 # Shallow clone for speed
|
320
|
-
|
321
|
-
- name: Set up Ruby
|
322
|
-
uses: ruby/setup-ruby@v1
|
323
|
-
with:
|
324
|
-
ruby-version: '3.2'
|
325
|
-
bundler-cache: true
|
326
|
-
|
327
|
-
- name: Run tests
|
328
|
-
run: bundle exec rspec
|
329
|
-
```
|
330
|
-
|
331
|
-
### 5.2 Cassette Maintenance
|
332
|
-
```ruby
|
333
|
-
# lib/tasks/vcr.rake
|
334
|
-
namespace :vcr do
|
335
|
-
desc "Delete cassettes older than 30 days"
|
336
|
-
task :clean_old do
|
337
|
-
Dir.glob("spec/fixtures/vcr_cassettes/**/*.yml").each do |cassette|
|
338
|
-
if File.mtime(cassette) < 30.days.ago
|
339
|
-
puts "Deleting old cassette: #{cassette}"
|
340
|
-
File.delete(cassette)
|
341
|
-
end
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
345
|
-
desc "Re-record all cassettes (run during market hours)"
|
346
|
-
task :refresh_all do
|
347
|
-
unless market_open?
|
348
|
-
puts "ERROR: Market is closed. Run during market hours (9:30 AM - 4:00 PM ET)"
|
349
|
-
puts "Next market open: #{next_market_open_time}"
|
350
|
-
exit 1
|
351
|
-
end
|
352
|
-
|
353
|
-
ENV['VCR_MODE'] = 'rec'
|
354
|
-
system('bundle exec rspec --tag vcr')
|
355
|
-
end
|
356
|
-
|
357
|
-
desc "Show cassette statistics"
|
358
|
-
task :stats do
|
359
|
-
cassettes = Dir.glob("spec/fixtures/vcr_cassettes/**/*.yml")
|
360
|
-
total_size = cassettes.sum { |f| File.size(f) }
|
361
|
-
|
362
|
-
puts "Total cassettes: #{cassettes.count}"
|
363
|
-
puts "Total size: #{(total_size / 1024.0 / 1024.0).round(2)} MB"
|
364
|
-
puts "Oldest cassette: #{cassettes.min_by { |f| File.mtime(f) }}"
|
365
|
-
puts "Newest cassette: #{cassettes.max_by { |f| File.mtime(f) }}"
|
366
|
-
end
|
367
|
-
end
|
368
|
-
```
|
369
|
-
|
370
|
-
## Phase 6: Documentation (Week 5)
|
371
|
-
|
372
|
-
### 6.1 Developer Guide
|
373
|
-
Create `docs/vcr_testing.md`:
|
374
|
-
- How to record new cassettes
|
375
|
-
- Market hours requirements
|
376
|
-
- Credential setup
|
377
|
-
- Troubleshooting guide
|
378
|
-
- Best practices
|
379
|
-
|
380
|
-
### 6.2 Git Configuration
|
381
|
-
```gitignore
|
382
|
-
# .gitignore
|
383
|
-
.env.test
|
384
|
-
.env.local
|
385
|
-
|
386
|
-
# .gitattributes
|
387
|
-
spec/fixtures/vcr_cassettes/**/*.yml -diff
|
388
|
-
```
|
389
|
-
|
390
|
-
## Success Metrics
|
391
|
-
- [ ] All tests pass with VCR cassettes
|
392
|
-
- [ ] CI build time reduced by >50%
|
393
|
-
- [ ] Tests work offline
|
394
|
-
- [ ] No sensitive data in cassettes
|
395
|
-
- [ ] Documentation complete
|
396
|
-
- [ ] Team trained on VCR workflow
|
397
|
-
|
398
|
-
## Risk Mitigation
|
399
|
-
1. **Market Hours Dependency**: Use strict `:once` recording mode
|
400
|
-
2. **Sensitive Data Leaks**: Multiple layers of filtering
|
401
|
-
3. **Cassette Bloat**: Regular cleanup tasks
|
402
|
-
4. **API Changes**: Monthly cassette refresh schedule
|
403
|
-
5. **Team Adoption**: Comprehensive documentation and helpers
|
@@ -1,330 +0,0 @@
|
|
1
|
-
# VCR Implementation Research - Best Practices
|
2
|
-
|
3
|
-
## Research Progress
|
4
|
-
|
5
|
-
### Question 1: Test Environment & Credentials
|
6
|
-
**Question**: Do you have access to a Tastytrade sandbox/test account?
|
7
|
-
**Status**: ✅ ANSWERED
|
8
|
-
**Best Practice**: Use sandbox/test accounts for recording. APIs should provide sandbox environments for testing that don't affect production data.
|
9
|
-
**Recommendation**: We should use Tastytrade's sandbox environment for all VCR recordings.
|
10
|
-
**Outstanding**: Need to confirm if you have sandbox credentials.
|
11
|
-
|
12
|
-
### Question 2: How should we handle test credentials?
|
13
|
-
**Question**: Store in .env.test file? GitHub secrets for CI? Other approach?
|
14
|
-
**Status**: ✅ ANSWERED
|
15
|
-
**Best Practice**:
|
16
|
-
- Use environment variables for credentials (12-Factor App practice)
|
17
|
-
- Local: Use .env.test file (gitignored) with dotenv gem
|
18
|
-
- CI: Use GitHub Actions secrets
|
19
|
-
- Filter sensitive data in VCR configuration
|
20
|
-
**Recommendation**:
|
21
|
-
1. Create `.env.test.example` with placeholders
|
22
|
-
2. Use `.env.test` locally (gitignored)
|
23
|
-
3. Store credentials in GitHub secrets for CI
|
24
|
-
4. Configure VCR to filter all sensitive data
|
25
|
-
|
26
|
-
### Question 3: What recording mode should we use?
|
27
|
-
**Question**: :once, :new_episodes, :none, or custom strategy?
|
28
|
-
**Status**: ✅ ANSWERED
|
29
|
-
**Best Practice**:
|
30
|
-
- Use `:once` as default (recommended by VCR documentation)
|
31
|
-
- Use environment variable for re-recording: `VCR_MODE=rec` → `:all`
|
32
|
-
- Use `:none` in CI for safety (prevents accidental API calls)
|
33
|
-
- Avoid `:new_episodes` unless specifically needed (can silently record unmatched requests)
|
34
|
-
**Recommendation**:
|
35
|
-
```ruby
|
36
|
-
vcr_mode = ENV['VCR_MODE'] =~ /rec/i ? :all : :once
|
37
|
-
vcr_mode = :none if ENV['CI']
|
38
|
-
```
|
39
|
-
|
40
|
-
### Question 4: Should we record against production or sandbox API?
|
41
|
-
**Question**: Sandbox vs production for recordings?
|
42
|
-
**Status**: ✅ ANSWERED
|
43
|
-
**Best Practice**:
|
44
|
-
- Always use sandbox/test environments for recording cassettes
|
45
|
-
- Maintain complete isolation from production
|
46
|
-
- Don't change API URLs in test environment - point to real service
|
47
|
-
- Ensure proper compliance and data segregation
|
48
|
-
**Recommendation**: Use Tastytrade sandbox API exclusively for all recordings
|
49
|
-
|
50
|
-
### Question 5: Which test areas are most critical to convert first?
|
51
|
-
**Question**: Authentication, Orders, Account/Balance, or CLI?
|
52
|
-
**Status**: ✅ ANSWERED
|
53
|
-
**Best Practice**:
|
54
|
-
- Start with Authentication/Session tests (foundation for all other tests)
|
55
|
-
- Then Session management (stateful interactions)
|
56
|
-
- Then critical business logic (orders, accounts)
|
57
|
-
- Finally secondary features (CLI, etc.)
|
58
|
-
**Recommendation**: Priority order:
|
59
|
-
1. Authentication/Session tests (fundamental)
|
60
|
-
2. Account/Balance retrieval (frequently used)
|
61
|
-
3. Order placement (high risk, needs accurate testing)
|
62
|
-
4. CLI commands (user-facing, depends on above)
|
63
|
-
|
64
|
-
### Question 6: Should we keep existing mock tests during transition?
|
65
|
-
**Question**: Run both in parallel or replace completely?
|
66
|
-
**Status**: ✅ ANSWERED
|
67
|
-
**Best Practice**:
|
68
|
-
- Can use both VCR and WebMock together
|
69
|
-
- Use `:use_vcr` metadata to enable VCR for specific tests
|
70
|
-
- Gradually transition from mocks to VCR
|
71
|
-
- Keep mocks for simple unit tests, use VCR for integration tests
|
72
|
-
**Recommendation**:
|
73
|
-
1. Keep existing mock tests initially
|
74
|
-
2. Add `:use_vcr` metadata to tests being converted
|
75
|
-
3. Run both in parallel during transition
|
76
|
-
4. Remove mocks after VCR tests are stable
|
77
|
-
|
78
|
-
### Question 7: How should we handle dynamic data in recordings?
|
79
|
-
**Question**: Timestamps, Order IDs, Market prices - custom matchers or freeze time?
|
80
|
-
**Status**: ✅ ANSWERED
|
81
|
-
**Best Practice**:
|
82
|
-
- Use `uri_without_params` for ignoring dynamic URL parameters
|
83
|
-
- Create custom matchers for complex scenarios
|
84
|
-
- Use ERB in cassettes for dynamic content
|
85
|
-
- Filter/replace dynamic data with placeholders
|
86
|
-
**Recommendation**:
|
87
|
-
1. Use `uri_without_params(:timestamp, :order_id)` for URLs
|
88
|
-
2. Filter sensitive/dynamic data in VCR config
|
89
|
-
3. Consider Timecop gem for freezing time in tests
|
90
|
-
4. Use custom matchers for market price matching
|
91
|
-
|
92
|
-
### Question 8: Cassette organization preference?
|
93
|
-
**Question**: One per test, one per file, or grouped by endpoint?
|
94
|
-
**Status**: ✅ ANSWERED
|
95
|
-
**Best Practice**:
|
96
|
-
- Use automatic naming with `configure_rspec_metadata!`
|
97
|
-
- Organize by API endpoint/functionality in subdirectories
|
98
|
-
- One cassette per test example (automatic with metadata)
|
99
|
-
- Group related cassettes in subdirectories
|
100
|
-
**Recommendation**:
|
101
|
-
```
|
102
|
-
spec/fixtures/vcr_cassettes/
|
103
|
-
authentication/
|
104
|
-
accounts/
|
105
|
-
orders/
|
106
|
-
positions/
|
107
|
-
```
|
108
|
-
With automatic naming: `ClassName/test_description.yml`
|
109
|
-
|
110
|
-
### Question 9: How should cassettes be managed in CI?
|
111
|
-
**Question**: Commit to repo, Git LFS, or generate fresh?
|
112
|
-
**Status**: ✅ ANSWERED
|
113
|
-
**Best Practice**:
|
114
|
-
- Commit cassettes to repo for CI reuse (faster builds)
|
115
|
-
- Use Git LFS only if cassettes are very large
|
116
|
-
- Use shallow clones in CI (`fetch-depth: 1`)
|
117
|
-
- Set recording mode to `:none` in CI
|
118
|
-
**Recommendation**:
|
119
|
-
1. Commit cassettes directly to repo (unless >100MB)
|
120
|
-
2. Use `.gitattributes` to reduce diff noise
|
121
|
-
3. Configure CI with `VCR_MODE=none`
|
122
|
-
4. Use `fetch-depth: 1` in GitHub Actions
|
123
|
-
|
124
|
-
### Question 10: Should we implement automatic cassette refresh?
|
125
|
-
**Question**: Monthly refresh, manual, or automated detection?
|
126
|
-
**Status**: ✅ ANSWERED
|
127
|
-
**Best Practice**:
|
128
|
-
- No built-in auto-expiration in VCR (feature request)
|
129
|
-
- Delete cassettes liberally when in doubt
|
130
|
-
- Use environment variable for re-recording
|
131
|
-
- Implement custom age-checking if needed
|
132
|
-
**Recommendation**:
|
133
|
-
1. Manual refresh with `VCR_MODE=rec bundle exec rspec`
|
134
|
-
2. Create rake task for bulk cassette deletion by age
|
135
|
-
3. Document refresh schedule (e.g., monthly)
|
136
|
-
4. Consider custom age-checker in VCR config
|
137
|
-
|
138
|
-
### Question 11: How to handle rate-limited endpoints?
|
139
|
-
**Question**: Add delays or use specific cassettes?
|
140
|
-
**Status**: ✅ ANSWERED
|
141
|
-
**Best Practice**:
|
142
|
-
- Build throttling into the API client itself
|
143
|
-
- VCR eliminates rate limit issues during playback
|
144
|
-
- Record cassettes respecting rate limits initially
|
145
|
-
- Use `:once` mode to avoid re-recording
|
146
|
-
**Recommendation**:
|
147
|
-
1. Add rate limiting to API client (not just tests)
|
148
|
-
2. Record cassettes once with proper delays
|
149
|
-
3. Playback doesn't need delays (cassettes replay instantly)
|
150
|
-
4. Consider separate cassettes for rate-limited endpoints
|
151
|
-
|
152
|
-
### Question 12: WebSocket/streaming endpoints?
|
153
|
-
**Question**: Does the API have real-time endpoints needing special handling?
|
154
|
-
**Status**: ✅ ANSWERED
|
155
|
-
**Best Practice**:
|
156
|
-
- Use specialized libraries like `simple-websocket-vcr` for WebSockets
|
157
|
-
- Standard VCR doesn't handle WebSocket protocol
|
158
|
-
- Record multiple messages/frames in WebSocket sessions
|
159
|
-
- Consider excluding real-time endpoints from VCR
|
160
|
-
**Recommendation**:
|
161
|
-
1. Check if Tastytrade API has WebSocket endpoints
|
162
|
-
2. If yes, use `simple-websocket-vcr` gem
|
163
|
-
3. If no, standard VCR is sufficient
|
164
|
-
4. Mock WebSocket connections for unit tests
|
165
|
-
|
166
|
-
### Question 13: Should we create a proof-of-concept first?
|
167
|
-
**Question**: Convert one simple test file first?
|
168
|
-
**Status**: ✅ ANSWERED
|
169
|
-
**Best Practice**:
|
170
|
-
- Start with single external service/API wrapper
|
171
|
-
- Create dedicated API wrapper classes
|
172
|
-
- Test wrapper with VCR, mock wrapper elsewhere
|
173
|
-
- Run tests twice (record then replay)
|
174
|
-
**Recommendation**:
|
175
|
-
1. YES - Start with proof-of-concept
|
176
|
-
2. Choose Session or Client class first
|
177
|
-
3. Establish patterns and conventions
|
178
|
-
4. Document learnings before full rollout
|
179
|
-
|
180
|
-
### Question 14: Compliance or security requirements?
|
181
|
-
**Question**: Extra sanitization for financial data? Regulatory requirements?
|
182
|
-
**Status**: ✅ ANSWERED
|
183
|
-
**Best Practice**:
|
184
|
-
- Filter all PII from cassettes (names, addresses, SSNs, account numbers)
|
185
|
-
- Use minimum necessary data for tests
|
186
|
-
- Implement access controls for test data
|
187
|
-
- Regular audits of cassette content
|
188
|
-
**Recommendation**:
|
189
|
-
1. Enhance VCR filter_sensitive_data for all PII
|
190
|
-
2. Use synthetic test data where possible
|
191
|
-
3. Document data handling procedures
|
192
|
-
4. Store cassettes securely (encrypted if needed)
|
193
|
-
5. Regular review of cassettes for leaked data
|
194
|
-
|
195
|
-
## Summary
|
196
|
-
|
197
|
-
### Questions Answered by Best Practices (10/14)
|
198
|
-
1. ✅ Test environment - Use sandbox
|
199
|
-
2. ✅ Credentials handling - .env.test + GitHub secrets
|
200
|
-
3. ✅ Recording mode - :once default, :none in CI
|
201
|
-
4. ✅ Production vs sandbox - Always use sandbox
|
202
|
-
5. ✅ Test priorities - Auth → Account → Orders → CLI
|
203
|
-
6. ✅ Migration strategy - Keep mocks initially, parallel transition
|
204
|
-
7. ✅ Dynamic data - Custom matchers and filters
|
205
|
-
8. ✅ Cassette organization - Auto-naming by endpoint
|
206
|
-
9. ✅ CI management - Commit cassettes, use :none mode
|
207
|
-
10. ✅ Refresh strategy - Manual with rake task
|
208
|
-
11. ✅ Rate limiting - Build into client, record once
|
209
|
-
12. ✅ WebSocket - Use specialized gem if needed
|
210
|
-
13. ✅ Proof-of-concept - Yes, start small
|
211
|
-
14. ✅ Compliance - Filter all PII, secure storage
|
212
|
-
|
213
|
-
### Outstanding Questions for User (1/14)
|
214
|
-
1. ~~**Do you have Tastytrade sandbox credentials?**~~ - ✅ CONFIRMED: User has sandbox credentials
|
215
|
-
|
216
|
-
### Critical Sandbox Behavior Note
|
217
|
-
**IMPORTANT**: Tastytrade sandbox endpoints behave like production - they only route orders during normal market hours. This impacts:
|
218
|
-
- Order placement tests (will fail outside market hours)
|
219
|
-
- Order validation tests (may behave differently based on market status)
|
220
|
-
- Any tests that depend on real-time market data
|
221
|
-
|
222
|
-
### Handling Market Hours Limitation
|
223
|
-
|
224
|
-
#### Strategy 1: Time-Independent Cassettes (RECOMMENDED)
|
225
|
-
1. **Record cassettes during market hours**
|
226
|
-
- Schedule recording sessions during market hours (9:30 AM - 4:00 PM ET)
|
227
|
-
- Use a specific day/time for consistency
|
228
|
-
- Document the recording time in cassette metadata
|
229
|
-
|
230
|
-
2. **Use `:once` mode strictly**
|
231
|
-
- Never re-record automatically
|
232
|
-
- Cassettes remain valid regardless of current time
|
233
|
-
- Tests pass 24/7 once recorded
|
234
|
-
|
235
|
-
3. **Separate market-dependent tests**
|
236
|
-
```ruby
|
237
|
-
context "market hour dependent", :market_hours_only do
|
238
|
-
# Tests that need live market
|
239
|
-
end
|
240
|
-
|
241
|
-
context "market hour independent", :vcr do
|
242
|
-
# Most tests with cassettes
|
243
|
-
end
|
244
|
-
```
|
245
|
-
|
246
|
-
#### Strategy 2: Mock Market Hours in Tests
|
247
|
-
1. **Use Timecop to freeze time during playback**
|
248
|
-
```ruby
|
249
|
-
around(:each, :vcr) do |example|
|
250
|
-
# Freeze to a known market hour when cassette was recorded
|
251
|
-
Timecop.freeze(cassette_recorded_at) do
|
252
|
-
example.run
|
253
|
-
end
|
254
|
-
end
|
255
|
-
```
|
256
|
-
|
257
|
-
2. **Add metadata to cassettes**
|
258
|
-
```ruby
|
259
|
-
VCR.configure do |c|
|
260
|
-
c.before_record do |interaction|
|
261
|
-
interaction.response.headers['X-Cassette-Recorded-At'] = Time.current.iso8601
|
262
|
-
interaction.response.headers['X-Market-Status'] = market_open? ? 'open' : 'closed'
|
263
|
-
end
|
264
|
-
end
|
265
|
-
```
|
266
|
-
|
267
|
-
### Handling Sandbox Credentials
|
268
|
-
|
269
|
-
#### Secure Credential Management
|
270
|
-
1. **Local Development (.env.test)**
|
271
|
-
```bash
|
272
|
-
# .env.test (gitignored)
|
273
|
-
TASTYTRADE_SANDBOX_USERNAME=your_sandbox_username
|
274
|
-
TASTYTRADE_SANDBOX_PASSWORD=your_sandbox_password
|
275
|
-
TASTYTRADE_SANDBOX_ACCOUNT=your_sandbox_account_number
|
276
|
-
```
|
277
|
-
|
278
|
-
2. **CI/CD (GitHub Secrets)**
|
279
|
-
- Store same credentials as GitHub secrets
|
280
|
-
- Reference in workflow: `${{ secrets.TASTYTRADE_SANDBOX_USERNAME }}`
|
281
|
-
|
282
|
-
3. **VCR Configuration Enhancement**
|
283
|
-
```ruby
|
284
|
-
VCR.configure do |config|
|
285
|
-
# Enhanced filtering for Tastytrade-specific data
|
286
|
-
config.filter_sensitive_data('<SANDBOX_USERNAME>') { ENV['TASTYTRADE_SANDBOX_USERNAME'] }
|
287
|
-
config.filter_sensitive_data('<SANDBOX_ACCOUNT>') { ENV['TASTYTRADE_SANDBOX_ACCOUNT'] }
|
288
|
-
|
289
|
-
# Filter account numbers from responses
|
290
|
-
config.before_record do |interaction|
|
291
|
-
if interaction.response.body
|
292
|
-
body = interaction.response.body
|
293
|
-
# Replace any account numbers in response
|
294
|
-
body.gsub!(/5W[A-Z0-9]{6}/, '<ACCOUNT_NUMBER>')
|
295
|
-
end
|
296
|
-
end
|
297
|
-
end
|
298
|
-
```
|
299
|
-
|
300
|
-
4. **Test Helper for Sandbox Sessions**
|
301
|
-
```ruby
|
302
|
-
# spec/support/sandbox_helpers.rb
|
303
|
-
module SandboxHelpers
|
304
|
-
def sandbox_session
|
305
|
-
@sandbox_session ||= VCR.use_cassette('authentication/sandbox_login') do
|
306
|
-
Tastytrade::Session.new(
|
307
|
-
username: ENV['TASTYTRADE_SANDBOX_USERNAME'],
|
308
|
-
password: ENV['TASTYTRADE_SANDBOX_PASSWORD'],
|
309
|
-
is_test: true
|
310
|
-
).login
|
311
|
-
end
|
312
|
-
end
|
313
|
-
|
314
|
-
def with_market_hours_check
|
315
|
-
if !VCR.current_cassette && !market_open?
|
316
|
-
skip "Skipping test - market closed and no cassette available"
|
317
|
-
end
|
318
|
-
yield
|
319
|
-
end
|
320
|
-
end
|
321
|
-
```
|
322
|
-
|
323
|
-
### Recommended Implementation Order (UPDATED)
|
324
|
-
1. ~~Obtain/confirm sandbox credentials~~ ✅ Complete
|
325
|
-
2. **Set up credential management** (.env.test + GitHub secrets)
|
326
|
-
3. **Record initial cassettes during market hours**
|
327
|
-
4. Create proof-of-concept with Session class
|
328
|
-
5. Establish VCR helper patterns with market hour handling
|
329
|
-
6. Convert tests incrementally following priority order
|
330
|
-
7. Document recording schedule and market hour dependencies
|