aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-common/tests')
-rw-r--r--packages/meshbay-common/tests/test_paths.py123
1 files changed, 123 insertions, 0 deletions
diff --git a/packages/meshbay-common/tests/test_paths.py b/packages/meshbay-common/tests/test_paths.py
new file mode 100644
index 0000000..b9052e3
--- /dev/null
+++ b/packages/meshbay-common/tests/test_paths.py
@@ -0,0 +1,123 @@
+"""
+Name rules that have to give the same answer on every platform.
+
+Most people share from an external drive formatted exFAT or NTFS, on Windows, so
+these are not compatibility niceties — they decide whether two index entries are
+one file, and whether a member can save what they downloaded.
+"""
+
+import pytest
+
+from meshbay_common.paths import (
+ find_fold_collisions,
+ fold,
+ fold_path,
+ is_portable_name,
+ nfc,
+ portable_name_problem,
+ sanitize_for_download,
+)
+
+
+# ── Same file or not ─────────────────────────────────────────────────────────
+
+def test_case_differences_fold_together():
+ assert fold("README.TXT") == fold("readme.txt")
+
+
+def test_folding_is_not_lowercasing():
+ """`casefold` handles what `lower()` misses, and those are the pairs that
+ arrive as a bug report rather than a test failure."""
+ assert fold("STRASSE") == fold("straße")
+ assert "STRASSE".lower() != "straße".lower()
+
+
+def test_the_two_unicode_spellings_of_an_accent_are_one_name():
+ """
+ `Café.mkv` written on macOS (NFD) and on Windows (NFC) are different byte
+ strings naming the same file. For French filenames this is routine.
+ """
+ nfc_form = "Café.mkv" # é precomposed
+ nfd_form = "Café.mkv" # e + combining acute
+ assert nfc_form != nfd_form
+ assert nfc(nfd_form) == nfc_form
+ assert fold(nfd_form) == fold(nfc_form)
+
+
+def test_distinct_names_stay_distinct():
+ assert fold("film.mkv") != fold("film2.mkv")
+
+
+def test_folding_a_path_keeps_its_separators():
+ assert fold_path("Films/2024/A.MKV") == "films/2024/a.mkv"
+
+
+def test_collisions_are_found_and_grouped():
+ found = find_fold_collisions(
+ ["README.txt", "readme.TXT", "notes.md", "Café.mkv", "Café.mkv"])
+ groups = {frozenset(v) for v in found.values()}
+ assert len(groups) == 2
+ assert frozenset({"README.txt", "readme.TXT"}) in groups
+ # The other pair is the two Unicode spellings of the same accented name,
+ # which are different byte strings — so compare by size, not by literal.
+ assert any(len(g) == 2 and all(f.endswith('.mkv') for f in g)
+ for g in groups)
+ assert all("notes.md" not in v for v in found.values())
+
+
+def test_no_collision_means_no_report():
+ assert find_fold_collisions(["a.txt", "b.txt"]) == {}
+
+
+# ── What Windows refuses ─────────────────────────────────────────────────────
+
+@pytest.mark.parametrize("name", ["CON", "PRN", "AUX", "NUL", "COM1", "LPT9",
+ "aux", "Aux.txt", "COM3.tar.gz"])
+def test_windows_reserved_names_are_flagged(name):
+ """`AUX.txt` is as impossible as `AUX` — the extension does not help."""
+ assert portable_name_problem(name) is not None
+
+
+@pytest.mark.parametrize("name", ['a<b', 'a>b', 'a:b', 'a"b', "a/b", "a\\b",
+ "a|b", "a?b", "a*b", "a\x00b", "a\tb"])
+def test_reserved_characters_are_flagged(name):
+ assert portable_name_problem(name) is not None
+
+
+@pytest.mark.parametrize("name", ["trailing ", "trailing.", " "])
+def test_a_trailing_space_or_dot_is_flagged(name):
+ """Windows strips them silently, so `file .` and `file` come back the same."""
+ assert portable_name_problem(name) is not None
+
+
+@pytest.mark.parametrize("name", ["film.mkv", "Café.mkv", "rapport (1).pdf",
+ "été.txt", "COMET.txt", "AUXILIARY.doc",
+ "日本語.txt"])
+def test_ordinary_names_are_portable(name):
+ assert is_portable_name(name), portable_name_problem(name)
+
+
+def test_empty_and_dot_names_are_refused():
+ assert portable_name_problem("") is not None
+ assert portable_name_problem(".") is not None
+ assert portable_name_problem("..") is not None
+
+
+# ── Saving a file whose name the platform refuses ────────────────────────────
+
+def test_a_portable_name_is_returned_unchanged():
+ """Identity in the common case, so a caller can tell whether it renamed
+ anything by comparing."""
+ assert sanitize_for_download("film.mkv") == "film.mkv"
+ assert sanitize_for_download("Café (2024).mkv") == "Café (2024).mkv"
+
+
+def test_sanitizing_produces_something_writable():
+ for hostile in ["a<b>c.txt", "AUX", "trailing .", "with/slash.txt", "COM1.log"]:
+ cleaned = sanitize_for_download(hostile)
+ assert is_portable_name(cleaned), f"{hostile!r} → {cleaned!r} still refused"
+
+
+def test_sanitizing_never_returns_nothing():
+ assert sanitize_for_download("...") not in ("", None)
+ assert sanitize_for_download("???") not in ("", None)