feedly_api 0.5.0

rust implementation of the feedly REST API
Documentation
use super::models::{Entry, SubscriptionInput, Tag};
use super::FeedlyApi;
use chrono::Utc;

fn read_secrets() -> (String, String) {
    (String::from(""), String::from(""))
}

fn generate_api() -> FeedlyApi {
    FeedlyApi::new(
        String::from(""),
        String::from(""),
        String::from(""),
        String::from(""),
        Utc::now(),
    )
    .unwrap()
}

#[tokio::test(flavor = "current_thread")]
async fn login_url() {
    let (id, secret) = self::read_secrets();
    let url = FeedlyApi::login_url(&id, &secret).unwrap();
    let expected = format!("http://cloud.feedly.com/v3/auth/auth?client_secret={}&client_id={}&redirect_uri=http://localhost/&scope=https://cloud.feedly.com/subscriptions&response_type=code&state=getting_code", secret, id);
    assert_eq!(url.as_str(), &expected);
}

#[test]
fn parse_expiration_date() {
    let access_token_expires = FeedlyApi::parse_expiration_date("604800").unwrap();
    assert!(access_token_expires > Utc::now());
}

#[tokio::test(flavor = "current_thread")]
async fn get_token() {
    let (id, secret) = self::read_secrets();
    let auth_code = String::from("");
    let acces_token =
        FeedlyApi::request_auth_token(&id, &secret, auth_code, &reqwest::Client::new())
            .await
            .unwrap();

    assert_eq!(
        acces_token.state,
        Some(String::from("feedly-api rust crate"))
    );
    assert!(acces_token.expires_in > 600_000);
}

#[tokio::test(flavor = "current_thread")]
async fn subscriptions() {
    let api = self::generate_api();

    // get subscriptions
    let subscriptions = api.get_subsriptions(&reqwest::Client::new()).await.unwrap();
    assert_eq!(
        subscriptions.get(0).unwrap().title,
        Some("Planet GNOME".into())
    );
    let count_before = subscriptions.len();

    // add new subscription
    let new_subscription = SubscriptionInput {
        id: String::from("feed/https://rss.golem.de/rss.php?feed=ATOM1.0"),
        title: Some(String::from("Golem")),
        categories: None,
    };
    api.add_subscription(new_subscription, &reqwest::Client::new())
        .await
        .unwrap();
    let subscriptions = api.get_subsriptions(&reqwest::Client::new()).await.unwrap();
    assert_eq!(
        subscriptions.get(1).unwrap().title.as_deref(),
        Some("Golem")
    );

    // rename added subscription
    let update_subscription = SubscriptionInput {
        id: String::from("feed/https://rss.golem.de/rss.php?feed=ATOM1.0"),
        title: Some(String::from("GolemXYZ")),
        categories: None,
    };
    api.update_subscriptions(vec![update_subscription], &reqwest::Client::new())
        .await
        .unwrap();
    let subscriptions = api.get_subsriptions(&reqwest::Client::new()).await.unwrap();
    assert_eq!(
        subscriptions.get(1).unwrap().title.as_deref(),
        Some("GolemXYZ")
    );

    // delete added subscription
    api.delete_subscription(&subscriptions.get(1).unwrap().id, &reqwest::Client::new())
        .await
        .unwrap();
    let subscriptions = api.get_subsriptions(&reqwest::Client::new()).await.unwrap();
    assert_eq!(subscriptions.len(), count_before);
}

