import io import secrets import shutil import tempfile 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_/). 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, '

Hello

', 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'}])