#![allow(missing_docs)]
use entropy_auth::{
AuthorizationRequest, OAuthConfig, PkceChallenge, RefreshRequest, TokenResponse,
};
fn test_config() -> OAuthConfig {
OAuthConfig::builder("my-client-id", "my-client-secret")
.authorization_endpoint("https://auth.example.com/authorize")
.token_endpoint("https://auth.example.com/token")
.redirect_uri("https://myapp.example.com/callback")
.scope("openid")
.scope("profile")
.build()
.unwrap()
}
#[test]
fn oauth_config_build_with_all_required_fields() {
let config = test_config();
assert_eq!(config.client_id(), "my-client-id");
assert_eq!(config.client_secret(), "my-client-secret");
assert_eq!(
config.authorization_endpoint(),
"https://auth.example.com/authorize",
);
assert_eq!(config.token_endpoint(), "https://auth.example.com/token");
assert_eq!(config.redirect_uri(), "https://myapp.example.com/callback");
assert_eq!(config.scopes(), &["openid", "profile"]);
}
#[test]
fn oauth_config_rejects_empty_client_id() {
let result = OAuthConfig::builder("", "secret")
.authorization_endpoint("https://auth.example.com/authorize")
.token_endpoint("https://auth.example.com/token")
.redirect_uri("https://myapp.example.com/callback")
.build();
assert!(result.is_err());
}
#[test]
fn authorization_url_contains_client_id() {
let config = test_config();
let req = AuthorizationRequest::build(&config).unwrap();
assert!(
req.url().contains("client_id=my-client-id"),
"URL should contain client_id: {}",
req.url(),
);
}
#[test]
fn authorization_url_contains_redirect_uri() {
let config = test_config();
let req = AuthorizationRequest::build(&config).unwrap();
assert!(
req.url().contains("redirect_uri="),
"URL should contain redirect_uri: {}",
req.url(),
);
assert!(
req.url().contains("myapp.example.com"),
"URL should reference the redirect host: {}",
req.url(),
);
}
#[test]
fn authorization_url_contains_state() {
let config = test_config();
let req = AuthorizationRequest::build(&config).unwrap();
assert!(
req.url().contains("state="),
"URL should contain state: {}",
req.url(),
);
assert!(
req.url().contains(req.state().value()),
"URL should contain the state value: {}",
req.url(),
);
}
#[test]
fn authorization_url_contains_code_challenge() {
let config = test_config();
let req = AuthorizationRequest::build(&config).unwrap();
assert!(
req.url().contains("code_challenge="),
"URL should contain code_challenge: {}",
req.url(),
);
assert!(
req.url().contains(req.pkce().challenge()),
"URL should contain the PKCE challenge value: {}",
req.url(),
);
}
#[test]
fn authorization_url_does_not_contain_client_secret() {
let config = test_config();
let req = AuthorizationRequest::build(&config).unwrap();
assert!(
!req.url().contains("my-client-secret"),
"URL must not contain the client secret: {}",
req.url(),
);
}
#[test]
fn pkce_challenge_method_is_s256() {
let pkce = PkceChallenge::generate().unwrap();
assert_eq!(pkce.method(), "S256");
}
#[test]
fn pkce_verifier_and_challenge_are_non_empty() {
let pkce = PkceChallenge::generate().unwrap();
assert!(!pkce.verifier().is_empty());
assert!(!pkce.challenge().is_empty());
}
#[test]
fn authorization_request_pkce_method_is_s256() {
let config = test_config();
let req = AuthorizationRequest::build(&config).unwrap();
assert!(
req.url().contains("code_challenge_method=S256"),
"URL should specify S256 method: {}",
req.url(),
);
assert_eq!(req.pkce().method(), "S256");
}
#[test]
fn parse_token_response_with_access_token() {
let json = r#"{
"access_token": "ya29.a0AX",
"token_type": "Bearer",
"expires_in": 3600
}"#;
let resp = TokenResponse::parse(json).unwrap();
assert_eq!(resp.access_token(), "ya29.a0AX");
assert_eq!(resp.token_type(), "Bearer");
assert_eq!(resp.expires_in(), Some(3600));
assert!(resp.refresh_token().is_none());
}
#[test]
fn parse_token_response_with_refresh_token() {
let json = r#"{
"access_token": "access-tok",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "refresh-tok"
}"#;
let resp = TokenResponse::parse(json).unwrap();
assert_eq!(resp.refresh_token(), Some("refresh-tok"));
}
#[test]
fn parse_invalid_json_returns_error() {
let result = TokenResponse::parse("this is not json");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("invalid JSON"),
"error should mention invalid JSON: {err}",
);
}
#[test]
fn parse_oauth_error_response() {
let json = r#"{"error":"invalid_grant","error_description":"Code expired"}"#;
let result = TokenResponse::parse(json);
assert!(result.is_err());
let err = result.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("invalid_grant"),
"error should contain error code: {msg}",
);
}
#[test]
fn parse_empty_json_returns_error() {
let result = TokenResponse::parse("");
assert!(result.is_err());
}
#[test]
fn parse_missing_access_token_returns_error() {
let json = r#"{"token_type":"Bearer"}"#;
let result = TokenResponse::parse(json);
assert!(result.is_err());
}
#[test]
fn parse_missing_token_type_returns_error() {
let json = r#"{"access_token":"tok"}"#;
let result = TokenResponse::parse(json);
assert!(result.is_err());
}
#[test]
fn refresh_request_from_token_response() {
let json = r#"{
"access_token": "access-tok",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "refresh-tok-123"
}"#;
let resp = TokenResponse::parse(json).unwrap();
let refresh_token = resp.refresh_token().unwrap();
let config = test_config();
let req = RefreshRequest::new(&config, refresh_token);
assert!(
req.body().contains("grant_type=refresh_token"),
"body should contain grant_type: {}",
req.body(),
);
assert!(
req.body().contains("refresh_token=refresh-tok-123"),
"body should contain refresh_token: {}",
req.body(),
);
assert!(
req.body().contains("client_id=my-client-id"),
"body should contain client_id: {}",
req.body(),
);
assert_eq!(req.content_type(), "application/x-www-form-urlencoded");
}