Phase 2.9 complete

This commit is contained in:
2026-08-04 12:05:45 -05:00
parent b7b21ab5e0
commit a45b686f6c
7 changed files with 549 additions and 20 deletions
+195 -5
View File
@@ -1277,7 +1277,7 @@ class UGCAnimationTests(UGCMediaTestCase, UGCGatedTestCase):
self.assertIn('missionary', names)
self.assertIn('bugged', names)
def test_version_upload_warns_without_animation_id(self):
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,
)
@@ -1285,11 +1285,9 @@ class UGCAnimationTests(UGCMediaTestCase, UGCGatedTestCase):
self.assertRedirects(resp, reverse('library:project_detail', args=['anim-pack']),
fetch_redirect_response=False)
version = self.project.versions.first()
self.assertEqual(version.animation_id, '')
# 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)
# The warning banner renders on the redirect target (before it's consumed).
resp = self.client.get(reverse('library:project_detail', args=['anim-pack']))
self.assertContains(resp, 'incompatible with the update-check API')
def test_latest_api_returns_version_and_url(self):
self.project = Project.objects.create(
@@ -1574,3 +1572,195 @@ class UGCThumbnailTests(UGCMediaTestCase, UGCGatedTestCase):
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')
self.client.post(reverse('library:api_upload_temp'),
{'kind': 'version', 'file': SimpleUploadedFile('pack.zip', _logical_zip(), content_type='application/zip')},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
resp = self.client.post(reverse('library:project_create'), {
'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}')