#[tokio::test(flavor = "current_thread")]
async fn tags() {
    let api = self::generate_api();
    let entry_id = "vW7ltq/KF6A6KAnuQylk2Q7xpz+R7bniO3psKBctmBE=_1646c2afacc:11c13a7:b83afde1";

    // get tags
    let tags = api.get_tags(&reqwest::Client::new()).await.unwrap();
    assert_eq!(tags.get(1).unwrap().label, Some(String::from("test")));

    // create new tag and tag entry with it
    let new_tag_name = "testTag123";
    let new_tag_id = api
        .generate_tag_id(new_tag_name, &reqwest::Client::new())
        .await
        .unwrap();
    api.tag_entry(entry_id, vec![&new_tag_id], &reqwest::Client::new())
        .await
        .unwrap();

    let tags = api.get_tags(&reqwest::Client::new()).await.unwrap();
    let tag = tags.get(2).unwrap();
    assert_eq!(tag.label, Some(String::from(new_tag_name)));

    let tag_id = &tag.id;

    let entries = api
        .get_entries(vec![entry_id], &reqwest::Client::new())
        .await
        .unwrap();
    let entry: &Entry = entries.get(0).as_ref().unwrap();
    let tags: &Vec<Tag> = entry.tags.as_ref().unwrap();
    let tag: &Tag = tags.get(1).as_ref().unwrap();
    assert_eq!(&tag.id, tag_id);

    // remove tag from entry
    api.untag_entries(vec![entry_id], vec![tag_id], &reqwest::Client::new())
        .await
        .unwrap();

    let entries = api
        .get_entries(vec![entry_id], &reqwest::Client::new())
        .await
        .unwrap();
    let entry: &Entry = entries.get(0).as_ref().unwrap();
    let tags: &Vec<Tag> = entry.tags.as_ref().unwrap();
    assert!(tags.get(1).is_none());

    // delte tag
    api.delete_tags(vec![tag_id], &reqwest::Client::new())
        .await
        .unwrap();

    let tags = api.get_tags(&reqwest::Client::new()).await.unwrap();
    assert!(tags.get(2).is_none());
}

#[tokio::test(flavor = "current_thread")]
async fn streams() {
    let api = self::generate_api();

    let stream = api
        .get_stream(
            "feed/http://planet.gnome.org/rss20.xml",
            None,
            None,
            None,
            None,
            None,
            &reqwest::Client::new(),
        )
        .await
        .unwrap();

    assert!(stream.items.get(0).is_some());
}

#[tokio::test(flavor = "current_thread")]
async fn categories() {
    let api = self::generate_api();

    let categories = api.get_categories(&reqwest::Client::new()).await.unwrap();
    assert!(categories.get(0).is_some());
}

#[tokio::test(flavor = "current_thread")]
async fn discover_news() {
    let client = reqwest::Client::new();
    let search_result = FeedlyApi::search_feedly_cloud(&client, "golem", Some(2), Some("de_DE"))
        .await
        .unwrap();

    assert_eq!(
        search_result.related,
        Some(vec!["tech".to_owned(), "it".to_owned()])
    );
}

#[tokio::test(flavor = "current_thread")]
async fn discover_cooking() {
    let client = reqwest::Client::new();
    let search_result = FeedlyApi::search_feedly_cloud(&client, "cooking", Some(5), Some("en_EN"))
        .await
        .unwrap();

    assert_eq!(
        search_result.related,
        Some(vec!["food".to_owned(), "recipes".to_owned()])
    );
}

