dimescheduler 0.1.0

Official Rust client library for the Dime.Scheduler API.
Documentation
use reqwest::Method;
use serde_json::json;
use wiremock::matchers::{body_json, header, method, path, query_param};
use wiremock::{Mock, MockServer, ResponseTemplate};

use crate::transport::{encode_segment, Transport};
use crate::Error;

fn transport(server: &MockServer) -> Transport {
    Transport::new(server.uri(), "secret-key".into())
}

#[tokio::test]
async fn get_sends_api_key_user_agent_and_query() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/thing"))
        .and(header("X-API-KEY", "secret-key"))
        .and(header("Accept", "application/json"))
        .and(query_param("a", "1"))
        .and(query_param("resources", "R1"))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!([1, 2])))
        .expect(1)
        .mount(&server)
        .await;

    let out: Vec<i32> = transport(&server)
        .get_json(
            "/thing",
            &[
                ("a", "1".into()),
                ("resources", "R1".into()),
                ("resources", "R2".into()),
            ],
        )
        .await
        .unwrap();
    assert_eq!(out, vec![1, 2]);

    let reqs = server.received_requests().await.unwrap();
    let ua = reqs[0]
        .headers
        .get("user-agent")
        .unwrap()
        .to_str()
        .unwrap()
        .to_string();
    assert!(ua.starts_with("dimescheduler-rust/"), "{ua}");
    assert!(reqs[0].url.query().unwrap().contains("resources=R2"));
}

#[tokio::test]
async fn send_json_posts_body_and_decodes() {
    let server = MockServer::start().await;
    Mock::given(method("PUT"))
        .and(path("/thing"))
        .and(body_json(json!({ "k": "v" })))
        .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "ok": true })))
        .expect(1)
        .mount(&server)
        .await;

    let out: serde_json::Value = transport(&server)
        .send_json(Method::PUT, "/thing", &json!({ "k": "v" }))
        .await
        .unwrap();
    assert_eq!(out["ok"], true);
}

#[tokio::test]
async fn non_success_maps_to_api_error_with_extracted_message() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/thing"))
        .respond_with(
            ResponseTemplate::new(400).set_body_json(
                json!({ "Error": "bad", "Description": "jobNo missing", "Code": "E1" }),
            ),
        )
        .mount(&server)
        .await;

    let err = transport(&server)
        .get_json::<serde_json::Value>("/thing", &[])
        .await
        .unwrap_err();
    match err {
        Error::Api { status, body } => {
            assert_eq!(status, 400);
            assert_eq!(body, "jobNo missing (code: E1)");
        }
        other => panic!("expected Api error, got {other:?}"),
    }
}

#[tokio::test]
async fn empty_body_ok_for_unit() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/thing"))
        .respond_with(ResponseTemplate::new(200))
        .mount(&server)
        .await;
    transport(&server)
        .send_empty(Method::POST, "/thing", &json!({}))
        .await
        .unwrap();
}

#[tokio::test]
async fn empty_body_on_typed_response_is_http_error() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/thing"))
        .respond_with(ResponseTemplate::new(200))
        .mount(&server)
        .await;
    let err = transport(&server)
        .get_json::<Vec<i32>>("/thing", &[])
        .await
        .unwrap_err();
    assert!(matches!(err, Error::Http(_)), "{err:?}");
}

#[test]
fn encode_segment_matches_encode_uri_component() {
    assert_eq!(encode_segment("APT 1/2"), "APT%201%2F2");
    assert_eq!(encode_segment("JOB-001_a.b~c"), "JOB-001_a.b~c");
    assert_eq!(encode_segment("a?b#c"), "a%3Fb%23c");
}