Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dnd/api/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def get_campaign_info_api(
campaign=campaign_obj, user_id=user_id
).exists()
):
# 👌 disguise private campaigns as non-existent
# disguise private campaigns as non-existent
raise HttpError(404, "requested campaign does not exist")

return campaign_obj
Expand Down
219 changes: 70 additions & 149 deletions dnd/tests/test_campaign.py
Original file line number Diff line number Diff line change
@@ -1,283 +1,204 @@
import base64
from io import BytesIO

from PIL import Image
from django.urls import reverse
from rest_framework.test import APITestCase, APIClient

from ..models import Player, Campaign, CampaignMembership
from dnd.models import Player, Campaign, CampaignMembership


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

def setUp(self):
self.url = reverse("api-1.0.0:create_campaign_api")
self.player = Player.objects.create(telegram_id=12345, bio="test bio")
self.player = Player.objects.create(
telegram_id=111, bio="test bio", verified=True
)

def test_campaign_create_success(self):
data = {
"telegram_id": self.player.telegram_id,
"title": "test campaign",
"description": "the best campaign ever",
"icon": base64.b64encode(
open("dnd/tests/test-campaign-icon.jpg", "rb").read()
),
"title": "Cool Campaign",
"description": "An awesome campaign",
"icon": generate_base64_icon(),
}
response = self.client.post(self.url, data, format="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")

def test_campaign_create_missing_telegram_id(self):
data = {"title": "no user campaign"}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 400)

def test_campaign_create_invalid_telegram_id_type(self):
data = {"telegram_id": "not an int", "title": "fail"}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 400)

def test_campaign_create_nonexistent_user(self):
data = {"telegram_id": 99999, "title": "ghost campaign"}
def test_campaign_create_invalid_user(self):
data = {
"telegram_id": 9999, # Nonexistent user
"title": "Invalid Campaign",
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 404)

def test_campaign_create_missing_title(self):
data = {"telegram_id": self.player.telegram_id}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 400)

def test_campaign_create_invalid_title_type(self):
data = {"telegram_id": self.player.telegram_id, "title": 12345}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 400)


class TestGetCampaignInfo(APITestCase):
client: APIClient

