Skip to content

Commit 833cc3d

Browse files
authored
feat(ads-client): handle different ad types (#7073)
* feat(ads-client): handle different ad types * test(ads-client): test all three ad variants in client * refactor(ads-client): deserialize ad using generic * test(ads-client): setup context id component for tests * doc(ads-client): rename UATile to Tile and update usage doc with new names
1 parent d4753ab commit 833cc3d

File tree

11 files changed

+831
-589
lines changed

11 files changed

+831
-589
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
## ✨ What's New ✨
1919

20+
### Ads Client
21+
- Add support for three ad types: Image, Spoc (Sponsored Content), and UA Tile
22+
2023
### Autofill
2124
- Adds a migration to migrate users to use subregion codes over fully qualified strings. ([bug 1993388](https://bugzilla.mozilla.org/show_bug.cgi?id=1993388))
2225
- Added credit card verification logic ([#7047](https://github.com/mozilla/application-services/pull/7047)).

components/ads-client/docs/usage.md

Lines changed: 180 additions & 67 deletions
Large diffs are not rendered by default.

components/ads-client/src/client.rs

Lines changed: 159 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
use std::collections::HashMap;
77
use std::time::Duration;
88

9-
use crate::client::ad_response::Ad;
9+
use crate::client::ad_response::{AdImage, AdResponse, AdSpoc, AdTile};
1010
use crate::client::config::AdsClientConfig;
1111
use crate::error::{RecordClickError, RecordImpressionError, ReportAdError, RequestAdsError};
1212
use crate::http_cache::{HttpCache, RequestCachePolicy};
13-
use crate::mars::{DefaultMARSClient, MARSClient};
13+
use crate::mars::MARSClient;
1414
use ad_request::{AdPlacementRequest, AdRequest};
15+
use context_id::{ContextIDComponent, DefaultContextIdCallback};
16+
use serde::de::DeserializeOwned;
1517
use url::Url;
1618
use uuid::Uuid;
1719

@@ -25,7 +27,8 @@ const DEFAULT_TTL_SECONDS: u64 = 300;
2527
const DEFAULT_MAX_CACHE_SIZE_MIB: u64 = 10;
2628

2729
pub struct AdsClient {
28-
client: Box<dyn MARSClient>,
30+
client: MARSClient,
31+
context_id_component: ContextIDComponent,
2932
}
3033

3134
impl AdsClient {
@@ -34,6 +37,13 @@ impl AdsClient {
3437

3538
let client_config = client_config.unwrap_or_default();
3639

40+
let context_id_component = ContextIDComponent::new(
41+
&context_id,
42+
0,
43+
cfg!(test),
44+
Box::new(DefaultContextIdCallback),
45+
);
46+
3747
// Configure the cache if a path is provided.
3848
// Defaults for ttl and cache size are also set if unspecified.
3949
if let Some(cache_cfg) = client_config.cache_config {
@@ -51,32 +61,60 @@ impl AdsClient {
5161
.build()
5262
.ok(); // TODO: handle error with telemetry
5363

54-
let client = Box::new(DefaultMARSClient::new(
55-
context_id,
56-
client_config.environment,
57-
http_cache,
58-
));
59-
return Self { client };
64+
let client = MARSClient::new(client_config.environment, http_cache);
65+
return Self {
66+
context_id_component,
67+
client,
68+
};
6069
}
6170

62-
let client = Box::new(DefaultMARSClient::new(
63-
context_id,
64-
client_config.environment,
65-
None,
66-
));
67-
Self { client }
71+
let client = MARSClient::new(client_config.environment, None);
72+
Self {
73+
context_id_component,
74+
client,
75+
}
6876
}
6977

70-
pub fn request_ads(
78+
fn request_ads<T>(
7179
&self,
7280
ad_placement_requests: Vec<AdPlacementRequest>,
7381
options: Option<RequestCachePolicy>,
74-
) -> Result<HashMap<String, Vec<Ad>>, RequestAdsError> {
75-
let ad_request = AdRequest::build(self.client.get_context_id()?, ad_placement_requests)?;
82+
) -> Result<AdResponse<T>, RequestAdsError>
83+
where
84+
T: DeserializeOwned,
85+
{
86+
let context_id = self.get_context_id()?;
87+
let ad_request = AdRequest::build(context_id, ad_placement_requests)?;
7688
let cache_policy = options.unwrap_or_default();
7789
let response = self.client.fetch_ads(&ad_request, &cache_policy)?;
78-
let placements = response.build_placements(&ad_request)?;
79-
Ok(placements)
90+
Ok(response)
91+
}
92+
93+
pub fn request_image_ads(
94+
&self,
95+
ad_placement_requests: Vec<AdPlacementRequest>,
96+
options: Option<RequestCachePolicy>,
97+
) -> Result<HashMap<String, AdImage>, RequestAdsError> {
98+
let response = self.request_ads::<AdImage>(ad_placement_requests, options)?;
99+
Ok(response.take_first())
100+
}
101+
102+
pub fn request_spoc_ads(
103+
&self,
104+
ad_placement_requests: Vec<AdPlacementRequest>,
105+
options: Option<RequestCachePolicy>,
106+
) -> Result<HashMap<String, Vec<AdSpoc>>, RequestAdsError> {
107+
let response = self.request_ads::<AdSpoc>(ad_placement_requests, options)?;
108+
Ok(response.data)
109+
}
110+
111+
pub fn request_tile_ads(
112+
&self,
113+
ad_placement_requests: Vec<AdPlacementRequest>,
114+
options: Option<RequestCachePolicy>,
115+
) -> Result<HashMap<String, AdTile>, RequestAdsError> {
116+
let response = self.request_ads::<AdTile>(ad_placement_requests, options)?;
117+
Ok(response.take_first())
80118
}
81119

82120
pub fn record_impression(&self, impression_url: Url) -> Result<(), RecordImpressionError> {
@@ -92,8 +130,14 @@ impl AdsClient {
92130
Ok(())
93131
}
94132

133+
pub fn get_context_id(&self) -> context_id::ApiResult<String> {
134+
self.context_id_component.request(0)
135+
}
136+
95137
pub fn cycle_context_id(&mut self) -> context_id::ApiResult<String> {
96-
self.client.cycle_context_id()
138+
let old_context_id = self.get_context_id()?;
139+
self.context_id_component.force_rotation()?;
140+
Ok(old_context_id)
97141
}
98142

99143
pub fn clear_cache(&self) -> Result<(), HttpCacheError> {
@@ -103,78 +147,123 @@ impl AdsClient {
103147

104148
#[cfg(test)]
105149
mod tests {
106-
use url::Url;
107-
108-
use crate::{
109-
client::ad_request::{AdContentCategory, IABContentTaxonomy},
110-
mars::MockMARSClient,
111-
test_utils::{
112-
get_example_happy_ad_response, get_example_happy_placements,
113-
make_happy_placement_requests,
114-
},
150+
use crate::test_utils::{
151+
get_example_happy_image_response, get_example_happy_spoc_response,
152+
get_example_happy_uatile_response, make_happy_placement_requests,
115153
};
116154

117155
use super::*;
118156

119157
#[test]
120-
fn test_request_ads_happy() {
121-
let mut mock = MockMARSClient::new();
122-
mock.expect_fetch_ads()
123-
.returning(|_req, _| Ok(get_example_happy_ad_response()));
124-
mock.expect_get_context_id()
125-
.returning(|| Ok("mock-context-id".to_string()));
158+
fn test_get_context_id() {
159+
let client = AdsClient::new(None);
160+
let context_id = client.get_context_id().unwrap();
161+
assert!(!context_id.is_empty());
162+
}
126163

127-
mock.expect_get_mars_endpoint()
128-
.return_const(Url::parse("https://mock.endpoint/ads").unwrap());
164+
#[test]
165+
fn test_cycle_context_id() {
166+
let mut client = AdsClient::new(None);
167+
let old_id = client.get_context_id().unwrap();
168+
let previous_id = client.cycle_context_id().unwrap();
169+
assert_eq!(previous_id, old_id);
170+
let new_id = client.get_context_id().unwrap();
171+
assert_ne!(new_id, old_id);
172+
}
129173

174+
#[test]
175+
fn test_request_image_ads_happy() {
176+
use crate::test_utils::create_test_client;
177+
use context_id::{ContextIDComponent, DefaultContextIdCallback};
178+
viaduct_dev::init_backend_dev();
179+
180+
let expected_response = get_example_happy_image_response();
181+
let _m = mockito::mock("POST", "/ads")
182+
.with_status(200)
183+
.with_header("content-type", "application/json")
184+
.with_body(serde_json::to_string(&expected_response).unwrap())
185+
.create();
186+
187+
let mars_client = create_test_client(mockito::server_url());
188+
let context_id_component = ContextIDComponent::new(
189+
&uuid::Uuid::new_v4().to_string(),
190+
0,
191+
false,
192+
Box::new(DefaultContextIdCallback),
193+
);
130194
let component = AdsClient {
131-
client: Box::new(mock),
195+
context_id_component,
196+
client: mars_client,
132197
};
133198

134199
let ad_placement_requests = make_happy_placement_requests();
135200

136-
let result = component.request_ads(ad_placement_requests, None);
201+
let result = component.request_image_ads(ad_placement_requests, None);
137202

138203
assert!(result.is_ok());
139204
}
140205

141206
#[test]
142-
fn test_request_ads_multiset_happy() {
143-
let mut mock = MockMARSClient::new();
144-
mock.expect_fetch_ads()
145-
.returning(|_req, _| Ok(get_example_happy_ad_response()));
146-
mock.expect_get_context_id()
147-
.returning(|| Ok("mock-context-id".to_string()));
207+
fn test_request_spocs_happy() {
208+
use crate::test_utils::create_test_client;
209+
use context_id::{ContextIDComponent, DefaultContextIdCallback};
210+
viaduct_dev::init_backend_dev();
211+
212+
let expected_response = get_example_happy_spoc_response();
213+
let _m = mockito::mock("POST", "/ads")
214+
.with_status(200)
215+
.with_header("content-type", "application/json")
216+
.with_body(serde_json::to_string(&expected_response).unwrap())
217+
.create();
218+
219+
let mars_client = create_test_client(mockito::server_url());
220+
let context_id_component = ContextIDComponent::new(
221+
&uuid::Uuid::new_v4().to_string(),
222+
0,
223+
false,
224+
Box::new(DefaultContextIdCallback),
225+
);
226+
let component = AdsClient {
227+
context_id_component,
228+
client: mars_client,
229+
};
148230

149-
mock.expect_get_mars_endpoint()
150-
.return_const(Url::parse("https://mock.endpoint/ads").unwrap());
231+
let ad_placement_requests = make_happy_placement_requests();
232+
233+
let result = component.request_spoc_ads(ad_placement_requests, None);
234+
235+
assert!(result.is_ok());
236+
}
151237

238+
#[test]
239+
fn test_request_tiles_happy() {
240+
use crate::test_utils::create_test_client;
241+
use context_id::{ContextIDComponent, DefaultContextIdCallback};
242+
viaduct_dev::init_backend_dev();
243+
244+
let expected_response = get_example_happy_uatile_response();
245+
let _m = mockito::mock("POST", "/ads")
246+
.with_status(200)
247+
.with_header("content-type", "application/json")
248+
.with_body(serde_json::to_string(&expected_response).unwrap())
249+
.create();
250+
251+
let mars_client = create_test_client(mockito::server_url());
252+
let context_id_component = ContextIDComponent::new(
253+
&uuid::Uuid::new_v4().to_string(),
254+
0,
255+
false,
256+
Box::new(DefaultContextIdCallback),
257+
);
152258
let component = AdsClient {
153-
client: Box::new(mock),
259+
context_id_component,
260+
client: mars_client,
154261
};
155262

156-
let ad_placement_requests: Vec<AdPlacementRequest> = vec![
157-
AdPlacementRequest {
158-
placement: "example_placement_1".to_string(),
159-
count: 1,
160-
content: Some(AdContentCategory {
161-
taxonomy: IABContentTaxonomy::IAB2_1,
162-
categories: vec!["entertainment".to_string()],
163-
}),
164-
},
165-
AdPlacementRequest {
166-
placement: "example_placement_2".to_string(),
167-
count: 2,
168-
content: Some(AdContentCategory {
169-
taxonomy: IABContentTaxonomy::IAB3_0,
170-
categories: vec![],
171-
}),
172-
},
173-
];
174-
175-
let result = component.request_ads(ad_placement_requests, None);
263+
let ad_placement_requests = make_happy_placement_requests();
264+
265+
let result = component.request_tile_ads(ad_placement_requests, None);
176266

177267
assert!(result.is_ok());
178-
assert_eq!(result.unwrap(), get_example_happy_placements());
179268
}
180269
}

components/ads-client/src/client/ad_request.rs

Lines changed: 1 addition & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,7 @@ pub enum IABContentTaxonomy {
8989

9090
#[cfg(test)]
9191
mod tests {
92-
use crate::test_utils::{
93-
get_example_happy_ad_response, get_example_happy_placements, make_happy_placement_requests,
94-
TEST_CONTEXT_ID,
95-
};
92+
use crate::test_utils::TEST_CONTEXT_ID;
9693

9794
use super::*;
9895
use serde_json::{json, to_value};
@@ -224,53 +221,4 @@ mod tests {
224221
let request = AdRequest::build(TEST_CONTEXT_ID.to_string(), vec![]);
225222
assert!(request.is_err());
226223
}
227-
228-
#[test]
229-
fn test_build_placements_with_empty_placement_in_response() {
230-
let mut ad_placement_requests = make_happy_placement_requests();
231-
// Adding an extra placement request
232-
ad_placement_requests.push(AdPlacementRequest {
233-
placement: "example_placement_3".to_string(),
234-
count: 1,
235-
content: Some(AdContentCategory {
236-
taxonomy: IABContentTaxonomy::IAB2_1,
237-
categories: vec![],
238-
}),
239-
});
240-
241-
let mut api_resp = get_example_happy_ad_response();
242-
api_resp
243-
.data
244-
.insert("example_placement_3".to_string(), vec![]);
245-
246-
let ad_request =
247-
AdRequest::build(TEST_CONTEXT_ID.to_string(), ad_placement_requests).unwrap();
248-
249-
let placements = api_resp.build_placements(&ad_request).unwrap();
250-
251-
assert_eq!(placements, get_example_happy_placements());
252-
}
253-
254-
#[test]
255-
fn test_request_ads_with_missing_callback_in_response() {
256-
let mut ad_placement_requests = make_happy_placement_requests();
257-
// Adding an extra placement request
258-
ad_placement_requests.push(AdPlacementRequest {
259-
placement: "example_placement_3".to_string(),
260-
count: 1,
261-
content: Some(AdContentCategory {
262-
taxonomy: IABContentTaxonomy::IAB2_1,
263-
categories: vec![],
264-
}),
265-
});
266-
267-
let ad_request =
268-
AdRequest::build(TEST_CONTEXT_ID.to_string(), ad_placement_requests).unwrap();
269-
270-
let placements = get_example_happy_ad_response()
271-
.build_placements(&ad_request)
272-
.unwrap();
273-
274-
assert_eq!(placements, get_example_happy_placements());
275-
}
276224
}

0 commit comments

Comments
 (0)