Skip to content

Add focused coverage for production readiness test gaps#688

Open
ChristianPavilonis wants to merge 4 commits into
mainfrom
test-coverage-cleanup-396
Open

Add focused coverage for production readiness test gaps#688
ChristianPavilonis wants to merge 4 commits into
mainfrom
test-coverage-cleanup-396

Conversation

@ChristianPavilonis

@ChristianPavilonis ChristianPavilonis commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Changes

File Change
crates/trusted-server-core/src/error.rs Added direct status-code mapping coverage for every current TrustedServerError variant, including a compile-time exhaustive match guard for new variants.
crates/trusted-server-core/src/host_rewrite.rs Added direct boundary tests for exact hosts, paths, punctuation, multiple occurrences, subdomains, suffixes, empty inputs, and ports.
crates/trusted-server-core/src/tsjs.rs Added tests for unified/deferred script source formatting, hash shape, script tag attributes, empty deferred output, and deferred order preservation.
crates/trusted-server-core/src/auction/formats.rs Added request/response conversion tests for banner formats, bidder params, context allowlisting, malformed sizes, unsupported media skip behavior, OpenRTB response fields, mediation strategy, missing prices, and out-of-range dimensions.

Closes

Closes #448
Closes #449
Closes #450
Closes #453

Note: #455 references the old synthetic-template path, which no longer exists on current main after the EC ID migration. It should be triaged separately rather than auto-closed by this PR.

Test plan

  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/js/lib && npx vitest run
  • JS format: cd crates/js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: cargo test -p trusted-server-core status_code_maps_each_error_variant_to_expected_http_response -- --nocapture; cargo test -p trusted-server-core tsjs_ -- --nocapture; cargo test -p trusted-server-core rewrite -- --nocapture; cargo test -p trusted-server-core convert_tsjs_to_auction_request -- --nocapture; cargo test -p trusted-server-core convert_to_openrtb_response -- --nocapture

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses log macros (not println!)
  • New code has tests
  • No secrets or credentials committed

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Pure test additions across four files — error.rs, host_rewrite.rs, tsjs.rs, and auction/formats.rs. No production code changed. All CI checks pass. Test quality is high overall; a few missing edge cases and one misleading helper message noted inline.

Non-blocking

🏕 camp site

  • Forbidden and AllowlistViolation user_message untested — Both variants fall into the _ catch-all in user_message and return "An internal error occurred" despite being 4xx responses. The existing server_errors_return_generic_message test does not include them. Worth adding both to explicitly document (and lock in) the intended behavior — or, if a more descriptive message is preferred for 403s, this is the place to catch it.

🌱 seedling

  • Host-in-path-segment behavior unspecified (host_rewrite.rs) — Input https://cdn.example.com/assets/origin.example.com/image.png will be rewritten because / is not a is_host_char. This may be intentional for Next.js __next_f payloads but is undocumented and untested. A test explicitly asserting current behavior would prevent silent regressions.

  • Empty module list edge case (tsjs.rs) — tsjs_script_src(&[]) behavior is unspecified. What does concatenated_hash(&[]) return? Low risk but a documentation test would lock in the contract.

CI Status

  • cargo fmt: PASS
  • cargo clippy: PASS
  • cargo test: PASS
  • vitest: PASS
  • integration tests: PASS
  • browser integration tests: PASS

Comment thread crates/trusted-server-core/src/error.rs Outdated
Comment thread crates/trusted-server-core/src/error.rs Outdated
Comment thread crates/trusted-server-core/src/host_rewrite.rs Outdated
Comment thread crates/trusted-server-core/src/host_rewrite.rs
Comment thread crates/trusted-server-core/src/tsjs.rs
Comment thread crates/trusted-server-core/src/auction/formats.rs
Comment thread crates/trusted-server-core/src/auction/formats.rs
Comment thread crates/trusted-server-core/src/auction/formats.rs Outdated

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Follow-up to the prior review on the same head commit — only new findings missed last pass. Still no blocking items; verdict is COMMENT.

  • 3 🌱 seedling — coverage gaps: hash order/dedup invariants, empty winning_bids response, height out-of-range symmetry.
  • 2 ⛏ nitpick — redundant use in the test module; mixed *.example / example.com test-fixture TLDs.
  • 2 👍 praise — compile-time exhaustiveness guard pattern; banner-only behavior pinned by a test.

The prior review's findings remain open (no commits since 2a3edb77) — they're tracked there, not re-posted here.

