- Add token-based admin login/logout/me endpoints - Bootstrap admin from ADMIN_USERNAME/ADMIN_PASSWORD in Docker entrypoint - Read ALLOWED_HOSTS and CORS from env only (no hardcoded server IPs) - Keep docs/Postman/tests free of real credentials - Cover login and auth flows with 31 local API tests Co-authored-by: Cursor <cursoragent@cursor.com>
389 lines
15 KiB
Python
389 lines
15 KiB
Python
import io
|
|
from datetime import timedelta
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
from django.utils import timezone
|
|
from PIL import Image
|
|
from rest_framework import status
|
|
from rest_framework.test import APITestCase
|
|
|
|
from .models import Campaign, Composition, CompositionImage, ContactUs
|
|
|
|
|
|
def make_test_image(name='test.jpg', color='red', size=(100, 100)):
|
|
"""Create a minimal valid JPEG for upload tests."""
|
|
buffer = io.BytesIO()
|
|
Image.new('RGB', size, color=color).save(buffer, format='JPEG')
|
|
buffer.seek(0)
|
|
return SimpleUploadedFile(name, buffer.read(), content_type='image/jpeg')
|
|
|
|
|
|
class ContactUsAPITests(APITestCase):
|
|
def setUp(self):
|
|
self.contact = ContactUs.objects.create(
|
|
name='Ali Reza',
|
|
email_or_phone='ali@example.com',
|
|
description='Need support',
|
|
category='پشتیبانی',
|
|
)
|
|
|
|
def test_list_contacts(self):
|
|
response = self.client.get('/api/contact-us/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(response.data['count'], 1)
|
|
|
|
def test_create_contact(self):
|
|
payload = {
|
|
'name': 'Sara',
|
|
'email_or_phone': '09121234567',
|
|
'description': 'Sales inquiry',
|
|
'category': 'فروش',
|
|
}
|
|
response = self.client.post('/api/contact-us/', payload, format='json')
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
self.assertEqual(ContactUs.objects.count(), 2)
|
|
|
|
def test_retrieve_contact(self):
|
|
response = self.client.get(f'/api/contact-us/{self.contact.id}/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(response.data['name'], 'Ali Reza')
|
|
|
|
def test_by_category(self):
|
|
response = self.client.get('/api/contact-us/by_category/?category=پشتیبانی')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(len(response.data), 1)
|
|
|
|
def test_by_category_missing_param(self):
|
|
response = self.client.get('/api/contact-us/by_category/')
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
|
|
def test_update_requires_auth(self):
|
|
response = self.client.patch(
|
|
f'/api/contact-us/{self.contact.id}/',
|
|
{'name': 'Updated'},
|
|
format='json',
|
|
)
|
|
self.assertIn(response.status_code, (
|
|
status.HTTP_401_UNAUTHORIZED,
|
|
status.HTTP_403_FORBIDDEN,
|
|
))
|
|
|
|
|
|
class CompositionAPITests(APITestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.admin = User.objects.create_user(
|
|
username='testadmin', password='testpass123', is_staff=True
|
|
)
|
|
self.composition = Composition.objects.create(
|
|
name='Test Composition',
|
|
description='Test description',
|
|
)
|
|
|
|
def test_list_compositions(self):
|
|
response = self.client.get('/api/compositions/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(response.data['count'], 1)
|
|
|
|
def test_create_composition_json(self):
|
|
payload = {'name': 'New Comp', 'description': 'Desc'}
|
|
response = self.client.post('/api/compositions/', payload, format='json')
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
self.assertEqual(response.data['name'], 'New Comp')
|
|
self.assertEqual(response.data['images'], [])
|
|
|
|
def test_retrieve_composition(self):
|
|
response = self.client.get(f'/api/compositions/{self.composition.id}/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertIn('images', response.data)
|
|
self.assertIn('main_image', response.data)
|
|
self.assertIn('created_at', response.data)
|
|
|
|
def test_by_created_at(self):
|
|
now = timezone.now()
|
|
Composition.objects.create(
|
|
name='Old Composition',
|
|
description='Old',
|
|
)
|
|
Composition.objects.filter(name='Old Composition').update(
|
|
created_at=now - timedelta(days=10)
|
|
)
|
|
from_dt = (now - timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
response = self.client.get(
|
|
f'/api/compositions/by-created-at/?from={from_dt}'
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
names = [item['name'] for item in response.data]
|
|
self.assertIn('Test Composition', names)
|
|
self.assertNotIn('Old Composition', names)
|
|
|
|
def test_by_created_at_missing_params(self):
|
|
response = self.client.get('/api/compositions/by-created-at/')
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
|
|
def test_by_created_at_invalid_datetime(self):
|
|
response = self.client.get(
|
|
'/api/compositions/by-created-at/?from=not-a-date'
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
|
|
def test_create_with_multiple_images(self):
|
|
img1 = make_test_image('img1.jpg', 'red')
|
|
img2 = make_test_image('img2.jpg', 'blue')
|
|
response = self.client.post(
|
|
'/api/compositions/',
|
|
{
|
|
'name': 'Multi Image',
|
|
'description': 'With images',
|
|
'uploaded_images': [img1, img2],
|
|
'main_image_index': '1',
|
|
},
|
|
format='multipart',
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
self.assertEqual(len(response.data['images']), 2)
|
|
self.assertTrue(response.data['main_image']['is_main'])
|
|
|
|
composition = Composition.objects.get(name='Multi Image')
|
|
self.assertEqual(composition.images.count(), 2)
|
|
self.assertEqual(composition.main_image.is_main, True)
|
|
|
|
def test_add_images(self):
|
|
self.client.force_authenticate(user=self.admin)
|
|
img = make_test_image('added.jpg', 'green')
|
|
response = self.client.post(
|
|
f'/api/compositions/{self.composition.id}/add-images/',
|
|
{'uploaded_images': [img]},
|
|
format='multipart',
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(len(response.data['images']), 1)
|
|
self.assertTrue(response.data['main_image']['is_main'])
|
|
|
|
def test_set_main_image(self):
|
|
self.client.force_authenticate(user=self.admin)
|
|
img1 = CompositionImage.objects.create(
|
|
composition=self.composition,
|
|
image=make_test_image('a.jpg'),
|
|
is_main=True,
|
|
)
|
|
img2 = CompositionImage.objects.create(
|
|
composition=self.composition,
|
|
image=make_test_image('b.jpg'),
|
|
is_main=False,
|
|
)
|
|
response = self.client.post(
|
|
f'/api/compositions/{self.composition.id}/set-main-image/',
|
|
{'image_id': img2.id},
|
|
format='json',
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
img1.refresh_from_db()
|
|
img2.refresh_from_db()
|
|
self.assertFalse(img1.is_main)
|
|
self.assertTrue(img2.is_main)
|
|
|
|
def test_delete_image(self):
|
|
self.client.force_authenticate(user=self.admin)
|
|
img = CompositionImage.objects.create(
|
|
composition=self.composition,
|
|
image=make_test_image('del.jpg'),
|
|
is_main=True,
|
|
)
|
|
response = self.client.delete(
|
|
f'/api/compositions/{self.composition.id}/images/{img.id}/'
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
self.assertFalse(CompositionImage.objects.filter(pk=img.id).exists())
|
|
|
|
def test_update_requires_auth(self):
|
|
response = self.client.patch(
|
|
f'/api/compositions/{self.composition.id}/',
|
|
{'name': 'Updated'},
|
|
format='json',
|
|
)
|
|
self.assertIn(response.status_code, (
|
|
status.HTTP_401_UNAUTHORIZED,
|
|
status.HTTP_403_FORBIDDEN,
|
|
))
|
|
|
|
|
|
class CampaignAPITests(APITestCase):
|
|
def setUp(self):
|
|
now = timezone.now()
|
|
self.active = Campaign.objects.create(
|
|
name='Active Campaign',
|
|
description='Running now',
|
|
start_time=now - timedelta(days=1),
|
|
end_time=now + timedelta(days=1),
|
|
)
|
|
self.upcoming = Campaign.objects.create(
|
|
name='Upcoming Campaign',
|
|
description='Starts soon',
|
|
start_time=now + timedelta(days=2),
|
|
end_time=now + timedelta(days=5),
|
|
)
|
|
self.ended = Campaign.objects.create(
|
|
name='Ended Campaign',
|
|
description='Already finished',
|
|
start_time=now - timedelta(days=10),
|
|
end_time=now - timedelta(days=5),
|
|
)
|
|
|
|
def test_list_campaigns(self):
|
|
response = self.client.get('/api/campaigns/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(response.data['count'], 3)
|
|
|
|
def test_create_campaign(self):
|
|
now = timezone.now()
|
|
payload = {
|
|
'name': 'New Campaign',
|
|
'description': 'Desc',
|
|
'start_time': (now + timedelta(days=1)).isoformat(),
|
|
'end_time': (now + timedelta(days=3)).isoformat(),
|
|
}
|
|
response = self.client.post('/api/campaigns/', payload, format='json')
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
|
|
def test_retrieve_campaign(self):
|
|
response = self.client.get(f'/api/campaigns/{self.active.id}/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertTrue(response.data['is_active'])
|
|
self.assertFalse(response.data['is_upcoming'])
|
|
self.assertFalse(response.data['is_ended'])
|
|
|
|
def test_active_campaigns(self):
|
|
response = self.client.get('/api/campaigns/active/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
names = [item['name'] for item in response.data]
|
|
self.assertIn('Active Campaign', names)
|
|
self.assertNotIn('Upcoming Campaign', names)
|
|
|
|
def test_upcoming_campaigns(self):
|
|
response = self.client.get('/api/campaigns/upcoming/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
names = [item['name'] for item in response.data]
|
|
self.assertIn('Upcoming Campaign', names)
|
|
|
|
def test_ended_campaigns(self):
|
|
response = self.client.get('/api/campaigns/ended/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
names = [item['name'] for item in response.data]
|
|
self.assertIn('Ended Campaign', names)
|
|
|
|
def test_create_campaign_invalid_dates(self):
|
|
now = timezone.now()
|
|
payload = {
|
|
'name': 'Bad Dates',
|
|
'description': 'Desc',
|
|
'start_time': (now + timedelta(days=3)).isoformat(),
|
|
'end_time': (now + timedelta(days=1)).isoformat(),
|
|
}
|
|
response = self.client.post('/api/campaigns/', payload, format='json')
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class AdminLoginAPITests(APITestCase):
|
|
def setUp(self):
|
|
User = get_user_model()
|
|
self.admin_username = 'admin_test'
|
|
self.admin_password = 'test_admin_pass_123'
|
|
self.admin = User.objects.create_user(
|
|
username=self.admin_username,
|
|
password=self.admin_password,
|
|
is_staff=True,
|
|
is_superuser=True,
|
|
)
|
|
self.regular = User.objects.create_user(
|
|
username='normaluser',
|
|
password='normalpass123',
|
|
is_staff=False,
|
|
)
|
|
|
|
def test_admin_login_success(self):
|
|
response = self.client.post(
|
|
'/api/admin/login/',
|
|
{'username': self.admin_username, 'password': self.admin_password},
|
|
format='json',
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertIn('token', response.data)
|
|
self.assertEqual(response.data['user']['username'], self.admin_username)
|
|
self.assertTrue(response.data['user']['is_staff'])
|
|
|
|
def test_admin_login_wrong_password(self):
|
|
response = self.client.post(
|
|
'/api/admin/login/',
|
|
{'username': self.admin_username, 'password': 'wrong'},
|
|
format='json',
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
|
|
def test_non_staff_cannot_login(self):
|
|
response = self.client.post(
|
|
'/api/admin/login/',
|
|
{'username': 'normaluser', 'password': 'normalpass123'},
|
|
format='json',
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
|
|
|
def test_admin_me_with_token(self):
|
|
login = self.client.post(
|
|
'/api/admin/login/',
|
|
{'username': self.admin_username, 'password': self.admin_password},
|
|
format='json',
|
|
)
|
|
token = login.data['token']
|
|
self.client.credentials(HTTP_AUTHORIZATION=f'Token {token}')
|
|
response = self.client.get('/api/admin/me/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(response.data['username'], self.admin_username)
|
|
|
|
def test_admin_me_requires_auth(self):
|
|
response = self.client.get('/api/admin/me/')
|
|
self.assertIn(response.status_code, (
|
|
status.HTTP_401_UNAUTHORIZED,
|
|
status.HTTP_403_FORBIDDEN,
|
|
))
|
|
|
|
def test_token_allows_protected_update(self):
|
|
contact = ContactUs.objects.create(
|
|
name='Old Name',
|
|
email_or_phone='a@b.com',
|
|
description='msg',
|
|
category='سایر',
|
|
)
|
|
login = self.client.post(
|
|
'/api/admin/login/',
|
|
{'username': self.admin_username, 'password': self.admin_password},
|
|
format='json',
|
|
)
|
|
self.client.credentials(HTTP_AUTHORIZATION=f'Token {login.data["token"]}')
|
|
response = self.client.patch(
|
|
f'/api/contact-us/{contact.id}/',
|
|
{'name': 'New Name'},
|
|
format='json',
|
|
)
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
self.assertEqual(response.data['name'], 'New Name')
|
|
|
|
def test_admin_logout(self):
|
|
login = self.client.post(
|
|
'/api/admin/login/',
|
|
{'username': self.admin_username, 'password': self.admin_password},
|
|
format='json',
|
|
)
|
|
token = login.data['token']
|
|
self.client.credentials(HTTP_AUTHORIZATION=f'Token {token}')
|
|
response = self.client.post('/api/admin/logout/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
# Token should no longer work
|
|
response = self.client.get('/api/admin/me/')
|
|
self.assertIn(response.status_code, (
|
|
status.HTTP_401_UNAUTHORIZED,
|
|
status.HTTP_403_FORBIDDEN,
|
|
))
|