2005 lines
90 KiB
Python
2005 lines
90 KiB
Python
import io
|
|
import json
|
|
import os
|
|
import secrets
|
|
import shutil
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
|
|
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,
|
|
VersionFile,
|
|
)
|
|
|
|
|
|
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, draft_uuid=None):
|
|
data = {'kind': kind, 'file': SimpleUploadedFile(name, content, content_type=content_type)}
|
|
if draft_uuid is not None:
|
|
data['draft_uuid'] = str(draft_uuid)
|
|
return self.client.post(
|
|
reverse('library:api_upload_temp'), data,
|
|
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')
|
|
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
|
|
self._upload_temp('thumbnail', 'thumb.png', make_png_bytes(), 'image/png', draft_uuid=uuid)
|
|
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid)
|
|
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', args=[uuid]), data)
|
|
|
|
def test_create_requires_login(self):
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:project_create_new'))
|
|
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().files.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_links_auto_tags(self):
|
|
"""Auto tags (from version metadata) must be linked to the project on
|
|
creation, not just created as orphan Tag rows."""
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
|
|
self._upload_temp(
|
|
'version', 'testmod.jar',
|
|
_zip_bytes({'META-INF/mods.toml': 'modLoader="javafml"\nloaderVersion="[1,)"\n[[mods]]\nmodId="testmod"\nversion="1.0.0"\ndisplayName="Test Mod"\n'}),
|
|
'application/java-archive',
|
|
draft_uuid=uuid,
|
|
)
|
|
self.client.post(reverse('library:project_create', args=[uuid]), {
|
|
'title': 'Auto Tag Mod', 'category': 'mod', 'version_name': '1.0.0',
|
|
})
|
|
project = Project.objects.get(slug='auto-tag-mod')
|
|
loader_links = TagList.objects.filter(
|
|
project=project, tag__category__slug='loader',
|
|
)
|
|
self.assertTrue(loader_links.exists(), 'auto loader tag should be linked to the project')
|
|
|
|
def test_create_requires_version_file(self):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
|
|
resp = self.client.post(reverse('library:project_create', args=[uuid]), {
|
|
'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_captions(self):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
|
|
self._upload_temp('thumbnail', 'thumb.png', make_png_bytes(), 'image/png', draft_uuid=uuid)
|
|
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid)
|
|
self._upload_temp('media', 'pic1.png', make_png_bytes(), 'image/png', draft_uuid=uuid)
|
|
self._upload_temp('media', 'pic2.png', make_png_bytes(), 'image/png', draft_uuid=uuid)
|
|
media = TempUpload.objects.filter(kind='media', draft_uuid=uuid).order_by('created_at')
|
|
caption_payload = {
|
|
f'caption_{media[0].uuid}': 'Showcase',
|
|
f'caption_{media[1].uuid}': 'Second angle',
|
|
}
|
|
resp = self.client.post(reverse('library:project_create', args=[uuid]), {
|
|
'title': 'Media Pack',
|
|
'category': 'skin',
|
|
'version_name': '1.0.0',
|
|
**caption_payload,
|
|
})
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['media-pack']))
|
|
project = Project.objects.get(slug='media-pack')
|
|
self.assertEqual(project.assets.count(), 2)
|
|
# Per-file captions stored individually.
|
|
captions = dict(project.assets.values_list('file__original_filename', 'caption'))
|
|
self.assertEqual(captions['pic1.png'], 'Showcase')
|
|
self.assertEqual(captions['pic2.png'], 'Second angle')
|
|
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 UGCProjectDraftTests(UGCMediaTestCase, UGCGatedTestCase):
|
|
"""Multiple per-user drafts, keyed by UUID, with upload scoping."""
|
|
|
|
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 _auth(self, user):
|
|
self.client.force_login(user)
|
|
self.gate()
|
|
|
|
def _upload_temp(self, kind, name, content, content_type, draft_uuid=None):
|
|
data = {'kind': kind, 'file': SimpleUploadedFile(name, content, content_type=content_type)}
|
|
if draft_uuid is not None:
|
|
data['draft_uuid'] = str(draft_uuid)
|
|
return self.client.post(
|
|
reverse('library:api_upload_temp'), data,
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
|
|
def _new_draft(self, user=None):
|
|
self._auth(user or self.alice)
|
|
resp = self.client.get(reverse('library:project_create_new'))
|
|
uuid = resp.url.rstrip('/').split('/')[-1]
|
|
return uuid, resp
|
|
|
|
def test_new_project_creates_draft_and_redirects(self):
|
|
uuid, resp = self._new_draft()
|
|
self.assertRedirects(
|
|
resp, reverse('library:project_create', args=[uuid]),
|
|
fetch_redirect_response=False,
|
|
)
|
|
self.assertTrue(ProjectDraft.objects.filter(uuid=uuid, user=self.alice).exists())
|
|
|
|
def test_my_projects_lists_projects_and_drafts(self):
|
|
project = Project.objects.create(
|
|
slug='mine', title='Mine', category='mod', owner=self.alice,
|
|
)
|
|
uuid, _ = self._new_draft()
|
|
self._auth(self.alice)
|
|
resp = self.client.get(reverse('library:my_projects'))
|
|
self.assertContains(resp, 'Mine')
|
|
self.assertContains(resp, 'Untitled draft')
|
|
self.assertEqual(Project.objects.get(pk=project.pk).title, 'Mine')
|
|
|
|
def test_wrong_user_cannot_access_draft(self):
|
|
uuid, _ = self._new_draft()
|
|
self._auth(self.bob)
|
|
resp = self.client.get(reverse('library:project_create', args=[uuid]))
|
|
self.assertEqual(resp.status_code, 302)
|
|
self.assertEqual(resp.url, reverse('library:my_projects'))
|
|
# API save also refused.
|
|
resp = self.client.post(
|
|
reverse('library:api_draft'),
|
|
data=json.dumps({'draft': uuid, 'data': {'title': 'hack'}}),
|
|
content_type='application/json',
|
|
)
|
|
self.assertEqual(resp.status_code, 404)
|
|
draft = ProjectDraft.objects.get(uuid=uuid)
|
|
self.assertNotEqual(draft.data.get('title'), 'hack')
|
|
|
|
def test_autosave_scopes_to_its_draft(self):
|
|
uuid_a, _ = self._new_draft()
|
|
uuid_b, _ = self._new_draft()
|
|
self._auth(self.alice)
|
|
self.client.post(
|
|
reverse('library:api_draft'),
|
|
data=json.dumps({'draft': uuid_a, 'data': {'title': 'Draft A'}}),
|
|
content_type='application/json',
|
|
)
|
|
self.assertEqual(ProjectDraft.objects.get(uuid=uuid_a).data.get('title'), 'Draft A')
|
|
self.assertEqual(ProjectDraft.objects.get(uuid=uuid_b).data.get('title'), None)
|
|
|
|
def test_uploads_scoped_per_draft(self):
|
|
uuid_a, _ = self._new_draft()
|
|
uuid_b, _ = self._new_draft()
|
|
self._auth(self.alice)
|
|
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid_a)
|
|
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid_a).count(), 1)
|
|
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid_b).count(), 0)
|
|
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid_a, status='pending').count(), 1)
|
|
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid_b, status='pending').count(), 0)
|
|
|
|
def test_submit_consumes_draft_uploads_and_deletes_draft(self):
|
|
uuid, _ = self._new_draft()
|
|
self._auth(self.alice)
|
|
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid)
|
|
resp = self.client.post(
|
|
reverse('library:project_create', args=[uuid]),
|
|
{
|
|
'title': 'From Draft', 'summary': 's', 'description': 'd',
|
|
'category': 'mod', 'tags': 'general:test',
|
|
'version_name': '1.0.0', 'changelog': 'c',
|
|
},
|
|
)
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['from-draft']))
|
|
self.assertTrue(Project.objects.filter(title='From Draft', owner=self.alice).exists())
|
|
self.assertFalse(ProjectDraft.objects.filter(uuid=uuid).exists())
|
|
self.assertEqual(TempUpload.objects.filter(draft_uuid=uuid, status='used').count(), 1)
|
|
|
|
def test_draft_delete_purges_uploads(self):
|
|
uuid, _ = self._new_draft()
|
|
self._auth(self.alice)
|
|
self._upload_temp('version', 'pack.zip', make_zip_bytes(), 'application/zip', draft_uuid=uuid)
|
|
resp = self.client.post(reverse('library:project_draft_delete', args=[uuid]))
|
|
self.assertEqual(resp.status_code, 302)
|
|
self.assertFalse(ProjectDraft.objects.filter(uuid=uuid).exists())
|
|
self.assertFalse(TempUpload.objects.filter(draft_uuid=uuid).exists())
|
|
|
|
def test_skin_individual_png_builds_manifest(self):
|
|
uuid, _ = self._new_draft()
|
|
self._auth(self.alice)
|
|
self._upload_temp('version', 'skin.png', make_png_bytes(size=(64, 64)), 'image/png', draft_uuid=uuid)
|
|
self.client.post(reverse('library:project_create', args=[uuid]), {
|
|
'title': 'My Skin', 'category': 'skin', 'version_name': '1.0.0',
|
|
})
|
|
version = Project.objects.get(slug='my-skin').versions.first()
|
|
manifest = version.skins_manifest
|
|
self.assertIn('skins', manifest)
|
|
self.assertEqual(manifest['skins'][0]['width'], 64)
|
|
self.assertEqual(manifest['skins'][0]['height'], 64)
|
|
self.assertTrue(manifest['skins'][0]['url'].startswith('/api/files/'))
|
|
|
|
def test_skin_rejects_non_skin_sized_png(self):
|
|
uuid, _ = self._new_draft()
|
|
self._auth(self.alice)
|
|
self._upload_temp('version', 'small.png', make_png_bytes(size=(8, 8)), 'image/png', draft_uuid=uuid)
|
|
self.client.post(reverse('library:project_create', args=[uuid]), {
|
|
'title': 'Bad Skin', 'category': 'skin', 'version_name': '1.0.0',
|
|
})
|
|
version = Project.objects.get(slug='bad-skin').versions.first()
|
|
self.assertEqual(version.skins_manifest, {})
|
|
|
|
|
|
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')
|
|
self.client.post(
|
|
reverse('library:api_upload_temp'),
|
|
{'kind': 'version', 'file': SimpleUploadedFile('pack2.zip', make_zip_bytes(), content_type='application/zip')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
resp = self.client.post(
|
|
reverse('library:version_upload', args=['test-pack']),
|
|
{'version_name': '2.0.0', 'changelog': 'More stuff.'},
|
|
)
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
|
self.assertEqual(self.project.versions.count(), 1)
|
|
version = self.project.versions.first()
|
|
self.assertEqual(version.version_name, '2.0.0')
|
|
self.assertEqual(version.files.count(), 1)
|
|
# Adopted file moved under project_<pk>/versions/ and temp marked used.
|
|
self.assertTrue(version.files.first().file.stored_path.startswith('project_'))
|
|
self.assertTrue(
|
|
TempUpload.objects.filter(user=self.alice, kind='version', status='used').exists()
|
|
)
|
|
|
|
def test_upload_version_multiple_files(self):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
for name in ('skin1.png', 'skin2.png'):
|
|
self.client.post(
|
|
reverse('library:api_upload_temp'),
|
|
{'kind': 'version', 'file': SimpleUploadedFile(name, make_png_bytes(), content_type='image/png')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
resp = self.client.post(
|
|
reverse('library:version_upload', args=['test-pack']),
|
|
{'version_name': '3.0.0'},
|
|
)
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
|
version = self.project.versions.first()
|
|
self.assertEqual(version.version_name, '3.0.0')
|
|
# One version, several files, PNGs classified as png kind.
|
|
self.assertEqual(version.files.count(), 2)
|
|
self.assertEqual(
|
|
set(version.files.values_list('kind', flat=True)), {'png'}
|
|
)
|
|
self.assertEqual(
|
|
set(version.files.values_list('file__original_filename', flat=True)),
|
|
{'skin1.png', 'skin2.png'},
|
|
)
|
|
|
|
def test_upload_version_requires_pending_file(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'},
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(self.project.versions.count(), 0)
|
|
|
|
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'},
|
|
)
|
|
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')
|
|
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:version_upload', args=['test-pack']),
|
|
{'version_name': '2.0.0'},
|
|
)
|
|
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')
|
|
VersionFile.objects.create(version=version, file=index, kind='release')
|
|
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())
|
|
|
|
def test_per_file_download(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')
|
|
vf = VersionFile.objects.create(version=version, file=index, kind='release')
|
|
self.gate()
|
|
resp = self.client.get(
|
|
reverse('library:version_file_download', args=['test-pack', version.pk, index.uuid])
|
|
)
|
|
self.assertEqual(resp.status_code, 302)
|
|
version.refresh_from_db()
|
|
self.assertEqual(version.downloads, 1)
|
|
self.assertEqual(resp.url, reverse('library:file_request', args=[index.uuid]) + '?download=1')
|
|
# The parent version_download route still works (first file).
|
|
resp = self.client.get(reverse('library:version_download', args=['test-pack', version.pk]))
|
|
self.assertEqual(resp.status_code, 302)
|
|
self.assertTrue(vf)
|
|
|
|
|
|
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_with_caption(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',
|
|
)
|
|
temp = TempUpload.objects.get(kind='media')
|
|
resp = self.client.post(
|
|
reverse('library:asset_upload', args=['test-pack']),
|
|
{f'caption_{temp.uuid}': '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_asset_captions_are_per_file(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(color='red'), content_type='image/png')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
self.client.post(
|
|
reverse('library:api_upload_temp'),
|
|
{'kind': 'media', 'file': SimpleUploadedFile('b.png', make_png_bytes(color='blue'), content_type='image/png')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
temps = list(TempUpload.objects.filter(kind='media').order_by('created_at'))
|
|
self.client.post(reverse('library:asset_upload', args=['test-pack']), {
|
|
f'caption_{temps[0].uuid}': 'First',
|
|
f'caption_{temps[1].uuid}': 'Second',
|
|
})
|
|
captions = dict(ProjectAsset.objects.values_list('file__original_filename', 'caption'))
|
|
self.assertEqual(captions, {'a.png': 'First', 'b.png': 'Second'})
|
|
|
|
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']))
|
|
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_ajax_returns_json(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]),
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertJSONEqual(resp.content, {'success': True})
|
|
self.assertEqual(ProjectAsset.objects.count(), 0)
|
|
|
|
def test_asset_delete_requires_post(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.get(reverse('library:asset_delete', args=['test-pack', asset.pk]))
|
|
self.assertEqual(resp.status_code, 405)
|
|
self.assertEqual(ProjectAsset.objects.count(), 1)
|
|
|
|
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_single_slot_version_multi(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',
|
|
)
|
|
# Thumbnail is a single slot; version accepts multiple files.
|
|
self.assertEqual(
|
|
TempUpload.objects.filter(kind='thumbnail', status='pending').count(), 1
|
|
)
|
|
self.assertEqual(
|
|
TempUpload.objects.filter(kind='version', status='pending').count(), 2
|
|
)
|
|
|
|
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')
|
|
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
|
|
self._post_json(reverse('library:api_draft'), {'draft': uuid, 'data': {
|
|
'title': 'Draft Title',
|
|
'summary': 'Draft summary',
|
|
'category': 'mod',
|
|
'description': 'Hello draft',
|
|
'tags': 'species:dragon',
|
|
'version_name': '2.0.0',
|
|
}})
|
|
draft = ProjectDraft.objects.get(uuid=uuid, user=self.alice)
|
|
self.assertEqual(draft.data['title'], 'Draft Title')
|
|
|
|
resp = self.client.get(reverse('library:api_draft'), {'draft': uuid})
|
|
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')
|
|
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
|
|
self._post_json(reverse('library:api_draft'), {'draft': uuid, 'data': {'title': 'My Draft Pack'}})
|
|
resp = self.client.get(reverse('library:project_create', args=[uuid]))
|
|
self.assertContains(resp, 'value="My Draft Pack"')
|
|
|
|
def test_create_clears_draft(self):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
|
|
self._post_json(reverse('library:api_draft'), {'draft': uuid, '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'), 'draft_uuid': uuid},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
resp = self.client.post(reverse('library:project_create', args=[uuid]), {
|
|
'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(uuid=uuid).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_replaces_thumbnail_via_temp(self):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
self.client.post(
|
|
reverse('library:api_upload_temp'),
|
|
{'kind': 'thumbnail', 'file': SimpleUploadedFile('thumb.png', make_png_bytes(), content_type='image/png')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
resp = self.client.post(
|
|
reverse('library:project_edit', args=['test-pack']),
|
|
{'title': 'Test Pack', 'category': 'mod', 'description': 'x'},
|
|
)
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
|
self.project.refresh_from_db()
|
|
self.assertIsNotNone(self.project.thumbnail)
|
|
self.assertTrue(self.project.thumbnail.stored_path.startswith('project_'))
|
|
self.assertTrue(
|
|
TempUpload.objects.filter(user=self.alice, kind='thumbnail', status='used').exists()
|
|
)
|
|
|
|
def test_edit_shows_existing_thumbnail_preview(self):
|
|
from library.storage import store_file
|
|
|
|
index = store_file(
|
|
self.alice, 'thumbnail',
|
|
SimpleUploadedFile('thumb.png', make_png_bytes(), content_type='image/png'),
|
|
self.project.pk, subdir='',
|
|
)
|
|
self.project.thumbnail = index
|
|
self.project.save()
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.get(reverse('library:project_edit', args=['test-pack']))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, str(index.uuid))
|
|
|
|
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())
|
|
|
|
def _add_manual_creator_tag(self, name='lizo'):
|
|
creator_cat = TagCategory.objects.get(slug='creator')
|
|
tag, _ = Tag.objects.get_or_create(name=name, category=creator_cat, created_by=self.alice)
|
|
TagList.objects.create(project=self.project, tag=tag, added_by=self.alice)
|
|
return tag
|
|
|
|
def test_edit_shows_manual_creator_tag(self):
|
|
self.project.sync_creator_tags()
|
|
self._add_manual_creator_tag('lizo')
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.get(reverse('library:project_edit', args=['test-pack']))
|
|
# The manual creator tag (no matching account) is serialized into the editor.
|
|
self.assertContains(resp, 'creator:lizo')
|
|
|
|
def test_edit_hides_account_backed_creator_tags(self):
|
|
ProjectContributor.objects.create(project=self.project, user=self.bob, added_by=self.alice)
|
|
self.project.sync_creator_tags()
|
|
self._add_manual_creator_tag('lizo')
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.get(reverse('library:project_edit', args=['test-pack']))
|
|
# Manual tag visible; account-backed creator tags (alice, bob) hidden.
|
|
self.assertContains(resp, 'creator:lizo')
|
|
self.assertNotContains(resp, 'creator:alice')
|
|
self.assertNotContains(resp, 'creator:bob')
|
|
|
|
def test_edit_removing_manual_creator_tag_deletes_it(self):
|
|
self.project.sync_creator_tags()
|
|
self._add_manual_creator_tag('lizo')
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.post(
|
|
reverse('library:project_edit', args=['test-pack']),
|
|
{'title': 'Test Pack', 'category': 'mod', 'description': 'x', 'tags': ''},
|
|
)
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
|
# Manual creator tag removed from the form is now deleted...
|
|
self.assertFalse(
|
|
TagList.objects.filter(project=self.project, tag__name='lizo').exists()
|
|
)
|
|
# ...while the account-backed owner creator tag survives.
|
|
self.assertTrue(
|
|
TagList.objects.filter(project=self.project, tag__name='alice').exists()
|
|
)
|
|
|
|
def test_edit_keeps_manual_creator_tag_when_resubmitted(self):
|
|
self.project.sync_creator_tags()
|
|
self._add_manual_creator_tag('lizo')
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.post(
|
|
reverse('library:project_edit', args=['test-pack']),
|
|
{'title': 'Test Pack', 'category': 'mod', 'description': 'x',
|
|
'tags': 'creator:lizo species:dragon'},
|
|
)
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['test-pack']))
|
|
self.assertTrue(
|
|
TagList.objects.filter(project=self.project, tag__name='lizo').exists()
|
|
)
|
|
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'}])
|
|
|
|
|
|
def _zip_bytes(entries):
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, 'w') as zf:
|
|
for name, content in entries.items():
|
|
zf.writestr(name, content)
|
|
return buf.getvalue()
|
|
|
|
|
|
def _pack_zip():
|
|
return _zip_bytes({
|
|
'pack.mcmeta': json.dumps({
|
|
'pack': {'pack_format': 64, 'description': 'A test pack'},
|
|
'animationframework': {'id': 'jakebreath:test'},
|
|
}),
|
|
})
|
|
|
|
|
|
class UGCZipParsingTests(TestCase):
|
|
def _write_zip(self, data, suffix='.zip'):
|
|
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
|
|
f.write(data)
|
|
return f.name
|
|
|
|
def test_read_pack_mcmeta(self):
|
|
from library.zips import read_pack_mcmeta
|
|
path = self._write_zip(_pack_zip())
|
|
try:
|
|
fmt, desc = read_pack_mcmeta(path)
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertEqual(fmt, 64)
|
|
self.assertEqual(desc, 'A test pack')
|
|
|
|
def test_read_pack_mcmeta_missing(self):
|
|
from library.zips import read_pack_mcmeta
|
|
path = self._write_zip(_zip_bytes({'assets/x.txt': 'x'}))
|
|
try:
|
|
fmt, desc = read_pack_mcmeta(path)
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertIsNone(fmt)
|
|
self.assertEqual(desc, '')
|
|
|
|
def test_parse_mods_folder(self):
|
|
from library.zips import parse_mods_manifest
|
|
path = self._write_zip(_zip_bytes({
|
|
'minecraft/mods/a.jar': 'x', 'minecraft/mods/b.jar': 'y',
|
|
'pack.mcmeta': '{}',
|
|
}))
|
|
try:
|
|
manifest = parse_mods_manifest(path, 'pack.zip')
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertEqual(manifest['source'], 'folder')
|
|
self.assertEqual([m['name'] for m in manifest['files']], ['a.jar', 'b.jar'])
|
|
|
|
def test_parse_mods_mrpack(self):
|
|
from library.zips import parse_mods_manifest
|
|
path = self._write_zip(_zip_bytes({
|
|
'modrinth.index.json': json.dumps({
|
|
'files': [
|
|
{'path': 'mods/mod-a.jar', 'env': {'client': 'required', 'server': 'required'}},
|
|
],
|
|
}),
|
|
}), suffix='.mrpack')
|
|
try:
|
|
manifest = parse_mods_manifest(path, 'pack.mrpack')
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertEqual(manifest['source'], 'modrinth')
|
|
self.assertEqual(manifest['files'][0]['name'], 'mod-a.jar')
|
|
|
|
def test_parse_mods_curseforge(self):
|
|
from library.zips import parse_mods_manifest
|
|
path = self._write_zip(_zip_bytes({
|
|
'manifest.json': json.dumps({
|
|
'files': [{'projectID': 123, 'fileID': 456, 'fileName': 'cool-mod.jar'}],
|
|
}),
|
|
}))
|
|
try:
|
|
manifest = parse_mods_manifest(path, 'pack.zip')
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertEqual(manifest['source'], 'curseforge')
|
|
self.assertEqual(manifest['files'][0]['name'], 'cool-mod.jar')
|
|
|
|
|
|
class UGCCategoryTests(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 _upload_version(self, name, content, content_type='application/zip'):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.post(
|
|
reverse('library:api_upload_temp'),
|
|
{'kind': 'version', 'file': SimpleUploadedFile(name, content, content_type=content_type)},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
|
|
def _upload_version_form(self, version_name='1.0.0'):
|
|
resp = self.client.post(
|
|
reverse('library:version_upload', args=[self.project.slug]),
|
|
{'version_name': version_name},
|
|
)
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=[self.project.slug]))
|
|
|
|
def test_non_pack_captures_pack_mcmeta(self):
|
|
self.project = Project.objects.create(
|
|
slug='non-pack', title='NoN Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('pack.zip', _pack_zip())
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
self.assertEqual(version.pack_format, 64)
|
|
self.assertEqual(version.pack_description, 'A test pack')
|
|
|
|
def test_modpack_captures_mods_manifest(self):
|
|
self.project = Project.objects.create(
|
|
slug='modpack', title='Modpack', category='modpack', owner=self.alice,
|
|
)
|
|
self._upload_version('pack.zip', _zip_bytes({'minecraft/mods/a.jar': 'x'}))
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
self.assertEqual(version.mods_manifest['source'], 'folder')
|
|
self.assertEqual(version.mods_manifest['files'][0]['name'], 'a.jar')
|
|
|
|
def test_guide_uploads_md_and_zip_and_renders(self):
|
|
self.project = Project.objects.create(
|
|
slug='guide', title='Guide', category='guide', owner=self.alice,
|
|
)
|
|
self._upload_version('README.md', b'# Title\n\nBody text.', content_type='text/markdown')
|
|
self._upload_version('assets.zip', _zip_bytes({'data/x.json': '{}'}))
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
self.assertEqual(
|
|
set(version.files.values_list('kind', flat=True)), {'markdown', 'release'}
|
|
)
|
|
md_vf = version.files.get(kind='markdown')
|
|
# guide_doc endpoint renders the markdown server-side.
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:guide_doc', args=['guide', version.pk, md_vf.file.uuid]))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, '<h1>Title</h1>', html=True)
|
|
# Detail page exposes the guide switcher.
|
|
resp = self.client.get(reverse('library:project_detail', args=['guide']))
|
|
self.assertContains(resp, 'guide-switcher')
|
|
self.assertContains(resp, 'README.md')
|
|
|
|
def test_guide_doc_rejects_non_markdown(self):
|
|
self.project = Project.objects.create(
|
|
slug='guide', title='Guide', category='guide', owner=self.alice,
|
|
)
|
|
self._upload_version('pack.zip', _zip_bytes({'data/x.json': '{}'}))
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
vf = version.files.get(kind='release')
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:guide_doc', args=['guide', version.pk, vf.file.uuid]))
|
|
self.assertEqual(resp.status_code, 404)
|
|
|
|
def test_guide_rewrites_links_to_separately_uploaded_files(self):
|
|
self.project = Project.objects.create(
|
|
slug='guide', title='Guide', category='guide', owner=self.alice,
|
|
)
|
|
self._upload_version(
|
|
'README.md', b'# Title\n\n\n', content_type='text/markdown',
|
|
)
|
|
self._upload_version('images/logo.png', b'PNGDATA', content_type='image/png')
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
md_vf = version.files.get(kind='markdown')
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:guide_doc', args=['guide', version.pk, md_vf.file.uuid]))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, '<img src="/api/files/', count=1)
|
|
self.assertNotContains(resp, 'images/logo.png')
|
|
|
|
def test_guide_zip_docs_listed_and_links_rewritten(self):
|
|
self.project = Project.objects.create(
|
|
slug='guide', title='Guide', category='guide', owner=self.alice,
|
|
)
|
|
self._upload_version('guide.zip', _zip_bytes({
|
|
'README.md': '# Read\n\n\n\nNext: [part two](part2.md)\n',
|
|
'images/a.png': b'PNG2',
|
|
'part2.md': '# Two\n',
|
|
}))
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
self.gate()
|
|
# Both .md members appear in the guide switcher.
|
|
resp = self.client.get(reverse('library:project_detail', args=['guide']))
|
|
self.assertContains(resp, 'README.md')
|
|
self.assertContains(resp, 'part2.md')
|
|
# Zip doc renders with image + cross-doc link rewritten.
|
|
resp = self.client.get(reverse('library:guide_zip_doc', args=['guide', version.pk, 'README.md']))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, '<img src="/packs/guide/versions/%d/asset/images/a.png"' % version.pk, count=1)
|
|
self.assertContains(resp, 'href="/packs/guide/guide/zip/%d/part2.md"' % version.pk, count=1)
|
|
|
|
def test_guide_zip_doc_rejects_non_guide_or_missing(self):
|
|
self.project = Project.objects.create(
|
|
slug='guide', title='Guide', category='guide', owner=self.alice,
|
|
)
|
|
self._upload_version('guide.zip', _zip_bytes({'README.md': '# Read\n'}))
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
self.gate()
|
|
self.assertEqual(
|
|
self.client.get(reverse('library:guide_zip_doc', args=['guide', version.pk, 'missing.md'])).status_code,
|
|
404,
|
|
)
|
|
self.assertEqual(
|
|
self.client.get(reverse('library:guide_zip_doc', args=['guide', version.pk, 'data.json'])).status_code,
|
|
404,
|
|
)
|
|
|
|
def test_guide_links_not_rewritten_outside_guide_category(self):
|
|
self.project = Project.objects.create(
|
|
slug='pack', title='Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version(
|
|
'notes.md', b'# Notes\n\n\n', content_type='text/markdown',
|
|
)
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
md_vf = version.files.get(kind='markdown')
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:guide_doc', args=['pack', version.pk, md_vf.file.uuid]))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertContains(resp, 'src="images/x.png"')
|
|
|
|
def test_regular_category_has_no_extra_metadata(self):
|
|
self.project = Project.objects.create(
|
|
slug='mod', title='Mod', category='mod', owner=self.alice,
|
|
)
|
|
self._upload_version('pack.zip', _pack_zip())
|
|
self._upload_version_form()
|
|
version = self.project.versions.first()
|
|
self.assertIsNone(version.pack_format)
|
|
self.assertEqual(version.mods_manifest, {})
|
|
|
|
|
|
def _animation_pack_zip(with_id=True, entity_x_player=False, problem=True):
|
|
actor_a = {
|
|
'label': 'actor1', 'entity_types': ['minecraft:player'],
|
|
'actor_tags': ['gender.male'], 'activity': 'active', 'injector': 'V',
|
|
}
|
|
actor_b = {
|
|
'label': 'actor2',
|
|
'entity_types': ['minecraft:zombie'] if entity_x_player else ['minecraft:player'],
|
|
'actor_tags': [] if entity_x_player else ['gender.female'],
|
|
'activity': 'passive', 'receiver': True,
|
|
}
|
|
content_tags = ['missionary'] + (['bugged'] if problem else [])
|
|
af = {
|
|
'id': 'jakebreath:testpack', 'name': 'Test Pack', 'author': 'Alice, Bob',
|
|
'version': '1.0.0', 'description': 'A test pack.',
|
|
} if with_id else {'name': 'Test Pack', 'author': 'Alice'}
|
|
return _zip_bytes({
|
|
'pack.mcmeta': json.dumps({'pack': {'pack_format': 64}, 'animationframework': af}),
|
|
'data/jakebreath/afw_animdefs/ground.json': json.dumps({
|
|
'display_name': 'Ground', 'content_tags': content_tags,
|
|
'actors': [actor_a, actor_b],
|
|
'stages': [{'stage': 1, 'loop': True, 'cycle_seconds': 1.0},
|
|
{'stage': 2, 'loop': False, 'non_peak': True}],
|
|
}),
|
|
})
|
|
|
|
|
|
def _bom_pack_zip():
|
|
mcmeta = b'\xef\xbb\xbf' + json.dumps({
|
|
'pack': {'pack_format': 64},
|
|
'animationframework': {'id': 'needsofnature:default', 'name': 'Default', 'author': 'NoN Team'},
|
|
}).encode()
|
|
return _zip_bytes({
|
|
'pack.mcmeta': mcmeta,
|
|
'data/needsofnature/afw_animdefs/solo.json': json.dumps({
|
|
'actors': [{'label': 'actor1', 'entity_types': ['minecraft:player'], 'activity': 'passive'}],
|
|
'content_tags': ['solo'],
|
|
'stages': [{'stage': 1, 'loop': True}],
|
|
}),
|
|
})
|
|
|
|
|
|
class UGCAnimationTests(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 _write_zip(self, data):
|
|
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as f:
|
|
f.write(data)
|
|
return f.name
|
|
|
|
def _upload_version(self, slug, name, content):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.post(
|
|
reverse('library:api_upload_temp'),
|
|
{'kind': 'version', 'file': SimpleUploadedFile(name, content, content_type='application/zip')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
return self.client.post(
|
|
reverse('library:version_upload', args=[slug]), {'version_name': '1.0.0'},
|
|
)
|
|
|
|
def test_read_animation_manifest_player_x_player(self):
|
|
from library.zips import read_animation_manifest
|
|
path = self._write_zip(_animation_pack_zip())
|
|
try:
|
|
manifest = read_animation_manifest(path)
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertEqual(manifest['animation_id'], 'jakebreath:testpack')
|
|
self.assertEqual(manifest['authors'], ['Alice', 'Bob'])
|
|
anim = manifest['animations'][0]
|
|
self.assertEqual(anim['name'], 'Ground')
|
|
self.assertEqual(anim['type'], 'Pair')
|
|
self.assertEqual(anim['content_tags'], ['missionary', 'bugged'])
|
|
self.assertEqual(anim['problem_tags'], ['bugged'])
|
|
self.assertEqual(anim['actors'][0]['injector'], 'V')
|
|
self.assertEqual(anim['actors'][0]['injector_name'], 'Vaginal')
|
|
self.assertEqual(anim['actors'][1]['gender'], 'female')
|
|
self.assertEqual(anim['stages'][1]['climax'], True)
|
|
|
|
def test_read_animation_manifest_entity_x_player(self):
|
|
from library.zips import read_animation_manifest
|
|
path = self._write_zip(_animation_pack_zip(entity_x_player=True))
|
|
try:
|
|
manifest = read_animation_manifest(path)
|
|
finally:
|
|
os.unlink(path)
|
|
anim = manifest['animations'][0]
|
|
self.assertEqual(anim['type'], 'Entity x Player')
|
|
self.assertEqual(anim['actors'][1]['entity'], 'Zombie')
|
|
self.assertEqual(anim['entity_names'], ['Zombie'])
|
|
|
|
def test_read_animation_manifest_bom_mcmeta(self):
|
|
from library.zips import read_animation_manifest, read_pack_mcmeta
|
|
path = self._write_zip(_bom_pack_zip())
|
|
try:
|
|
fmt, desc = read_pack_mcmeta(path)
|
|
manifest = read_animation_manifest(path)
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertEqual(fmt, 64)
|
|
self.assertEqual(manifest['animation_id'], 'needsofnature:default')
|
|
self.assertEqual(manifest['animations'][0]['type'], 'Solo')
|
|
|
|
def test_read_animation_manifest_not_animation(self):
|
|
from library.zips import read_animation_manifest
|
|
path = self._write_zip(_zip_bytes({'data/x.txt': 'x'}))
|
|
try:
|
|
manifest = read_animation_manifest(path)
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertIsNone(manifest)
|
|
|
|
def test_version_upload_captures_manifest_and_tags(self):
|
|
self.project = Project.objects.create(
|
|
slug='anim-pack', title='Anim Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
resp = self._upload_version('anim-pack', 'pack.zip', _animation_pack_zip())
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['anim-pack']))
|
|
version = self.project.versions.first()
|
|
self.assertEqual(version.animation_id, 'jakebreath:testpack')
|
|
self.assertEqual(version.animation_manifest['animations'][0]['name'], 'Ground')
|
|
# Auto-created content tags (including the problem tag).
|
|
names = set(TagList.objects.filter(project=self.project).values_list('tag__name', flat=True))
|
|
self.assertIn('missionary', names)
|
|
self.assertIn('bugged', names)
|
|
|
|
def test_animation_pack_without_id_gets_injected(self):
|
|
self.project = Project.objects.create(
|
|
slug='anim-pack', title='Anim Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
resp = self._upload_version('anim-pack', 'pack.zip', _animation_pack_zip(with_id=False))
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['anim-pack']),
|
|
fetch_redirect_response=False)
|
|
version = self.project.versions.first()
|
|
# The missing id is auto-assigned instead of warning.
|
|
self.assertEqual(version.animation_id, f'alice:project_{self.project.pk}')
|
|
self.assertTrue(version.animation_manifest)
|
|
|
|
def test_latest_api_returns_version_and_url(self):
|
|
self.project = Project.objects.create(
|
|
slug='anim-pack', title='Anim Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('anim-pack', 'pack.zip', _animation_pack_zip())
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.get(reverse('library:api_packs_latest', args=['jakebreath', 'testpack']))
|
|
self.assertEqual(resp.status_code, 200)
|
|
data = resp.json()
|
|
self.assertEqual(data['version'], '1.0.0')
|
|
self.assertEqual(data['pack_format'], 64)
|
|
self.assertIn('/api/files/', data['download_url'])
|
|
self.assertIn('?download=1', data['download_url'])
|
|
|
|
def test_latest_api_404_unknown_pack(self):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.get(reverse('library:api_packs_latest', args=['nope', 'missing']))
|
|
self.assertEqual(resp.status_code, 404)
|
|
|
|
def test_latest_api_404_without_animation_id(self):
|
|
self.project = Project.objects.create(
|
|
slug='anim-pack', title='Anim Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('anim-pack', 'pack.zip', _animation_pack_zip(with_id=False))
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.get(reverse('library:api_packs_latest', args=['jakebreath', 'testpack']))
|
|
self.assertEqual(resp.status_code, 404)
|
|
|
|
|
|
def _geo_model_zip():
|
|
geo = {
|
|
'format_version': '1.12.0',
|
|
'afw_bone_textures': {'body': 'needsofnature:textures/entity/zombie/zombie.png'},
|
|
'minecraft:geometry': [{
|
|
'description': {'identifier': 'geometry.unknown', 'texture_width': 64, 'texture_height': 64},
|
|
'bones': [
|
|
{'name': 'root', 'pivot': [0, 0, 0]},
|
|
{'name': 'body', 'pivot': [0, 12, 0], 'cubes': [
|
|
{'origin': [-4, 0, -4], 'size': [8, 8, 8], 'uv': [0, 0]},
|
|
]},
|
|
],
|
|
}],
|
|
}
|
|
return _zip_bytes({
|
|
'pack.mcmeta': json.dumps({
|
|
'pack': {'pack_format': 88},
|
|
'animationframework': {'id': 'christmaspuffin:models_and_textures', 'name': 'Models',
|
|
'author': 'CP', 'version': '1.0'},
|
|
}),
|
|
'assets/animationframework/geckolib/models/entity/zombie.m.geo.json': json.dumps(geo),
|
|
'assets/needsofnature/textures/entity/zombie/zombie.png': b'PNG-DATA',
|
|
})
|
|
|
|
|
|
def _logical_zip():
|
|
return _zip_bytes({
|
|
'pack.mcmeta': json.dumps({'pack': {'pack_format': 61}}),
|
|
'assets/usefulcum/items/potion_gender.json': json.dumps(
|
|
{'model': {'type': 'minecraft:model', 'model': 'usefulcum:item/potion_gender'}}),
|
|
'assets/usefulcum/textures/item/potion_gender.png': b'PNG',
|
|
'data/usefulcum/recipe/cum_combination.json': json.dumps({
|
|
'type': 'minecraft:crafting_shapeless',
|
|
'ingredients': ['minecraft:potion', 'minecraft:potion'],
|
|
'result': {'id': 'minecraft:glass_bottle', 'count': 2},
|
|
}),
|
|
'data/usefulcum/advancement/recipe/blaze_cum.json': json.dumps(
|
|
{'rewards': {'function': 'usefulcum:recipe/blaze_cum'}}),
|
|
'data/usefulcum/advancement/change_gender.json': json.dumps(
|
|
{'rewards': {'function': 'usefulcum:change_gender'}}),
|
|
'data/usefulcum/function/change_gender.mcfunction': b'# x',
|
|
'data/usefulcum/function/tick.mcfunction': b'# y',
|
|
})
|
|
|
|
|
|
class UGCContentTabsTests(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 _write_zip(self, data):
|
|
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as f:
|
|
f.write(data)
|
|
return f.name
|
|
|
|
def _upload_version(self, slug, name, content):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.post(
|
|
reverse('library:api_upload_temp'),
|
|
{'kind': 'version', 'file': SimpleUploadedFile(name, content, content_type='application/zip')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
return self.client.post(
|
|
reverse('library:version_upload', args=[slug]), {'version_name': '1.0.0'},
|
|
)
|
|
|
|
def test_read_pack_meta_without_animdefs(self):
|
|
from library.zips import read_pack_meta
|
|
path = self._write_zip(_geo_model_zip())
|
|
try:
|
|
meta = read_pack_meta(path)
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertEqual(meta['animation_id'], 'christmaspuffin:models_and_textures')
|
|
self.assertEqual(meta['authors'], ['CP'])
|
|
|
|
def test_read_models_manifest(self):
|
|
from library.zips import read_models_manifest
|
|
path = self._write_zip(_geo_model_zip())
|
|
try:
|
|
manifest = read_models_manifest(path)
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertIsNotNone(manifest)
|
|
model = manifest['models'][0]
|
|
self.assertEqual(model['name'], 'zombie.m.geo.json')
|
|
self.assertEqual(model['bones'], 2)
|
|
self.assertEqual(model['cubes'], 1)
|
|
self.assertEqual(
|
|
model['bone_textures']['body'],
|
|
'assets/needsofnature/textures/entity/zombie/zombie.png',
|
|
)
|
|
|
|
def test_read_logical_manifest(self):
|
|
from library.zips import read_logical_manifest
|
|
path = self._write_zip(_logical_zip())
|
|
try:
|
|
manifest = read_logical_manifest(path)
|
|
finally:
|
|
os.unlink(path)
|
|
self.assertIsNotNone(manifest)
|
|
self.assertEqual(manifest['items'][0]['name'], 'usefulcum:potion_gender')
|
|
self.assertEqual(manifest['recipes'][0]['ingredients'], ['minecraft:potion'])
|
|
self.assertEqual(manifest['recipes'][0]['result'], 'minecraft:glass_bottle x2')
|
|
self.assertEqual(manifest['custom_recipes'][0]['name'], 'blaze_cum')
|
|
self.assertEqual(manifest['advancements'][0]['name'], 'change_gender')
|
|
self.assertEqual(len(manifest['functions']), 2)
|
|
|
|
def test_models_pack_gets_id_and_models_tab(self):
|
|
self.project = Project.objects.create(
|
|
slug='models-pack', title='Models Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('models-pack', 'pack.zip', _geo_model_zip())
|
|
version = self.project.versions.first()
|
|
self.assertEqual(version.animation_id, 'christmaspuffin:models_and_textures')
|
|
self.assertTrue(version.models_manifest)
|
|
self.assertFalse(version.animation_manifest)
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:project_detail', args=['models-pack']))
|
|
self.assertContains(resp, 'tab-models')
|
|
self.assertContains(resp, 'Render')
|
|
self.assertNotContains(resp, 'tab-animations')
|
|
|
|
def test_logical_pack_shows_only_logical_tab(self):
|
|
self.project = Project.objects.create(
|
|
slug='logical-pack', title='Logical Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('logical-pack', 'pack.zip', _logical_zip())
|
|
version = self.project.versions.first()
|
|
self.assertTrue(version.logical_manifest)
|
|
self.assertFalse(version.models_manifest)
|
|
self.assertFalse(version.animation_manifest)
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:project_detail', args=['logical-pack']))
|
|
self.assertContains(resp, 'tab-logical')
|
|
self.assertNotContains(resp, 'tab-models')
|
|
self.assertNotContains(resp, 'tab-animations')
|
|
self.assertContains(resp, 'usefulcum:potion_gender')
|
|
|
|
def test_pack_asset_serves_member(self):
|
|
self.project = Project.objects.create(
|
|
slug='models-pack', title='Models Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('models-pack', 'pack.zip', _geo_model_zip())
|
|
version = self.project.versions.first()
|
|
self.gate()
|
|
resp = self.client.get(reverse(
|
|
'library:pack_asset',
|
|
args=['models-pack', version.pk, 'assets/needsofnature/textures/entity/zombie/zombie.png'],
|
|
))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp['Content-Type'].split(';')[0], 'image/png')
|
|
self.assertEqual(resp.content, b'PNG-DATA')
|
|
|
|
def test_pack_asset_rejects_unknown_member(self):
|
|
self.project = Project.objects.create(
|
|
slug='models-pack', title='Models Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('models-pack', 'pack.zip', _geo_model_zip())
|
|
version = self.project.versions.first()
|
|
self.gate()
|
|
resp = self.client.get(reverse(
|
|
'library:pack_asset',
|
|
args=['models-pack', version.pk, 'assets/nope/nothing.png'],
|
|
))
|
|
self.assertEqual(resp.status_code, 404)
|
|
|
|
def test_gallery_video_is_muted(self):
|
|
from library.storage import store_file
|
|
self.project = Project.objects.create(
|
|
slug='muted', title='Muted', category='mod', owner=self.alice,
|
|
)
|
|
index = store_file(
|
|
self.alice, 'asset',
|
|
SimpleUploadedFile('clip.webm', b'webm', content_type='video/webm'),
|
|
self.project.pk, subdir='gallery',
|
|
)
|
|
ProjectAsset.objects.create(project=self.project, file=index, uploaded_by=self.alice)
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:project_detail', args=['muted']))
|
|
self.assertContains(resp, 'muted')
|
|
|
|
|
|
def make_gif_bytes():
|
|
import PIL.Image
|
|
|
|
frames = [PIL.Image.new('RGB', (8, 8), c) for c in ('red', 'green')]
|
|
buf = io.BytesIO()
|
|
frames[0].save(buf, format='GIF', save_all=True, append_images=frames[1:], duration=100, loop=0)
|
|
return buf.getvalue()
|
|
|
|
|
|
class UGCThumbnailTests(UGCMediaTestCase, UGCGatedTestCase):
|
|
@unittest.skipUnless(shutil.which('ffmpeg'), 'ffmpeg required')
|
|
def test_video_and_gif_thumbnails(self):
|
|
User = get_user_model()
|
|
alice = User.objects.create_user(username='Alice', password='pw')
|
|
UserProfile.objects.get_or_create(user=alice)
|
|
project = Project.objects.create(slug='media', title='Media', category='mod', owner=alice)
|
|
from library.storage import store_file
|
|
|
|
gif = ProjectAsset.objects.create(
|
|
project=project, uploaded_by=alice,
|
|
file=store_file(
|
|
alice, 'asset',
|
|
SimpleUploadedFile('anim.gif', make_gif_bytes(), content_type='image/gif'),
|
|
project.pk, subdir='gallery',
|
|
),
|
|
)
|
|
png = ProjectAsset.objects.create(
|
|
project=project, uploaded_by=alice,
|
|
file=store_file(
|
|
alice, 'asset',
|
|
SimpleUploadedFile('pic.png', make_png_bytes(), content_type='image/png'),
|
|
project.pk, subdir='gallery',
|
|
),
|
|
)
|
|
self.gate()
|
|
# Gif gets a cached JPEG thumbnail.
|
|
resp = self.client.get(reverse('library:asset_thumbnail', args=['media', gif.pk]))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp['Content-Type'].split(';')[0], 'image/jpeg')
|
|
self.assertGreater(len(b''.join(resp.streaming_content)), 0)
|
|
# Cached on the second request.
|
|
resp2 = self.client.get(reverse('library:asset_thumbnail', args=['media', gif.pk]))
|
|
self.assertEqual(resp2.status_code, 200)
|
|
# A plain image is not thumbnailed → 404.
|
|
resp3 = self.client.get(reverse('library:asset_thumbnail', args=['media', png.pk]))
|
|
self.assertEqual(resp3.status_code, 404)
|
|
|
|
def test_gallery_grid_uses_thumbnail_for_gif(self):
|
|
User = get_user_model()
|
|
alice = User.objects.create_user(username='Alice', password='pw')
|
|
UserProfile.objects.get_or_create(user=alice)
|
|
project = Project.objects.create(slug='media', title='Media', category='mod', owner=alice)
|
|
from library.storage import store_file
|
|
|
|
ProjectAsset.objects.create(
|
|
project=project, uploaded_by=alice,
|
|
file=store_file(
|
|
alice, 'asset',
|
|
SimpleUploadedFile('anim.gif', make_gif_bytes(), content_type='image/gif'),
|
|
project.pk, subdir='gallery',
|
|
),
|
|
)
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:project_detail', args=['media']))
|
|
self.assertContains(resp, '/thumb/')
|
|
|
|
|
|
def _stored_mcmeta(version):
|
|
from django.core.files.storage import default_storage
|
|
release = version.files.filter(kind='release').first()
|
|
with default_storage.open(release.file.stored_path, 'rb') as f:
|
|
with zipfile.ZipFile(f) as zf:
|
|
return json.loads(zf.read('pack.mcmeta').decode('utf-8'))
|
|
|
|
|
|
class UGCAnimationIdTests(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 _write_zip(self, data):
|
|
with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as f:
|
|
f.write(data)
|
|
return f.name
|
|
|
|
def _upload_version(self, slug, name, content):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.post(
|
|
reverse('library:api_upload_temp'),
|
|
{'kind': 'version', 'file': SimpleUploadedFile(name, content, content_type='application/zip')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest',
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
return self.client.post(
|
|
reverse('library:version_upload', args=[slug]),
|
|
{'version_name': '1.0.0'},
|
|
)
|
|
|
|
def test_inject_merge_and_create(self):
|
|
from library.zips import inject_animationframework
|
|
path = self._write_zip(_logical_zip())
|
|
try:
|
|
ok = inject_animationframework(path, {'id': 'alice:project_1', 'name': 'X',
|
|
'author': 'Alice', 'version': '1.0', 'description': 'y'})
|
|
self.assertTrue(ok)
|
|
with zipfile.ZipFile(path) as zf:
|
|
mc = json.loads(zf.read('pack.mcmeta').decode('utf-8'))
|
|
self.assertEqual(mc['animationframework']['id'], 'alice:project_1')
|
|
self.assertEqual(mc['pack']['pack_format'], 61) # existing block preserved
|
|
|
|
path2 = self._write_zip(_zip_bytes({'data/x.txt': 'x'}))
|
|
inject_animationframework(path2, {'id': 'a:b'})
|
|
with zipfile.ZipFile(path2) as zf:
|
|
mc2 = json.loads(zf.read('pack.mcmeta').decode('utf-8'))
|
|
self.assertEqual(mc2['pack']['pack_format'], 64)
|
|
self.assertEqual(mc2['pack']['supported_formats'], [64, 81])
|
|
self.assertEqual(mc2['pack']['min_format'], 64)
|
|
self.assertEqual(mc2['pack']['max_format'], 81)
|
|
self.assertEqual(mc2['animationframework']['id'], 'a:b')
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
def test_version_upload_injects_defaults(self):
|
|
self.project = Project.objects.create(
|
|
slug='np', title='NoN Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
resp = self._upload_version('np', 'pack.zip', _logical_zip())
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['np']))
|
|
version = self.project.versions.first()
|
|
expected = f'alice:project_{self.project.pk}'
|
|
self.assertEqual(version.animation_id, expected)
|
|
mc = _stored_mcmeta(version)
|
|
self.assertEqual(mc['animationframework']['id'], expected)
|
|
self.assertEqual(mc['animationframework']['name'], 'NoN Pack')
|
|
# latest API works for the injected id.
|
|
self.gate()
|
|
resp = self.client.get(reverse('library:api_packs_latest', args=['alice', f'project_{self.project.pk}']))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp.json()['version'], '1.0.0')
|
|
# Download is not truncated: FileIndex size refreshed after injection.
|
|
release = version.files.first()
|
|
resp = self.client.get(reverse('library:file_request', args=[release.file.uuid]))
|
|
served = b''.join(resp.streaming_content)
|
|
self.assertEqual(int(resp['Content-Length']), release.file.size)
|
|
self.assertEqual(len(served), release.file.size)
|
|
with zipfile.ZipFile(io.BytesIO(served)) as zf:
|
|
self.assertIsNone(zf.testzip())
|
|
|
|
def test_version_upload_establishes_from_pack(self):
|
|
self.project = Project.objects.create(
|
|
slug='np', title='NoN Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('np', 'pack.zip', _animation_pack_zip())
|
|
version = self.project.versions.first()
|
|
self.assertEqual(version.animation_id, 'jakebreath:testpack')
|
|
|
|
def test_version_upload_reuses_project_id(self):
|
|
self.project = Project.objects.create(
|
|
slug='np', title='NoN Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('np', 'pack.zip', _animation_pack_zip()) # establishes jakebreath:testpack
|
|
self._upload_version('np', 'pack2.zip', _logical_zip()) # no id → reuse
|
|
versions = list(self.project.versions.order_by('created_at'))
|
|
self.assertEqual(versions[-1].animation_id, 'jakebreath:testpack')
|
|
|
|
def test_different_id_requires_confirm(self):
|
|
self.project = Project.objects.create(
|
|
slug='np', title='NoN Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('np', 'pack.zip', _animation_pack_zip()) # jakebreath:testpack
|
|
other = _zip_bytes({
|
|
'pack.mcmeta': json.dumps({'pack': {'pack_format': 64},
|
|
'animationframework': {'id': 'other:pack'}}),
|
|
'data/other/afw_animdefs/x.json': '{}',
|
|
})
|
|
# Without confirmation → form error, no new version.
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
self.client.post(reverse('library:api_upload_temp'),
|
|
{'kind': 'version', 'file': SimpleUploadedFile('other.zip', other, content_type='application/zip')},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
resp = self.client.post(reverse('library:version_upload', args=['np']), {'version_name': '2.0.0'})
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(self.project.versions.count(), 1)
|
|
# With confirmation → accepted.
|
|
resp = self.client.post(reverse('library:version_upload', args=['np']),
|
|
{'version_name': '2.0.0', 'confirm_replace_identity': '1'})
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['np']))
|
|
self.assertEqual(self.project.versions.count(), 2)
|
|
self.assertEqual(self.project.versions.first().animation_id, 'other:pack')
|
|
|
|
def test_non_non_pack_not_injected(self):
|
|
self.project = Project.objects.create(
|
|
slug='mod', title='A Mod', category='mod', owner=self.alice,
|
|
)
|
|
self._upload_version('mod', 'pack.zip', _logical_zip())
|
|
version = self.project.versions.first()
|
|
self.assertEqual(version.animation_id, '')
|
|
self.assertNotIn('animationframework', _stored_mcmeta(version))
|
|
|
|
def test_regenerate_metadata(self):
|
|
self.project = Project.objects.create(
|
|
slug='np', title='NoN Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('np', 'pack.zip', _logical_zip())
|
|
version = self.project.versions.first()
|
|
self.assertTrue(version.animation_id) # auto-injected at upload
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.post(reverse('library:regenerate_metadata', args=['np']),
|
|
{'af_namespace': 'foo', 'af_pack_id': 'bar'})
|
|
self.assertRedirects(resp, reverse('library:project_edit', args=['np']))
|
|
version.refresh_from_db()
|
|
self.assertEqual(version.animation_id, 'foo:bar')
|
|
mc = _stored_mcmeta(version)
|
|
self.assertEqual(mc['animationframework']['id'], 'foo:bar')
|
|
resp = self.client.get(reverse('library:api_packs_latest', args=['foo', 'bar']))
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp.json()['version'], '1.0.0')
|
|
# Regenerated download is intact (FileIndex size refreshed).
|
|
release = version.files.first()
|
|
resp = self.client.get(reverse('library:file_request', args=[release.file.uuid]))
|
|
served = b''.join(resp.streaming_content)
|
|
self.assertEqual(int(resp['Content-Length']), release.file.size)
|
|
self.assertEqual(len(served), release.file.size)
|
|
with zipfile.ZipFile(io.BytesIO(served)) as zf:
|
|
self.assertIsNone(zf.testzip())
|
|
|
|
def test_regenerate_validates_id(self):
|
|
self.project = Project.objects.create(
|
|
slug='np', title='NoN Pack', category='non_pack', owner=self.alice,
|
|
)
|
|
self._upload_version('np', 'pack.zip', _logical_zip())
|
|
before = self.project.versions.first().animation_id
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
resp = self.client.post(reverse('library:regenerate_metadata', args=['np']),
|
|
{'af_namespace': 'bad id!', 'af_pack_id': 'bar'})
|
|
self.assertRedirects(resp, reverse('library:project_edit', args=['np']))
|
|
self.project.versions.first().refresh_from_db()
|
|
self.assertEqual(self.project.versions.first().animation_id, before)
|
|
|
|
def test_create_injects_defaults(self):
|
|
self.gate()
|
|
self.client.login(username='Alice', password='pw')
|
|
uuid = self.client.get(reverse('library:project_create_new')).url.rstrip('/').split('/')[-1]
|
|
self.client.post(reverse('library:api_upload_temp'),
|
|
{'kind': 'version', 'file': SimpleUploadedFile('pack.zip', _logical_zip(), content_type='application/zip'), 'draft_uuid': uuid},
|
|
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
resp = self.client.post(reverse('library:project_create', args=[uuid]), {
|
|
'title': 'New Pack', 'category': 'non_pack', 'version_name': '1.0.0',
|
|
})
|
|
self.assertRedirects(resp, reverse('library:project_detail', args=['new-pack']))
|
|
project = Project.objects.get(slug='new-pack')
|
|
version = project.versions.first()
|
|
self.assertEqual(version.animation_id, f'alice:project_{project.pk}')
|