CI Status

  • cargo fmt: PASS
  • cargo clippy: PASS
  • cargo test: PASS
  • vitest: PASS
  • integration tests: PASS
  • browser integration tests: PASS

Comment thread crates/trusted-server-core/src/tsjs.rs
Comment thread crates/trusted-server-core/src/error.rs Outdated
Comment thread crates/trusted-server-core/src/auction/formats.rs Outdated
Comment thread crates/trusted-server-core/src/auction/formats.rs Outdated
Comment thread crates/trusted-server-core/src/auction/formats.rs
Comment thread crates/trusted-server-core/src/auction/formats.rs
Comment thread crates/trusted-server-core/src/auction/formats.rs

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes to address the 5 non-praise findings (3 🌱 + 2 ⛏) in my prior review — and the 9 still-open items from the earlier review that haven't been addressed yet.

@aram356 aram356 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This test-coverage PR was branched from a stale main (merge-base f275aba5). Since then, #621 (Edge Cookie identity system) reshaped the auction APIs, and the PR no longer compiles against current main — which is why the cargo test CI job is failing. cargo fmt is also red on the merged tree. host_rewrite.rs and tsjs.rs are sound and land cleanly; auction/formats.rs and error.rs need a rebase and rework. Requesting changes on the three blocking items below.

Blocking

🔧 wrench

  • Duplicate mod tests + stale auction API: main already has a test module at formats.rs:339; the new module collides (E0428) and is written against pre-#621 signatures (7-arg conversion, UserInfo.fresh_id, missing AdRequest.eids, removed HEADER_X_TS_EC/_FRESH). (auction/formats.rs:353)
  • Undisclosed runtime behavior change: convert_to_openrtb_response now emits explicit empty seatbid/adomain arrays via a second serialization pass, contradicting the PR's "without changing runtime behavior" claim. (auction/formats.rs:310)
  • Exhaustive error guard out of date: references removed TrustedServerError::Ec and omits new EdgeCookie/PartnerNotFound/InsecureDefault. (error.rs:229)

Non-blocking

🤔 thinking

  • Double serialization on response path: if the explicit-empty-array contract is required, customize serialization on the OpenRTB response types rather than re-walking the JSON tree (to_value + to_vec) on every auction response. (auction/formats.rs:310)

♻️ refactor

  • On rebase: fold both new test modules into the existing mod tests; update to UserInfo { id: Some(...), .. } (no fresh_id), AdRequest { eids: None, .. }, the 5-arg convert_tsjs_to_auction_request, and assert the current HEADER_X_TS_EC_CONSENT/HEADER_X_TS_EIDS headers.

🌱 seedling

  • Unknown-module hash: tsjs_deferred_script_src_uses_empty_hash_for_unknown_module characterizes "unknown module → ?v= with empty hash." Worth a follow-up on whether that should instead be rejected. (tsjs.rs)

📝 note

  • tsjs.rs tests are independent of the EC migration and should merge + compile unchanged.

CI Status

  • fmt: FAIL
  • clippy: PASS
  • rust tests: FAIL (compile errors against merged main — see blocking findings)
  • js tests (vitest): PASS

Comment thread crates/trusted-server-core/src/auction/formats.rs
Comment thread crates/trusted-server-core/src/auction/formats.rs Outdated
Comment thread crates/trusted-server-core/src/error.rs
@ChristianPavilonis ChristianPavilonis force-pushed the test-coverage-cleanup-396 branch from 37b41f0 to 8d63e24 Compare June 5, 2026 20:04

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Third-pass review of head 8d63e24 — all findings from the two prior review rounds are addressed. Only new findings below; nothing blocking. Verdict remains COMMENT.

  • 2 ⛏ nitpick — redundant HeaderValue::from_str, under-enumerated user_message doc
  • 1 🏕 camp site — stale # Errors doc on convert_tsjs_to_auction_request (below)
  • 1 📌 out of scope — misleading generic message now pinned for 403/404 variants
  • 3 🌱 seedling — ec_id: None propagation, empty banner sizes, host-rewrite case sensitivity
  • 1 👍 praise — order-insensitive multi-winner assertions

Non-blocking (body-level)

