fixed implementation of UGC content uploader
This commit is contained in:
+732
-2
@@ -1,3 +1,733 @@
|
||||
from django.test import TestCase
|
||||
import io
|
||||
import secrets
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
# Create your tests here.
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from profiles.models import ApiToken, UserProfile
|
||||
|
||||
from .models import (
|
||||
Project,
|
||||
ProjectAsset,
|
||||
ProjectContributor,
|
||||
ProjectDraft,
|
||||
Tag,
|
||||
TagCategory,
|
||||
TagList,
|
||||
TempUpload,
|
||||
Version,
|
||||
)
|
||||
|
||||
|
||||
class UGCMediaTestCase(TestCase):
|
||||
"""Base class that redirects uploaded media to a temp directory."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._media_root = tempfile.mkdtemp(prefix='packs_ugc_test_media_')
|
||||
cls.override = override_settings(MEDIA_ROOT=cls._media_root)
|
||||
cls.override.enable()
|
||||
super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
cls.override.disable()
|
||||
shutil.rmtree(cls._media_root, ignore_errors=True)
|
||||
|
||||
|
||||
class UGCGatedTestCase(TestCase):
|
||||
"""Base class with a helper to authorize the client past the gate."""
|
||||
|
||||
def gate(self):
|
||||
session = self.client.session
|
||||
session['authorized'] = True
|
||||
session.save()
|
||||
|
||||
|
||||
def make_png_bytes(size=(8, 8), color='red'):
|
||||
import PIL.Image
|
||||
|
||||
buf = io.BytesIO()
|
||||
PIL.Image.new('RGB', size, color).save(buf, format='PNG')
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def make_zip_bytes():
|
||||
import zipfile
|
||||
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w') as zf:
|
||||
zf.writestr('pack.mcmeta', '{"pack": {"pack_format": 15}}')
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
class UGCProjectTests(UGCMediaTestCase, UGCGatedTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.alice = User.objects.create_user(username='Alice', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.alice)
|
||||
self.bob = User.objects.create_user(username='Bob', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.bob)
|
||||
|
||||
def _upload_temp(self, kind, name, content, content_type):
|
||||
return self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': kind, 'file': SimpleUploadedFile(name, content, content_type=content_type)},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
|
||||
def _create_project(self, user, title='Test Pack', tags='general:furry content:threesome'):
|
||||
self.gate()
|
||||
self.client.login(username=user.username, password='pw')
|
||||
self._upload_temp('thumbnail', 'thumb.png', make_png_bytes(), 'image/png')
|
||||
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip')
|
||||
data = {
|
||||
'title': title,
|
||||
'summary': 'A test pack.',
|
||||
'category': 'mod',
|
||||
'description': '# Hello\n\nWorld.',
|
||||
'tags': tags,
|
||||
'version_name': '1.0.0',
|
||||
'changelog': 'First release.',
|
||||
}
|
||||
return self.client.post(reverse('library:project_create'), data)
|
||||
|
||||
def test_create_requires_login(self):
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:project_create'))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
def test_create_project(self):
|
||||
self.gate()
|
||||
resp = self._create_project(self.alice)
|
||||
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
||||
project = Project.objects.get(slug='test-pack')
|
||||
self.assertEqual(project.owner, self.alice)
|
||||
self.assertEqual(project.category, 'mod')
|
||||
# First version + thumbnail stored and adopted (moved under project_<pk>/).
|
||||
self.assertEqual(project.versions.count(), 1)
|
||||
self.assertIsNotNone(project.thumbnail)
|
||||
self.assertTrue(project.thumbnail.stored_path.startswith('project_'))
|
||||
self.assertTrue(project.versions.first().file.stored_path.startswith('project_'))
|
||||
# Temp uploads marked used.
|
||||
self.assertEqual(
|
||||
TempUpload.objects.filter(user=self.alice, status='used').count(), 2
|
||||
)
|
||||
# Tags auto-created and linked.
|
||||
names = set(TagList.objects.filter(project=project).values_list('tag__name', flat=True))
|
||||
self.assertIn('furry', names)
|
||||
self.assertIn('threesome', names)
|
||||
self.assertIn('alice', names) # creator tag auto-synced
|
||||
# Creator tag exists with category creator.
|
||||
creator_cat = TagCategory.objects.get(slug='creator')
|
||||
self.assertTrue(Tag.objects.filter(name='alice', category=creator_cat).exists())
|
||||
|
||||
def test_create_requires_version_file(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(reverse('library:project_create'), {
|
||||
'title': 'No File',
|
||||
'category': 'mod',
|
||||
'version_name': '1.0.0',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(Project.objects.filter(slug='no-file').exists())
|
||||
|
||||
def test_create_adopts_media_and_caption(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self._upload_temp('thumbnail', 'thumb.png', make_png_bytes(), 'image/png')
|
||||
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip')
|
||||
self._upload_temp('media', 'pic1.png', make_png_bytes(), 'image/png')
|
||||
self._upload_temp('media', 'pic2.png', make_png_bytes(), 'image/png')
|
||||
resp = self.client.post(reverse('library:project_create'), {
|
||||
'title': 'Media Pack',
|
||||
'category': 'skin',
|
||||
'version_name': '1.0.0',
|
||||
'caption': 'Showcase',
|
||||
})
|
||||
self.assertRedirects(resp, reverse('library:project_detail', args=['media-pack']))
|
||||
project = Project.objects.get(slug='media-pack')
|
||||
self.assertEqual(project.assets.count(), 2)
|
||||
self.assertEqual(project.assets.first().caption, 'Showcase')
|
||||
for asset in project.assets.all():
|
||||
self.assertTrue(asset.file.stored_path.startswith('project_'))
|
||||
|
||||
def test_detail_requires_gate(self):
|
||||
self._create_project(self.alice)
|
||||
self.client.logout()
|
||||
resp = self.client.get(reverse('library:project_detail', args=['test-pack']))
|
||||
self.assertEqual(resp.status_code, 302) # gate redirects HTML pages
|
||||
|
||||
def test_detail_gated_ok(self):
|
||||
self._create_project(self.alice)
|
||||
self.client.logout()
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:project_detail', args=['test-pack']))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'Test Pack')
|
||||
self.assertContains(resp, '<h1>Hello</h1>', html=True)
|
||||
|
||||
def test_detail_unknown_404(self):
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:project_detail', args=['nope']))
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
|
||||
class UGCVersionTests(UGCMediaTestCase, UGCGatedTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.alice = User.objects.create_user(username='Alice', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.alice)
|
||||
self.bob = User.objects.create_user(username='Bob', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.bob)
|
||||
self.project = Project.objects.create(
|
||||
slug='test-pack', title='Test Pack', category='mod', owner=self.alice,
|
||||
)
|
||||
self.project.sync_creator_tags()
|
||||
|
||||
def test_upload_version_owner(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:version_upload', args=['test-pack']),
|
||||
{
|
||||
'version_name': '2.0.0',
|
||||
'changelog': 'More stuff.',
|
||||
'file': SimpleUploadedFile('pack2.zip', make_zip_bytes(), content_type='application/zip'),
|
||||
},
|
||||
)
|
||||
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
||||
self.assertEqual(self.project.versions.count(), 1)
|
||||
self.assertEqual(self.project.versions.first().version_name, '2.0.0')
|
||||
# Newest is latest (ordering).
|
||||
self.assertTrue(self.project.versions.first().file_id)
|
||||
|
||||
def test_upload_version_non_contributor_denied(self):
|
||||
self.gate()
|
||||
self.client.login(username='Bob', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:version_upload', args=['test-pack']),
|
||||
{
|
||||
'version_name': '2.0.0',
|
||||
'file': SimpleUploadedFile('pack.zip', make_zip_bytes(), content_type='application/zip'),
|
||||
},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.assertEqual(self.project.versions.count(), 0)
|
||||
|
||||
def test_contributor_can_upload(self):
|
||||
ProjectContributor.objects.create(project=self.project, user=self.bob, added_by=self.alice)
|
||||
self.project.sync_creator_tags()
|
||||
self.gate()
|
||||
self.client.login(username='Bob', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:version_upload', args=['test-pack']),
|
||||
{
|
||||
'version_name': '2.0.0',
|
||||
'file': SimpleUploadedFile('pack.zip', make_zip_bytes(), content_type='application/zip'),
|
||||
},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(self.project.versions.count(), 1)
|
||||
|
||||
def test_download_bumps_counter_and_serves(self):
|
||||
from library.storage import store_file
|
||||
|
||||
index = store_file(
|
||||
self.alice, 'version',
|
||||
SimpleUploadedFile('pack.zip', make_zip_bytes(), content_type='application/zip'),
|
||||
self.project.pk, subdir='versions',
|
||||
)
|
||||
version = Version.objects.create(project=self.project, version_name='1.0.0', file=index)
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:version_download', args=['test-pack', version.pk]))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
version.refresh_from_db()
|
||||
self.assertEqual(version.downloads, 1)
|
||||
# Follow redirect → file served.
|
||||
resp = self.client.get(resp.url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(b''.join(resp.streaming_content), make_zip_bytes())
|
||||
|
||||
|
||||
class UGCGalleryTests(UGCMediaTestCase, UGCGatedTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.alice = User.objects.create_user(username='Alice', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.alice)
|
||||
self.bob = User.objects.create_user(username='Bob', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.bob)
|
||||
self.project = Project.objects.create(
|
||||
slug='test-pack', title='Test Pack', category='skin', owner=self.alice,
|
||||
)
|
||||
|
||||
def test_upload_image_asset(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'media', 'file': SimpleUploadedFile('pic.png', make_png_bytes(), content_type='image/png')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
resp = self.client.post(reverse('library:asset_upload', args=['test-pack']), {'caption': 'Preview'})
|
||||
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
||||
asset = ProjectAsset.objects.get()
|
||||
self.assertEqual(asset.caption, 'Preview')
|
||||
self.assertEqual(asset.uploaded_by, self.alice)
|
||||
self.assertFalse(asset.is_video)
|
||||
|
||||
def test_reject_non_media_file(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'media', 'file': SimpleUploadedFile('evil.zip', make_zip_bytes(), content_type='application/zip')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(ProjectAsset.objects.count(), 0)
|
||||
|
||||
def test_gallery_upload_requires_pending_media(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(reverse('library:asset_upload', args=['test-pack']), {'caption': 'none'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(ProjectAsset.objects.count(), 0)
|
||||
|
||||
def test_asset_delete_removes_index(self):
|
||||
from library.storage import store_file
|
||||
|
||||
index = store_file(
|
||||
self.alice, 'asset',
|
||||
SimpleUploadedFile('pic.png', make_png_bytes(), content_type='image/png'),
|
||||
self.project.pk, subdir='gallery',
|
||||
)
|
||||
asset = ProjectAsset.objects.create(project=self.project, file=index, uploaded_by=self.alice)
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(reverse('library:asset_delete', args=['test-pack', asset.pk]))
|
||||
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
||||
self.assertEqual(ProjectAsset.objects.count(), 0)
|
||||
self.assertFalse(Project.objects.filter(pk=self.project.pk).exists() is False) # project intact
|
||||
self.assertTrue(Project.objects.filter(pk=self.project.pk).exists())
|
||||
|
||||
def test_asset_delete_non_contributor_denied(self):
|
||||
from library.storage import store_file
|
||||
|
||||
index = store_file(
|
||||
self.alice, 'asset',
|
||||
SimpleUploadedFile('pic.png', make_png_bytes(), content_type='image/png'),
|
||||
self.project.pk, subdir='gallery',
|
||||
)
|
||||
asset = ProjectAsset.objects.create(project=self.project, file=index, uploaded_by=self.alice)
|
||||
self.gate()
|
||||
self.client.login(username='Bob', password='pw')
|
||||
resp = self.client.post(reverse('library:asset_delete', args=['test-pack', asset.pk]))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.assertEqual(ProjectAsset.objects.count(), 1)
|
||||
|
||||
|
||||
class UGCDraftTests(UGCMediaTestCase, UGCGatedTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.alice = User.objects.create_user(username='Alice', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.alice)
|
||||
|
||||
def _post_json(self, url, payload):
|
||||
import json
|
||||
|
||||
return self.client.post(url, json.dumps(payload), content_type='application/json')
|
||||
|
||||
def test_upload_api_requires_auth(self):
|
||||
resp = self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'media', 'file': SimpleUploadedFile('a.png', make_png_bytes(), content_type='image/png')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_temp_upload_media_accepts_video(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'media', 'file': SimpleUploadedFile('clip.webm', b'webm', content_type='video/webm')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(resp.json()['uuid'])
|
||||
upload = TempUpload.objects.get()
|
||||
self.assertEqual(upload.kind, 'media')
|
||||
self.assertEqual(upload.status, 'pending')
|
||||
self.assertTrue(upload.file.stored_path.startswith('uploads/user_'))
|
||||
|
||||
def test_temp_upload_thumbnail_rejects_video(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'thumbnail', 'file': SimpleUploadedFile('clip.webm', b'webm', content_type='video/webm')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(TempUpload.objects.count(), 0)
|
||||
|
||||
def test_thumbnail_and_version_are_single_slot(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'thumbnail', 'file': SimpleUploadedFile('a.png', make_png_bytes(), content_type='image/png')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'thumbnail', 'file': SimpleUploadedFile('b.png', make_png_bytes(color='blue'), content_type='image/png')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'version', 'file': SimpleUploadedFile('v1.zip', make_zip_bytes(), content_type='application/zip')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'version', 'file': SimpleUploadedFile('v2.zip', make_zip_bytes(), content_type='application/zip')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
# Only the latest thumbnail + version remain; media unaffected.
|
||||
self.assertEqual(
|
||||
TempUpload.objects.filter(kind='thumbnail', status='pending').count(), 1
|
||||
)
|
||||
self.assertEqual(
|
||||
TempUpload.objects.filter(kind='version', status='pending').count(), 1
|
||||
)
|
||||
|
||||
def test_temp_upload_delete(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'media', 'file': SimpleUploadedFile('a.png', make_png_bytes(), content_type='image/png')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
upload = TempUpload.objects.get()
|
||||
resp = self.client.post(
|
||||
reverse('library:api_delete_temp', args=[upload.uuid]), HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(TempUpload.objects.count(), 0)
|
||||
|
||||
def test_delete_others_upload_forbidden(self):
|
||||
User = get_user_model()
|
||||
bob = User.objects.create_user(username='Bob', password='pw')
|
||||
UserProfile.objects.get_or_create(user=bob)
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'media', 'file': SimpleUploadedFile('a.png', make_png_bytes(), content_type='image/png')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
upload = TempUpload.objects.get()
|
||||
self.client.logout()
|
||||
self.gate()
|
||||
self.client.login(username='Bob', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:api_delete_temp', args=[upload.uuid]), HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
self.assertEqual(TempUpload.objects.count(), 1)
|
||||
|
||||
def test_draft_save_and_restore(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self._post_json(reverse('library:api_draft'), {'data': {
|
||||
'title': 'Draft Title',
|
||||
'summary': 'Draft summary',
|
||||
'category': 'mod',
|
||||
'description': 'Hello draft',
|
||||
'tags': 'species:dragon',
|
||||
'version_name': '2.0.0',
|
||||
}})
|
||||
draft = ProjectDraft.objects.get(user=self.alice)
|
||||
self.assertEqual(draft.data['title'], 'Draft Title')
|
||||
|
||||
resp = self.client.get(reverse('library:api_draft'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.json()['data']['title'], 'Draft Title')
|
||||
|
||||
def test_create_form_prefills_draft(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self._post_json(reverse('library:api_draft'), {'data': {'title': 'My Draft Pack'}})
|
||||
resp = self.client.get(reverse('library:project_create'))
|
||||
self.assertContains(resp, 'value="My Draft Pack"')
|
||||
|
||||
def test_create_clears_draft(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self._post_json(reverse('library:api_draft'), {'data': {'title': 'My Draft Pack'}})
|
||||
self.client.post(
|
||||
reverse('library:api_upload_temp'),
|
||||
{'kind': 'version', 'file': SimpleUploadedFile('pack.zip', make_zip_bytes(), content_type='application/zip')},
|
||||
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
||||
)
|
||||
resp = self.client.post(reverse('library:project_create'), {
|
||||
'title': 'My Draft Pack', 'category': 'mod', 'version_name': '1.0.0',
|
||||
})
|
||||
self.assertRedirects(resp, reverse('library:project_detail', args=['my-draft-pack']))
|
||||
self.assertFalse(ProjectDraft.objects.filter(user=self.alice).exists())
|
||||
|
||||
|
||||
class UGCPermissionTests(UGCMediaTestCase, UGCGatedTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.alice = User.objects.create_user(username='Alice', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.alice)
|
||||
self.bob = User.objects.create_user(username='Bob', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.bob)
|
||||
self.carol = User.objects.create_user(username='Carol', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.carol)
|
||||
self.project = Project.objects.create(
|
||||
slug='test-pack', title='Test Pack', category='mod', owner=self.alice,
|
||||
)
|
||||
|
||||
def test_edit_owner_ok(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:project_edit', args=['test-pack']),
|
||||
{
|
||||
'title': 'Renamed Pack',
|
||||
'summary': 'New summary',
|
||||
'description': 'Updated.',
|
||||
'category': 'mod',
|
||||
},
|
||||
)
|
||||
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
||||
self.project.refresh_from_db()
|
||||
self.assertEqual(self.project.title, 'Renamed Pack')
|
||||
|
||||
def test_edit_non_contributor_denied(self):
|
||||
self.gate()
|
||||
self.client.login(username='Bob', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:project_edit', args=['test-pack']),
|
||||
{'title': 'Hijacked', 'category': 'mod'},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.project.refresh_from_db()
|
||||
self.assertEqual(self.project.title, 'Test Pack')
|
||||
|
||||
def test_edit_contributor_ok(self):
|
||||
ProjectContributor.objects.create(project=self.project, user=self.bob, added_by=self.alice)
|
||||
self.gate()
|
||||
self.client.login(username='Bob', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:project_edit', args=['test-pack']),
|
||||
{
|
||||
'title': 'Test Pack',
|
||||
'summary': 'Edits by contributor.',
|
||||
'description': 'Changed.',
|
||||
'category': 'mod',
|
||||
},
|
||||
)
|
||||
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
||||
self.project.refresh_from_db()
|
||||
self.assertEqual(self.project.summary, 'Edits by contributor.')
|
||||
|
||||
def test_delete_owner_only(self):
|
||||
self.gate()
|
||||
self.client.login(username='Bob', password='pw')
|
||||
resp = self.client.post(reverse('library:project_delete', args=['test-pack']))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.assertTrue(Project.objects.filter(pk=self.project.pk).exists())
|
||||
|
||||
self.client.logout()
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(reverse('library:project_delete', args=['test-pack']))
|
||||
self.assertRedirects(resp, reverse('library:browse'))
|
||||
self.assertFalse(Project.objects.filter(pk=self.project.pk).exists())
|
||||
|
||||
def test_contributor_management_owner_only(self):
|
||||
self.gate()
|
||||
self.client.login(username='Bob', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:contributors', args=['test-pack']),
|
||||
{'action': 'add', 'username': 'Carol'},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
self.assertEqual(ProjectContributor.objects.count(), 0)
|
||||
|
||||
self.client.logout()
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.post(
|
||||
reverse('library:contributors', args=['test-pack']),
|
||||
{'action': 'add', 'username': 'Carol'},
|
||||
)
|
||||
self.assertRedirects(resp, reverse('library:contributors', args=['test-pack']))
|
||||
self.assertTrue(
|
||||
ProjectContributor.objects.filter(project=self.project, user=self.carol).exists()
|
||||
)
|
||||
|
||||
def test_contributor_creator_tag_sync_and_detach(self):
|
||||
ProjectContributor.objects.create(project=self.project, user=self.bob, added_by=self.alice)
|
||||
self.project.sync_creator_tags()
|
||||
creator_cat = TagCategory.objects.get(slug='creator')
|
||||
self.assertTrue(Tag.objects.filter(name='alice', category=creator_cat).exists())
|
||||
self.assertTrue(Tag.objects.filter(name='bob', category=creator_cat).exists())
|
||||
self.assertEqual(
|
||||
TagList.objects.filter(project=self.project, tag__category=creator_cat).count(), 2
|
||||
)
|
||||
# Remove Bob as a contributor → his creator tag detaches.
|
||||
ProjectContributor.objects.filter(project=self.project, user=self.bob).delete()
|
||||
self.project.detach_creator_tag(self.bob)
|
||||
self.project.sync_creator_tags(actor=self.alice)
|
||||
self.assertEqual(
|
||||
TagList.objects.filter(project=self.project, tag__category=creator_cat).count(), 1
|
||||
)
|
||||
self.assertFalse(
|
||||
TagList.objects.filter(project=self.project, tag__category=creator_cat, tag__name='bob').exists()
|
||||
)
|
||||
|
||||
def test_edit_syncs_creator_tags(self):
|
||||
ProjectContributor.objects.create(project=self.project, user=self.bob, added_by=self.alice)
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
self.client.post(
|
||||
reverse('library:project_edit', args=['test-pack']),
|
||||
{
|
||||
'title': 'Test Pack',
|
||||
'description': 'x',
|
||||
'category': 'mod',
|
||||
'tags': 'species:dragon',
|
||||
},
|
||||
)
|
||||
creator_cat = TagCategory.objects.get(slug='creator')
|
||||
names = set(TagList.objects.filter(project=self.project, tag__category=creator_cat).values_list('tag__name', flat=True))
|
||||
self.assertIn('alice', names)
|
||||
self.assertIn('bob', names)
|
||||
# Manual species tag applied.
|
||||
self.assertTrue(TagList.objects.filter(project=self.project, tag__name='dragon').exists())
|
||||
|
||||
|
||||
class UGCBrowseTests(UGCGatedTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.alice = User.objects.create_user(username='Alice', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.alice)
|
||||
self.bob = User.objects.create_user(username='Bob', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.bob)
|
||||
|
||||
def _seed(self):
|
||||
p1 = Project.objects.create(slug='furry-mod', title='Furry Mod', category='mod', owner=self.alice, summary='Dragon fun')
|
||||
p2 = Project.objects.create(slug='skin-pack', title='Skin Pack', category='skin', owner=self.bob)
|
||||
p1.sync_creator_tags()
|
||||
# Tag p1: species:dragon, content:threesome. Tag p2: species:dragon only.
|
||||
for cat_slug, name in [('species', 'dragon'), ('content', 'threesome')]:
|
||||
cat = TagCategory.objects.get(slug=cat_slug)
|
||||
tag = Tag.objects.get_or_create(name=name, category=cat)[0]
|
||||
TagList.objects.create(project=p1, tag=tag, added_by=self.alice)
|
||||
cat = TagCategory.objects.get(slug='species')
|
||||
tag = Tag.objects.get_or_create(name='dragon', category=cat)[0]
|
||||
TagList.objects.create(project=p2, tag=tag, added_by=self.bob)
|
||||
return p1, p2
|
||||
|
||||
def test_browse_gated(self):
|
||||
resp = self.client.get(reverse('library:browse'))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
def test_browse_lists_projects(self):
|
||||
self._seed()
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:browse'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertContains(resp, 'Furry Mod')
|
||||
self.assertContains(resp, 'Skin Pack')
|
||||
|
||||
def test_browse_search(self):
|
||||
self._seed()
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:browse'), {'q': 'Furry'})
|
||||
self.assertContains(resp, 'Furry Mod')
|
||||
self.assertNotContains(resp, 'Skin Pack')
|
||||
|
||||
def test_browse_category_filter(self):
|
||||
self._seed()
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:browse'), {'category': 'skin'})
|
||||
self.assertContains(resp, 'Skin Pack')
|
||||
self.assertNotContains(resp, 'Furry Mod')
|
||||
|
||||
def test_browse_tag_filter_and(self):
|
||||
self._seed()
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:browse'), {'tag': ['dragon', 'threesome']})
|
||||
self.assertContains(resp, 'Furry Mod')
|
||||
self.assertNotContains(resp, 'Skin Pack')
|
||||
|
||||
def test_browse_tag_filter_single(self):
|
||||
self._seed()
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:browse'), {'tag': ['dragon']})
|
||||
self.assertContains(resp, 'Furry Mod')
|
||||
self.assertContains(resp, 'Skin Pack')
|
||||
|
||||
def test_browse_sort_recent(self):
|
||||
self._seed()
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('library:browse'))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# Skin Pack created last → first card.
|
||||
html = resp.content.decode()
|
||||
self.assertLess(html.find('Skin Pack'), html.find('Furry Mod'))
|
||||
|
||||
def test_home_shows_real_projects(self):
|
||||
self._seed()
|
||||
self.gate()
|
||||
resp = self.client.get(reverse('landing:home'))
|
||||
self.assertContains(resp, 'Furry Mod')
|
||||
self.assertContains(resp, 'Skin Pack')
|
||||
|
||||
|
||||
class UGCApiTests(UGCMediaTestCase, UGCGatedTestCase):
|
||||
def setUp(self):
|
||||
User = get_user_model()
|
||||
self.alice = User.objects.create_user(username='Alice', password='pw')
|
||||
UserProfile.objects.get_or_create(user=self.alice)
|
||||
cat = TagCategory.objects.get(slug='species')
|
||||
Tag.objects.create(name='dragon', category=cat, created_by=self.alice)
|
||||
Tag.objects.create(name='dragoness', category=cat, created_by=self.alice)
|
||||
self.token = secrets.token_urlsafe(48)
|
||||
ApiToken.objects.create(user=self.alice, token=self.token, key_prefix=self.token[:8], label='test')
|
||||
|
||||
def test_tags_autocomplete_gated(self):
|
||||
resp = self.client.get(reverse('library:api_tags_autocomplete'), {'q': 'dra'})
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def test_tags_autocomplete_with_token(self):
|
||||
resp = self.client.get(
|
||||
reverse('library:api_tags_autocomplete'), {'q': 'dra'},
|
||||
HTTP_AUTHORIZATION=f'Bearer Alice {self.token}',
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertJSONEqual(resp.content, [
|
||||
{'name': 'dragon', 'category': 'species', 'category_name': 'Species', 'color': '#06a77d'},
|
||||
{'name': 'dragoness', 'category': 'species', 'category_name': 'Species', 'color': '#06a77d'},
|
||||
])
|
||||
|
||||
def test_users_autocomplete(self):
|
||||
self.gate()
|
||||
self.client.login(username='Alice', password='pw')
|
||||
resp = self.client.get(reverse('library:api_users_autocomplete'), {'q': 'Ali'})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertJSONEqual(resp.content, [{'username': 'Alice'}])
|
||||
|
||||
Reference in New Issue
Block a user