From 14ee4d276736c1a2f134ab5707731a249afde86f Mon Sep 17 00:00:00 2001 From: Philipp Hofer Date: Thu, 16 Oct 2025 10:53:20 +0200 Subject: [PATCH] add test structs --- Cargo.lock | 1 + Cargo.toml | 1 + src/lib.rs | 109 ++++++++++++++++++++++++++++++++++++++++++----------- src/rss.rs | 4 +- src/web.rs | 42 +++++++++++++++++++-- 5 files changed, 131 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d3ec26..1cd060b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -724,6 +724,7 @@ dependencies = [ "quick-xml", "reqwest", "serde_json", + "thiserror", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index a48b0c7..a13c63c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ reqwest = { version = "0.12", features = ["stream", "json", "rustls-tls"], defau serde_json = "1" chrono = "0.4" quick-xml = "0.38" +thiserror = "2" diff --git a/src/lib.rs b/src/lib.rs index 118741b..bd894e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ mod web; use chrono::DateTime; use serde_json::Value; use std::sync::Arc; +use thiserror::Error; use tokio::{net::TcpListener, sync::RwLock}; pub async fn start( @@ -14,7 +15,9 @@ pub async fn start( listener: TcpListener, ) -> Result<(), Box> { let state = Arc::new(RwLock::new( - Feed::new(title, link, desc, filter_titles).await.unwrap(), + LiveFeed::new(title, link, desc, filter_titles) + .await + .unwrap(), )); web::serve(state, listener).await?; @@ -22,7 +25,57 @@ pub async fn start( Ok(()) } -struct Feed { +#[derive(Error, Debug)] +enum FetchError {} + +trait Feed { + async fn fetch(&mut self) -> Result<(), Box>; +} + +#[cfg(test)] +struct TestFeed { + episodes: Vec, + pub(crate) amount_fetch_calls: usize, +} + +#[cfg(test)] +impl Default for TestFeed { + fn default() -> Self { + Self { + episodes: vec![Broadcast::test()], + amount_fetch_calls: 0, + } + } +} + +#[cfg(test)] +impl rss::ToRss for TestFeed { + fn title(&self) -> &str { + "Test RSS Title" + } + + fn link(&self) -> &str { + "https://test.rss" + } + + fn desc(&self) -> &str { + "Test RSS Desc" + } + + fn episodes(&self) -> &Vec { + &self.episodes + } +} + +#[cfg(test)] +impl Feed for TestFeed { + async fn fetch(&mut self) -> Result<(), Box> { + self.amount_fetch_calls += 1; + Ok(()) + } +} + +struct LiveFeed { episodes: Vec, title: String, link: String, @@ -30,7 +83,27 @@ struct Feed { filter_titles: Vec, } -impl Feed { +impl Feed for LiveFeed { + async fn fetch(&mut self) -> Result<(), Box> { + let broadcasts = self.get_all_broadcasts().await?; + + for broadcast in broadcasts { + if !self.has_broadcast_url(&broadcast) { + if let Some(broadcast) = Broadcast::from_url(broadcast).await.unwrap() { + self.episodes.push(broadcast); + } else { + return Ok(()); + } + } + } + + self.only_keep_last_episodes(); + + Ok(()) + } +} + +impl LiveFeed { async fn new( title: String, link: String, @@ -50,24 +123,6 @@ impl Feed { Ok(ret) } - async fn fetch(&mut self) -> Result<(), Box> { - let broadcasts = self.get_all_broadcasts().await?; - - for broadcast in broadcasts { - if !self.has_broadcast_url(&broadcast) { - if let Some(broadcast) = Broadcast::from_url(broadcast).await.unwrap() { - self.episodes.push(broadcast); - } else { - return Ok(()); - } - } - } - - self.only_keep_last_episodes(); - - Ok(()) - } - fn only_keep_last_episodes(&mut self) { self.episodes = self.episodes.clone().into_iter().rev().take(10).collect(); // only keep last 10 @@ -117,6 +172,18 @@ struct Broadcast { } impl Broadcast { + #[cfg(test)] + fn test() -> Self { + use chrono::Local; + + Self { + url: "test.url".into(), + media_url: "test.media.url".into(), + title: "Test title".into(), + timestamp: Local::now().into(), + } + } + async fn from_url(url: String) -> Result, Box> { let data: Value = reqwest::get(&url).await?.json().await?; let Some(streams) = data["streams"].as_array() else { diff --git a/src/rss.rs b/src/rss.rs index fc507e2..a5148f2 100644 --- a/src/rss.rs +++ b/src/rss.rs @@ -1,4 +1,4 @@ -use crate::{Broadcast, Feed}; +use crate::{Broadcast, LiveFeed}; pub(super) trait ToRss { fn title(&self) -> &str; @@ -39,7 +39,7 @@ pub(super) trait ToRss { } } -impl ToRss for Feed { +impl ToRss for LiveFeed { fn title(&self) -> &str { &self.title } diff --git a/src/web.rs b/src/web.rs index 940a1a2..076b611 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,10 +1,10 @@ -use crate::{rss::ToRss, Feed}; +use crate::{rss::ToRss, Feed, LiveFeed}; use axum::{extract::State, http::HeaderMap, response::IntoResponse, routing::get, Router}; use reqwest::header; use std::sync::Arc; use tokio::{net::TcpListener, sync::RwLock}; -async fn stream_handler(State(state): State>>) -> impl IntoResponse { +async fn stream_handler(State(state): State>>) -> impl IntoResponse { state.write().await.fetch().await.unwrap(); let content = state.read().await.to_rss(); @@ -15,7 +15,7 @@ async fn stream_handler(State(state): State>>) -> impl IntoResp } pub(super) async fn serve( - state: Arc>, + state: Arc>, listener: TcpListener, ) -> Result<(), Box> { let app = Router::new() @@ -26,3 +26,39 @@ pub(super) async fn serve( Ok(()) } + +//#[cfg(test)] +//mod tests { +// use crate::{rss::ToRss, TestFeed}; +// use axum::http::StatusCode; +// use std::sync::Arc; +// use tokio::sync::RwLock; +// +// #[tokio::test] +// async fn serve_serves_rss() { +// let feed = Arc::new(RwLock::new(TestFeed::default())); +// +// let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); +// let addr = listener.local_addr().unwrap(); +// +// tokio::spawn(super::serve(feed.clone(), listener)); +// +// let client = reqwest::Client::new(); +// let resp = client.get(format!("http://{}", addr)).send().await.unwrap(); +// +// assert_eq!(resp.status(), StatusCode::OK); +// assert_eq!( +// resp.headers() +// .get("content-type") +// .unwrap() +// .to_str() +// .unwrap(), +// "application/rss+xml" +// ); +// +// let body = resp.text().await.unwrap(); +// assert_eq!(body, feed.read().await.to_rss()); +// +// assert_eq!(feed.read().await.amount_fetch_calls, 1); +// } +//}