CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

Refactor to clean up code

+471 -427
+15 -11
src/commands/test/labeler.rs
··· 1 1 //! `atproto-devtool test labeler <target>` command. 2 2 3 - pub mod create_report; 4 - pub mod crypto; 5 - pub mod http; 6 - pub mod identity; 7 3 pub mod pipeline; 8 4 pub mod report; 9 - pub mod subscription; 5 + pub mod target; 10 6 11 7 use std::io; 12 8 use std::process::ExitCode; ··· 15 11 use clap::Args; 16 12 use miette::Report; 17 13 18 - use crate::commands::test::labeler::create_report::self_mint::{SelfMintCurve, SelfMintSigner}; 14 + use self::{ 15 + pipeline::{ 16 + LabelerOptions, 17 + create_report::{ 18 + self, 19 + self_mint::{SelfMintCurve, SelfMintSigner}, 20 + }, 21 + run_pipeline, 22 + }, 23 + report::RenderConfig, 24 + }; 19 25 use crate::common::{ 20 26 APP_USER_AGENT, 21 27 identity::{Did, RealDnsResolver, RealHttpClient, is_local_labeler_hostname}, 22 28 }; 23 - use pipeline::{LabelerOptions, parse_target, run_pipeline}; 24 - use report::RenderConfig; 25 29 26 30 /// Run the labeler conformance suite against a handle, DID, or endpoint URL. 27 31 #[derive(Debug, Args)] ··· 90 94 pub async fn run(self, no_color: bool) -> Result<ExitCode, Report> { 91 95 // Parse the target. 92 96 let target = 93 - parse_target(&self.target, self.did.as_deref()).map_err(|e| miette::miette!("{e}"))?; 97 + target::parse(&self.target, self.did.as_deref()).map_err(|e| miette::miette!("{e}"))?; 94 98 95 99 // Determine tentative endpoint for the locality check. When the target is a 96 100 // DID or handle, the endpoint is known only after identity stage; for the 97 101 // self-mint signer construction we need it now. We construct the signer 98 102 // pessimistically (endpoint unknown) only when --force-self-mint is set. 99 103 let tentative_endpoint: Option<url::Url> = match &target { 100 - pipeline::LabelerTarget::Endpoint { url, .. } => Some(url.clone()), 101 - pipeline::LabelerTarget::Identified { .. } => None, 104 + target::LabelerTarget::Endpoint { url, .. } => Some(url.clone()), 105 + target::LabelerTarget::Identified { .. } => None, 102 106 }; 103 107 104 108 let tentative_local = tentative_endpoint
+4 -3
src/commands/test/labeler/CLAUDE.md
··· 17 17 - `LabelerCmd::run(no_color) -> Result<ExitCode, miette::Report>` (in 18 18 `labeler.rs`) — constructs the shared reqwest client and calls 19 19 `pipeline::run_pipeline`. 20 - - `pipeline::parse_target(raw, explicit_did) -> LabelerTarget` — the 20 + - `target::parse(raw, explicit_did) -> LabelerTarget` — the 21 21 accepted target grammar is handle, `did:*`, `https://` URL, or 22 22 `http://` URL with a local hostname (loopback, RFC 1918, `.local`). 23 23 Remote HTTP is rejected with a helpful error; raw endpoints with 24 24 no DID simply skip identity/crypto. 25 25 - `pipeline::run_pipeline(target, LabelerOptions) -> LabelerReport` — the 26 26 one orchestrator that every test hits. 27 - - **Per-stage entry points**: `identity::run`, `http::run`, 28 - `subscription::run`, `crypto::run`, `create_report::run`. Each returns a 27 + - **Per-stage entry points**: `pipeline::identity::run`, `pipeline::http::run`, 28 + `pipeline::subscription::run`, `pipeline::crypto::run`, 29 + `pipeline::create_report::run`. Each returns a 29 30 `*StageOutput` with an `Option<*Facts>` (populated only when the stage 30 31 succeeds enough to let downstream stages run, or `None` when there are no 31 32 meaningful facts to carry forward) plus a `Vec<CheckResult>`.
+2 -2
src/commands/test/labeler/create_report.rs src/commands/test/labeler/pipeline/create_report.rs
··· 14 14 use reqwest::StatusCode; 15 15 use thiserror::Error; 16 16 17 - use crate::commands::test::labeler::identity::IdentityFacts; 17 + use super::identity::IdentityFacts; 18 18 use crate::commands::test::labeler::report::{CheckResult, CheckStatus, Stage}; 19 19 use crate::common::diagnostics::pretty_json_for_display; 20 20 use crate::common::identity::{Did, is_local_labeler_hostname}; ··· 645 645 /// always emits exactly 10 `report::*` CheckResults (AC7.1) in canonical 646 646 /// order (AC7.2), regardless of gating decisions. 647 647 pub async fn run( 648 - identity_facts: Option<&crate::commands::test::labeler::identity::IdentityFacts>, 648 + identity_facts: Option<&super::identity::IdentityFacts>, 649 649 report_tee: &dyn CreateReportTee, 650 650 opts: &CreateReportRunOptions<'_>, 651 651 ) -> CreateReportStageOutput {
src/commands/test/labeler/create_report/did_doc_server.rs src/commands/test/labeler/pipeline/create_report/did_doc_server.rs
src/commands/test/labeler/create_report/pollution.rs src/commands/test/labeler/pipeline/create_report/pollution.rs
src/commands/test/labeler/create_report/self_mint.rs src/commands/test/labeler/pipeline/create_report/self_mint.rs
src/commands/test/labeler/create_report/sentinel.rs src/commands/test/labeler/pipeline/create_report/sentinel.rs
+2 -2
src/commands/test/labeler/crypto.rs src/commands/test/labeler/pipeline/crypto.rs
··· 459 459 /// 6. Else if `did:plc`: fetch PLC audit log and retry against historic keys. 460 460 /// 7. Else (`did:web`): emit `crypto::rollup` SpecViolation with no rotation history. 461 461 pub async fn run( 462 - identity: &crate::commands::test::labeler::identity::IdentityFacts, 462 + identity: &super::identity::IdentityFacts, 463 463 labels: &[Label], 464 464 http: &dyn crate::common::identity::HttpClient, 465 465 ) -> CryptoStageOutput { ··· 788 788 789 789 #[cfg(test)] 790 790 mod tests { 791 + use super::super::identity::IdentityFacts; 791 792 use super::*; 792 - use crate::commands::test::labeler::identity::IdentityFacts; 793 793 use crate::common::identity::{ 794 794 AnySignature, AnyVerifyingKey, Did, DidDocument, IdentityError, RawDidDocument, 795 795 encode_multikey,
src/commands/test/labeler/http.rs src/commands/test/labeler/pipeline/http.rs
src/commands/test/labeler/identity.rs src/commands/test/labeler/pipeline/identity.rs
+14 -331
src/commands/test/labeler/pipeline.rs
··· 3 3 use std::borrow::Cow; 4 4 use std::time::Duration; 5 5 6 - use miette::Diagnostic; 7 - use thiserror::Error; 8 6 use url::Url; 9 7 10 - use crate::commands::test::labeler::create_report::self_mint::{SelfMintCurve, SelfMintSigner}; 11 - use crate::commands::test::labeler::create_report::{ 12 - self, CreateReportTee, PdsXrpcClient, RealCreateReportTee, 13 - }; 14 - use crate::commands::test::labeler::crypto; 15 - use crate::commands::test::labeler::http::{self, RealHttpTee}; 16 - use crate::commands::test::labeler::identity; 17 - use crate::commands::test::labeler::report::{ 18 - CheckResult, CheckStatus, LabelerReport, ReportHeader, Stage, 8 + use super::{ 9 + report::{CheckResult, CheckStatus, LabelerReport, ReportHeader, Stage}, 10 + target::{AtIdentifier, LabelerTarget}, 19 11 }; 20 - use crate::commands::test::labeler::subscription::{self, RealWebSocketClient}; 21 12 use crate::common::identity::{ 22 - Did, DnsResolver, HttpClient, find_service, is_local_labeler_hostname, resolve_did, 23 - resolve_handle, 13 + Did, DnsResolver, HttpClient, find_service, resolve_did, resolve_handle, 24 14 }; 25 15 26 - /// A labeler target: either a resolvable identifier (handle or DID) or a raw endpoint URL. 27 - #[derive(Debug, Clone)] 28 - pub enum LabelerTarget { 29 - /// A handle or DID that can be resolved. 30 - Identified { 31 - /// The handle or DID to resolve. 32 - identifier: AtIdentifier, 33 - /// An optional explicit DID override (for cross-checking). 34 - explicit_did: Option<Did>, 35 - }, 36 - /// A raw HTTP endpoint, optionally with a DID for identity checks. 37 - Endpoint { 38 - /// The endpoint URL. 39 - url: Url, 40 - /// An optional DID to cross-check against the endpoint. 41 - did: Option<Did>, 42 - }, 43 - } 16 + pub mod create_report; 17 + pub mod crypto; 18 + pub mod http; 19 + pub mod identity; 20 + pub mod subscription; 44 21 45 - /// An ATProto identifier: a handle or a DID. 46 - #[derive(Debug, Clone)] 47 - pub enum AtIdentifier { 48 - /// An ATProto handle (e.g., `alice.bsky.social`). 49 - Handle(String), 50 - /// A decentralized identifier (e.g., `did:plc:...` or `did:web:...`). 51 - Did(Did), 52 - } 22 + use self::create_report::self_mint::{SelfMintCurve, SelfMintSigner}; 23 + use self::create_report::{CreateReportTee, PdsXrpcClient, RealCreateReportTee}; 24 + use self::http::RealHttpTee; 25 + use self::subscription::RealWebSocketClient; 53 26 54 27 /// Options for running the labeler pipeline. 55 28 pub struct LabelerOptions<'a> { ··· 124 97 pub app_password: String, 125 98 } 126 99 127 - /// Error from target parsing. 128 - #[derive(Debug, Error, Diagnostic)] 129 - #[error("{message}")] 130 - pub struct TargetParseError { 131 - /// The error message. 132 - pub message: String, 133 - } 134 - 135 - impl TargetParseError { 136 - fn new(message: impl Into<String>) -> Self { 137 - Self { 138 - message: message.into(), 139 - } 140 - } 141 - 142 - fn unrecognized_target(raw: &str) -> Self { 143 - Self::new(format!( 144 - "Unrecognized target '{raw}'. Expected one of:\n - ATProto handle (e.g., alice.bsky.social)\n - DID (e.g., did:plc:abc123 or did:web:example.com)\n - HTTPS endpoint URL (e.g., https://labeler.example.com)\n - HTTP endpoint URL with a local hostname (e.g., http://localhost:8080)" 145 - )) 146 - } 147 - 148 - fn http_not_supported(raw: &str) -> Self { 149 - Self::new(format!( 150 - "HTTP endpoint '{raw}' is not supported for remote hosts. Use HTTPS, or point at a local labeler (localhost / 127.0.0.0/8 / RFC 1918 / .local) to allow plaintext HTTP." 151 - )) 152 - } 153 - 154 - fn ambiguous_did(raw: &str, explicit: &str) -> Self { 155 - Self::new(format!( 156 - "Ambiguous target specification: target '{raw}' is already a DID, but --did {explicit} was also provided. Please use only one." 157 - )) 158 - } 159 - } 160 - 161 - /// Check if a string is a valid ATProto handle. 162 - /// 163 - /// A valid handle: 164 - /// - Contains at least one dot. 165 - /// - Contains only alphanumeric characters, hyphens, and dots. 166 - /// - Does not start or end with a hyphen or dot. 167 - /// - Has no empty segments (no consecutive dots or leading/trailing dots). 168 - fn is_valid_handle(s: &str) -> bool { 169 - if !s.contains('.') { 170 - return false; 171 - } 172 - 173 - // Check for empty string or leading/trailing special chars. 174 - if s.is_empty() 175 - || s.starts_with('-') 176 - || s.starts_with('.') 177 - || s.ends_with('-') 178 - || s.ends_with('.') 179 - { 180 - return false; 181 - } 182 - 183 - // Check all characters are alphanumeric, hyphen, or dot. 184 - for c in s.chars() { 185 - if !c.is_ascii_alphanumeric() && c != '-' && c != '.' { 186 - return false; 187 - } 188 - } 189 - 190 - // Check no empty segments (no consecutive dots). 191 - if s.contains("..") { 192 - return false; 193 - } 194 - 195 - true 196 - } 197 - 198 - /// Parse a labeler target from a string and optional explicit DID. 199 - /// 200 - /// Returns a `LabelerTarget` on success, or a `TargetParseError` on failure. 201 - /// 202 - /// Rules: 203 - /// - If `raw` starts with `did:`, parse as a DID. If `explicit_did` is also provided, return an error. 204 - /// - If `raw` starts with `https://`, parse as a URL. `explicit_did` is carried as an optional DID. 205 - /// - If `raw` starts with `http://`, return an error pointing the user to HTTPS. 206 - /// - If `raw` contains a dot and matches handle grammar, treat as a handle. `explicit_did` is carried. 207 - /// - Otherwise, return an unrecognized target error. 208 - pub fn parse_target( 209 - raw: &str, 210 - explicit_did: Option<&str>, 211 - ) -> Result<LabelerTarget, TargetParseError> { 212 - // Check for DID. 213 - if raw.starts_with("did:") { 214 - if let Some(ed) = explicit_did { 215 - return Err(TargetParseError::ambiguous_did(raw, ed)); 216 - } 217 - return Ok(LabelerTarget::Identified { 218 - identifier: AtIdentifier::Did(Did(raw.to_string())), 219 - explicit_did: None, 220 - }); 221 - } 222 - 223 - // Check for HTTPS URL. 224 - if raw.starts_with("https://") { 225 - let url = Url::parse(raw) 226 - .map_err(|e| TargetParseError::new(format!("Invalid URL '{raw}': {e}")))?; 227 - return Ok(LabelerTarget::Endpoint { 228 - url, 229 - did: explicit_did.map(|d| Did(d.to_string())), 230 - }); 231 - } 232 - 233 - // Check for HTTP URL. Local hostnames (loopback, RFC 1918, .local, mDNS) 234 - // are accepted so developers can target a labeler running on their 235 - // machine or LAN. Remote HTTP is still rejected to guard against 236 - // accidental plaintext traffic to a production labeler. 237 - if raw.starts_with("http://") { 238 - let url = Url::parse(raw) 239 - .map_err(|e| TargetParseError::new(format!("Invalid URL '{raw}': {e}")))?; 240 - if is_local_labeler_hostname(&url) { 241 - return Ok(LabelerTarget::Endpoint { 242 - url, 243 - did: explicit_did.map(|d| Did(d.to_string())), 244 - }); 245 - } 246 - return Err(TargetParseError::http_not_supported(raw)); 247 - } 248 - 249 - // Check for handle. 250 - if is_valid_handle(raw) { 251 - return Ok(LabelerTarget::Identified { 252 - identifier: AtIdentifier::Handle(raw.to_string()), 253 - explicit_did: explicit_did.map(|d| Did(d.to_string())), 254 - }); 255 - } 256 - 257 - // Unrecognized target. 258 - Err(TargetParseError::unrecognized_target(raw)) 259 - } 260 - 261 100 /// Run the full labeler conformance pipeline. 262 101 /// 263 102 /// This is the main driver that orchestrates all validation stages. 264 103 pub async fn run_pipeline(target: LabelerTarget, opts: LabelerOptions<'_>) -> LabelerReport { 265 104 // Build initial header from target. 266 105 let header = ReportHeader { 267 - target: format_target(&target), 106 + target: target.to_string(), 268 107 resolved_did: None, 269 108 pds_endpoint: None, 270 109 labeler_endpoint: None, ··· 552 391 ) 553 392 }) 554 393 } 555 - 556 - /// Format a target for display in the report header. 557 - fn format_target(target: &LabelerTarget) -> String { 558 - match target { 559 - LabelerTarget::Identified { 560 - identifier, 561 - explicit_did, 562 - } => { 563 - let id_str = match identifier { 564 - AtIdentifier::Handle(h) => h.clone(), 565 - AtIdentifier::Did(d) => d.0.clone(), 566 - }; 567 - if explicit_did.is_some() { 568 - format!("{id_str} (with explicit DID)") 569 - } else { 570 - id_str 571 - } 572 - } 573 - LabelerTarget::Endpoint { url, did } => { 574 - if did.is_some() { 575 - format!("{url} (with explicit DID)") 576 - } else { 577 - url.to_string() 578 - } 579 - } 580 - } 581 - } 582 - 583 - #[cfg(test)] 584 - mod tests { 585 - use super::*; 586 - 587 - #[test] 588 - fn parse_target_handle() { 589 - let target = parse_target("alice.bsky.social", None).expect("should parse"); 590 - match target { 591 - LabelerTarget::Identified { 592 - identifier, 593 - explicit_did, 594 - } => { 595 - assert!( 596 - matches!(identifier, AtIdentifier::Handle(ref h) if h == "alice.bsky.social") 597 - ); 598 - assert!(explicit_did.is_none()); 599 - } 600 - _ => panic!("expected Identified variant"), 601 - } 602 - } 603 - 604 - #[test] 605 - fn parse_target_did_plc() { 606 - let target = parse_target("did:plc:abc123", None).expect("should parse"); 607 - match target { 608 - LabelerTarget::Identified { 609 - identifier, 610 - explicit_did, 611 - } => { 612 - assert!(matches!(identifier, AtIdentifier::Did(ref d) if d.0 == "did:plc:abc123")); 613 - assert!(explicit_did.is_none()); 614 - } 615 - _ => panic!("expected Identified variant"), 616 - } 617 - } 618 - 619 - #[test] 620 - fn parse_target_did_web() { 621 - let target = parse_target("did:web:example.com", None).expect("should parse"); 622 - match target { 623 - LabelerTarget::Identified { 624 - identifier, 625 - explicit_did, 626 - } => { 627 - assert!( 628 - matches!(identifier, AtIdentifier::Did(ref d) if d.0 == "did:web:example.com") 629 - ); 630 - assert!(explicit_did.is_none()); 631 - } 632 - _ => panic!("expected Identified variant"), 633 - } 634 - } 635 - 636 - #[test] 637 - fn parse_target_endpoint_https() { 638 - let target = parse_target("https://example.com/labeler", None).expect("should parse"); 639 - match target { 640 - LabelerTarget::Endpoint { url, did } => { 641 - assert_eq!(url.as_str(), "https://example.com/labeler"); 642 - assert!(did.is_none()); 643 - } 644 - _ => panic!("expected Endpoint variant"), 645 - } 646 - } 647 - 648 - #[test] 649 - fn parse_target_endpoint_with_explicit_did() { 650 - let target = 651 - parse_target("https://example.com/labeler", Some("did:plc:xyz")).expect("should parse"); 652 - match target { 653 - LabelerTarget::Endpoint { url, did } => { 654 - assert_eq!(url.as_str(), "https://example.com/labeler"); 655 - assert_eq!(did.map(|d| d.0.clone()), Some("did:plc:xyz".to_string())); 656 - } 657 - _ => panic!("expected Endpoint variant"), 658 - } 659 - } 660 - 661 - #[test] 662 - fn parse_target_endpoint_http_remote_rejected() { 663 - let err = parse_target("http://evil.example", None).expect_err("should reject http"); 664 - assert!(err.message.contains("HTTP")); 665 - assert!(err.message.contains("local")); 666 - } 667 - 668 - #[test] 669 - fn parse_target_endpoint_http_local_accepted() { 670 - // Each of these hostnames is classified as local by 671 - // `is_local_labeler_hostname`, so plaintext HTTP is allowed. 672 - let cases = &[ 673 - "http://localhost:8080", 674 - "http://127.0.0.1:5000", 675 - "http://127.1.2.3/", 676 - "http://[::1]:8080/", 677 - "http://10.0.0.1/", 678 - "http://192.168.1.100:8080", 679 - "http://172.16.0.1/", 680 - "http://mybox.local:8080", 681 - ]; 682 - for raw in cases { 683 - let target = parse_target(raw, None) 684 - .unwrap_or_else(|e| panic!("expected {raw} to parse, got: {}", e.message)); 685 - match target { 686 - LabelerTarget::Endpoint { url, did } => { 687 - assert_eq!( 688 - url.as_str().trim_end_matches('/'), 689 - raw.trim_end_matches('/') 690 - ); 691 - assert!(did.is_none()); 692 - } 693 - _ => panic!("expected Endpoint variant for {raw}"), 694 - } 695 - } 696 - } 697 - 698 - #[test] 699 - fn parse_target_unrecognised() { 700 - let err = parse_target("not a handle or did", None).expect_err("should fail"); 701 - assert!(err.message.contains("Unrecognized target")); 702 - } 703 - 704 - #[test] 705 - fn parse_target_did_with_conflicting_flag() { 706 - let err = parse_target("did:plc:abc", Some("did:web:example.com")) 707 - .expect_err("should reject ambiguous target"); 708 - assert!(err.message.contains("Ambiguous")); 709 - } 710 - }
src/commands/test/labeler/subscription.rs src/commands/test/labeler/pipeline/subscription.rs
+324
src/commands/test/labeler/target.rs
··· 1 + use std::fmt; 2 + 3 + use miette::Diagnostic; 4 + use thiserror::Error; 5 + use url::Url; 6 + 7 + use crate::common::identity::{Did, is_local_labeler_hostname}; 8 + 9 + /// A labeler target: either a resolvable identifier (handle or DID) or a raw endpoint URL. 10 + #[derive(Debug, Clone)] 11 + pub enum LabelerTarget { 12 + /// A handle or DID that can be resolved. 13 + Identified { 14 + /// The handle or DID to resolve. 15 + identifier: AtIdentifier, 16 + /// An optional explicit DID override (for cross-checking). 17 + explicit_did: Option<Did>, 18 + }, 19 + /// A raw HTTP endpoint, optionally with a DID for identity checks. 20 + Endpoint { 21 + /// The endpoint URL. 22 + url: Url, 23 + /// An optional DID to cross-check against the endpoint. 24 + did: Option<Did>, 25 + }, 26 + } 27 + 28 + /// An ATProto identifier: a handle or a DID. 29 + #[derive(Debug, Clone)] 30 + pub enum AtIdentifier { 31 + /// An ATProto handle (e.g., `alice.bsky.social`). 32 + Handle(String), 33 + /// A decentralized identifier (e.g., `did:plc:...` or `did:web:...`). 34 + Did(Did), 35 + } 36 + 37 + /// Parse a labeler target from a string and optional explicit DID. 38 + /// 39 + /// Returns a `LabelerTarget` on success, or a `TargetParseError` on failure. 40 + /// 41 + /// Rules: 42 + /// - If `raw` starts with `did:`, parse as a DID. If `explicit_did` is also provided, return an error. 43 + /// - If `raw` starts with `https://`, parse as a URL. `explicit_did` is carried as an optional DID. 44 + /// - If `raw` starts with `http://`, return an error pointing the user to HTTPS. 45 + /// - If `raw` contains a dot and matches handle grammar, treat as a handle. `explicit_did` is carried. 46 + /// - Otherwise, return an unrecognized target error. 47 + pub fn parse(raw: &str, explicit_did: Option<&str>) -> Result<LabelerTarget, TargetParseError> { 48 + // Check for DID. 49 + if raw.starts_with("did:") { 50 + if let Some(ed) = explicit_did { 51 + return Err(TargetParseError::ambiguous_did(raw, ed)); 52 + } 53 + return Ok(LabelerTarget::Identified { 54 + identifier: AtIdentifier::Did(Did(raw.to_string())), 55 + explicit_did: None, 56 + }); 57 + } 58 + 59 + // Check for HTTPS URL. 60 + if raw.starts_with("https://") { 61 + let url = Url::parse(raw) 62 + .map_err(|e| TargetParseError::new(format!("Invalid URL '{raw}': {e}")))?; 63 + return Ok(LabelerTarget::Endpoint { 64 + url, 65 + did: explicit_did.map(|d| Did(d.to_string())), 66 + }); 67 + } 68 + 69 + // Check for HTTP URL. Local hostnames (loopback, RFC 1918, .local, mDNS) 70 + // are accepted so developers can target a labeler running on their 71 + // machine or LAN. Remote HTTP is still rejected to guard against 72 + // accidental plaintext traffic to a production labeler. 73 + if raw.starts_with("http://") { 74 + let url = Url::parse(raw) 75 + .map_err(|e| TargetParseError::new(format!("Invalid URL '{raw}': {e}")))?; 76 + if is_local_labeler_hostname(&url) { 77 + return Ok(LabelerTarget::Endpoint { 78 + url, 79 + did: explicit_did.map(|d| Did(d.to_string())), 80 + }); 81 + } 82 + return Err(TargetParseError::http_not_supported(raw)); 83 + } 84 + 85 + // Check for handle. 86 + if is_valid_handle(raw) { 87 + return Ok(LabelerTarget::Identified { 88 + identifier: AtIdentifier::Handle(raw.to_string()), 89 + explicit_did: explicit_did.map(|d| Did(d.to_string())), 90 + }); 91 + } 92 + 93 + // Unrecognized target. 94 + Err(TargetParseError::unrecognized_target(raw)) 95 + } 96 + 97 + impl fmt::Display for LabelerTarget { 98 + /// Format a target for display in the report header. 99 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 100 + match self { 101 + LabelerTarget::Identified { 102 + identifier, 103 + explicit_did, 104 + } => { 105 + let id_str = match identifier { 106 + AtIdentifier::Handle(h) => h.clone(), 107 + AtIdentifier::Did(d) => d.0.clone(), 108 + }; 109 + if explicit_did.is_some() { 110 + write!(f, "{id_str} (with explicit DID)") 111 + } else { 112 + id_str.fmt(f) 113 + } 114 + } 115 + LabelerTarget::Endpoint { url, did } => { 116 + if did.is_some() { 117 + write!(f, "{url} (with explicit DID)") 118 + } else { 119 + url.fmt(f) 120 + } 121 + } 122 + } 123 + } 124 + } 125 + 126 + /// Check if a string is a valid ATProto handle. 127 + /// 128 + /// A valid handle: 129 + /// - Contains at least one dot. 130 + /// - Contains only alphanumeric characters, hyphens, and dots. 131 + /// - Does not start or end with a hyphen or dot. 132 + /// - Has no empty segments (no consecutive dots or leading/trailing dots). 133 + fn is_valid_handle(s: &str) -> bool { 134 + if !s.contains('.') { 135 + return false; 136 + } 137 + 138 + // Check for empty string or leading/trailing special chars. 139 + if s.is_empty() 140 + || s.starts_with('-') 141 + || s.starts_with('.') 142 + || s.ends_with('-') 143 + || s.ends_with('.') 144 + { 145 + return false; 146 + } 147 + 148 + // Check all characters are alphanumeric, hyphen, or dot. 149 + for c in s.chars() { 150 + if !c.is_ascii_alphanumeric() && c != '-' && c != '.' { 151 + return false; 152 + } 153 + } 154 + 155 + // Check no empty segments (no consecutive dots). 156 + if s.contains("..") { 157 + return false; 158 + } 159 + 160 + true 161 + } 162 + 163 + /// Error from target parsing. 164 + #[derive(Debug, Error, Diagnostic)] 165 + #[error("{message}")] 166 + pub struct TargetParseError { 167 + /// The error message. 168 + pub message: String, 169 + } 170 + 171 + impl TargetParseError { 172 + fn new(message: impl Into<String>) -> Self { 173 + Self { 174 + message: message.into(), 175 + } 176 + } 177 + 178 + fn unrecognized_target(raw: &str) -> Self { 179 + Self::new(format!( 180 + "Unrecognized target '{raw}'. Expected one of:\n - ATProto handle (e.g., alice.bsky.social)\n - DID (e.g., did:plc:abc123 or did:web:example.com)\n - HTTPS endpoint URL (e.g., https://labeler.example.com)\n - HTTP endpoint URL with a local hostname (e.g., http://localhost:8080)" 181 + )) 182 + } 183 + 184 + fn http_not_supported(raw: &str) -> Self { 185 + Self::new(format!( 186 + "HTTP endpoint '{raw}' is not supported for remote hosts. Use HTTPS, or point at a local labeler (localhost / 127.0.0.0/8 / RFC 1918 / .local) to allow plaintext HTTP." 187 + )) 188 + } 189 + 190 + fn ambiguous_did(raw: &str, explicit: &str) -> Self { 191 + Self::new(format!( 192 + "Ambiguous target specification: target '{raw}' is already a DID, but --did {explicit} was also provided. Please use only one." 193 + )) 194 + } 195 + } 196 + 197 + #[cfg(test)] 198 + mod tests { 199 + use super::*; 200 + 201 + #[test] 202 + fn parse_target_handle() { 203 + let target = parse("alice.bsky.social", None).expect("should parse"); 204 + match target { 205 + LabelerTarget::Identified { 206 + identifier, 207 + explicit_did, 208 + } => { 209 + assert!( 210 + matches!(identifier, AtIdentifier::Handle(ref h) if h == "alice.bsky.social") 211 + ); 212 + assert!(explicit_did.is_none()); 213 + } 214 + _ => panic!("expected Identified variant"), 215 + } 216 + } 217 + 218 + #[test] 219 + fn parse_target_did_plc() { 220 + let target = parse("did:plc:abc123", None).expect("should parse"); 221 + match target { 222 + LabelerTarget::Identified { 223 + identifier, 224 + explicit_did, 225 + } => { 226 + assert!(matches!(identifier, AtIdentifier::Did(ref d) if d.0 == "did:plc:abc123")); 227 + assert!(explicit_did.is_none()); 228 + } 229 + _ => panic!("expected Identified variant"), 230 + } 231 + } 232 + 233 + #[test] 234 + fn parse_target_did_web() { 235 + let target = parse("did:web:example.com", None).expect("should parse"); 236 + match target { 237 + LabelerTarget::Identified { 238 + identifier, 239 + explicit_did, 240 + } => { 241 + assert!( 242 + matches!(identifier, AtIdentifier::Did(ref d) if d.0 == "did:web:example.com") 243 + ); 244 + assert!(explicit_did.is_none()); 245 + } 246 + _ => panic!("expected Identified variant"), 247 + } 248 + } 249 + 250 + #[test] 251 + fn parse_target_endpoint_https() { 252 + let target = parse("https://example.com/labeler", None).expect("should parse"); 253 + match target { 254 + LabelerTarget::Endpoint { url, did } => { 255 + assert_eq!(url.as_str(), "https://example.com/labeler"); 256 + assert!(did.is_none()); 257 + } 258 + _ => panic!("expected Endpoint variant"), 259 + } 260 + } 261 + 262 + #[test] 263 + fn parse_target_endpoint_with_explicit_did() { 264 + let target = 265 + parse("https://example.com/labeler", Some("did:plc:xyz")).expect("should parse"); 266 + match target { 267 + LabelerTarget::Endpoint { url, did } => { 268 + assert_eq!(url.as_str(), "https://example.com/labeler"); 269 + assert_eq!(did.map(|d| d.0.clone()), Some("did:plc:xyz".to_string())); 270 + } 271 + _ => panic!("expected Endpoint variant"), 272 + } 273 + } 274 + 275 + #[test] 276 + fn parse_target_endpoint_http_remote_rejected() { 277 + let err = parse("http://evil.example", None).expect_err("should reject http"); 278 + assert!(err.message.contains("HTTP")); 279 + assert!(err.message.contains("local")); 280 + } 281 + 282 + #[test] 283 + fn parse_target_endpoint_http_local_accepted() { 284 + // Each of these hostnames is classified as local by 285 + // `is_local_labeler_hostname`, so plaintext HTTP is allowed. 286 + let cases = &[ 287 + "http://localhost:8080", 288 + "http://127.0.0.1:5000", 289 + "http://127.1.2.3/", 290 + "http://[::1]:8080/", 291 + "http://10.0.0.1/", 292 + "http://192.168.1.100:8080", 293 + "http://172.16.0.1/", 294 + "http://mybox.local:8080", 295 + ]; 296 + for raw in cases { 297 + let target = parse(raw, None) 298 + .unwrap_or_else(|e| panic!("expected {raw} to parse, got: {}", e.message)); 299 + match target { 300 + LabelerTarget::Endpoint { url, did } => { 301 + assert_eq!( 302 + url.as_str().trim_end_matches('/'), 303 + raw.trim_end_matches('/') 304 + ); 305 + assert!(did.is_none()); 306 + } 307 + _ => panic!("expected Endpoint variant for {raw}"), 308 + } 309 + } 310 + } 311 + 312 + #[test] 313 + fn parse_target_unrecognised() { 314 + let err = parse("not a handle or did", None).expect_err("should fail"); 315 + assert!(err.message.contains("Unrecognized target")); 316 + } 317 + 318 + #[test] 319 + fn parse_target_did_with_conflicting_flag() { 320 + let err = parse("did:plc:abc", Some("did:web:example.com")) 321 + .expect_err("should reject ambiguous target"); 322 + assert!(err.message.contains("Ambiguous")); 323 + } 324 + }
+7 -7
tests/common/mod.rs
··· 8 8 #![allow(dead_code)] 9 9 10 10 use async_trait::async_trait; 11 - use atproto_devtool::commands::test::labeler::create_report::{ 12 - CreateReportStageError, CreateReportTee, PdsXrpcClient, RawCreateReportResponse, 13 - RawPdsXrpcResponse, 14 - }; 15 - use atproto_devtool::commands::test::labeler::http::{HttpStageError, RawHttpTee, RawXrpcResponse}; 16 - use atproto_devtool::commands::test::labeler::subscription::{ 17 - FrameStream, SubscriptionStageError, WebSocketClient, 11 + use atproto_devtool::commands::test::labeler::pipeline::{ 12 + create_report::{ 13 + CreateReportStageError, CreateReportTee, PdsXrpcClient, RawCreateReportResponse, 14 + RawPdsXrpcResponse, 15 + }, 16 + http::{HttpStageError, RawHttpTee, RawXrpcResponse}, 17 + subscription::{FrameStream, SubscriptionStageError, WebSocketClient}, 18 18 }; 19 19 use reqwest::StatusCode; 20 20 use std::collections::HashMap;
+1 -1
tests/common_fakes.rs
··· 2 2 3 3 mod common; 4 4 5 - use atproto_devtool::commands::test::labeler::create_report::CreateReportTee; 5 + use atproto_devtool::commands::test::labeler::pipeline::create_report::CreateReportTee; 6 6 use common::*; 7 7 use reqwest::StatusCode; 8 8
+29 -17
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 - use atproto_devtool::commands::test::labeler::crypto::canonicalize_label_for_signing; 8 - use atproto_devtool::commands::test::labeler::pipeline::{ 9 - CreateReportTeeKind, HttpTee, LabelerOptions, parse_target, run_pipeline, 6 + use atproto_devtool::{ 7 + commands::test::labeler::{ 8 + pipeline::{ 9 + CreateReportTeeKind, HttpTee, LabelerOptions, create_report::self_mint::SelfMintCurve, 10 + crypto::canonicalize_label_for_signing, run_pipeline, 11 + }, 12 + target, 13 + }, 14 + common::identity::{DnsResolver, HttpClient, IdentityError}, 10 15 }; 11 - use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError}; 12 16 use atrium_api::com::atproto::label::defs::{Label, LabelData}; 13 17 use atrium_api::com::atproto::label::query_labels::{Output, OutputData}; 14 18 use atrium_api::types::string::Datetime; ··· 284 288 labeler_record_json.to_vec(), 285 289 ); 286 290 287 - let target = parse_target(did, None).expect("parse failed"); 291 + let target = target::parse(did, None).expect("parse failed"); 288 292 let fake_tee = common::FakeRawHttpTee::new(); 289 293 fake_tee.add_response(None, 200, labels_response); 290 294 let fake_ws = common::FakeWebSocketClient::empty(); ··· 349 353 serde_json::to_vec(&did_json).unwrap(), 350 354 ); 351 355 352 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 356 + let target = 357 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 353 358 let fake_tee = common::FakeRawHttpTee::new(); 354 359 let fake_ws = common::FakeWebSocketClient::empty(); 355 360 let fake_report_tee = common::FakeCreateReportTee::new(); ··· 404 409 labeler_record_json.to_vec(), 405 410 ); 406 411 407 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 412 + let target = 413 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 408 414 let fake_tee = common::FakeRawHttpTee::new(); 409 415 fake_tee.add_response(None, 200, b"{malformed json".to_vec()); 410 416 let fake_ws = common::FakeWebSocketClient::empty(); ··· 457 463 labeler_record_json.to_vec(), 458 464 ); 459 465 460 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 466 + let target = 467 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 461 468 let fake_tee = common::FakeRawHttpTee::new(); 462 469 fake_tee.add_response(None, 200, br#"{"cursor": null, "labels": []}"#.to_vec()); 463 470 ··· 561 568 serde_json::to_vec(&plc_audit_log).expect("serialize plc audit log"), 562 569 ); 563 570 564 - let target = parse_target(did, None).expect("parse failed"); 571 + let target = target::parse(did, None).expect("parse failed"); 565 572 let fake_tee = common::FakeRawHttpTee::new(); 566 573 fake_tee.add_response(None, 200, labels_response); 567 574 let fake_ws = common::FakeWebSocketClient::empty(); ··· 654 661 serde_json::to_vec(&plc_audit_log).unwrap(), 655 662 ); 656 663 657 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 664 + let target = 665 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 658 666 let fake_tee = common::FakeRawHttpTee::new(); 659 667 fake_tee.add_response(None, 200, labels_response); 660 668 let fake_ws = common::FakeWebSocketClient::empty(); ··· 744 752 labeler_record_json.to_vec(), 745 753 ); 746 754 747 - let target = parse_target("did:web:web-labeler.example", None).expect("parse failed"); 755 + let target = target::parse("did:web:web-labeler.example", None).expect("parse failed"); 748 756 let fake_tee = common::FakeRawHttpTee::new(); 749 757 fake_tee.add_response(None, 200, labels_response); 750 758 let fake_ws = common::FakeWebSocketClient::empty(); ··· 799 807 labeler_record_json.to_vec(), 800 808 ); 801 809 802 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 810 + let target = 811 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 803 812 let fake_tee = common::FakeRawHttpTee::new(); 804 813 fake_tee.add_response(None, 200, labels_response); 805 814 let fake_ws = common::FakeWebSocketClient::empty(); ··· 879 888 "https://plc.directory/did:plc:test123456789abcdefghijklmnop/log/audit", 880 889 ); 881 890 882 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 891 + let target = 892 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 883 893 let fake_tee = common::FakeRawHttpTee::new(); 884 894 fake_tee.add_response(None, 200, labels_response); 885 895 let fake_ws = common::FakeWebSocketClient::empty(); ··· 935 945 labeler_record_json.to_vec(), 936 946 ); 937 947 938 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 948 + let target = 949 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 939 950 let fake_tee = common::FakeRawHttpTee::new(); 940 951 fake_tee.add_response(None, 200, br#"{"cursor": null, "labels": []}"#.to_vec()); 941 952 let fake_ws = common::FakeWebSocketClient::empty(); ··· 995 1006 labeler_record_json.to_vec(), 996 1007 ); 997 1008 998 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 1009 + let target = 1010 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 999 1011 let fake_tee = common::FakeRawHttpTee::new(); 1000 1012 fake_tee.set_transport_error(); // Force HTTP stage transport error. 1001 1013 let fake_ws = common::FakeWebSocketClient::empty(); ··· 1040 1052 let http = FakeHttpClient::new(); 1041 1053 let dns = FakeDnsResolver::new(); 1042 1054 1043 - let target = parse_target("https://labeler.example.com", None).expect("parse failed"); 1055 + let target = target::parse("https://labeler.example.com", None).expect("parse failed"); 1044 1056 let fake_tee = common::FakeRawHttpTee::new(); 1045 1057 fake_tee.add_response(None, 200, br#"{"cursor": null, "labels": []}"#.to_vec()); 1046 1058 let fake_ws = common::FakeWebSocketClient::empty();
+6 -6
tests/labeler_http.rs
··· 2 2 3 3 mod common; 4 4 5 - use atproto_devtool::commands::test::labeler::http::run; 5 + use atproto_devtool::commands::test::labeler::pipeline::http; 6 6 use common::FakeRawHttpTee; 7 7 8 8 /// Helper to render a report to a string for snapshot testing. ··· 29 29 tee.add_response(None, 200, first_page); 30 30 tee.add_response(Some("cursor1"), 200, second_page); 31 31 32 - let output = run(&tee).await; 32 + let output = http::run(&tee).await; 33 33 34 34 // Build a minimal report for snapshot testing. 35 35 let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( ··· 57 57 58 58 tee.add_response(None, 200, empty_page); 59 59 60 - let output = run(&tee).await; 60 + let output = http::run(&tee).await; 61 61 62 62 let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 63 63 atproto_devtool::commands::test::labeler::report::ReportHeader { ··· 84 84 85 85 tee.add_response(None, 200, malformed.clone()); 86 86 87 - let output = run(&tee).await; 87 + let output = http::run(&tee).await; 88 88 89 89 let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 90 90 atproto_devtool::commands::test::labeler::report::ReportHeader { ··· 116 116 tee.add_response(None, 200, first_page); 117 117 tee.add_response(Some("cursor1"), 200, second_page); 118 118 119 - let output = run(&tee).await; 119 + let output = http::run(&tee).await; 120 120 121 121 let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 122 122 atproto_devtool::commands::test::labeler::report::ReportHeader { ··· 141 141 let tee = FakeRawHttpTee::new(); 142 142 tee.set_transport_error(); 143 143 144 - let output = run(&tee).await; 144 + let output = http::run(&tee).await; 145 145 146 146 let mut report = atproto_devtool::commands::test::labeler::report::LabelerReport::new( 147 147 atproto_devtool::commands::test::labeler::report::ReportHeader {
+27 -19
tests/labeler_identity.rs
··· 2 2 3 3 mod common; 4 4 5 - use async_trait::async_trait; 6 - use atproto_devtool::commands::test::labeler::create_report::self_mint::SelfMintCurve; 7 - use atproto_devtool::commands::test::labeler::pipeline::{ 8 - CreateReportTeeKind, HttpTee, LabelerOptions, parse_target, run_pipeline, 9 - }; 10 - use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError}; 11 5 use std::collections::HashMap; 12 6 use std::sync::{Arc, Mutex}; 13 7 8 + use async_trait::async_trait; 9 + use atproto_devtool::{ 10 + commands::test::labeler::{ 11 + pipeline::{ 12 + CreateReportTeeKind, HttpTee, LabelerOptions, create_report::self_mint::SelfMintCurve, 13 + run_pipeline, 14 + }, 15 + target, 16 + }, 17 + common::identity::{DnsResolver, HttpClient, IdentityError}, 18 + }; 14 19 use url::Url; 15 20 16 21 /// Type alias for the response map in FakeHttpClient. ··· 143 148 labeler_record_json, 144 149 ); 145 150 146 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 151 + let target = 152 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 147 153 let fake_tee = common::FakeRawHttpTee::new(); 148 154 fake_tee.add_response(None, 200, healthy_labels_response()); 149 155 let fake_ws = common::FakeWebSocketClient::empty(); ··· 179 185 let http = FakeHttpClient::new(); 180 186 let dns = FakeDnsResolver::new(); 181 187 182 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 188 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 183 189 let fake_tee = common::FakeRawHttpTee::new(); 184 190 let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec(); 185 191 fake_tee.add_response(None, 200, empty_response); ··· 240 246 labeler_record_json, 241 247 ); 242 248 243 - let target = parse_target("alice.example", None).expect("parse failed"); 249 + let target = target::parse("alice.example", None).expect("parse failed"); 244 250 let fake_tee = common::FakeRawHttpTee::new(); 245 251 let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec(); 246 252 fake_tee.add_response(None, 200, empty_response); ··· 295 301 labeler_record_json, 296 302 ); 297 303 298 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 304 + let target = 305 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 299 306 let fake_tee = common::FakeRawHttpTee::new(); 300 307 fake_tee.add_response(None, 200, healthy_labels_response()); 301 308 let fake_ws = common::FakeWebSocketClient::empty(); ··· 348 355 labeler_record_json, 349 356 ); 350 357 351 - let target = parse_target("did:web:web-labeler.example", None).expect("parse failed"); 358 + let target = target::parse("did:web:web-labeler.example", None).expect("parse failed"); 352 359 let fake_tee = common::FakeRawHttpTee::new(); 353 360 let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec(); 354 361 fake_tee.add_response(None, 200, empty_response); ··· 388 395 389 396 // Don't add any response for PLC directory or DNS resolver - causes network error. 390 397 391 - let target = parse_target("alice.test", None).expect("parse failed"); 398 + let target = target::parse("alice.test", None).expect("parse failed"); 392 399 let fake_tee = common::FakeRawHttpTee::new(); 393 400 let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec(); 394 401 fake_tee.add_response(None, 200, empty_response); ··· 441 448 b"Not found".to_vec(), 442 449 ); 443 450 444 - let target = parse_target("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 451 + let target = 452 + target::parse("did:plc:test123456789abcdefghijklmnop", None).expect("parse failed"); 445 453 let fake_tee = common::FakeRawHttpTee::new(); 446 454 fake_tee.add_response(None, 200, healthy_labels_response()); 447 455 let fake_ws = common::FakeWebSocketClient::empty(); ··· 495 503 ); 496 504 497 505 let target = 498 - parse_target("did:plc:missing_service_test_123456789", None).expect("parse failed"); 506 + target::parse("did:plc:missing_service_test_123456789", None).expect("parse failed"); 499 507 let fake_tee = common::FakeRawHttpTee::new(); 500 508 let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec(); 501 509 fake_tee.add_response(None, 200, empty_response); ··· 553 561 ); 554 562 555 563 let target = 556 - parse_target("did:plc:missing_signing_key_test_12345", None).expect("parse failed"); 564 + target::parse("did:plc:missing_signing_key_test_12345", None).expect("parse failed"); 557 565 let fake_tee = common::FakeRawHttpTee::new(); 558 566 let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec(); 559 567 fake_tee.add_response(None, 200, empty_response); ··· 609 617 ); 610 618 611 619 let target = 612 - parse_target("did:plc:non_https_endpoint_test_123456", None).expect("parse failed"); 620 + target::parse("did:plc:non_https_endpoint_test_123456", None).expect("parse failed"); 613 621 let fake_tee = common::FakeRawHttpTee::new(); 614 622 let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec(); 615 623 fake_tee.add_response(None, 200, empty_response); ··· 665 673 ); 666 674 667 675 let target = 668 - parse_target("did:plc:empty_policies_test_123456789ab", None).expect("parse failed"); 676 + target::parse("did:plc:empty_policies_test_123456789ab", None).expect("parse failed"); 669 677 let fake_tee = common::FakeRawHttpTee::new(); 670 678 let empty_response = br#"{"cursor":null,"labels":[]}"#.to_vec(); 671 679 fake_tee.add_response(None, 200, empty_response); ··· 721 729 ); 722 730 723 731 // The target is the mismatched URL; the DID says a different endpoint. 724 - let target = parse_target( 732 + let target = target::parse( 725 733 "https://other-labeler.example/", 726 734 Some("did:plc:endpoint_mismatch_test_123456789"), 727 735 ) ··· 791 799 ); 792 800 793 801 // Target is a local labeler copy; the DID document points somewhere else. 794 - let target = parse_target( 802 + let target = target::parse( 795 803 "http://localhost:8080", 796 804 Some("did:plc:endpoint_mismatch_test_123456789"), 797 805 )
+19 -14
tests/labeler_report.rs
··· 11 11 use std::sync::Arc; 12 12 use std::time::Duration; 13 13 14 - use atproto_devtool::commands::test::labeler::create_report; 15 - use atproto_devtool::commands::test::labeler::create_report::Check; 16 - use atproto_devtool::commands::test::labeler::create_report::self_mint::{ 17 - SelfMintCurve, SelfMintSigner, 18 - }; 19 - use atproto_devtool::commands::test::labeler::identity::IdentityFacts; 20 - use atproto_devtool::commands::test::labeler::pipeline::{ 21 - CreateReportTeeKind, HttpTee, LabelerOptions, parse_target, run_pipeline, 14 + use atproto_devtool::{ 15 + commands::test::labeler::{ 16 + report::{CheckResult, CheckStatus, LabelerReport, RenderConfig, ReportHeader}, 17 + { 18 + pipeline::{ 19 + CreateReportTeeKind, HttpTee, LabelerOptions, 20 + create_report::{ 21 + self, 22 + self_mint::{SelfMintCurve, SelfMintSigner}, 23 + }, 24 + identity::IdentityFacts, 25 + run_pipeline, 26 + }, 27 + target, 28 + }, 29 + }, 30 + common::identity::{Did, DnsResolver, HttpClient, IdentityError}, 22 31 }; 23 - use atproto_devtool::commands::test::labeler::report::{ 24 - CheckResult, CheckStatus, LabelerReport, RenderConfig, ReportHeader, 25 - }; 26 - use atproto_devtool::common::identity::{Did, DnsResolver, HttpClient, IdentityError}; 27 32 28 33 use atrium_api::app::bsky::labeler::defs::LabelerPolicies; 29 34 ··· 478 483 let fake_create_report_tee = common::FakeCreateReportTee::new(); 479 484 480 485 // Use endpoint target (no identity stage needed). 481 - let target = parse_target("https://labeler.example.com", None).expect("parse endpoint target"); 486 + let target = target::parse("https://labeler.example.com", None).expect("parse endpoint target"); 482 487 483 488 let opts = LabelerOptions { 484 489 http: &StubHttpClient, ··· 1522 1527 "AC7.1 failed: expected 10 rows with commit={commit}, force={force}, pds={pds}" 1523 1528 ); 1524 1529 // Verify Check::ORDER is respected. 1525 - let expected_ids: Vec<_> = Check::ORDER.iter().map(|c| c.id()).collect(); 1530 + let expected_ids: Vec<_> = create_report::Check::ORDER.iter().map(|c| c.id()).collect(); 1526 1531 let actual_ids: Vec<_> = results.iter().map(|r| r.id).collect(); 1527 1532 assert_eq!(actual_ids, expected_ids, "Check order mismatch"); 1528 1533 }
+21 -14
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 - use atproto_devtool::commands::test::labeler::pipeline::{ 12 - CreateReportTeeKind, HttpTee, LabelerOptions, parse_target, run_pipeline, 13 - }; 14 - use atproto_devtool::commands::test::labeler::subscription::{FrameHeader, SubscribeLabelsPayload}; 15 - use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError}; 16 10 use std::collections::HashMap; 17 11 use std::sync::{Arc, Mutex}; 18 12 use std::time::Duration; 13 + 14 + use atproto_devtool::{ 15 + commands::test::labeler::{ 16 + pipeline::{ 17 + CreateReportTeeKind, HttpTee, LabelerOptions, 18 + create_report::self_mint::SelfMintCurve, 19 + run_pipeline, 20 + subscription::{FrameHeader, SubscribeLabelsPayload}, 21 + }, 22 + target, 23 + }, 24 + common::identity::{DnsResolver, HttpClient, IdentityError}, 25 + }; 19 26 use url::Url; 20 27 21 28 /// Type alias for the response map in FakeHttpClient. ··· 284 291 let http = FakeHttpClient::new(); 285 292 let dns = FakeDnsResolver::new(); 286 293 let fake_tee = make_passing_http_tee(); 287 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 294 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 288 295 289 296 let opts = LabelerOptions { 290 297 http: &http, ··· 343 350 let http = FakeHttpClient::new(); 344 351 let dns = FakeDnsResolver::new(); 345 352 let fake_tee = make_passing_http_tee(); 346 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 353 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 347 354 348 355 let opts = LabelerOptions { 349 356 http: &http, ··· 388 395 let http = FakeHttpClient::new(); 389 396 let dns = FakeDnsResolver::new(); 390 397 let fake_tee = make_passing_http_tee(); 391 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 398 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 392 399 393 400 let opts = LabelerOptions { 394 401 http: &http, ··· 444 451 let http = FakeHttpClient::new(); 445 452 let dns = FakeDnsResolver::new(); 446 453 let fake_tee = make_passing_http_tee(); 447 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 454 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 448 455 449 456 let opts = LabelerOptions { 450 457 http: &http, ··· 501 508 let http = FakeHttpClient::new(); 502 509 let dns = FakeDnsResolver::new(); 503 510 let fake_tee = make_passing_http_tee(); 504 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 511 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 505 512 506 513 let opts = LabelerOptions { 507 514 http: &http, ··· 545 552 let http = FakeHttpClient::new(); 546 553 let dns = FakeDnsResolver::new(); 547 554 let fake_tee = make_passing_http_tee(); 548 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 555 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 549 556 550 557 let opts = LabelerOptions { 551 558 http: &http, ··· 601 608 let http = FakeHttpClient::new(); 602 609 let dns = FakeDnsResolver::new(); 603 610 let fake_tee = make_passing_http_tee(); 604 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 611 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 605 612 606 613 let opts = LabelerOptions { 607 614 http: &http, ··· 668 675 let http = FakeHttpClient::new(); 669 676 let dns = FakeDnsResolver::new(); 670 677 let fake_tee = make_passing_http_tee(); 671 - let target = parse_target("https://example.com/labeler", None).expect("parse failed"); 678 + let target = target::parse("https://example.com/labeler", None).expect("parse failed"); 672 679 673 680 let opts = LabelerOptions { 674 681 http: &http,