From 4e9d8234bb3cdffa415b2aaa01f12ca5f87d08e9 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 18 Apr 2026 13:24:48 +0000 Subject: [PATCH] feat(create_report): RealCreateReportTee wrapping reqwest Implements RealCreateReportTee struct and CreateReportTee trait impl. Constructs the POST URL with the xrpc path, serializes the body as JSON, handles Authorization header for authenticated requests, and captures status, content-type, and raw body. Mirrors the RealHttpTee pattern with production tracing instrumentation. Co-Authored-By: Claude Haiku 4.5 --- src/commands/test/labeler/create_report.rs | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/commands/test/labeler/create_report.rs b/src/commands/test/labeler/create_report.rs index 4c2f921..60e2665 100644 --- a/src/commands/test/labeler/create_report.rs +++ b/src/commands/test/labeler/create_report.rs @@ -108,6 +108,91 @@ pub trait CreateReportTee: Send + Sync { ) -> Result; } +/// Real `CreateReportTee` implementation using reqwest. +pub struct RealCreateReportTee { + client: reqwest::Client, + endpoint: Url, +} + +impl RealCreateReportTee { + /// Create a new `RealCreateReportTee` using the given shared reqwest + /// client and labeler endpoint. The endpoint is the labeler's service + /// URL (e.g., `https://labeler.example.com`); the POST path + /// `/xrpc/com.atproto.moderation.createReport` is appended. + pub fn new(client: reqwest::Client, endpoint: Url) -> Self { + Self { client, endpoint } + } +} + +#[async_trait] +impl CreateReportTee for RealCreateReportTee { + async fn post_create_report( + &self, + auth: Option<&str>, + body: &serde_json::Value, + ) -> Result { + let mut url = self.endpoint.clone(); + url.set_path("xrpc/com.atproto.moderation.createReport"); + let source_url = url.to_string(); + + tracing::debug!( + url = %source_url, + auth_kind = match auth { + None => "none", + Some(t) if !t.starts_with("ey") => "malformed", + Some(_) => "jwt", + }, + "report stage: issuing createReport POST" + ); + + let mut req = self + .client + .post(url.as_str()) + .header("Content-Type", "application/json") + .body(serde_json::to_vec(body).expect("serde_json::Value always serializes")); + if let Some(token) = auth { + req = req.header("Authorization", format!("Bearer {token}")); + } + + let response = req + .send() + .await + .map_err(|e| CreateReportStageError::Transport { + message: e.to_string(), + source: Some(Box::new(e)), + })?; + + let status = response.status(); + let content_type = response + .headers() + .get(reqwest::header::CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .map(|s| s.to_ascii_lowercase()); + + let body_bytes = response + .bytes() + .await + .map_err(|e| CreateReportStageError::Transport { + message: e.to_string(), + source: Some(Box::new(e)), + })?; + + tracing::debug!( + url = %source_url, + status = %status, + body_len = body_bytes.len(), + "report stage: createReport response received" + ); + + Ok(RawCreateReportResponse { + status, + content_type, + raw_body: Arc::from(body_bytes.as_ref()), + source_url, + }) + } +} + #[cfg(test)] mod did_tests { use super::*; -- 2.51.2