boosty_api 0.27.0

API client for interacting with Boosty platform
Documentation
use std::fs;

use boosty_api::{api_client::ApiClient, error::ApiError};
use reqwest::{Client, header::CONTENT_TYPE};
use serde_json::{Value, json};

use crate::helpers::{api_path, setup};

mod helpers;

#[tokio::test]
async fn test_get_post_unauthorized() {
    let (mut server, base) = setup().await;
    let client = ApiClient::new(Client::new(), &base);

    let blog = "blogx";
    let post_id = "pid";
    let path = api_path(&format!("blog/{blog}/post/{post_id}"));

    server
        .mock("GET", path.as_str())
        .with_status(401)
        .create_async()
        .await;

    let res = client.get_post(blog, post_id).await;
    assert!(matches!(res, Err(ApiError::Unauthorized)));
}

#[tokio::test]
async fn test_get_post_invalid_json() {
    let (mut server, base) = setup().await;
    let client = ApiClient::new(Client::new(), &base);

    let blog = "blog";
    let post_id = "p";
    let path = api_path(&format!("blog/{blog}/post/{post_id}"));

    server
        .mock("GET", path.as_str())
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body("not a valid json")
        .create_async()
        .await;

    let res = client.get_post(blog, post_id).await;
    assert!(matches!(res, Err(ApiError::JsonParseDetailed { .. })));
}

#[tokio::test]
async fn test_get_post_not_available_but_no_refresh() {
    let (mut server, base) = setup().await;
    let client = ApiClient::new(Client::new(), &base);

    let blog = "blog";
    let post_id = "99";
    let path = api_path(&format!("blog/{blog}/post/{post_id}"));

    let raw = fs::read_to_string("tests/fixtures/api_response_video_image.json").unwrap();
    let mut value: Value = serde_json::from_str(&raw).unwrap();
    value["id"] = Value::String(post_id.to_string());
    value["title"] = Value::String("Unavailable but returned".to_string());
    let body = value.to_string();

    server
        .mock("GET", path.as_str())
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(body)
        .create_async()
        .await;

    let result = client.get_post(blog, post_id).await.unwrap();
    assert_eq!(result.id, "99");
    assert_eq!(result.title, "Unavailable but returned");
}

#[tokio::test]
async fn test_get_post_with_refresh() {
    let (mut server, base) = setup().await;
    let req_client = Client::new();
    let client = ApiClient::new(req_client.clone(), &base);

    client
        .set_refresh_token_and_device_id("old_refresh", "device123")
        .await
        .unwrap();

    let blog = "blog";
    let post_id = "100";
    let api_get_path = api_path(&format!("blog/{blog}/post/{post_id}"));

    let raw = fs::read_to_string("tests/fixtures/api_response_video_image.json").unwrap();
    let mut first_value: Value = serde_json::from_str(&raw).unwrap();
    first_value["id"] = Value::String(post_id.to_string());
    first_value["title"] = Value::String("Old Title".to_string());
    let first_body = first_value.to_string();

    server
        .mock("GET", api_get_path.as_str())
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(first_body)
        .expect(1)
        .create_async()
        .await;

    let oauth_resp = json!({
        "access_token": "new_access_token",
        "refresh_token": "new_refresh_token",
        "expires_in": 3600
    })
    .to_string();
    server
        .mock("POST", "/oauth/token/")
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(oauth_resp)
        .expect(1)
        .create_async()
        .await;

    let mut second_value: Value = serde_json::from_str(&raw).unwrap();
    second_value["id"] = Value::String(post_id.to_string());
    second_value["title"] = Value::String("New Title".to_string());
    let second_body = second_value.to_string();

    server
        .mock("GET", api_get_path.as_str())
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(second_body)
        .expect(1)
        .create_async()
        .await;

    let result = client.get_post(blog, post_id).await.unwrap();
    assert_eq!(result.id, "100");
    assert_eq!(result.title, "Old Title");
}

