use httpmock::prelude::*;
use serde_json::{json, Value};
#[cfg(feature = "capture-v1")]
const CAPTURE_PATH: &str = "/i/v1/analytics/events";
#[cfg(not(feature = "capture-v1"))]
const CAPTURE_PATH: &str = "/batch/";
const PREVIOUS_ID: &str = "anon-abc123";
const DISTINCT_ID: &str = "user-42";
fn ok_response() -> Value {
json!({ "results": {} })
}
fn alias_event(body: &Value) -> Option<&Value> {
body.get("batch")?
.as_array()?
.iter()
.find(|event| event.get("event").and_then(Value::as_str) == Some("$create_alias"))
}
fn request_has_well_formed_alias_event(req: &HttpMockRequest) -> bool {
let Ok(body) = serde_json::from_slice::<Value>(req.body_ref()) else {
return false;
};
let Some(event) = alias_event(&body) else {
return false;
};
event.get("distinct_id").and_then(Value::as_str) == Some(PREVIOUS_ID)
&& event
.pointer("/properties/distinct_id")
.and_then(Value::as_str)
== Some(PREVIOUS_ID)
&& event.pointer("/properties/alias").and_then(Value::as_str) == Some(DISTINCT_ID)
}
fn request_does_not_disable_person_processing(req: &HttpMockRequest) -> bool {
let Ok(body) = serde_json::from_slice::<Value>(req.body_ref()) else {
return false;
};
let Some(event) = alias_event(&body) else {
return false;
};
event
.pointer("/properties/$process_person_profile")
.and_then(Value::as_bool)
!= Some(false)
}
fn request_has_two_distinct_event_uuids(req: &HttpMockRequest) -> bool {
let Ok(body) = serde_json::from_slice::<Value>(req.body_ref()) else {
return false;
};
let Some(batch) = body.get("batch").and_then(Value::as_array) else {
return false;
};
if batch.len() != 2 {
return false;
}
let uuid_at = |i: usize| {
batch[i]
.get("uuid")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string()
};
let (first, second) = (uuid_at(0), uuid_at(1));
const NIL_UUID: &str = "00000000-0000-0000-0000-000000000000";
!first.is_empty() && first != second && first != NIL_UUID && second != NIL_UUID
}
const BLANK_IDS: [&str; 2] = ["", " "];
#[cfg(not(feature = "async-client"))]
mod blocking {
use super::*;
fn create_test_client(base_url: String) -> posthog_rs::Client {
let options: posthog_rs::ClientOptions = ("test_api_key", base_url.as_str()).into();
posthog_rs::client(options)
}
#[test]
fn alias_sends_create_alias_event() {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST)
.path(CAPTURE_PATH)
.matches(request_has_well_formed_alias_event)
.matches(request_does_not_disable_person_processing);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url());
client.alias(PREVIOUS_ID, DISTINCT_ID);
client.flush();
capture_mock.assert();
}
#[test]
fn alias_accepts_owned_and_borrowed_ids() {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST)
.path(CAPTURE_PATH)
.matches(request_has_well_formed_alias_event);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url());
client.alias(PREVIOUS_ID, String::from(DISTINCT_ID));
client.flush();
capture_mock.assert();
}
#[test]
fn repeated_aliases_get_distinct_event_uuids() {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST)
.path(CAPTURE_PATH)
.matches(request_has_two_distinct_event_uuids);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url());
client.alias(PREVIOUS_ID, DISTINCT_ID);
client.alias("anon-def456", "user-99");
client.flush();
capture_mock.assert();
}
#[test]
fn alias_with_blank_previous_id_is_dropped() {
for blank in BLANK_IDS {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST).path(CAPTURE_PATH);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url());
client.alias(blank, DISTINCT_ID);
client.flush();
capture_mock.assert_hits(0);
}
}
#[test]
fn alias_with_blank_distinct_id_is_dropped() {
for blank in BLANK_IDS {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST).path(CAPTURE_PATH);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url());
client.alias(PREVIOUS_ID, blank);
client.flush();
capture_mock.assert_hits(0);
}
}
#[test]
fn alias_is_noop_for_disabled_client() {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST).path(CAPTURE_PATH);
then.status(200).json_body(ok_response());
});
let options = posthog_rs::ClientOptionsBuilder::default()
.host(server.base_url())
.build()
.unwrap();
assert!(options.is_disabled());
let client = posthog_rs::client(options);
client.alias(PREVIOUS_ID, DISTINCT_ID);
client.flush();
capture_mock.assert_hits(0);
}
}
#[cfg(feature = "async-client")]
mod async_client {
use super::*;
async fn create_test_client(base_url: String) -> posthog_rs::Client {
let options: posthog_rs::ClientOptions = ("test_api_key", base_url.as_str()).into();
posthog_rs::client(options).await
}
#[tokio::test]
async fn alias_sends_create_alias_event() {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST)
.path(CAPTURE_PATH)
.matches(request_has_well_formed_alias_event)
.matches(request_does_not_disable_person_processing);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url()).await;
client.alias(PREVIOUS_ID, DISTINCT_ID);
client.flush().await;
capture_mock.assert();
}
#[tokio::test]
async fn alias_accepts_owned_and_borrowed_ids() {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST)
.path(CAPTURE_PATH)
.matches(request_has_well_formed_alias_event);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url()).await;
client.alias(PREVIOUS_ID, String::from(DISTINCT_ID));
client.flush().await;
capture_mock.assert();
}
#[tokio::test]
async fn repeated_aliases_get_distinct_event_uuids() {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST)
.path(CAPTURE_PATH)
.matches(request_has_two_distinct_event_uuids);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url()).await;
client.alias(PREVIOUS_ID, DISTINCT_ID);
client.alias("anon-def456", "user-99");
client.flush().await;
capture_mock.assert();
}
#[tokio::test]
async fn alias_with_blank_previous_id_is_dropped() {
for blank in BLANK_IDS {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST).path(CAPTURE_PATH);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url()).await;
client.alias(blank, DISTINCT_ID);
client.flush().await;
capture_mock.assert_hits(0);
}
}
#[tokio::test]
async fn alias_with_blank_distinct_id_is_dropped() {
for blank in BLANK_IDS {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST).path(CAPTURE_PATH);
then.status(200).json_body(ok_response());
});
let client = create_test_client(server.base_url()).await;
client.alias(PREVIOUS_ID, blank);
client.flush().await;
capture_mock.assert_hits(0);
}
}
#[tokio::test]
async fn alias_is_noop_for_disabled_client() {
let server = MockServer::start();
let capture_mock = server.mock(|when, then| {
when.method(POST).path(CAPTURE_PATH);
then.status(200).json_body(ok_response());
});
let options = posthog_rs::ClientOptionsBuilder::default()
.host(server.base_url())
.build()
.unwrap();
assert!(options.is_disabled());
let client = posthog_rs::client(options).await;
client.alias(PREVIOUS_ID, DISTINCT_ID);
client.flush().await;
capture_mock.assert_hits(0);
}
}