import os import stat from app.services import credential_service as cs from app.services import playlist_service as pl def test_set_conf_field_replaces_existing(): text = "user = OLD\npass = OLD\n" out = cs._set_conf_field(text, "user", "newuser") assert "user = newuser" in out assert "pass = OLD" in out # other lines untouched def test_set_conf_field_appends_when_absent(): out = cs._set_conf_field("input = x\n", "user", "bob") assert out.rstrip().endswith("user = bob") def test_set_conf_field_strips_newlines(): # a newline in a value must not inject a second config line out = cs._set_conf_field("user = x\n", "user", "bad\nname = injected") assert "\nname = injected" not in out assert "user = badname = injected" in out def test_secret_keys_classification(): assert "password" in cs.SECRET_KEYS assert "client_secret" in cs.SECRET_KEYS # identifiers/urls/prefs are NOT secret (shown as text in the UI) assert "username" not in cs.SECRET_KEYS assert "base_url" not in cs.SECRET_KEYS assert "region" not in cs.SECRET_KEYS def test_render_playlist_confs_injects_creds_safely(db, config_dir): cs.set_credentials(db, "spotify", {"client_id": "CID", "client_secret": "CSECRET"}) # a password containing shell metacharacters must land literally cs.set_credentials(db, "soulseek", {"username": "seek", "password": "p'a$(id)ss"}) pl.create(db, name="rtest", spotify_url="https://open.spotify.com/playlist/Z") conf = config_dir / "pipeline" / "rtest.conf" text = conf.read_text() assert "user = seek" in text assert "pass = p'a$(id)ss" in text # run-playlist.sh scrapes the playlist URL from this line to build the CSV assert "input = https://open.spotify.com/playlist/Z" in text # Spotify creds must NOT be rendered into confs: sldl never talks to # Spotify (run-playlist.sh feeds it a CSV); the creds live in _spotify.env assert "spotify-id" not in text assert "spotify-secret" not in text assert "spotify-refresh" not in text assert "CID" not in text assert "CSECRET" not in text # rendered config must be owner-only (holds the Soulseek password) assert stat.S_IMODE(os.stat(conf).st_mode) == 0o600 def test_spotify_env_renders_creds_and_refresh_token(db, config_dir): # _spotify.env is the ONLY rendered home of Spotify creds; run-playlist.sh # sources it to fetch the playlist CSV, so values must be shell-quoted cs.set_credentials( db, "spotify", {"client_id": "CID", "client_secret": "CS'EC$(id)RET", "refresh_token": "RTOK"} ) env = config_dir / "pipeline" / "_spotify.env" text = env.read_text() assert "SPOTIFY_CLIENT_ID=CID" in text # shlex.quote keeps a metacharacter-laden secret a single literal value assert "SPOTIFY_CLIENT_SECRET='CS'\"'\"'EC$(id)RET'" in text assert "SPOTIFY_REFRESH_TOKEN=RTOK" in text assert stat.S_IMODE(os.stat(env).st_mode) == 0o600 def test_spotify_env_refresh_token_blank_until_connected(db, config_dir): cs.set_credentials(db, "spotify", {"client_id": "CID", "client_secret": "CSECRET"}) text = (config_dir / "pipeline" / "_spotify.env").read_text() # rendered blank (''), which sources cleanly and stays falsy in bash assert "SPOTIFY_REFRESH_TOKEN=''" in text def test_azuracast_renders_and_clears_base_url(db, config_dir): cs.set_credentials(db, "azuracast", {"base_url": "https://radio.example.com", "api_key": "KEY"}) az_dir = config_dir / "pipeline" / "azuracast" assert (az_dir / "base_url").read_text() == "https://radio.example.com" assert (az_dir / "api_key").read_text() == "KEY" cs.set_scope_enabled(db, "azuracast", False) assert not (az_dir / "base_url").exists() assert not (az_dir / "api_key").exists() cs.set_scope_enabled(db, "azuracast", True) assert (az_dir / "base_url").read_text() == "https://radio.example.com" def test_bandcamp_cookie_expiry_parsing(): jar = "\n".join([ "# Netscape HTTP Cookie File", "\t".join([".bandcamp.com", "TRUE", "/", "TRUE", "1893456000", "identity", "abc"]), ]) exp = cs._bandcamp_cookie_expiry(jar) assert exp == 1893456000.0 assert cs._bandcamp_cookie_expiry("no identity cookie here") is None