# retoor from datetime import datetime, timedelta, timezone import pytest from devplacepy.services.sync.normalize import ( clamp_instant, clean_text, fingerprint, instant_iso, normalize_origin, normalize_origin_uid, normalize_username, parse_instant, username_candidates, ) from devplacepy.services.sync.spec import LIMITS @pytest.mark.parametrize( "raw,expected", [ ("Other.Example.ORG", "other.example.org"), (" a1 ", "a1"), ("devplace-net", "devplace-net"), ], ) def test_origin_is_normalized(raw, expected): assert normalize_origin(raw) == expected @pytest.mark.parametrize("raw", ["", "a", "-bad", "bad-", "a..b", "has space", "x" * 64, "ünï.org"]) def test_origin_is_rejected(raw): with pytest.raises(ValueError): normalize_origin(raw) @pytest.mark.parametrize("raw", ["", " ", "a b", "x" * 65, "a/b"]) def test_origin_uid_is_rejected(raw): with pytest.raises(ValueError): normalize_origin_uid(raw) def test_origin_uid_accepts_integers_and_uuids(): assert normalize_origin_uid(42) == "42" assert normalize_origin_uid("01a139e9-a2f0-7fa1-8635-1a98fbf95e3d") def test_clean_text_strips_control_characters_and_newlines(): assert clean_text("a\x00b\r\nc\rd\x07", multiline=True) == "ab\nc\nd" assert clean_text("a \n b\t c", multiline=False) == "a b c" def test_username_always_valid_across_inputs(): samples = ["", "a", "ab", "Ünï cøde!", "x" * 80, "--__--", "john.doe@mail", "日本語", "ok_name-1"] for sample in samples: name = normalize_username(sample, "uid-1") assert LIMITS.min_username_length <= len(name) <= LIMITS.max_username_length assert all(ch.isalnum() or ch in "_-" for ch in name) and name.isascii() def test_username_is_stable_for_same_input(): assert normalize_username("a", "u1") == normalize_username("a", "u1") assert normalize_username("a", "u1") != normalize_username("a", "u2") def test_username_candidates_are_distinct_and_bounded(): seen = [] for candidate in username_candidates("x" * 32, "other.example.org"): seen.append(candidate) assert len(candidate) <= LIMITS.max_username_length if len(seen) == 50: break assert len(set(seen)) == 50 assert seen[1].endswith("-other") def test_instants_are_utc_iso_and_clamped(): assert instant_iso("2025-01-01T01:00:00+01:00") == "2025-01-01T00:00:00+00:00" assert instant_iso("2025-01-01").startswith("2025-01-01T00:00:00") assert parse_instant(1700000000).year == 2023 now = datetime.now(timezone.utc) future = now + timedelta(days=30) assert clamp_instant(future, now) == now assert instant_iso("1970-01-01").startswith("2000-01-01") @pytest.mark.parametrize("raw", ["", None, "nope", True, "2025-13-45"]) def test_instant_rejects_garbage(raw): with pytest.raises(ValueError): parse_instant(raw) def test_fingerprint_ignores_key_order_and_detects_change(): assert fingerprint({"a": 1, "b": 2}) == fingerprint({"b": 2, "a": 1}) assert fingerprint({"a": 1}) != fingerprint({"a": 2})