from django.test import TestCase, Client from django.urls import reverse from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.http import QueryDict from accounts.models import FriendCode from cards.models import Card from trades.models import ( TradeOffer, TradeOfferHaveCard, TradeOfferWantCard, TradeAcceptance, ) from trades.forms import ( TradeOfferCreateForm, TradeAcceptanceCreateForm, TradeOfferAcceptForm, TradeAcceptanceTransitionForm, ) from tests.utils.rarity import RARITY_MAPPING # ------------------------------------------------------------------------ # Model Tests # ------------------------------------------------------------------------ class TradeOfferModelTest(TestCase): def setUp(self): User = get_user_model() self.user = User.objects.create_user( username="testuser", email="test@example.com", password="password" ) self.friend_code = FriendCode.objects.create( friend_code="1234-5678-9012-3456", in_game_name="TestInGame", user=self.user ) # Create cards with the same rarity (valid scenario) self.card1 = Card.objects.create( name="Card1", cardset="set1", cardnum=1, style="default", rarity_icon=RARITY_MAPPING[1], rarity_level=1 ) self.card2 = Card.objects.create( name="Card2", cardset="set1", cardnum=2, style="default", rarity_icon=RARITY_MAPPING[1], rarity_level=1 ) # Create a card with a different rarity (to test invalid trade offers) self.card3 = Card.objects.create( name="Card3", cardset="set1", cardnum=3, style="default", rarity_icon=RARITY_MAPPING[8], rarity_level=8 ) # Create a valid trade offer with consistent rarity details self.trade_offer = TradeOffer.objects.create(initiated_by=self.friend_code) TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer, card=self.card1, quantity=2 ) TradeOfferWantCard.objects.create( trade_offer=self.trade_offer, card=self.card2, quantity=3 ) def test_update_rarity_fields_valid(self): """Test update_rarity_fields succeeds with cards sharing the same rarity.""" self.trade_offer.update_rarity_fields() self.assertEqual(self.trade_offer.rarity_level, 1) self.assertEqual(self.trade_offer.rarity_icon, "🔷") def test_update_rarity_fields_invalid(self): """If a card with a different rarity is added, update_rarity_fields should raise an error.""" with self.assertRaisesMessage( ValidationError, "All cards in a trade offer must have the same rarity." ): TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer, card=self.card3, quantity=1 ) def test_hash_generation(self): """Verify that TradeOffer.hash is generated and 9 characters long ending with 'z'.""" self.assertTrue(self.trade_offer.hash.endswith("z")) self.assertEqual(len(self.trade_offer.hash), 9) class TradeAcceptanceModelTest(TestCase): def setUp(self): User = get_user_model() # Create two users for testing state transitions self.user = User.objects.create_user( username="acceptuser", email="acc@example.com", password="password" ) self.friend_code = FriendCode.objects.create( friend_code="1111-2222-3333-4444", in_game_name="AccInGame", user=self.user ) self.other_user = User.objects.create_user( username="initiator", email="init@example.com", password="password" ) self.initiator_friend_code = FriendCode.objects.create( friend_code="5555-6666-7777-8888", in_game_name="InitInGame", user=self.other_user ) # Create two cards (with the same rarity) self.card1 = Card.objects.create( name="CardA", cardset="setA", cardnum=1, style="default", rarity_icon=RARITY_MAPPING[2], rarity_level=2 ) self.card2 = Card.objects.create( name="CardB", cardset="setA", cardnum=2, style="default", rarity_icon=RARITY_MAPPING[2], rarity_level=2 ) # Create a trade offer by the initiator. self.trade_offer = TradeOffer.objects.create( initiated_by=self.initiator_friend_code ) TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer, card=self.card1, quantity=1 ) TradeOfferWantCard.objects.create( trade_offer=self.trade_offer, card=self.card2, quantity=1 ) # Create an initial acceptance in state 'ACCEPTED' self.acceptance = TradeAcceptance.objects.create( trade_offer=self.trade_offer, accepted_by=self.friend_code, requested_card=self.card1, offered_card=self.card2, state=TradeAcceptance.AcceptanceState.ACCEPTED, ) def test_invalid_state_transition(self): """ Test that an invalid state transition (not allowed by the current state) raises a ValueError. """ # Attempt to transition to a state that is not allowed. with self.assertRaises(ValueError): self.acceptance.update_state( TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR, user=self.user ) def test_valid_state_transition_initiator(self): """ For a user that owns the trade offer (initiator), check that a valid transition (e.g. from ACCEPTED to SENT) succeeds. """ allowed_transitions = dict( self.acceptance.get_allowed_state_transitions(user=self.other_user) ) # 'SENT' should be among allowed states for the initiator. self.assertIn(TradeAcceptance.AcceptanceState.SENT, allowed_transitions) self.acceptance.update_state( TradeAcceptance.AcceptanceState.SENT, user=self.other_user ) self.assertEqual( self.acceptance.state, TradeAcceptance.AcceptanceState.SENT ) def test_signal_adjusts_qty_accepted(self): """ Test that creation, state change, and deletion of a TradeAcceptance update qty_accepted counters in through models correctly. """ have_through = TradeOfferHaveCard.objects.get( trade_offer=self.trade_offer, card=self.card1 ) want_through = TradeOfferWantCard.objects.get( trade_offer=self.trade_offer, card=self.card2 ) # Initially, one active acceptance should set qty_accepted to 1. self.assertEqual(have_through.qty_accepted, 1) self.assertEqual(want_through.qty_accepted, 1) # Change state to a terminal state so that signals decrement the counter. self.acceptance.state = TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR self.acceptance.save() have_through.refresh_from_db() want_through.refresh_from_db() self.assertEqual(have_through.qty_accepted, 0) self.assertEqual(want_through.qty_accepted, 0) # Create a new acceptance and then delete it. new_acceptance = TradeAcceptance.objects.create( trade_offer=self.trade_offer, accepted_by=self.friend_code, requested_card=self.card1, offered_card=self.card2, state=TradeAcceptance.AcceptanceState.ACCEPTED, ) have_through.refresh_from_db() self.assertEqual(have_through.qty_accepted, 1) new_acceptance.delete() have_through.refresh_from_db() self.assertEqual(have_through.qty_accepted, 0) # ------------------------------------------------------------------------ # Form Tests # ------------------------------------------------------------------------ class TradeOfferFormTest(TestCase): def setUp(self): User = get_user_model() self.user = User.objects.create_user( username="formuser", email="form@example.com", password="password" ) self.friend_code = FriendCode.objects.create( friend_code="9999-8888-7777-6666", in_game_name="FormUser", user=self.user ) # Create two cards with the same rarity details. self.card1 = Card.objects.create( name="FormCard1", cardset="formset", cardnum=1, style="default", rarity_icon=RARITY_MAPPING[3], rarity_level=3 ) self.card2 = Card.objects.create( name="FormCard2", cardset="formset", cardnum=2, style="default", rarity_icon=RARITY_MAPPING[3], rarity_level=3 ) def test_trade_offer_create_form_valid(self): """ A valid POST using colon-separated quantity strings should succeed. """ # Build a QueryDict with multiple values for each list field. qd = QueryDict('', mutable=True) qd.setlist("have_cards", [f"{self.card1.pk}:2"]) qd.setlist("want_cards", [f"{self.card2.pk}:3"]) # 'initiated_by' is a normal field so we can update it directly. qd.update({"initiated_by": self.friend_code.pk}) form = TradeOfferCreateForm(data=qd) self.assertTrue(form.is_valid()) def test_trade_offer_create_form_invalid_quantity(self): """ If quantity cannot be parsed as an integer a ValidationError should be raised. """ qd = QueryDict('', mutable=True) # Provide an invalid quantity ("two" instead of an integer). qd.setlist("have_cards", [f"{self.card1.pk}:two"]) qd.setlist("want_cards", [f"{self.card2.pk}:3"]) qd.update({"initiated_by": self.friend_code.pk}) form = TradeOfferCreateForm(data=qd) self.assertFalse(form.is_valid()) self.assertIn("Invalid quantity provided", str(form.errors)) def test_trade_offer_create_form_missing_colon(self): """ An entry missing a colon should be ignored. """ qd = QueryDict('', mutable=True) # No colon present in the selections. qd.setlist("have_cards", [f"{self.card1.pk}"]) qd.setlist("want_cards", [f"{self.card2.pk}"]) qd.update({"initiated_by": self.friend_code.pk}) form = TradeOfferCreateForm(data=qd) self.assertTrue(form.is_valid()) # Since the entries are ignored, cleaned_data should have empty dictionaries. self.assertEqual(form.cleaned_data["have_cards"], {}) self.assertEqual(form.cleaned_data["want_cards"], {}) def test_trade_acceptance_create_form(self): """Test that the TradeAcceptanceCreateForm filters available cards based on trade offer availability.""" # Create a trade offer with available quantities. trade_offer = TradeOffer.objects.create(initiated_by=self.friend_code) TradeOfferHaveCard.objects.create( trade_offer=trade_offer, card=self.card1, quantity=2 ) TradeOfferWantCard.objects.create( trade_offer=trade_offer, card=self.card2, quantity=2 ) friend_codes = FriendCode.objects.filter(pk=self.friend_code.pk) form_data = { "accepted_by": self.friend_code.pk, "requested_card": self.card1.pk, "offered_card": self.card2.pk, } form = TradeAcceptanceCreateForm( data=form_data, trade_offer=trade_offer, friend_codes=friend_codes ) self.assertTrue(form.is_valid()) instance = form.save() self.assertEqual(instance.trade_offer, trade_offer) self.assertEqual(instance.accepted_by, self.friend_code) def test_trade_offer_accept_form(self): """Test that TradeOfferAcceptForm correctly sets the friend_code queryset.""" friend_codes = FriendCode.objects.filter(pk=self.friend_code.pk) form = TradeOfferAcceptForm(friend_codes=friend_codes) self.assertEqual( list(form.fields["friend_code"].queryset), list(friend_codes) ) def test_trade_acceptance_transition_form(self): """Test that the transition form provides only allowed transitions.""" other_user = get_user_model().objects.create_user( username="transuser", email="trans@example.com", password="password" ) other_friend_code = FriendCode.objects.create( friend_code="FC-TRANS", in_game_name="TransUser", user=other_user ) # Create a trade offer with initiator being our test friend code. trade_offer = TradeOffer.objects.create(initiated_by=self.friend_code) TradeOfferHaveCard.objects.create( trade_offer=trade_offer, card=self.card1, quantity=1 ) TradeOfferWantCard.objects.create( trade_offer=trade_offer, card=self.card2, quantity=1 ) acceptance = TradeAcceptance.objects.create( trade_offer=trade_offer, accepted_by=other_friend_code, requested_card=self.card1, offered_card=self.card2, state=TradeAcceptance.AcceptanceState.ACCEPTED, ) form = TradeAcceptanceTransitionForm(instance=acceptance, user=other_user) # Compare the form's state choices with the allowed transitions. allowed = [choice[0] for choice in acceptance.get_allowed_state_transitions(user=other_user)] form_choices = [choice[0] for choice in form.fields["state"].choices] for choice in allowed: self.assertIn(choice, form_choices) # ------------------------------------------------------------------------ # View Tests # ------------------------------------------------------------------------ class TradeViewsTest(TestCase): def setUp(self): User = get_user_model() self.client = Client() self.user = User.objects.create_user( username="viewuser", email="view@example.com", password="password" ) self.friend_code = FriendCode.objects.create( friend_code="4444-3333-2222-1111", in_game_name="ViewUser", user=self.user ) self.user.default_friend_code = self.friend_code self.user.save(update_fields=["default_friend_code"]) self.client.login(username="viewuser", password="password") # Create sample cards. self.card1 = Card.objects.create( name="ViewCard1", cardset="setV", cardnum=1, style="default", rarity_icon=RARITY_MAPPING[7], rarity_level=7 ) self.card2 = Card.objects.create( name="ViewCard2", cardset="setV", cardnum=2, style="default", rarity_icon=RARITY_MAPPING[7], rarity_level=7 ) # Create a trade offer initiated by the logged-in user's friend code. self.trade_offer = TradeOffer.objects.create(initiated_by=self.friend_code) TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer, card=self.card1, quantity=1 ) TradeOfferWantCard.objects.create( trade_offer=self.trade_offer, card=self.card2, quantity=1 ) def test_trade_offer_create_view_get(self): """GET request to TradeOfferCreateView should include friend_codes and cards in context.""" response = self.client.get(reverse("trade_offer_create")) self.assertEqual(response.status_code, 200) self.assertIn("friend_codes", response.context) self.assertIn("cards", response.context) # When there is only one friend code, the initial value should be preset. self.assertEqual( response.context["form"].initial.get("initiated_by"), self.friend_code.pk, ) def test_trade_offer_delete_view_permission(self): """ The delete view should enforce that only trade offers initiated by one of the user's friend codes can be deleted. """ other_user = get_user_model().objects.create_user( username="otheruser", email="other@example.com", password="password" ) other_friend_code = FriendCode.objects.create( friend_code="FC-OTHER", in_game_name="OtherUser", user=other_user ) trade_offer_other = TradeOffer.objects.create(initiated_by=other_friend_code) url = reverse("trade_offer_delete", kwargs={"pk": trade_offer_other.pk}) response = self.client.get(url) self.assertEqual(response.status_code, 403) def test_trade_offer_delete_view_close(self): """ If a trade offer has active acceptances, the delete view should not delete it. Instead, if no active acceptances remain it should mark the offer as closed. """ # Create a trade offer with an active acceptance. trade_offer_with_acceptance = TradeOffer.objects.create(initiated_by=self.friend_code) # Use quantity=2 so the trade offer isn't automatically closed when one acceptance is created TradeOfferHaveCard.objects.create( trade_offer=trade_offer_with_acceptance, card=self.card1, quantity=2 ) TradeOfferWantCard.objects.create( trade_offer=trade_offer_with_acceptance, card=self.card2, quantity=2 ) # Create an acceptance that takes one card from each side TradeAcceptance.objects.create( trade_offer=trade_offer_with_acceptance, accepted_by=self.friend_code, requested_card=self.card1, offered_card=self.card2, state=TradeAcceptance.AcceptanceState.ACCEPTED, ) delete_url = reverse("trade_offer_delete", kwargs={"pk": trade_offer_with_acceptance.pk}) # --- Patch the view's get_object() method to return our trade offer --- from trades.views import TradeOfferDeleteView orig_get_object = TradeOfferDeleteView.get_object TradeOfferDeleteView.get_object = lambda self: trade_offer_with_acceptance try: # First POST: with active acceptance, deletion should not close the offer. response = self.client.post(delete_url) trade_offer_with_acceptance.refresh_from_db() self.assertFalse(trade_offer_with_acceptance.is_closed) # Now simulate no active acceptances by updating the acceptance state. acceptance = trade_offer_with_acceptance.acceptances.first() acceptance.state = TradeAcceptance.AcceptanceState.REJECTED_BY_ACCEPTOR acceptance.save(update_fields=["state"]) response = self.client.post(delete_url) trade_offer_with_acceptance.refresh_from_db() self.assertTrue(trade_offer_with_acceptance.is_closed) finally: # Always restore the original method. TradeOfferDeleteView.get_object = orig_get_object def test_trade_acceptance_update_view(self): """Test updating a trade acceptance via the update view.""" trade_offer_new = TradeOffer.objects.create(initiated_by=self.friend_code) TradeOfferHaveCard.objects.create( trade_offer=trade_offer_new, card=self.card1, quantity=1 ) TradeOfferWantCard.objects.create( trade_offer=trade_offer_new, card=self.card2, quantity=1 ) acceptance = TradeAcceptance.objects.create( trade_offer=trade_offer_new, accepted_by=self.friend_code, requested_card=self.card1, offered_card=self.card2, state=TradeAcceptance.AcceptanceState.ACCEPTED, ) update_url = reverse("trade_acceptance_update", kwargs={"pk": acceptance.pk}) # First, try an invalid state update. response = self.client.post(update_url, {"state": "INVALID_STATE"}) self.assertEqual(response.status_code, 200) form = response.context.get("form") self.assertIsNotNone(form, "Form should be present in the response context.") self.assertIn( "state", form.errors, "Expected an error on the 'state' field when an invalid state is submitted." ) self.assertTrue(form.errors["state"], "The 'state' field should have error messages.") # Next, if there is an allowed valid transition, try it. allowed_states = [choice[0] for choice in acceptance.get_allowed_state_transitions(user=self.user)] if allowed_states: valid_state = allowed_states[0] response = self.client.post(update_url, {"state": valid_state}) self.assertEqual(response.status_code, 302) class TradeOfferSecurityTests(TestCase): def setUp(self): User = get_user_model() # Create three users for testing various security scenarios self.user1 = User.objects.create_user( username="user1", email="user1@example.com", password="password1" ) self.user2 = User.objects.create_user( username="user2", email="user2@example.com", password="password2" ) self.user3 = User.objects.create_user( username="user3", email="user3@example.com", password="password3" ) # Create friend codes for each user with correct format self.fc1 = FriendCode.objects.create( friend_code="1111-2222-3333-4444", in_game_name="User1Game", user=self.user1 ) self.fc2 = FriendCode.objects.create( friend_code="5555-6666-7777-8888", in_game_name="User2Game", user=self.user2 ) self.fc3 = FriendCode.objects.create( friend_code="9999-0000-1111-2222", in_game_name="User3Game", user=self.user3 ) # Create test cards with proper rarity levels self.card1 = Card.objects.create( name="SecCard1", cardset="secset", cardnum=1, style="default", rarity_icon=RARITY_MAPPING[3], rarity_level=3 ) self.card2 = Card.objects.create( name="SecCard2", cardset="secset", cardnum=2, style="default", rarity_icon=RARITY_MAPPING[3], rarity_level=3 ) # Create a trade offer by user1 self.trade_offer = TradeOffer.objects.create(initiated_by=self.fc1) TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer, card=self.card1, quantity=1 ) TradeOfferWantCard.objects.create( trade_offer=self.trade_offer, card=self.card2, quantity=1 ) self.client = Client() def test_unauthorized_trade_offer_deletion(self): """Test that users cannot delete trade offers they don't own.""" self.client.login(username="user2", password="password2") response = self.client.post( reverse("trade_offer_delete", kwargs={"pk": self.trade_offer.pk}) ) self.assertEqual(response.status_code, 403) self.assertTrue(TradeOffer.objects.filter(pk=self.trade_offer.pk).exists()) def test_unauthorized_trade_acceptance_update(self): """Test that uninvolved users cannot update trade acceptances.""" # Create an acceptance between user2 and user1's offer acceptance = TradeAcceptance.objects.create( trade_offer=self.trade_offer, accepted_by=self.fc2, requested_card=self.card1, offered_card=self.card2, state=TradeAcceptance.AcceptanceState.ACCEPTED, ) # Try to update the acceptance as user3 (uninvolved) self.client.login(username="user3", password="password3") response = self.client.post( reverse("trade_acceptance_update", kwargs={"pk": acceptance.pk}), {"state": TradeAcceptance.AcceptanceState.SENT} ) self.assertEqual(response.status_code, 403) def test_cross_user_friend_code_manipulation(self): """Test that users cannot use other users' friend codes.""" self.client.login(username="user2", password="password2") # Try to create a trade offer using user1's friend code response = self.client.get( reverse("trade_offer_create"), { "initiated_by": self.fc1.pk, # User1's friend code "have_cards": [f"{self.card1.pk}:1"], "want_cards": [f"{self.card2.pk}:1"], } ) self.assertEqual(response.status_code, 200) # Form should fail validation self.assertFalse( TradeOffer.objects.filter(initiated_by=self.fc1).count() > 1 ) def test_authenticated_only_views(self): """Test that authenticated-only views are properly protected.""" # Test without login urls_to_test = [ reverse("trade_offer_create"), reverse("trade_offer_dashboard"), reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}), ] # First ensure we're logged out self.client.logout() for url in urls_to_test: response = self.client.get(url) self.assertRedirects( response, f"/accounts/login/?next={url}", msg_prefix=f"URL {url} should require authentication" ) class TradeOfferEdgeCasesTest(TestCase): def setUp(self): User = get_user_model() self.user = User.objects.create_user( username="edgeuser", email="edge@example.com", password="password" ) self.friend_code = FriendCode.objects.create( friend_code="3333-4444-5555-6666", in_game_name="EdgeUser", user=self.user ) # Create test cards with different rarities using proper levels and icons self.common_card = Card.objects.create( name="CommonCard", cardset="edgeset", cardnum=1, style="default", rarity_icon=RARITY_MAPPING[1], rarity_level=1 ) self.rare_card = Card.objects.create( name="RareCard", cardset="edgeset", cardnum=2, style="default", rarity_icon=RARITY_MAPPING[5], rarity_level=5 ) self.crown_card = Card.objects.create( name="CrownCard", cardset="edgeset", cardnum=3, style="default", rarity_icon=RARITY_MAPPING[8], rarity_level=8 ) self.client = Client() self.client.login(username="edgeuser", password="password") def test_zero_quantity_trade_offer(self): """Test that trade offers with zero quantity are handled properly.""" response = self.client.get( reverse("trade_offer_create"), { "initiated_by": self.friend_code.pk, "have_cards": [f"{self.common_card.pk}:0"], "want_cards": [f"{self.common_card.pk}:1"], } ) self.assertEqual(response.status_code, 200) self.assertFalse( TradeOffer.objects.filter(initiated_by=self.friend_code).exists() ) def test_negative_quantity_trade_offer(self): """Test that trade offers with negative quantity are handled properly.""" response = self.client.get( reverse("trade_offer_create"), { "initiated_by": self.friend_code.pk, "have_cards": [f"{self.common_card.pk}:-1"], "want_cards": [f"{self.common_card.pk}:1"], } ) self.assertEqual(response.status_code, 200) self.assertFalse( TradeOffer.objects.filter(initiated_by=self.friend_code).exists() ) def test_mixed_rarity_trade_offer(self): """Test that trade offers with mixed rarity cards are rejected.""" response = self.client.get( reverse("trade_offer_create"), { "initiated_by": self.friend_code.pk, "have_cards": [f"{self.common_card.pk}:1"], "want_cards": [f"{self.crown_card.pk}:1"], } ) self.assertEqual(response.status_code, 200) self.assertFalse( TradeOffer.objects.filter(initiated_by=self.friend_code).exists() ) def test_duplicate_card_entries(self): """Test handling of duplicate card entries in trade offers.""" response = self.client.get( reverse("trade_offer_create"), { "initiated_by": self.friend_code.pk, "have_cards": [ f"{self.common_card.pk}:1", f"{self.common_card.pk}:1" ], "want_cards": [f"{self.common_card.pk}:1"], } ) self.assertEqual(response.status_code, 200) self.assertFalse( TradeOffer.objects.filter(initiated_by=self.friend_code).exists() ) class TradeSearchTests(TestCase): def setUp(self): User = get_user_model() self.user = User.objects.create_user( username="searchuser", email="search@example.com", password="password" ) self.friend_code = FriendCode.objects.create( friend_code="7777-8888-9999-0000", in_game_name="SearchUser", user=self.user ) # Create test cards with proper rarity levels self.card1 = Card.objects.create( name="SearchCard1", cardset="sc1", cardnum=1, style="default", rarity_icon=RARITY_MAPPING[4], rarity_level=4 ) self.card2 = Card.objects.create( name="SearchCard2", cardset="sc1", cardnum=2, style="default", rarity_icon=RARITY_MAPPING[4], rarity_level=4 ) self.card3 = Card.objects.create( name="SearchCard3", cardset="sc1", cardnum=3, style="default", rarity_icon=RARITY_MAPPING[4], rarity_level=4 ) # Create some trade offers self.trade_offer1 = TradeOffer.objects.create(initiated_by=self.friend_code) TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer1, card=self.card1, quantity=2 ) TradeOfferWantCard.objects.create( trade_offer=self.trade_offer1, card=self.card2, quantity=1 ) self.trade_offer2 = TradeOffer.objects.create(initiated_by=self.friend_code) TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer2, card=self.card2, quantity=1 ) TradeOfferWantCard.objects.create( trade_offer=self.trade_offer2, card=self.card3, quantity=1 ) self.client = Client() def test_search_by_have_cards(self): """Test searching for trade offers by cards the user has doesn't show offers initiated by the user.""" response = self.client.post( reverse("trade_offer_search"), { "have_cards": [f"{self.card2.pk}:1"], } ) self.assertEqual(response.status_code, 200) self.assertNotContains(response, self.trade_offer1.initiated_by.in_game_name) self.assertNotContains(response, self.trade_offer2.initiated_by.in_game_name) def test_search_by_want_cards(self): """Test searching for trade offers by cards the user wants doesn't show offers initiated by the user.""" response = self.client.post( reverse("trade_offer_search"), { "want_cards": [f"{self.card1.pk}:1"], } ) self.assertEqual(response.status_code, 200) self.assertNotContains(response, self.trade_offer1.initiated_by.in_game_name) self.assertNotContains(response, self.trade_offer2.initiated_by.in_game_name) def test_search_with_invalid_card_id(self): """Test search behavior with invalid card IDs.""" response = self.client.post( reverse("trade_offer_search"), { "have_cards": ["999999:1"], # Non-existent card ID } ) self.assertEqual(response.status_code, 200) self.assertNotContains(response, self.trade_offer1.initiated_by.in_game_name) self.assertNotContains(response, self.trade_offer2.initiated_by.in_game_name) def test_search_closed_trades(self): """Test that closed trades don't appear in search results.""" self.trade_offer1.is_closed = True self.trade_offer1.save() response = self.client.post( reverse("trade_offer_search"), { "have_cards": [f"{self.card2.pk}:1"], } ) self.assertEqual(response.status_code, 200) self.assertNotContains(response, self.trade_offer1.initiated_by.in_game_name) class TradeAcceptanceComplexTests(TestCase): def setUp(self): User = get_user_model() self.initiator = User.objects.create_user( username="initiator", email="init@example.com", password="password" ) self.acceptor = User.objects.create_user( username="acceptor", email="accept@example.com", password="password" ) self.initiator_fc = FriendCode.objects.create( friend_code="1234-5678-9012-3456", in_game_name="InitUser", user=self.initiator ) self.acceptor_fc = FriendCode.objects.create( friend_code="6543-2109-8765-4321", in_game_name="AcceptUser", user=self.acceptor ) # Create test cards with proper rarity levels self.card1 = Card.objects.create( name="ComplexCard1", cardset="cx1", cardnum=1, style="default", rarity_icon=RARITY_MAPPING[6], rarity_level=6 ) self.card2 = Card.objects.create( name="ComplexCard2", cardset="cx1", cardnum=2, style="default", rarity_icon=RARITY_MAPPING[6], rarity_level=6 ) self.card3 = Card.objects.create( name="ComplexCard3", cardset="cx1", cardnum=3, style="default", rarity_icon=RARITY_MAPPING[6], rarity_level=6 ) self.card4 = Card.objects.create( name="ComplexCard4", cardset="cx1", cardnum=4, style="default", rarity_icon=RARITY_MAPPING[6], rarity_level=6 ) # Create a trade offer with multiple quantities self.trade_offer = TradeOffer.objects.create(initiated_by=self.initiator_fc) TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer, card=self.card1, quantity=3 ) TradeOfferHaveCard.objects.create( trade_offer=self.trade_offer, card=self.card3, quantity=1 ) TradeOfferWantCard.objects.create( trade_offer=self.trade_offer, card=self.card2, quantity=3 ) TradeOfferWantCard.objects.create( trade_offer=self.trade_offer, card=self.card4, quantity=1 ) self.client = Client() def test_multiple_acceptances_quantity_limit(self): """Test that multiple acceptances cannot exceed the offer's quantity limit.""" self.client.login(username="acceptor", password="password") # Create first acceptance response1 = self.client.post( reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}), { "accepted_by": self.acceptor_fc.pk, "requested_card": self.card1.pk, "offered_card": self.card2.pk, } ) self.assertEqual(response1.status_code, 302) # Successful creation # Create second acceptance response2 = self.client.post( reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}), { "accepted_by": self.acceptor_fc.pk, "requested_card": self.card1.pk, "offered_card": self.card2.pk, } ) self.assertEqual(response2.status_code, 302) # Successful creation # Try to create a fourth acceptance (should fail as only 3 are allowed) response3 = self.client.post( reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}), { "accepted_by": self.acceptor_fc.pk, "requested_card": self.card1.pk, "offered_card": self.card2.pk, } ) self.assertEqual(response3.status_code, 302) # Successful creation response4 = self.client.post( reverse("trade_acceptance_create", kwargs={"offer_pk": self.trade_offer.pk}), { "accepted_by": self.acceptor_fc.pk, "requested_card": self.card1.pk, "offered_card": self.card2.pk, } ) self.assertEqual(response4.status_code, 200) # Should fail self.assertEqual( self.trade_offer.acceptances.count(), 3, "Should not allow more acceptances than the quantity limit" ) def test_complex_state_transitions(self): """Test complex state transition scenarios.""" self.client.login(username="acceptor", password="password") # Create an acceptance acceptance = TradeAcceptance.objects.create( trade_offer=self.trade_offer, accepted_by=self.acceptor_fc, requested_card=self.card1, offered_card=self.card2, state=TradeAcceptance.AcceptanceState.ACCEPTED, ) # Test invalid state transition sequence invalid_transitions = [ TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR, # Can't thank before sending TradeAcceptance.AcceptanceState.RECEIVED, # Can't receive before sending TradeAcceptance.AcceptanceState.THANKED_BY_BOTH, # Can't thank by both directly ] for invalid_state in invalid_transitions: response = self.client.post( reverse("trade_acceptance_update", kwargs={"pk": acceptance.pk}), {"state": invalid_state} ) self.assertEqual(response.status_code, 200) # Should stay on form acceptance.refresh_from_db() self.assertEqual( acceptance.state, TradeAcceptance.AcceptanceState.ACCEPTED, f"Invalid transition to {invalid_state} should not be allowed" ) # Test valid state transition sequence valid_transitions = [ (self.initiator, TradeAcceptance.AcceptanceState.SENT), (self.acceptor, TradeAcceptance.AcceptanceState.RECEIVED), (self.initiator, TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR), (self.acceptor, TradeAcceptance.AcceptanceState.THANKED_BY_BOTH), ] for user, state in valid_transitions: self.client.login(username=user.username, password="password") response = self.client.post( reverse("trade_acceptance_update", kwargs={"pk": acceptance.pk}), {"state": state} ) self.assertEqual(response.status_code, 302) # Should redirect on success acceptance.refresh_from_db() self.assertEqual( acceptance.state, state, f"Valid transition to {state} should be allowed" )