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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa2b6fbe38925319fd0577393a7c12f9b852500c85185c51ad73da56b8724422
4
- data.tar.gz: 5c650bdbceeb19a32ddcc6e4c610f63d5a0cb8212df4c3875c1544ef91714204
3
+ metadata.gz: dc1c01296e2566f4fc37858644a653d88592959a377bf2170872e469a7364875
4
+ data.tar.gz: 1358a0e80d2cb413336edd44b0a38fdeecb21a57c6dafd23358b183f570a426f
5
5
  SHA512:
6
- metadata.gz: 9c3a31572d50be8e5bf74aed909e08c0befa1882de0d671c40ad8b9de956ad9c62c1fbededb5bbc5ea883a96aeaee9e1a27c63616c3ef21eaef7e9888a1ea746
7
- data.tar.gz: dcf15ae057baeb7aa067c21444efbe481ea6bdc23515ab9f286935d8a406b5fa5d91c0b2e439fa3103bba0c97b76996074d569253a5b7f6671172a0c8a9d4376
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tastytrade
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
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", :vcr do
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", :vcr do
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", :vcr do
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.0
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
@@ -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