pkmntrade.club/trades/tests.py

925 lines
38 KiB
Python

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"
)