#[tokio::test(flavor = "current_thread")]
async fn manually_deserialize_faulty_stream() {
    let stream_data = r#"
{
    "id": "feed/https://www.theverge.com/rss/full.xml",
    "title": "The Verge -  All Posts",
    "direction": "ltr",
    "continuation": "gRtwnDeqCDpZ42bXE9Sp7dNhm4R6NsipqFVbXn2XpDA=_13fb9d6f274:2ac9c5:f5718180",
    "self": [
        {
        " href": "https://cloud.feedly.com/reader/3/stream/contents/feed%2Fhttp%3A%2F%2Fwww.theverge.com%2Frss%2Ffull.xml?n=20&unreadOnly=true"
        }
    ],
    "alternate": [
        {
        "href": "http://www.theverge.com/",
        "type": "text/html"
        }
    ],
    "updated": 1367539068016,
    "items": [
        {
        "id": "gRtwnDeqCDpZ42bXE9Sp7dNhm4R6NsipqFVbXn2XpDA=_13fb9d6f274:2ac9c5:f5718180",
        "unread": true,
        "categories": [
            {
            "id": "user/c805fcbf-3acf-4302-a97e-d82f9d7c897f/category/tech",
            "label": "tech"
            }
        ],
        "tags": [
            {
            "id": "user/c805fcbf-3acf-4302-a97e-d82f9d7c897f/tag/inspiration",
            "label": "inspiration"
            }
        ],
        "tags": [
            {
            "id": "user/c805fcbf-3acf-4302-a97e-d82f9d7c897f/tag/inspiration2",
            "label": "inspiration2"
            }
        ],
        "title": "NBC's reviled sci-fi drama 'Heroes' may get a second lease on life as Xbox Live exclusive",
        "published": 1367539068016,
        "updated": 1367539068016,
        "crawled": 1367539068016,
        "alternate": [
            {
            "href": "http://www.theverge.com/2013/4/17/4236096/nbc-heroes-may-get-a-second-lease-on-life-on-xbox-live",
            "type": "text/html"
            }
        ],
        "content": {
            "direction": "ltr",
            "content": "..."
        },
        "author": "Nathan Ingraham",
        "origin": {
            "streamId": "feed/http://www.theverge.com/rss/full.xml",
            "title": "The Verge -  All Posts",
            "htmlUrl": "http://www.theverge.com/"
        },
        "engagement": 15
        },
        {
        "id": "gRtwnDeqCDpZ42bXE9Sp7dNhm4R6NsipqFVbXn2XpDA=_13fb9d6f274:2ac9c5:f5718182",
        "unread": true,
        "categories": [
            {
            "id": "user/c805fcbf-3acf-4302-a97e-d82f9d7c897f/category/tech",
            "label": "tech"
            }
        ],
        "tags": [
            {
            "id": "user/c805fcbf-3acf-4302-a97e-d82f9d7c897f/tag/inspiration",
            "label": "inspiration"
            }
        ],
        "title": "Senate rejects bipartisan gun control measure for background checks despite broad public support",
        "published": 1367539068016,
        "updated": 1367539068016,
        "crawled": 1367539068016,
        "alternate": [
            {
            "href": "http://www.theverge.com/2013/4/17/4236136/senate-rejects-gun-control-amendment",
            "type": "text/html"
            }
        ],
        "content": {
            "direction": "ltr",
            "content": "...html content..."
        },
        "author": "T.C. Sottek",
        "origin": {
            "streamId": "feed/http://www.theverge.com/rss/full.xml",
            "title": "The Verge -  All Posts",
            "htmlUrl": "http://www.theverge.com/"
        },
        "engagement": 39
        }
    ]
}
    "#;

    // deserializing to struct should fail because of double 'tags' field
    let stream: Result<crate::Stream, serde_json::Error> = serde_json::from_str(stream_data);
    assert_eq!(stream.is_err(), true);

    let stream = crate::Stream::manual_deserialize(stream_data).unwrap();
    assert_eq!(&stream.id, "feed/https://www.theverge.com/rss/full.xml");
    assert_eq!(stream.items.len(), 2);

    let first_entry = &stream.items[0];
    let second_entry = &stream.items[1];

    assert_eq!(
        &first_entry.id,
        "gRtwnDeqCDpZ42bXE9Sp7dNhm4R6NsipqFVbXn2XpDA=_13fb9d6f274:2ac9c5:f5718180"
    );
    assert_eq!(
        &second_entry.id,
        "gRtwnDeqCDpZ42bXE9Sp7dNhm4R6NsipqFVbXn2XpDA=_13fb9d6f274:2ac9c5:f5718182"
    );

    // if a field exists more than once the last occurance is used
    assert_eq!(
        &first_entry.tags.as_ref().unwrap()[0].id,
        "user/c805fcbf-3acf-4302-a97e-d82f9d7c897f/tag/inspiration2"
    );
}