AkurAI Build
Menu

BifrOSt-Apps

public

Latest change f67fd8437552caf5eb9371b0012e81d27c807ca3 - Add native RÚV television application by Ólafur Búi Ólafsson

use ruv::api::RuvClient;
use ruv::model::{AppError, Channel};

#[test]
fn public_contract_is_constructible_and_channels_are_stable() {
    let client = RuvClient::new().expect("the finite-timeout HTTPS client should build");
    let cloned = client.clone();
    drop(cloned);
    assert_eq!(Channel::Ruv.slug(), "ruv");
    assert_eq!(Channel::Ruv.title(), "RÚV");
    assert_eq!(Channel::Ruv2.slug(), "ruv2");
    assert_eq!(Channel::Ruv2.title(), "RÚV 2");
    assert_eq!(
        AppError::UntrustedUrl.to_string(),
        "Ótraust veffang var stöðvað."
    );
}

#[allow(dead_code)]
#[path = "../src/model.rs"]
mod model;

mod fixture_contract {
    #![allow(dead_code)]
    include!("../src/api.rs");

    const NEWS: &[u8] = include_bytes!("fixtures/news.xml");
    const VOD: &[u8] = include_bytes!("fixtures/vod.json");
    const PROGRAM: &[u8] = include_bytes!("fixtures/program.json");
    const LIVE: &[u8] = include_bytes!("fixtures/live.json");
    const EPG: &[u8] = include_bytes!("fixtures/epg.json");
    const SEARCH_ARTICLES: &[u8] = include_bytes!("fixtures/search_articles.json");
    const SEARCH_VOD: &[u8] = include_bytes!("fixtures/search_vod.json");

    #[test]
    fn rss_maps_html_body_author_dates_and_discards_untrusted_items() {
        let articles = parse_rss_body(NEWS).expect("RSS fixture should parse");
        assert_eq!(articles.len(), 1);
        let article = &articles[0];
        assert_eq!(article.title, "Prófunarfrétt");
        assert_eq!(article.author.as_deref(), Some("Fréttastofa RÚV"));
        assert!(article.body.contains("Fyrsta"));
        assert!(article.body.contains("Önnur málsgrein."));
        assert!(!article.body.contains('<'));
        assert_eq!(article.published.expect("date").timestamp(), 1_786_019_696);
        assert_eq!(
            article.image_url.as_deref(),
            Some("https://images.nyr.ruv.is/example.jpg")
        );
    }

    #[test]
    fn frontpage_panels_map_to_shelves_with_tolerant_optionals() {
        let shelves = parse_frontpage_body(VOD).expect("VOD fixture should parse");
        assert_eq!(shelves.len(), 2);
        assert_eq!(shelves[0].slug, "hladbord");
        assert_eq!(shelves[0].programs[0].program_id, 30_762);
        assert_eq!(shelves[0].programs[0].episode_id.as_deref(), Some("bfblpb"));
        assert_eq!(shelves[0].programs[0].rating, Some(6));
        assert_eq!(shelves[1].programs[0].program_id, 41_000);
        assert_eq!(shelves[1].programs[0].foreign_title, None);
        assert_eq!(shelves[1].programs[0].image_url, None);
    }

    #[test]
    fn program_maps_playback_fields_and_rejects_bad_media_origins() {
        let program = parse_program_body(PROGRAM, 1).expect("program fixture should parse");
        assert_eq!(program.program_id, 30_762);
        assert_eq!(program.episodes.len(), 2);
        let episode = &program.episodes[0];
        assert_eq!(episode.duration_seconds, Some(1_800));
        assert_eq!(
            episode
                .first_run
                .expect("first run")
                .offset()
                .local_minus_utc(),
            0
        );
        assert_eq!(episode.rating, Some(6));
        assert_eq!(episode.subtitles.len(), 1);
        assert!(
            episode
                .stream_url
                .as_deref()
                .is_some_and(|url| url.ends_with("token=opaque"))
        );
        assert_eq!(program.episodes[1].title, "Þáttur");
        assert_eq!(program.episodes[1].stream_url, None);
    }

