Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ads-client-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Ads Client Tests

on:
push:
branches: [main]
paths:
- "components/ads-client/**"
pull_request:
branches: [main]
paths:
- "components/ads-client/**"

workflow_dispatch:

jobs:
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: "recursive"
- name: Install Rust
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
rustup toolchain install
- name: Run ads-client integration tests against MARS staging
run: cargo test -p ads-client-integration-tests -- --ignored
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
[workspace]
resolver = "2"

# Note: Any additions here should be repeated in default-members below.
# Note: Any additions here should be repeated in default-members below (unless
# the crate should not be built by default, e.g. integration-test-only crates).
members = [
"components/ads-client",
"components/ads-client/integration-tests",
"components/as-ohttp-client",
"components/autofill",
"components/context_id",
Expand Down
14 changes: 8 additions & 6 deletions components/ads-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,23 @@ cargo test -p ads-client

### Integration Tests

Integration tests make real HTTP calls to the Mozilla Ads Routing Service (MARS) and are not run automatically in CI. They are marked with `#[ignore]` and must be run manually.
Integration tests make real HTTP calls to the Mozilla Ads Routing Service (MARS) staging environment. They live in a dedicated crate (`integration-tests/`) and are marked `#[ignore]` so they do not run with a plain `cargo test`.

To run integration tests:
They are run by the dedicated GitHub Actions workflow (`.github/workflows/ads-client-tests.yaml`), and can also be run manually:

```shell
cargo test -p ads-client --test integration_test -- --ignored
cargo test -p ads-client-integration-tests -- --ignored
```

To run a specific integration test:
To run a specific test file or test:

```shell
cargo test -p ads-client --test integration_test -- --ignored test_mock_pocket_billboard_1_placement
cargo test -p ads-client-integration-tests --test mars -- --ignored
cargo test -p ads-client-integration-tests --test http_cache -- --ignored
cargo test -p ads-client-integration-tests --test mars test_contract_image_staging -- --ignored
```

**Note:** Integration tests require network access and will make real HTTP requests to the MARS API.
**Note:** Integration tests require network access and will make real HTTP requests to the MARS staging API.

## Usage

Expand Down
17 changes: 17 additions & 0 deletions components/ads-client/integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "ads-client-integration-tests"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
publish = false

[dependencies]

[dev-dependencies]
ads-client = { path = ".." }
serde_json = "1"
url = "2"
mockito = { version = "0.31", default-features = false }
viaduct = { path = "../../../components/viaduct" }
viaduct-dev = { path = "../../../components/support/viaduct-dev" }
viaduct-hyper = { path = "../../../components/support/viaduct-hyper" }
94 changes: 94 additions & 0 deletions components/ads-client/integration-tests/tests/http_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* 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 std::hash::{Hash, Hasher};
use std::time::Duration;

use ads_client::http_cache::{ByteSize, CacheMode, CacheOutcome, HttpCache, RequestCachePolicy};
use mockito::mock;
use viaduct::Request;

/// Test-only hashable wrapper around Request.
#[derive(Clone)]
struct TestRequest(Request);

impl Hash for TestRequest {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.method.as_str().hash(state);
self.0.url.as_str().hash(state);
}
}

impl From<TestRequest> for Request {
fn from(t: TestRequest) -> Self {
t.0
}
}

#[test]
#[ignore = "integration test: run manually with -- --ignored"]
fn test_cache_works_using_real_timeouts() {
viaduct_dev::init_backend_dev();

let cache = HttpCache::<TestRequest>::builder("integration_tests.db")
.default_ttl(Duration::from_secs(60))
.max_size(ByteSize::mib(1))
.build()
.expect("cache build should succeed");

let url = format!("{}/v1/ads", mockito::server_url()).parse().unwrap();
let req = TestRequest(Request::post(url).json(&serde_json::json!({
"context_id": "12347fff-00b0-aaaa-0978-189231239808",
"placements": [
{
"placement": "mock_pocket_billboard_1",
"count": 1,
}
],
})));

let test_ttl = 2;

let _m1 = mock("POST", "/v1/ads")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true}"#)
.expect(1)
.create();

// First call: miss -> store
let (_, outcomes) = cache
.send_with_policy(
req.clone(),
&RequestCachePolicy {
mode: CacheMode::CacheFirst,
ttl_seconds: Some(test_ttl),
},
)
.unwrap();
assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored));

// Second call: hit (no extra HTTP due to expect(1))
let (response, outcomes) = cache
.send_with_policy(req.clone(), &RequestCachePolicy::default())
.unwrap();
assert!(matches!(outcomes.last().unwrap(), CacheOutcome::Hit));
assert_eq!(response.status, 200);

let _m2 = mock("POST", "/v1/ads")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"ok":true}"#)
.expect(1)
.create();

// Third call: Miss due to timeout for the test_ttl duration
std::thread::sleep(Duration::from_secs(test_ttl));
let (response, outcomes) = cache
.send_with_policy(req, &RequestCachePolicy::default())
.unwrap();
assert!(matches!(outcomes.last().unwrap(), CacheOutcome::MissStored));
assert_eq!(response.status, 200);
}
84 changes: 84 additions & 0 deletions components/ads-client/integration-tests/tests/mars.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* 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 std::sync::Arc;

use ads_client::{
MozAdsClientBuilder, MozAdsEnvironment, MozAdsPlacementRequest, MozAdsPlacementRequestWithCount,
};

fn init_backend() {
// Err means the backend is already initialized.
let _ = viaduct_hyper::viaduct_init_backend_hyper();
}

fn staging_client() -> ads_client::MozAdsClient {
Arc::new(MozAdsClientBuilder::new())
.environment(MozAdsEnvironment::Staging)
.build()
}

#[test]
#[ignore = "integration test: run manually with -- --ignored"]
fn test_contract_image_staging() {
init_backend();

let client = staging_client();
let result = client.request_image_ads(
vec![MozAdsPlacementRequest {
placement_id: "mock_billboard_1".to_string(),
iab_content: None,
}],
None,
);

assert!(
result.is_ok(),
"Image ad request failed: {:?}",
result.err()
);
let placements = result.unwrap();
assert!(placements.contains_key("mock_billboard_1"));
}

#[test]
#[ignore = "integration test: run manually with -- --ignored"]
fn test_contract_spoc_staging() {
init_backend();

let client = staging_client();
let result = client.request_spoc_ads(
vec![MozAdsPlacementRequestWithCount {
placement_id: "mock_spoc_1".to_string(),
count: 3,
iab_content: None,
}],
None,
);

assert!(result.is_ok(), "Spoc ad request failed: {:?}", result.err());
let placements = result.unwrap();
assert!(placements.contains_key("mock_spoc_1"));
assert!(placements.get("mock_spoc_1").unwrap().len() == 3);
}

#[test]
#[ignore = "integration test: run manually with -- --ignored"]
fn test_contract_tile_staging() {
init_backend();

let client = staging_client();
let result = client.request_tile_ads(
vec![MozAdsPlacementRequest {
placement_id: "mock_tile_1".to_string(),
iab_content: None,
}],
None,
);

assert!(result.is_ok(), "Tile ad request failed: {:?}", result.err());
let placements = result.unwrap();
assert!(placements.contains_key("mock_tile_1"));
}
Loading
Loading