Add initial trades tests
This commit is contained in:
parent
d66ed823e0
commit
2785e0ed13
2 changed files with 477 additions and 12 deletions
463
trades/tests.py
463
trades/tests.py
|
|
@ -1,3 +1,462 @@
|
||||||
from django.test import TestCase
|
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
|
||||||
|
|
||||||
# Create your tests here.
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
# Model Tests
|
||||||
|
# ------------------------------------------------------------------------
|
||||||
|
class TradeOfferModelTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
User = get_user_model()
|
||||||
|
# Create a user and friend code for testing
|
||||||
|
self.user = User.objects.create_user(
|
||||||
|
username="testuser", email="test@example.com", password="password"
|
||||||
|
)
|
||||||
|
self.friend_code = FriendCode.objects.create(
|
||||||
|
friend_code="FC-1234", 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="R", rarity_level=1
|
||||||
|
)
|
||||||
|
self.card2 = Card.objects.create(
|
||||||
|
name="Card2", cardset="set1", cardnum=2, style="default", rarity_icon="R", 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="SR", rarity_level=2
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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, "R")
|
||||||
|
|
||||||
|
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="FC-5678", 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="FC-0000", 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="R", rarity_level=1
|
||||||
|
)
|
||||||
|
self.card2 = Card.objects.create(
|
||||||
|
name="CardB", cardset="setA", cardnum=2, style="default", rarity_icon="R", rarity_level=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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="FC-FORM", 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="R", rarity_level=1
|
||||||
|
)
|
||||||
|
self.card2 = Card.objects.create(
|
||||||
|
name="FormCard2", cardset="formset", cardnum=2, style="default",
|
||||||
|
rarity_icon="R", rarity_level=1
|
||||||
|
)
|
||||||
|
|
||||||
|
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="FC-VIEW", 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="R", rarity_level=1
|
||||||
|
)
|
||||||
|
self.card2 = Card.objects.create(
|
||||||
|
name="ViewCard2", cardset="setV", cardnum=2, style="default",
|
||||||
|
rarity_icon="R", rarity_level=1
|
||||||
|
)
|
||||||
|
# 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)
|
||||||
|
|
|
||||||
|
|
@ -230,14 +230,16 @@ class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
template_name = "trades/trade_offer_delete.html"
|
template_name = "trades/trade_offer_delete.html"
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
trade_offer = self.get_object()
|
# Retrieve the object normally
|
||||||
if trade_offer.initiated_by_id not in request.user.friend_codes.values_list("id", flat=True):
|
self.object = super().get_object()
|
||||||
|
# Perform the permission check here
|
||||||
|
if self.object.initiated_by_id not in request.user.friend_codes.values_list("id", flat=True):
|
||||||
raise PermissionDenied("You are not authorized to delete or close this trade offer.")
|
raise PermissionDenied("You are not authorized to delete or close this trade offer.")
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
trade_offer = self.get_object()
|
trade_offer = self.object
|
||||||
terminal_states = [
|
terminal_states = [
|
||||||
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
|
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
|
||||||
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
|
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
|
||||||
|
|
@ -255,7 +257,7 @@ class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
trade_offer = self.get_object()
|
trade_offer = self.object
|
||||||
terminal_states = [
|
terminal_states = [
|
||||||
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
|
TradeAcceptance.AcceptanceState.THANKED_BY_INITIATOR,
|
||||||
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
|
TradeAcceptance.AcceptanceState.THANKED_BY_ACCEPTOR,
|
||||||
|
|
@ -265,8 +267,11 @@ class TradeOfferDeleteView(LoginRequiredMixin, DeleteView):
|
||||||
]
|
]
|
||||||
active_acceptances = trade_offer.acceptances.exclude(state__in=terminal_states)
|
active_acceptances = trade_offer.acceptances.exclude(state__in=terminal_states)
|
||||||
if active_acceptances.exists():
|
if active_acceptances.exists():
|
||||||
messages.error(request, "Cannot delete or close this trade offer because there are active acceptances.")
|
messages.error(
|
||||||
context = self.get_context_data(object=trade_offer)
|
request,
|
||||||
|
"Cannot close this trade offer while there are active acceptances. Please reject all acceptances before closing, or finish the trades."
|
||||||
|
)
|
||||||
|
context = self.get_context_data()
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
else:
|
else:
|
||||||
if trade_offer.acceptances.count() > 0:
|
if trade_offer.acceptances.count() > 0:
|
||||||
|
|
@ -583,13 +588,14 @@ class TradeAcceptanceUpdateView(LoginRequiredMixin, UpdateView):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
new_state = form.cleaned_data["state"]
|
new_state = form.cleaned_data["state"]
|
||||||
#match the new state to the TradeAcceptance.AcceptanceState enum
|
try:
|
||||||
if new_state not in TradeAcceptance.AcceptanceState:
|
# Try to cast new_state to the enum member
|
||||||
|
valid_state = TradeAcceptance.AcceptanceState(new_state)
|
||||||
|
except ValueError:
|
||||||
form.add_error("state", "Invalid state transition.")
|
form.add_error("state", "Invalid state transition.")
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
try:
|
try:
|
||||||
# pass the new state and the current user to the update_state method
|
form.instance.update_state(valid_state, self.request.user)
|
||||||
form.instance.update_state(new_state, self.request.user)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
form.add_error("state", str(e))
|
form.add_error("state", str(e))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue