use std::time::Duration;
use grr_cli::core::{GoogleAuth, GrrConfig, GrrError, TokenStorage};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn expired_storage_with_refresh_token() -> TokenStorage {
TokenStorage {
access_token: "stale-access-token".into(),
refresh_token: Some("stale-refresh-token".into()),
expires_at: 1, token_type: "Bearer".into(),
scope: "test-scope".into(),
}
}
#[tokio::test]
async fn rejected_refresh_token_returns_auth_error_without_oauth_flow() {
let server = MockServer::start().await;
let guard = Mock::given(method("POST"))
.and(path("/token"))
.respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({
"error": "invalid_grant",
"error_description": "Token has been expired or revoked."
})))
.expect(1)
.mount_as_scoped(&server)
.await;
let config = GrrConfig::default();
let auth = GoogleAuth::with_token(config.oauth.clone(), expired_storage_with_refresh_token())
.await
.unwrap()
.with_token_endpoint(format!("{}/token", server.uri()));
let result = tokio::time::timeout(Duration::from_secs(5), auth.get_access_token())
.await
.expect("get_access_token must fail fast, not wait on an OAuth callback timeout");
let err = result.expect_err("a rejected refresh token must yield an error");
assert!(
matches!(err, GrrError::Auth(_)),
"unexpected error: {err:?}"
);
let message = err.to_string();
assert!(
message.contains("invalid_grant"),
"underlying reason not surfaced: {message}"
);
assert!(
message.contains("grr auth login"),
"remediation guidance missing: {message}"
);
drop(guard);
}
#[tokio::test]
async fn refresh_persists_to_overridden_token_path_only() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "fresh-access-token",
"expires_in": 3600,
"token_type": "Bearer"
})))
.mount(&server)
.await;
let dir = tempfile::tempdir().unwrap();
let token_path = dir.path().join("nested").join("token.json");
let config = GrrConfig::default();
let auth = GoogleAuth::with_token(config.oauth.clone(), expired_storage_with_refresh_token())
.await
.unwrap()
.with_token_endpoint(format!("{}/token", server.uri()))
.with_token_path(token_path.clone());
let token = tokio::time::timeout(Duration::from_secs(5), auth.get_access_token())
.await
.expect("refresh must complete promptly")
.unwrap();
assert_eq!(token, "fresh-access-token");
let persisted = tokio::fs::read_to_string(&token_path).await.unwrap();
assert!(
persisted.contains("fresh-access-token"),
"refreshed token not persisted to overridden path: {persisted}"
);
}
#[tokio::test]
async fn refresh_response_without_refresh_token_preserves_stored_refresh_token() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "rotating-access-token",
"expires_in": 0, "token_type": "Bearer"
})))
.mount(&server)
.await;
let config = GrrConfig::default();
let dir = tempfile::tempdir().unwrap();
let auth = GoogleAuth::with_token(config.oauth.clone(), expired_storage_with_refresh_token())
.await
.unwrap()
.with_token_endpoint(format!("{}/token", server.uri()))
.with_token_path(dir.path().join("token.json"));
let first = tokio::time::timeout(Duration::from_secs(5), auth.get_access_token())
.await
.expect("first refresh must complete promptly")
.unwrap();
let second = tokio::time::timeout(Duration::from_secs(5), auth.get_access_token())
.await
.expect("second refresh must complete promptly, not hit a missing refresh token")
.unwrap();
assert_eq!(first, "rotating-access-token");
assert_eq!(second, "rotating-access-token");
assert_eq!(server.received_requests().await.unwrap().len(), 2);
}
#[tokio::test]
async fn successful_refresh_still_updates_the_access_token() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"access_token": "fresh-access-token",
"expires_in": 3600,
"token_type": "Bearer"
})))
.mount(&server)
.await;
let config = GrrConfig::default();
let dir = tempfile::tempdir().unwrap();
let auth = GoogleAuth::with_token(config.oauth.clone(), expired_storage_with_refresh_token())
.await
.unwrap()
.with_token_endpoint(format!("{}/token", server.uri()))
.with_token_path(dir.path().join("token.json"));
let token = tokio::time::timeout(Duration::from_secs(5), auth.get_access_token())
.await
.expect("refresh must complete promptly")
.unwrap();
assert_eq!(token, "fresh-access-token");
}