"""Unit tests for Subsonic crypto + id helpers (no DB, no network).""" import hashlib import uuid import pytest from app.api.rest import ids from app.api.rest.ids import IdKind from app.core.security import SubsonicPasswordCipher, generate_subsonic_password from app.domain.errors import AuthenticationError, NotFoundError def test_generate_subsonic_password_is_long_and_unique() -> None: a = generate_subsonic_password() b = generate_subsonic_password() assert a != b assert len(a) >= 20 def test_cipher_roundtrip() -> None: cipher = SubsonicPasswordCipher("a-secret-key") plaintext = generate_subsonic_password() token = cipher.encrypt(plaintext) assert token != plaintext assert cipher.decrypt(token) == plaintext def test_cipher_token_then_md5_matches() -> None: """The decrypted app-password must reproduce a client's t=md5(password+salt).""" cipher = SubsonicPasswordCipher("a-secret-key") password = generate_subsonic_password() enc = cipher.encrypt(password) salt = "c19b2d" decrypted = cipher.decrypt(enc) expected = hashlib.md5((decrypted + salt).encode(), usedforsecurity=False).hexdigest() client_token = hashlib.md5((password + salt).encode(), usedforsecurity=False).hexdigest() assert expected == client_token def test_cipher_wrong_key_fails() -> None: token = SubsonicPasswordCipher("key-one").encrypt("hunter2") with pytest.raises(AuthenticationError): SubsonicPasswordCipher("key-two").decrypt(token) def test_id_encode_decode_roundtrip() -> None: value = uuid.uuid4() assert ids.decode_track(ids.encode_track(value)) == value assert ids.decode_album(ids.encode_album(value)) == value assert ids.decode_artist(ids.encode_artist(value)) == value assert ids.decode_playlist(ids.encode_playlist(value)) == value def test_id_parse_returns_kind() -> None: value = uuid.uuid4() kind, parsed = ids.parse(ids.encode_album(value)) assert kind is IdKind.ALBUM assert parsed == value def test_id_wrong_prefix_rejected() -> None: track = ids.encode_track(uuid.uuid4()) with pytest.raises(NotFoundError): ids.decode_album(track) def test_id_malformed_rejected() -> None: with pytest.raises(NotFoundError): ids.parse("not-a-real-id")