    #[test]
    fn epg_uses_utc_for_offsetless_reykjavik_wall_times_and_skips_bad_dates() {
        let events = parse_schedule_body(EPG).expect("EPG fixture should parse");
        assert_eq!(events.len(), 2);
        assert_eq!(events[0].start.offset().local_minus_utc(), 0);
        assert_eq!(events[0].start.timestamp_subsec_millis(), 125);
        assert_eq!(events[0].program_id, Some(100));
        assert!(events[0].is_live);
        assert_eq!(events[1].subtitle, None);
        assert_eq!(events[1].end, None);
    }

    #[test]
    fn live_resolver_only_yields_allowlisted_hls() {
        let live = parse_live_body(LIVE).expect("live fixture should parse");
        assert_eq!(
            live.url.as_deref().and_then(stream_url).as_deref(),
            Some("https://ruvlive.akamaized.net/out/v1/example/index.m3u8")
        );
        assert_eq!(
            stream_url("https://ed4.cdn.ruv.is/token/live/d0/ruv/example/index.m3u8").as_deref(),
            Some("https://ed4.cdn.ruv.is/token/live/d0/ruv/example/index.m3u8")
        );
        assert!(stream_url("https://attacker.example/live.m3u8").is_none());
        assert!(stream_url("http://ruvlive.akamaized.net/live.m3u8").is_none());
        assert!(stream_url("https://ruvlive.akamaized.net/not-hls.mp4").is_none());
    }

    #[test]
    fn unified_search_fixtures_map_articles_and_programs() {
        let articles = parse_article_search_body(SEARCH_ARTICLES).expect("article search");
        let programs = parse_vod_search_body(SEARCH_VOD).expect("VOD search");
        assert_eq!(articles.len(), 1);
        assert_eq!(articles[0].title, "Leitarniðurstaða");
        assert_eq!(
            articles[0]
                .published
                .expect("date")
                .offset()
                .local_minus_utc(),
            0
        );
        assert!(articles[0].body.contains("Leitartexti"));
        assert_eq!(programs.len(), 1);
        assert_eq!(programs[0].program_id, 30_762);
        assert_eq!(programs[0].episode_id.as_deref(), Some("bfblpb"));
    }

    #[test]
    fn search_url_percent_encodes_data_instead_of_accepting_query_injection() {
        let url = build_search_url("eldgos &page=99/ís").expect("fixed search URL");
        assert_eq!(url.scheme(), "https");
        assert_eq!(url.host_str(), Some("gql.nyr.ruv.is"));
        let pairs: Vec<_> = url.query_pairs().collect();
        assert_eq!(pairs[0].0, "search");
        assert_eq!(pairs[0].1, "eldgos &page=99/ís");
        assert_eq!(pairs[1], ("page".into(), "1".into()));
        assert_eq!(pairs[2], ("limit".into(), "20".into()));
    }

    #[test]
    fn malformed_graphql_and_oversized_chunks_return_user_safe_errors() {
        let graph_error = br#"{"data":null,"errors":[{"message":"database secret"}]}"#;
        assert_eq!(
            parse_frontpage_body(graph_error),
            Err(AppError::InvalidResponse)
        );
        assert_eq!(
            parse_frontpage_body(b"not json"),
            Err(AppError::InvalidResponse)
        );

        let mut body = vec![0_u8; 4];
        assert_eq!(
            bounded_extend(&mut body, &[1, 2], 5),
            Err(AppError::ResponseTooLarge)
        );
        assert_eq!(body, vec![0_u8; 4]);
        assert!(!AppError::InvalidResponse.to_string().contains("database"));
    }

    #[test]
    fn timestamp_parser_accepts_rfc2822_iso_offsets_and_fractional_wall_time() {
        assert_eq!(
            parse_timestamp("Thu, 06 Aug 2026 12:34:56 +0000")
                .expect("RFC 2822")
                .timestamp(),
            1_786_019_696
        );
        assert_eq!(
            parse_timestamp("2026-08-06T14:34:56+02:00")
                .expect("RFC 3339")
                .timestamp(),
            1_786_019_696
        );
        assert_eq!(
            parse_timestamp("2026-08-06T12:34:56.250")
                .expect("RÚV wall time")
                .timestamp_subsec_millis(),
            250
        );
        assert!(parse_timestamp("2026-99-99").is_none());
    }
}