rtblint-resolve 0.14.6

Opt-in sellers.json / ads.txt resolution for RTBlint, against a locally cached reference database
Documentation
//! Resolution against the fixture cache.

use std::path::PathBuf;

use rtblint_core::{Severity, ValidationResult};
use rtblint_resolve::{merge_into, resolve_bid_request, Cache};

fn cache() -> Cache {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cache");
    Cache::open(root).expect("fixture cache")
}

fn load(name: &str) -> String {
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/requests")
        .join(name);
    std::fs::read_to_string(path).expect(name)
}

fn has_issue(issues: &[rtblint_core::Issue], id: &str, path: &str) -> bool {
    issues
        .iter()
        .any(|issue| issue.id == id && issue.path.as_deref() == Some(path))
}

#[test]
fn matching_cache_is_clean() {
    let issues = resolve_bid_request(&load("valid-resolved.json"), &cache());
    assert!(issues.is_empty(), "{issues:?}");
}

#[test]
fn unknown_sid_is_an_error() {
    let issues = resolve_bid_request(&load("unknown-sid.json"), &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.sid_not_in_sellers",
        "source.schain.nodes[0].sid"
    ));
    assert_eq!(
        issues
            .iter()
            .find(|issue| issue.id == "openrtb.resolve.sid_not_in_sellers")
            .map(|issue| issue.severity),
        Some(Severity::Error)
    );
}

#[test]
fn missing_sellers_json_is_a_warning() {
    let issues = resolve_bid_request(&load("missing-sellers.json"), &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.sellers_json_unavailable",
        "source.schain.nodes[0].asi"
    ));
}

#[test]
fn unparseable_sellers_json_is_a_warning() {
    let payload = r#"{
        "id": "req-1",
        "source": {
            "schain": {
                "complete": 1,
                "ver": "1.0",
                "nodes": [{ "asi": "broken.example", "sid": "x", "hp": 1 }]
            }
        },
        "imp": [{ "id": "1", "banner": { "w": 1, "h": 1 } }]
    }"#;
    let issues = resolve_bid_request(payload, &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.sellers_json_unparseable",
        "source.schain.nodes[0].asi"
    ));
}

#[test]
fn ads_txt_must_list_the_first_payment_hop() {
    let payload = r#"{
        "id": "req-1",
        "source": {
            "schain": {
                "complete": 1,
                "ver": "1.0",
                "nodes": [{ "asi": "exchange2.example", "sid": "seller-xyz", "hp": 1 }]
            }
        },
        "site": { "domain": "publisher.example" },
        "imp": [{ "id": "1", "banner": { "w": 1, "h": 1 } }]
    }"#;
    let issues = resolve_bid_request(payload, &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.ads_txt_unauthorized",
        "source.schain.nodes[0].sid"
    ));
}

#[test]
fn missing_ads_txt_is_a_warning() {
    let payload = r#"{
        "id": "req-1",
        "source": {
            "schain": {
                "complete": 1,
                "ver": "1.0",
                "nodes": [{ "asi": "exchange1.example", "sid": "seller-abc", "hp": 1 }]
            }
        },
        "site": { "domain": "other-publisher.example" },
        "imp": [{ "id": "1", "banner": { "w": 1, "h": 1 } }]
    }"#;
    let issues = resolve_bid_request(payload, &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.ads_txt_unavailable",
        "site.domain"
    ));
}

#[test]
fn www_prefix_falls_back_to_the_registrable_domain() {
    let payload = r#"{
        "id": "req-1",
        "source": {
            "schain": {
                "complete": 1,
                "ver": "1.0",
                "nodes": [{ "asi": "exchange1.example", "sid": "seller-abc", "hp": 1 }]
            }
        },
        "site": { "domain": "www.publisher.example" },
        "imp": [{ "id": "1", "banner": { "w": 1, "h": 1 } }]
    }"#;
    let issues = resolve_bid_request(payload, &cache());
    assert!(
        !issues.iter().any(|issue| issue.id.contains("ads_txt")),
        "{issues:?}"
    );
}

#[test]
fn app_ads_txt_authorizes_the_bundle() {
    let issues = resolve_bid_request(&load("valid-app.json"), &cache());
    assert!(issues.is_empty(), "{issues:?}");
}

#[test]
fn missing_app_ads_txt_is_a_warning() {
    let payload = r#"{
        "id": "req-1",
        "source": {
            "schain": {
                "complete": 1,
                "ver": "1.0",
                "nodes": [{ "asi": "exchange1.example", "sid": "seller-abc", "hp": 1 }]
            }
        },
        "app": { "bundle": "com.missing.app" },
        "imp": [{ "id": "1", "banner": { "w": 1, "h": 1 } }]
    }"#;
    let issues = resolve_bid_request(payload, &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.app_ads_txt_unavailable",
        "app.bundle"
    ));
}

#[test]
fn layered_3_0_paths_include_the_envelope() {
    let issues = resolve_bid_request(&load("layered-3-0-unknown-sid.json"), &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.sid_not_in_sellers",
        "openrtb.request.source.schain.nodes[0].sid"
    ));
}

#[test]
fn hp_zero_nodes_are_not_checked() {
    let payload = r#"{
        "id": "req-1",
        "source": {
            "schain": {
                "complete": 1,
                "ver": "1.0",
                "nodes": [
                    { "asi": "unknown-ssp.example", "sid": "nope", "hp": 0 },
                    { "asi": "exchange1.example", "sid": "seller-abc", "hp": 1 }
                ]
            }
        },
        "site": { "domain": "publisher.example" },
        "imp": [{ "id": "1", "banner": { "w": 1, "h": 1 } }]
    }"#;
    let issues = resolve_bid_request(payload, &cache());
    assert!(
        !issues
            .iter()
            .any(|issue| issue.path.as_deref() == Some("source.schain.nodes[0].asi")),
        "{issues:?}"
    );
}

#[test]
fn merge_into_clears_valid_on_resolve_errors() {
    let mut result = ValidationResult::default();
    let extra = resolve_bid_request(&load("unknown-sid.json"), &cache());
    merge_into(&mut result, extra);
    assert!(!result.valid);
}

#[test]
fn app_ads_txt_must_list_the_first_payment_hop() {
    let payload = r#"{
        "id": "req-1",
        "source": {
            "schain": {
                "complete": 1,
                "ver": "1.0",
                "nodes": [{ "asi": "exchange2.example", "sid": "seller-xyz", "hp": 1 }]
            }
        },
        "app": { "bundle": "com.example.app" },
        "imp": [{ "id": "1", "banner": { "w": 1, "h": 1 } }]
    }"#;
    let issues = resolve_bid_request(payload, &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.app_ads_txt_unauthorized",
        "source.schain.nodes[0].sid"
    ));
}

#[test]
fn ext_schain_is_resolved() {
    let payload = r#"{
        "id": "req-1",
        "source": {
            "ext": {
                "schain": {
                    "complete": 1,
                    "ver": "1.0",
                    "nodes": [{ "asi": "exchange1.example", "sid": "unknown-sid", "hp": 1 }]
                }
            }
        },
        "site": { "domain": "publisher.example" },
        "imp": [{ "id": "1", "banner": { "w": 1, "h": 1 } }]
    }"#;
    let issues = resolve_bid_request(payload, &cache());
    assert!(has_issue(
        &issues,
        "openrtb.resolve.sid_not_in_sellers",
        "source.ext.schain.nodes[0].sid"
    ));
}

#[test]
fn cache_open_rejects_a_missing_directory() {
    let err = Cache::open("/no/such/rtblint-cache").unwrap_err();
    assert!(err.contains("not a directory"), "{err}");
}