From 571826e9d439a7849d3ff2c577843006e6438b7d Mon Sep 17 00:00:00 2001 From: ahanot Date: Mon, 9 Mar 2026 15:00:25 -0400 Subject: [PATCH 1/5] feat(ads-client): add contract tests to validate types against MARS OpenAPI spec Add two layers of contract validation: 1. Unit contract tests (run on every cargo test, zero cost): - ad_response.rs: spec-accurate JSON fixtures for Image, Spoc, and Tile deserialization, including tests that document intentional deviations (click/impression required by us but optional in spec; attributions field not modelled but silently ignored) - ad_request.rs: serialization shape tests and taxonomy string validation 2. Integration contract tests against staging (ignored by default): - Three new #[ignore] tests in integration_test.rs that hit ads.allizom.org and assert all required spec fields are present and non-empty for each ad type (Image, Spoc, Tile) - Run with: cargo test -p ads-client --test integration_test -- --ignored --- .../ads-client/src/client/ad_request.rs | 86 ++++++++ .../ads-client/src/client/ad_response.rs | 196 ++++++++++++++++++ .../ads-client/tests/integration_test.rs | 148 ++++++++++++- 3 files changed, 429 insertions(+), 1 deletion(-) diff --git a/components/ads-client/src/client/ad_request.rs b/components/ads-client/src/client/ad_request.rs index 031406d2cd..10a627c81f 100644 --- a/components/ads-client/src/client/ad_request.rs +++ b/components/ads-client/src/client/ad_request.rs @@ -274,6 +274,92 @@ mod tests { assert_eq!(RequestHash::new(&req1), RequestHash::new(&req2)); } + /// Contract tests: validate our request serialization against the MARS OpenAPI spec. + /// + /// The spec lives at: + /// https://ads.mozilla.org/assets/docs/openapi/mars-api.html#operation/getAds + mod contract { + use super::*; + use serde_json::{json, to_value}; + + #[test] + fn test_request_serializes_to_spec_shape() { + let url: Url = "https://ads.mozilla.org/v1/ads".parse().unwrap(); + let request = AdRequest::try_new( + "decafbad-0cd1-0cd2-0cd3-decafbad1000".to_string(), + vec![AdPlacementRequest { + placement: "newtab_tile_1".to_string(), + count: 2, + content: Some(AdContentCategory { + taxonomy: IABContentTaxonomy::IAB2_1, + categories: vec!["technology".to_string()], + }), + }], + url, + ) + .unwrap(); + + let json = to_value(&request).unwrap(); + + // Spec requires: context_id (UUID string), placements (non-empty array) + assert_eq!(json["context_id"], "decafbad-0cd1-0cd2-0cd3-decafbad1000"); + assert!(json["placements"].is_array()); + assert_eq!(json["placements"][0]["placement"], "newtab_tile_1"); + assert_eq!(json["placements"][0]["count"], 2); + assert_eq!(json["placements"][0]["content"]["taxonomy"], "IAB-2.1"); + assert_eq!( + json["placements"][0]["content"]["categories"][0], + "technology" + ); + // url must NOT appear in the body (it is sent as the HTTP endpoint) + assert!(json.get("url").is_none()); + } + + #[test] + fn test_taxonomy_values_match_spec_strings() { + // Spec enum: "IAB-1.0" | "IAB-2.0" | "IAB-2.1" | "IAB-2.2" | "IAB-3.0" + let cases = [ + (IABContentTaxonomy::IAB1_0, "IAB-1.0"), + (IABContentTaxonomy::IAB2_0, "IAB-2.0"), + (IABContentTaxonomy::IAB2_1, "IAB-2.1"), + (IABContentTaxonomy::IAB2_2, "IAB-2.2"), + (IABContentTaxonomy::IAB3_0, "IAB-3.0"), + ]; + for (variant, expected) in cases { + let json = to_value(&variant).unwrap(); + assert_eq!( + json, + json!(expected), + "IABContentTaxonomy variant should serialize to {expected}" + ); + } + } + + // INTENTIONAL OMISSION: The MARS spec supports optional `blocks` (array + // of block_key strings) and `consent` (object with gpp string) fields on + // the request body. These are not yet modelled because no current embedder + // needs them. Adding them is additive and non-breaking when needed. + #[test] + fn test_optional_spec_fields_not_present_in_serialized_request() { + let url: Url = "https://ads.mozilla.org/v1/ads".parse().unwrap(); + let request = AdRequest::try_new( + "decafbad-0cd1-0cd2-0cd3-decafbad1000".to_string(), + vec![AdPlacementRequest { + placement: "newtab_tile_1".to_string(), + count: 1, + content: None, + }], + url, + ) + .unwrap(); + + let json = to_value(&request).unwrap(); + // blocks and consent are not modelled — confirm they are absent + assert!(json.get("blocks").is_none(), "blocks field should not be serialized"); + assert!(json.get("consent").is_none(), "consent field should not be serialized"); + } + } + #[test] fn test_different_placements_produce_different_hash() { use crate::http_cache::RequestHash; diff --git a/components/ads-client/src/client/ad_response.rs b/components/ads-client/src/client/ad_response.rs index 8cf72206fc..d5bf19825e 100644 --- a/components/ads-client/src/client/ad_response.rs +++ b/components/ads-client/src/client/ad_response.rs @@ -469,6 +469,202 @@ mod tests { .contains("request_hash=abc123def456")); } + /// Contract tests: validate our types against the MARS OpenAPI spec. + /// + /// These tests use JSON fixtures that mirror the MARS API spec and document + /// any intentional deviations from it. The spec lives at: + /// https://ads.mozilla.org/assets/docs/openapi/mars-api.html#operation/getAds + mod contract { + use super::*; + use serde_json::json; + + // ── Image ────────────────────────────────────────────────────────────── + + #[test] + fn test_image_deserializes_from_spec_example() { + let json = json!({ + "url": "https://example.com/landing", + "callbacks": { + "click": "https://ads.mozilla.org/v1/t?click", + "impression": "https://ads.mozilla.org/v1/t?impression", + "report": "https://ads.mozilla.org/v1/t?report" + }, + "format": "billboard", + "image_url": "https://example.com/image.png", + "alt_text": "Example ad", + "block_key": "block-abc-123" + }); + let ad: AdImage = + serde_json::from_value(json).expect("Image should deserialize from spec JSON"); + assert_eq!(ad.format, "billboard"); + assert_eq!(ad.block_key, "block-abc-123"); + assert_eq!(ad.alt_text, Some("Example ad".to_string())); + assert_eq!(ad.callbacks.report.unwrap().as_str(), "https://ads.mozilla.org/v1/t?report"); + } + + #[test] + fn test_image_without_optional_fields() { + let json = json!({ + "url": "https://example.com/landing", + "callbacks": { + "click": "https://ads.mozilla.org/v1/t?click", + "impression": "https://ads.mozilla.org/v1/t?impression" + }, + "format": "rectangle", + "image_url": "https://example.com/image.png", + "block_key": "block-abc-123" + }); + let ad: AdImage = serde_json::from_value(json) + .expect("Image should deserialize without optional fields"); + assert_eq!(ad.alt_text, None); + assert_eq!(ad.callbacks.report, None); + } + + // INTENTIONAL DEVIATION: The MARS spec marks `click` and `impression` + // inside AdCallbacks as optional strings. We require them as non-optional + // `Url` fields because an ad without tracking callbacks is not + // actionable — we'd rather drop it (AdResponse::parse silently skips + // items that fail to deserialize) than show a broken ad. + #[test] + fn test_image_missing_click_fails_deserialization() { + let json = json!({ + "url": "https://example.com/landing", + "callbacks": { "impression": "https://ads.mozilla.org/v1/t?impression" }, + "format": "rectangle", + "image_url": "https://example.com/image.png", + "block_key": "block-abc-123" + }); + assert!( + serde_json::from_value::(json).is_err(), + "Image without click URL should fail to deserialize" + ); + } + + #[test] + fn test_image_missing_impression_fails_deserialization() { + let json = json!({ + "url": "https://example.com/landing", + "callbacks": { "click": "https://ads.mozilla.org/v1/t?click" }, + "format": "rectangle", + "image_url": "https://example.com/image.png", + "block_key": "block-abc-123" + }); + assert!( + serde_json::from_value::(json).is_err(), + "Image without impression URL should fail to deserialize" + ); + } + + // INTENTIONAL OMISSION: The MARS spec includes an `attributions` object + // (partner_id UUID + index number) on all ad types. We do not model it + // because it is not needed by any current embedder. serde ignores unknown + // fields by default so this is safe — we just don't surface the data. + #[test] + fn test_unknown_attributions_field_ignored() { + let json = json!({ + "url": "https://example.com/landing", + "callbacks": { + "click": "https://ads.mozilla.org/v1/t?click", + "impression": "https://ads.mozilla.org/v1/t?impression" + }, + "format": "billboard", + "image_url": "https://example.com/image.png", + "block_key": "block-abc-123", + "attributions": { + "partner_id": "00000000-0000-0000-0000-000000000001", + "index": 0 + } + }); + assert!( + serde_json::from_value::(json).is_ok(), + "Unknown attributions field should be ignored gracefully" + ); + } + + // ── Spoc ─────────────────────────────────────────────────────────────── + + #[test] + fn test_spoc_deserializes_from_spec_example() { + let json = json!({ + "url": "https://example.com/article", + "callbacks": { + "click": "https://ads.mozilla.org/v1/t?click", + "impression": "https://ads.mozilla.org/v1/t?impression" + }, + "format": "spoc", + "image_url": "https://example.com/image.png", + "title": "Sponsored article title", + "domain": "example.com", + "excerpt": "A short excerpt of the sponsored content.", + "sponsor": "Example Sponsor", + "sponsored_by_override": null, + "block_key": "block-spoc-123", + "caps": { "cap_key": "example-cap", "day": 3 }, + "ranking": { + "priority": 1, + "personalization_models": { "model_a": 42 }, + "item_score": 0.95 + } + }); + let ad: AdSpoc = + serde_json::from_value(json).expect("Spoc should deserialize from spec JSON"); + assert_eq!(ad.format, "spoc"); + assert_eq!(ad.domain, "example.com"); + assert_eq!(ad.sponsor, "Example Sponsor"); + assert_eq!(ad.caps.day, 3); + assert_eq!(ad.caps.cap_key, "example-cap"); + assert_eq!(ad.ranking.priority, 1); + assert!((ad.ranking.item_score - 0.95).abs() < f64::EPSILON); + assert_eq!(ad.ranking.personalization_models.unwrap()["model_a"], 42); + } + + #[test] + fn test_spoc_without_optional_fields() { + let json = json!({ + "url": "https://example.com/article", + "callbacks": { + "click": "https://ads.mozilla.org/v1/t?click", + "impression": "https://ads.mozilla.org/v1/t?impression" + }, + "format": "spoc", + "image_url": "https://example.com/image.png", + "title": "Sponsored article title", + "domain": "example.com", + "excerpt": "A short excerpt.", + "sponsor": "Example Sponsor", + "block_key": "block-spoc-123", + "caps": { "cap_key": "example-cap", "day": 3 }, + "ranking": { "priority": 1, "item_score": 0.5 } + }); + let ad: AdSpoc = serde_json::from_value(json) + .expect("Spoc should deserialize without optional fields"); + assert_eq!(ad.sponsored_by_override, None); + assert_eq!(ad.ranking.personalization_models, None); + } + + // ── Tile ─────────────────────────────────────────────────────────────── + + #[test] + fn test_tile_deserializes_from_spec_example() { + let json = json!({ + "url": "https://example.com", + "callbacks": { + "click": "https://ads.mozilla.org/v1/t?click", + "impression": "https://ads.mozilla.org/v1/t?impression" + }, + "format": "tile", + "image_url": "https://example.com/logo.png", + "name": "Example Site", + "block_key": "block-tile-123" + }); + let ad: AdTile = + serde_json::from_value(json).expect("Tile should deserialize from spec JSON"); + assert_eq!(ad.format, "tile"); + assert_eq!(ad.name, "Example Site"); + assert_eq!(ad.block_key, "block-tile-123"); + } + } + #[test] fn test_pop_request_hash_from_url() { let mut url_with_hash = diff --git a/components/ads-client/tests/integration_test.rs b/components/ads-client/tests/integration_test.rs index b73d8fa542..31de61c87b 100644 --- a/components/ads-client/tests/integration_test.rs +++ b/components/ads-client/tests/integration_test.rs @@ -8,8 +8,9 @@ use std::time::Duration; use ads_client::{ http_cache::{ByteSize, CacheOutcome, HttpCache, RequestCachePolicy}, - MozAdsClientBuilder, MozAdsPlacementRequest, MozAdsPlacementRequestWithCount, + MozAdsClientBuilder, MozAdsEnvironment, MozAdsPlacementRequest, MozAdsPlacementRequestWithCount, }; +use std::sync::Arc; use url::Url; use viaduct::Request; @@ -30,6 +31,151 @@ impl From for Request { } } +// ── Contract tests against the MARS staging server ──────────────────────────── +// +// These tests validate that our Rust types can round-trip real responses from +// the MARS staging environment (ads.allizom.org). They are #[ignore] by default +// and should be run: +// - manually: cargo test -p ads-client --test integration_test -- --ignored +// - in CI: a dedicated Taskcluster task gated on components/ads-client/** changes +// +// If a test fails it means either our types have drifted from the MARS schema +// or the staging server is returning unexpected data — both are worth investigating. + +/// Build a client pointed at the MARS staging server. +fn staging_client() -> ads_client::MozAdsClient { + Arc::new(MozAdsClientBuilder::new()) + .environment(MozAdsEnvironment::Staging) + .build() +} + +#[test] +#[ignore = "contract test: run manually or in dedicated CI against ads.allizom.org"] +fn test_contract_image_staging() { + viaduct_dev::init_backend_dev(); + + let client = staging_client(); + let result = client.request_image_ads( + vec![MozAdsPlacementRequest { + placement_id: "mock_pocket_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_pocket_billboard_1"), + "Response missing expected placement key" + ); + + let ad = placements + .get("mock_pocket_billboard_1") + .expect("Placement should exist"); + + // Assert all required spec fields are present and non-empty + assert!(!ad.block_key.is_empty(), "block_key should be non-empty"); + assert!(!ad.format.is_empty(), "format should be non-empty"); + assert!(!ad.image_url.is_empty(), "image_url should be non-empty"); + assert!(!ad.url.is_empty(), "url should be non-empty"); + assert!( + !ad.callbacks.click.as_str().is_empty(), + "callbacks.click should be non-empty" + ); + assert!( + !ad.callbacks.impression.as_str().is_empty(), + "callbacks.impression should be non-empty" + ); +} + +#[test] +#[ignore = "contract test: run manually or in dedicated CI against ads.allizom.org"] +fn test_contract_spoc_staging() { + viaduct_dev::init_backend_dev(); + + let client = staging_client(); + let result = client.request_spoc_ads( + vec![MozAdsPlacementRequestWithCount { + placement_id: "newtab_spocs".to_string(), + count: 1, + iab_content: None, + }], + None, + ); + + assert!(result.is_ok(), "Spoc ad request failed: {:?}", result.err()); + let placements = result.unwrap(); + assert!( + placements.contains_key("newtab_spocs"), + "Response missing expected placement key" + ); + + let spocs = placements.get("newtab_spocs").expect("Placement should exist"); + assert!(!spocs.is_empty(), "Should have received at least one spoc"); + + let ad = &spocs[0]; + assert!(!ad.block_key.is_empty(), "block_key should be non-empty"); + assert!(!ad.format.is_empty(), "format should be non-empty"); + assert!(!ad.image_url.is_empty(), "image_url should be non-empty"); + assert!(!ad.url.is_empty(), "url should be non-empty"); + assert!(!ad.title.is_empty(), "title should be non-empty"); + assert!(!ad.domain.is_empty(), "domain should be non-empty"); + assert!(!ad.excerpt.is_empty(), "excerpt should be non-empty"); + assert!(!ad.sponsor.is_empty(), "sponsor should be non-empty"); + assert!(!ad.caps.cap_key.is_empty(), "caps.cap_key should be non-empty"); + assert!( + !ad.callbacks.click.as_str().is_empty(), + "callbacks.click should be non-empty" + ); + assert!( + !ad.callbacks.impression.as_str().is_empty(), + "callbacks.impression should be non-empty" + ); +} + +#[test] +#[ignore = "contract test: run manually or in dedicated CI against ads.allizom.org"] +fn test_contract_tile_staging() { + viaduct_dev::init_backend_dev(); + + let client = staging_client(); + let result = client.request_tile_ads( + vec![MozAdsPlacementRequest { + placement_id: "newtab_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("newtab_tile_1"), + "Response missing expected placement key" + ); + + let ad = placements + .get("newtab_tile_1") + .expect("Placement should exist"); + + assert!(!ad.block_key.is_empty(), "block_key should be non-empty"); + assert!(!ad.format.is_empty(), "format should be non-empty"); + assert!(!ad.image_url.is_empty(), "image_url should be non-empty"); + assert!(!ad.url.is_empty(), "url should be non-empty"); + assert!(!ad.name.is_empty(), "name should be non-empty"); + assert!( + !ad.callbacks.click.as_str().is_empty(), + "callbacks.click should be non-empty" + ); + assert!( + !ad.callbacks.impression.as_str().is_empty(), + "callbacks.impression should be non-empty" + ); +} + +// ── Prod tests (existing) ────────────────────────────────────────────────────── + #[test] #[ignore] fn test_mock_pocket_billboard_1_placement() { From ac3cd0c109ac52fd9d35617d7b72745579b40938 Mon Sep 17 00:00:00 2001 From: ahanot Date: Mon, 9 Mar 2026 15:20:08 -0400 Subject: [PATCH 2/5] feat(ads-client): validate types against MARS OpenAPI schema from openapi.json Load schemas from the checked-in openapi.json with recursive $ref resolution instead of hardcoding them. Add skip_serializing_if for optional content field. Add GitHub Actions workflow for integration tests on ads-client changes. --- .github/workflows/ads-client-tests.yaml | 28 + Cargo.lock | 469 +++++- components/ads-client/Cargo.toml | 1 + components/ads-client/openapi.json | 1397 +++++++++++++++++ .../ads-client/src/client/ad_request.rs | 118 +- .../ads-client/src/client/ad_response.rs | 91 +- components/ads-client/src/test_utils.rs | 57 + 7 files changed, 2040 insertions(+), 121 deletions(-) create mode 100644 .github/workflows/ads-client-tests.yaml create mode 100644 components/ads-client/openapi.json diff --git a/.github/workflows/ads-client-tests.yaml b/.github/workflows/ads-client-tests.yaml new file mode 100644 index 0000000000..aaf9af29b8 --- /dev/null +++ b/.github/workflows/ads-client-tests.yaml @@ -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 --test integration_test -- --ignored diff --git a/Cargo.lock b/Cargo.lock index 390bab124d..e2d9efb10a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ dependencies = [ "chrono", "context_id", "error-support", + "jsonschema", "mockall", "mockito", "once_cell", @@ -51,6 +52,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.1", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.0.5" @@ -224,9 +239,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "itoa", "matchit", "memchr", @@ -238,7 +253,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "tokio", "tower", "tower-layer", @@ -254,8 +269,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "mime", "rustversion", "tower-layer", @@ -340,6 +355,21 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -370,12 +400,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "byteorder" version = "1.4.3" @@ -1060,6 +1102,15 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "embedded-uniffi-bindgen" version = "0.1.0" @@ -1472,6 +1523,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -1531,6 +1593,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1567,6 +1640,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "fragile" version = "2.0.0" @@ -1788,7 +1871,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.9", "indexmap 2.5.0", "slab", "tokio", @@ -1893,6 +1976,16 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -1900,7 +1993,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.9", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -1939,19 +2055,38 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", "want", ] +[[package]] +name = "hyper" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1959,12 +2094,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.27", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.5.2", + "pin-project-lite", + "socket2 0.5.8", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.53" @@ -2356,6 +2510,31 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonschema" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f66fe41fa46a5c83ed1c717b7e0b4635988f427083108c8cf0a882cc13441" +dependencies = [ + "ahash", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "once_cell", + "percent-encoding", + "referencing", + "regex-syntax", + "reqwest 0.12.9", + "serde", + "serde_json", + "uuid-simd", +] + [[package]] name = "jwcrypto" version = "0.1.0" @@ -2842,7 +3021,7 @@ dependencies = [ "copypasta", "glob", "heck", - "hyper", + "hyper 0.14.27", "local-ip-address", "nimbus-fml", "percent-encoding", @@ -2974,17 +3153,87 @@ dependencies = [ "nss_build_common", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -3164,6 +3413,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "parking_lot" version = "0.12.1" @@ -3612,6 +3867,39 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "referencing" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0dcb5ab28989ad7c91eb1b9531a37a1a137cc69a0499aee4117cae4a107c464" +dependencies = [ + "ahash", + "fluent-uri", + "once_cell", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.11.1" @@ -3723,9 +4011,9 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-tls", "ipnet", "js-sys", @@ -3748,6 +4036,42 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.2", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + [[package]] name = "restmail-client" version = "0.1.0" @@ -4184,6 +4508,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "sql-support" version = "0.1.0" @@ -4399,6 +4733,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.1" @@ -4591,7 +4934,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio-macros", "windows-sys 0.48.0", ] @@ -4722,8 +5065,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "http-range-header", "httpdate", "mime", @@ -4750,8 +5093,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e90e6da0427c5e111e03c764d49c4e970f5a9f6569fe408e5a1cbe257f48388" dependencies = [ "bytes", - "http", - "http-body", + "http 0.2.9", + "http-body 0.4.5", "pin-project-lite", "tokio", "tower", @@ -5071,6 +5414,17 @@ dependencies = [ "serde", ] +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -5121,7 +5475,7 @@ version = "0.2.0" dependencies = [ "async-trait", "error-support", - "hyper", + "hyper 0.14.27", "hyper-tls", "tokio", "uniffi", @@ -5135,10 +5489,16 @@ version = "0.2.0" dependencies = [ "error-support", "once_cell", - "reqwest", + "reqwest 0.11.18", "viaduct", ] +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -5442,6 +5802,36 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.36.1" @@ -5479,6 +5869,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.0" @@ -5802,6 +6201,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "zerofrom" version = "0.1.5" diff --git a/components/ads-client/Cargo.toml b/components/ads-client/Cargo.toml index 67d93c1ed5..30d23a60cc 100644 --- a/components/ads-client/Cargo.toml +++ b/components/ads-client/Cargo.toml @@ -31,6 +31,7 @@ viaduct = { path = "../viaduct" } sql-support = { path = "../support/sql" } [dev-dependencies] +jsonschema = "0.28" mockall = "0.12" mockito = { version = "0.31", default-features = false } viaduct-dev = { path = "../support/viaduct-dev" } diff --git a/components/ads-client/openapi.json b/components/ads-client/openapi.json new file mode 100644 index 0000000000..388f249223 --- /dev/null +++ b/components/ads-client/openapi.json @@ -0,0 +1,1397 @@ +{ + "openapi": "3.0.0", + "security": [], + "servers": [ + { + "url": "https://ads.mozilla.org" + } + ], + "info": { + "description": "Mozilla Ad Routing Service", + "version": "1.0", + "title": "MARS" + }, + "paths": { + "/v1/ads": { + "post": { + "summary": "Get Unified API ads", + "operationId": "getAds", + "parameters": [ + { + "in": "header", + "name": "X-User-Agent", + "description": "For OHTTP requests, the X-User-Agent header must be populated with a valid User Agent string. This string will typically be acquired from a /v1/ads-preflight response.\n\nFor non-OHTTP requests, the header should be omitted.", + "schema": { + "type": "string" + } + }, + { + "in": "header", + "name": "X-Geo-Location", + "description": "For OHTTP requests, the X-Geo-Location header must be populated with a valide geo location string. This string will typically be acquired from a /v1/ads-preflight response.\n\nFor non-OHTTP requests, the header should be omitted.", + "schema": { + "type": "string", + "pattern": "^(\\d+)?,(\\w+)?,(\\w+)?$", + "example": "506,MA,US" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response. The response can still be empty, no error codes will be returned if no ads can be served after being filtered for correctness and being on the block list.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdResponse" + } + } + } + }, + "400": { + "description": "Bad Request. Requests are invalid if they contain unsupported placements or request too many ads for a placement." + } + } + } + }, + "/v1/ads-preflight": { + "get": { + "summary": "Preflight request for OHTTP callers", + "description": "This endpoint is used to obtain information that OHTTP callers will need to include in OHTTP requests to the /v1/ads endpoint.", + "parameters": [], + "responses": { + "200": { + "description": "Successful response. The response can still be empty, no error codes will be returned if no ads are available for the given device and location.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PreflightResponse" + } + } + } + }, + "400": { + "description": "Bad Request. Requests are expected to include a User-Agent header." + } + } + } + }, + "/v1/t": { + "get": { + "summary": "Report ad interaction", + "description": "Interaction callback URLs are returned in an ad response. When the corresponding action on the client occurs, those URLs should be fetched.", + "parameters": [ + { + "name": "data", + "in": "query", + "description": "Encoded interaction data", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "placement_id", + "in": "query", + "description": "Identifier representing the instance of the placement (different identifier than placement from the ad request), used only in special situations.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "position", + "in": "query", + "description": "Identifier indicating the position of the placement (optional). May be a string or numeric. If a numeric index is used it must be 0-based.", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "reason", + "in": "query", + "description": "Identifier indicating the reason for the ad reporting interaction. Used only for, and required with, the 'report' action.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "inappropriate", + "not_interested", + "seen_too_many_times" + ] + } + } + ], + "responses": { + "200": { + "description": "Successful response" + } + } + } + }, + "/v1/log": { + "get": { + "summary": "Record client events", + "description": "This endpoint can be used to persist a prometheus metric.", + "parameters": [ + { + "name": "event", + "in": "query", + "description": "Identifier of the event to capture.", + "required": false, + "schema": { + "type": "string", + "enum": [ + "init", + "error" + ] + } + } + ], + "responses": { + "200": { + "description": "Successful response" + }, + "400": { + "description": "Bad Request. Requests are invalid if they contain unsupported or empty events." + } + } + } + }, + "/v1/delete_user": { + "delete": { + "summary": "Delete user data", + "description": "Delete any data persisted associated with a given context_id.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "context_id" + ], + "properties": { + "context_id": { + "type": "string", + "format": "uuid", + "example": "12347fff-00b0-aaaa-0978-189231239808" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully deleted user data." + } + } + } + }, + "/v1/images": { + "get": { + "summary": "Get ad image", + "description": "Proxies an ad image from an encoded URL. Encoded image URLs are returned in an ad response, calls to this endpoint shouldn't be constructed manually.", + "parameters": [ + { + "name": "image_data", + "in": "query", + "description": "Encoded ad image url", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response" + } + } + } + }, + "/v1/attribution": { + "get": { + "summary": "Get attribution reporting configuration", + "description": "The attribution reporting configuration is returned for the partner ID specified in the URL.", + "parameters": [ + { + "name": "partner_id", + "in": "query", + "description": "The ID of the partner.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + }, + "example": "295beef7-1e3b-4128-b8f8-858e12aa660b" + } + ], + "responses": { + "200": { + "description": "Successful response. Attribution reporting configuration found for the specified partner ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Task" + } + } + } + }, + "204": { + "description": "Not Found. Returned when no attribution reporting configuration is found for the specified partner ID." + }, + "400": { + "description": "Bad Request. Requests are invalid if the parameter includes an invalid partner ID query parameter." + } + } + } + }, + "/spocs": { + "post": { + "operationId": "getSpocs", + "summary": "(legacy) Get sponsored content", + "description": "Get a list of spocs based on region and pocket_id. The IP address is used to deduce a rough geographic region, for example \"Texas\" in the U.S. or \"England\" in the U.K. The IP is not stored or shared to preserve privacy.", + "parameters": [ + { + "in": "query", + "name": "site", + "schema": { + "type": "integer", + "format": "int32", + "minimum": 1, + "maximum": 2147483647 + }, + "required": false, + "description": "override siteId in ad decision requests", + "example": 2500 + }, + { + "in": "query", + "name": "region", + "schema": { + "type": "string" + }, + "required": false, + "description": "override region in keywords of ad decision requests for testing" + }, + { + "in": "query", + "name": "country", + "schema": { + "type": "string" + }, + "required": false, + "description": "override country in keywords of ad decision requests for testing" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SpocRequest" + }, + "examples": { + "version_one": { + "summary": "Request from client that does not support collections, FireFox version <= 74", + "value": { + "version": 1, + "consumer_key": "40249-e88c401e1b1f2242d9e441c4", + "pocket_id": "{12345678-8901-2345-aaaa-bbbbbbcccccc}" + } + }, + "version_one_collection_req": { + "summary": "Request for collection placements with version=1", + "value": { + "version": 1, + "consumer_key": "40249-e88c401e1b1f2242d9e441c4", + "pocket_id": "{12345678-8901-2345-aaaa-bbbbbbcccccc}", + "placements": [ + { + "name": "collections-div", + "ad_types": [ + 1234 + ], + "zone_ids": [ + 5000 + ], + "count": 10 + } + ] + } + }, + "version_two_collection_req": { + "summary": "Request for collection placements with version=2", + "value": { + "version": 2, + "consumer_key": "40249-e88c401e1b1f2242d9e441c4", + "pocket_id": "{12345678-8901-2345-aaaa-bbbbbbcccccc}", + "placements": [ + { + "name": "collections-div", + "ad_types": [ + 1234 + ], + "zone_ids": [ + 5000 + ], + "count": 10 + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Responds with settings and a list of spocs.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/SpocFeed" + }, + "properties": { + "settings": { + "$ref": "#/components/schemas/Settings" + }, + "__debug__": { + "description": "Informational object returned in non-prod environments", + "type": "object", + "additionalProperties": true + } + } + } + } + } + } + } + } + }, + "/user": { + "delete": { + "operationId": "deleteUser", + "summary": "(legacy) Delete a user's personal data", + "description": "Used when a user opts-out of sponsored content to delete the user's data.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "pocket_id" + ], + "properties": { + "pocket_id": { + "description": "ID that uniquely identifies a session.", + "example": "{12345678-8901-2345-aaaa-bbbbbbcccccc}", + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully deleted user data." + } + } + } + }, + "/v1/tiles": { + "get": { + "operationId": "getTiles", + "summary": "(legacy) Get tiles", + "responses": { + "200": { + "description": "Get a list of tiles based on region. The IP address is used to deduce a rough geographic region, for example \"Texas\" in the U.S. or \"England\" in the U.K.", + "headers": { + "cache-control": { + "schema": { + "type": "string" + }, + "description": "indicates tiles valid duration." + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tiles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LegacyTile" + } + }, + "sov": { + "type": "string", + "description": "SoV configuration", + "example": "kdfsi90wfglmnds" + } + } + } + } + } + }, + "204": { + "description": "No tiles available" + }, + "403": { + "description": "Tiles service is for Firefox only" + } + } + } + } + }, + "components": { + "schemas": { + "SpocRequest": { + "type": "object", + "required": [ + "version", + "consumer_key", + "pocket_id" + ], + "additionalProperties": false, + "properties": { + "version": { + "type": "integer", + "description": "API version", + "format": "int32", + "minimum": 1, + "maximum": 2, + "example": 2 + }, + "consumer_key": { + "type": "string", + "description": "Identifies that the request is coming from Firefox.", + "example": "40249-e88c401e1b1f2242d9e441c4" + }, + "pocket_id": { + "type": "string", + "description": "ID that uniquely identifies a session.", + "example": "{12345678-8901-2345-aaaa-bbbbbbcccccc}", + "pattern": "^\\{[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\\}$" + }, + "placements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Placement" + } + }, + "site": { + "type": "integer", + "format": "int32", + "minimum": 1, + "maximum": 2147483647, + "description": "override siteId in ad decision requests", + "example": 2500 + }, + "country": { + "type": "string", + "description": "override country in keywords of ad decision requests for testing" + }, + "region": { + "type": "string", + "description": "override region in keywords of ad decision requests for testing" + } + } + }, + "Placement": { + "type": "object", + "description": "Placement describes parameters for a set of ads to return", + "required": [ + "name" + ], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "example": "spocs", + "description": "Corresponds to the key in the response object." + }, + "ad_types": { + "type": "array", + "description": "IDs of Ad Types, indicating the size & dimensions of the ads to return.", + "items": { + "type": "integer", + "format": "int32", + "example": 1234, + "minimum": 1, + "maximum": 2147483647 + } + }, + "zone_ids": { + "type": "array", + "description": "ID of Zones, indicating what area these ads will be shown.", + "items": { + "type": "integer", + "format": "int32", + "example": 123456, + "minimum": 1, + "maximum": 2147483647 + } + }, + "count": { + "type": "integer", + "example": 20, + "minimum": 1, + "maximum": 20, + "description": "number of ads to return for this placement" + } + } + }, + "Settings": { + "type": "object", + "additionalProperties": false, + "required": [ + "feature_flags", + "spocsPerNewTabs", + "domainAffinityParameterSets", + "timeSegments" + ], + "properties": { + "spocsPerNewTabs": { + "type": "integer", + "minimum": 1, + "example": 1 + }, + "domainAffinityParameterSets": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/DomainAffinityParameterSet" + } + }, + "timeSegments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeSegment" + } + }, + "feature_flags": { + "type": "object", + "$ref": "#/components/schemas/FeatureFlags" + } + } + }, + "FeatureFlags": { + "type": "object", + "additionalProperties": false, + "required": [ + "spoc_v2", + "collections" + ], + "properties": { + "spoc_v2": { + "type": "boolean" + }, + "collections": { + "type": "boolean" + } + } + }, + "DomainAffinityParameterSet": { + "type": "object", + "additionalProperties": false, + "required": [ + "recencyFactor", + "frequencyFactor", + "combinedDomainFactor", + "perfectCombinedDomainScore", + "multiDomainBoost", + "itemScoreFactor" + ], + "properties": { + "recencyFactor": { + "type": "number" + }, + "frequencyFactor": { + "type": "number" + }, + "combinedDomainFactor": { + "type": "number" + }, + "perfectFrequencyVisits": { + "type": "number" + }, + "perfectCombinedDomainScore": { + "type": "number" + }, + "multiDomainBoost": { + "type": "number" + }, + "itemScoreFactor": { + "type": "number" + } + } + }, + "TimeSegment": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "startTime", + "endTime", + "weightPosition" + ], + "properties": { + "id": { + "type": "string" + }, + "startTime": { + "type": "integer" + }, + "endTime": { + "type": "integer" + }, + "weightPosition": { + "example": 1 + } + } + }, + "SpocFeed": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/components/schemas/SpocFeedItem" + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "flight_id" + ], + "properties": { + "title": { + "type": "string", + "example": "Best of the Web" + }, + "flight_id": { + "type": "integer", + "example": 4321 + }, + "sponsor": { + "type": "string", + "example": "AdvertiserName" + }, + "context": { + "type": "string", + "example": "Sponsored by AdvertiserName" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SpocFeedItem" + } + } + } + } + ] + }, + "Shim": { + "type": "object", + "additionalProperties": false, + "properties": { + "click": { + "type": "string", + "example": "1234123asdf4tYadsfQ,xY-01BU12" + }, + "impression": { + "type": "string", + "example": "a0c3943asdf4tYadsf300,xY-01BU9aadc" + }, + "delete": { + "type": "string", + "example": "fdea123asdf4tYadsf1000,xY-01BUa654" + }, + "save": { + "type": "string", + "example": "4567123asdf4tYadsfQcda,xY-01BU123" + } + } + }, + "Caps": { + "type": "object", + "additionalProperties": false, + "required": [ + "lifetime", + "flight", + "campaign" + ], + "properties": { + "lifetime": { + "type": "integer", + "example": 50 + }, + "flight": { + "type": "object", + "additionalProperties": false, + "required": [ + "count", + "period" + ], + "properties": { + "count": { + "type": "integer", + "example": 10 + }, + "period": { + "type": "integer", + "description": "Period in seconds", + "example": 86400 + } + } + }, + "campaign": { + "type": "object", + "additionalProperties": false, + "required": [ + "count", + "period" + ], + "properties": { + "count": { + "type": "integer", + "example": 10 + }, + "period": { + "type": "integer", + "description": "Period in seconds", + "example": 86400 + } + } + } + } + }, + "SpocFeedItem": { + "type": "object", + "additionalProperties": false, + "properties": { + "campaign_id": { + "type": "integer", + "example": 784 + }, + "caps": { + "type": "object", + "$ref": "#/components/schemas/Caps" + }, + "collection_title": { + "type": "string", + "description": "Shared title if all ads are one collection" + }, + "context": { + "type": "string", + "description": "Deprecated. Use sponsor field instead.", + "example": "Sponsored by AdvertiserName" + }, + "cta": { + "type": "string", + "description": "Text to display on CTA button", + "example": "Learn more" + }, + "domain": { + "type": "string", + "example": "mozilla.net" + }, + "domain_affinities": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "example": { + "vanguard.com": 0.9956, + "wealthsimple.com": 0.9193 + } + }, + "excerpt": { + "type": "string", + "example": "Driving excerpt" + }, + "flight_id": { + "type": "integer", + "example": 432 + }, + "id": { + "type": "integer", + "example": 30295 + }, + "image_src": { + "type": "string", + "example": "https://img-getpocket.cdn.mozilla.net/ad.gif" + }, + "is_video": { + "type": "boolean" + }, + "item_score": { + "type": "number", + "format": "float", + "example": 0.2 + }, + "min_score": { + "type": "number", + "format": "float", + "example": 0.1 + }, + "parameter_set": { + "type": "string", + "example": "default" + }, + "personalization_models": { + "type": "object", + "additionalProperties": true + }, + "priority": { + "type": "integer", + "description": "The priority order. 1-100, 1 is highest priority.", + "minimum": 1, + "maximum": 100 + }, + "raw_image_src": { + "type": "string", + "example": "https://mozilla.net/ad.gif" + }, + "shim": { + "type": "object", + "$ref": "#/components/schemas/Shim" + }, + "sponsor": { + "type": "string", + "example": "AdvertiserName" + }, + "sponsored_by_override": { + "type": "string", + "example": "AdvertiserName" + }, + "title": { + "type": "string", + "example": "Why driving is hard—even for AIs" + }, + "url": { + "type": "string", + "example": "http://mozilla.net/page" + } + } + }, + "LegacyTile": { + "type": "object", + "description": "tile format", + "required": [ + "id", + "name", + "url", + "click_url", + "image_url", + "image_size", + "impression_url" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "integer", + "format": "int32", + "example": 1234, + "description": "Partner specific id for ad", + "minimum": 1, + "maximum": 2147483647 + }, + "name": { + "type": "string", + "example": "Example COM", + "description": "Advertiser name" + }, + "url": { + "type": "string", + "example": "https://www.example.com/desktop_macos", + "description": "Advertiser URL" + }, + "click_url": { + "type": "string", + "example": "https://example.com/desktop_macos?version=16.0.0&key=22.1&ci=6.2&ctag=1612376952400200000", + "description": "Click counting URL" + }, + "image_url": { + "type": "string", + "example": "https://example.com/desktop_macos01.jpg", + "description": "Ad image" + }, + "image_size": { + "type": "integer", + "nullable": true, + "format": "int32", + "example": 200, + "description": "Image size" + }, + "impression_url": { + "type": "string", + "example": "https://example.com/desktop_macos?id=0001", + "description": "Impression counting URL" + } + } + }, + "AdPlacement": { + "type": "object", + "properties": { + "placement": { + "type": "string", + "example": "placement_1", + "description": "Specifies the placement location of the ad. Values will be Mozilla supplied and specific to the integration." + }, + "count": { + "type": "integer", + "format": "int32", + "default": 1, + "minimum": 1, + "maximum": 20, + "description": "The number of ads to be placed in the specified location." + }, + "content": { + "type": "object", + "$ref": "#/components/schemas/AdContent" + } + }, + "required": [ + "placement" + ] + }, + "AdRequest": { + "type": "object", + "properties": { + "context_id": { + "type": "string", + "format": "uuid", + "example": "12347fff-00b0-aaaa-0978-189231239808", + "description": "An identifier for the user's context." + }, + "placements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AdPlacement" + }, + "minItems": 1, + "description": "A list of `AdPlacement` objects, specifying where ads should be placed." + }, + "blocks": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "CAISEm15IHNwZWNpYWwgc3BvbnNvcg" + ], + "description": "A list of strings specifying blocked content. The string values come from the `block_key` field in returned ads." + }, + "consent": { + "type": "object", + "$ref": "#/components/schemas/Consent" + } + }, + "required": [ + "context_id", + "placements" + ] + }, + "AdContent": { + "type": "object", + "properties": { + "taxonomy": { + "type": "string", + "enum": [ + "IAB-1.0", + "IAB-2.0", + "IAB-2.1", + "IAB-2.2", + "IAB-3.0" + ], + "description": "A valid taxonomy identifier recognized by MARS", + "example": "IAB-1.0" + }, + "categories": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "example": [ + "IAB1-5" + ] + } + }, + "required": [ + "taxonomy", + "categories" + ] + }, + "Consent": { + "type": "object", + "description": "An object to specify consent specifiers for this request", + "properties": { + "gpp": { + "type": "string", + "description": "Global Privacy Platform consent string" + } + } + }, + "AdCallbacks": { + "type": "object", + "description": "An object containing callback URLs for interactions with an ad.", + "properties": { + "click": { + "type": "string", + "description": "This URL should be requested with an HTTP GET when the ad is clicked. Response should be ignored." + }, + "impression": { + "type": "string", + "description": "This URL should be requested with an HTTP GET when half of the ad is visible in the viewport for 1 second. If the ad's pixel size is greater that 242500 (970 * 250) only 30% visibility is required. Response should be ignored." + }, + "report": { + "type": "string", + "description": "This URL may be issued by a client on behalf of a user to report an ad that is inappropriate or otherwise unsatisfying. Response should be ignored. The reason parameter is required with this action." + } + } + }, + "Attributions": { + "type": "object", + "description": "An object containing attribution configuration for enabled ads.", + "properties": { + "partner_id": { + "type": "string", + "format": "uuid", + "description": "Advertising partner associated with the ad." + }, + "index": { + "type": "number", + "description": "Index allocated to be used when a non-default report is sent." + } + }, + "required": [ + "partner_id", + "index" + ] + }, + "Task": { + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "DAP task ID.", + "example": "pL9iVhsLWNFb_W044YEbdOYC5y5_RZlPJZovrlk-8Vs" + }, + "vdaf": { + "type": "string", + "description": "DAP data type of the task.", + "example": "histogram" + }, + "bits": { + "type": "number", + "description": "DAP data size of the task" + }, + "length": { + "type": "number", + "description": "DAP legnth of the task.", + "example": 10 + }, + "time_precision": { + "type": "number", + "description": "DAP time precision. Determines rounding of dates in DAP report.", + "example": 3600 + }, + "default_measurement": { + "type": "number", + "description": "Measurement to be used when a default report is sent.", + "example": 0 + } + }, + "required": [ + "task_id", + "vdaf", + "length", + "time_precision" + ] + }, + "AdFormatBase": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The target destination URL of the ad." + }, + "callbacks": { + "$ref": "#/components/schemas/AdCallbacks" + }, + "attributions": { + "$ref": "#/components/schemas/Attributions" + } + }, + "required": [ + "url", + "callbacks" + ] + }, + "SpocFrequencyCaps": { + "type": "object", + "description": "Client-side enforced frequency capping information.", + "properties": { + "cap_key": { + "type": "string", + "description": "A key that identifies the frequency cap.", + "example": 345678901 + }, + "day": { + "type": "integer", + "format": "int32", + "description": "Number of times to show the same ad during a one day period.", + "example": 10 + } + }, + "required": [ + "cap_key", + "day" + ] + }, + "SpocRanking": { + "type": "object", + "description": "Ranking information for personalized content.", + "properties": { + "priority": { + "type": "integer", + "format": "int32", + "description": "The priority in the ranking. Reranking of ads should prefer priority before personalization.", + "example": 1 + }, + "personalization_models": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + }, + "description": "A map of model names to scores for personalization.", + "example": { + "arts_and_entertainment": 1, + "autos_and_vehicles": 1 + } + }, + "item_score": { + "type": "number", + "format": "float", + "description": "The overall score for the item.", + "example": 0.2 + } + }, + "required": [ + "priority", + "item_score" + ] + }, + "Image": { + "allOf": [ + { + "$ref": "#/components/schemas/AdFormatBase" + }, + { + "type": "object", + "properties": { + "format": { + "type": "string", + "description": "The format of the ad, indicating it is an image ad of a specific size. Values include `rectangle`, `billboard`, `skyscraper`, `leaderboard`.", + "example": "rectangle" + }, + "image_url": { + "type": "string", + "description": "URL of the ad image." + }, + "alt_text": { + "type": "string", + "description": "Alt text to describe the ad image.", + "example": "ACME Corp. Spring Launcher" + }, + "block_key": { + "type": "string", + "description": "The block key generated for the advertiser.", + "example": "CAISEm15IHNwZWNpYWwgc3BvbnNvcg" + } + }, + "required": [ + "format", + "image_url", + "block_key" + ] + } + ] + }, + "Spoc": { + "allOf": [ + { + "$ref": "#/components/schemas/AdFormatBase" + }, + { + "type": "object", + "properties": { + "format": { + "type": "string", + "description": "The format of the ad, `spoc`.", + "example": "spoc" + }, + "image_url": { + "type": "string", + "description": "URL of the ad image." + }, + "title": { + "type": "string", + "description": "Title of the sponsored content.", + "example": "Good news everyone!" + }, + "domain": { + "type": "string", + "description": "The domain where the content is hosted.", + "example": "example.com" + }, + "excerpt": { + "type": "string", + "description": "A short excerpt from the sponsored content.", + "example": "Read more about this..." + }, + "sponsor": { + "type": "string", + "description": "The name of the sponsor.", + "example": "ACME Corp" + }, + "sponsored_by_override": { + "type": "string", + "description": "An optional override for the sponsor name.", + "example": "Organized by ACME Corp" + }, + "block_key": { + "type": "string", + "description": "The block key generated for the advertiser.", + "example": "CAISEm15IHNwZWNpYWwgc3BvbnNvcg" + }, + "caps": { + "$ref": "#/components/schemas/SpocFrequencyCaps" + }, + "ranking": { + "$ref": "#/components/schemas/SpocRanking" + } + }, + "required": [ + "format", + "image_url", + "title", + "domain", + "excerpt", + "sponsor", + "block_key", + "caps", + "ranking" + ] + } + ] + }, + "Tile": { + "allOf": [ + { + "$ref": "#/components/schemas/AdFormatBase" + }, + { + "type": "object", + "properties": { + "format": { + "type": "string", + "description": "The format of the ad, `tile`.", + "example": "tile" + }, + "image_url": { + "type": "string", + "description": "URL of the ad image." + }, + "name": { + "type": "string", + "description": "The name displayed under the tile.", + "example": "Amazon" + }, + "block_key": { + "type": "string", + "description": "The block key generated for the advertiser.", + "example": "CAISEm15IHNwZWNpYWwgc3BvbnNvcg" + } + }, + "required": [ + "format", + "image_url", + "name", + "block_key" + ] + } + ] + }, + "AdResponse": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/Image" + }, + { + "$ref": "#/components/schemas/Spoc" + }, + { + "$ref": "#/components/schemas/Tile" + } + ] + } + } + }, + "TelemetryResponse": { + "type": "object", + "description": "An empty response object" + }, + "PreflightResponse": { + "type": "object", + "properties": { + "geoname_id": { + "type": "string", + "pattern": "^\\s*\\d+\\s*(,\\s*\\d+\\s*)*$", + "description": "A comma delimited list of geoname IDs representing the caller's location, based on their IP address. The first member of the list is the most specific location, with subsequent members being less specific locations.", + "example": "6077243,6252001" + }, + "geo_location": { + "type": "string", + "pattern": "^(\\d+)?,(\\w+)?,(\\w+)?$", + "description": "A structured representation of the caller's location, based on their IP address.", + "example": "506,MA,US" + }, + "normalized_ua": { + "type": "string", + "description": "A user agent string that will be similar to the caller's user agent string in characteristics, but will have some data normalized to make it less unique." + } + }, + "required": [ + "geoname_id", + "geo_location" + ] + } + } + } +} \ No newline at end of file diff --git a/components/ads-client/src/client/ad_request.rs b/components/ads-client/src/client/ad_request.rs index 10a627c81f..e6fb54d299 100644 --- a/components/ads-client/src/client/ad_request.rs +++ b/components/ads-client/src/client/ad_request.rs @@ -84,6 +84,7 @@ impl AdRequest { pub struct AdPlacementRequest { pub placement: String, pub count: u32, + #[serde(skip_serializing_if = "Option::is_none")] pub content: Option, } @@ -274,89 +275,90 @@ mod tests { assert_eq!(RequestHash::new(&req1), RequestHash::new(&req2)); } - /// Contract tests: validate our request serialization against the MARS OpenAPI spec. + /// Contract tests: validate our serialized requests against the MARS OpenAPI + /// JSON Schema loaded from `components/ads-client/openapi.json`. /// - /// The spec lives at: - /// https://ads.mozilla.org/assets/docs/openapi/mars-api.html#operation/getAds + /// Spec: https://ads.mozilla.org/assets/docs/openapi/mars-api.html#operation/getAds mod contract { use super::*; - use serde_json::{json, to_value}; + use crate::test_utils::{mars_schema, validate_against_mars_schema}; + use serde_json::to_value; + + fn ad_request_schema() -> serde_json::Value { + mars_schema("AdRequest") + } #[test] - fn test_request_serializes_to_spec_shape() { + fn test_minimal_request_validates_against_spec() { let url: Url = "https://ads.mozilla.org/v1/ads".parse().unwrap(); let request = AdRequest::try_new( "decafbad-0cd1-0cd2-0cd3-decafbad1000".to_string(), vec![AdPlacementRequest { placement: "newtab_tile_1".to_string(), - count: 2, - content: Some(AdContentCategory { - taxonomy: IABContentTaxonomy::IAB2_1, - categories: vec!["technology".to_string()], - }), + count: 1, + content: None, }], url, ) .unwrap(); - let json = to_value(&request).unwrap(); - - // Spec requires: context_id (UUID string), placements (non-empty array) - assert_eq!(json["context_id"], "decafbad-0cd1-0cd2-0cd3-decafbad1000"); - assert!(json["placements"].is_array()); - assert_eq!(json["placements"][0]["placement"], "newtab_tile_1"); - assert_eq!(json["placements"][0]["count"], 2); - assert_eq!(json["placements"][0]["content"]["taxonomy"], "IAB-2.1"); - assert_eq!( - json["placements"][0]["content"]["categories"][0], - "technology" - ); - // url must NOT appear in the body (it is sent as the HTTP endpoint) - assert!(json.get("url").is_none()); - } - - #[test] - fn test_taxonomy_values_match_spec_strings() { - // Spec enum: "IAB-1.0" | "IAB-2.0" | "IAB-2.1" | "IAB-2.2" | "IAB-3.0" - let cases = [ - (IABContentTaxonomy::IAB1_0, "IAB-1.0"), - (IABContentTaxonomy::IAB2_0, "IAB-2.0"), - (IABContentTaxonomy::IAB2_1, "IAB-2.1"), - (IABContentTaxonomy::IAB2_2, "IAB-2.2"), - (IABContentTaxonomy::IAB3_0, "IAB-3.0"), - ]; - for (variant, expected) in cases { - let json = to_value(&variant).unwrap(); - assert_eq!( - json, - json!(expected), - "IABContentTaxonomy variant should serialize to {expected}" - ); - } + validate_against_mars_schema(&ad_request_schema(), &to_value(&request).unwrap()); } - // INTENTIONAL OMISSION: The MARS spec supports optional `blocks` (array - // of block_key strings) and `consent` (object with gpp string) fields on - // the request body. These are not yet modelled because no current embedder - // needs them. Adding them is additive and non-breaking when needed. #[test] - fn test_optional_spec_fields_not_present_in_serialized_request() { + fn test_full_request_with_content_validates_against_spec() { let url: Url = "https://ads.mozilla.org/v1/ads".parse().unwrap(); let request = AdRequest::try_new( "decafbad-0cd1-0cd2-0cd3-decafbad1000".to_string(), - vec![AdPlacementRequest { - placement: "newtab_tile_1".to_string(), - count: 1, - content: None, - }], + vec![ + AdPlacementRequest { + placement: "newtab_tile_1".to_string(), + count: 2, + content: Some(AdContentCategory { + taxonomy: IABContentTaxonomy::IAB2_1, + categories: vec!["technology".to_string()], + }), + }, + AdPlacementRequest { + placement: "pocket_billboard".to_string(), + count: 1, + content: None, + }, + ], url, ) .unwrap(); - let json = to_value(&request).unwrap(); - // blocks and consent are not modelled — confirm they are absent - assert!(json.get("blocks").is_none(), "blocks field should not be serialized"); - assert!(json.get("consent").is_none(), "consent field should not be serialized"); + validate_against_mars_schema(&ad_request_schema(), &to_value(&request).unwrap()); + } + + #[test] + fn test_all_taxonomy_variants_validate_against_spec() { + let schema = ad_request_schema(); + let url: Url = "https://ads.mozilla.org/v1/ads".parse().unwrap(); + + for taxonomy in [ + IABContentTaxonomy::IAB1_0, + IABContentTaxonomy::IAB2_0, + IABContentTaxonomy::IAB2_1, + IABContentTaxonomy::IAB2_2, + IABContentTaxonomy::IAB3_0, + ] { + let request = AdRequest::try_new( + "decafbad-0cd1-0cd2-0cd3-decafbad1000".to_string(), + vec![AdPlacementRequest { + placement: "test".to_string(), + count: 1, + content: Some(AdContentCategory { + taxonomy, + categories: vec!["cat".to_string()], + }), + }], + url.clone(), + ) + .unwrap(); + validate_against_mars_schema(&schema, &to_value(&request).unwrap()); + } } } diff --git a/components/ads-client/src/client/ad_response.rs b/components/ads-client/src/client/ad_response.rs index d5bf19825e..0350f2e6b4 100644 --- a/components/ads-client/src/client/ad_response.rs +++ b/components/ads-client/src/client/ad_response.rs @@ -469,20 +469,25 @@ mod tests { .contains("request_hash=abc123def456")); } - /// Contract tests: validate our types against the MARS OpenAPI spec. + /// Contract tests: validate our types against the MARS OpenAPI JSON Schema + /// loaded from `components/ads-client/openapi.json`. /// - /// These tests use JSON fixtures that mirror the MARS API spec and document - /// any intentional deviations from it. The spec lives at: - /// https://ads.mozilla.org/assets/docs/openapi/mars-api.html#operation/getAds + /// Each test fixture is first validated against the spec schema (with `$ref` + /// resolved), then deserialized into our Rust types. This ensures our + /// fixtures are spec-accurate AND our types can handle real MARS responses. + /// + /// Spec: https://ads.mozilla.org/assets/docs/openapi/mars-api.html#operation/getAds mod contract { use super::*; + use crate::test_utils::{mars_schema, validate_against_mars_schema}; use serde_json::json; // ── Image ────────────────────────────────────────────────────────────── #[test] - fn test_image_deserializes_from_spec_example() { - let json = json!({ + fn test_image_full_validates_and_deserializes() { + let schema = mars_schema("Image"); + let fixture = json!({ "url": "https://example.com/landing", "callbacks": { "click": "https://ads.mozilla.org/v1/t?click", @@ -494,17 +499,18 @@ mod tests { "alt_text": "Example ad", "block_key": "block-abc-123" }); + validate_against_mars_schema(&schema, &fixture); let ad: AdImage = - serde_json::from_value(json).expect("Image should deserialize from spec JSON"); + serde_json::from_value(fixture).expect("Image should deserialize from spec JSON"); assert_eq!(ad.format, "billboard"); assert_eq!(ad.block_key, "block-abc-123"); assert_eq!(ad.alt_text, Some("Example ad".to_string())); - assert_eq!(ad.callbacks.report.unwrap().as_str(), "https://ads.mozilla.org/v1/t?report"); } #[test] - fn test_image_without_optional_fields() { - let json = json!({ + fn test_image_minimal_validates_and_deserializes() { + let schema = mars_schema("Image"); + let fixture = json!({ "url": "https://example.com/landing", "callbacks": { "click": "https://ads.mozilla.org/v1/t?click", @@ -514,7 +520,8 @@ mod tests { "image_url": "https://example.com/image.png", "block_key": "block-abc-123" }); - let ad: AdImage = serde_json::from_value(json) + validate_against_mars_schema(&schema, &fixture); + let ad: AdImage = serde_json::from_value(fixture) .expect("Image should deserialize without optional fields"); assert_eq!(ad.alt_text, None); assert_eq!(ad.callbacks.report, None); @@ -526,42 +533,48 @@ mod tests { // actionable — we'd rather drop it (AdResponse::parse silently skips // items that fail to deserialize) than show a broken ad. #[test] - fn test_image_missing_click_fails_deserialization() { - let json = json!({ + fn test_image_missing_click_is_spec_valid_but_fails_our_deserialization() { + let schema = mars_schema("Image"); + let fixture = json!({ "url": "https://example.com/landing", "callbacks": { "impression": "https://ads.mozilla.org/v1/t?impression" }, "format": "rectangle", "image_url": "https://example.com/image.png", "block_key": "block-abc-123" }); + // Valid according to the MARS spec (callbacks fields are optional) + validate_against_mars_schema(&schema, &fixture); + // But fails OUR deserialization (we intentionally require click) assert!( - serde_json::from_value::(json).is_err(), + serde_json::from_value::(fixture).is_err(), "Image without click URL should fail to deserialize" ); } #[test] - fn test_image_missing_impression_fails_deserialization() { - let json = json!({ + fn test_image_missing_impression_is_spec_valid_but_fails_our_deserialization() { + let schema = mars_schema("Image"); + let fixture = json!({ "url": "https://example.com/landing", "callbacks": { "click": "https://ads.mozilla.org/v1/t?click" }, "format": "rectangle", "image_url": "https://example.com/image.png", "block_key": "block-abc-123" }); + validate_against_mars_schema(&schema, &fixture); assert!( - serde_json::from_value::(json).is_err(), + serde_json::from_value::(fixture).is_err(), "Image without impression URL should fail to deserialize" ); } // INTENTIONAL OMISSION: The MARS spec includes an `attributions` object - // (partner_id UUID + index number) on all ad types. We do not model it - // because it is not needed by any current embedder. serde ignores unknown - // fields by default so this is safe — we just don't surface the data. + // on all ad types. We do not model it. serde ignores unknown fields by + // default so this is safe — we just don't surface attribution data. #[test] - fn test_unknown_attributions_field_ignored() { - let json = json!({ + fn test_image_with_attributions_validates_and_deserializes() { + let schema = mars_schema("Image"); + let fixture = json!({ "url": "https://example.com/landing", "callbacks": { "click": "https://ads.mozilla.org/v1/t?click", @@ -575,8 +588,9 @@ mod tests { "index": 0 } }); + validate_against_mars_schema(&schema, &fixture); assert!( - serde_json::from_value::(json).is_ok(), + serde_json::from_value::(fixture).is_ok(), "Unknown attributions field should be ignored gracefully" ); } @@ -584,8 +598,9 @@ mod tests { // ── Spoc ─────────────────────────────────────────────────────────────── #[test] - fn test_spoc_deserializes_from_spec_example() { - let json = json!({ + fn test_spoc_full_validates_and_deserializes() { + let schema = mars_schema("Spoc"); + let fixture = json!({ "url": "https://example.com/article", "callbacks": { "click": "https://ads.mozilla.org/v1/t?click", @@ -597,7 +612,7 @@ mod tests { "domain": "example.com", "excerpt": "A short excerpt of the sponsored content.", "sponsor": "Example Sponsor", - "sponsored_by_override": null, + "sponsored_by_override": "Override text", "block_key": "block-spoc-123", "caps": { "cap_key": "example-cap", "day": 3 }, "ranking": { @@ -606,21 +621,19 @@ mod tests { "item_score": 0.95 } }); + validate_against_mars_schema(&schema, &fixture); let ad: AdSpoc = - serde_json::from_value(json).expect("Spoc should deserialize from spec JSON"); + serde_json::from_value(fixture).expect("Spoc should deserialize from spec JSON"); assert_eq!(ad.format, "spoc"); assert_eq!(ad.domain, "example.com"); - assert_eq!(ad.sponsor, "Example Sponsor"); assert_eq!(ad.caps.day, 3); - assert_eq!(ad.caps.cap_key, "example-cap"); - assert_eq!(ad.ranking.priority, 1); assert!((ad.ranking.item_score - 0.95).abs() < f64::EPSILON); - assert_eq!(ad.ranking.personalization_models.unwrap()["model_a"], 42); } #[test] - fn test_spoc_without_optional_fields() { - let json = json!({ + fn test_spoc_minimal_validates_and_deserializes() { + let schema = mars_schema("Spoc"); + let fixture = json!({ "url": "https://example.com/article", "callbacks": { "click": "https://ads.mozilla.org/v1/t?click", @@ -636,7 +649,8 @@ mod tests { "caps": { "cap_key": "example-cap", "day": 3 }, "ranking": { "priority": 1, "item_score": 0.5 } }); - let ad: AdSpoc = serde_json::from_value(json) + validate_against_mars_schema(&schema, &fixture); + let ad: AdSpoc = serde_json::from_value(fixture) .expect("Spoc should deserialize without optional fields"); assert_eq!(ad.sponsored_by_override, None); assert_eq!(ad.ranking.personalization_models, None); @@ -645,8 +659,9 @@ mod tests { // ── Tile ─────────────────────────────────────────────────────────────── #[test] - fn test_tile_deserializes_from_spec_example() { - let json = json!({ + fn test_tile_validates_and_deserializes() { + let schema = mars_schema("Tile"); + let fixture = json!({ "url": "https://example.com", "callbacks": { "click": "https://ads.mozilla.org/v1/t?click", @@ -657,11 +672,11 @@ mod tests { "name": "Example Site", "block_key": "block-tile-123" }); + validate_against_mars_schema(&schema, &fixture); let ad: AdTile = - serde_json::from_value(json).expect("Tile should deserialize from spec JSON"); + serde_json::from_value(fixture).expect("Tile should deserialize from spec JSON"); assert_eq!(ad.format, "tile"); assert_eq!(ad.name, "Example Site"); - assert_eq!(ad.block_key, "block-tile-123"); } } diff --git a/components/ads-client/src/test_utils.rs b/components/ads-client/src/test_utils.rs index 6e5407da66..40a4a9aac7 100644 --- a/components/ads-client/src/test_utils.rs +++ b/components/ads-client/src/test_utils.rs @@ -14,6 +14,63 @@ use crate::client::{ }, }; +// ── MARS OpenAPI schema helpers ───────────────────────────────────────────── + +/// The MARS OpenAPI spec, embedded at compile time. +const OPENAPI_JSON: &str = include_str!("../openapi.json"); + +/// Load a named schema from `components/schemas/` in the MARS OpenAPI +/// spec, with all `$ref` pointers recursively resolved inline. +pub fn mars_schema(name: &str) -> serde_json::Value { + let root: serde_json::Value = + serde_json::from_str(OPENAPI_JSON).expect("openapi.json should parse"); + let schema = root["components"]["schemas"][name].clone(); + assert!( + !schema.is_null(), + "Schema '{name}' not found in openapi.json" + ); + resolve_refs(&schema, &root) +} + +/// Validate a JSON instance against a MARS OpenAPI schema. Panics with a +/// descriptive message on validation failure. +pub fn validate_against_mars_schema(schema: &serde_json::Value, instance: &serde_json::Value) { + let validator = jsonschema::validator_for(schema).expect("schema should compile"); + if let Err(e) = validator.validate(instance) { + panic!( + "JSON Schema validation failed:\n {e}\nInstance:\n{}", + serde_json::to_string_pretty(instance).unwrap() + ); + } +} + +/// Recursively resolve `$ref` JSON pointers within a JSON Schema value, +/// using `root` (the full OpenAPI document) as the resolution base. +fn resolve_refs(value: &serde_json::Value, root: &serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Object(map) => { + if let Some(serde_json::Value::String(ref_path)) = map.get("$ref") { + // Follow the JSON Pointer (e.g. "#/components/schemas/AdContent") + let pointer = ref_path.trim_start_matches('#'); + let resolved = root + .pointer(pointer) + .unwrap_or_else(|| panic!("$ref '{ref_path}' not found in openapi.json")); + resolve_refs(resolved, root) + } else { + let new_map: serde_json::Map = map + .iter() + .map(|(k, v)| (k.clone(), resolve_refs(v, root))) + .collect(); + serde_json::Value::Object(new_map) + } + } + serde_json::Value::Array(arr) => { + serde_json::Value::Array(arr.iter().map(|v| resolve_refs(v, root)).collect()) + } + other => other.clone(), + } +} + pub const TEST_CONTEXT_ID: &str = "00000000-0000-4000-8000-000000000001"; pub fn make_happy_placement_requests() -> Vec { From ffe50690ed429bcf997aa77b81bbea7f364e9995 Mon Sep 17 00:00:00 2001 From: ahanot Date: Mon, 9 Mar 2026 17:03:12 -0400 Subject: [PATCH 3/5] clean up contract tests: trim comments, restore cache test --- .../ads-client/src/client/ad_request.rs | 5 +- .../ads-client/src/client/ad_response.rs | 73 ++------ components/ads-client/src/test_utils.rs | 11 +- .../ads-client/tests/integration_test.rs | 170 +----------------- 4 files changed, 22 insertions(+), 237 deletions(-) diff --git a/components/ads-client/src/client/ad_request.rs b/components/ads-client/src/client/ad_request.rs index e6fb54d299..1ba5e2cffd 100644 --- a/components/ads-client/src/client/ad_request.rs +++ b/components/ads-client/src/client/ad_request.rs @@ -275,10 +275,7 @@ mod tests { assert_eq!(RequestHash::new(&req1), RequestHash::new(&req2)); } - /// Contract tests: validate our serialized requests against the MARS OpenAPI - /// JSON Schema loaded from `components/ads-client/openapi.json`. - /// - /// Spec: https://ads.mozilla.org/assets/docs/openapi/mars-api.html#operation/getAds + /// Validate serialized requests against the MARS OpenAPI spec. mod contract { use super::*; use crate::test_utils::{mars_schema, validate_against_mars_schema}; diff --git a/components/ads-client/src/client/ad_response.rs b/components/ads-client/src/client/ad_response.rs index 0350f2e6b4..06cd7f3ab9 100644 --- a/components/ads-client/src/client/ad_response.rs +++ b/components/ads-client/src/client/ad_response.rs @@ -469,21 +469,12 @@ mod tests { .contains("request_hash=abc123def456")); } - /// Contract tests: validate our types against the MARS OpenAPI JSON Schema - /// loaded from `components/ads-client/openapi.json`. - /// - /// Each test fixture is first validated against the spec schema (with `$ref` - /// resolved), then deserialized into our Rust types. This ensures our - /// fixtures are spec-accurate AND our types can handle real MARS responses. - /// - /// Spec: https://ads.mozilla.org/assets/docs/openapi/mars-api.html#operation/getAds + /// Validate fixtures against the MARS OpenAPI spec, then deserialize into our types. mod contract { use super::*; use crate::test_utils::{mars_schema, validate_against_mars_schema}; use serde_json::json; - // ── Image ────────────────────────────────────────────────────────────── - #[test] fn test_image_full_validates_and_deserializes() { let schema = mars_schema("Image"); @@ -500,11 +491,8 @@ mod tests { "block_key": "block-abc-123" }); validate_against_mars_schema(&schema, &fixture); - let ad: AdImage = - serde_json::from_value(fixture).expect("Image should deserialize from spec JSON"); - assert_eq!(ad.format, "billboard"); - assert_eq!(ad.block_key, "block-abc-123"); - assert_eq!(ad.alt_text, Some("Example ad".to_string())); + serde_json::from_value::(fixture) + .expect("Image should deserialize from spec JSON"); } #[test] @@ -521,17 +509,12 @@ mod tests { "block_key": "block-abc-123" }); validate_against_mars_schema(&schema, &fixture); - let ad: AdImage = serde_json::from_value(fixture) + serde_json::from_value::(fixture) .expect("Image should deserialize without optional fields"); - assert_eq!(ad.alt_text, None); - assert_eq!(ad.callbacks.report, None); } - // INTENTIONAL DEVIATION: The MARS spec marks `click` and `impression` - // inside AdCallbacks as optional strings. We require them as non-optional - // `Url` fields because an ad without tracking callbacks is not - // actionable — we'd rather drop it (AdResponse::parse silently skips - // items that fail to deserialize) than show a broken ad. + // Spec marks click/impression as optional; we require them so ads + // without callbacks are dropped during deserialization. #[test] fn test_image_missing_click_is_spec_valid_but_fails_our_deserialization() { let schema = mars_schema("Image"); @@ -542,13 +525,8 @@ mod tests { "image_url": "https://example.com/image.png", "block_key": "block-abc-123" }); - // Valid according to the MARS spec (callbacks fields are optional) validate_against_mars_schema(&schema, &fixture); - // But fails OUR deserialization (we intentionally require click) - assert!( - serde_json::from_value::(fixture).is_err(), - "Image without click URL should fail to deserialize" - ); + assert!(serde_json::from_value::(fixture).is_err()); } #[test] @@ -562,15 +540,10 @@ mod tests { "block_key": "block-abc-123" }); validate_against_mars_schema(&schema, &fixture); - assert!( - serde_json::from_value::(fixture).is_err(), - "Image without impression URL should fail to deserialize" - ); + assert!(serde_json::from_value::(fixture).is_err()); } - // INTENTIONAL OMISSION: The MARS spec includes an `attributions` object - // on all ad types. We do not model it. serde ignores unknown fields by - // default so this is safe — we just don't surface attribution data. + // We don't model attributions; serde ignores unknown fields. #[test] fn test_image_with_attributions_validates_and_deserializes() { let schema = mars_schema("Image"); @@ -589,14 +562,10 @@ mod tests { } }); validate_against_mars_schema(&schema, &fixture); - assert!( - serde_json::from_value::(fixture).is_ok(), - "Unknown attributions field should be ignored gracefully" - ); + serde_json::from_value::(fixture) + .expect("attributions field should be ignored"); } - // ── Spoc ─────────────────────────────────────────────────────────────── - #[test] fn test_spoc_full_validates_and_deserializes() { let schema = mars_schema("Spoc"); @@ -622,12 +591,8 @@ mod tests { } }); validate_against_mars_schema(&schema, &fixture); - let ad: AdSpoc = - serde_json::from_value(fixture).expect("Spoc should deserialize from spec JSON"); - assert_eq!(ad.format, "spoc"); - assert_eq!(ad.domain, "example.com"); - assert_eq!(ad.caps.day, 3); - assert!((ad.ranking.item_score - 0.95).abs() < f64::EPSILON); + serde_json::from_value::(fixture) + .expect("Spoc should deserialize from spec JSON"); } #[test] @@ -650,14 +615,10 @@ mod tests { "ranking": { "priority": 1, "item_score": 0.5 } }); validate_against_mars_schema(&schema, &fixture); - let ad: AdSpoc = serde_json::from_value(fixture) + serde_json::from_value::(fixture) .expect("Spoc should deserialize without optional fields"); - assert_eq!(ad.sponsored_by_override, None); - assert_eq!(ad.ranking.personalization_models, None); } - // ── Tile ─────────────────────────────────────────────────────────────── - #[test] fn test_tile_validates_and_deserializes() { let schema = mars_schema("Tile"); @@ -673,10 +634,8 @@ mod tests { "block_key": "block-tile-123" }); validate_against_mars_schema(&schema, &fixture); - let ad: AdTile = - serde_json::from_value(fixture).expect("Tile should deserialize from spec JSON"); - assert_eq!(ad.format, "tile"); - assert_eq!(ad.name, "Example Site"); + serde_json::from_value::(fixture) + .expect("Tile should deserialize from spec JSON"); } } diff --git a/components/ads-client/src/test_utils.rs b/components/ads-client/src/test_utils.rs index 40a4a9aac7..5669058e74 100644 --- a/components/ads-client/src/test_utils.rs +++ b/components/ads-client/src/test_utils.rs @@ -14,13 +14,9 @@ use crate::client::{ }, }; -// ── MARS OpenAPI schema helpers ───────────────────────────────────────────── - -/// The MARS OpenAPI spec, embedded at compile time. const OPENAPI_JSON: &str = include_str!("../openapi.json"); -/// Load a named schema from `components/schemas/` in the MARS OpenAPI -/// spec, with all `$ref` pointers recursively resolved inline. +/// Load a named schema from the MARS OpenAPI spec with `$ref` pointers resolved. pub fn mars_schema(name: &str) -> serde_json::Value { let root: serde_json::Value = serde_json::from_str(OPENAPI_JSON).expect("openapi.json should parse"); @@ -32,8 +28,6 @@ pub fn mars_schema(name: &str) -> serde_json::Value { resolve_refs(&schema, &root) } -/// Validate a JSON instance against a MARS OpenAPI schema. Panics with a -/// descriptive message on validation failure. pub fn validate_against_mars_schema(schema: &serde_json::Value, instance: &serde_json::Value) { let validator = jsonschema::validator_for(schema).expect("schema should compile"); if let Err(e) = validator.validate(instance) { @@ -44,13 +38,10 @@ pub fn validate_against_mars_schema(schema: &serde_json::Value, instance: &serde } } -/// Recursively resolve `$ref` JSON pointers within a JSON Schema value, -/// using `root` (the full OpenAPI document) as the resolution base. fn resolve_refs(value: &serde_json::Value, root: &serde_json::Value) -> serde_json::Value { match value { serde_json::Value::Object(map) => { if let Some(serde_json::Value::String(ref_path)) = map.get("$ref") { - // Follow the JSON Pointer (e.g. "#/components/schemas/AdContent") let pointer = ref_path.trim_start_matches('#'); let resolved = root .pointer(pointer) diff --git a/components/ads-client/tests/integration_test.rs b/components/ads-client/tests/integration_test.rs index 31de61c87b..fb0411eb40 100644 --- a/components/ads-client/tests/integration_test.rs +++ b/components/ads-client/tests/integration_test.rs @@ -8,13 +8,13 @@ use std::time::Duration; use ads_client::{ http_cache::{ByteSize, CacheOutcome, HttpCache, RequestCachePolicy}, - MozAdsClientBuilder, MozAdsEnvironment, MozAdsPlacementRequest, MozAdsPlacementRequestWithCount, + MozAdsClientBuilder, MozAdsEnvironment, MozAdsPlacementRequest, + MozAdsPlacementRequestWithCount, }; use std::sync::Arc; use url::Url; use viaduct::Request; -/// Test-only hashable wrapper around Request. #[derive(Clone)] struct TestRequest(Request); @@ -31,18 +31,8 @@ impl From for Request { } } -// ── Contract tests against the MARS staging server ──────────────────────────── -// -// These tests validate that our Rust types can round-trip real responses from -// the MARS staging environment (ads.allizom.org). They are #[ignore] by default -// and should be run: -// - manually: cargo test -p ads-client --test integration_test -- --ignored -// - in CI: a dedicated Taskcluster task gated on components/ads-client/** changes -// -// If a test fails it means either our types have drifted from the MARS schema -// or the staging server is returning unexpected data — both are worth investigating. - -/// Build a client pointed at the MARS staging server. +/// Contract tests against the MARS staging server (ads.allizom.org). +/// Run with: cargo test -p ads-client --test integration_test -- --ignored fn staging_client() -> ads_client::MozAdsClient { Arc::new(MozAdsClientBuilder::new()) .environment(MozAdsEnvironment::Staging) @@ -69,24 +59,6 @@ fn test_contract_image_staging() { placements.contains_key("mock_pocket_billboard_1"), "Response missing expected placement key" ); - - let ad = placements - .get("mock_pocket_billboard_1") - .expect("Placement should exist"); - - // Assert all required spec fields are present and non-empty - assert!(!ad.block_key.is_empty(), "block_key should be non-empty"); - assert!(!ad.format.is_empty(), "format should be non-empty"); - assert!(!ad.image_url.is_empty(), "image_url should be non-empty"); - assert!(!ad.url.is_empty(), "url should be non-empty"); - assert!( - !ad.callbacks.click.as_str().is_empty(), - "callbacks.click should be non-empty" - ); - assert!( - !ad.callbacks.impression.as_str().is_empty(), - "callbacks.impression should be non-empty" - ); } #[test] @@ -110,28 +82,6 @@ fn test_contract_spoc_staging() { placements.contains_key("newtab_spocs"), "Response missing expected placement key" ); - - let spocs = placements.get("newtab_spocs").expect("Placement should exist"); - assert!(!spocs.is_empty(), "Should have received at least one spoc"); - - let ad = &spocs[0]; - assert!(!ad.block_key.is_empty(), "block_key should be non-empty"); - assert!(!ad.format.is_empty(), "format should be non-empty"); - assert!(!ad.image_url.is_empty(), "image_url should be non-empty"); - assert!(!ad.url.is_empty(), "url should be non-empty"); - assert!(!ad.title.is_empty(), "title should be non-empty"); - assert!(!ad.domain.is_empty(), "domain should be non-empty"); - assert!(!ad.excerpt.is_empty(), "excerpt should be non-empty"); - assert!(!ad.sponsor.is_empty(), "sponsor should be non-empty"); - assert!(!ad.caps.cap_key.is_empty(), "caps.cap_key should be non-empty"); - assert!( - !ad.callbacks.click.as_str().is_empty(), - "callbacks.click should be non-empty" - ); - assert!( - !ad.callbacks.impression.as_str().is_empty(), - "callbacks.impression should be non-empty" - ); } #[test] @@ -154,118 +104,6 @@ fn test_contract_tile_staging() { placements.contains_key("newtab_tile_1"), "Response missing expected placement key" ); - - let ad = placements - .get("newtab_tile_1") - .expect("Placement should exist"); - - assert!(!ad.block_key.is_empty(), "block_key should be non-empty"); - assert!(!ad.format.is_empty(), "format should be non-empty"); - assert!(!ad.image_url.is_empty(), "image_url should be non-empty"); - assert!(!ad.url.is_empty(), "url should be non-empty"); - assert!(!ad.name.is_empty(), "name should be non-empty"); - assert!( - !ad.callbacks.click.as_str().is_empty(), - "callbacks.click should be non-empty" - ); - assert!( - !ad.callbacks.impression.as_str().is_empty(), - "callbacks.impression should be non-empty" - ); -} - -// ── Prod tests (existing) ────────────────────────────────────────────────────── - -#[test] -#[ignore] -fn test_mock_pocket_billboard_1_placement() { - viaduct_dev::init_backend_dev(); - - let client = MozAdsClientBuilder::new().build(); - - let placement_request = MozAdsPlacementRequest { - placement_id: "mock_pocket_billboard_1".to_string(), - iab_content: None, - }; - - let result = client.request_image_ads(vec![placement_request], None); - - assert!(result.is_ok(), "Failed to request ads: {:?}", result.err()); - - let placements = result.unwrap(); - - assert!( - placements.contains_key("mock_pocket_billboard_1"), - "Response should contain placement_id 'mock_pocket_billboard_1'" - ); - - placements - .get("mock_pocket_billboard_1") - .expect("Placement should exist"); -} - -#[test] -#[ignore] -fn test_newtab_spocs_placement() { - viaduct_dev::init_backend_dev(); - - let client = MozAdsClientBuilder::new().build(); - - let count = 3; - let placement_request = MozAdsPlacementRequestWithCount { - placement_id: "newtab_spocs".to_string(), - count, - iab_content: None, - }; - - let result = client.request_spoc_ads(vec![placement_request], None); - - assert!(result.is_ok(), "Failed to request ads: {:?}", result.err()); - - let placements = result.unwrap(); - - assert!( - placements.contains_key("newtab_spocs"), - "Response should contain placement_id 'newtab_spocs'" - ); - - let spocs = placements - .get("newtab_spocs") - .expect("Placement should exist"); - - assert_eq!( - spocs.len(), - count as usize, - "Number of spocs should equal count parameter" - ); -} - -#[test] -#[ignore] -fn test_newtab_tile_1_placement() { - viaduct_dev::init_backend_dev(); - - let client = MozAdsClientBuilder::new().build(); - - let placement_request = MozAdsPlacementRequest { - placement_id: "newtab_tile_1".to_string(), - iab_content: None, - }; - - let result = client.request_tile_ads(vec![placement_request], None); - - assert!(result.is_ok(), "Failed to request ads: {:?}", result.err()); - - let placements = result.unwrap(); - - assert!( - placements.contains_key("newtab_tile_1"), - "Response should contain placement_id 'newtab_tile_1'" - ); - - placements - .get("newtab_tile_1") - .expect("Placement should exist"); } #[test] From a478333ddfb3e91c08b4f633342441b83a3a8597 Mon Sep 17 00:00:00 2001 From: ahanot Date: Mon, 9 Mar 2026 17:23:23 -0400 Subject: [PATCH 4/5] fix integration tests: use hyper backend and mock placements --- Cargo.lock | 1 + components/ads-client/Cargo.toml | 1 + .../ads-client/tests/integration_test.rs | 47 +++++++++---------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e2d9efb10a..ddaa5d40e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,7 @@ dependencies = [ "uuid", "viaduct", "viaduct-dev", + "viaduct-hyper", ] [[package]] diff --git a/components/ads-client/Cargo.toml b/components/ads-client/Cargo.toml index 30d23a60cc..11c0f22253 100644 --- a/components/ads-client/Cargo.toml +++ b/components/ads-client/Cargo.toml @@ -35,6 +35,7 @@ jsonschema = "0.28" mockall = "0.12" mockito = { version = "0.31", default-features = false } viaduct-dev = { path = "../support/viaduct-dev" } +viaduct-hyper = { path = "../support/viaduct-hyper" } [build-dependencies] uniffi = { version = "0.31", features = ["build"] } diff --git a/components/ads-client/tests/integration_test.rs b/components/ads-client/tests/integration_test.rs index fb0411eb40..2f2620b429 100644 --- a/components/ads-client/tests/integration_test.rs +++ b/components/ads-client/tests/integration_test.rs @@ -31,8 +31,10 @@ impl From for Request { } } -/// Contract tests against the MARS staging server (ads.allizom.org). -/// Run with: cargo test -p ads-client --test integration_test -- --ignored +fn init_backend() { + let _ = viaduct_hyper::viaduct_init_backend_hyper(); +} + fn staging_client() -> ads_client::MozAdsClient { Arc::new(MozAdsClientBuilder::new()) .environment(MozAdsEnvironment::Staging) @@ -40,36 +42,37 @@ fn staging_client() -> ads_client::MozAdsClient { } #[test] -#[ignore = "contract test: run manually or in dedicated CI against ads.allizom.org"] +#[ignore = "contract test: hits MARS staging"] fn test_contract_image_staging() { - viaduct_dev::init_backend_dev(); + init_backend(); let client = staging_client(); let result = client.request_image_ads( vec![MozAdsPlacementRequest { - placement_id: "mock_pocket_billboard_1".to_string(), + 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_pocket_billboard_1"), - "Response missing expected placement key" + result.is_ok(), + "Image ad request failed: {:?}", + result.err() ); + let placements = result.unwrap(); + assert!(placements.contains_key("mock_billboard_1")); } #[test] -#[ignore = "contract test: run manually or in dedicated CI against ads.allizom.org"] +#[ignore = "contract test: hits MARS staging"] fn test_contract_spoc_staging() { - viaduct_dev::init_backend_dev(); + init_backend(); let client = staging_client(); let result = client.request_spoc_ads( vec![MozAdsPlacementRequestWithCount { - placement_id: "newtab_spocs".to_string(), + placement_id: "mock_spoc_1".to_string(), count: 1, iab_content: None, }], @@ -78,21 +81,18 @@ fn test_contract_spoc_staging() { assert!(result.is_ok(), "Spoc ad request failed: {:?}", result.err()); let placements = result.unwrap(); - assert!( - placements.contains_key("newtab_spocs"), - "Response missing expected placement key" - ); + assert!(placements.contains_key("mock_spoc_1")); } #[test] -#[ignore = "contract test: run manually or in dedicated CI against ads.allizom.org"] +#[ignore = "contract test: hits MARS staging"] fn test_contract_tile_staging() { - viaduct_dev::init_backend_dev(); + init_backend(); let client = staging_client(); let result = client.request_tile_ads( vec![MozAdsPlacementRequest { - placement_id: "newtab_tile_1".to_string(), + placement_id: "mock_tile_1".to_string(), iab_content: None, }], None, @@ -100,16 +100,13 @@ fn test_contract_tile_staging() { assert!(result.is_ok(), "Tile ad request failed: {:?}", result.err()); let placements = result.unwrap(); - assert!( - placements.contains_key("newtab_tile_1"), - "Response missing expected placement key" - ); + assert!(placements.contains_key("mock_tile_1")); } #[test] #[ignore] fn test_cache_works_using_real_timeouts() { - viaduct_dev::init_backend_dev(); + init_backend(); let cache: HttpCache = HttpCache::builder("integration_tests.db") .default_ttl(Duration::from_secs(60)) @@ -122,7 +119,7 @@ fn test_cache_works_using_real_timeouts() { "context_id": "12347fff-00b0-aaaa-0978-189231239808", "placements": [ { - "placement": "mock_pocket_billboard_1", + "placement": "mock_billboard_1", "count": 1, } ], From e532c83b0d6326888e77a0891c22899099441ed5 Mon Sep 17 00:00:00 2001 From: ahanot Date: Mon, 9 Mar 2026 17:28:38 -0400 Subject: [PATCH 5/5] Upgrade jsonschema to 0.43 (latest compatible with serde_json 1.0.142) Versions 0.44+ have a CompactFormatter: Default compile error with the current serde_json version. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 671 ++++++++++++++++++++++++------- components/ads-client/Cargo.toml | 2 +- 2 files changed, 524 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddaa5d40e8..5df4c08ce5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,7 +60,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.1", + "getrandom 0.3.4", "once_cell", "serde", "version_check", @@ -76,6 +76,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -199,6 +205,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.1.0" @@ -229,6 +241,28 @@ dependencies = [ "url", ] +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.6.19" @@ -256,7 +290,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 0.1.2", "tokio", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", ] @@ -344,7 +378,7 @@ version = "0.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -379,11 +413,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -517,15 +551,22 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.10" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -553,7 +594,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -678,6 +719,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -688,6 +738,16 @@ dependencies = [ "unicode-width 0.1.11", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console" version = "0.15.5" @@ -750,11 +810,21 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core_maths" @@ -945,6 +1015,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + [[package]] name = "deflate64" version = "0.1.9" @@ -1083,6 +1159,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ece" version = "2.3.1" @@ -1526,9 +1608,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fancy-regex" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" dependencies = [ "bit-set", "regex-automata", @@ -1567,6 +1649,12 @@ dependencies = [ "uniffi", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "find-places-db" version = "0.1.0" @@ -1596,9 +1684,9 @@ dependencies = [ [[package]] name = "fluent-uri" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" dependencies = [ "borrow-or-share", "ref-cast", @@ -1617,6 +1705,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1663,6 +1757,12 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd79fa345a495d3ae89fb7165fec01c0e72f41821d642dda363a1e97975652e" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -1815,27 +1915,27 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", + "r-efi", + "wasip2", "wasm-bindgen", - "windows-targets 0.52.6", ] [[package]] @@ -1880,6 +1980,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "1.8.2" @@ -1904,7 +2023,18 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "foldhash", + "foldhash 0.1.4", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -2029,9 +2159,9 @@ checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" [[package]] name = "httparse" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -2055,7 +2185,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.9", "http-body 0.4.5", "httparse", @@ -2071,23 +2201,42 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -2103,18 +2252,22 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-util", "http 1.4.0", "http-body 1.0.1", - "hyper 1.5.2", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2 0.5.8", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -2410,9 +2563,19 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] [[package]] name = "is-docker" @@ -2492,6 +2655,28 @@ dependencies = [ "regex", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.32" @@ -2513,26 +2698,30 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.28.3" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f66fe41fa46a5c83ed1c717b7e0b4635988f427083108c8cf0a882cc13441" +checksum = "2dcfbe6df48e0121219eefc8d6a30b872ac2769c7896454bda06f7b64129fa22" dependencies = [ "ahash", - "base64 0.22.1", "bytecount", + "data-encoding", "email_address", "fancy-regex", "fraction", + "getrandom 0.3.4", "idna", "itoa", "num-cmp", - "once_cell", + "num-traits", "percent-encoding", "referencing", + "regex", "regex-syntax", - "reqwest 0.12.9", + "reqwest 0.13.2", + "rustls", "serde", "serde_json", + "unicode-general-category", "uuid-simd", ] @@ -2573,9 +2762,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -2599,7 +2788,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.11.0", "libc", "redox_syscall 0.5.17", ] @@ -2662,11 +2851,10 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -2919,7 +3107,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] @@ -2977,10 +3165,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.5", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.6.1", "security-framework-sys", "tempfile", ] @@ -3033,8 +3221,8 @@ dependencies = [ "termcolor", "thiserror 2.0.3", "tokio", - "tower", - "tower-http", + "tower 0.4.13", + "tower-http 0.4.2", "tower-livereload", "unicode-segmentation", "update-informer", @@ -3351,7 +3539,7 @@ version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -3377,6 +3565,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-src" version = "300.3.1+3.3.1" @@ -3422,9 +3616,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -3432,15 +3626,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.13", + "redox_syscall 0.5.17", "smallvec", - "windows-sys 0.36.1", + "windows-link 0.2.1", ] [[package]] @@ -3734,6 +3928,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -3761,7 +3961,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.17", ] [[package]] @@ -3848,7 +4048,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.11.0", ] [[package]] @@ -3863,7 +4063,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.7", + "getrandom 0.2.17", "redox_syscall 0.2.13", "thiserror 1.0.69", ] @@ -3890,13 +4090,15 @@ dependencies = [ [[package]] name = "referencing" -version = "0.28.3" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0dcb5ab28989ad7c91eb1b9531a37a1a137cc69a0499aee4117cae4a107c464" +checksum = "37add1aa1d619a975521d262d09f100f1f767791c9386c03679450f30acd78c1" dependencies = [ "ahash", "fluent-uri", - "once_cell", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "parking_lot", "percent-encoding", "serde_json", ] @@ -4011,7 +4213,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.9", "http-body 0.4.5", "hyper 0.14.27", @@ -4039,38 +4241,41 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.9" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.8.1", + "hyper-rustls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", - "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", + "tokio-rustls", + "tower 0.5.3", + "tower-http 0.6.8", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-registry", ] [[package]] @@ -4084,6 +4289,20 @@ dependencies = [ "viaduct", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rkv" version = "0.20.0" @@ -4092,7 +4311,7 @@ checksum = "0f67a9dbc634fcd36a2d1d800ca818065dcf71a1d907dc35130c2d1552c6e1dc" dependencies = [ "arrayref", "bincode", - "bitflags 2.8.0", + "bitflags 2.11.0", "id-arena", "lazy_static", "log", @@ -4110,7 +4329,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.11.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4170,6 +4389,80 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -4263,7 +4556,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.3", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -4271,9 +4577,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -4511,12 +4817,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.8" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4961,6 +5267,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.3" @@ -5056,13 +5372,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ac8060a61f8758a61562f6fb53ba3cbe1ca906f001df2e53cccddcdbee91e7c" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.11.0", "bytes", "futures-core", "futures-util", @@ -5081,11 +5412,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-livereload" @@ -5098,14 +5447,14 @@ dependencies = [ "http-body 0.4.5", "pin-project-lite", "tokio", - "tower", + "tower 0.4.13", ] [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -5209,6 +5558,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -5375,6 +5730,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "update-informer" version = "1.0.0" @@ -5411,7 +5772,7 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.4", "serde", ] @@ -5422,7 +5783,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" dependencies = [ "outref", - "uuid", "vsimd", ] @@ -5527,12 +5887,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -5738,6 +6098,15 @@ dependencies = [ "webext-storage", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "weedle2" version = "5.0.0" @@ -5804,34 +6173,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result", - "windows-targets 0.52.6", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -5852,13 +6197,22 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm 0.42.0", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm 0.42.0", - "windows_x86_64_msvc 0.42.0", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", ] [[package]] @@ -5879,6 +6233,30 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.0" @@ -5912,9 +6290,9 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" @@ -5936,9 +6314,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" @@ -5960,9 +6338,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" @@ -5990,9 +6368,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" @@ -6014,9 +6392,9 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" @@ -6032,9 +6410,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" @@ -6056,9 +6434,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" @@ -6100,13 +6478,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags 2.8.0", -] +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -6311,7 +6686,7 @@ dependencies = [ "deflate64", "displaydoc", "flate2", - "getrandom 0.3.1", + "getrandom 0.3.4", "hmac", "indexmap 2.5.0", "lzma-rs", diff --git a/components/ads-client/Cargo.toml b/components/ads-client/Cargo.toml index 11c0f22253..5457e95a46 100644 --- a/components/ads-client/Cargo.toml +++ b/components/ads-client/Cargo.toml @@ -31,7 +31,7 @@ viaduct = { path = "../viaduct" } sql-support = { path = "../support/sql" } [dev-dependencies] -jsonschema = "0.28" +jsonschema = "0.43" mockall = "0.12" mockito = { version = "0.31", default-features = false } viaduct-dev = { path = "../support/viaduct-dev" }