# ============================================================================== # Author: Simon-Pierre Boucher # File: tests/test_vague2.py # Desc: Tests vague 2 — flux RSS des balados, nouvelles plateformes # (spotify/discord/apple-podcasts), liaison bio-liens, compteurs X # ============================================================================== from pathlib import Path from creaka.connectors.balados_rss import parse_feed, _parse_feed_regex from creaka.connectors.bio_liens import extract_bio_accounts from creaka.connectors.x_profil import parse_counts, parse_syndication from creaka.normalize import canonical_url, normalize_platform, platform_from_url FIXTURE = (Path(__file__).parent / "fixtures" / "balado_sample.xml").read_bytes() # --- balados-rss ---------------------------------------------------------------- def test_parse_feed_champs(): feed = parse_feed(FIXTURE) assert feed["episodes"] == 3 assert feed["last_episode"].strftime("%Y-%m-%d") == "2026-08-01" assert feed["image"] == "https://exemple.qc.ca/pochette.jpg" assert feed["link"] == "https://www.balado-exemple.ca" assert "balado d'exemple" in feed["description"].lower() assert "Comedy" in feed["categories"] def test_parse_feed_repli_regex_flux_tronque(): # flux coupé au milieu d'un item : le repli regex garde l'en-tête du canal trunc = FIXTURE.split(b"")[0] + b"coup" feed = _parse_feed_regex(trunc) assert feed is not None assert feed["image"] == "https://exemple.qc.ca/pochette.jpg" assert feed["link"] == "https://www.balado-exemple.ca" assert feed["episodes"] == 1 def test_parse_feed_illisible(): assert parse_feed(b"pas du xml") is None # --- nouvelles plateformes (normalize) -------------------------------------------- def test_platform_from_url_spotify_discord_apple(): assert platform_from_url( "https://open.spotify.com/show/4rOoJ6Egrf8K2IrywzwOMk") == \ ("spotify", "4rooj6egrf8k2irywzwomk") assert platform_from_url( "https://open.spotify.com/intl-fr/artist/06HL4z0CvFAxyc27GXpf02") is not None assert platform_from_url("https://discord.gg/AbCd123") == ("discord", "abcd123") assert platform_from_url("https://discord.com/invite/AbCd123") == \ ("discord", "abcd123") assert platform_from_url( "https://podcasts.apple.com/ca/podcast/mon-balado/id1502950331") == \ ("podcast", "1502950331") def test_canonical_url_conserve_url_source(): # IDs Spotify et invitations Discord : sensibles à la casse → on garde l'URL src = "https://open.spotify.com/show/4rOoJ6Egrf8K2IrywzwOMk" assert canonical_url("spotify", "4roo", fallback=src) == src assert normalize_platform("spotify") == "spotify" assert normalize_platform("discord") == "discord" assert normalize_platform("apple podcasts") == "podcast" # --- bio-liens -------------------------------------------------------------------- def test_extract_bio_accounts_urls_completes_seulement(): bio = ("Humoriste. Balado : https://open.spotify.com/show/4rOoJ6Egrf8K2Iryw " "· YouTube www.youtube.com/@moncompte · suivez @autretiktok !") accounts = extract_bio_accounts(bio, existing_keys=set()) keys = {a.key for a in accounts} assert "youtube:moncompte" in keys assert any(k.startswith("spotify:") for k in keys) # « @autretiktok » textuel = ambigu → JAMAIS rattaché assert not any("autretiktok" in k for k in keys) assert all(a.signal == "cross_link" for a in accounts) def test_extract_bio_accounts_ignore_comptes_connus(): bio = "Mon YouTube : https://youtube.com/@moncompte" assert extract_bio_accounts(bio, existing_keys={"youtube:moncompte"}) == [] # --- x-profil --------------------------------------------------------------------- def test_parse_syndication_next_data(): html = ('<html><script id="__NEXT_DATA__" type="application/json">' '{"props":{"pageProps":{"timeline":{"entries":[{"content":{"tweet":' '{"user":{"screen_name":"MonCompte","followers_count":4321,' '"friends_count":99,"verified":true,' '"profile_image_url_https":"https://pbs.twimg.com/x_normal.jpg"' '}}}}]}}}}</script></html>') info = parse_syndication(html, "moncompte") assert info["followers"] == 4321 assert info["following"] == 99 assert info["verified"] is True assert info["avatar"].endswith("x_400x400.jpg") # autre handle dans la timeline (retweet) : JAMAIS pris pour le profil assert parse_syndication(html, "autrecompte") is None def test_parse_counts_etat_integre(): html = ('...t:VXNlcjo=:relationship_counts",__typename:' '"UserRelationshipCounts",followers:12068,following:847,...') assert parse_counts(html) == (12068, 847) assert parse_counts("<html>Se connecter</html>") is None