Menu
BifrOSt-Apps
publicLatest 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());
}
}