Manifest: transform path before checking against the zip file list #171

Merged
Oleg-Komarov merged 1 commits from manifest-canonical-path into main 2024-06-06 14:48:14 +02:00
3 changed files with 110 additions and 6 deletions

View File

@ -102,7 +102,7 @@ EXPECTED_VALIDATION_ERRORS = {
'invalid-theme-multiple-xmls.zip': {'source': ['A theme should have exactly one XML file.']},
'invalid-missing-wheels.zip': {
'source': [
'A declared wheel is missing in the zip file, expected path: addon/./wheels/test-wheel-whatever.whl'
'A declared wheel is missing in the zip file, expected path: addon/wheels/test-wheel-whatever.whl'
]
},
}

View File

@ -1,5 +1,6 @@
from pathlib import Path
from unittest.mock import patch, ANY
import dataclasses
import tempfile
from django.test import TestCase
@ -11,6 +12,7 @@ from files.utils import (
find_path_by_name,
get_thumbnail_upload_to,
make_thumbnails,
validate_file_list,
)
# Reusing test files from the extensions app
@ -171,3 +173,84 @@ class UtilsTest(TestCase):
mock_ffmpeg.return_value.option.return_value.input.assert_any_call(
'path/to/source/video.mp4'
)
def test_validate_file_list(self):
@dataclasses.dataclass
class TestParams:
name: str
toml_content: dict
manifest_filepath: dict
file_list: list
expected: list
for test in [
TestParams(
name='valid add-on',
toml_content={'type': 'add-on'},
manifest_filepath='blender_manifest.toml',
file_list=['__init__.py'],
expected=[],
),
TestParams(
name='add-on missing init',
toml_content={'type': 'add-on'},
manifest_filepath='blender_manifest.toml',
file_list=[],
expected=['invalid_missing_init'],
),
TestParams(
name='valid theme',
toml_content={'type': 'theme'},
manifest_filepath='blender_manifest.toml',
file_list=['theme.xml'],
expected=[],
),
TestParams(
name='missing theme file',
toml_content={'type': 'theme'},
manifest_filepath='blender_manifest.toml',
file_list=[],
expected=['missing_or_multiple_theme_xml'],
),
TestParams(
name='multiple theme files',
toml_content={'type': 'theme'},
manifest_filepath='blender_manifest.toml',
file_list=['theme.xml', 'theme/2.xml'],
expected=['missing_or_multiple_theme_xml'],
),
TestParams(
name='valid nested wheels',
toml_content={'type': 'add-on', 'wheels': ['wheels/1.whl']},
manifest_filepath='addon/blender_manifest.toml',
file_list=['addon/__init__.py', 'addon/wheels/1.whl'],
expected=[],
),
TestParams(
name='valid nested wheels with ./',
toml_content={'type': 'add-on', 'wheels': ['./wheels/1.whl']},
manifest_filepath='blender_manifest.toml',
file_list=['__init__.py', 'wheels/1.whl'],
expected=[],
),
TestParams(
name='missing wheel',
toml_content={'type': 'add-on', 'wheels': ['./wheels/1.whl', './wheels/2.whl']},
manifest_filepath='blender_manifest.toml',
file_list=['__init__.py', 'wheels/1.whl'],
expected=[{'code': 'missing_wheel', 'params': {'path': 'wheels/2.whl'}}],
),
TestParams(
name='incorrect wheel nesting',
toml_content={'type': 'add-on', 'wheels': ['./wheels/1.whl']},
manifest_filepath='add-on/blender_manifest.toml',
file_list=['add-on/__init__.py', 'wheels/1.whl'],
expected=[{'code': 'missing_wheel', 'params': {'path': 'add-on/wheels/1.whl'}}],
),
]:
with self.subTest(**dataclasses.asdict(test)):
self.assertEqual(
test.expected,
validate_file_list(test.toml_content, test.manifest_filepath, test.file_list),
test.name,
)

View File

@ -147,7 +147,14 @@ def read_manifest_from_zip(archive_path):
error_codes.append('invalid_manifest_toml')
return None, error_codes
# If manifest was parsed successfully, do additional type-specific validation
file_list_error_codes = validate_file_list(toml_content, manifest_filepath, file_list)
error_codes.extend(file_list_error_codes)
return toml_content, error_codes
def validate_file_list(toml_content, manifest_filepath, file_list):
"""Check the files in in the archive against manifest."""
error_codes = []
type_slug = toml_content['type']
if type_slug == 'theme':
theme_xmls = filter_paths_by_ext(file_list, '.xml')
@ -155,22 +162,36 @@ def read_manifest_from_zip(archive_path):
error_codes.append('missing_or_multiple_theme_xml')
elif type_slug == 'add-on':
# __init__.py is expected to be next to the manifest
expected_init_path = os.path.join(os.path.dirname(manifest_filepath), '__init__.py')
expected_init_path = _canonical_path('__init__.py', manifest_filepath)
init_filepath = find_exact_path(file_list, expected_init_path)
if not init_filepath:
error_codes.append('invalid_missing_init')
wheels = toml_content.get('wheels')
if wheels:
for wheel in wheels:
expected_wheel_path = os.path.join(os.path.dirname(manifest_filepath), wheel)
expected_wheel_path = _canonical_path(wheel, manifest_filepath)
wheel_filepath = find_exact_path(file_list, expected_wheel_path)
if not wheel_filepath:
error_codes.append(
{'code': 'missing_wheel', 'params': {'path': expected_wheel_path}}
)
return error_codes
return toml_content, error_codes
def _canonical_path(path, manifest_filepath):
"""Transform path before checking against the zip file list.
We expect to support other manifest fields (e.g. in the [build] section) that will potentially
point to directories, including the "current" directory, which has to be denoted as "./".
To avoid inconsistencies in file path notations supported for different fields, we process all
paths values in manifest in a uniform way, allowing the leading "./" in all file paths.
All paths mentioned in manifest are treated as relative for the directory that contains
manifest_filepath.
"""
if path.startswith('./'):
path = path[2:]
return os.path.join(os.path.dirname(manifest_filepath), path)
def guess_mimetype_from_ext(file_name: str) -> str: