use indexmap::IndexMap;
use indicatif::{ProgressBar, ProgressStyle};
use reqwest::blocking::Client;
use serde_json::Value;
use crate::config::TestResult;
struct GoogleApiTest {
name: &'static str,
url_template: &'static str,
method: Method,
body: Option<Value>,
success_check: fn(status: u16, body: &Option<Value>, content_len: usize) -> bool,
}
#[derive(Clone, Copy)]
enum Method {
Get,
Post,
}
fn google_api_tests() -> Vec<GoogleApiTest> {
vec![
GoogleApiTest {
name: "Static Maps API",
url_template: "https://maps.googleapis.com/maps/api/staticmap?center=0,0&zoom=0&size=1x1&key={key}",
method: Method::Get,
body: None,
success_check: |status, _body, content_len| status == 200 && content_len > 100,
},
GoogleApiTest {
name: "Geocoding API",
url_template: "https://maps.googleapis.com/maps/api/geocode/json?address=1600+Amphitheatre&key={key}",
method: Method::Get,
body: None,
success_check: |_status, body, _| {
body.as_ref()
.and_then(|b| b.get("status"))
.and_then(|s| s.as_str())
== Some("OK")
},
},
GoogleApiTest {
name: "Places Text Search",
url_template: "https://maps.googleapis.com/maps/api/place/textsearch/json?query=restaurant&key={key}",
method: Method::Get,
body: None,
success_check: |_status, body, _| {
let s = body
.as_ref()
.and_then(|b| b.get("status"))
.and_then(|s| s.as_str())
.unwrap_or("");
s == "OK" || s == "ZERO_RESULTS"
},
},
GoogleApiTest {
name: "YouTube Data API v3",
url_template: "https://www.googleapis.com/youtube/v3/search?part=snippet&q=test&maxResults=1&key={key}",
method: Method::Get,
body: None,
success_check: |status, body, _| {
status == 200
&& body
.as_ref()
.map(|b| b.get("items").is_some())
.unwrap_or(false)
},
},
GoogleApiTest {
name: "Gemini API",
url_template: "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={key}",
method: Method::Post,
body: Some(serde_json::json!({
"contents": [{"parts": [{"text": "ping"}]}]
})),
success_check: |status, body, _| {
status == 200
|| body
.as_ref()
.map(|b| b.get("candidates").is_some())
.unwrap_or(false)
},
},
GoogleApiTest {
name: "Google Translate v2",
url_template: "https://translation.googleapis.com/language/translate/v2?key={key}",
method: Method::Post,
body: Some(serde_json::json!({"q": "Hello", "target": "es"})),
success_check: |status, body, _| {
status == 200
&& body
.as_ref()
.map(|b| b.to_string().contains("translations"))
.unwrap_or(false)
},
},
GoogleApiTest {
name: "Directions API",
url_template: "https://maps.googleapis.com/maps/api/directions/json?origin=NYC&destination=Boston&key={key}",
method: Method::Get,
body: None,
success_check: |_status, body, _| {
let s = body
.as_ref()
.and_then(|b| b.get("status"))
.and_then(|s| s.as_str())
.unwrap_or("");
s == "OK" || s == "ZERO_RESULTS"
},
},
]
}
pub fn run_google_api_tests(
client: &Client,
key: &str,
) -> IndexMap<String, TestResult> {
let tests = google_api_tests();
let mut results = IndexMap::new();
let pb = ProgressBar::new(tests.len() as u64);
pb.set_style(
ProgressStyle::with_template("Testing Google APIs... [{bar:30.cyan/dim}] {pos}/{len}")
.unwrap()
.progress_chars("━╸─"),
);
for test in &tests {
let url = test.url_template.replace("{key}", key);
let outcome = match test.method {
Method::Get => client.get(&url).send(),
Method::Post => {
let mut req = client.post(&url);
if let Some(ref body) = test.body {
req = req.json(body);
}
req.send()
}
};
let result = match outcome {
Ok(resp) => {
let status = resp.status().as_u16();
let bytes = resp.bytes().unwrap_or_default();
let content_len = bytes.len();
let json_body: Option<Value> = serde_json::from_slice(&bytes).ok();
let is_success = (test.success_check)(status, &json_body, content_len);
if is_success {
TestResult::ok(status, "Works")
} else {
let (error_msg, hint) = classify_failure(status, &json_body);
TestResult {
success: false,
status_code: Some(status),
error: Some(error_msg),
detail: hint,
extra: Default::default(),
}
}
}
Err(e) => TestResult::fail(
None,
format!("Network: {}", &e.to_string().chars().take(50).collect::<String>()),
None,
),
};
results.insert(test.name.to_string(), result);
pb.inc(1);
}
pb.finish_and_clear();
results
}
fn classify_failure(status: u16, body: &Option<Value>) -> (String, Option<String>) {
match status {
403 => (
"403 Forbidden".to_string(),
Some("restricted/not enabled/billing".to_string()),
),
401 => (
"401 Unauthorized".to_string(),
Some("key invalid/deleted".to_string()),
),
429 => (
"429 Rate Limited".to_string(),
Some("quota exceeded".to_string()),
),
_ => {
if let Some(ref b) = body {
if let Some(err) = b.get("error") {
let msg = if let Some(m) = err.get("message").and_then(|m| m.as_str()) {
m.to_string()
} else {
err.to_string()
};
return (msg, None);
}
}
(format!("HTTP {}", status), None)
}
}
}