def setUp(self):
self.url = reverse("api-1.0.0:get_campaign_info_api")
self.player = Player.objects.create(telegram_id=111, bio="user")
self.public_campaign = Campaign.objects.create(
self.owner = Player.objects.create(telegram_id=222)
self.campaign_public = Campaign.objects.create(
title="Public Campaign", private=False
)
self.private_campaign = Campaign.objects.create(
self.campaign_private = Campaign.objects.create(
title="Private Campaign", private=True
)
CampaignMembership.objects.create(
user=self.player, campaign=self.private_campaign, status=2
user=self.owner, campaign=self.campaign_private, status=2
)

def test_get_specific_public_campaign(self):
def test_get_single_public_campaign(self):
response = self.client.get(
self.url, {"campaign_id": self.public_campaign.id}
self.url, {"campaign_id": self.campaign_public.id}
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["title"], "Public Campaign")

def test_get_specific_campaign_not_found(self):
response = self.client.get(self.url, {"campaign_id": 9999})
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json()["title"], self.campaign_public.title)

def test_get_private_campaign_with_access(self):
def test_get_private_campaign_as_member(self):
response = self.client.get(
self.url,
{
"campaign_id": self.private_campaign.id,
"user_id": self.player.id,
"campaign_id": self.campaign_private.id,
"user_id": self.owner.id,
},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["title"], "Private Campaign")

def test_get_private_campaign_without_access(self):
another_user = Player.objects.create(telegram_id=222)
def test_get_private_campaign_as_non_member(self):
response = self.client.get(
self.url,
{
"campaign_id": self.private_campaign.id,
"user_id": another_user.id,
},
self.url, {"campaign_id": self.campaign_private.id}
)
self.assertEqual(response.status_code, 404)

def test_get_private_campaign_no_user_id(self):
response = self.client.get(
self.url, {"campaign_id": self.private_campaign.id}
def test_get_all_campaigns_for_user(self):
new_campaign = Campaign.objects.create(
title="Another Campaign", private=False
)
self.assertEqual(response.status_code, 404)

def test_get_all_public_campaigns(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTrue(
any(c["title"] == "Public Campaign" for c in response.json())
CampaignMembership.objects.create(
user=self.owner, campaign=new_campaign
)

def test_get_campaigns_with_valid_user_id(self):
response = self.client.get(self.url, {"user_id": self.player.id})
response = self.client.get(self.url, {"user_id": self.owner.id})
self.assertEqual(response.status_code, 200)
titles = [c["title"] for c in response.json()]
self.assertIn("Public Campaign", titles)
self.assertIn("Private Campaign", titles)

def test_get_campaigns_with_invalid_user_id_type(self):
response = self.client.get(self.url, {"user_id": "notanint"})
self.assertEqual(response.status_code, 400)
self.assertTrue(isinstance(response.json(), list))
self.assertGreaterEqual(len(response.json()), 2)


class TestAddToCampaign(APITestCase):
client: APIClient

def setUp(self):
self.url = reverse("api-1.0.0:add_to_campaign_api")
self.owner = Player.objects.create(telegram_id=1)
self.user = Player.objects.create(telegram_id=2)
self.campaign = Campaign.objects.create(
title="Test Campaign", private=False
)
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, # Owner
user=self.owner, campaign=self.campaign, status=2
)

def test_add_user_success(self):
data = {
"campaign_id": self.campaign.id,
"owner_id": self.owner.id,
"user_id": self.user.id,
"user_id": self.new_user.id,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 201)
self.assertTrue(
CampaignMembership.objects.filter(
user=self.user, campaign=self.campaign
campaign=self.campaign, user=self.new_user
).exists()
)

def test_add_user_already_exists(self):
CampaignMembership.objects.create(
user=self.user, campaign=self.campaign, status=1
user=self.new_user, campaign=self.campaign
)
data = {
"campaign_id": self.campaign.id,
"owner_id": self.owner.id,
"user_id": self.user.id,
"user_id": self.new_user.id,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 200)
membership = CampaignMembership.objects.get(
user=self.user, campaign=self.campaign
)
self.assertEqual(membership.status, 0) # reset to player

def test_missing_parameters(self):
response = self.client.post(self.url, {}, format="json")
self.assertEqual(response.status_code, 400)

def test_campaign_not_found(self):
data = {
"campaign_id": 999,
"owner_id": self.owner.id,
"user_id": self.user.id,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 404)

def test_owner_permission_denied(self):
other_player = Player.objects.create(telegram_id=3)
def test_add_user_forbidden(self):
fake_owner = Player.objects.create(telegram_id=999)
data = {
"campaign_id": self.campaign.id,
"owner_id": other_player.id,
"user_id": self.user.id,
"owner_id": fake_owner.id,
"user_id": self.new_user.id,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 403)

def test_user_not_found(self):
data = {
"campaign_id": self.campaign.id,
"owner_id": self.owner.id,
"user_id": 9999,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 404)


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=1)
self.user = Player.objects.create(telegram_id=2)
self.campaign = Campaign.objects.create(
title="Permission Campaign", private=False
)
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
)
CampaignMembership.objects.create(
user=self.user, campaign=self.campaign, status=0
user=self.member, campaign=self.campaign, status=0
)

def test_edit_permissions_success(self):
def test_edit_permission_success(self):
data = {
"campaign_id": self.campaign.id,
"owner_id": self.owner.id,
"user_id": self.user.id,
"user_id": self.member.id,
"status": 1,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 200)
membership = CampaignMembership.objects.get(
user=self.user, campaign=self.campaign
)
self.assertEqual(membership.status, 1)

def test_missing_parameters(self):
response = self.client.post(self.url, {}, format="json")
self.assertEqual(response.status_code, 400)

def test_invalid_status_value(self):
def test_edit_permission_invalid_status(self):
data = {
"campaign_id": self.campaign.id,
"owner_id": self.owner.id,
"user_id": self.user.id,
"status": 5, # invalid
"user_id": self.member.id,
"status": 5,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 400)

def test_campaign_not_found(self):
data = {
"campaign_id": 9999,
"owner_id": self.owner.id,
"user_id": self.user.id,
"status": 1,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 404)

def test_owner_permission_denied(self):
other_player = Player.objects.create(telegram_id=3)
def test_edit_permission_forbidden(self):
not_owner = Player.objects.create(telegram_id=446)
data = {
"campaign_id": self.campaign.id,
"owner_id": other_player.id,
"user_id": self.user.id,
"owner_id": not_owner.id,
"user_id": self.member.id,
"status": 1,
}
response = self.client.post(self.url, data, format="json")
self.assertEqual(response.status_code, 403)

def test_membership_not_found(self):
stranger = Player.objects.create(telegram_id=4)
def test_edit_permission_user_not_found(self):
data = {
"campaign_id": self.campaign.id,
"owner_id": self.owner.id,
"user_id": stranger.id,
"user_id": 9999,
"status": 1,
}
response = self.client.post(self.url, data, format="json")
Expand Down
3 changes: 1 addition & 2 deletions my_app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@
if debug:
DEBUG = debug.lower() == "true"

ALLOWED_HOSTS = []

ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "*").split(",")

# Application definition

Expand Down
19 changes: 10 additions & 9 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
django
django-ninja
djangorestframework # used in testing
requests
beautifulsoup4
gunicorn
pillow
coverage
ruff
Django==5.2.6
django-ninja==1.4.3
djangorestframework==3.16.1
requests==2.32.5
beautifulsoup4==4.13.5
gunicorn==23.0.0
pillow==11.3.0
ruff==0.14.0
coverage==7.10.7
pydantic==2.11.10