CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

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

feat(oauth-client): wire pipeline runner and ClientCmd::run to discovery stage

Implement Task 3 of Phase 3: complete the OAuth client command wiring by adding
the pipeline orchestrator and hooking ClientCmd::run to execute the discovery
stage and render results.

Adds to pipeline.rs:
- OauthClientOptions: carries http client and verbose flag for pipeline stages
- OauthClientReport: newtype wrapping LabelerReport, maintaining compatibility
- run_pipeline: async orchestrator that runs discovery stage, records all
results, and returns a report ready for rendering

Updates ClientCmd::run to:
- Construct shared reqwest client with rustls + 10s timeout + USER_AGENT
- Parse target via pipeline::parse_target (surfacing diagnostic errors)
- Build RealHttpClient from shared client
- OR-combine CLI and command-level --no-color flags
- Execute pipeline and render report to stdout
- Handle interactive mode placeholder (prints TODO, proceeds as static)
- Return exit code from report (0 for pass, 1 for spec violation, 2 for
network error)

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

+164 -6
+71 -6
src/commands/test/oauth/client.rs
··· 63 63 } 64 64 65 65 impl ClientCmd { 66 - pub async fn run(self, _no_color: bool) -> Result<ExitCode, Report> { 67 - let target = &self.target; 66 + pub async fn run(self, no_color: bool) -> Result<ExitCode, Report> { 67 + use crate::common::report::RenderConfig; 68 + use crate::common::{APP_USER_AGENT, identity::RealHttpClient}; 69 + use std::io; 70 + 71 + // Parse the target. 72 + let target = pipeline::parse_target(&self.target).map_err(|e| miette::miette!("{e}"))?; 73 + 74 + // Build a single shared HTTP client. 75 + let reqwest_client = reqwest::Client::builder() 76 + .use_rustls_tls() 77 + .user_agent(APP_USER_AGENT) 78 + .timeout(std::time::Duration::from_secs(10)) 79 + .build() 80 + .map_err(|e| miette::miette!("Failed to initialize HTTP client: {e}"))?; 81 + 82 + // Build HTTP client using the shared client. 83 + let http = RealHttpClient::from_client(reqwest_client); 84 + 85 + // Combine CLI and command-level no_color flags. 86 + let combined_no_color = no_color || self.no_color; 87 + 88 + // Handle mode selection. 68 89 match self.mode { 69 90 None => { 70 - println!("test oauth client: not implemented yet (target: {target})"); 71 - Ok(ExitCode::from(0)) 91 + // Static mode. 92 + let opts = pipeline::OauthClientOptions { 93 + http: &http, 94 + verbose: self.verbose, 95 + }; 96 + 97 + let report = pipeline::run_pipeline(target, opts).await; 98 + 99 + // Render the report to stdout. 100 + let mut stdout = io::stdout().lock(); 101 + report 102 + .render( 103 + &mut stdout, 104 + &RenderConfig { 105 + no_color: combined_no_color, 106 + }, 107 + ) 108 + .map_err(|e| miette::miette!("Failed to render report: {e}"))?; 109 + 110 + // Return appropriate exit code. 111 + let exit_code = report.exit_code(); 112 + Ok(ExitCode::from(exit_code as u8)) 72 113 } 73 114 Some(ClientMode::Interactive(_args)) => { 74 - println!("test oauth client interactive: not implemented yet (target: {target})"); 75 - Ok(ExitCode::from(0)) 115 + // For now, print a placeholder and proceed as static mode. 116 + println!( 117 + "interactive mode: static stages run, interactive stage not yet implemented (Phase 6-8)" 118 + ); 119 + 120 + let opts = pipeline::OauthClientOptions { 121 + http: &http, 122 + verbose: self.verbose, 123 + }; 124 + 125 + let report = pipeline::run_pipeline(target, opts).await; 126 + 127 + // Render the report to stdout. 128 + let mut stdout = io::stdout().lock(); 129 + report 130 + .render( 131 + &mut stdout, 132 + &RenderConfig { 133 + no_color: combined_no_color, 134 + }, 135 + ) 136 + .map_err(|e| miette::miette!("Failed to render report: {e}"))?; 137 + 138 + // Return appropriate exit code. 139 + let exit_code = report.exit_code(); 140 + Ok(ExitCode::from(exit_code as u8)) 76 141 } 77 142 } 78 143 }
+93
src/commands/test/oauth/client/pipeline.rs
··· 176 176 } 177 177 } 178 178 179 + // Pipeline runner and report types for Task 3. 180 + 181 + use crate::common::identity::HttpClient; 182 + use crate::common::report::{LabelerReport, ReportHeader}; 183 + 184 + /// Options passed to the pipeline runner. 185 + pub struct OauthClientOptions<'a> { 186 + /// The HTTP client for network requests. 187 + pub http: &'a dyn HttpClient, 188 + /// Whether to emit verbose diagnostics. 189 + pub verbose: bool, 190 + } 191 + 192 + /// Report wrapper for OAuth client conformance tests. 193 + pub struct OauthClientReport(LabelerReport); 194 + 195 + impl OauthClientReport { 196 + /// Create a new report. 197 + pub fn new(header: ReportHeader) -> Self { 198 + OauthClientReport(LabelerReport::new(header)) 199 + } 200 + 201 + /// Record a check result. 202 + pub fn record(&mut self, result: crate::common::report::CheckResult) { 203 + self.0.record(result); 204 + } 205 + 206 + /// Mark the report as finished. 207 + pub fn finish(&mut self) { 208 + self.0.finish(); 209 + } 210 + 211 + /// Get the exit code for this report. 212 + pub fn exit_code(&self) -> i32 { 213 + self.0.exit_code() 214 + } 215 + 216 + /// Render the report to a writer. 217 + pub fn render<W: std::io::Write>( 218 + &self, 219 + out: &mut W, 220 + cfg: &crate::common::report::RenderConfig, 221 + ) -> std::io::Result<()> { 222 + self.0.render(out, cfg) 223 + } 224 + } 225 + 226 + /// Run the full OAuth client pipeline. 227 + #[tracing::instrument(level = "debug", skip(opts))] 228 + pub async fn run_pipeline( 229 + target: OauthClientTarget, 230 + opts: OauthClientOptions<'_>, 231 + ) -> OauthClientReport { 232 + // Build report header. 233 + let target_str = match &target { 234 + OauthClientTarget::HttpsUrl(url) => url.to_string(), 235 + OauthClientTarget::Loopback(lt) => { 236 + let host_str = match lt.host { 237 + LoopbackHost::Localhost => "localhost", 238 + LoopbackHost::Loopback127 => "127.0.0.1", 239 + }; 240 + if let Some(port) = lt.port { 241 + format!("http://{}:{}{}", host_str, port, lt.path) 242 + } else { 243 + format!("http://{}{}", host_str, lt.path) 244 + } 245 + } 246 + }; 247 + 248 + let header = ReportHeader { 249 + target: target_str, 250 + resolved_did: None, 251 + pds_endpoint: None, 252 + labeler_endpoint: None, 253 + }; 254 + 255 + let mut report = OauthClientReport::new(header); 256 + 257 + // Run discovery stage. 258 + let discovery_output = discovery::run(&target, opts.http).await; 259 + for result in discovery_output.results { 260 + report.record(result); 261 + } 262 + 263 + // Stash facts for future stages (Phase 4+). 264 + let _discovery_facts = discovery_output.facts; 265 + 266 + // Mark the report as finished. 267 + report.finish(); 268 + 269 + report 270 + } 271 + 179 272 #[cfg(test)] 180 273 mod tests { 181 274 use super::*;