Skip to content

Commit 1c28936

Browse files
committed
test: add unit test for sell_joker endpoint
1 parent e4b3b6e commit 1c28936

File tree

1 file changed

+277
-0
lines changed

1 file changed

+277
-0
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import socket
2+
from typing import Generator
3+
4+
import pytest
5+
6+
from balatrobot.enums import ErrorCode, State
7+
8+
from ..conftest import assert_error_response, send_and_receive_api_message
9+
10+
11+
class TestSellJoker:
12+
"""Tests for the sell_joker API endpoint."""
13+
14+
@pytest.fixture(autouse=True)
15+
def setup_and_teardown(
16+
self, tcp_client: socket.socket
17+
) -> Generator[dict, None, None]:
18+
"""Start a run, reach SELECTING_HAND phase with jokers, yield initial state, then clean up."""
19+
# Begin a run with The Omelette challenge which starts with jokers
20+
send_and_receive_api_message(
21+
tcp_client,
22+
"start_run",
23+
{
24+
"deck": "Red Deck",
25+
"stake": 1,
26+
"challenge": "The Omelette",
27+
"seed": "OOOO155",
28+
},
29+
)
30+
31+
# Select blind to enter SELECTING_HAND state with jokers already available
32+
game_state = send_and_receive_api_message(
33+
tcp_client, "skip_or_select_blind", {"action": "select"}
34+
)
35+
36+
assert game_state["state"] == State.SELECTING_HAND.value
37+
38+
# Skip if we don't have any jokers to test with
39+
if (
40+
not game_state.get("jokers")
41+
or not game_state["jokers"].get("cards")
42+
or len(game_state["jokers"]["cards"]) < 1
43+
):
44+
pytest.skip("No jokers available for testing sell_joker")
45+
46+
yield game_state
47+
send_and_receive_api_message(tcp_client, "go_to_menu", {})
48+
49+
# ------------------------------------------------------------------
50+
# Success scenario
51+
# ------------------------------------------------------------------
52+
53+
def test_sell_joker_success(
54+
self, tcp_client: socket.socket, setup_and_teardown: dict
55+
) -> None:
56+
"""Sell the first joker and verify it's removed from the collection."""
57+
initial_state = setup_and_teardown
58+
initial_jokers = initial_state["jokers"]["cards"]
59+
initial_count = len(initial_jokers)
60+
initial_money = initial_state.get("dollars", 0)
61+
62+
# Get the joker we're about to sell for reference
63+
joker_to_sell = initial_jokers[0]
64+
65+
# Sell the first joker (index 0)
66+
final_state = send_and_receive_api_message(
67+
tcp_client,
68+
"sell_joker",
69+
{"index": 0},
70+
)
71+
72+
# Ensure we remain in selecting hand state
73+
assert final_state["state"] == State.SELECTING_HAND.value
74+
75+
# Verify joker count decreased by 1
76+
final_jokers = final_state["jokers"]["cards"]
77+
assert len(final_jokers) == initial_count - 1
78+
79+
# Verify the sold joker is no longer in the collection
80+
final_sort_ids = [joker["sort_id"] for joker in final_jokers]
81+
assert joker_to_sell["sort_id"] not in final_sort_ids
82+
83+
# Verify money increased (jokers typically have sell value)
84+
final_money = final_state.get("dollars", 0)
85+
assert final_money >= initial_money
86+
87+
def test_sell_joker_last_joker(
88+
self, tcp_client: socket.socket, setup_and_teardown: dict
89+
) -> None:
90+
"""Sell the last joker by index and verify it's removed."""
91+
initial_state = setup_and_teardown
92+
initial_jokers = initial_state["jokers"]["cards"]
93+
initial_count = len(initial_jokers)
94+
last_index = initial_count - 1
95+
96+
# Get the last joker for reference
97+
joker_to_sell = initial_jokers[last_index]
98+
99+
# Sell the last joker
100+
final_state = send_and_receive_api_message(
101+
tcp_client,
102+
"sell_joker",
103+
{"index": last_index},
104+
)
105+
106+
# Verify joker count decreased by 1
107+
final_jokers = final_state["jokers"]["cards"]
108+
assert len(final_jokers) == initial_count - 1
109+
110+
# Verify the sold joker is no longer in the collection
111+
final_sort_ids = [joker["sort_id"] for joker in final_jokers]
112+
assert joker_to_sell["sort_id"] not in final_sort_ids
113+
114+
def test_sell_joker_multiple_sequential(
115+
self, tcp_client: socket.socket, setup_and_teardown: dict
116+
) -> None:
117+
"""Sell multiple jokers sequentially and verify each removal."""
118+
initial_state = setup_and_teardown
119+
initial_jokers = initial_state["jokers"]["cards"]
120+
initial_count = len(initial_jokers)
121+
122+
# Skip if we don't have enough jokers for this test
123+
if initial_count < 2:
124+
pytest.skip("Need at least 2 jokers for sequential selling test")
125+
126+
current_state = initial_state
127+
128+
# Sell jokers one by one, always selling index 0
129+
for i in range(2): # Sell 2 jokers
130+
current_jokers = current_state["jokers"]["cards"]
131+
joker_to_sell = current_jokers[0]
132+
133+
current_state = send_and_receive_api_message(
134+
tcp_client,
135+
"sell_joker",
136+
{"index": 0},
137+
)
138+
139+
# Verify the joker was removed
140+
remaining_jokers = current_state["jokers"]["cards"]
141+
remaining_sort_ids = [joker["sort_id"] for joker in remaining_jokers]
142+
assert joker_to_sell["sort_id"] not in remaining_sort_ids
143+
assert len(remaining_jokers) == len(current_jokers) - 1
144+
145+
# Verify final count
146+
assert len(current_state["jokers"]["cards"]) == initial_count - 2
147+
148+
# ------------------------------------------------------------------
149+
# Validation / error scenarios
150+
# ------------------------------------------------------------------
151+
152+
def test_sell_joker_index_out_of_range_high(
153+
self, tcp_client: socket.socket, setup_and_teardown: dict
154+
) -> None:
155+
"""Providing an index >= jokers count should error."""
156+
jokers_count = len(setup_and_teardown["jokers"]["cards"])
157+
invalid_index = jokers_count # out-of-range zero-based index
158+
159+
response = send_and_receive_api_message(
160+
tcp_client,
161+
"sell_joker",
162+
{"index": invalid_index},
163+
)
164+
165+
assert_error_response(
166+
response,
167+
"Joker index out of range",
168+
["index", "jokers_count"],
169+
ErrorCode.PARAMETER_OUT_OF_RANGE.value,
170+
)
171+
172+
def test_sell_joker_negative_index(
173+
self, tcp_client: socket.socket, setup_and_teardown: dict
174+
) -> None:
175+
"""Providing a negative index should error."""
176+
response = send_and_receive_api_message(
177+
tcp_client,
178+
"sell_joker",
179+
{"index": -1},
180+
)
181+
182+
assert_error_response(
183+
response,
184+
"Joker index out of range",
185+
["index", "jokers_count"],
186+
ErrorCode.PARAMETER_OUT_OF_RANGE.value,
187+
)
188+
189+
def test_sell_joker_no_jokers_available(self, tcp_client: socket.socket) -> None:
190+
"""Calling sell_joker when no jokers are available should error."""
191+
# Start a run without jokers (regular Red Deck without The Omelette challenge)
192+
send_and_receive_api_message(
193+
tcp_client,
194+
"start_run",
195+
{
196+
"deck": "Red Deck",
197+
"stake": 1,
198+
"seed": "OOOO155",
199+
},
200+
)
201+
send_and_receive_api_message(
202+
tcp_client, "skip_or_select_blind", {"action": "select"}
203+
)
204+
205+
response = send_and_receive_api_message(
206+
tcp_client,
207+
"sell_joker",
208+
{"index": 0},
209+
)
210+
211+
assert_error_response(
212+
response,
213+
"No jokers available to sell",
214+
["jokers_available"],
215+
ErrorCode.MISSING_GAME_OBJECT.value,
216+
)
217+
218+
# Clean up
219+
send_and_receive_api_message(tcp_client, "go_to_menu", {})
220+
221+
def test_sell_joker_missing_required_field(
222+
self, tcp_client: socket.socket, setup_and_teardown: dict
223+
) -> None:
224+
"""Calling sell_joker without the index field should error."""
225+
response = send_and_receive_api_message(
226+
tcp_client,
227+
"sell_joker",
228+
{}, # Missing required 'index' field
229+
)
230+
231+
assert_error_response(
232+
response,
233+
"Missing required field: index",
234+
["field"],
235+
ErrorCode.INVALID_PARAMETER.value,
236+
)
237+
238+
def test_sell_joker_non_numeric_index(
239+
self, tcp_client: socket.socket, setup_and_teardown: dict
240+
) -> None:
241+
"""Providing a non-numeric index should error."""
242+
response = send_and_receive_api_message(
243+
tcp_client,
244+
"sell_joker",
245+
{"index": "invalid"},
246+
)
247+
248+
assert_error_response(
249+
response,
250+
"Invalid parameter type",
251+
["parameter", "expected_type"],
252+
ErrorCode.INVALID_PARAMETER.value,
253+
)
254+
255+
def test_sell_joker_unsellable_joker(self, tcp_client: socket.socket) -> None:
256+
"""Attempting to sell an unsellable joker should error."""
257+
258+
initial_state = send_and_receive_api_message(
259+
tcp_client,
260+
"start_run",
261+
{
262+
"deck": "Red Deck",
263+
"stake": 1,
264+
"challenge": "Bram Poker", # contains an unsellable joker
265+
"seed": "OOOO155",
266+
},
267+
)
268+
269+
assert len(initial_state["jokers"]["cards"]) == 1
270+
271+
response = send_and_receive_api_message(
272+
tcp_client,
273+
"sell_joker",
274+
{"index": 0},
275+
)
276+
277+
assert "cannot be sold" in response.get("error", "").lower()

0 commit comments

Comments
 (0)