#![expect(
clippy::unwrap_used,
reason = "tests do not need to meet production lint standards"
)]
use async_trait::async_trait;
use loopauth::{
CliTokenClient, PageContext, RequestScope, SuccessPageRenderer, test_support::FakeOAuthServer,
};
use std::sync::{Arc, Mutex};
struct ScopeCapturingRenderer {
captured_scopes: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl SuccessPageRenderer for ScopeCapturingRenderer {
async fn render_success(&self, ctx: &PageContext<'_>) -> String {
let scopes: Vec<String> = ctx.scopes().iter().map(ToString::to_string).collect();
*self.captured_scopes.lock().unwrap() = scopes;
"ok".to_string()
}
}
#[tokio::test]
async fn page_context_shows_response_granted_scopes_not_builder_scopes() {
let fake = FakeOAuthServer::start_with_scope("tok", "read").await;
tokio::task::yield_now().await;
let captured_scopes: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
let renderer_scopes = Arc::clone(&captured_scopes);
let (url_tx, url_rx) = std::sync::mpsc::channel::<String>();
tokio::spawn(async move {
if let Ok(url) = url_rx.recv() {
let _ = reqwest::get(url).await;
}
});
let client = CliTokenClient::builder()
.client_id("test-client")
.auth_url(fake.auth_url())
.token_url(fake.token_url())
.add_scopes([
RequestScope::Custom("read".into()),
RequestScope::Custom("write".into()),
])
.open_browser(false)
.success_renderer(ScopeCapturingRenderer {
captured_scopes: renderer_scopes,
})
.on_url(move |url| {
let _ = url_tx.send(url.to_string());
})
.build();
let tokens = client.run_authorization_flow().await.unwrap();
let token_scopes: Vec<String> = tokens.scopes().iter().map(ToString::to_string).collect();
assert_eq!(
token_scopes,
vec!["read"],
"TokenSet should have response-granted scopes"
);
let page_scopes = captured_scopes.lock().unwrap().clone();
assert_eq!(
page_scopes,
vec!["read"],
"PageContext should show response-granted scopes, not builder-configured scopes"
);
}
#[tokio::test]
async fn page_context_falls_back_to_requested_scopes_when_response_omits_scope() {
let fake = FakeOAuthServer::start("tok").await;
tokio::task::yield_now().await;
let captured_scopes: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(vec![]));
let renderer_scopes = Arc::clone(&captured_scopes);
let (url_tx, url_rx) = std::sync::mpsc::channel::<String>();
tokio::spawn(async move {
if let Ok(url) = url_rx.recv() {
let _ = reqwest::get(url).await;
}
});
let client = CliTokenClient::builder()
.client_id("test-client")
.auth_url(fake.auth_url())
.token_url(fake.token_url())
.add_scopes([
RequestScope::Custom("read".into()),
RequestScope::Custom("write".into()),
])
.open_browser(false)
.success_renderer(ScopeCapturingRenderer {
captured_scopes: renderer_scopes,
})
.on_url(move |url| {
let _ = url_tx.send(url.to_string());
})
.build();
let tokens = client.run_authorization_flow().await.unwrap();
let token_scopes: Vec<String> = tokens.scopes().iter().map(ToString::to_string).collect();
assert!(
token_scopes.contains(&"read".to_string()),
"TokenSet should fall back to requested scopes"
);
assert!(
token_scopes.contains(&"write".to_string()),
"TokenSet should fall back to requested scopes"
);
let page_scopes = captured_scopes.lock().unwrap().clone();
assert_eq!(
page_scopes, token_scopes,
"PageContext scopes should match TokenSet scopes"
);
}