diff --git a/koucha/src/db/channel.rs b/koucha/src/db/channel.rs index 22bea40..0ea271b 100644 --- a/koucha/src/db/channel.rs +++ b/koucha/src/db/channel.rs @@ -160,19 +160,71 @@ impl Channel { #[cfg(test)] mod tests { use super::*; - use crate::{Adapter, AdapterBuilder}; + use crate::{ + db::{ + Feed, + User, + }, + test_utils::{ + FEED1, FEED2, CHANNEL_TITLE, CHANNEL_DESC, USERNAME, FEED_TITLE, + FEED_TITLE2, ITEM_GUID, ITEM_GUID2, + setup_adapter, + setup_channel, + }, + }; + use chrono::TimeZone; + + #[test] + fn parse_unparsed_item() { + const CHANNEL_ID: i64 = 1; + let date: DateTime = Utc.with_ymd_and_hms(2020,1,1,0,0,0).unwrap(); - const FEED1: &str = "https://example.com/feed"; - const FEED2: &str = "https://example2.com/feed"; - - async fn setup_adapter() -> Adapter { - AdapterBuilder::new() - .database_url("sqlite::memory:") - .create().await.unwrap() + let raw_channel = UnparsedChannel { + id: CHANNEL_ID, + title: CHANNEL_TITLE.to_string(), + link: FEED1.to_string(), + description: Some(CHANNEL_DESC.to_string()), + last_fetched: Some(date.to_rfc2822()), + }; + let channel = raw_channel.parse().unwrap(); + + assert_eq!(channel.id.0, CHANNEL_ID); + assert_eq!(channel.title, CHANNEL_TITLE); + assert_eq!(channel.link.as_str(), FEED1); + assert_eq!(channel.description, Some(CHANNEL_DESC.to_string())); + assert_eq!(channel.last_fetched, Some(date)); } #[tokio::test] - async fn create_channel() { + async fn get_all() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let url1 = Url::parse(FEED1).unwrap(); + let url2 = Url::parse(FEED2).unwrap(); + Channel::get_or_create(pool, url1).await.unwrap(); + Channel::get_or_create(pool, url2).await.unwrap(); + + let channels = Channel::get_all(pool).await.unwrap(); + assert_eq!(channels.len(), 2); + } + + #[tokio::test] + async fn get() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let channel_a = setup_channel(pool).await; + + let channel_b = Channel::get(pool, channel_a.id()).await.unwrap(); + + assert_eq!(channel_a.id, channel_b.id); + assert_eq!(channel_a.title, channel_b.title); + assert_eq!(channel_a.link, channel_b.link); + assert_eq!(channel_a.last_fetched, channel_b.last_fetched); + assert_eq!(channel_a.description, channel_b.description); + } + + #[tokio::test] + async fn create() { let adapter = setup_adapter().await; let pool = adapter.get_pool(); let url_feed = Url::parse(FEED1).unwrap(); @@ -213,4 +265,54 @@ mod tests { assert_eq!(channels.len(), 2); } + + #[tokio::test] + async fn get_feed_channels() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let channel = setup_channel(pool).await; + + let user = User::create(pool, USERNAME).await.unwrap(); + let feed1 = Feed::create(pool, user.id(), FEED_TITLE).await.unwrap(); + let feed2 = Feed::create(pool, user.id(), FEED_TITLE2).await.unwrap(); + + feed1.add_channel(pool, channel.id).await.unwrap(); + feed2.add_channel(pool, channel.id).await.unwrap(); + + let fc_list = channel.get_feed_channels(pool).await.unwrap(); + + assert_eq!(fc_list.len(), 2); + } + + #[tokio::test] + async fn get_channels() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let channel = setup_channel(pool).await; + + let user = User::create(pool, USERNAME).await.unwrap(); + let feed1 = Feed::create(pool, user.id(), FEED_TITLE).await.unwrap(); + let feed2 = Feed::create(pool, user.id(), FEED_TITLE2).await.unwrap(); + + feed1.add_channel(pool, channel.id).await.unwrap(); + feed2.add_channel(pool, channel.id).await.unwrap(); + + let fc_list = channel.get_feed_channels(pool).await.unwrap(); + + assert_eq!(fc_list.len(), 2); + } + + #[tokio::test] + async fn get_items() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let channel = setup_channel(pool).await; + + Item::get_or_create(pool, channel.id(), ITEM_GUID).await.unwrap(); + Item::get_or_create(pool, channel.id(), ITEM_GUID2).await.unwrap(); + + let items = channel.get_items(pool).await.unwrap(); + + assert_eq!(items.len(), 2); + } } diff --git a/koucha/src/db/feed.rs b/koucha/src/db/feed.rs index bb4783d..a769b1b 100644 --- a/koucha/src/db/feed.rs +++ b/koucha/src/db/feed.rs @@ -124,39 +124,96 @@ impl Feed { mod tests { use super::*; use crate::{ - Adapter, - AdapterBuilder, db::User, + test_utils::{ + FEED_TITLE, USERNAME, FEED1, FEED2, + setup_adapter, + setup_feed, + setup_channel, + } }; + use reqwest::Url; - async fn setup_adapter() -> Adapter { - AdapterBuilder::new() - .database_url("sqlite::memory:") - .create().await.unwrap() + #[test] + fn parse() { + const FID: i64 = 1; + let uf = UnparsedFeed { + id: FID, + title: FEED_TITLE.to_string(), + }; + + let f = uf.parse().unwrap(); + + assert_eq!(f.id.0, FID); + assert_eq!(f.title, FEED_TITLE); + } + + #[tokio::test] + async fn get() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let feed = setup_feed(pool).await; + + let gotten_feed = Feed::get(pool, feed.id).await.unwrap(); + + assert_eq!(feed.id, gotten_feed.id); + assert_eq!(feed.title, gotten_feed.title); } #[tokio::test] - async fn create_feed() { + async fn create() { let adapter = setup_adapter().await; let pool = adapter.get_pool(); - let user = User::create(pool, "Alice").await.unwrap(); + let user = User::create(pool, USERNAME).await.unwrap(); + let feed = Feed::create(pool, user.id(), FEED_TITLE).await.unwrap(); - let feed = Feed::create(pool, user.id(), "Tech News").await.unwrap(); - - assert_eq!(feed.title(), "Tech News"); assert!(feed.id().0 > 0); + assert_eq!(feed.title(), FEED_TITLE); } #[tokio::test] - async fn test_update_title() { + async fn update_title() { + const NEW_FEED_TITLE: &str = "My NEW feed!"; let adapter = setup_adapter().await; let pool = adapter.get_pool(); - let user = User::create(pool, "Alice").await.unwrap(); - let feed = Feed::create(pool, user.id(), "Tech News").await.unwrap(); + let feed = setup_feed(pool).await; - Feed::update_title(pool, feed.id(), "Technology").await.unwrap(); + Feed::update_title(pool, feed.id(), NEW_FEED_TITLE).await.unwrap(); let updated = Feed::get(pool, feed.id()).await.unwrap(); - assert_eq!(updated.title(), "Technology"); + assert_eq!(updated.title(), NEW_FEED_TITLE); + } + + #[tokio::test] + async fn add_channel() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let feed = setup_feed(pool).await; + let channel = setup_channel(pool).await; + + feed.add_channel(pool, channel.id()).await.unwrap(); + + let channels = feed.get_channels(pool).await.unwrap(); + let gotten_channel = &channels[0]; + + assert_eq!(gotten_channel.id().0, channel.id().0); + } + + #[tokio::test] + async fn get_channels() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let feed = setup_feed(pool).await; + let url1 = Url::parse(FEED1).unwrap(); + let channel1 = Channel::get_or_create(pool, url1).await.unwrap(); + let url2 = Url::parse(FEED2).unwrap(); + let channel2 = Channel::get_or_create(pool, url2).await.unwrap(); + + feed.add_channel(pool, channel1.id()).await.unwrap(); + feed.add_channel(pool, channel2.id()).await.unwrap(); + + let channels = feed.get_channels(pool).await.unwrap(); + + assert_eq!(channels.len(), 2); } } diff --git a/koucha/src/db/feed_channel.rs b/koucha/src/db/feed_channel.rs index 4421b88..19656cb 100644 --- a/koucha/src/db/feed_channel.rs +++ b/koucha/src/db/feed_channel.rs @@ -56,21 +56,29 @@ mod tests { use super::*; use reqwest::Url; use crate::{ - Adapter, - AdapterBuilder, db::{ Channel, FeedId, User - } + }, + test_utils::{ + FEED1, setup_adapter, + }, }; - const FEED1: &str = "https://example.com/feed"; + #[test] + fn parse() { + const CID: i64 = 1; + const FID: i64 = 1; + let ufc = UnparsedFeedChannel { + channel_id: CID, + feed_id: FID, + }; - async fn setup_adapter() -> Adapter { - AdapterBuilder::new() - .database_url("sqlite::memory:") - .create().await.unwrap() + let fc = ufc.parse().unwrap(); + + assert_eq!(fc.channel_id.0, CID); + assert_eq!(fc.feed_id.0, FID); } // FeedChannel Tests diff --git a/koucha/src/db/item.rs b/koucha/src/db/item.rs index 39a1b49..cb25331 100644 --- a/koucha/src/db/item.rs +++ b/koucha/src/db/item.rs @@ -97,32 +97,42 @@ impl Item { #[cfg(test)] mod tests { use super::*; - use reqwest::Url; - use crate::{ - Adapter, - AdapterBuilder, - db::{ - Channel, - } + use crate::test_utils::{ + ITEM_GUID, ITEM_TITLE, ITEM_DESC, ITEM_CONT, + setup_adapter, + setup_channel, }; + use chrono::TimeZone; - const FEED1: &str = "https://example.com/feed"; - const ITEM_GUID: &str = "item-guid"; + // UnparsedItem tests + #[test] + fn parse_unparsed_item() { + const ITEM_ID: i64 = 1; + const CHANNEL_ID: i64 = 1; - async fn setup_adapter() -> Adapter { - AdapterBuilder::new() - .database_url("sqlite::memory:") - .create().await.unwrap() - } + let date: DateTime = Utc.with_ymd_and_hms(2020,1,1,0,0,0).unwrap(); + let raw_item = UnparsedItem { + id: ITEM_ID, + channel_id: CHANNEL_ID, + fetched_at: Some(date.to_rfc2822()), + title: Some(ITEM_TITLE.to_string()), + description: Some(ITEM_DESC.to_string()), + content: Some(ITEM_CONT.to_string()), + }; + let item = raw_item.parse().unwrap(); + + assert_eq!(item.id.0, ITEM_ID); + assert_eq!(item.channel_id.0, CHANNEL_ID); + assert_eq!(item.fetched_at, Some(date)); + assert_eq!(item.title, Some(ITEM_TITLE.to_string())); + assert_eq!(item.description, Some(ITEM_DESC.to_string())); + assert_eq!(item.content, Some(ITEM_CONT.to_string())); - async fn setup_channel(pool: &AdapterPool) -> Channel { - let url = Url::parse(FEED1).unwrap(); - Channel::get_or_create(pool, url).await.unwrap() } // Item Tests #[tokio::test] - pub async fn get_or_create_duplicate() { + async fn get_or_create_duplicate() { let adapter = setup_adapter().await; let pool = adapter.get_pool(); let channel = setup_channel(pool).await; diff --git a/koucha/src/db/user.rs b/koucha/src/db/user.rs index 05b7a4b..63f24d8 100644 --- a/koucha/src/db/user.rs +++ b/koucha/src/db/user.rs @@ -39,6 +39,7 @@ impl User { user } + pub async fn get_all(pool: &AdapterPool) -> Result> { let users: Result> = sqlx::query_as!( UnparsedUser, @@ -88,93 +89,118 @@ impl User { #[cfg(test)] mod tests { use super::*; - use crate::{AdapterBuilder, Adapter}; + use crate::{ + db::Feed, + test_utils::{ + USERNAME, USERNAME2, FEED_TITLE, FEED_TITLE2, + setup_adapter, + }, + }; - async fn setup_adapter() -> Adapter { - AdapterBuilder::new() - .database_url("sqlite::memory:") - .create().await.unwrap() + #[test] + fn parse() { + const UID: i64 = 1; + let unparsed_user = UnparsedUser { + id: UID, + name: USERNAME.to_string(), + }; + + let user = unparsed_user.parse().unwrap(); + assert_eq!(user.id.0, UID); + assert_eq!(user.name, USERNAME); + } + + #[tokio::test] + async fn get() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let new_user = User::create(pool, USERNAME).await.unwrap(); + + let fetched_user = User::get(pool, new_user.id).await.unwrap(); + assert_eq!(fetched_user.name, USERNAME); + assert_eq!(fetched_user.id.0, 1); } #[tokio::test] - async fn get_user() { + async fn get_all() { let adapter = setup_adapter().await; let pool = adapter.get_pool(); + User::create(pool, USERNAME).await.unwrap(); + User::create(pool, USERNAME2).await.unwrap(); - let new_user = User::create(pool, "Alice").await.unwrap(); - - let fetched_user = User::get(pool, new_user.id).await.unwrap(); - assert_eq!(fetched_user.name, "Alice"); - assert!(fetched_user.id.0 > 0); + let users = User::get_all(pool).await.unwrap(); + assert_eq!(users.len(), 2); + assert!(users.iter().any(|u| u.name == USERNAME)); + assert!(users.iter().any(|u| u.name == USERNAME2)); } #[tokio::test] async fn create_user() { let adapter = setup_adapter().await; - let pool = adapter.get_pool(); + let pool = adapter.get_pool(); - let user = User::create(pool, "Alice").await.unwrap(); + let user = User::create(pool, USERNAME).await.unwrap(); - assert_eq!(user.name, "Alice"); - assert!(user.id.0 > 0); + assert_eq!(user.name, USERNAME); + assert_eq!(user.id.0, 1); } - + #[tokio::test] async fn create_duplicate_user() { let adapter = setup_adapter().await; - let pool = adapter.get_pool(); + let pool = adapter.get_pool(); - let _user = User::create(pool, "Alice").await.unwrap(); - let duplicate_user = User::create(pool, "Alice").await; + User::create(pool, USERNAME).await.unwrap(); + let duplicate_user = User::create(pool, USERNAME).await; assert!(duplicate_user.is_err()); } - - #[tokio::test] - async fn get_all_users() { - let adapter = setup_adapter().await; - let pool = adapter.get_pool(); - - User::create(pool, "Alice").await.unwrap(); - User::create(pool, "Bob").await.unwrap(); - - let users = User::get_all(pool).await.unwrap(); - - assert_eq!(users.len(), 2); - assert!(users.iter().any(|u| u.name == "Alice")); - assert!(users.iter().any(|u| u.name == "Bob")); - } #[tokio::test] async fn update_name() { + const NEW_USERNAME: &str = "Alicia"; + assert!(NEW_USERNAME != USERNAME); let adapter = setup_adapter().await; - let pool = adapter.get_pool(); + let pool = adapter.get_pool(); - let user = User::create(pool, "Alice").await.unwrap(); - User::update_name(pool, user.id, "Alicia").await.unwrap(); + let user = User::create(pool, USERNAME).await.unwrap(); + User::update_name(pool, user.id, NEW_USERNAME).await.unwrap(); let updated = User::get(pool, user.id).await.unwrap(); - assert_eq!(updated.name, "Alicia"); + assert_eq!(updated.name, NEW_USERNAME); } - + #[tokio::test] async fn update_name_to_duplicate() { let adapter = setup_adapter().await; - let pool = adapter.get_pool(); + let pool = adapter.get_pool(); - let alice = User::create(pool, "Alice").await.unwrap(); - let _sam = User::create(pool, "Sam").await.unwrap(); - let status = User::update_name(pool, alice.id, "Sam").await; + let user1 = User::create(pool, USERNAME).await.unwrap(); + User::create(pool, USERNAME2).await.unwrap(); + let status = User::update_name(pool, user1.id, USERNAME2).await; assert!(status.is_err()); } + #[tokio::test] + async fn get_feeds() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let user = User::create(pool, USERNAME).await.unwrap(); + Feed::create(pool, user.id, FEED_TITLE).await.unwrap(); + Feed::create(pool, user.id, FEED_TITLE2).await.unwrap(); + + let feeds = user.get_feeds(pool).await.unwrap(); + assert_eq!(feeds.len(), 2); + assert!(feeds.iter().any(|f| f.title() == FEED_TITLE)); + assert!(feeds.iter().any(|f| f.title() == FEED_TITLE2)); + } + #[tokio::test] async fn get_feeds_empty() { let adapter = setup_adapter().await; - let pool = adapter.get_pool(); - - let user = User::create(pool, "Alice").await.unwrap(); + let pool = adapter.get_pool(); + let user = User::create(pool, USERNAME).await.unwrap(); let feeds = user.get_feeds(pool).await.unwrap(); assert_eq!(feeds.len(), 0); diff --git a/koucha/src/fetch.rs b/koucha/src/fetch.rs index b71b30c..62e4505 100644 --- a/koucha/src/fetch.rs +++ b/koucha/src/fetch.rs @@ -70,10 +70,12 @@ impl FetchedRSSChannel { let rss_channel = rss::Channel::read_from(&bytestream[..])?; - Ok(Some(FetchedRSSChannel::parse(rss_channel)?)) + let now = Utc::now(); + + Ok(Some(FetchedRSSChannel::parse(rss_channel, now)?)) } - fn parse(rss: rss::Channel) -> Result { + fn parse(rss: rss::Channel, fetched_at: DateTime) -> Result { Ok(FetchedRSSChannel { title: rss.title, link: Url::parse(&rss.link)?, @@ -81,7 +83,73 @@ impl FetchedRSSChannel { items: rss.items.into_iter().map(FetchedRSSItem::parse).collect(), - fetched_at: Utc::now(), + fetched_at: fetched_at, }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{ + ITEM_TITLE, ITEM_GUID, ITEM_GUID2, ITEM_DESC, ITEM_CONT, + CHANNEL_TITLE, CHANNEL_DESC, FEED1 + }; + use chrono::TimeZone; + + fn create_guid(value: String) -> rss::Guid { + rss::Guid { value, permalink: false } + } + + fn create_item(guid: rss::Guid) -> rss::Item { + rss::ItemBuilder::default() + .title(ITEM_TITLE.to_string()) + .guid(guid) + .description(ITEM_DESC.to_string()) + .content(ITEM_CONT.to_string()) + .build() + } + + fn create_channel(items: Vec) -> rss::Channel { + rss::ChannelBuilder::default() + .title(CHANNEL_TITLE.to_string()) + .description(CHANNEL_DESC.to_string()) + .link(FEED1.to_string()) + .items(items) + .build() + } + + #[test] + fn parse_item() { + let rss_guid = create_guid(ITEM_GUID.to_string()); + let rss_item = create_item(rss_guid); + let item = FetchedRSSItem::parse(rss_item); + + assert_eq!(item.guid, ITEM_GUID); + assert_eq!(item.title, ITEM_TITLE); + assert_eq!(item.description, ITEM_DESC); + assert_eq!(item.content, ITEM_CONT); + } + + #[test] + fn parse_feed() { + let rss_guid = create_guid(ITEM_GUID.to_string()); + let rss_guid2 = create_guid(ITEM_GUID2.to_string()); + let rss_item = create_item(rss_guid); + let rss_item2 = create_item(rss_guid2); + + let rss_channel = create_channel([rss_item, rss_item2].to_vec()); + + let date: DateTime = Utc.with_ymd_and_hms(2020,1,1,0,0,0).unwrap(); + + let channel = FetchedRSSChannel::parse(rss_channel, date).unwrap(); + + assert_eq!(channel.title, CHANNEL_TITLE); + assert_eq!(channel.link.as_str(), FEED1); + assert_eq!(channel.description, CHANNEL_DESC); + assert_eq!(channel.fetched_at, date); + assert_eq!(channel.items.len(), 2); + assert!(channel.items.iter().any(|i| i.guid() == ITEM_GUID)); + assert!(channel.items.iter().any(|i| i.guid() == ITEM_GUID2)); + } +} diff --git a/koucha/src/lib.rs b/koucha/src/lib.rs index 1e29303..a9de79f 100644 --- a/koucha/src/lib.rs +++ b/koucha/src/lib.rs @@ -5,6 +5,9 @@ type Result = std::result::Result>; pub mod db; pub mod fetch; +#[cfg(test)] +pub mod test_utils; + pub struct AdapterPool(sqlx::SqlitePool); pub struct AdapterClient(reqwest::Client); diff --git a/koucha/src/test_utils.rs b/koucha/src/test_utils.rs new file mode 100644 index 0000000..5f2cb9b --- /dev/null +++ b/koucha/src/test_utils.rs @@ -0,0 +1,43 @@ +#![cfg(test)] + +use crate::{ + Adapter, + AdapterBuilder, + AdapterPool, + db::{ + Channel, + Feed, + User, + } +}; +use reqwest::Url; + +pub const FEED1: &str = "https://example.com/feed"; +pub const FEED2: &str = "https://example2.com/feed"; +pub const USERNAME: &str = "Alice"; +pub const USERNAME2: &str = "Bob"; +pub const FEED_TITLE: &str = "My Feed!"; +pub const FEED_TITLE2: &str = "My Second Feed!"; +pub const CHANNEL_TITLE: &str = "My Channel!"; +pub const CHANNEL_DESC: &str = "My Channel's description"; +pub const ITEM_GUID: &str = "item-guid"; +pub const ITEM_GUID2: &str = "item-guid2"; +pub const ITEM_TITLE: &str = "My Item!"; +pub const ITEM_DESC: &str = "My Item's description"; +pub const ITEM_CONT: &str = "The content of my Item"; + +pub async fn setup_adapter() -> Adapter { + AdapterBuilder::new() + .database_url("sqlite::memory:") + .create().await.unwrap() +} + +pub async fn setup_channel(pool: &AdapterPool) -> Channel { + let url = Url::parse(FEED1).unwrap(); + Channel::get_or_create(pool, url).await.unwrap() +} + +pub async fn setup_feed(pool: &AdapterPool) -> Feed { + let user = User::create(pool, USERNAME).await.unwrap(); + Feed::create(pool, user.id(), FEED_TITLE).await.unwrap() +}