Bug 1952316 - Vendor in context_id component from application-services. r=markh

Differential Revision: https://phabricator.services.mozilla.com/D248972
This commit is contained in:
Mike Conley
2025-05-16 20:08:58 +00:00
committed by mconley@mozilla.com
parent 5b95cb085a
commit b6a5beb740
10 changed files with 813 additions and 0 deletions

20
Cargo.lock generated
View File

@@ -970,6 +970,25 @@ dependencies = [
"winapi",
]
[[package]]
name = "context_id"
version = "0.1.0"
source = "git+https://github.com/mozilla/application-services?rev=fa8a72a77f88bc8b3743b50d76fb85cb37a38285#fa8a72a77f88bc8b3743b50d76fb85cb37a38285"
dependencies = [
"chrono",
"error-support",
"lazy_static",
"log",
"parking_lot",
"serde",
"serde_json",
"thiserror 1.999.999",
"uniffi",
"url",
"uuid",
"viaduct",
]
[[package]]
name = "cookie"
version = "0.16.2"
@@ -2629,6 +2648,7 @@ dependencies = [
name = "gkrust-uniffi-components"
version = "0.1.0"
dependencies = [
"context_id",
"relevancy",
"search",
"suggest",

View File

@@ -262,6 +262,7 @@ wr_malloc_size_of = { path = "gfx/wr/wr_malloc_size_of" }
objc = { git = "https://github.com/glandium/rust-objc", rev = "4de89f5aa9851ceca4d40e7ac1e2759410c04324" }
# application-services overrides to make updating them all simpler.
context_id = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
interrupt-support = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
relevancy = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }
search = { git = "https://github.com/mozilla/application-services", rev = "fa8a72a77f88bc8b3743b50d76fb85cb37a38285" }

View File

@@ -0,0 +1 @@
{"files":{"Cargo.toml":"6a4710d72aec46571c411e76c2c4fd62097e887fc90cc62bf44c454fef618d62","README.md":"478bd3dcd41863df0975c96fd38e34334d8146799bf1a16204bab2cc12dcffd2","src/callback.rs":"13305f4665e62abb4de08d9486295a60afd327b6c839d7ef314312d96dc098dc","src/error.rs":"969ba252410f2adff0ff91ec7eeaf52ffb9e91925eb9db0529f56c09872f78f8","src/lib.rs":"199c109f3a4bab8441ba46e7513602bf732c9b381477867d717dcab24b0983bf","src/mars.rs":"4909fc4f8b397d32144878d6ec64c414b7d724ed2532513f165c768cfe5b983f"},"package":null}

62
third_party/rust/context_id/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,62 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "context_id"
version = "0.1.0"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
readme = "README.md"
license = "MPL-2.0"
[lib]
name = "context_id"
path = "src/lib.rs"
[dependencies]
chrono = "0.4"
log = "0.4"
parking_lot = "0.12"
serde = "1"
serde_json = "1"
thiserror = "1.0"
url = "2"
[dependencies.error-support]
path = "../support/error"
[dependencies.lazy_static]
version = "1.4"
[dependencies.uniffi]
version = "0.29.0"
[dependencies.uuid]
version = "1.3"
features = ["v4"]
[dependencies.viaduct]
path = "../viaduct"
[dev-dependencies]
mockito = "0.31"
[dev-dependencies.viaduct-reqwest]
path = "../support/viaduct-reqwest"
[build-dependencies.uniffi]
version = "0.29.0"
features = ["build"]

19
third_party/rust/context_id/README.md vendored Normal file
View File

@@ -0,0 +1,19 @@
# Context ID
A context ID is a UUID that is used when making requests to MARS or to Merino to help reduce click fraud.
The **Context ID Rust component** creates a shared mechanism for managing context IDs, and to allow them to be rotated after they have exceeded some age.
It is currently under construction and not yet used.
## Tests
Tests are run with
```shell
cargo test -p context_id
```
## Bugs
We use Bugzilla to track bugs and feature work. You can use [this link](bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=New Tab Page) to file bugs in the `Firefox :: New Tab Page` bug component.

View File

@@ -0,0 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#[uniffi::export(callback_interface)]
pub trait ContextIdCallback: Sync + Send {
fn persist(&self, context_id: String, creation_date: i64);
fn rotated(&self, old_context_id: String);
}
pub struct DefaultContextIdCallback;
impl ContextIdCallback for DefaultContextIdCallback {
fn persist(&self, _context_id: String, _creation_date: i64) {}
fn rotated(&self, _old_context_id: String) {}
}

View File

@@ -0,0 +1,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use error_support::{ErrorHandling, GetErrorHandling};
pub type Result<T> = std::result::Result<T, Error>;
pub type ApiResult<T> = std::result::Result<T, ApiError>;
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum ApiError {
#[error("Something unexpected occurred.")]
Other { reason: String },
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Timestamp was invalid")]
InvalidTimestamp { timestamp: i64 },
#[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("Viaduct error: {0}")]
ViaductError(#[from] viaduct::Error),
#[error("UniFFI callback error: {0}")]
UniFFICallbackError(#[from] uniffi::UnexpectedUniFFICallbackError),
}
// Define how our internal errors are handled and converted to external errors.
impl GetErrorHandling for Error {
type ExternalError = ApiError;
fn get_error_handling(&self) -> ErrorHandling<Self::ExternalError> {
ErrorHandling::convert(ApiError::Other {
reason: self.to_string(),
})
}
}

587
third_party/rust/context_id/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,587 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
mod error;
pub use error::{ApiError, ApiResult, Error, Result};
use chrono::{DateTime, Duration, Utc};
use error_support::handle_error;
use log::error;
use parking_lot::{Mutex, RwLock};
use uuid::Uuid;
uniffi::setup_scaffolding!("context_id");
mod callback;
pub use callback::{ContextIdCallback, DefaultContextIdCallback};
mod mars;
use mars::{MARSClient, SimpleMARSClient};
/// Top-level API for the context_id component
#[derive(uniffi::Object)]
pub struct ContextIDComponent {
inner: Mutex<ContextIDComponentInner>,
}
#[uniffi::export]
impl ContextIDComponent {
/// Construct a new [ContextIDComponent].
///
/// If no creation timestamp is provided, the current time will be used.
#[uniffi::constructor]
#[handle_error(Error)]
pub fn new(
init_context_id: &str,
creation_timestamp_s: i64,
running_in_test_automation: bool,
callback: Box<dyn ContextIdCallback>,
) -> ApiResult<Self> {
Ok(Self {
inner: Mutex::new(ContextIDComponentInner::new(
init_context_id,
creation_timestamp_s,
running_in_test_automation,
callback,
Utc::now(),
Box::new(SimpleMARSClient::new()),
)?),
})
}
/// Return the current context ID string.
#[handle_error(Error)]
pub fn request(&self, rotation_days_in_s: u8) -> ApiResult<String> {
let mut inner = self.inner.lock();
inner.request(rotation_days_in_s, Utc::now())
}
/// Regenerate the context ID.
#[handle_error(Error)]
pub fn force_rotation(&self) -> ApiResult<()> {
let mut inner = self.inner.lock();
inner.force_rotation(Utc::now());
Ok(())
}
/// Unset the callbacks set during construction, and use a default
/// no-op ContextIdCallback instead.
#[handle_error(Error)]
pub fn unset_callback(&self) -> ApiResult<()> {
let mut inner = self.inner.lock();
inner.unset_callback();
Ok(())
}
}
struct ContextIDComponentInner {
context_id: String,
creation_timestamp: DateTime<Utc>,
callback_handle: RwLock<Box<dyn ContextIdCallback>>,
mars_client: Box<dyn MARSClient>,
running_in_test_automation: bool,
}
impl ContextIDComponentInner {
pub fn new(
init_context_id: &str,
creation_timestamp_s: i64,
running_in_test_automation: bool,
callback: Box<dyn ContextIdCallback>,
now: DateTime<Utc>,
mars_client: Box<dyn MARSClient>,
) -> Result<Self> {
// Some historical context IDs are stored within opening and closing
// braces, and our endpoints have tolerated this, but ideally we'd
// send without the braces, so we strip any off here.
let (context_id, generated_context_id) = match init_context_id
.trim()
.trim_start_matches('{')
.trim_end_matches('}')
{
"" => (Uuid::new_v4().to_string(), true),
// If the passed in string isn't empty, but still not a valid UUID,
// just go ahead and generate a new UUID.
s => match Uuid::parse_str(s) {
Ok(_) => (s.to_string(), false),
Err(_) => (Uuid::new_v4().to_string(), true),
},
};
let (creation_timestamp, generated_creation_timestamp) = if generated_context_id {
// If we had to generate a UUID, also force a new timestamp.
(now, true)
} else {
match creation_timestamp_s {
secs if secs > 0 => (
DateTime::<Utc>::from_timestamp(secs, 0)
.ok_or(Error::InvalidTimestamp { timestamp: secs })?,
false,
),
_ => (now, true),
}
};
let instance = Self {
context_id,
creation_timestamp,
callback_handle: RwLock::new(callback),
mars_client,
running_in_test_automation,
};
// We only need to persist these if we just generated one.
if generated_context_id || generated_creation_timestamp {
instance.persist();
}
Ok(instance)
}
pub fn request(&mut self, rotation_days: u8, now: DateTime<Utc>) -> Result<String> {
if rotation_days == 0 {
return Ok(self.context_id.clone());
}
let age = now - self.creation_timestamp;
if age >= Duration::days(rotation_days.into()) {
self.rotate_context_id(now);
}
Ok(self.context_id.clone())
}
pub fn rotate_context_id(&mut self, now: DateTime<Utc>) {
let original_context_id = self.context_id.clone();
self.context_id = Uuid::new_v4().to_string();
self.creation_timestamp = now;
self.persist();
// If we're running in test automation in the embedder, we don't want
// to be sending actual network requests to MARS.
if !self.running_in_test_automation {
let _ = self
.mars_client
.delete(original_context_id.clone())
.inspect_err(|e| error!("Failed to contact MARS: {}", e));
}
// In a perfect world, we'd call Glean ourselves here - however,
// our uniffi / Rust infrastructure doesn't yet support doing that,
// so we'll delegate to our embedder to send the Glean ping by
// calling a `rotated` callback method.
self.callback_handle
.read()
.rotated(original_context_id.clone());
}
pub fn force_rotation(&mut self, now: DateTime<Utc>) {
self.rotate_context_id(now);
}
pub fn persist(&self) {
self.callback_handle
.read()
.persist(self.context_id.clone(), self.creation_timestamp.timestamp());
}
pub fn unset_callback(&mut self) {
self.callback_handle = RwLock::new(Box::new(DefaultContextIdCallback));
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex};
// 1745859061 ~= the timestamp for when this test was written (Apr 28, 2025)
const FAKE_NOW_TS: i64 = 1745859061;
const TEST_CONTEXT_ID: &str = "decafbad-0cd1-0cd2-0cd3-decafbad1000";
// 1706763600 ~= Jan 1st, 2024, which is long ago compared to FAKE_NOW.
const FAKE_LONG_AGO_TS: i64 = 1706763600;
lazy_static! {
static ref FAKE_NOW: DateTime<Utc> = DateTime::from_timestamp(FAKE_NOW_TS, 0).unwrap();
static ref FAKE_LONG_AGO: DateTime<Utc> =
DateTime::from_timestamp(FAKE_LONG_AGO_TS, 0).unwrap();
}
pub struct TestMARSClient {
delete_called: Arc<Mutex<bool>>,
}
impl TestMARSClient {
pub fn new(delete_called: Arc<Mutex<bool>>) -> Self {
Self { delete_called }
}
}
impl MARSClient for TestMARSClient {
fn delete(&self, _context_id: String) -> crate::Result<()> {
*self.delete_called.lock().unwrap() = true;
Ok(())
}
}
fn with_test_mars<F: FnOnce(Box<dyn MARSClient + Send + Sync>, Arc<Mutex<bool>>)>(test: F) {
let delete_called = Arc::new(Mutex::new(false));
let mars = Box::new(TestMARSClient::new(Arc::clone(&delete_called)));
test(mars, delete_called);
}
#[test]
fn test_creation_timestamp_with_some_value() {
with_test_mars(|mars, delete_called| {
let creation_timestamp = FAKE_NOW_TS;
let component = ContextIDComponentInner::new(
TEST_CONTEXT_ID,
creation_timestamp,
false,
Box::new(DefaultContextIdCallback),
*FAKE_NOW,
mars,
)
.unwrap();
// We should have left the context_id and creation_timestamp
// untouched if a creation_timestamp was passed.
assert_eq!(component.context_id, TEST_CONTEXT_ID);
assert_eq!(component.creation_timestamp.timestamp(), creation_timestamp);
assert!(!*delete_called.lock().unwrap());
});
}
#[test]
fn test_creation_timestamp_with_zero_value() {
with_test_mars(|mars, delete_called| {
let component = ContextIDComponentInner::new(
TEST_CONTEXT_ID,
0,
false,
Box::new(DefaultContextIdCallback),
*FAKE_NOW,
mars,
)
.unwrap();
// If 0 was passed as the creation_timestamp, we'll interpret that
// as there having been no stored creation_timestamp. In that case,
// we'll use "now" as the creation_timestamp.
assert_eq!(component.context_id, TEST_CONTEXT_ID);
assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
assert!(!*delete_called.lock().unwrap());
});
}
#[test]
fn test_empty_initial_context_id() {
with_test_mars(|mars, delete_called| {
let component = ContextIDComponentInner::new(
"",
0,
false,
Box::new(DefaultContextIdCallback),
*FAKE_NOW,
mars,
)
.unwrap();
// We expect a new UUID to have been generated for context_id.
assert!(Uuid::parse_str(&component.context_id).is_ok());
assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
assert!(!*delete_called.lock().unwrap());
});
}
#[test]
fn test_empty_initial_context_id_with_creation_date() {
with_test_mars(|mars, delete_called| {
let component = ContextIDComponentInner::new(
"",
0,
false,
Box::new(DefaultContextIdCallback),
*FAKE_NOW,
mars,
)
.unwrap();
// We expect a new UUID to have been generated for context_id.
assert!(Uuid::parse_str(&component.context_id).is_ok());
assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
assert!(!*delete_called.lock().unwrap());
});
}
#[test]
fn test_invalid_context_id_with_no_creation_date() {
with_test_mars(|mars, delete_called| {
let component = ContextIDComponentInner::new(
"something-invalid",
0,
false,
Box::new(DefaultContextIdCallback),
*FAKE_NOW,
mars,
)
.unwrap();
// We expect a new UUID to have been generated for context_id.
assert!(Uuid::parse_str(&component.context_id).is_ok());
assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
assert!(!*delete_called.lock().unwrap());
});
}
#[test]
fn test_invalid_context_id_with_creation_date() {
with_test_mars(|mars, delete_called| {
let component = ContextIDComponentInner::new(
"something-invalid",
FAKE_LONG_AGO_TS,
false,
Box::new(DefaultContextIdCallback),
*FAKE_NOW,
mars,
)
.unwrap();
// We expect a new UUID to have been generated for context_id.
assert!(Uuid::parse_str(&component.context_id).is_ok());
assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
assert!(!*delete_called.lock().unwrap());
});
}
#[test]
fn test_request_no_rotation() {
with_test_mars(|mars, delete_called| {
// Let's create a context_id with a creation date far in the past.
let mut component = ContextIDComponentInner::new(
TEST_CONTEXT_ID,
FAKE_LONG_AGO_TS,
false,
Box::new(DefaultContextIdCallback),
*FAKE_NOW,
mars,
)
.unwrap();
// We expect neither the UUID nor creation_timestamp to have been changed.
assert_eq!(component.context_id, TEST_CONTEXT_ID);
assert_eq!(component.creation_timestamp, FAKE_LONG_AGO.clone());
// Now request the context_id, passing 0 for the rotation_days. We
// interpret this to mean "do not rotate".
assert_eq!(
component.request(0, *FAKE_NOW).unwrap(),
component.context_id
);
assert_eq!(component.creation_timestamp, FAKE_LONG_AGO.clone());
assert!(!*delete_called.lock().unwrap());
});
}
#[test]
fn test_request_with_rotation() {
with_test_mars(|mars, delete_called| {
struct FakeContextIdCallback {
rotated_called: Arc<Mutex<bool>>,
original_context_id: Arc<Mutex<Option<String>>>,
}
impl ContextIdCallback for FakeContextIdCallback {
fn persist(&self, _context_id: String, _creation_date: i64) {}
fn rotated(&self, original_context_id: String) {
*self.rotated_called.lock().unwrap() = true;
*self.original_context_id.lock().unwrap() = Some(original_context_id);
}
}
let rotated_called_flag = Arc::new(Mutex::new(false));
let original_context_id = Arc::new(Mutex::new(None));
let callback = FakeContextIdCallback {
rotated_called: Arc::clone(&rotated_called_flag),
original_context_id: Arc::clone(&original_context_id),
};
// Let's create a context_id with a creation date far in the past.
let mut component = ContextIDComponentInner::new(
TEST_CONTEXT_ID,
FAKE_LONG_AGO_TS,
false,
Box::new(callback),
*FAKE_NOW,
mars,
)
.unwrap();
// We expect neither the UUID nor creation_timestamp to have been changed.
assert_eq!(component.context_id, TEST_CONTEXT_ID);
assert_eq!(component.creation_timestamp, FAKE_LONG_AGO.clone());
// Now request the context_id, passing 2 for the rotation_days. Since
// the number of days since FAKE_LONG_AGO is greater than 2 days, we
// expect a new context_id to be generated, and the creation_timestamp
// to update to now.
assert!(Uuid::parse_str(&component.request(2, *FAKE_NOW).unwrap()).is_ok());
assert_ne!(component.context_id, TEST_CONTEXT_ID);
assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
assert!(*delete_called.lock().unwrap());
assert!(
*rotated_called_flag.lock().unwrap(),
"rotated() should have been called"
);
assert_eq!(
original_context_id.lock().unwrap().as_deref().unwrap(),
TEST_CONTEXT_ID
);
});
}
#[test]
fn test_force_rotation() {
with_test_mars(|mars, delete_called| {
struct FakeContextIdCallback {
rotated_called: Arc<Mutex<bool>>,
original_context_id: Arc<Mutex<Option<String>>>,
}
impl ContextIdCallback for FakeContextIdCallback {
fn persist(&self, _context_id: String, _creation_date: i64) {}
fn rotated(&self, original_context_id: String) {
*self.rotated_called.lock().unwrap() = true;
*self.original_context_id.lock().unwrap() = Some(original_context_id);
}
}
let rotated_called_flag = Arc::new(Mutex::new(false));
let original_context_id = Arc::new(Mutex::new(None));
let callback = FakeContextIdCallback {
rotated_called: Arc::clone(&rotated_called_flag),
original_context_id: Arc::clone(&original_context_id),
};
let mut component = ContextIDComponentInner::new(
TEST_CONTEXT_ID,
FAKE_LONG_AGO_TS,
false,
Box::new(callback),
*FAKE_NOW,
mars,
)
.unwrap();
component.force_rotation(*FAKE_NOW);
assert!(Uuid::parse_str(&component.request(2, *FAKE_NOW).unwrap()).is_ok());
assert_ne!(component.context_id, TEST_CONTEXT_ID);
assert_eq!(component.creation_timestamp, FAKE_NOW.clone());
assert!(*delete_called.lock().unwrap());
assert!(
*rotated_called_flag.lock().unwrap(),
"rotated() should have been called"
);
assert_eq!(
original_context_id.lock().unwrap().as_deref().unwrap(),
TEST_CONTEXT_ID
);
});
}
#[test]
fn test_accept_braces() {
with_test_mars(|mars, delete_called| {
// Some callers may store pre-existing context IDs with opening
// and closing curly braces. Our component should accept them, but
// return (and persist) UUIDs without such braces.
let wrapped_context_id = ["{", TEST_CONTEXT_ID, "}"].concat();
let mut component = ContextIDComponentInner::new(
&wrapped_context_id,
0,
false,
Box::new(DefaultContextIdCallback),
*FAKE_NOW,
mars,
)
.unwrap();
// We expect to be storing TEST_CONTEXT_ID, and to return
// TEST_CONTEXT_ID without the braces.
assert_eq!(component.context_id, TEST_CONTEXT_ID);
assert!(Uuid::parse_str(&component.request(0, *FAKE_NOW).unwrap()).is_ok());
assert!(!*delete_called.lock().unwrap());
});
}
#[test]
fn test_persist_callback() {
with_test_mars(|mars, delete_called| {
struct FakeContextIdCallback {
persist_called: Arc<Mutex<bool>>,
context_id: Arc<Mutex<Option<String>>>,
creation_timestamp: Arc<Mutex<Option<i64>>>,
}
impl ContextIdCallback for FakeContextIdCallback {
fn persist(&self, context_id: String, creation_date: i64) {
*self.persist_called.lock().unwrap() = true;
*self.context_id.lock().unwrap() = Some(context_id);
*self.creation_timestamp.lock().unwrap() = Some(creation_date);
}
fn rotated(&self, _original_context_id: String) {}
}
let persist_called_flag = Arc::new(Mutex::new(false));
let context_id = Arc::new(Mutex::new(None));
let creation_timestamp = Arc::new(Mutex::new(None));
let callback = FakeContextIdCallback {
persist_called: Arc::clone(&persist_called_flag),
context_id: Arc::clone(&context_id),
creation_timestamp: Arc::clone(&creation_timestamp),
};
let mut component = ContextIDComponentInner::new(
TEST_CONTEXT_ID,
FAKE_LONG_AGO_TS,
false,
Box::new(callback),
*FAKE_NOW,
mars,
)
.unwrap();
component.force_rotation(*FAKE_NOW);
assert!(
*persist_called_flag.lock().unwrap(),
"persist() should have been called"
);
assert!(
Uuid::parse_str(context_id.lock().unwrap().as_deref().unwrap()).is_ok(),
"persist() should have received a valid context_id string"
);
assert_eq!(
*creation_timestamp.lock().unwrap(),
Some(FAKE_NOW_TS),
"persist() should have received the expected creation date"
);
assert!(*delete_called.lock().unwrap());
});
}
}

67
third_party/rust/context_id/src/mars.rs vendored Normal file
View File

@@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use serde_json::json;
use url::Url;
use viaduct::Request;
const DEFAULT_MARS_API_ENDPOINT: &str = "https://ads.mozilla.org/v1/";
pub trait MARSClient: Sync + Send {
fn delete(&self, context_id: String) -> crate::Result<()>;
}
pub struct SimpleMARSClient {
endpoint: String,
}
impl SimpleMARSClient {
pub fn new() -> Self {
Self {
endpoint: DEFAULT_MARS_API_ENDPOINT.to_string(),
}
}
#[allow(dead_code)]
fn new_with_endpoint(endpoint: String) -> Self {
Self { endpoint }
}
}
impl MARSClient for SimpleMARSClient {
fn delete(&self, context_id: String) -> crate::Result<()> {
let delete_url = Url::parse(&format!("{}/delete_user", self.endpoint))?;
let body = json!({
"context_id": context_id,
})
.to_string();
let request = Request::delete(delete_url).body(body);
let _ = request.send()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::mock;
#[test]
fn test_delete() {
viaduct_reqwest::use_reqwest_backend();
let expected_context_id = "some-fake-context-id";
let expected_body = format!(r#"{{"context_id":"{}"}}"#, expected_context_id);
let m = mock("DELETE", "/delete_user")
.match_body(expected_body.as_str())
.create();
let client = SimpleMARSClient::new_with_endpoint(mockito::server_url());
let _ = client.delete(expected_context_id.to_string());
m.expect(1).assert();
}
}

View File

@@ -17,6 +17,7 @@ harness = false
[dependencies]
uniffi = { workspace = true }
context_id = "0.1"
tabs = "0.1"
search = "0.1"
suggest = "0.1"