appblocklist 1.0.0

Client for the AppBlockList App Lookup API: categorize iOS and Android apps by bundle ID, App Store ID or package name for MDM app blocklists.
Documentation
//! Offline tests: every answer documented for the API, parsed without network.

use appblocklist::{parse_response, App, Error};

const TIKTOK: &str = r#"{
  "found": true,
  "name": "TikTok - Videos, Shop & LIVE",
  "developer": "TikTok Ltd.",
  "bundle_id": "com.zhiliaoapp.musically",
  "app_store_id": "835599320",
  "android_package": "com.zhiliaoapp.musically",
  "app_store_genre": "Entertainment",
  "age_rating": "12+",
  "filtering_categories": ["Social Networking", "Streaming Media"],
  "developer_domain": "tiktok.com",
  "iab_category": "Technology & Computing > Computing > Internet > Social Networking"
}"#;

#[test]
fn found_app() {
    let app = parse_response(200, TIKTOK).unwrap();
    assert!(app.found);
    assert_eq!(app.name, "TikTok - Videos, Shop & LIVE");
    assert_eq!(app.developer, "TikTok Ltd.");
    assert_eq!(app.app_store_id, "835599320");
    assert_eq!(app.age_rating, "12+");
    assert_eq!(app.primary_category(), Some("Social Networking"));
    assert!(app.in_category(&["streaming media"]));
    assert!(!app.in_category(&["Shopping"]));
    assert!(app.on_google_play());
    assert_eq!(app.block_ids(), vec!["com.zhiliaoapp.musically"]);
    assert_eq!(app.message, None);
}

#[test]
fn not_found_is_a_result() {
    let app = parse_response(404, r#"{"found": false, "error": "App not found."}"#).unwrap();
    assert!(!app.found);
    assert_eq!(app.message.as_deref(), Some("App not found."));
    assert!(app.filtering_categories.is_empty());

    let play = parse_response(
        404,
        r#"{"found": false, "android_package": "com.example.app", "error": "Listed on Google Play, not yet in the categorized database."}"#,
    )
    .unwrap();
    assert!(!play.found);
    assert_eq!(play.android_package, "com.example.app");
    assert!(play.on_google_play());
}

#[test]
fn error_statuses() {
    assert!(matches!(
        parse_response(400, r#"{"error":"bad id"}"#),
        Err(Error::InvalidId(m)) if m == "bad id"
    ));
    assert!(matches!(
        parse_response(401, r#"{"error":"Invalid api_key."}"#),
        Err(Error::Unauthorized(_))
    ));
    match parse_response(403, r#"{"error":"Monthly lookups used.","monthly_lookups":100000}"#) {
        Err(Error::QuotaExceeded { monthly_lookups, .. }) => assert_eq!(monthly_lookups, Some(100000)),
        other => panic!("unexpected {:?}", other),
    }
    let e = parse_response(500, r#"{"error":"Temporary error."}"#).unwrap_err();
    assert!(e.is_temporary());
    assert_eq!(e.status(), Some(500));
    assert!(parse_response(401, "{}").unwrap_err().is_fatal());
}

#[test]
fn lenient_fields() {
    let app = parse_response(200, r#"{"found":true,"app_store_id":835599320,"filtering_categories":null}"#).unwrap();
    assert_eq!(app.app_store_id, "835599320");
    assert!(app.filtering_categories.is_empty());
    assert!(matches!(parse_response(200, "not json"), Err(Error::Decode(_))));
}

#[test]
fn round_trips_through_serde() {
    let app = parse_response(200, TIKTOK).unwrap();
    let json = serde_json::to_string(&app).unwrap();
    let back: App = serde_json::from_str(&json).unwrap();
    assert_eq!(app, back);
    assert!(!json.contains("\"error\""));
}