|
4 | 4 |
|
5 | 5 | import hashlib |
6 | 6 | import io |
| 7 | +import json |
7 | 8 | import zipfile |
| 9 | +from datetime import datetime, timezone |
8 | 10 | from pathlib import Path |
9 | 11 | from unittest.mock import MagicMock, patch |
10 | 12 |
|
@@ -82,6 +84,62 @@ def _response(data: bytes, url: str) -> MagicMock: |
82 | 84 | return response |
83 | 85 |
|
84 | 86 |
|
| 87 | +def _duplicate_release_json() -> bytes: |
| 88 | + entry = _entry() |
| 89 | + payload = json.dumps({"schema_version": "1.0", "presets": {"sample": entry}}) |
| 90 | + record = f'"1.0.0": {json.dumps(entry["releases"]["1.0.0"])}' |
| 91 | + conflicting = {**entry["releases"]["1.0.0"], "download_url": CURRENT_URL} |
| 92 | + assert record in payload |
| 93 | + return payload.replace( |
| 94 | + record, f'{record}, "1.0.0": {json.dumps(conflicting)}', 1 |
| 95 | + ).encode() |
| 96 | + |
| 97 | + |
| 98 | +@pytest.mark.parametrize("legacy", [False, True], ids=["stack", "single-catalog"]) |
| 99 | +def test_duplicate_release_key_rejected_from_network(project_dir, legacy): |
| 100 | + catalog = PresetCatalog(project_dir) |
| 101 | + url = catalog.DEFAULT_CATALOG_URL |
| 102 | + entry = PresetCatalogEntry(url, "default", 1, True) |
| 103 | + with ( |
| 104 | + patch.object(catalog, "get_catalog_url", return_value=url), |
| 105 | + patch.object( |
| 106 | + catalog, "_open_url", return_value=_response(_duplicate_release_json(), url) |
| 107 | + ), |
| 108 | + pytest.raises(PresetError, match="duplicate.*1.0.0"), |
| 109 | + ): |
| 110 | + if legacy: |
| 111 | + catalog.fetch_catalog(force_refresh=True) |
| 112 | + else: |
| 113 | + catalog._fetch_single_catalog(entry, force_refresh=True) |
| 114 | + assert not catalog.cache_file.exists() |
| 115 | + |
| 116 | + |
| 117 | +@pytest.mark.parametrize("legacy", [False, True], ids=["stack", "single-catalog"]) |
| 118 | +def test_duplicate_release_key_in_cache_refetches(project_dir, legacy): |
| 119 | + catalog = PresetCatalog(project_dir) |
| 120 | + url = catalog.DEFAULT_CATALOG_URL |
| 121 | + entry = PresetCatalogEntry(url, "default", 1, True) |
| 122 | + catalog.cache_dir.mkdir(parents=True) |
| 123 | + catalog.cache_file.write_bytes(_duplicate_release_json()) |
| 124 | + catalog.cache_metadata_file.write_text( |
| 125 | + json.dumps({ |
| 126 | + "cached_at": datetime.now(timezone.utc).isoformat(), |
| 127 | + "catalog_url": url, |
| 128 | + }) |
| 129 | + ) |
| 130 | + valid = {"schema_version": "1.0", "presets": {"sample": _entry()}} |
| 131 | + with ( |
| 132 | + patch.object(catalog, "get_catalog_url", return_value=url), |
| 133 | + patch.object( |
| 134 | + catalog, "_open_url", return_value=_response(json.dumps(valid).encode(), url) |
| 135 | + ) as opened, |
| 136 | + ): |
| 137 | + result = catalog.fetch_catalog() if legacy else catalog._fetch_single_catalog(entry) |
| 138 | + assert result == valid |
| 139 | + opened.assert_called_once() |
| 140 | + assert json.loads(catalog.cache_file.read_text()) == valid |
| 141 | + |
| 142 | + |
85 | 143 | def test_current_and_exact_selection_keep_current_fields(project_dir): |
86 | 144 | catalog = PresetCatalog(project_dir) |
87 | 145 | entry = _entry() |
@@ -204,6 +262,42 @@ def test_malformed_history_rejected_even_for_current(project_dir, change, error) |
204 | 262 | catalog.get_pack_info("sample") |
205 | 263 |
|
206 | 264 |
|
| 265 | +@pytest.mark.parametrize( |
| 266 | + "dependencies", |
| 267 | + [ |
| 268 | + [123], |
| 269 | + [{}], |
| 270 | + [{"id": "dep", "version": 2}], |
| 271 | + [{"id": "dep", "required": 0}], |
| 272 | + ["bad id"], |
| 273 | + ], |
| 274 | +) |
| 275 | +def test_historical_release_rejects_malformed_extension_dependencies( |
| 276 | + project_dir, dependencies |
| 277 | +): |
| 278 | + entry = _entry() |
| 279 | + entry["releases"]["1.0.0"]["requires"]["extensions"] = dependencies |
| 280 | + catalog = PresetCatalog(project_dir) |
| 281 | + with ( |
| 282 | + patch.object(catalog, "_get_merged_packs", return_value={"sample": entry}), |
| 283 | + pytest.raises(PresetError, match="requires.extensions"), |
| 284 | + ): |
| 285 | + catalog.get_pack_info("sample") |
| 286 | + |
| 287 | + |
| 288 | +def test_historical_release_accepts_manifest_extension_dependencies(project_dir): |
| 289 | + dependencies = [ |
| 290 | + "plain-ext", |
| 291 | + {"id": "other-ext", "version": ">=1.2", "required": False}, |
| 292 | + ] |
| 293 | + entry = _entry() |
| 294 | + entry["releases"]["1.0.0"]["requires"]["extensions"] = dependencies |
| 295 | + catalog = PresetCatalog(project_dir) |
| 296 | + with patch.object(catalog, "_get_merged_packs", return_value={"sample": entry}): |
| 297 | + selected = catalog.get_pack_info("sample", "1.0.0") |
| 298 | + assert selected["requires"]["extensions"] == dependencies |
| 299 | + |
| 300 | + |
207 | 301 | def test_winning_source_does_not_fall_back_to_lower_release(project_dir): |
208 | 302 | catalog = PresetCatalog(project_dir) |
209 | 303 | sources = [ |
@@ -400,6 +494,21 @@ def open_url(_self, url, **_kwargs): |
400 | 494 | assert PresetManager(project_dir).get_pack("sample").version == "1.0.0" |
401 | 495 |
|
402 | 496 |
|
| 497 | +def test_cli_versions_use_winning_entry_snapshot(project_dir): |
| 498 | + first = {**_entry(), "_install_allowed": False} |
| 499 | + second = {"id": "sample", "version": "3.0.0"} |
| 500 | + with ( |
| 501 | + patch.object(Path, "cwd", return_value=project_dir), |
| 502 | + patch.object(PresetCatalog, "get_pack_info", side_effect=[first, second]) as lookup, |
| 503 | + ): |
| 504 | + result = CliRunner().invoke(app, ["preset", "info", "sample", "--versions"]) |
| 505 | + assert result.exit_code == 0, result.output |
| 506 | + assert "2.0.0 (current)" in result.output and "1.0.0" in result.output |
| 507 | + assert "3.0.0" not in result.output |
| 508 | + assert "Discovery only" in result.output |
| 509 | + lookup.assert_called_once_with("sample") |
| 510 | + |
| 511 | + |
403 | 512 | def test_cli_rejects_missing_release_and_discovery_without_download(project_dir): |
404 | 513 | entry = _entry() |
405 | 514 | with ( |
|
0 commit comments