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 JWKS stage into pipeline

Adds the jwks field to OauthClientOptions and wires the JWKS validation
stage into the pipeline after the metadata stage. Constructs RealJwksFetcher
in ClientCmd::run and passes it through the options.

Updates existing tests to provide FakeJwksFetcher instances.

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

+129 -2
+6 -1
src/commands/test/oauth/client.rs
··· 80 80 .map_err(|e| miette::miette!("Failed to initialize HTTP client: {e}"))?; 81 81 82 82 // Build HTTP client using the shared client. 83 - let http = RealHttpClient::from_client(reqwest_client); 83 + let http = RealHttpClient::from_client(reqwest_client.clone()); 84 + 85 + // Build JWKS fetcher using the shared client. 86 + let jwks_fetcher = pipeline::jwks::RealJwksFetcher::new(reqwest_client); 84 87 85 88 // Combine CLI and command-level no_color flags. 86 89 let combined_no_color = no_color || self.no_color; ··· 91 94 // Static mode. 92 95 let opts = pipeline::OauthClientOptions { 93 96 http: &http, 97 + jwks: &jwks_fetcher, 94 98 verbose: self.verbose, 95 99 }; 96 100 ··· 119 123 120 124 let opts = pipeline::OauthClientOptions { 121 125 http: &http, 126 + jwks: &jwks_fetcher, 122 127 verbose: self.verbose, 123 128 }; 124 129
+12 -1
src/commands/test/oauth/client/pipeline.rs
··· 269 269 pub struct OauthClientOptions<'a> { 270 270 /// The HTTP client for network requests. 271 271 pub http: &'a dyn HttpClient, 272 + /// The JWKS fetcher for external JWKS documents. 273 + pub jwks: &'a dyn jwks::JwksFetcher, 272 274 /// Whether to emit verbose diagnostics. 273 275 pub verbose: bool, 274 276 } ··· 356 358 report.record(result); 357 359 } 358 360 359 - // TODO: Phase 5 will use metadata_output.facts to drive JWKS stage. 361 + // Run JWKS stage (consumes metadata facts). 362 + let jwks_output = if let Some(metadata_facts) = metadata_output.facts { 363 + jwks::run(&metadata_facts, opts.jwks, "<jwks>").await 364 + } else { 365 + jwks::emit_all_blocked_by("oauth_client::metadata::raw_document_deserializes").await 366 + }; 367 + for result in jwks_output.results { 368 + report.record(result); 369 + } 370 + let _jwks_facts = jwks_output.facts; // Used by Phase 7 interactive stage 360 371 361 372 // Mark the report as finished. 362 373 report.finish();
+75
tests/common/mod.rs
··· 12 12 use atproto_devtool::commands::test::labeler::subscription::{ 13 13 FrameStream, SubscriptionStageError, WebSocketClient, 14 14 }; 15 + use atproto_devtool::commands::test::oauth::client::pipeline::jwks::{ 16 + JwksFetchError, JwksFetchResponse, JwksFetcher, 17 + }; 15 18 use atproto_devtool::common::identity::{DnsResolver, HttpClient, IdentityError}; 16 19 use std::collections::{HashMap, HashSet}; 17 20 use std::sync::{Arc, Mutex}; ··· 26 29 27 30 /// Type alias for DNS records map in FakeDnsResolver. 28 31 type FakeDnsRecords = Arc<Mutex<HashMap<String, Vec<String>>>>; 32 + 33 + /// Type alias for JWKS responses in FakeJwksFetcher. 34 + type FakeJwksResponses = 35 + Arc<Mutex<HashMap<String, Result<(u16, Vec<u8>, Option<String>), String>>>>; 29 36 30 37 /// Fake HTTP tee for testing, returns pre-defined responses. 31 38 pub struct FakeRawHttpTee { ··· 441 448 } 442 449 result 443 450 } 451 + 452 + /// Fake JWKS fetcher for testing JWKS validation stage. 453 + /// 454 + /// Seeded with responses per URL. Panics if a URL is requested that wasn't seeded, 455 + /// to catch test-author mistakes. Tests must explicitly seed every URL they expect 456 + /// the pipeline to fetch via `add_response` or `add_transport_error`. 457 + pub struct FakeJwksFetcher { 458 + responses: FakeJwksResponses, 459 + } 460 + 461 + impl FakeJwksFetcher { 462 + /// Create a new FakeJwksFetcher. 463 + pub fn new() -> Self { 464 + Self { 465 + responses: Arc::new(Mutex::new(HashMap::new())), 466 + } 467 + } 468 + 469 + /// Add a successful response for the given URL. 470 + pub fn add_response( 471 + &self, 472 + url: &Url, 473 + status: u16, 474 + body: Vec<u8>, 475 + content_type: Option<String>, 476 + ) { 477 + self.responses 478 + .lock() 479 + .unwrap() 480 + .insert(url.as_str().to_string(), Ok((status, body, content_type))); 481 + } 482 + 483 + /// Add a transport error for the given URL. 484 + pub fn add_transport_error(&self, url: &Url, message: impl Into<String>) { 485 + self.responses 486 + .lock() 487 + .unwrap() 488 + .insert(url.as_str().to_string(), Err(message.into())); 489 + } 490 + } 491 + 492 + impl Default for FakeJwksFetcher { 493 + fn default() -> Self { 494 + Self::new() 495 + } 496 + } 497 + 498 + #[async_trait] 499 + impl JwksFetcher for FakeJwksFetcher { 500 + async fn fetch(&self, url: &Url) -> Result<JwksFetchResponse, JwksFetchError> { 501 + let url_str = url.as_str(); 502 + match self.responses.lock().unwrap().get(url_str) { 503 + Some(Ok((status, body, content_type))) => Ok(JwksFetchResponse { 504 + status: *status, 505 + body: body.clone(), 506 + content_type: content_type.clone(), 507 + }), 508 + Some(Err(message)) => Err(JwksFetchError { 509 + url: url.clone(), 510 + message: message.clone(), 511 + }), 512 + None => panic!( 513 + "FakeJwksFetcher: no response seeded for URL {url_str}. Tests must seed every URL \ 514 + they expect the pipeline to fetch with add_response or add_transport_error." 515 + ), 516 + } 517 + } 518 + }
+16
tests/oauth_client_discovery.rs
··· 30 30 ); 31 31 32 32 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 33 + let jwks_fetcher = common::FakeJwksFetcher::new(); 33 34 let opts = OauthClientOptions { 34 35 http: &http, 36 + jwks: &jwks_fetcher, 35 37 verbose: false, 36 38 }; 37 39 ··· 52 54 ); 53 55 54 56 let target = parse_target("https://client.example.com/missing.json").expect("parse failed"); 57 + let jwks_fetcher = common::FakeJwksFetcher::new(); 55 58 let opts = OauthClientOptions { 56 59 http: &http, 60 + jwks: &jwks_fetcher, 57 61 verbose: false, 58 62 }; 59 63 ··· 71 75 http.add_transport_error(&url); 72 76 73 77 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 78 + let jwks_fetcher = common::FakeJwksFetcher::new(); 74 79 let opts = OauthClientOptions { 75 80 http: &http, 81 + jwks: &jwks_fetcher, 76 82 verbose: false, 77 83 }; 78 84 ··· 94 100 ); 95 101 96 102 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 103 + let jwks_fetcher = common::FakeJwksFetcher::new(); 97 104 let opts = OauthClientOptions { 98 105 http: &http, 106 + jwks: &jwks_fetcher, 99 107 verbose: false, 100 108 }; 101 109 ··· 119 127 ); 120 128 121 129 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 130 + let jwks_fetcher = common::FakeJwksFetcher::new(); 122 131 let opts = OauthClientOptions { 123 132 http: &http, 133 + jwks: &jwks_fetcher, 124 134 verbose: false, 125 135 }; 126 136 ··· 136 146 let http = common::FakeHttpClient::new(); 137 147 138 148 let target = parse_target("http://localhost").expect("parse failed"); 149 + let jwks_fetcher = common::FakeJwksFetcher::new(); 139 150 let opts = OauthClientOptions { 140 151 http: &http, 152 + jwks: &jwks_fetcher, 141 153 verbose: false, 142 154 }; 143 155 ··· 153 165 let http = common::FakeHttpClient::new(); 154 166 155 167 let target = parse_target("http://localhost:8080/client.json").expect("parse failed"); 168 + let jwks_fetcher = common::FakeJwksFetcher::new(); 156 169 let opts = OauthClientOptions { 157 170 http: &http, 171 + jwks: &jwks_fetcher, 158 172 verbose: false, 159 173 }; 160 174 ··· 170 184 let http = common::FakeHttpClient::new(); 171 185 172 186 let target = parse_target("http://127.0.0.1:3000/").expect("parse failed"); 187 + let jwks_fetcher = common::FakeJwksFetcher::new(); 173 188 let opts = OauthClientOptions { 174 189 http: &http, 190 + jwks: &jwks_fetcher, 175 191 verbose: false, 176 192 }; 177 193
+20
tests/oauth_client_metadata.rs
··· 33 33 ); 34 34 35 35 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 36 + let jwks_fetcher = common::FakeJwksFetcher::new(); 36 37 let opts = OauthClientOptions { 37 38 http: &http, 39 + jwks: &jwks_fetcher, 38 40 verbose: false, 39 41 }; 40 42 ··· 60 62 ); 61 63 62 64 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 65 + let jwks_fetcher = common::FakeJwksFetcher::new(); 63 66 let opts = OauthClientOptions { 64 67 http: &http, 68 + jwks: &jwks_fetcher, 65 69 verbose: false, 66 70 }; 67 71 ··· 88 92 89 93 let target = 90 94 parse_target("https://app.example.com/oauth-client-metadata.json").expect("parse failed"); 95 + let jwks_fetcher = common::FakeJwksFetcher::new(); 91 96 let opts = OauthClientOptions { 92 97 http: &http, 98 + jwks: &jwks_fetcher, 93 99 verbose: false, 94 100 }; 95 101 ··· 115 121 ); 116 122 117 123 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 124 + let jwks_fetcher = common::FakeJwksFetcher::new(); 118 125 let opts = OauthClientOptions { 119 126 http: &http, 127 + jwks: &jwks_fetcher, 120 128 verbose: false, 121 129 }; 122 130 ··· 143 151 ); 144 152 145 153 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 154 + let jwks_fetcher = common::FakeJwksFetcher::new(); 146 155 let opts = OauthClientOptions { 147 156 http: &http, 157 + jwks: &jwks_fetcher, 148 158 verbose: false, 149 159 }; 150 160 ··· 172 182 ); 173 183 174 184 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 185 + let jwks_fetcher = common::FakeJwksFetcher::new(); 175 186 let opts = OauthClientOptions { 176 187 http: &http, 188 + jwks: &jwks_fetcher, 177 189 verbose: false, 178 190 }; 179 191 ··· 202 214 203 215 let target = 204 216 parse_target("https://app.example.com/oauth-client-metadata.json").expect("parse failed"); 217 + let jwks_fetcher = common::FakeJwksFetcher::new(); 205 218 let opts = OauthClientOptions { 206 219 http: &http, 220 + jwks: &jwks_fetcher, 207 221 verbose: false, 208 222 }; 209 223 ··· 230 244 ); 231 245 232 246 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 247 + let jwks_fetcher = common::FakeJwksFetcher::new(); 233 248 let opts = OauthClientOptions { 234 249 http: &http, 250 + jwks: &jwks_fetcher, 235 251 verbose: false, 236 252 }; 237 253 ··· 252 268 // No seeded responses needed for loopback 253 269 254 270 let target = parse_target("http://localhost/").expect("parse failed"); 271 + let jwks_fetcher = common::FakeJwksFetcher::new(); 255 272 let opts = OauthClientOptions { 256 273 http: &http, 274 + jwks: &jwks_fetcher, 257 275 verbose: false, 258 276 }; 259 277 ··· 279 297 ); 280 298 281 299 let target = parse_target("https://client.example.com/metadata.json").expect("parse failed"); 300 + let jwks_fetcher = common::FakeJwksFetcher::new(); 282 301 let opts = OauthClientOptions { 283 302 http: &http, 303 + jwks: &jwks_fetcher, 284 304 verbose: false, 285 305 }; 286 306