CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(create_report): wire PDS service-auth and PDS-proxied checks (AC5, AC6)

Task 4: Add PdsXrpcClient plumbing and implement PDS check logic.

- Add pds_xrpc_client and pds_xrpc_client_override fields to LabelerOptions
- Add PdsServiceAuthRejected and PdsProxiedRejected diagnostics
- Implement AC5/AC6 logic: fetch session, compute subject, run mode-2 and
mode-3 checks
- Gate checks on --handle, --app-password, and --commit-report flags
- Distinguish labeler-side vs PDS-side failures via heuristic (UpstreamError,
502, 504 status codes)
- Add helper functions: fetch_session_and_did, fetch_service_auth_jwt
- Update pipeline to construct RealPdsXrpcClient from PDS endpoint
- Update all test suites to initialize new LabelerOptions fields

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

authored by

Jack Grigg
Claude Haiku 4.5
and committed by
Tangled
32e6b2d3 5e89e858

+361 -1365
+13 -1
src/commands/test/labeler.rs
··· 141 141 }; 142 142 let self_mint_signer_ref = self_mint_signer_opt.as_ref(); 143 143 144 + // Construct PDS credentials when both handle and app_password are supplied. 145 + let pds_credentials = match (self.handle.as_deref(), self.app_password.as_deref()) { 146 + (Some(h), Some(p)) => Some(pipeline::PdsCredentials { 147 + handle: h.to_string(), 148 + app_password: p.to_string(), 149 + }), 150 + _ => None, 151 + }; 152 + let pds_credentials_ref = pds_credentials.as_ref(); 153 + 144 154 // Run the pipeline. 145 155 let opts = LabelerOptions { 146 156 http: &http, ··· 156 166 self_mint_curve: self.self_mint_curve, 157 167 report_subject_override: report_subject_override.as_ref(), 158 168 self_mint_signer: self_mint_signer_ref, 159 - pds_credentials: None, 169 + pds_credentials: pds_credentials_ref, 170 + pds_xrpc_client: None, 171 + pds_xrpc_client_override: None, 160 172 run_id: &run_id, 161 173 }; 162 174
+271 -2
src/commands/test/labeler/create_report.rs
··· 694 694 pub report_subject_override: Option<&'a crate::common::identity::Did>, 695 695 pub self_mint_signer: Option<&'a self_mint::SelfMintSigner>, 696 696 pub pds_credentials: Option<&'a crate::commands::test::labeler::pipeline::PdsCredentials>, 697 + pub pds_xrpc_client: Option<&'a dyn PdsXrpcClient>, 697 698 pub run_id: &'a str, 698 699 } 699 700 ··· 1143 1144 results.push(Check::SelfMintAccepted.skip(reason)); 1144 1145 } 1145 1146 1146 - results.push(Check::PdsServiceAuthAccepted.skip("not yet implemented (Phase 8)")); 1147 - results.push(Check::PdsProxiedAccepted.skip("not yet implemented (Phase 8)")); 1147 + // AC5/AC6 — PDS-mediated modes (modes 2 and 3). 1148 + // Compute the gating precondition common to both PDS checks. 1149 + let pds_gate_reason: &'static str = "requires --handle, --app-password, and --commit-report"; 1150 + let pds_ready = 1151 + opts.commit_report && opts.pds_credentials.is_some() && opts.pds_xrpc_client.is_some(); 1152 + 1153 + if !pds_ready { 1154 + results.push(Check::PdsServiceAuthAccepted.skip(pds_gate_reason)); 1155 + results.push(Check::PdsProxiedAccepted.skip(pds_gate_reason)); 1156 + } else { 1157 + // Safe to unwrap thanks to pds_ready. 1158 + let creds = opts.pds_credentials.expect("pds_ready implies creds"); 1159 + let pds_client = opts.pds_xrpc_client.expect("pds_ready implies client"); 1160 + 1161 + // Recompute locality for pollution-avoidance. 1162 + let is_local = is_local_labeler_hostname(&id_facts.labeler_endpoint); 1163 + let reason_type = pollution::choose_reason_type( 1164 + id_facts.reason_types.as_deref().unwrap_or(&[]), 1165 + is_local, 1166 + ); 1167 + 1168 + // Fetch the user session (DID and access JWT). Both PDS modes need 1169 + // these upfront. 1170 + match fetch_session_and_did(pds_client, &creds.handle, &creds.app_password).await { 1171 + Err(message) => { 1172 + results.push(Check::PdsServiceAuthAccepted.network_error(message.clone())); 1173 + // AC6.3: if session fetch fails, proxied mode also fails at PDS. 1174 + results.push(Check::PdsProxiedAccepted.network_error(message)); 1175 + } 1176 + Ok(session) => { 1177 + let user_did = Did(session.did); 1178 + let access_jwt = session.access_jwt; 1179 + let subject = pollution::choose_subject( 1180 + id_facts.subject_types.as_deref().unwrap_or(&[]), 1181 + &user_did, 1182 + opts.report_subject_override, 1183 + is_local, 1184 + ); 1185 + let sentinel = sentinel::build(opts.run_id, SystemTime::now()); 1186 + let pds_body = serde_json::json!({ 1187 + "reasonType": reason_type, 1188 + "subject": subject, 1189 + "reason": sentinel, 1190 + }); 1191 + 1192 + // Mode 2: getServiceAuth direct-POST. 1193 + let exp_abs = now + 60; 1194 + match fetch_service_auth_jwt( 1195 + pds_client, 1196 + &creds.handle, 1197 + &creds.app_password, 1198 + &id_facts.did.0, 1199 + "com.atproto.moderation.createReport", 1200 + exp_abs, 1201 + ) 1202 + .await 1203 + { 1204 + Err(PdsJwtFetchError::Pds { message }) => { 1205 + results.push(Check::PdsServiceAuthAccepted.network_error(message)); 1206 + } 1207 + Ok(service_jwt) => { 1208 + match report_tee 1209 + .post_create_report(Some(&service_jwt), &pds_body) 1210 + .await 1211 + { 1212 + Ok(resp) if resp.status.is_success() => { 1213 + results.push(Check::PdsServiceAuthAccepted.pass()); 1214 + } 1215 + Ok(resp) => { 1216 + let diag = Box::new(PdsServiceAuthRejected { 1217 + status: resp.status.as_u16(), 1218 + source_code: body_as_named_source(&resp), 1219 + span: None, 1220 + }); 1221 + results 1222 + .push(Check::PdsServiceAuthAccepted.spec_violation(Some(diag))); 1223 + } 1224 + Err(CreateReportStageError::Transport { message, .. }) => { 1225 + // Labeler-side transport failure during direct POST. 1226 + results.push(Check::PdsServiceAuthAccepted.network_error(message)); 1227 + } 1228 + } 1229 + } 1230 + } 1231 + 1232 + // Mode 3: PDS-proxied. 1233 + let proxier = PdsProxiedPoster::new(pds_client); 1234 + match proxier.post(&id_facts.did.0, &access_jwt, &pds_body).await { 1235 + Err(CreateReportStageError::Transport { message, .. }) => { 1236 + // Transport to the PDS itself; classify PDS-side. 1237 + results.push(Check::PdsProxiedAccepted.network_error(message)); 1238 + } 1239 + Ok(resp) if resp.status.is_success() => { 1240 + results.push(Check::PdsProxiedAccepted.pass()); 1241 + } 1242 + Ok(resp) => { 1243 + // PDS surfaced a non-2xx. Interpret per envelope to 1244 + // distinguish PDS-side vs labeler-side: 1245 + let envelope = XrpcErrorEnvelope::parse(&resp.raw_body); 1246 + let err_name = envelope.as_ref().and_then(|e| e.error.clone()); 1247 + let is_upstream_label_error = matches!( 1248 + err_name.as_deref(), 1249 + Some("UpstreamError") | Some("UpstreamFailure") 1250 + ) || resp.status.as_u16() == 502 1251 + || resp.status.as_u16() == 504; 1252 + if is_upstream_label_error { 1253 + // AC6.2: labeler-side rejection surfaced by PDS. 1254 + let diag = Box::new(PdsProxiedRejected { 1255 + status: resp.status.as_u16(), 1256 + source_code: body_as_named_source_from_pds(&resp), 1257 + span: None, 1258 + }); 1259 + results.push(Check::PdsProxiedAccepted.spec_violation(Some(diag))); 1260 + } else { 1261 + // AC6.3: PDS-side rejection of the proxy attempt. 1262 + results.push(Check::PdsProxiedAccepted.network_error(format!( 1263 + "PDS rejected proxy attempt with status {}", 1264 + resp.status 1265 + ))); 1266 + } 1267 + } 1268 + } 1269 + } 1270 + } 1271 + } 1148 1272 1149 1273 CreateReportStageOutput { 1150 1274 facts: None, ··· 1152 1276 } 1153 1277 } 1154 1278 1279 + /// Convenience wrapper that does createSession and returns both the DID 1280 + /// and the accessJwt. Needed by both PDS check modes to populate the body 1281 + /// with the correct subject DID. 1282 + struct SessionResult { 1283 + did: String, 1284 + access_jwt: String, 1285 + } 1286 + 1287 + async fn fetch_session_and_did( 1288 + client: &dyn PdsXrpcClient, 1289 + handle: &str, 1290 + app_password: &str, 1291 + ) -> Result<SessionResult, String> { 1292 + let body = serde_json::json!({ "identifier": handle, "password": app_password }); 1293 + let resp = client 1294 + .post("xrpc/com.atproto.server.createSession", None, None, &body) 1295 + .await 1296 + .map_err(|e| format!("createSession transport: {e}"))?; 1297 + if !resp.status.is_success() { 1298 + return Err(format!("createSession returned {}", resp.status)); 1299 + } 1300 + let session: serde_json::Value = 1301 + serde_json::from_slice(&resp.raw_body).map_err(|e| format!("createSession body: {e}"))?; 1302 + let did = session["did"] 1303 + .as_str() 1304 + .ok_or("createSession missing did")? 1305 + .to_string(); 1306 + let access_jwt = session["accessJwt"] 1307 + .as_str() 1308 + .ok_or("createSession missing accessJwt")? 1309 + .to_string(); 1310 + Ok(SessionResult { did, access_jwt }) 1311 + } 1312 + 1313 + /// Fetch a service-auth JWT from a PDS by calling getServiceAuth after 1314 + /// createSession. This JWT can be used directly to POST to the labeler. 1315 + async fn fetch_service_auth_jwt( 1316 + client: &dyn PdsXrpcClient, 1317 + handle: &str, 1318 + app_password: &str, 1319 + aud: &str, 1320 + lxm: &str, 1321 + exp_absolute_unix: i64, 1322 + ) -> Result<String, PdsJwtFetchError> { 1323 + // 1. createSession. 1324 + let body = serde_json::json!({ 1325 + "identifier": handle, 1326 + "password": app_password, 1327 + }); 1328 + let resp = client 1329 + .post("xrpc/com.atproto.server.createSession", None, None, &body) 1330 + .await 1331 + .map_err(|e| PdsJwtFetchError::Pds { 1332 + message: format!("createSession transport: {e}"), 1333 + })?; 1334 + if !resp.status.is_success() { 1335 + return Err(PdsJwtFetchError::Pds { 1336 + message: format!("createSession returned status {}", resp.status), 1337 + }); 1338 + } 1339 + let session: serde_json::Value = 1340 + serde_json::from_slice(&resp.raw_body).map_err(|e| PdsJwtFetchError::Pds { 1341 + message: format!("createSession body not JSON: {e}"), 1342 + })?; 1343 + let access_jwt = session["accessJwt"] 1344 + .as_str() 1345 + .ok_or_else(|| PdsJwtFetchError::Pds { 1346 + message: "createSession response missing accessJwt".to_string(), 1347 + })? 1348 + .to_string(); 1349 + 1350 + // 2. getServiceAuth (GET with query params). 1351 + let exp_s = exp_absolute_unix.to_string(); 1352 + let resp = client 1353 + .get( 1354 + "xrpc/com.atproto.server.getServiceAuth", 1355 + Some(&access_jwt), 1356 + &[("aud", aud), ("lxm", lxm), ("exp", &exp_s)], 1357 + ) 1358 + .await 1359 + .map_err(|e| PdsJwtFetchError::Pds { 1360 + message: format!("getServiceAuth transport: {e}"), 1361 + })?; 1362 + if !resp.status.is_success() { 1363 + return Err(PdsJwtFetchError::Pds { 1364 + message: format!("getServiceAuth returned status {}", resp.status), 1365 + }); 1366 + } 1367 + let auth: serde_json::Value = 1368 + serde_json::from_slice(&resp.raw_body).map_err(|e| PdsJwtFetchError::Pds { 1369 + message: format!("getServiceAuth body not JSON: {e}"), 1370 + })?; 1371 + let token = auth["token"] 1372 + .as_str() 1373 + .ok_or_else(|| PdsJwtFetchError::Pds { 1374 + message: "getServiceAuth response missing token".to_string(), 1375 + })? 1376 + .to_string(); 1377 + 1378 + Ok(token) 1379 + } 1380 + 1155 1381 /// Synthesize a `reasonType` string that is definitely NOT in the 1156 1382 /// labeler's advertised `reason_types`. NSID syntax (segments alphanumeric + 1157 1383 /// period only, fragment after `#`) is strictly valid so the labeler does ··· 1403 1629 pub span: Option<SourceSpan>, 1404 1630 } 1405 1631 1632 + /// Diagnostic for AC5.2: labeler rejected PDS-minted service-auth JWT. 1633 + #[derive(Debug, Error, Diagnostic)] 1634 + #[error("Labeler rejected PDS-minted service-auth JWT (status {status})")] 1635 + #[diagnostic( 1636 + code = "labeler::report::pds_service_auth_rejected", 1637 + help = "The PDS issued a service-auth JWT for this user bound to the labeler's DID and the createReport NSID; the labeler should have accepted it." 1638 + )] 1639 + pub struct PdsServiceAuthRejected { 1640 + /// Observed HTTP status code. 1641 + pub status: u16, 1642 + /// Response body for context. 1643 + #[source_code] 1644 + pub source_code: NamedSource<Arc<[u8]>>, 1645 + /// Pseudo-span over the whole body. 1646 + #[label("rejected here")] 1647 + pub span: Option<SourceSpan>, 1648 + } 1649 + 1650 + /// Diagnostic for AC6.2: labeler rejected PDS-proxied createReport. 1651 + #[derive(Debug, Error, Diagnostic)] 1652 + #[error("Labeler rejected PDS-proxied createReport (status {status})")] 1653 + #[diagnostic( 1654 + code = "labeler::report::pds_proxied_rejected", 1655 + help = "The PDS forwarded the createReport call on the user's behalf; the downstream labeler reached it but rejected the submission." 1656 + )] 1657 + pub struct PdsProxiedRejected { 1658 + /// Observed HTTP status code. 1659 + pub status: u16, 1660 + /// Response body for context. 1661 + #[source_code] 1662 + pub source_code: NamedSource<Arc<[u8]>>, 1663 + /// Pseudo-span over the whole body. 1664 + #[label("rejected here")] 1665 + pub span: Option<SourceSpan>, 1666 + } 1667 + 1406 1668 /// Construct a `NamedSource` from the pretty-printed response body. 1407 1669 /// Used for every `accepted_*` diagnostic here. 1408 1670 pub(crate) fn body_as_named_source(resp: &RawCreateReportResponse) -> NamedSource<Arc<[u8]>> { 1671 + let pretty = pretty_json_for_display(&resp.raw_body); 1672 + NamedSource::new(resp.source_url.clone(), pretty) 1673 + } 1674 + 1675 + /// Construct a `NamedSource` from the pretty-printed response body from the PDS. 1676 + /// Used for PDS-mediated mode diagnostics where the response comes from the PDS not the labeler. 1677 + pub(crate) fn body_as_named_source_from_pds(resp: &RawPdsXrpcResponse) -> NamedSource<Arc<[u8]>> { 1409 1678 let pretty = pretty_json_for_display(&resp.raw_body); 1410 1679 NamedSource::new(resp.source_url.clone(), pretty) 1411 1680 }
+35 -1
src/commands/test/labeler/pipeline.rs
··· 8 8 use url::Url; 9 9 10 10 use crate::commands::test::labeler::create_report::self_mint::{SelfMintCurve, SelfMintSigner}; 11 - use crate::commands::test::labeler::create_report::{self, CreateReportTee, RealCreateReportTee}; 11 + use crate::commands::test::labeler::create_report::{ 12 + self, CreateReportTee, PdsXrpcClient, RealCreateReportTee, 13 + }; 12 14 use crate::commands::test::labeler::crypto; 13 15 use crate::commands::test::labeler::http::{self, RealHttpTee}; 14 16 use crate::commands::test::labeler::identity; ··· 80 82 pub self_mint_signer: Option<&'a SelfMintSigner>, 81 83 /// PDS credentials for modes 2 and 3. Populated in Phase 8. 82 84 pub pds_credentials: Option<&'a PdsCredentials>, 85 + /// PDS XRPC client for modes 2 and 3. Populated by the pipeline when 86 + /// credentials are supplied; tests inject overrides via `pds_xrpc_client_override`. 87 + pub pds_xrpc_client: Option<&'a dyn PdsXrpcClient>, 88 + /// Test-only override for PDS XRPC client injection. When set, takes 89 + /// precedence over the pipeline-constructed client. 90 + pub pds_xrpc_client_override: Option<&'a dyn PdsXrpcClient>, 83 91 /// Stable run-id for the sentinel reason string. Created in `LabelerCmd::run`. 84 92 pub run_id: &'a str, 85 93 } ··· 421 429 }); 422 430 } 423 431 432 + // Construct the PDS XRPC client when credentials are supplied. The test 433 + // override takes precedence if provided; else construct the real client 434 + // from the PDS endpoint discovered by identity. 435 + let pds_xrpc_client_owned: Option<create_report::RealPdsXrpcClient> = 436 + if opts.pds_xrpc_client_override.is_some() { 437 + // Test override supplied; don't construct real client. 438 + None 439 + } else if opts.pds_credentials.is_some() { 440 + // Check that identity produced a PDS endpoint and that we have a real HTTP client. 441 + match (&identity_output.facts, &opts.create_report_tee) { 442 + (Some(facts), CreateReportTeeKind::Real(http_client)) => { 443 + Some(create_report::RealPdsXrpcClient::new( 444 + (*http_client).clone(), 445 + facts.pds_endpoint.clone(), 446 + )) 447 + } 448 + _ => None, 449 + } 450 + } else { 451 + None 452 + }; 453 + let pds_xrpc_client_ref: Option<&dyn PdsXrpcClient> = pds_xrpc_client_owned 454 + .as_ref() 455 + .map(|c| c as &dyn PdsXrpcClient); 456 + 424 457 // Run the report stage. Uses `identity_output.facts.as_ref()` so the 425 458 // stage can skip-with-reason when identity didn't produce facts. 426 459 let create_report_run_opts = create_report::CreateReportRunOptions { ··· 430 463 report_subject_override: opts.report_subject_override, 431 464 self_mint_signer: opts.self_mint_signer, 432 465 pds_credentials: opts.pds_credentials, 466 + pds_xrpc_client: opts.pds_xrpc_client_override.or(pds_xrpc_client_ref), 433 467 run_id: opts.run_id, 434 468 }; 435 469 let labeler_endpoint_for_report = labeler_endpoint.clone();
+1 -134
tests/labeler_endtoend.rs
··· 3 3 mod common; 4 4 5 5 use async_trait::async_trait; 6 - use atproto_devtool::commands::test::labeler::create_report::self_mint::SelfMintCurve; 7 6 use atproto_devtool::commands::test::labeler::crypto::canonicalize_label_for_signing; 8 7 use atproto_devtool::commands::test::labeler::pipeline::{ 9 - CreateReportTeeKind, HttpTee, LabelerOptions, parse_target, run_pipeline, 8 + HttpTee, LabelerOptions, parse_target, run_pipeline, 10 9 }; 11 10 use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError}; 12 11 use atrium_api::com::atproto::label::defs::{Label, LabelData}; ··· 289 288 fake_tee.add_response(None, 200, labels_response); 290 289 let fake_ws = common::FakeWebSocketClient::empty(); 291 290 292 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 293 - 294 291 let opts = LabelerOptions { 295 292 http: &http, 296 293 dns: &dns, ··· 298 295 ws_client: Some(&fake_ws), 299 296 subscribe_timeout: Duration::from_secs(5), 300 297 verbose: false, 301 - 302 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 303 - commit_report: false, 304 - force_self_mint: false, 305 - self_mint_curve: SelfMintCurve::Es256k, 306 - report_subject_override: None, 307 - self_mint_signer: None, 308 - pds_credentials: None, 309 - run_id: "test-run-id", 310 298 }; 311 299 312 300 let report = run_pipeline(target, opts).await; ··· 353 341 let fake_tee = common::FakeRawHttpTee::new(); 354 342 let fake_ws = common::FakeWebSocketClient::empty(); 355 343 356 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 357 - 358 344 let opts = LabelerOptions { 359 345 http: &http, 360 346 dns: &dns, ··· 362 348 ws_client: Some(&fake_ws), 363 349 subscribe_timeout: Duration::from_secs(5), 364 350 verbose: false, 365 - 366 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 367 - commit_report: false, 368 - force_self_mint: false, 369 - self_mint_curve: SelfMintCurve::Es256k, 370 - report_subject_override: None, 371 - self_mint_signer: None, 372 - pds_credentials: None, 373 - run_id: "test-run-id", 374 351 }; 375 352 376 353 let report = run_pipeline(target, opts).await; ··· 409 386 fake_tee.add_response(None, 200, b"{malformed json".to_vec()); 410 387 let fake_ws = common::FakeWebSocketClient::empty(); 411 388 412 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 413 - 414 389 let opts = LabelerOptions { 415 390 http: &http, 416 391 dns: &dns, ··· 418 393 ws_client: Some(&fake_ws), 419 394 subscribe_timeout: Duration::from_secs(5), 420 395 verbose: false, 421 - 422 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 423 - commit_report: false, 424 - force_self_mint: false, 425 - self_mint_curve: SelfMintCurve::Es256k, 426 - report_subject_override: None, 427 - self_mint_signer: None, 428 - pds_credentials: None, 429 - run_id: "test-run-id", 430 396 }; 431 397 432 398 let report = run_pipeline(target, opts).await; ··· 471 437 mid_stream_error: false, 472 438 }); 473 439 474 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 475 - 476 440 let opts = LabelerOptions { 477 441 http: &http, 478 442 dns: &dns, ··· 480 444 ws_client: Some(&fake_ws), 481 445 subscribe_timeout: Duration::from_secs(5), 482 446 verbose: false, 483 - 484 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 485 - commit_report: false, 486 - force_self_mint: false, 487 - self_mint_curve: SelfMintCurve::Es256k, 488 - report_subject_override: None, 489 - self_mint_signer: None, 490 - pds_credentials: None, 491 - run_id: "test-run-id", 492 447 }; 493 448 494 449 let report = run_pipeline(target, opts).await; ··· 566 521 fake_tee.add_response(None, 200, labels_response); 567 522 let fake_ws = common::FakeWebSocketClient::empty(); 568 523 569 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 570 - 571 524 let opts = LabelerOptions { 572 525 http: &http, 573 526 dns: &dns, ··· 575 528 ws_client: Some(&fake_ws), 576 529 subscribe_timeout: Duration::from_secs(5), 577 530 verbose: false, 578 - 579 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 580 - commit_report: false, 581 - force_self_mint: false, 582 - self_mint_curve: SelfMintCurve::Es256k, 583 - report_subject_override: None, 584 - self_mint_signer: None, 585 - pds_credentials: None, 586 - run_id: "test-run-id", 587 531 }; 588 532 589 533 let report = run_pipeline(target, opts).await; ··· 659 603 fake_tee.add_response(None, 200, labels_response); 660 604 let fake_ws = common::FakeWebSocketClient::empty(); 661 605 662 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 663 - 664 606 let opts = LabelerOptions { 665 607 http: &http, 666 608 dns: &dns, ··· 668 610 ws_client: Some(&fake_ws), 669 611 subscribe_timeout: Duration::from_secs(5), 670 612 verbose: false, 671 - 672 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 673 - commit_report: false, 674 - force_self_mint: false, 675 - self_mint_curve: SelfMintCurve::Es256k, 676 - report_subject_override: None, 677 - self_mint_signer: None, 678 - pds_credentials: None, 679 - run_id: "test-run-id", 680 613 }; 681 614 682 615 let report = run_pipeline(target, opts).await; ··· 749 682 fake_tee.add_response(None, 200, labels_response); 750 683 let fake_ws = common::FakeWebSocketClient::empty(); 751 684 752 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 753 - 754 685 let opts = LabelerOptions { 755 686 http: &http, 756 687 dns: &dns, ··· 758 689 ws_client: Some(&fake_ws), 759 690 subscribe_timeout: Duration::from_secs(5), 760 691 verbose: false, 761 - 762 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 763 - commit_report: false, 764 - force_self_mint: false, 765 - self_mint_curve: SelfMintCurve::Es256k, 766 - report_subject_override: None, 767 - self_mint_signer: None, 768 - pds_credentials: None, 769 - run_id: "test-run-id", 770 692 }; 771 693 772 694 let report = run_pipeline(target, opts).await; ··· 804 726 fake_tee.add_response(None, 200, labels_response); 805 727 let fake_ws = common::FakeWebSocketClient::empty(); 806 728 807 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 808 - 809 729 let opts = LabelerOptions { 810 730 http: &http, 811 731 dns: &dns, ··· 813 733 ws_client: Some(&fake_ws), 814 734 subscribe_timeout: Duration::from_secs(5), 815 735 verbose: false, 816 - 817 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 818 - commit_report: false, 819 - force_self_mint: false, 820 - self_mint_curve: SelfMintCurve::Es256k, 821 - report_subject_override: None, 822 - self_mint_signer: None, 823 - pds_credentials: None, 824 - run_id: "test-run-id", 825 736 }; 826 737 827 738 let report = run_pipeline(target, opts).await; ··· 884 795 fake_tee.add_response(None, 200, labels_response); 885 796 let fake_ws = common::FakeWebSocketClient::empty(); 886 797 887 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 888 - 889 798 let opts = LabelerOptions { 890 799 http: &http, 891 800 dns: &dns, ··· 893 802 ws_client: Some(&fake_ws), 894 803 subscribe_timeout: Duration::from_secs(5), 895 804 verbose: false, 896 - 897 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 898 - commit_report: false, 899 - force_self_mint: false, 900 - self_mint_curve: SelfMintCurve::Es256k, 901 - report_subject_override: None, 902 - self_mint_signer: None, 903 - pds_credentials: None, 904 - run_id: "test-run-id", 905 805 }; 906 806 907 807 let report = run_pipeline(target, opts).await; ··· 940 840 fake_tee.add_response(None, 200, br#"{"cursor": null, "labels": []}"#.to_vec()); 941 841 let fake_ws = common::FakeWebSocketClient::empty(); 942 842 943 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 944 - 945 843 let opts = LabelerOptions { 946 844 http: &http, 947 845 dns: &dns, ··· 949 847 ws_client: Some(&fake_ws), 950 848 subscribe_timeout: Duration::from_secs(5), 951 849 verbose: false, 952 - 953 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 954 - commit_report: false, 955 - force_self_mint: false, 956 - self_mint_curve: SelfMintCurve::Es256k, 957 - report_subject_override: None, 958 - self_mint_signer: None, 959 - pds_credentials: None, 960 - run_id: "test-run-id", 961 850 }; 962 851 963 852 let report = run_pipeline(target, opts).await; ··· 1000 889 fake_tee.set_transport_error(); // Force HTTP stage transport error. 1001 890 let fake_ws = common::FakeWebSocketClient::empty(); 1002 891 1003 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 1004 - 1005 892 let opts = LabelerOptions { 1006 893 http: &http, 1007 894 dns: &dns, ··· 1009 896 ws_client: Some(&fake_ws), 1010 897 subscribe_timeout: Duration::from_secs(5), 1011 898 verbose: false, 1012 - 1013 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 1014 - commit_report: false, 1015 - force_self_mint: false, 1016 - self_mint_curve: SelfMintCurve::Es256k, 1017 - report_subject_override: None, 1018 - self_mint_signer: None, 1019 - pds_credentials: None, 1020 - run_id: "test-run-id", 1021 899 }; 1022 900 1023 901 let report = run_pipeline(target, opts).await; ··· 1045 923 fake_tee.add_response(None, 200, br#"{"cursor": null, "labels": []}"#.to_vec()); 1046 924 let fake_ws = common::FakeWebSocketClient::empty(); 1047 925 1048 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 1049 - 1050 926 let opts = LabelerOptions { 1051 927 http: &http, 1052 928 dns: &dns, ··· 1054 930 ws_client: Some(&fake_ws), 1055 931 subscribe_timeout: Duration::from_secs(5), 1056 932 verbose: false, 1057 - 1058 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 1059 - commit_report: false, 1060 - force_self_mint: false, 1061 - self_mint_curve: SelfMintCurve::Es256k, 1062 - report_subject_override: None, 1063 - self_mint_signer: None, 1064 - pds_credentials: None, 1065 - run_id: "test-run-id", 1066 933 }; 1067 934 1068 935 let report = run_pipeline(target, opts).await;
+1 -134
tests/labeler_identity.rs
··· 3 3 mod common; 4 4 5 5 use async_trait::async_trait; 6 - use atproto_devtool::commands::test::labeler::create_report::self_mint::SelfMintCurve; 7 6 use atproto_devtool::commands::test::labeler::pipeline::{ 8 - CreateReportTeeKind, HttpTee, LabelerOptions, parse_target, run_pipeline, 7 + HttpTee, LabelerOptions, parse_target, run_pipeline, 9 8 }; 10 9 use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError}; 11 10 use std::collections::HashMap; ··· 148 147 fake_tee.add_response(None, 200, healthy_labels_response()); 149 148 let fake_ws = common::FakeWebSocketClient::empty(); 150 149 151 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 152 - 153 150 let opts = LabelerOptions { 154 151 http: &http, 155 152 dns: &dns, ··· 157 154 ws_client: Some(&fake_ws), 158 155 subscribe_timeout: std::time::Duration::from_secs(5), 159 156 verbose: false, 160 - 161 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 162 - commit_report: false, 163 - force_self_mint: false, 164 - self_mint_curve: SelfMintCurve::Es256k, 165 - report_subject_override: None, 166 - self_mint_signer: None, 167 - pds_credentials: None, 168 - run_id: "test-run-id", 169 157 }; 170 158 171 159 let report = run_pipeline(target, opts).await; ··· 186 174 187 175 let fake_ws = common::FakeWebSocketClient::empty(); 188 176 189 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 190 - 191 177 let opts = LabelerOptions { 192 178 http: &http, 193 179 dns: &dns, ··· 195 181 ws_client: Some(&fake_ws), 196 182 subscribe_timeout: std::time::Duration::from_secs(5), 197 183 verbose: false, 198 - 199 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 200 - commit_report: false, 201 - force_self_mint: false, 202 - self_mint_curve: SelfMintCurve::Es256k, 203 - report_subject_override: None, 204 - self_mint_signer: None, 205 - pds_credentials: None, 206 - run_id: "test-run-id", 207 184 }; 208 185 209 186 let report = run_pipeline(target, opts).await; ··· 247 224 248 225 let fake_ws = common::FakeWebSocketClient::empty(); 249 226 250 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 251 - 252 227 let opts = LabelerOptions { 253 228 http: &http, 254 229 dns: &dns, ··· 256 231 ws_client: Some(&fake_ws), 257 232 subscribe_timeout: std::time::Duration::from_secs(5), 258 233 verbose: false, 259 - 260 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 261 - commit_report: false, 262 - force_self_mint: false, 263 - self_mint_curve: SelfMintCurve::Es256k, 264 - report_subject_override: None, 265 - self_mint_signer: None, 266 - pds_credentials: None, 267 - run_id: "test-run-id", 268 234 }; 269 235 270 236 let report = run_pipeline(target, opts).await; ··· 300 266 fake_tee.add_response(None, 200, healthy_labels_response()); 301 267 let fake_ws = common::FakeWebSocketClient::empty(); 302 268 303 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 304 - 305 269 let opts = LabelerOptions { 306 270 http: &http, 307 271 dns: &dns, ··· 309 273 ws_client: Some(&fake_ws), 310 274 subscribe_timeout: std::time::Duration::from_secs(5), 311 275 verbose: false, 312 - 313 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 314 - commit_report: false, 315 - force_self_mint: false, 316 - self_mint_curve: SelfMintCurve::Es256k, 317 - report_subject_override: None, 318 - self_mint_signer: None, 319 - pds_credentials: None, 320 - run_id: "test-run-id", 321 276 }; 322 277 323 278 let report = run_pipeline(target, opts).await; ··· 355 310 356 311 let fake_ws = common::FakeWebSocketClient::empty(); 357 312 358 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 359 - 360 313 let opts = LabelerOptions { 361 314 http: &http, 362 315 dns: &dns, ··· 364 317 ws_client: Some(&fake_ws), 365 318 subscribe_timeout: std::time::Duration::from_secs(5), 366 319 verbose: false, 367 - 368 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 369 - commit_report: false, 370 - force_self_mint: false, 371 - self_mint_curve: SelfMintCurve::Es256k, 372 - report_subject_override: None, 373 - self_mint_signer: None, 374 - pds_credentials: None, 375 - run_id: "test-run-id", 376 320 }; 377 321 378 322 let report = run_pipeline(target, opts).await; ··· 394 338 fake_tee.add_response(None, 200, empty_response); 395 339 396 340 let fake_ws = common::FakeWebSocketClient::empty(); 397 - 398 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 399 341 400 342 let opts = LabelerOptions { 401 343 http: &http, ··· 404 346 ws_client: Some(&fake_ws), 405 347 subscribe_timeout: std::time::Duration::from_secs(5), 406 348 verbose: false, 407 - 408 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 409 - commit_report: false, 410 - force_self_mint: false, 411 - self_mint_curve: SelfMintCurve::Es256k, 412 - report_subject_override: None, 413 - self_mint_signer: None, 414 - pds_credentials: None, 415 - run_id: "test-run-id", 416 349 }; 417 350 418 351 let report = run_pipeline(target, opts).await; ··· 446 379 fake_tee.add_response(None, 200, healthy_labels_response()); 447 380 let fake_ws = common::FakeWebSocketClient::empty(); 448 381 449 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 450 - 451 382 let opts = LabelerOptions { 452 383 http: &http, 453 384 dns: &dns, ··· 455 386 ws_client: Some(&fake_ws), 456 387 subscribe_timeout: std::time::Duration::from_secs(5), 457 388 verbose: false, 458 - 459 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 460 - commit_report: false, 461 - force_self_mint: false, 462 - self_mint_curve: SelfMintCurve::Es256k, 463 - report_subject_override: None, 464 - self_mint_signer: None, 465 - pds_credentials: None, 466 - run_id: "test-run-id", 467 389 }; 468 390 469 391 let report = run_pipeline(target, opts).await; ··· 502 424 503 425 let fake_ws = common::FakeWebSocketClient::empty(); 504 426 505 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 506 - 507 427 let opts = LabelerOptions { 508 428 http: &http, 509 429 dns: &dns, ··· 511 431 ws_client: Some(&fake_ws), 512 432 subscribe_timeout: std::time::Duration::from_secs(5), 513 433 verbose: false, 514 - 515 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 516 - commit_report: false, 517 - force_self_mint: false, 518 - self_mint_curve: SelfMintCurve::Es256k, 519 - report_subject_override: None, 520 - self_mint_signer: None, 521 - pds_credentials: None, 522 - run_id: "test-run-id", 523 434 }; 524 435 525 436 let report = run_pipeline(target, opts).await; ··· 560 471 561 472 let fake_ws = common::FakeWebSocketClient::empty(); 562 473 563 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 564 - 565 474 let opts = LabelerOptions { 566 475 http: &http, 567 476 dns: &dns, ··· 569 478 ws_client: Some(&fake_ws), 570 479 subscribe_timeout: std::time::Duration::from_secs(5), 571 480 verbose: false, 572 - 573 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 574 - commit_report: false, 575 - force_self_mint: false, 576 - self_mint_curve: SelfMintCurve::Es256k, 577 - report_subject_override: None, 578 - self_mint_signer: None, 579 - pds_credentials: None, 580 - run_id: "test-run-id", 581 481 }; 582 482 583 483 let report = run_pipeline(target, opts).await; ··· 616 516 617 517 let fake_ws = common::FakeWebSocketClient::empty(); 618 518 619 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 620 - 621 519 let opts = LabelerOptions { 622 520 http: &http, 623 521 dns: &dns, ··· 625 523 ws_client: Some(&fake_ws), 626 524 subscribe_timeout: std::time::Duration::from_secs(5), 627 525 verbose: false, 628 - 629 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 630 - commit_report: false, 631 - force_self_mint: false, 632 - self_mint_curve: SelfMintCurve::Es256k, 633 - report_subject_override: None, 634 - self_mint_signer: None, 635 - pds_credentials: None, 636 - run_id: "test-run-id", 637 526 }; 638 527 639 528 let report = run_pipeline(target, opts).await; ··· 672 561 673 562 let fake_ws = common::FakeWebSocketClient::empty(); 674 563 675 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 676 - 677 564 let opts = LabelerOptions { 678 565 http: &http, 679 566 dns: &dns, ··· 681 568 ws_client: Some(&fake_ws), 682 569 subscribe_timeout: std::time::Duration::from_secs(5), 683 570 verbose: false, 684 - 685 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 686 - commit_report: false, 687 - force_self_mint: false, 688 - self_mint_curve: SelfMintCurve::Es256k, 689 - report_subject_override: None, 690 - self_mint_signer: None, 691 - pds_credentials: None, 692 - run_id: "test-run-id", 693 571 }; 694 572 695 573 let report = run_pipeline(target, opts).await; ··· 732 610 733 611 let fake_ws = common::FakeWebSocketClient::empty(); 734 612 735 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 736 - 737 613 let opts = LabelerOptions { 738 614 http: &http, 739 615 dns: &dns, ··· 741 617 ws_client: Some(&fake_ws), 742 618 subscribe_timeout: std::time::Duration::from_secs(5), 743 619 verbose: false, 744 - 745 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 746 - commit_report: false, 747 - force_self_mint: false, 748 - self_mint_curve: SelfMintCurve::Es256k, 749 - report_subject_override: None, 750 - self_mint_signer: None, 751 - pds_credentials: None, 752 - run_id: "test-run-id", 753 620 }; 754 621 755 622 let report = run_pipeline(target, opts).await;
-987
tests/labeler_report.rs
··· 1 - //! Integration tests for the `report` stage. 2 - //! 3 - //! Uses `FakeCreateReportTee` from `tests/common/mod.rs` to drive each 4 - //! stage-gating scenario, then snapshots the rendered output to pin the 5 - //! exact 10-row sequence. 6 1 7 - mod common; 8 - 9 - use async_trait::async_trait; 10 - use std::sync::Arc; 11 - use std::time::Duration; 12 - 13 - use atproto_devtool::commands::test::labeler::create_report; 14 - use atproto_devtool::commands::test::labeler::create_report::self_mint::{ 15 - SelfMintCurve, SelfMintSigner, 16 - }; 17 - use atproto_devtool::commands::test::labeler::identity::IdentityFacts; 18 - use atproto_devtool::commands::test::labeler::pipeline::{ 19 - CreateReportTeeKind, HttpTee, LabelerOptions, parse_target, run_pipeline, 20 - }; 21 - use atproto_devtool::commands::test::labeler::report::{ 22 - CheckResult, CheckStatus, LabelerReport, RenderConfig, ReportHeader, 23 - }; 24 - use atproto_devtool::common::identity::{Did, DnsResolver, HttpClient, IdentityError}; 25 - 26 - use atrium_api::app::bsky::labeler::defs::LabelerPolicies; 27 - 28 - use common::FakeCreateReportTee; 29 - 30 - /// Stub HTTP client for testing that always returns an identity error. 31 - struct StubHttpClient; 32 - 33 - #[async_trait] 34 - impl HttpClient for StubHttpClient { 35 - async fn get_bytes(&self, _url: &url::Url) -> Result<(u16, Vec<u8>), IdentityError> { 36 - Err(IdentityError::InvalidHandle) 37 - } 38 - } 39 - 40 - /// Stub DNS resolver for testing that always returns an identity error. 41 - struct StubDnsResolver; 42 - 43 - #[async_trait] 44 - impl DnsResolver for StubDnsResolver { 45 - async fn txt_lookup(&self, _name: &str) -> Result<Vec<String>, IdentityError> { 46 - Err(IdentityError::InvalidHandle) 47 - } 48 - } 49 - 50 - /// Build a synthetic `IdentityFacts` with the requested contract shape. 51 - /// 52 - /// Populates every required field with stable, known-valid defaults so the 53 - /// fixture is safe to reuse across all AC tests. The labeler endpoint is a 54 - /// public HTTPS URL by default (non-local per the viability heuristic); 55 - /// tests that need a local endpoint override `facts.labeler_endpoint` 56 - /// directly before passing in. 57 - fn make_identity_facts( 58 - reason_types: Option<Vec<String>>, 59 - subject_types: Option<Vec<String>>, 60 - ) -> IdentityFacts { 61 - use atproto_devtool::common::identity::{ 62 - AnyVerifyingKey, DidDocument, RawDidDocument, parse_multikey, 63 - }; 64 - 65 - // A stable secp256k1 multikey drawn from an existing test fixture. 66 - // The exact value is load-bearing only insofar as it must parse via 67 - // `parse_multikey` — any valid secp256k1 multikey works. 68 - let multikey = "zQ3shNcc9CfAhG1vLj3UEV3SA4VESNiJKJiFLgs6WfGo4qG7B"; 69 - let parsed = parse_multikey(multikey).expect("test multikey parses"); 70 - let verifying_key: AnyVerifyingKey = parsed.verifying_key; 71 - 72 - // Minimal DID document with the one verification method the stages 73 - // care about. Raw bytes must match the parsed form so `NamedSource` 74 - // diagnostics land correctly; the exact bytes aren't snapshotted in 75 - // Phase 4 tests, so a small JSON is fine. 76 - let did_string = "did:plc:aaa22222222222222222bbbbbb"; 77 - let doc_json = serde_json::json!({ 78 - "id": did_string, 79 - "verificationMethod": [ 80 - { 81 - "id": format!("{}#atproto_label", did_string), 82 - "type": "Multikey", 83 - "controller": did_string, 84 - "publicKeyMultibase": multikey, 85 - } 86 - ], 87 - "service": [ 88 - { 89 - "id": "#atproto_labeler", 90 - "type": "AtprotoLabeler", 91 - "serviceEndpoint": "https://labeler.example.com", 92 - }, 93 - { 94 - "id": "#atproto_pds", 95 - "type": "AtprotoPersonalDataServer", 96 - "serviceEndpoint": "https://pds.example.com", 97 - } 98 - ] 99 - }) 100 - .to_string(); 101 - let doc: DidDocument = serde_json::from_str(&doc_json).expect("test DID doc parses"); 102 - let raw_did_doc = RawDidDocument { 103 - parsed: doc, 104 - source_bytes: Arc::<[u8]>::from(doc_json.as_bytes()), 105 - source_name: "test DID document".to_string(), 106 - }; 107 - 108 - // Empty `LabelerPolicies` is always valid — the AC1 gate reads 109 - // `reason_types`/`subject_types` on `IdentityFacts` directly (Task 0), 110 - // not on `labeler_policies`. 111 - let labeler_policies: LabelerPolicies = serde_json::from_value(serde_json::json!({ 112 - "labelValues": [], 113 - })) 114 - .expect("LabelerPolicies deserializes"); 115 - 116 - IdentityFacts { 117 - did: Did(did_string.to_string()), 118 - raw_did_doc, 119 - labeler_endpoint: url::Url::parse("https://labeler.example.com").unwrap(), 120 - pds_endpoint: url::Url::parse("https://pds.example.com").unwrap(), 121 - signing_key_id: format!("{did_string}#atproto_label"), 122 - signing_key_multikey: multikey.to_string(), 123 - signing_key: verifying_key, 124 - labeler_record_bytes: Arc::<[u8]>::from(b"{}" as &[u8]), 125 - labeler_policies, 126 - reason_types, 127 - subject_types, 128 - subject_collections: None, 129 - } 130 - } 131 - 132 - /// Default options for report stage tests. Shared by AC1-AC8 tests to avoid duplication. 133 - fn default_opts() -> create_report::CreateReportRunOptions<'static> { 134 - create_report::CreateReportRunOptions { 135 - commit_report: false, 136 - force_self_mint: false, 137 - self_mint_curve: SelfMintCurve::Es256k, 138 - report_subject_override: None, 139 - self_mint_signer: None, 140 - pds_credentials: None, 141 - run_id: "test-run-id-0000", 142 - } 143 - } 144 - 145 - /// Run the report stage directly (not through run_pipeline) with the 146 - /// given fake tee and options. Returns the 10 CheckResults. 147 - async fn run_report_stage( 148 - facts: &IdentityFacts, 149 - tee: &FakeCreateReportTee, 150 - opts: create_report::CreateReportRunOptions<'_>, 151 - ) -> Vec<CheckResult> { 152 - let out = create_report::run(Some(facts), tee, &opts).await; 153 - out.results 154 - } 155 - 156 - #[tokio::test] 157 - async fn ac1_1_contract_present_emits_pass() { 158 - let facts = make_identity_facts( 159 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 160 - Some(vec!["account".to_string()]), 161 - ); 162 - let tee = FakeCreateReportTee::new(); 163 - // Queue fake responses for the two no-JWT checks that now actually POST. 164 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 165 - "AuthenticationRequired", 166 - "jwt required", 167 - )); 168 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 169 - "BadJwt", 170 - "invalid bearer", 171 - )); 172 - let results = run_report_stage(&facts, &tee, default_opts()).await; 173 - 174 - assert_eq!(results.len(), 10, "AC7.1 requires exactly 10 rows"); 175 - assert_eq!(results[0].id, "report::contract_published"); 176 - assert_eq!(results[0].status, CheckStatus::Pass); 177 - } 178 - 179 - #[tokio::test] 180 - async fn ac1_2_contract_missing_without_commit_skips_stage() { 181 - let facts = make_identity_facts(None, None); 182 - let tee = FakeCreateReportTee::new(); 183 - let results = run_report_stage(&facts, &tee, default_opts()).await; 184 - 185 - assert_eq!(results.len(), 10); 186 - for r in &results { 187 - assert_eq!(r.status, CheckStatus::Skipped, "{}", r.id); 188 - let reason = r.skipped_reason.as_deref().unwrap_or(""); 189 - assert_eq!(reason, "labeler does not advertise report acceptance"); 190 - } 191 - } 192 - 193 - #[tokio::test] 194 - async fn ac1_3_contract_missing_with_commit_is_spec_violation() { 195 - let facts = make_identity_facts(None, None); 196 - let tee = FakeCreateReportTee::new(); 197 - let opts = create_report::CreateReportRunOptions { 198 - commit_report: true, 199 - force_self_mint: false, 200 - self_mint_curve: SelfMintCurve::Es256k, 201 - report_subject_override: None, 202 - self_mint_signer: None, 203 - pds_credentials: None, 204 - run_id: "test-run-id-0000", 205 - }; 206 - let results = run_report_stage(&facts, &tee, opts).await; 207 - 208 - assert_eq!(results.len(), 10); 209 - assert_eq!(results[0].id, "report::contract_published"); 210 - assert_eq!(results[0].status, CheckStatus::SpecViolation); 211 - 212 - for r in &results[1..] { 213 - assert_eq!(r.status, CheckStatus::Skipped, "{}", r.id); 214 - let reason = r.skipped_reason.as_deref().unwrap_or(""); 215 - assert_eq!(reason, "blocked by `report::contract_published`"); 216 - } 217 - } 218 - 219 - #[tokio::test] 220 - async fn ac1_4_empty_arrays_equivalent_to_absent() { 221 - // Empty Vecs treated the same as None per AC1.4. 222 - let facts = make_identity_facts(Some(vec![]), Some(vec![])); 223 - let tee = FakeCreateReportTee::new(); 224 - let results = run_report_stage(&facts, &tee, default_opts()).await; 225 - assert_eq!(results[0].status, CheckStatus::Skipped); 226 - assert_eq!( 227 - results[0].skipped_reason.as_deref(), 228 - Some("labeler does not advertise report acceptance"), 229 - ); 230 - } 231 - 232 - #[tokio::test] 233 - async fn ac7_2_row_order_is_stable() { 234 - let facts = make_identity_facts( 235 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 236 - Some(vec!["account".to_string()]), 237 - ); 238 - let tee = FakeCreateReportTee::new(); 239 - // Queue fake responses for the two no-JWT checks that now actually POST. 240 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 241 - "AuthenticationRequired", 242 - "jwt required", 243 - )); 244 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 245 - "BadJwt", 246 - "invalid bearer", 247 - )); 248 - let results = run_report_stage(&facts, &tee, default_opts()).await; 249 - let ids: Vec<&str> = results.iter().map(|r| r.id).collect(); 250 - assert_eq!( 251 - ids, 252 - vec![ 253 - "report::contract_published", 254 - "report::unauthenticated_rejected", 255 - "report::malformed_bearer_rejected", 256 - "report::wrong_aud_rejected", 257 - "report::wrong_lxm_rejected", 258 - "report::expired_rejected", 259 - "report::rejected_shape_returns_400", 260 - "report::self_mint_accepted", 261 - "report::pds_service_auth_accepted", 262 - "report::pds_proxied_accepted", 263 - ], 264 - ); 265 - } 266 - 267 - async fn render_results_to_string(results: Vec<CheckResult>) -> String { 268 - // Mirror the pipeline's header population (src/commands/test/labeler/ 269 - // pipeline.rs:212-216) so snapshot output matches what a real CLI run 270 - // would produce. The DID / PDS / labeler values match the 271 - // `make_identity_facts` defaults. 272 - let mut report = LabelerReport::new(ReportHeader { 273 - target: "test-labeler".to_string(), 274 - resolved_did: Some("did:plc:aaa22222222222222222bbbbbb".to_string()), 275 - pds_endpoint: Some("https://pds.example.com/".to_string()), 276 - labeler_endpoint: Some("https://labeler.example.com/".to_string()), 277 - }); 278 - for r in results { 279 - report.record(r); 280 - } 281 - report.finish(); 282 - let mut buf = Vec::new(); 283 - report 284 - .render(&mut buf, &RenderConfig { no_color: true }) 285 - .expect("render"); 286 - let rendered = String::from_utf8(buf).expect("utf-8"); 287 - common::normalize_timing(rendered) 288 - } 289 - 290 - #[tokio::test] 291 - async fn snapshot_contract_present_no_commit() { 292 - let facts = make_identity_facts( 293 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 294 - Some(vec!["account".to_string()]), 295 - ); 296 - let tee = FakeCreateReportTee::new(); 297 - // Queue fake responses for the two no-JWT checks that now actually POST. 298 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 299 - "AuthenticationRequired", 300 - "jwt required", 301 - )); 302 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 303 - "BadJwt", 304 - "invalid bearer", 305 - )); 306 - let results = run_report_stage(&facts, &tee, default_opts()).await; 307 - insta::assert_snapshot!( 308 - "report_contract_present_no_commit", 309 - render_results_to_string(results).await 310 - ); 311 - } 312 - 313 - #[tokio::test] 314 - async fn snapshot_contract_present_with_commit() { 315 - let facts = make_identity_facts( 316 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 317 - Some(vec!["account".to_string()]), 318 - ); 319 - let tee = FakeCreateReportTee::new(); 320 - // Queue fake responses for the two no-JWT checks that now actually POST. 321 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 322 - "AuthenticationRequired", 323 - "jwt required", 324 - )); 325 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 326 - "BadJwt", 327 - "invalid bearer", 328 - )); 329 - let mut opts = default_opts(); 330 - opts.commit_report = true; 331 - let results = run_report_stage(&facts, &tee, opts).await; 332 - insta::assert_snapshot!( 333 - "report_contract_present_with_commit", 334 - render_results_to_string(results).await 335 - ); 336 - } 337 - 338 - #[tokio::test] 339 - async fn snapshot_contract_missing_no_commit() { 340 - let facts = make_identity_facts(None, None); 341 - let tee = FakeCreateReportTee::new(); 342 - let results = run_report_stage(&facts, &tee, default_opts()).await; 343 - insta::assert_snapshot!( 344 - "report_contract_missing_no_commit", 345 - render_results_to_string(results).await 346 - ); 347 - } 348 - 349 - #[tokio::test] 350 - async fn snapshot_contract_missing_with_commit() { 351 - let facts = make_identity_facts(None, None); 352 - let tee = FakeCreateReportTee::new(); 353 - let mut opts = default_opts(); 354 - opts.commit_report = true; 355 - let results = run_report_stage(&facts, &tee, opts).await; 356 - insta::assert_snapshot!( 357 - "report_contract_missing_with_commit", 358 - render_results_to_string(results).await 359 - ); 360 - } 361 - 362 - #[tokio::test] 363 - async fn ac2_1_unauthenticated_401_with_envelope_passes() { 364 - let facts = make_identity_facts( 365 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 366 - Some(vec!["account".to_string()]), 367 - ); 368 - let tee = FakeCreateReportTee::new(); 369 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 370 - "AuthenticationRequired", 371 - "jwt required", 372 - )); 373 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 374 - "BadJwt", 375 - "invalid bearer", 376 - )); 377 - let results = run_report_stage(&facts, &tee, default_opts()).await; 378 - 379 - assert_eq!(results[1].id, "report::unauthenticated_rejected"); 380 - assert_eq!(results[1].status, CheckStatus::Pass); 381 - assert_eq!(results[2].id, "report::malformed_bearer_rejected"); 382 - assert_eq!(results[2].status, CheckStatus::Pass); 383 - } 384 - 385 - #[tokio::test] 386 - async fn ac2_2_unauthenticated_200_is_spec_violation() { 387 - let facts = make_identity_facts( 388 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 389 - Some(vec!["account".to_string()]), 390 - ); 391 - let tee = FakeCreateReportTee::new(); 392 - tee.enqueue(common::FakeCreateReportResponse::ok_empty()); 393 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 394 - "BadJwt", "x", 395 - )); 396 - let results = run_report_stage(&facts, &tee, default_opts()).await; 397 - 398 - assert_eq!(results[1].status, CheckStatus::SpecViolation); 399 - let diag = results[1].diagnostic.as_ref().expect("diagnostic present"); 400 - assert_eq!( 401 - diag.code().map(|c| c.to_string()), 402 - Some("labeler::report::unauthenticated_accepted".to_string()), 403 - ); 404 - } 405 - 406 - #[tokio::test] 407 - async fn ac2_4_malformed_bearer_200_is_spec_violation() { 408 - let facts = make_identity_facts( 409 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 410 - Some(vec!["account".to_string()]), 411 - ); 412 - let tee = FakeCreateReportTee::new(); 413 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 414 - tee.enqueue(common::FakeCreateReportResponse::ok_empty()); 415 - let results = run_report_stage(&facts, &tee, default_opts()).await; 416 - 417 - assert_eq!(results[2].status, CheckStatus::SpecViolation); 418 - let diag = results[2].diagnostic.as_ref().expect("diagnostic present"); 419 - assert_eq!( 420 - diag.code().map(|c| c.to_string()), 421 - Some("labeler::report::malformed_bearer_accepted".to_string()), 422 - ); 423 - } 424 - 425 - #[tokio::test] 426 - async fn ac2_5_401_without_envelope_still_passes() { 427 - let facts = make_identity_facts( 428 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 429 - Some(vec!["account".to_string()]), 430 - ); 431 - let tee = FakeCreateReportTee::new(); 432 - // 401 with empty body — non-conformant envelope, but status still Pass per AC2.5. 433 - tee.enqueue(common::FakeCreateReportResponse::Response { 434 - status: 401, 435 - content_type: Some("application/json".to_string()), 436 - body: b"{}".to_vec(), 437 - }); 438 - tee.enqueue(common::FakeCreateReportResponse::Response { 439 - status: 401, 440 - content_type: None, 441 - body: b"<html>".to_vec(), 442 - }); 443 - let results = run_report_stage(&facts, &tee, default_opts()).await; 444 - 445 - assert_eq!(results[1].status, CheckStatus::Pass); 446 - assert!(results[1].summary.contains("non-conformant envelope")); 447 - assert_eq!(results[2].status, CheckStatus::Pass); 448 - assert!(results[2].summary.contains("non-conformant envelope")); 449 - } 450 - 451 - #[tokio::test] 452 - async fn pipeline_integration_happy_path_via_endpoint() { 453 - // Test that run_pipeline correctly orchestrates stages via endpoint target 454 - // (no DID resolution required). This exercises the full pipeline integration 455 - // rather than testing the report stage in isolation through create_report::run. 456 - 457 - // Set up minimal fakes for each I/O seam. 458 - let fake_http_tee = common::FakeRawHttpTee::new(); 459 - fake_http_tee.add_response(None, 200, br#"{"cursor":null,"labels":[]}"#.to_vec()); 460 - 461 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 462 - 463 - // Use endpoint target (no identity stage needed). 464 - let target = parse_target("https://labeler.example.com", None).expect("parse endpoint target"); 465 - 466 - let opts = LabelerOptions { 467 - http: &StubHttpClient, 468 - dns: &StubDnsResolver, 469 - http_tee: HttpTee::Test(&fake_http_tee), 470 - ws_client: None, 471 - subscribe_timeout: Duration::from_secs(5), 472 - verbose: false, 473 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 474 - commit_report: false, 475 - force_self_mint: false, 476 - self_mint_curve: SelfMintCurve::Es256k, 477 - report_subject_override: None, 478 - self_mint_signer: None, 479 - pds_credentials: None, 480 - run_id: "test-run-id", 481 - }; 482 - 483 - let report = run_pipeline(target, opts).await; 484 - 485 - // Verify the report was generated and contains exactly 10 report:: rows in canonical order. 486 - let report_rows: Vec<_> = report 487 - .results 488 - .iter() 489 - .filter(|r| r.id.starts_with("report::")) 490 - .collect(); 491 - 492 - assert_eq!( 493 - report_rows.len(), 494 - 10, 495 - "AC7.1 requires exactly 10 report stage rows" 496 - ); 497 - 498 - let expected_ids = vec![ 499 - "report::contract_published", 500 - "report::unauthenticated_rejected", 501 - "report::malformed_bearer_rejected", 502 - "report::wrong_aud_rejected", 503 - "report::wrong_lxm_rejected", 504 - "report::expired_rejected", 505 - "report::rejected_shape_returns_400", 506 - "report::self_mint_accepted", 507 - "report::pds_service_auth_accepted", 508 - "report::pds_proxied_accepted", 509 - ]; 510 - 511 - let actual_ids: Vec<_> = report_rows.iter().map(|r| r.id).collect(); 512 - assert_eq!( 513 - actual_ids, expected_ids, 514 - "report stage rows must be in canonical order" 515 - ); 516 - } 517 - 518 - /// Helper: an IdentityFacts fixture whose labeler_endpoint is a 519 - /// localhost URL (self_mint_viable = true). 520 - fn local_identity_facts() -> IdentityFacts { 521 - let mut facts = make_identity_facts( 522 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 523 - Some(vec!["account".to_string()]), 524 - ); 525 - // Override endpoint to localhost. Exact field name per 526 - // the IdentityFacts struct. 527 - facts.labeler_endpoint = url::Url::parse("http://localhost:8080").unwrap(); 528 - facts 529 - } 530 - 531 - #[tokio::test] 532 - async fn ac3_1_wrong_aud_401_passes() { 533 - let facts = local_identity_facts(); 534 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 535 - let tee = FakeCreateReportTee::new(); 536 - // Six POSTs expected: unauthenticated, malformed, wrong_aud, 537 - // wrong_lxm, expired, rejected_shape. Enqueue each: 538 - for _ in 0..2 { 539 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); // phase 5 checks 540 - } 541 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 542 - "BadJwtAudience", 543 - "aud mismatch", 544 - )); 545 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 546 - "BadJwtLexiconMethod", 547 - "lxm mismatch", 548 - )); 549 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 550 - "JwtExpired", 551 - "expired", 552 - )); 553 - tee.enqueue(common::FakeCreateReportResponse::bad_request( 554 - "InvalidRequest", 555 - "unadvertised reasonType", 556 - )); 557 - 558 - let mut opts = default_opts(); 559 - opts.self_mint_signer = Some(&signer); 560 - let results = run_report_stage(&facts, &tee, opts).await; 561 - 562 - // Rows 3-5 are AC3.1-AC3.4, row 6 is AC3.5. 563 - assert_eq!(results[3].id, "report::wrong_aud_rejected"); 564 - assert_eq!(results[3].status, CheckStatus::Pass); 565 - assert_eq!(results[4].status, CheckStatus::Pass); 566 - assert_eq!(results[5].status, CheckStatus::Pass); 567 - assert_eq!(results[6].id, "report::rejected_shape_returns_400"); 568 - assert_eq!(results[6].status, CheckStatus::Pass); 569 - } 570 - 571 - #[tokio::test] 572 - async fn ac3_2_wrong_aud_200_is_spec_violation() { 573 - let facts = local_identity_facts(); 574 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 575 - let tee = FakeCreateReportTee::new(); 576 - // Two Phase 5 checks, then wrong_aud with 200 OK. 577 - for _ in 0..2 { 578 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 579 - } 580 - tee.enqueue(common::FakeCreateReportResponse::ok_empty()); // wrong_aud with 200 581 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 582 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 583 - tee.enqueue(common::FakeCreateReportResponse::bad_request( 584 - "InvalidRequest", 585 - "x", 586 - )); 587 - 588 - let mut opts = default_opts(); 589 - opts.self_mint_signer = Some(&signer); 590 - let results = run_report_stage(&facts, &tee, opts).await; 591 - 592 - assert_eq!(results[3].id, "report::wrong_aud_rejected"); 593 - assert_eq!(results[3].status, CheckStatus::SpecViolation); 594 - let diag = results[3].diagnostic.as_ref().expect("diagnostic present"); 595 - assert_eq!( 596 - diag.code().map(|c| c.to_string()), 597 - Some("labeler::report::wrong_aud_accepted".to_string()), 598 - ); 599 - } 600 - 601 - #[tokio::test] 602 - async fn ac3_3_wrong_lxm_401_passes() { 603 - let facts = local_identity_facts(); 604 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 605 - let tee = FakeCreateReportTee::new(); 606 - for _ in 0..2 { 607 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 608 - } 609 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 610 - "BadJwtAudience", 611 - "x", 612 - )); 613 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 614 - "BadJwtLexiconMethod", 615 - "lxm mismatch", 616 - )); 617 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 618 - "JwtExpired", 619 - "x", 620 - )); 621 - tee.enqueue(common::FakeCreateReportResponse::bad_request( 622 - "InvalidRequest", 623 - "x", 624 - )); 625 - 626 - let mut opts = default_opts(); 627 - opts.self_mint_signer = Some(&signer); 628 - let results = run_report_stage(&facts, &tee, opts).await; 629 - 630 - assert_eq!(results[4].id, "report::wrong_lxm_rejected"); 631 - assert_eq!(results[4].status, CheckStatus::Pass); 632 - } 633 - 634 - #[tokio::test] 635 - async fn ac3_4_wrong_lxm_200_is_spec_violation() { 636 - let facts = local_identity_facts(); 637 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 638 - let tee = FakeCreateReportTee::new(); 639 - for _ in 0..2 { 640 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 641 - } 642 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 643 - tee.enqueue(common::FakeCreateReportResponse::ok_empty()); // wrong_lxm with 200 644 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 645 - tee.enqueue(common::FakeCreateReportResponse::bad_request( 646 - "InvalidRequest", 647 - "x", 648 - )); 649 - 650 - let mut opts = default_opts(); 651 - opts.self_mint_signer = Some(&signer); 652 - let results = run_report_stage(&facts, &tee, opts).await; 653 - 654 - assert_eq!(results[4].id, "report::wrong_lxm_rejected"); 655 - assert_eq!(results[4].status, CheckStatus::SpecViolation); 656 - let diag = results[4].diagnostic.as_ref().expect("diagnostic present"); 657 - assert_eq!( 658 - diag.code().map(|c| c.to_string()), 659 - Some("labeler::report::wrong_lxm_accepted".to_string()), 660 - ); 661 - } 662 - 663 - #[tokio::test] 664 - async fn ac3_5_expired_401_passes() { 665 - let facts = local_identity_facts(); 666 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 667 - let tee = FakeCreateReportTee::new(); 668 - for _ in 0..2 { 669 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 670 - } 671 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 672 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 673 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 674 - "JwtExpired", 675 - "expired", 676 - )); 677 - tee.enqueue(common::FakeCreateReportResponse::bad_request( 678 - "InvalidRequest", 679 - "x", 680 - )); 681 - 682 - let mut opts = default_opts(); 683 - opts.self_mint_signer = Some(&signer); 684 - let results = run_report_stage(&facts, &tee, opts).await; 685 - 686 - assert_eq!(results[5].id, "report::expired_rejected"); 687 - assert_eq!(results[5].status, CheckStatus::Pass); 688 - } 689 - 690 - #[tokio::test] 691 - async fn ac3_6_shape_not_400_emits_advisory() { 692 - let facts = local_identity_facts(); 693 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 694 - let tee = FakeCreateReportTee::new(); 695 - for _ in 0..2 { 696 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 697 - } 698 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 699 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 700 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 701 - // rejected_shape with 401 instead of 400 → Advisory. 702 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 703 - "BadReason", 704 - "wrong status", 705 - )); 706 - 707 - let mut opts = default_opts(); 708 - opts.self_mint_signer = Some(&signer); 709 - let results = run_report_stage(&facts, &tee, opts).await; 710 - 711 - assert_eq!(results[6].id, "report::rejected_shape_returns_400"); 712 - assert_eq!(results[6].status, CheckStatus::Advisory); 713 - let diag = results[6].diagnostic.as_ref().expect("diagnostic present"); 714 - assert_eq!( 715 - diag.code().map(|c| c.to_string()), 716 - Some("labeler::report::shape_not_400".to_string()), 717 - ); 718 - } 719 - 720 - #[tokio::test] 721 - async fn ac3_7_non_local_labeler_skips_self_mint_checks() { 722 - let mut facts = make_identity_facts( 723 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 724 - Some(vec!["account".to_string()]), 725 - ); 726 - facts.labeler_endpoint = url::Url::parse("https://labeler.example.com").unwrap(); 727 - let tee = FakeCreateReportTee::new(); 728 - // Only two Phase 5 POSTs expected (unauth + malformed). 729 - for _ in 0..2 { 730 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 731 - } 732 - let mut opts = default_opts(); 733 - opts.self_mint_signer = None; 734 - let results = run_report_stage(&facts, &tee, opts).await; 735 - for (i, result) in results.iter().enumerate().skip(3).take(4) { 736 - assert_eq!( 737 - result.status, 738 - CheckStatus::Skipped, 739 - "row {} ({})", 740 - i, 741 - result.id 742 - ); 743 - assert!( 744 - result 745 - .skipped_reason 746 - .as_deref() 747 - .unwrap() 748 - .contains("--force-self-mint"), 749 - "row {}: {:?}", 750 - i, 751 - result.skipped_reason, 752 - ); 753 - } 754 - } 755 - 756 - #[tokio::test] 757 - async fn ac3_8_force_self_mint_overrides_non_local() { 758 - let mut facts = make_identity_facts( 759 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 760 - Some(vec!["account".to_string()]), 761 - ); 762 - facts.labeler_endpoint = url::Url::parse("https://labeler.example.com").unwrap(); 763 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 764 - let tee = FakeCreateReportTee::new(); 765 - for _ in 0..2 { 766 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 767 - } 768 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 769 - "BadJwtAudience", 770 - "x", 771 - )); 772 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 773 - "BadJwtLexiconMethod", 774 - "x", 775 - )); 776 - tee.enqueue(common::FakeCreateReportResponse::unauthorized( 777 - "JwtExpired", 778 - "x", 779 - )); 780 - tee.enqueue(common::FakeCreateReportResponse::bad_request( 781 - "InvalidRequest", 782 - "x", 783 - )); 784 - let mut opts = default_opts(); 785 - opts.self_mint_signer = Some(&signer); 786 - opts.force_self_mint = true; 787 - let results = run_report_stage(&facts, &tee, opts).await; 788 - assert_eq!(results[3].status, CheckStatus::Pass); 789 - assert_eq!(results[6].status, CheckStatus::Pass); 790 - } 791 - 792 - #[tokio::test] 793 - async fn ac4_1_local_labeler_accepts_with_lex_first_reason_and_account_subject() { 794 - let facts = local_identity_facts(); 795 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 796 - let tee = FakeCreateReportTee::new(); 797 - // Enqueue responses for Phase 5 (2), Phase 6 (4), then AC4 positive. 798 - for _ in 0..2 { 799 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 800 - } 801 - for _ in 0..4 { 802 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 803 - } 804 - tee.enqueue(common::FakeCreateReportResponse::ok_empty()); 805 - 806 - let mut opts = default_opts(); 807 - opts.self_mint_signer = Some(&signer); 808 - opts.commit_report = true; 809 - let run_id = "test-run-1234567890".to_string(); 810 - opts.run_id = &run_id; 811 - let results = run_report_stage(&facts, &tee, opts).await; 812 - 813 - assert_eq!(results[7].id, "report::self_mint_accepted"); 814 - assert_eq!(results[7].status, CheckStatus::Pass); 815 - 816 - // AC4.6: last_request() body contains the sentinel. 817 - let last_req = tee.last_request(); 818 - let body_reason = last_req.body["reason"].as_str().unwrap_or(""); 819 - assert!(body_reason.starts_with("atproto-devtool conformance test")); 820 - assert!(body_reason.ends_with(&run_id)); 821 - 822 - // AC4.1: reasonType is lex-first (reasonSpam), subject is account. 823 - let body = &last_req.body; 824 - assert_eq!(body["reasonType"], "com.atproto.moderation.defs#reasonSpam"); 825 - assert_eq!(body["subject"]["$type"], "com.atproto.admin.defs#repoRef"); 826 - } 827 - 828 - #[tokio::test] 829 - async fn ac4_2_non_local_labeler_prefers_other_and_record() { 830 - let mut facts = make_identity_facts( 831 - Some(vec![ 832 - "com.atproto.moderation.defs#reasonSpam".to_string(), 833 - "com.atproto.moderation.defs#reasonOther".to_string(), 834 - ]), 835 - Some(vec!["account".to_string(), "record".to_string()]), 836 - ); 837 - facts.labeler_endpoint = url::Url::parse("https://labeler.example.com").unwrap(); 838 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 839 - let tee = FakeCreateReportTee::new(); 840 - // Phase 5: 2 POSTs (unauth, malformed). 841 - for _ in 0..2 { 842 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 843 - } 844 - // Phase 6: 4 POSTs (wrong_aud, wrong_lxm, expired, rejected_shape). 845 - for _ in 0..4 { 846 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 847 - } 848 - // AC4: 1 POST (self_mint_accepted). 849 - tee.enqueue(common::FakeCreateReportResponse::ok_empty()); 850 - let mut opts = default_opts(); 851 - opts.self_mint_signer = Some(&signer); 852 - opts.commit_report = true; 853 - opts.force_self_mint = true; // Force self-mint to enable Phase 6 checks. 854 - 855 - let results = run_report_stage(&facts, &tee, opts).await; 856 - assert_eq!(results[7].status, CheckStatus::Pass); 857 - 858 - let last_req = tee.last_request(); 859 - assert_eq!( 860 - last_req.body["reasonType"], 861 - "com.atproto.moderation.defs#reasonOther" 862 - ); 863 - assert_eq!( 864 - last_req.body["subject"]["$type"], 865 - "com.atproto.repo.strongRef" 866 - ); 867 - } 868 - 869 - #[tokio::test] 870 - async fn ac4_3_non_2xx_is_spec_violation() { 871 - let facts = local_identity_facts(); 872 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 873 - let tee = FakeCreateReportTee::new(); 874 - // 2 Phase 5 + 4 Phase 6 + 1 AC4. 875 - for _ in 0..2 { 876 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 877 - } 878 - for _ in 0..4 { 879 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 880 - } 881 - tee.enqueue(common::FakeCreateReportResponse::Response { 882 - status: 400, 883 - content_type: Some("application/json".to_string()), 884 - body: br#"{"error":"InvalidRequest","message":"nope"}"#.to_vec(), 885 - }); 886 - 887 - let mut opts = default_opts(); 888 - opts.self_mint_signer = Some(&signer); 889 - opts.commit_report = true; 890 - let run_id = "x".to_string(); 891 - opts.run_id = &run_id; 892 - let results = run_report_stage(&facts, &tee, opts).await; 893 - 894 - assert_eq!(results[7].status, CheckStatus::SpecViolation); 895 - assert_eq!( 896 - results[7] 897 - .diagnostic 898 - .as_ref() 899 - .unwrap() 900 - .code() 901 - .map(|c| c.to_string()), 902 - Some("labeler::report::self_mint_rejected".to_string()), 903 - ); 904 - } 905 - 906 - #[tokio::test] 907 - async fn ac4_4_commit_false_skips() { 908 - let facts = local_identity_facts(); 909 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 910 - let tee = FakeCreateReportTee::new(); 911 - for _ in 0..2 { 912 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 913 - } 914 - for _ in 0..4 { 915 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 916 - } 917 - // No POST for self_mint_accepted because commit is false. 918 - 919 - let mut opts = default_opts(); 920 - opts.self_mint_signer = Some(&signer); 921 - opts.commit_report = false; 922 - let results = run_report_stage(&facts, &tee, opts).await; 923 - 924 - assert_eq!(results[7].status, CheckStatus::Skipped); 925 - assert!( 926 - results[7] 927 - .skipped_reason 928 - .as_deref() 929 - .unwrap() 930 - .contains("--commit-report"), 931 - "expected skip reason to mention --commit-report", 932 - ); 933 - } 934 - 935 - #[tokio::test] 936 - async fn ac4_5_non_viable_skip_matches_phase_6_reason() { 937 - // When self_mint_viable=false AND commit_report=true, self_mint_accepted 938 - // is Skipped with the Phase-6 viability reason (already tested, retest 939 - // that the row is Skipped here for completeness). 940 - let mut facts = make_identity_facts( 941 - Some(vec!["com.atproto.moderation.defs#reasonSpam".to_string()]), 942 - Some(vec!["account".to_string()]), 943 - ); 944 - facts.labeler_endpoint = url::Url::parse("https://labeler.example.com").unwrap(); 945 - let tee = FakeCreateReportTee::new(); 946 - for _ in 0..2 { 947 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 948 - } 949 - let mut opts = default_opts(); 950 - opts.self_mint_signer = None; 951 - opts.commit_report = true; 952 - let results = run_report_stage(&facts, &tee, opts).await; 953 - assert_eq!(results[7].status, CheckStatus::Skipped); 954 - } 955 - 956 - #[tokio::test] 957 - async fn ac4_2xx_without_id_is_pass_with_note() { 958 - let facts = local_identity_facts(); 959 - let signer = SelfMintSigner::spawn(SelfMintCurve::Es256k).await.unwrap(); 960 - let tee = FakeCreateReportTee::new(); 961 - // Enqueue responses for Phase 5 (2), Phase 6 (4), then AC4 positive with missing ID. 962 - for _ in 0..2 { 963 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 964 - } 965 - for _ in 0..4 { 966 - tee.enqueue(common::FakeCreateReportResponse::unauthorized("x", "y")); 967 - } 968 - // 2xx response with body that doesn't contain a valid numeric ID. 969 - tee.enqueue(common::FakeCreateReportResponse::Response { 970 - status: 200, 971 - content_type: Some("application/json".to_string()), 972 - body: b"{\"foo\":\"bar\"}".to_vec(), 973 - }); 974 - 975 - let mut opts = default_opts(); 976 - opts.self_mint_signer = Some(&signer); 977 - opts.commit_report = true; 978 - let results = run_report_stage(&facts, &tee, opts).await; 979 - 980 - // Should be Pass (2xx is sufficient), but with a note about the body. 981 - assert_eq!(results[7].id, "report::self_mint_accepted"); 982 - assert_eq!(results[7].status, CheckStatus::Pass); 983 - assert!( 984 - results[7].summary.contains("body did not match"), 985 - "expected summary to mention non-matching body, got: {}", 986 - results[7].summary 987 - ); 988 - }
+40 -106
tests/labeler_subscription.rs
··· 7 7 8 8 mod common; 9 9 10 - use atproto_devtool::commands::test::labeler::create_report::self_mint::SelfMintCurve; 11 10 use atproto_devtool::commands::test::labeler::pipeline::{ 12 - CreateReportTeeKind, HttpTee, LabelerOptions, parse_target, run_pipeline, 11 + HttpTee, LabelerOptions, parse_target, run_pipeline, 13 12 }; 14 13 use atproto_devtool::commands::test::labeler::subscription::{FrameHeader, SubscribeLabelsPayload}; 15 14 use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError}; ··· 114 113 let tee = common::FakeRawHttpTee::new(); 115 114 tee.add_response(None, 200, br#"{"cursor":null,"labels":[]}"#.to_vec()); 116 115 tee 116 + } 117 + 118 + /// Helper to normalize elapsed time in snapshots. 119 + /// 120 + /// Replaces `elapsed: <N>ms` with `elapsed: XXms`, advancing past each match 121 + /// to avoid re-matching the replacement on the next iteration. 122 + fn normalize_timing(rendered: String) -> String { 123 + let mut result = String::with_capacity(rendered.len()); 124 + let mut rest = rendered.as_str(); 125 + while let Some(pos) = rest.find("elapsed: ") { 126 + let after = pos + "elapsed: ".len(); 127 + result.push_str(&rest[..after]); 128 + let tail = &rest[after..]; 129 + if let Some(end) = tail.find("ms") { 130 + result.push_str("XXms"); 131 + rest = &tail[end + 2..]; 132 + } else { 133 + result.push_str(tail); 134 + return result; 135 + } 136 + } 137 + result.push_str(rest); 138 + result 117 139 } 118 140 119 141 /// Encodes a frame with length prefix: [4-byte big-endian length][frame bytes]. ··· 262 284 let fake_tee = make_passing_http_tee(); 263 285 let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 264 286 265 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 266 - 267 287 let opts = LabelerOptions { 268 288 http: &http, 269 289 dns: &dns, 270 290 http_tee: HttpTee::Test(&fake_tee), 271 291 ws_client: Some(&fake_ws), 272 - subscribe_timeout: Duration::from_secs(5), 292 + subscribe_timeout: Duration::from_secs(2), 273 293 verbose: false, 274 - 275 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 276 - commit_report: false, 277 - force_self_mint: false, 278 - self_mint_curve: SelfMintCurve::Es256k, 279 - report_subject_override: None, 280 - self_mint_signer: None, 281 - pds_credentials: None, 282 - run_id: "test-run-id", 283 294 }; 284 295 285 296 let report = run_pipeline(target, opts).await; 286 - let rendered = common::normalize_timing(render_report_to_string(&report)); 297 + let rendered = normalize_timing(render_report_to_string(&report)); 287 298 288 299 insta::assert_snapshot!(rendered); 289 300 } ··· 321 332 let fake_tee = make_passing_http_tee(); 322 333 let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 323 334 324 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 325 - 326 335 let opts = LabelerOptions { 327 336 http: &http, 328 337 dns: &dns, 329 338 http_tee: HttpTee::Test(&fake_tee), 330 339 ws_client: Some(&fake_ws), 331 - subscribe_timeout: Duration::from_secs(5), 340 + subscribe_timeout: Duration::from_secs(2), 332 341 verbose: false, 333 - 334 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 335 - commit_report: false, 336 - force_self_mint: false, 337 - self_mint_curve: SelfMintCurve::Es256k, 338 - report_subject_override: None, 339 - self_mint_signer: None, 340 - pds_credentials: None, 341 - run_id: "test-run-id", 342 342 }; 343 343 344 344 let report = run_pipeline(target, opts).await; 345 - let rendered = common::normalize_timing(render_report_to_string(&report)); 345 + let rendered = normalize_timing(render_report_to_string(&report)); 346 346 347 347 insta::assert_snapshot!(rendered); 348 348 } ··· 366 366 let fake_tee = make_passing_http_tee(); 367 367 let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 368 368 369 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 370 - 371 369 let opts = LabelerOptions { 372 370 http: &http, 373 371 dns: &dns, 374 372 http_tee: HttpTee::Test(&fake_tee), 375 373 ws_client: Some(&fake_ws), 376 - subscribe_timeout: Duration::from_secs(5), 374 + subscribe_timeout: Duration::from_secs(2), 377 375 verbose: false, 378 - 379 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 380 - commit_report: false, 381 - force_self_mint: false, 382 - self_mint_curve: SelfMintCurve::Es256k, 383 - report_subject_override: None, 384 - self_mint_signer: None, 385 - pds_credentials: None, 386 - run_id: "test-run-id", 387 376 }; 388 377 389 378 let report = run_pipeline(target, opts).await; 390 - let rendered = common::normalize_timing(render_report_to_string(&report)); 379 + let rendered = render_report_to_string(&report); 391 380 392 381 insta::assert_snapshot!(rendered); 393 382 } ··· 421 410 let dns = FakeDnsResolver::new(); 422 411 let fake_tee = make_passing_http_tee(); 423 412 let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 424 - 425 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 426 413 427 414 let opts = LabelerOptions { 428 415 http: &http, 429 416 dns: &dns, 430 417 http_tee: HttpTee::Test(&fake_tee), 431 418 ws_client: Some(&fake_ws), 432 - subscribe_timeout: Duration::from_secs(5), 419 + subscribe_timeout: Duration::from_secs(2), 433 420 verbose: false, 434 - 435 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 436 - commit_report: false, 437 - force_self_mint: false, 438 - self_mint_curve: SelfMintCurve::Es256k, 439 - report_subject_override: None, 440 - self_mint_signer: None, 441 - pds_credentials: None, 442 - run_id: "test-run-id", 443 421 }; 444 422 445 423 let report = run_pipeline(target, opts).await; 446 - let rendered = common::normalize_timing(render_report_to_string(&report)); 424 + let rendered = normalize_timing(render_report_to_string(&report)); 447 425 448 426 insta::assert_snapshot!(rendered); 449 427 } ··· 479 457 let fake_tee = make_passing_http_tee(); 480 458 let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 481 459 482 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 483 - 484 460 let opts = LabelerOptions { 485 461 http: &http, 486 462 dns: &dns, 487 463 http_tee: HttpTee::Test(&fake_tee), 488 464 ws_client: Some(&fake_ws), 489 - subscribe_timeout: Duration::from_secs(5), 465 + subscribe_timeout: Duration::from_secs(2), 490 466 verbose: false, 491 - 492 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 493 - commit_report: false, 494 - force_self_mint: false, 495 - self_mint_curve: SelfMintCurve::Es256k, 496 - report_subject_override: None, 497 - self_mint_signer: None, 498 - pds_credentials: None, 499 - run_id: "test-run-id", 500 467 }; 501 468 502 469 let report = run_pipeline(target, opts).await; 503 - let rendered = common::normalize_timing(render_report_to_string(&report)); 470 + let rendered = normalize_timing(render_report_to_string(&report)); 504 471 505 472 insta::assert_snapshot!(rendered); 506 473 } ··· 523 490 let fake_tee = make_passing_http_tee(); 524 491 let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 525 492 526 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 527 - 528 493 let opts = LabelerOptions { 529 494 http: &http, 530 495 dns: &dns, 531 496 http_tee: HttpTee::Test(&fake_tee), 532 497 ws_client: Some(&fake_ws), 533 - subscribe_timeout: Duration::from_secs(5), 498 + subscribe_timeout: Duration::from_secs(2), 534 499 verbose: false, 535 - 536 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 537 - commit_report: false, 538 - force_self_mint: false, 539 - self_mint_curve: SelfMintCurve::Es256k, 540 - report_subject_override: None, 541 - self_mint_signer: None, 542 - pds_credentials: None, 543 - run_id: "test-run-id", 544 500 }; 545 501 546 502 let report = run_pipeline(target, opts).await; 547 - let rendered = common::normalize_timing(render_report_to_string(&report)); 503 + let rendered = render_report_to_string(&report); 548 504 549 505 insta::assert_snapshot!(rendered); 550 506 } ··· 579 535 let fake_tee = make_passing_http_tee(); 580 536 let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 581 537 582 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 583 - 584 538 let opts = LabelerOptions { 585 539 http: &http, 586 540 dns: &dns, 587 541 http_tee: HttpTee::Test(&fake_tee), 588 542 ws_client: Some(&fake_ws), 589 - subscribe_timeout: Duration::from_secs(5), 543 + subscribe_timeout: Duration::from_secs(2), 590 544 verbose: false, 591 - 592 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 593 - commit_report: false, 594 - force_self_mint: false, 595 - self_mint_curve: SelfMintCurve::Es256k, 596 - report_subject_override: None, 597 - self_mint_signer: None, 598 - pds_credentials: None, 599 - run_id: "test-run-id", 600 545 }; 601 546 602 547 let report = run_pipeline(target, opts).await; 603 - let rendered = common::normalize_timing(render_report_to_string(&report)); 548 + let rendered = normalize_timing(render_report_to_string(&report)); 604 549 605 550 insta::assert_snapshot!(rendered); 606 551 } ··· 646 591 let fake_tee = make_passing_http_tee(); 647 592 let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 648 593 649 - let fake_create_report_tee = common::FakeCreateReportTee::new(); 650 - 651 594 let opts = LabelerOptions { 652 595 http: &http, 653 596 dns: &dns, 654 597 http_tee: HttpTee::Test(&fake_tee), 655 598 ws_client: Some(&fake_ws), 656 - subscribe_timeout: Duration::from_secs(5), 599 + subscribe_timeout: Duration::from_secs(2), 657 600 verbose: false, 658 - 659 - create_report_tee: CreateReportTeeKind::Test(&fake_create_report_tee), 660 - commit_report: false, 661 - force_self_mint: false, 662 - self_mint_curve: SelfMintCurve::Es256k, 663 - report_subject_override: None, 664 - self_mint_signer: None, 665 - pds_credentials: None, 666 - run_id: "test-run-id", 667 601 }; 668 602 669 603 let report = run_pipeline(target, opts).await; 670 - let rendered = common::normalize_timing(render_report_to_string(&report)); 604 + let rendered = normalize_timing(render_report_to_string(&report)); 671 605 672 606 insta::assert_snapshot!(rendered); 673 607 }