use serde::Deserialize;
use serde_json::{Value, json};
use zendriver_transport::SessionHandle;
use crate::error::DataDomeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataDomeSurface {
DeviceCheck,
Captcha,
Block,
None,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct DdConfig {
pub cid: Option<String>,
pub hsh: Option<String>,
pub t: Option<String>,
pub host: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectionSnapshot {
pub surface: DataDomeSurface,
pub datadome: Option<String>,
pub dd: Option<DdConfig>,
pub captcha_url: Option<String>,
pub body_clean: bool,
}
#[derive(Debug, Deserialize)]
struct RawSnapshot {
surface: String,
#[serde(default)]
datadome: Option<String>,
#[serde(default)]
dd: Option<DdConfig>,
#[serde(default)]
captcha_url: Option<String>,
body_clean: bool,
}
impl From<RawSnapshot> for DetectionSnapshot {
fn from(r: RawSnapshot) -> Self {
let surface = match r.surface.as_str() {
"device_check" => DataDomeSurface::DeviceCheck,
"captcha" => DataDomeSurface::Captcha,
"block" => DataDomeSurface::Block,
_ => DataDomeSurface::None,
};
Self {
surface,
datadome: r.datadome,
dd: r.dd,
captcha_url: r.captcha_url,
body_clean: r.body_clean,
}
}
}
pub(crate) async fn detect_snapshot(
session: &SessionHandle,
) -> Result<DetectionSnapshot, DataDomeError> {
let res = session
.call(
"Runtime.evaluate",
json!({
"expression": include_str!("detect.js"),
"returnByValue": true,
"awaitPromise": true,
}),
)
.await?;
if let Some(details) = res.get("exceptionDetails") {
let msg = details
.get("exception")
.and_then(|e| e.get("description"))
.and_then(|d| d.as_str())
.unwrap_or("unknown")
.to_string();
return Err(DataDomeError::JsError(msg));
}
let value = res
.get("result")
.and_then(|r| r.get("value"))
.cloned()
.unwrap_or(Value::Null);
let raw: RawSnapshot = serde_json::from_value(value)
.map_err(|e| DataDomeError::JsError(format!("invalid detect.js payload: {e}")))?;
Ok(raw.into())
}
pub async fn detect_surface(session: &SessionHandle) -> Result<DataDomeSurface, DataDomeError> {
Ok(detect_snapshot(session).await?.surface)
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use serde_json::json;
use zendriver_transport::testing::MockConnection;
fn reply(v: serde_json::Value) -> serde_json::Value {
json!({ "result": { "type": "object", "value": v } })
}
#[tokio::test]
async fn detect_surface_classifies_each_surface() {
for (payload, expected) in [
(
json!({"surface":"device_check","datadome":null,"dd":{"cid":"C","hsh":"H","t":"fe","host":"geo.captcha-delivery.com"},"captcha_url":null,"body_clean":false}),
DataDomeSurface::DeviceCheck,
),
(
json!({"surface":"captcha","datadome":"DD","dd":{"cid":"C","hsh":"H","t":"fe","host":"geo.captcha-delivery.com"},"captcha_url":"https://geo.captcha-delivery.com/captcha/?cid=C","body_clean":false}),
DataDomeSurface::Captcha,
),
(
json!({"surface":"block","datadome":null,"dd":{"cid":"C","hsh":"H","t":"bv","host":"geo.captcha-delivery.com"},"captcha_url":null,"body_clean":false}),
DataDomeSurface::Block,
),
(
json!({"surface":"none","datadome":"DD","dd":null,"captcha_url":null,"body_clean":true}),
DataDomeSurface::None,
),
] {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let fut = tokio::spawn({
let s = sess.clone();
async move { detect_surface(&s).await }
});
let id = mock.expect_cmd("Runtime.evaluate").await;
assert!(
mock.last_sent()["params"]["expression"]
.as_str()
.unwrap()
.contains("captcha-delivery.com")
);
mock.reply(id, reply(payload)).await;
assert_eq!(fut.await.unwrap().unwrap(), expected);
conn.shutdown();
}
}
}