Skip to content

Commit 123a639

Browse files
committed
test: add unit test for rearrange_consumeables
1 parent 06f5be7 commit 123a639

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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 TestRearrangeConsumeables:
12+
"""Tests for the rearrange_consumeables 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 shop phase, buy consumables, then enter selecting hand phase."""
19+
# Start a run with specific seed
20+
send_and_receive_api_message(
21+
tcp_client,
22+
"start_run",
23+
{"deck": "Red Deck", "seed": "OOOO155", "stake": 1},
24+
)
25+
26+
# Select blind to enter selecting hand state
27+
send_and_receive_api_message(
28+
tcp_client, "skip_or_select_blind", {"action": "select"}
29+
)
30+
31+
# Play hand to progress
32+
send_and_receive_api_message(
33+
tcp_client,
34+
"play_hand_or_discard",
35+
{"action": "play_hand", "cards": [0, 1, 2, 3]},
36+
)
37+
38+
# Cash out to enter shop
39+
send_and_receive_api_message(tcp_client, "cash_out", {})
40+
41+
# Buy first consumable card (usually index 1 is a planet/tarot card)
42+
game_state = send_and_receive_api_message(
43+
tcp_client, "shop", {"index": 1, "action": "buy_card"}
44+
)
45+
46+
# Go to next round to enter selecting hand state with consumables
47+
send_and_receive_api_message(tcp_client, "shop", {"action": "next_round"})
48+
49+
game_state = send_and_receive_api_message(
50+
tcp_client, "skip_or_select_blind", {"action": "select"}
51+
)
52+
53+
# add the other setup from the comment above
54+
55+
game_state = send_and_receive_api_message(
56+
tcp_client,
57+
"play_hand_or_discard",
58+
{"action": "discard", "cards": [0, 1, 2, 3, 4]},
59+
)
60+
game_state = send_and_receive_api_message(
61+
tcp_client,
62+
"play_hand_or_discard",
63+
{"action": "discard", "cards": [4, 5, 6, 7]},
64+
)
65+
game_state = send_and_receive_api_message(
66+
tcp_client,
67+
"play_hand_or_discard",
68+
{"action": "play_hand", "cards": [2, 3, 4, 5, 6]},
69+
)
70+
game_state = send_and_receive_api_message(
71+
tcp_client,
72+
"play_hand_or_discard",
73+
{"action": "discard", "cards": [1, 2, 3, 6]},
74+
)
75+
game_state = send_and_receive_api_message(
76+
tcp_client,
77+
"play_hand_or_discard",
78+
{"action": "play_hand", "cards": [0, 3, 4, 5, 6]},
79+
)
80+
81+
send_and_receive_api_message(tcp_client, "cash_out", {})
82+
83+
game_state = send_and_receive_api_message(
84+
tcp_client, "shop", {"action": "reroll"}
85+
)
86+
game_state = send_and_receive_api_message(
87+
tcp_client, "shop", {"action": "reroll"}
88+
)
89+
90+
game_state = send_and_receive_api_message(
91+
tcp_client, "shop", {"index": 1, "action": "buy_card"}
92+
)
93+
94+
assert game_state["state"] == State.SHOP.value
95+
assert len(game_state["consumeables"]["cards"]) == 2
96+
97+
yield game_state
98+
send_and_receive_api_message(tcp_client, "go_to_menu", {})
99+
100+
# ------------------------------------------------------------------
101+
# Success scenarios
102+
# ------------------------------------------------------------------
103+
104+
def test_rearrange_consumeables_success(
105+
self, tcp_client: socket.socket, setup_and_teardown: dict
106+
) -> None:
107+
"""Reverse the consumable order and verify the API response reflects it."""
108+
initial_state = setup_and_teardown
109+
initial_consumeables = initial_state["consumeables"]["cards"]
110+
consumeables_count: int = len(initial_consumeables)
111+
112+
# Reverse order indices (API expects zero-based indices)
113+
new_order = list(range(consumeables_count - 1, -1, -1))
114+
115+
final_state = send_and_receive_api_message(
116+
tcp_client,
117+
"rearrange_consumeables",
118+
{"consumeables": new_order},
119+
)
120+
121+
# Compare sort_id ordering to make sure it's reversed
122+
initial_sort_ids = [
123+
consumeable["sort_id"] for consumeable in initial_consumeables
124+
]
125+
final_sort_ids = [
126+
consumeable["sort_id"]
127+
for consumeable in final_state["consumeables"]["cards"]
128+
]
129+
assert final_sort_ids == list(reversed(initial_sort_ids))
130+
131+
def test_rearrange_consumeables_noop(
132+
self, tcp_client: socket.socket, setup_and_teardown: dict
133+
) -> None:
134+
"""Sending indices in current order should leave the consumables unchanged."""
135+
initial_state = setup_and_teardown
136+
initial_consumeables = initial_state["consumeables"]["cards"]
137+
consumeables_count: int = len(initial_consumeables)
138+
139+
# Existing order indices (0-based)
140+
current_order = list(range(consumeables_count))
141+
142+
final_state = send_and_receive_api_message(
143+
tcp_client,
144+
"rearrange_consumeables",
145+
{"consumeables": current_order},
146+
)
147+
148+
initial_sort_ids = [
149+
consumeable["sort_id"] for consumeable in initial_consumeables
150+
]
151+
final_sort_ids = [
152+
consumeable["sort_id"]
153+
for consumeable in final_state["consumeables"]["cards"]
154+
]
155+
assert final_sort_ids == initial_sort_ids
156+
157+
def test_rearrange_consumeables_single_consumable(
158+
self, tcp_client: socket.socket
159+
) -> None:
160+
"""Test rearranging when only one consumable is available."""
161+
# Start a simpler setup with just one consumable
162+
send_and_receive_api_message(
163+
tcp_client,
164+
"start_run",
165+
{"deck": "Red Deck", "seed": "OOOO155", "stake": 1},
166+
)
167+
168+
send_and_receive_api_message(
169+
tcp_client, "skip_or_select_blind", {"action": "select"}
170+
)
171+
172+
send_and_receive_api_message(
173+
tcp_client,
174+
"play_hand_or_discard",
175+
{"action": "play_hand", "cards": [0, 1, 2, 3]},
176+
)
177+
178+
send_and_receive_api_message(tcp_client, "cash_out", {})
179+
180+
# Buy only one consumable
181+
send_and_receive_api_message(
182+
tcp_client, "shop", {"index": 1, "action": "buy_card"}
183+
)
184+
185+
final_state = send_and_receive_api_message(
186+
tcp_client,
187+
"rearrange_consumeables",
188+
{"consumeables": [0]},
189+
)
190+
191+
assert len(final_state["consumeables"]["cards"]) == 1
192+
193+
# Clean up
194+
send_and_receive_api_message(tcp_client, "go_to_menu", {})
195+
196+
# ------------------------------------------------------------------
197+
# Validation / error scenarios
198+
# ------------------------------------------------------------------
199+
200+
def test_rearrange_consumeables_invalid_number_of_consumeables(
201+
self, tcp_client: socket.socket, setup_and_teardown: dict
202+
) -> None:
203+
"""Providing an index list with the wrong length should error."""
204+
consumeables_count = len(setup_and_teardown["consumeables"]["cards"])
205+
invalid_order = list(range(consumeables_count - 1)) # one short
206+
207+
response = send_and_receive_api_message(
208+
tcp_client,
209+
"rearrange_consumeables",
210+
{"consumeables": invalid_order},
211+
)
212+
213+
assert_error_response(
214+
response,
215+
"Invalid number of consumeables to rearrange",
216+
["consumeables_count", "valid_range"],
217+
ErrorCode.PARAMETER_OUT_OF_RANGE.value,
218+
)
219+
220+
def test_rearrange_consumeables_out_of_range_index(
221+
self, tcp_client: socket.socket, setup_and_teardown: dict
222+
) -> None:
223+
"""Including an index >= consumables count should error."""
224+
consumeables_count = len(setup_and_teardown["consumeables"]["cards"])
225+
order = list(range(consumeables_count))
226+
order[-1] = consumeables_count # out-of-range zero-based index
227+
228+
response = send_and_receive_api_message(
229+
tcp_client,
230+
"rearrange_consumeables",
231+
{"consumeables": order},
232+
)
233+
234+
assert_error_response(
235+
response,
236+
"Consumable index out of range",
237+
["index", "max_index"],
238+
ErrorCode.PARAMETER_OUT_OF_RANGE.value,
239+
)
240+
241+
def test_rearrange_consumeables_no_consumeables_available(
242+
self, tcp_client: socket.socket
243+
) -> None:
244+
"""Calling rearrange_consumeables when no consumables are available should error."""
245+
# Start a run without buying consumables
246+
send_and_receive_api_message(
247+
tcp_client,
248+
"start_run",
249+
{"deck": "Red Deck", "stake": 1, "seed": "OOOO155"},
250+
)
251+
send_and_receive_api_message(
252+
tcp_client, "skip_or_select_blind", {"action": "select"}
253+
)
254+
255+
response = send_and_receive_api_message(
256+
tcp_client,
257+
"rearrange_consumeables",
258+
{"consumeables": []},
259+
)
260+
261+
assert_error_response(
262+
response,
263+
"No consumeables available to rearrange",
264+
["consumeables_available"],
265+
ErrorCode.MISSING_GAME_OBJECT.value,
266+
)
267+
268+
# Clean up
269+
send_and_receive_api_message(tcp_client, "go_to_menu", {})
270+
271+
def test_rearrange_consumeables_missing_required_field(
272+
self, tcp_client: socket.socket, setup_and_teardown: dict
273+
) -> None:
274+
"""Calling rearrange_consumeables without the consumeables field should error."""
275+
response = send_and_receive_api_message(
276+
tcp_client,
277+
"rearrange_consumeables",
278+
{}, # Missing required 'consumeables' field
279+
)
280+
281+
assert_error_response(
282+
response,
283+
"Missing required field: consumeables",
284+
["field"],
285+
ErrorCode.INVALID_PARAMETER.value,
286+
)
287+
288+
def test_rearrange_consumeables_negative_index(
289+
self, tcp_client: socket.socket, setup_and_teardown: dict
290+
) -> None:
291+
"""Providing negative indices should error (after 0-to-1 based conversion)."""
292+
consumeables_count = len(setup_and_teardown["consumeables"]["cards"])
293+
order = list(range(consumeables_count))
294+
order[0] = -1 # negative index
295+
296+
response = send_and_receive_api_message(
297+
tcp_client,
298+
"rearrange_consumeables",
299+
{"consumeables": order},
300+
)
301+
302+
assert_error_response(
303+
response,
304+
"Consumable index out of range",
305+
["index", "max_index"],
306+
ErrorCode.PARAMETER_OUT_OF_RANGE.value,
307+
)
308+
309+
def test_rearrange_consumeables_duplicate_indices(
310+
self, tcp_client: socket.socket, setup_and_teardown: dict
311+
) -> None:
312+
"""Providing duplicate indices should work (last occurrence wins)."""
313+
consumeables_count = len(setup_and_teardown["consumeables"]["cards"])
314+
315+
if consumeables_count >= 2:
316+
# Use duplicate index (this should work in current implementation)
317+
order = [0, 0] # duplicate first index
318+
if consumeables_count > 2:
319+
order.extend(range(2, consumeables_count))
320+
321+
final_state = send_and_receive_api_message(
322+
tcp_client,
323+
"rearrange_consumeables",
324+
{"consumeables": order},
325+
)
326+
327+
assert len(final_state["consumeables"]["cards"]) == consumeables_count

0 commit comments

Comments
 (0)