#[tokio::test]
async fn test_get_post_refresh_error() {
    let (mut server, base) = setup().await;
    let client = ApiClient::new(Client::new(), &base);

    client
        .set_refresh_token_and_device_id("r", "d")
        .await
        .unwrap();

    let blog = "b";
    let post_id = "77";
    let api_get_path = api_path(&format!("blog/{blog}/post/{post_id}"));

    let raw = fs::read_to_string("tests/fixtures/api_response_video_image.json").unwrap();
    let mut value: Value = serde_json::from_str(&raw).unwrap();
    value["id"] = Value::String(post_id.to_string());
    value["title"] = Value::String("Will fail refresh".to_string());
    let body = value.to_string();

    server
        .mock("GET", api_get_path.as_str())
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(body)
        .expect(1)
        .create_async()
        .await;

    server
        .mock("POST", "/oauth/token/")
        .with_status(500)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(r#"{"error":"server"}"#)
        .expect(1)
        .create_async()
        .await;

    let res = client.get_post(blog, post_id).await;
    assert!(res.is_err());
}

#[tokio::test]
async fn test_get_posts_unauthorized() {
    let (mut server, base) = setup().await;
    let client = ApiClient::new(Client::new(), &base);

    let blog = "blog";
    let limit = 3;
    let api_path = api_path(&format!("blog/{blog}/post/?limit={limit}"));

    server
        .mock("GET", api_path.as_str())
        .with_status(401)
        .create_async()
        .await;

    let res = client.get_posts(blog, limit, None, None).await;
    assert!(res.is_err());
}

#[tokio::test]
async fn test_get_posts_invalid_json() {
    let (mut server, base) = setup().await;
    let client = ApiClient::new(Client::new(), &base);

    let blog = "blog";
    let limit = 5;
    let api_path = api_path(&format!("blog/{blog}/post/?limit={limit}"));

    server
        .mock("GET", api_path.as_str())
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body("not json")
        .create_async()
        .await;

    let res = client.get_posts(blog, limit, None, None).await;
    assert!(res.is_err());
}

#[tokio::test]
async fn test_set_bearer_token_in_get_posts() {
    let (mut server, base) = setup().await;
    let client = ApiClient::new(Client::new(), &base);

    client.set_bearer_token("tokXYZ").await.unwrap();

    let blog = "testblog";
    let limit = 2;
    let api_path = api_path(&format!("blog/{blog}/post/?limit={limit}"));

    let raw = fs::read_to_string("tests/fixtures/api_response_posts.json").unwrap();
    let mut value: Value = serde_json::from_str(&raw).unwrap();
    if let Some(arr) = value.get_mut("data").and_then(|v| v.as_array_mut())
        && let Some(first) = arr.get_mut(0)
    {
        first["id"] = Value::String("p1".to_string());
        first["title"] = Value::String("Title1".to_string());
        first["hasAccess"] = Value::Bool(true);
    }
    let list_body = value.to_string();

    server
        .mock("GET", api_path.as_str())
        .match_header("authorization", "Bearer tokXYZ")
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(list_body)
        .create_async()
        .await;

    let result = client.get_posts(blog, limit, None, None).await.unwrap();
    assert_eq!(result.len(), 2);
    assert_eq!(result[0].id, "p1");
}

#[tokio::test]
async fn test_set_refresh_and_get_post_header_and_flow() {
    let (mut server, base) = setup().await;
    let client = ApiClient::new(Client::new(), &base);

    client
        .set_refresh_token_and_device_id("refA", "devA")
        .await
        .unwrap();

    let oauth_resp = json!({
        "access_token": "fresh_token",
        "refresh_token": "fresh_ref",
        "expires_in": 3600
    })
    .to_string();
    server
        .mock("POST", "/oauth/token/")
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(oauth_resp)
        .expect(1)
        .create_async()
        .await;

    let blog = "blog";
    let post_id = "55";
    let api_get_path = api_path(&format!("blog/{blog}/post/{post_id}"));

    let raw = fs::read_to_string("tests/fixtures/api_response_video_image.json").unwrap();
    let mut value: Value = serde_json::from_str(&raw).unwrap();
    value["id"] = Value::String(post_id.to_string());
    value["title"] = Value::String("Title55".to_string());
    value["hasAccess"] = Value::Bool(true);
    let resp_body = value.to_string();

    server
        .mock("GET", api_get_path.as_str())
        .match_header("authorization", "Bearer fresh_token")
        .with_status(200)
        .with_header(CONTENT_TYPE, "application/json")
        .with_body(resp_body)
        .create_async()
        .await;

    let result = client.get_post(blog, post_id).await.unwrap();
    assert_eq!(result.id, "55");
}