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,444 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "bigdecimal"
|
5
|
+
|
6
|
+
RSpec.describe Tastytrade::Models::CurrentPosition do
|
7
|
+
let(:position_data) do
|
8
|
+
{
|
9
|
+
"account-number" => "5WX12345",
|
10
|
+
"symbol" => "AAPL",
|
11
|
+
"instrument-type" => "Equity",
|
12
|
+
"underlying-symbol" => "AAPL",
|
13
|
+
"quantity" => "100",
|
14
|
+
"quantity-direction" => "Long",
|
15
|
+
"close-price" => "150.00",
|
16
|
+
"average-open-price" => "145.00",
|
17
|
+
"average-yearly-market-close-price" => "140.00",
|
18
|
+
"average-daily-market-close-price" => "149.00",
|
19
|
+
"multiplier" => 1,
|
20
|
+
"cost-effect" => "Credit",
|
21
|
+
"is-suppressed" => false,
|
22
|
+
"is-frozen" => false,
|
23
|
+
"realized-day-gain" => "200.00",
|
24
|
+
"realized-today" => "100.00",
|
25
|
+
"created-at" => "2024-01-10T09:00:00Z",
|
26
|
+
"updated-at" => "2024-01-15T15:30:00Z",
|
27
|
+
"mark" => "152.00",
|
28
|
+
"mark-price" => "152.00",
|
29
|
+
"restricted-quantity" => "0"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
subject { described_class.new(position_data) }
|
34
|
+
|
35
|
+
describe "#initialize" do
|
36
|
+
it "parses basic position attributes" do
|
37
|
+
expect(subject.account_number).to eq("5WX12345")
|
38
|
+
expect(subject.symbol).to eq("AAPL")
|
39
|
+
expect(subject.instrument_type).to eq("Equity")
|
40
|
+
expect(subject.underlying_symbol).to eq("AAPL")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "converts quantity values to BigDecimal" do
|
44
|
+
expect(subject.quantity).to be_a(BigDecimal)
|
45
|
+
expect(subject.quantity).to eq(BigDecimal("100"))
|
46
|
+
expect(subject.restricted_quantity).to eq(BigDecimal("0"))
|
47
|
+
end
|
48
|
+
|
49
|
+
it "parses quantity direction" do
|
50
|
+
expect(subject.quantity_direction).to eq("Long")
|
51
|
+
end
|
52
|
+
|
53
|
+
it "converts all price values to BigDecimal" do
|
54
|
+
expect(subject.close_price).to eq(BigDecimal("150.00"))
|
55
|
+
expect(subject.average_open_price).to eq(BigDecimal("145.00"))
|
56
|
+
expect(subject.average_yearly_market_close_price).to eq(BigDecimal("140.00"))
|
57
|
+
expect(subject.average_daily_market_close_price).to eq(BigDecimal("149.00"))
|
58
|
+
expect(subject.mark).to eq(BigDecimal("152.00"))
|
59
|
+
expect(subject.mark_price).to eq(BigDecimal("152.00"))
|
60
|
+
end
|
61
|
+
|
62
|
+
it "parses multiplier as integer" do
|
63
|
+
expect(subject.multiplier).to eq(1)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "parses boolean fields" do
|
67
|
+
expect(subject.is_suppressed).to be false
|
68
|
+
expect(subject.is_frozen).to be false
|
69
|
+
end
|
70
|
+
|
71
|
+
it "converts realized gains to BigDecimal" do
|
72
|
+
expect(subject.realized_day_gain).to eq(BigDecimal("200.00"))
|
73
|
+
expect(subject.realized_today).to eq(BigDecimal("100.00"))
|
74
|
+
end
|
75
|
+
|
76
|
+
it "parses timestamps" do
|
77
|
+
expect(subject.created_at).to be_a(Time)
|
78
|
+
expect(subject.updated_at).to be_a(Time)
|
79
|
+
expect(subject.created_at.iso8601).to eq("2024-01-10T09:00:00Z")
|
80
|
+
expect(subject.updated_at.iso8601).to eq("2024-01-15T15:30:00Z")
|
81
|
+
end
|
82
|
+
|
83
|
+
context "with option position" do
|
84
|
+
let(:position_data) do
|
85
|
+
{
|
86
|
+
"account-number" => "5WX12345",
|
87
|
+
"symbol" => "AAPL 240119C150",
|
88
|
+
"instrument-type" => "Equity Option",
|
89
|
+
"underlying-symbol" => "AAPL",
|
90
|
+
"quantity" => "10",
|
91
|
+
"quantity-direction" => "Long",
|
92
|
+
"close-price" => "5.00",
|
93
|
+
"average-open-price" => "4.50",
|
94
|
+
"multiplier" => 100,
|
95
|
+
"mark-price" => "5.50",
|
96
|
+
"expires-at" => "2024-01-19T21:00:00Z",
|
97
|
+
"root-symbol" => "AAPL",
|
98
|
+
"option-expiration-type" => "Regular",
|
99
|
+
"strike-price" => "150.00",
|
100
|
+
"option-type" => "Call",
|
101
|
+
"contract-size" => 100,
|
102
|
+
"exercise-style" => "American"
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
it "parses option-specific fields" do
|
107
|
+
expect(subject.expires_at).to be_a(Time)
|
108
|
+
expect(subject.expires_at.iso8601).to eq("2024-01-19T21:00:00Z")
|
109
|
+
expect(subject.root_symbol).to eq("AAPL")
|
110
|
+
expect(subject.option_expiration_type).to eq("Regular")
|
111
|
+
expect(subject.strike_price).to eq(BigDecimal("150.00"))
|
112
|
+
expect(subject.option_type).to eq("Call")
|
113
|
+
expect(subject.contract_size).to eq(100)
|
114
|
+
expect(subject.exercise_style).to eq("American")
|
115
|
+
expect(subject.multiplier).to eq(100)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context "with nil values" do
|
120
|
+
let(:position_data) do
|
121
|
+
{
|
122
|
+
"account-number" => "5WX12345",
|
123
|
+
"symbol" => "AAPL",
|
124
|
+
"quantity" => nil,
|
125
|
+
"close-price" => "",
|
126
|
+
"multiplier" => nil
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
it "handles nil and empty values gracefully" do
|
131
|
+
expect(subject.quantity).to eq(BigDecimal("0"))
|
132
|
+
expect(subject.close_price).to eq(BigDecimal("0"))
|
133
|
+
expect(subject.multiplier).to eq(1)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#long?" do
|
139
|
+
it "returns true for long positions" do
|
140
|
+
expect(subject.long?).to be true
|
141
|
+
end
|
142
|
+
|
143
|
+
context "with short position" do
|
144
|
+
before { position_data["quantity-direction"] = "Short" }
|
145
|
+
it "returns false" do
|
146
|
+
expect(subject.long?).to be false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
describe "#short?" do
|
152
|
+
it "returns false for long positions" do
|
153
|
+
expect(subject.short?).to be false
|
154
|
+
end
|
155
|
+
|
156
|
+
context "with short position" do
|
157
|
+
before { position_data["quantity-direction"] = "Short" }
|
158
|
+
it "returns true" do
|
159
|
+
expect(subject.short?).to be true
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "#closed?" do
|
165
|
+
it "returns false for open positions" do
|
166
|
+
expect(subject.closed?).to be false
|
167
|
+
end
|
168
|
+
|
169
|
+
context "with zero quantity direction" do
|
170
|
+
before { position_data["quantity-direction"] = "Zero" }
|
171
|
+
it "returns true" do
|
172
|
+
expect(subject.closed?).to be true
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context "with zero quantity" do
|
177
|
+
before { position_data["quantity"] = "0" }
|
178
|
+
it "returns true" do
|
179
|
+
expect(subject.closed?).to be true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
describe "#equity?" do
|
185
|
+
it "returns true for equity positions" do
|
186
|
+
expect(subject.equity?).to be true
|
187
|
+
end
|
188
|
+
|
189
|
+
context "with option position" do
|
190
|
+
before { position_data["instrument-type"] = "Equity Option" }
|
191
|
+
it "returns false" do
|
192
|
+
expect(subject.equity?).to be false
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
describe "#option?" do
|
198
|
+
it "returns false for equity positions" do
|
199
|
+
expect(subject.option?).to be false
|
200
|
+
end
|
201
|
+
|
202
|
+
context "with option position" do
|
203
|
+
before { position_data["instrument-type"] = "Equity Option" }
|
204
|
+
it "returns true" do
|
205
|
+
expect(subject.option?).to be true
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe "#futures?" do
|
211
|
+
it "returns false for equity positions" do
|
212
|
+
expect(subject.futures?).to be false
|
213
|
+
end
|
214
|
+
|
215
|
+
context "with futures position" do
|
216
|
+
before { position_data["instrument-type"] = "Future" }
|
217
|
+
it "returns true" do
|
218
|
+
expect(subject.futures?).to be true
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe "#futures_option?" do
|
224
|
+
it "returns false for equity positions" do
|
225
|
+
expect(subject.futures_option?).to be false
|
226
|
+
end
|
227
|
+
|
228
|
+
context "with futures option position" do
|
229
|
+
before { position_data["instrument-type"] = "Future Option" }
|
230
|
+
it "returns true" do
|
231
|
+
expect(subject.futures_option?).to be true
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
describe "#position_value" do
|
237
|
+
it "calculates position value correctly for long positions" do
|
238
|
+
# 100 shares * $152 * 1 = $15,200
|
239
|
+
expect(subject.position_value).to eq(BigDecimal("15200"))
|
240
|
+
end
|
241
|
+
|
242
|
+
context "with short position" do
|
243
|
+
before do
|
244
|
+
position_data["quantity-direction"] = "Short"
|
245
|
+
position_data["quantity"] = "-100"
|
246
|
+
end
|
247
|
+
|
248
|
+
it "uses absolute quantity" do
|
249
|
+
expect(subject.position_value).to eq(BigDecimal("15200"))
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
context "with options" do
|
254
|
+
before do
|
255
|
+
position_data["quantity"] = "10"
|
256
|
+
position_data["mark-price"] = "5.00"
|
257
|
+
position_data["multiplier"] = 100
|
258
|
+
end
|
259
|
+
|
260
|
+
it "includes multiplier" do
|
261
|
+
# 10 contracts * $5 * 100 = $5,000
|
262
|
+
expect(subject.position_value).to eq(BigDecimal("5000"))
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
context "when position is closed" do
|
267
|
+
before { position_data["quantity-direction"] = "Zero" }
|
268
|
+
|
269
|
+
it "returns zero" do
|
270
|
+
expect(subject.position_value).to eq(BigDecimal("0"))
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
context "when mark price is nil" do
|
275
|
+
before { position_data["mark-price"] = nil }
|
276
|
+
|
277
|
+
it "falls back to close price" do
|
278
|
+
# 100 shares * $150 * 1 = $15,000
|
279
|
+
expect(subject.position_value).to eq(BigDecimal("15000"))
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
describe "#unrealized_pnl" do
|
285
|
+
it "calculates profit for long positions correctly" do
|
286
|
+
# (152 - 145) * 100 * 1 = $700
|
287
|
+
expect(subject.unrealized_pnl).to eq(BigDecimal("700"))
|
288
|
+
end
|
289
|
+
|
290
|
+
context "with short position" do
|
291
|
+
before do
|
292
|
+
position_data["quantity-direction"] = "Short"
|
293
|
+
position_data["quantity"] = "-100"
|
294
|
+
position_data["average-open-price"] = "155.00"
|
295
|
+
position_data["mark-price"] = "152.00"
|
296
|
+
end
|
297
|
+
|
298
|
+
it "calculates profit correctly" do
|
299
|
+
# (155 - 152) * 100 * 1 = $300
|
300
|
+
expect(subject.unrealized_pnl).to eq(BigDecimal("300"))
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
context "with losing long position" do
|
305
|
+
before do
|
306
|
+
position_data["average-open-price"] = "160.00"
|
307
|
+
position_data["mark-price"] = "152.00"
|
308
|
+
end
|
309
|
+
|
310
|
+
it "calculates loss correctly" do
|
311
|
+
# (152 - 160) * 100 * 1 = -$800
|
312
|
+
expect(subject.unrealized_pnl).to eq(BigDecimal("-800"))
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
context "when position is closed" do
|
317
|
+
before { position_data["quantity-direction"] = "Zero" }
|
318
|
+
|
319
|
+
it "returns zero" do
|
320
|
+
expect(subject.unrealized_pnl).to eq(BigDecimal("0"))
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
context "when average open price is zero" do
|
325
|
+
before { position_data["average-open-price"] = "0" }
|
326
|
+
|
327
|
+
it "returns zero" do
|
328
|
+
expect(subject.unrealized_pnl).to eq(BigDecimal("0"))
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
describe "#unrealized_pnl_percentage" do
|
334
|
+
it "calculates percentage correctly" do
|
335
|
+
# PnL = $700, Cost = 145 * 100 = $14,500
|
336
|
+
# 700 / 14500 * 100 = 4.83%
|
337
|
+
expect(subject.unrealized_pnl_percentage).to eq(BigDecimal("4.83"))
|
338
|
+
end
|
339
|
+
|
340
|
+
context "with losing position" do
|
341
|
+
before do
|
342
|
+
position_data["average-open-price"] = "160.00"
|
343
|
+
position_data["mark-price"] = "152.00"
|
344
|
+
end
|
345
|
+
|
346
|
+
it "calculates negative percentage" do
|
347
|
+
# PnL = -$800, Cost = 160 * 100 = $16,000
|
348
|
+
# -800 / 16000 * 100 = -5.00%
|
349
|
+
expect(subject.unrealized_pnl_percentage).to eq(BigDecimal("-5.00"))
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
context "when position is closed" do
|
354
|
+
before { position_data["quantity-direction"] = "Zero" }
|
355
|
+
|
356
|
+
it "returns zero" do
|
357
|
+
expect(subject.unrealized_pnl_percentage).to eq(BigDecimal("0"))
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
context "when cost basis is zero" do
|
362
|
+
before { position_data["average-open-price"] = "0" }
|
363
|
+
|
364
|
+
it "returns zero" do
|
365
|
+
expect(subject.unrealized_pnl_percentage).to eq(BigDecimal("0"))
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
describe "#total_pnl" do
|
371
|
+
it "sums realized and unrealized P&L" do
|
372
|
+
# Realized today: $100, Unrealized: $700
|
373
|
+
expect(subject.total_pnl).to eq(BigDecimal("800"))
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
describe "#display_symbol" do
|
378
|
+
context "with equity position" do
|
379
|
+
it "returns the symbol as-is" do
|
380
|
+
expect(subject.display_symbol).to eq("AAPL")
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
context "with option position" do
|
385
|
+
let(:position_data) do
|
386
|
+
{
|
387
|
+
"symbol" => "AAPL 240119C150",
|
388
|
+
"instrument-type" => "Equity Option",
|
389
|
+
"expires-at" => "2024-01-19T21:00:00Z",
|
390
|
+
"root-symbol" => "AAPL",
|
391
|
+
"strike-price" => "150.00",
|
392
|
+
"option-type" => "Call"
|
393
|
+
}
|
394
|
+
end
|
395
|
+
|
396
|
+
it "formats option symbol nicely" do
|
397
|
+
expect(subject.display_symbol).to eq("AAPL 01/19/24 C 150.0")
|
398
|
+
end
|
399
|
+
|
400
|
+
context "with put option" do
|
401
|
+
before { position_data["option-type"] = "Put" }
|
402
|
+
|
403
|
+
it "uses P for puts" do
|
404
|
+
expect(subject.display_symbol).to eq("AAPL 01/19/24 P 150.0")
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
context "with missing option data" do
|
409
|
+
before do
|
410
|
+
position_data["expires-at"] = nil
|
411
|
+
position_data["strike-price"] = nil
|
412
|
+
position_data["option-type"] = nil
|
413
|
+
end
|
414
|
+
|
415
|
+
it "returns original symbol" do
|
416
|
+
expect(subject.display_symbol).to eq("AAPL 240119C150")
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
describe "precision handling" do
|
423
|
+
context "with fractional shares" do
|
424
|
+
before { position_data["quantity"] = "0.5" }
|
425
|
+
|
426
|
+
it "handles fractional quantities" do
|
427
|
+
expect(subject.quantity).to eq(BigDecimal("0.5"))
|
428
|
+
expect(subject.position_value).to eq(BigDecimal("76")) # 0.5 * 152
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
context "with very small prices" do
|
433
|
+
before do
|
434
|
+
position_data["mark-price"] = "0.0001"
|
435
|
+
position_data["average-open-price"] = "0.0002"
|
436
|
+
end
|
437
|
+
|
438
|
+
it "maintains precision" do
|
439
|
+
expect(subject.mark_price).to eq(BigDecimal("0.0001"))
|
440
|
+
expect(subject.average_open_price).to eq(BigDecimal("0.0002"))
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Tastytrade::Models::User do
|
6
|
+
let(:user_data) do
|
7
|
+
{
|
8
|
+
"email" => "test@example.com",
|
9
|
+
"username" => "testuser",
|
10
|
+
"external-id" => "ext-123",
|
11
|
+
"is-professional" => false
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
subject(:user) { described_class.new(user_data) }
|
16
|
+
|
17
|
+
describe "#email" do
|
18
|
+
it "returns the email" do
|
19
|
+
expect(user.email).to eq("test@example.com")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#username" do
|
24
|
+
it "returns the username" do
|
25
|
+
expect(user.username).to eq("testuser")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#external_id" do
|
30
|
+
it "returns the external ID" do
|
31
|
+
expect(user.external_id).to eq("ext-123")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#professional?" do
|
36
|
+
context "when is-professional is true" do
|
37
|
+
let(:user_data) { super().merge("is-professional" => true) }
|
38
|
+
|
39
|
+
it "returns true" do
|
40
|
+
expect(user.professional?).to be true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "when is-professional is false" do
|
45
|
+
it "returns false" do
|
46
|
+
expect(user.professional?).to be false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when is-professional is nil" do
|
51
|
+
let(:user_data) { super().merge("is-professional" => nil) }
|
52
|
+
|
53
|
+
it "returns false" do
|
54
|
+
expect(user.professional?).to be false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|