use serde_json::{json, Value};
use wiremock::matchers::{body_partial_json, header, method, path};
use wiremock::{Mock, MockServer, Request, ResponseTemplate};
use super::*;
const APP: &str = "app_e2e123";
const SHA: &str = "AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB:AB";
fn app_json(native: Value, origins: Value) -> Value {
json!({ "id": APP, "name": "E2E", "origins": origins, "native_attestation": native, "android_package_names": [] })
}
async fn server_with(app: Value) -> MockServer {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!("/api/v1/developers/applications/{APP}")))
.and(header("authorization", "Bearer dev-token"))
.respond_with(
ResponseTemplate::new(200).set_body_json(json!({ "data": { "application": app } })),
)
.mount(&server)
.await;
for product in ["mesh_sdk", "proof_of_location"] {
Mock::given(method("GET"))
.and(path(format!(
"/api/v1/developers/applications/{APP}/product-settings/{product}"
)))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({ "data": { "product": product, "settings": {} } })),
)
.mount(&server)
.await;
}
server
}
fn ctx(server: &MockServer) -> AppContext {
AppContext {
client: DeveloperClient::new(format!("{}/api/v1", server.uri())).unwrap(),
token: "dev-token".into(),
app_id: APP.into(),
}
}
fn patches(requests: &[Request]) -> Vec<Value> {
requests
.iter()
.filter(|r| r.method.as_str() == "PATCH")
.map(|r| serde_json::from_slice(&r.body).unwrap())
.collect()
}
#[tokio::test]
async fn apply_registers_native_apps_origins_and_features_in_one_pass() {
let server = server_with(app_json(json!(null), json!([]))).await;
Mock::given(method("PATCH"))
.and(path(format!("/api/v1/developers/applications/{APP}")))
.respond_with(|req: &Request| {
let body: Value = serde_json::from_slice(&req.body).unwrap();
ResponseTemplate::new(200).set_body_json(json!({ "data": { "application": {
"id": APP, "origins": body.get("origins").cloned().unwrap_or(json!([])),
"native_attestation": body.get("native_attestation").cloned().unwrap_or(Value::Null),
} } }))
})
.mount(&server)
.await;
Mock::given(method("PATCH"))
.and(path(format!(
"/api/v1/developers/applications/{APP}/product-settings/mesh_sdk"
)))
.and(body_partial_json(json!({ "mesh_telemetry": true })))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(json!({ "data": { "settings": { "mesh_telemetry": true } } })),
)
.mount(&server)
.await;
let desired = desired_from_config(&json!({ "app": {
"native": {
"ios": [{ "bundleId": "com.example.app", "teamId": "ABCDE12345" }],
"android": [{ "package": "com.example.app", "sha256": [SHA] }],
"allowDevelopmentBuilds": true
},
"origins": ["https://example.com"],
"features": { "telemetry": true }
}}));
let changes = apply(&ctx(&server), &desired, false).await.unwrap();
assert_eq!(changes.len(), 5, "{changes:?}");
let sent = patches(&server.received_requests().await.unwrap());
let native = sent
.iter()
.find(|b| b.get("native_attestation").is_some())
.unwrap();
assert_eq!(
native["native_attestation"]["ios_apps"][0]["app_id_prefix"],
"ABCDE12345"
);
assert_eq!(
native["native_attestation"]["android_apps"][0]["signing_cert_sha256"][0],
SHA
);
assert_eq!(native["native_attestation"]["allow_development"], true);
assert_eq!(native["android_package_names"], json!(["com.example.app"]));
assert!(sent
.iter()
.any(|b| b["origins"] == json!(["https://example.com"])));
assert!(sent.iter().any(|b| b["mesh_telemetry"] == true));
}
#[tokio::test]
async fn a_second_run_changes_nothing_and_sends_nothing() {
let native = json!({ "ios_apps": [{ "bundle_id": "com.example.app", "app_id_prefix": "ABCDE12345" }],
"android_apps": [], "allow_development": false });
let server = server_with(app_json(native, json!(["https://example.com"]))).await;
let desired = desired_from_config(&json!({ "app": {
"native": { "ios": [{ "bundleId": "com.example.app", "teamId": "abcde12345" }] },
"origins": ["example.com/"],
"features": { "telemetry": false }
}}));
let c = ctx(&server);
assert!(apply(&c, &desired, false).await.unwrap().is_empty());
let (changed, _) = c
.edit_native(|n| Ok(n.add_ios("com.example.app", "ABCDE12345")))
.await
.unwrap();
assert!(!changed);
assert!(patches(&server.received_requests().await.unwrap()).is_empty());
}
#[tokio::test]
async fn api_errors_keep_the_servers_message_and_next_steps() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path(format!("/api/v1/developers/applications/{APP}")))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"message": "App app_e2e123 has no registered native app.",
"error": "app_credentials_required",
"data": { "next_steps": ["offline apps native add ios --bundle-id <bundle id> --team-id <Apple Team ID>"] }
})))
.mount(&server)
.await;
let err = ctx(&server).application().await.unwrap_err().to_string();
assert!(err.contains("(app_credentials_required)"), "{err}");
assert!(err.contains("has no registered native app"), "{err}");
assert!(
err.contains("next:\n offline apps native add ios"),
"{err}"
);
}