🏕 camp site

  • Stale # Errors doc on convert_tsjs_to_auction_request (crates/trusted-server-core/src/auction/formats.rs:82, outside this diff): the entry "Fresh EC ID generation fails" is stale — the function never generates EC IDs; the caller does, per the doc paragraph at lines 76–77, and the only error path is the invalid-banner-size ensure!. Since this PR's purpose is documenting this function's behavior, removing the stale bullet fits here:

    /// # Errors
    ///
    /// Returns an error if the request contains invalid banner sizes
    /// (must be `[width, height]`).
    

CI Status

  • fmt: PASS
  • clippy (Analyze rust): PASS
  • rust tests (cargo test): PASS
  • js tests (vitest): PASS
  • integration + browser integration: PASS

let mut req = Request::new("POST", "https://publisher.example.com/auction");
req.set_header(
header::USER_AGENT,
HeaderValue::from_str("Mozilla/5.0 test").expect("should create user-agent header"),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpickHeaderValue::from_str(...).expect(...) is unnecessary: fastly's set_header takes impl ToHeaderValue, so a plain &str works (production code does exactly this at line 320 with set_header(HEADER_X_TS_EC_CONSENT, "ok")). This also drops the use fastly::http::HeaderValue; import at line 344.

fn make_request() -> Request {
    let mut req = Request::new("POST", "https://publisher.example.com/auction");
    req.set_header(header::USER_AGENT, "Mozilla/5.0 test");
    req
}

/// Client errors (4xx) return a brief description; server/integration errors
/// return a generic message. Full error details are preserved in server logs.
/// Selected client errors return a brief description; forbidden and
/// server/integration errors return a generic message. Full error details

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick — The reworded doc says "forbidden and server/integration errors return a generic message", but PartnerNotFound (404) also returns the generic message, and it is neither forbidden nor a server/integration error. Enumerating categories here will keep drifting as variants are added. Simpler and future-proof:

/// Selected client errors return a brief description; all other errors
/// return a generic message. Full error details are preserved in server logs.

}

#[test]
fn other_client_errors_return_generic_user_message() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📌 out of scope — This test now pins "An internal error occurred" for Forbidden / AllowlistViolation (403) and PartnerNotFound (404). That message is misleading for client-fault statuses: a blocked redirect or unknown partner is not an internal error, and clients debugging a 403/404 get actively wrong guidance. Pinning current behavior is right for this PR — but a follow-up issue could introduce distinct generic messages (e.g. "Forbidden", "Not found") without leaking detail.

}
}

fn convert_body_to_auction_request(body: &AdRequest, settings: &Settings) -> AuctionRequest {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 seedling — Every conversion test routes through this helper with Some("existing-ec-id"), so the ec_id: Noneuser.id == None propagation (line 91 / 185 in production) is unpinned. One small test would cover it:

#[test]
fn convert_tsjs_to_auction_request_propagates_missing_ec_id() {
    let settings = make_settings();
    let req = make_request();
    let auction_request = convert_tsjs_to_auction_request(
        &make_banner_body(None),
        &settings,
        &req,
        ConsentContext::default(),
        None,
    )
    .expect("should convert request without EC ID");

    assert!(
        auction_request.user.id.is_none(),
        "should leave user ID unset when caller provides no EC ID"
    );
}

}

#[test]
fn convert_tsjs_to_auction_request_maps_banner_sizes_to_formats() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 seedling — A banner unit with empty sizes: vec![] passes the len() == 2 check vacuously and produces a slot with zero formats. Whether downstream providers handle an empty-formats slot gracefully is unverified. Since this PR pins malformed-size rejection and non-banner skipping, the zero-sizes boundary is the remaining gap — a documenting test (slot created with empty formats, or a decision to reject) would complete the set.

mod tests {
use super::*;

const ORIGIN_HOST: &str = "origin.example.com";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 seedling — Case sensitivity is unpinned: str::find is case-sensitive, so ORIGIN.EXAMPLE.COM (or Origin.Example.com in markup) is never rewritten even though hostnames are case-insensitive per RFC 4343. That may be acceptable for __next_f/Flight payloads where hosts are machine-emitted lowercase, but a one-line documenting test would make the limitation explicit:

#[test]
fn does_not_rewrite_differently_cased_host() {
    assert_no_rewrite(
        "ORIGIN.EXAMPLE.COM/news",
        "should not rewrite differently-cased host occurrences",
    );
}

}

#[test]
fn convert_to_openrtb_response_serializes_multiple_winning_bids() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 praise — Locating seatbids by impid lookup instead of array index makes these assertions immune to HashMap iteration order in winning_bids — exactly the kind of test that would otherwise flake intermittently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants