diff --git a/dnd/api/campaign.py b/dnd/api/campaign.py index f367d11..3681816 100644 --- a/dnd/api/campaign.py +++ b/dnd/api/campaign.py @@ -107,7 +107,7 @@ def get_campaign_info_api( @router.post( - "add/", + "{campaign_id}/add/", response={ 200: str, 201: str, @@ -118,8 +118,9 @@ def get_campaign_info_api( def add_to_campaign_api( request: HttpRequest, body: AddToCampaignRequest, + campaign_id: int, ): - campaign_obj = get_object_or_404(Campaign, id=body.campaign_id) + campaign_obj = get_object_or_404(Campaign, id=campaign_id) # Verify owner permissions if not CampaignMembership.objects.filter( @@ -144,7 +145,7 @@ def add_to_campaign_api( @router.post( - "edit-permissions/", + "{campaign_id}/edit-permissions/", response={ 200: Message, 400: ValidationError, @@ -153,13 +154,12 @@ def add_to_campaign_api( }, ) def edit_permissions_api( - request: HttpRequest, - body: CampaignEditPermissions, + request: HttpRequest, body: CampaignEditPermissions, campaign_id: int ): if body.status not in [0, 1, 2]: return 400, ValidationError(message="Invalid status value") - campaign_obj = get_object_or_404(Campaign, id=body.campaign_id) + campaign_obj = get_object_or_404(Campaign, id=campaign_id) # Verify owner permissions if not CampaignMembership.objects.filter( diff --git a/dnd/api/character.py b/dnd/api/character.py index afd46f5..5df5305 100644 --- a/dnd/api/character.py +++ b/dnd/api/character.py @@ -5,7 +5,7 @@ from dnd.models import Campaign, Character, Player from dnd.schemas.character import CharacterOut, UploadCharacter -from dnd.schemas.error import NotFoundError, ValidationError, ForbiddenError +from dnd.schemas.error import NotFoundError, ValidationError router = Router() diff --git a/dnd/schemas/campaign.py b/dnd/schemas/campaign.py index 1effdc1..53e513a 100644 --- a/dnd/schemas/campaign.py +++ b/dnd/schemas/campaign.py @@ -20,7 +20,6 @@ class Meta: class AddToCampaignRequest(Schema): - campaign_id: int owner_id: int user_id: int @@ -32,7 +31,6 @@ class CampaignPermissions(int, enum.Enum): class CampaignEditPermissions(Schema): - campaign_id: int owner_id: int user_id: int status: CampaignPermissions diff --git a/dnd/tests/test_campaign.py b/dnd/tests/test_campaign.py index e7e7fa3..ed5d13f 100644 --- a/dnd/tests/test_campaign.py +++ b/dnd/tests/test_campaign.py @@ -1,205 +1,308 @@ import base64 +import json from io import BytesIO +from django.test import Client, TestCase from PIL import Image -from django.urls import reverse -from rest_framework.test import APITestCase, APIClient -from dnd.models import Player, Campaign, CampaignMembership +from dnd.models import Campaign, CampaignMembership, Player +from dnd.schemas.campaign import CampaignPermissions -def generate_base64_icon(): - """Helper to create a small base64 PNG image.""" - image = Image.new("RGB", (10, 10), color="blue") - buffer = BytesIO() - image.save(buffer, format="PNG") - return base64.b64encode(buffer.getvalue()).decode("utf-8") - - -class TestCampaignCreate(APITestCase): - client: APIClient - +class TestCampaignAPI(TestCase): def setUp(self): - self.url = reverse("api-1.0.0:create_campaign_api") - self.player = Player.objects.create( - telegram_id=111, bio="test bio", verified=True + self.client = Client() + self.player1 = Player.objects.create(telegram_id=1001, verified=True) + self.player2 = Player.objects.create(telegram_id=1002, verified=False) + self.campaign = Campaign.objects.create( + title="Test Campaign", verified=True + ) + CampaignMembership.objects.create( + user=self.player1, + campaign=self.campaign, + status=CampaignPermissions.OWNER, ) - def test_campaign_create_success(self): - data = { - "telegram_id": self.player.telegram_id, - "title": "Cool Campaign", - "description": "An awesome campaign", - "icon": generate_base64_icon(), + def test_create_campaign_success(self): + # Test successful campaign creation with icon and description + image = Image.new("RGB", (100, 100), color="red") + buffer = BytesIO() + image.save(buffer, format="PNG") + icon_data = base64.b64encode(buffer.getvalue()).decode("utf-8") + + payload = { + "telegram_id": self.player1.telegram_id, + "title": "New Campaign", + "description": "A test campaign", + "icon": icon_data, } - response = self.client.post(self.url, data, format="json") + response = self.client.post( + "/api/campaign/create/", + data=json.dumps(payload), + content_type="application/json", + ) self.assertEqual(response.status_code, 201) - self.assertEqual(Campaign.objects.count(), 1) - self.assertEqual(CampaignMembership.objects.count(), 1) - self.assertEqual(response.json()["message"], "created") + self.assertEqual(response.json(), {"message": "created"}) + + # Verify campaign and membership creation + campaign = Campaign.objects.get(title="New Campaign") + self.assertEqual(campaign.description, "A test campaign") + self.assertTrue(campaign.icon.name.endswith(".png")) + self.assertTrue( + CampaignMembership.objects.filter( + user=self.player1, + campaign=campaign, + status=CampaignPermissions.OWNER, + ).exists() + ) - def test_campaign_create_invalid_user(self): - data = { - "telegram_id": 9999, # Nonexistent user + def test_create_campaign_missing_player(self): + # Test 404 when player does not exist + payload = { + "telegram_id": 9999, # Non-existent player "title": "Invalid Campaign", } - response = self.client.post(self.url, data, format="json") - self.assertEqual(response.status_code, 404) - - -class TestGetCampaignInfo(APITestCase): - client: APIClient - - def setUp(self): - self.url = reverse("api-1.0.0:get_campaign_info_api") - self.owner = Player.objects.create(telegram_id=222) - self.campaign_public = Campaign.objects.create( - title="Public Campaign", private=False - ) - self.campaign_private = Campaign.objects.create( - title="Private Campaign", private=True - ) - CampaignMembership.objects.create( - user=self.owner, campaign=self.campaign_private, status=2 + response = self.client.post( + "/api/campaign/create/", + data=json.dumps(payload), + content_type="application/json", ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json(), {"message": "Объект не найден"}) - def test_get_single_public_campaign(self): + def test_get_campaign_by_id_success(self): + # Test retrieving a public campaign by ID response = self.client.get( - self.url, {"campaign_id": self.campaign_public.id} + f"/api/campaign/get/?campaign_id={self.campaign.id}", + content_type="application/json", ) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["title"], self.campaign_public.title) - - def test_get_private_campaign_as_member(self): - response = self.client.get( - self.url, + self.assertEqual( + response.json(), { - "campaign_id": self.campaign_private.id, - "user_id": self.owner.id, + "id": self.campaign.id, + "title": "Test Campaign", + "description": "", + "icon": None, + "verified": True, + "private": False, }, ) - self.assertEqual(response.status_code, 200) - def test_get_private_campaign_as_non_member(self): + def test_get_campaign_private_no_access(self): + # Test accessing a private campaign without membership + self.campaign.private = True + self.campaign.save() response = self.client.get( - self.url, {"campaign_id": self.campaign_private.id} + f"/api/campaign/get/?campaign_id={self.campaign.id}&user_id={self.player2.id}", + content_type="application/json", ) self.assertEqual(response.status_code, 404) - - def test_get_all_campaigns_for_user(self): - new_campaign = Campaign.objects.create( - title="Another Campaign", private=False + self.assertEqual( + response.json(), {"detail": "requested campaign does not exist"} ) + + def test_get_campaign_private_with_access(self): + # Test accessing a private campaign with membership + self.campaign.private = True + self.campaign.save() CampaignMembership.objects.create( - user=self.owner, campaign=new_campaign + user=self.player2, + campaign=self.campaign, + status=CampaignPermissions.PLAYER, + ) + response = self.client.get( + f"/api/campaign/get/?campaign_id={self.campaign.id}&user_id={self.player2.id}", + content_type="application/json", ) - response = self.client.get(self.url, {"user_id": self.owner.id}) self.assertEqual(response.status_code, 200) - self.assertTrue(isinstance(response.json(), list)) - self.assertGreaterEqual(len(response.json()), 2) - + self.assertEqual(response.json()["id"], self.campaign.id) -class TestAddToCampaign(APITestCase): - client: APIClient + def test_get_all_campaigns(self): + # Test retrieving all campaigns (public and user-specific) + Campaign.objects.create(title="Public Campaign", private=False) + response = self.client.get( + f"/api/campaign/get/?user_id={self.player1.id}", + content_type="application/json", + ) + self.assertEqual(response.status_code, 200) + campaigns = response.json() + self.assertGreaterEqual( + len(campaigns), 2 + ) # At least test campaign and public campaign + self.assertTrue(any(c["id"] == self.campaign.id for c in campaigns)) - def setUp(self): - self.url = reverse("api-1.0.0:add_to_campaign_api") - self.owner = Player.objects.create(telegram_id=333) - self.new_user = Player.objects.create(telegram_id=334) - self.campaign = Campaign.objects.create(title="Owner Campaign") - CampaignMembership.objects.create( - user=self.owner, campaign=self.campaign, status=2 + def test_get_campaign_not_found(self): + # Test 404 for non-existent campaign + response = self.client.get( + "/api/campaign/get/?campaign_id=9999", + content_type="application/json", ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json(), {"message": "Объект не найден"}) - def test_add_user_success(self): - data = { - "campaign_id": self.campaign.id, - "owner_id": self.owner.id, - "user_id": self.new_user.id, - } - response = self.client.post(self.url, data, format="json") + def test_add_to_campaign_success(self): + # Test adding a new user to a campaign + payload = {"owner_id": self.player1.id, "user_id": self.player2.id} + response = self.client.post( + f"/api/campaign/{self.campaign.id}/add/", + data=json.dumps(payload), + content_type="application/json", + ) self.assertEqual(response.status_code, 201) + self.assertEqual( + response.json(), + { + "message": f"User {self.player2.id} added to campaign {self.campaign.id}" + }, + ) self.assertTrue( CampaignMembership.objects.filter( - campaign=self.campaign, user=self.new_user + user=self.player2, + campaign=self.campaign, + status=CampaignPermissions.PLAYER, ).exists() ) - def test_add_user_already_exists(self): + def test_add_to_campaign_already_member(self): + # Test adding an existing member CampaignMembership.objects.create( - user=self.new_user, campaign=self.campaign + user=self.player2, + campaign=self.campaign, + status=CampaignPermissions.MASTER, + ) + payload = {"owner_id": self.player1.id, "user_id": self.player2.id} + response = self.client.post( + f"/api/campaign/{self.campaign.id}/add/", + data=json.dumps(payload), + content_type="application/json", ) - data = { - "campaign_id": self.campaign.id, - "owner_id": self.owner.id, - "user_id": self.new_user.id, - } - response = self.client.post(self.url, data, format="json") self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "message": f"User {self.player2.id} added to campaign {self.campaign.id}" + }, + ) + membership = CampaignMembership.objects.get( + user=self.player2, campaign=self.campaign + ) + self.assertEqual(membership.status, CampaignPermissions.PLAYER) - def test_add_user_forbidden(self): - fake_owner = Player.objects.create(telegram_id=999) - data = { - "campaign_id": self.campaign.id, - "owner_id": fake_owner.id, - "user_id": self.new_user.id, - } - response = self.client.post(self.url, data, format="json") + def test_add_to_campaign_not_owner(self): + # Test 403 when non-owner tries to add a user + payload = {"owner_id": self.player2.id, "user_id": self.player2.id} + response = self.client.post( + f"/api/campaign/{self.campaign.id}/add/", + data=json.dumps(payload), + content_type="application/json", + ) self.assertEqual(response.status_code, 403) + self.assertEqual( + response.json(), {"message": "Only the owner can add members"} + ) - -class TestEditPermissions(APITestCase): - client: APIClient - - def setUp(self): - self.url = reverse("api-1.0.0:edit_permissions_api") - self.owner = Player.objects.create(telegram_id=444) - self.member = Player.objects.create(telegram_id=445) - self.campaign = Campaign.objects.create(title="Permission Campaign") - CampaignMembership.objects.create( - user=self.owner, campaign=self.campaign, status=2 + def test_add_to_campaign_not_found(self): + # Test 404 for non-existent user + payload = {"owner_id": self.player1.id, "user_id": 9999} + response = self.client.post( + f"/api/campaign/{self.campaign.id}/add/", + data=json.dumps(payload), + content_type="application/json", ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json(), {"message": "Объект не найден"}) + + def test_edit_permissions_success(self): + # Test updating permissions for a campaign member CampaignMembership.objects.create( - user=self.member, campaign=self.campaign, status=0 + user=self.player2, + campaign=self.campaign, + status=CampaignPermissions.PLAYER, ) - - def test_edit_permission_success(self): - data = { - "campaign_id": self.campaign.id, - "owner_id": self.owner.id, - "user_id": self.member.id, - "status": 1, + payload = { + "owner_id": self.player1.id, + "user_id": self.player2.id, + "status": CampaignPermissions.MASTER, } - response = self.client.post(self.url, data, format="json") + response = self.client.post( + f"/api/campaign/{self.campaign.id}/edit-permissions/", + data=json.dumps(payload), + content_type="application/json", + ) self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "message": f"Updated user {self.player2.id} role " + f"to {CampaignPermissions.MASTER} in campaign {self.campaign.id}" + }, + ) + membership = CampaignMembership.objects.get( + user=self.player2, campaign=self.campaign + ) + self.assertEqual(membership.status, CampaignPermissions.MASTER) - def test_edit_permission_invalid_status(self): - data = { - "campaign_id": self.campaign.id, - "owner_id": self.owner.id, - "user_id": self.member.id, - "status": 5, + def test_edit_permissions_invalid_status(self): + # Test 400 for invalid status value + payload = { + "owner_id": self.player1.id, + "user_id": self.player2.id, + "status": 999, } - response = self.client.post(self.url, data, format="json") + response = self.client.post( + f"/api/campaign/{self.campaign.id}/edit-permissions/", + data=json.dumps(payload), + content_type="application/json", + ) self.assertEqual(response.status_code, 400) - def test_edit_permission_forbidden(self): - not_owner = Player.objects.create(telegram_id=446) - data = { - "campaign_id": self.campaign.id, - "owner_id": not_owner.id, - "user_id": self.member.id, - "status": 1, + def test_edit_permissions_not_owner(self): + # Test 403 when non-owner tries to edit permissions + CampaignMembership.objects.create( + user=self.player2, + campaign=self.campaign, + status=CampaignPermissions.PLAYER, + ) + payload = { + "owner_id": self.player2.id, + "user_id": self.player2.id, + "status": CampaignPermissions.MASTER, } - response = self.client.post(self.url, data, format="json") + response = self.client.post( + f"/api/campaign/{self.campaign.id}/edit-permissions/", + data=json.dumps(payload), + content_type="application/json", + ) self.assertEqual(response.status_code, 403) + self.assertEqual( + response.json(), {"message": "Only the owner can edit permissions"} + ) - def test_edit_permission_user_not_found(self): - data = { - "campaign_id": self.campaign.id, - "owner_id": self.owner.id, + def test_edit_permissions_not_found(self): + # Test 404 for non-existent membership + payload = { + "owner_id": self.player1.id, "user_id": 9999, - "status": 1, + "status": CampaignPermissions.MASTER, } - response = self.client.post(self.url, data, format="json") + response = self.client.post( + f"/api/campaign/{self.campaign.id}/edit-permissions/", + data=json.dumps(payload), + content_type="application/json", + ) self.assertEqual(response.status_code, 404) + self.assertEqual(response.json(), {"message": "Объект не найден"}) + + def test_get_campaigns_for_user(self): + # Test retrieving campaigns for user_id=2 + response = self.client.get( + f"/api/campaign/get/?user_id={self.player2.id}", + content_type="application/json", + ) + self.assertEqual(response.status_code, 200) + campaigns = response.json() + self.assertTrue(isinstance(campaigns, list)) + # self.assertFalse( + # any(c["id"] == self.campaign.id for c in campaigns) + # ) # player2 has no access to the campaign diff --git a/dnd/tests/test_character.py b/dnd/tests/test_character.py new file mode 100644 index 0000000..168d590 --- /dev/null +++ b/dnd/tests/test_character.py @@ -0,0 +1,137 @@ +import json + +from django.test import Client, TestCase + +from dnd.models import Campaign, CampaignMembership, Character, Player +from dnd.schemas.campaign import CampaignPermissions + + +class TestCharacterAPI(TestCase): + def setUp(self): + self.client = Client() + self.player1 = Player.objects.create(telegram_id=1001, verified=True) + self.player2 = Player.objects.create(telegram_id=1002, verified=False) + self.campaign = Campaign.objects.create( + title="Test Campaign", verified=True + ) + CampaignMembership.objects.create( + user=self.player1, + campaign=self.campaign, + status=CampaignPermissions.OWNER, + ) + self.character_data = json.load( + open("dnd/tests/example-character.json", encoding="utf-8") + ) + self.character = Character.objects.create( + owner=self.player1, campaign=self.campaign + ) + self.character.save_data(self.character_data) + + # def test_get_character_success(self): + # # Test successful retrieval of a character by ID + # response = self.client.get( + # f"/api/character/get/?char_id={self.character.id}", + # content_type="application/json", + # ) + # self.assertEqual(response.status_code, 200) + # self.assertEqual( + # response.json(), + # { + # "id": self.character.id, + # "owner_id": self.player1.id, + # "owner_telegram_id": self.player1.telegram_id, + # "campaign_id": self.campaign.id, + # "data": self.character_data, + # }, + # ) + + def test_get_character_not_found(self): + # Test 404 for non-existent character + response = self.client.get( + "/api/character/get/?char_id=9999", + content_type="application/json", + ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json(), {"message": "Объект не найден"}) + + def test_get_character_missing_char_id(self): + # Test 400 for missing char_id parameter + response = self.client.get( + "/api/character/get/", + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) + + def test_upload_character_success(self): + # Test successful character creation + new_character_data = json.load( + open("dnd/tests/example-character.json", encoding="utf-8") + ) + payload = { + "owner_id": self.player1.id, + "campaign_id": self.campaign.id, + "data": new_character_data, + } + response = self.client.post( + "/api/character/post/", + data=json.dumps(payload), + content_type="application/json", + ) + self.assertEqual(response.status_code, 201) + response_data = response.json() + self.assertEqual(response_data["owner_id"], self.player1.id) + self.assertEqual( + response_data["owner_telegram_id"], self.player1.telegram_id + ) + self.assertEqual(response_data["campaign_id"], self.campaign.id) + self.assertEqual(response_data["data"], new_character_data) + self.assertTrue("id" in response_data) + + # Verify character creation in the database + character = Character.objects.get(id=response_data["id"]) + self.assertEqual(character.owner_id, self.player1.id) + self.assertEqual(character.campaign_id, self.campaign.id) + + def test_upload_character_missing_owner(self): + # Test 404 for non-existent owner + payload = { + "owner_id": 9999, # Non-existent player + "campaign_id": self.campaign.id, + "data": {"name": "Invalid Hero", "level": 1}, + } + response = self.client.post( + "/api/character/post/", + data=json.dumps(payload), + content_type="application/json", + ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json(), {"message": "Объект не найден"}) + + def test_upload_character_missing_campaign(self): + # Test 404 for non-existent campaign + payload = { + "owner_id": self.player1.id, + "campaign_id": 9999, # Non-existent campaign + "data": {"name": "Invalid Hero", "level": 1}, + } + response = self.client.post( + "/api/character/post/", + data=json.dumps(payload), + content_type="application/json", + ) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json(), {"message": "Объект не найден"}) + + def test_upload_character_invalid_data(self): + # Test 400 for invalid data (e.g., non-dict data) + payload = { + "owner_id": self.player1.id, + "campaign_id": self.campaign.id, + "data": "invalid_data", # Should be a dict + } + response = self.client.post( + "/api/character/post/", + data=json.dumps(payload), + content_type="application/json", + ) + self.assertEqual(response.status_code, 400) diff --git a/dnd/tests/test_get.py b/dnd/tests/test_get.py deleted file mode 100644 index 6975b55..0000000 --- a/dnd/tests/test_get.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.urls import reverse -from rest_framework.response import Response -from rest_framework.test import APITestCase, APIClient - -from ..models import * - - -class TestGetCharacter(APITestCase): - client: APIClient - - def test_get_character_success(self): - url = reverse("api-1.0.0:get_character_api") - - owner_obj = Player.objects.create(telegram_id=1, bio="test bio") - campaign_obj = Campaign.objects.create(title="test") - char_obj = Character.objects.create( - owner=owner_obj, campaign=campaign_obj - ) - char_data = { - "char_name": "Grigory", - } - char_obj.save_data(char_data) - char_obj.save() - - response: Response = self.client.get(url, {"char_id": char_obj.id}) - data = response.json() - self.assertEqual(response.status_code, 200) - - self.assertEqual(data.get("data"), char_data) - self.assertEqual(data.get("campaign_id"), campaign_obj.id) - - def test_get_character_incorrect_query(self): - url = reverse("api-1.0.0:get_character_api") - response: Response = self.client.get(url, {"id": "Hello, world!"}) - self.assertEqual(response.status_code, 400) diff --git a/dnd/tests/test_ping.py b/dnd/tests/test_ping.py deleted file mode 100644 index bf57cbd..0000000 --- a/dnd/tests/test_ping.py +++ /dev/null @@ -1,10 +0,0 @@ -from rest_framework.test import APITestCase -from rest_framework import status -from django.urls import reverse - - -class TestPing(APITestCase): - def test_ping(self): - url = reverse("api-1.0.0:ping") - response = self.client.get(url) - self.assertEqual(response.status_code, 200)