news-flash 3.0.1

Base library for a modern feed reader
Documentation
use crate::NewsFlash;
use crate::config::{Config, ConfigHandlerExt, MockConfigHandlerExt};
use crate::feed_api::{FeedApi, MockFeedApi};
use crate::feed_api_implementations::local::metadata::LocalMetadata;
use crate::models::{Category, CategoryID, Feed, FeedID, NEWSFLASH_TOPLEVEL, Url};
use reqwest::Client;
use test_log::test;

fn mock_config() -> Box<dyn ConfigHandlerExt> {
    let mut config_mock = Box::new(MockConfigHandlerExt::new());
    config_mock.expect_load().returning(|_path| Ok(Config::default()));
    config_mock
}

fn mock_api() -> Box<dyn FeedApi> {
    let mut api = Box::new(MockFeedApi::new());
    api.expect_is_logged_in().returning(|_client| Ok(true));
    api.expect_add_category().returning(|_title, _parent, _client| {
        let uuid = uuid::Uuid::new_v4();
        Ok(CategoryID::from_owned(uuid.to_string()))
    });
    api.expect_add_feed().returning(|url, title, category_id, _client| {
        let feed = Feed {
            feed_id: FeedID::new(url.as_str()),
            label: title.as_deref().unwrap_or("title").to_string(),
            website: None,
            feed_url: Some(url.clone()),
            icon_url: None,
            error_count: 0,
            error_message: None,
        };
        let category = category_id.map(|category_id| Category {
            category_id,
            label: "parent".to_string(),
        });
        Ok((feed, category))
    });
    api.expect_remove_category().returning(|_id, _remove_children, _client| Ok(()));
    api
}

fn build_news_flash(dir: &str) -> NewsFlash {
    if std::fs::exists(dir).unwrap() {
        std::fs::remove_dir_all(dir).unwrap();
    }

    let config_mock = mock_config();
    let api = mock_api();

    NewsFlash::builder()
        .plugin(LocalMetadata::get_id())
        .data_dir(dir)
        .config_dir(dir)
        .config_handler(config_mock)
        .api_mock(api)
        .create()
        .unwrap()
}

#[test(tokio::test)]
async fn remove_category_recursive() {
    let client = Client::new();
    let news_flash = build_news_flash("./test-output/remove_category_recursive");

    let (category_test, _mapping) = news_flash.add_category("test", None, &client).await.unwrap();
    let (category_sub, _mapping) = news_flash.add_category("sub", Some(&category_test.category_id), &client).await.unwrap();
    let (feed, _feed_mapping, _parent, _parent_mapping) = news_flash
        .add_feed(
            &Url::parse("https://example.com/feed").unwrap(),
            Some("feed1".to_string()),
            Some(category_sub.category_id),
            &client,
        )
        .await
        .unwrap();

    let (feeds, _mappings) = news_flash.get_feeds().unwrap();
    assert_eq!(feeds.len(), 1);
    assert_eq!(feeds.first().unwrap().feed_id, feed.feed_id);

    news_flash.remove_category(&category_test.category_id, true, &client).await.unwrap();

    let (categories, mappings) = news_flash.get_categories().unwrap();
    let (feeds, _mappings) = news_flash.get_feeds().unwrap();

    tracing::debug!(?categories);
    tracing::debug!(?mappings);

    assert!(categories.is_empty());
    assert!(mappings.is_empty());
    assert!(feeds.is_empty());
}

#[test(tokio::test)]
async fn remove_category_keep_children() {
    let client = Client::new();
    let news_flash = build_news_flash("./test-output/remove_category_keep_children");

    let (category_test, _mapping) = news_flash.add_category("test", None, &client).await.unwrap();
    let (category_sub, _mapping) = news_flash.add_category("sub", Some(&category_test.category_id), &client).await.unwrap();
    let (feed, _feed_mapping, _parent, _parent_mapping) = news_flash
        .add_feed(
            &Url::parse("https://example.com/feed").unwrap(),
            Some("feed1".to_string()),
            Some(category_sub.category_id.clone()),
            &client,
        )
        .await
        .unwrap();

    news_flash.remove_category(&category_test.category_id, false, &client).await.unwrap();

    let (categories, category_mappings) = news_flash.get_categories().unwrap();
    let (feeds, feed_mappings) = news_flash.get_feeds().unwrap();

    tracing::debug!(?categories);
    tracing::debug!(?category_mappings);

    assert_eq!(categories.len(), 1);
    assert_eq!(category_mappings.len(), 1);
    assert_eq!(feeds.len(), 1);

    assert_eq!(categories.first().unwrap().category_id, category_sub.category_id);
    assert_eq!(category_mappings.first().unwrap().category_id, category_sub.category_id);
    assert_eq!(category_mappings.first().unwrap().parent_id, *NEWSFLASH_TOPLEVEL);

    assert_eq!(feeds.first().unwrap().feed_id, feed.feed_id);
    assert_eq!(feed_mappings.first().unwrap().feed_id, feed.feed_id);
    assert_eq!(feed_mappings.first().unwrap().category_id, category_sub.category_id);
}

#[test(tokio::test)]
async fn add_multiple_categories_with_same_name() {
    let client = Client::new();
    let news_flash = build_news_flash("./test-output/add_multiple_categories_with_same_name");

    let (category_test, _mapping) = news_flash.add_category("test", None, &client).await.unwrap();
    let (category_sub, _mapping) = news_flash.add_category("sub", Some(&category_test.category_id), &client).await.unwrap();
    let (category_sub_test, _mapping) = news_flash.add_category("test", Some(&category_sub.category_id), &client).await.unwrap();

    let (categories, mappings) = news_flash.get_categories().unwrap();

    tracing::debug!(?categories);
    tracing::debug!(?mappings);

    assert_eq!(categories.len(), 3);
    assert_eq!(mappings.len(), 3);

    assert_eq!(mappings.first().unwrap().category_id, category_test.category_id);
    assert_eq!(mappings.first().unwrap().parent_id, *NEWSFLASH_TOPLEVEL);

    assert_eq!(mappings.get(1).unwrap().category_id, category_sub.category_id);
    assert_eq!(mappings.get(1).unwrap().parent_id, category_test.category_id);

    assert_eq!(mappings.get(2).unwrap().category_id, category_sub_test.category_id);
    assert_eq!(mappings.get(2).unwrap().parent_id, category_sub.category_id);
}