#![cfg(test)]
use std::str::FromStr;
use http::header::{CONTENT_TYPE, LINK};
use miette::IntoDiagnostic;
use url::Url;
use crate::{
http::CONTENT_TYPE_JSON,
standards::indieauth::{Scopes, ServerMetadata},
};
use super::{ClientId, CodeChallenge};
#[tokio::test]
async fn obtain_metadata_none_found() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let html_remote_page_mock = server
.mock("GET", "/profile")
.expect_at_most(1)
.create_async()
.await;
let headers_remote_page_mock = server
.mock("HEAD", "/profile")
.expect_at_most(1)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let remote_url = format!("{}/profile", server.url()).parse().unwrap();
let resp = client.obtain_metadata(&remote_url).await;
assert_eq!(
resp,
Err(super::Error::NoMetadataEndpoint.into()),
"expected no metadata anywhere due to lack of an endpoint"
);
headers_remote_page_mock.assert_async().await;
html_remote_page_mock.assert_async().await;
Ok(())
}
#[tokio::test]
async fn obtain_metadata_via_individual_endpoints() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let html_remote_page_mock = server
.mock("GET", "/profile")
.expect_at_most(1)
.with_body(format!(
r#"
<html>
<head>
<link rel="authorization_endpoint" href="{server_url}/auth" />
<link rel="token_endpoint" href="{server_url}/token" />
</head>
</html>
"#
))
.create_async()
.await;
let headers_remote_page_mock = server
.mock("HEAD", "/profile")
.expect_at_most(1)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let remote_url = format!("{}/profile", server.url()).parse().unwrap();
let resp = client.obtain_metadata(&remote_url).await;
headers_remote_page_mock.assert_async().await;
html_remote_page_mock.assert_async().await;
let metadata = resp?;
assert_eq!(metadata.issuer, server.url().parse().unwrap());
assert_eq!(
metadata.authorization_endpoint,
format!("{}/auth", server.url()).parse().unwrap()
);
assert_eq!(
metadata.token_endpoint,
format!("{}/token", server.url()).parse().unwrap()
);
assert_eq!(metadata.scopes_supported, super::Scopes::default());
assert_eq!(
metadata.code_challenge_methods_supported,
super::ServerMetadata::recommended_code_challenge_methods()
);
Ok(())
}
#[tokio::test]
async fn obtain_metadata_via_individual_endpoints_headers() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let _server_url = server.url();
let html_remote_page_mock = server
.mock("GET", "/profile")
.expect_at_most(0)
.create_async()
.await;
let headers_remote_page_mock = server
.mock("HEAD", "/profile")
.with_header(
LINK,
&format!(
r#"<{}/auth>; rel="authorization_endpoint""#,
server.url()
),
)
.with_header(
LINK,
&format!(r#"<{}/token>; rel="token_endpoint""#, server.url()),
)
.expect_at_most(1)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let remote_url = format!("{}/profile", server.url()).parse().unwrap();
let resp = client.obtain_metadata(&remote_url).await;
headers_remote_page_mock.assert_async().await;
html_remote_page_mock.assert_async().await;
let metadata = resp?;
assert_eq!(metadata.issuer, server.url().parse().unwrap());
assert_eq!(
metadata.authorization_endpoint,
format!("{}/auth", server.url()).parse().unwrap()
);
assert_eq!(
metadata.token_endpoint,
format!("{}/token", server.url()).parse().unwrap()
);
Ok(())
}
#[tokio::test]
async fn obtain_metadata_individual_endpoints_missing_auth() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let html_remote_page_mock = server
.mock("GET", "/profile")
.expect_at_most(1)
.with_body(format!(
r#"
<html>
<head>
<link rel="token_endpoint" href="{server_url}/token" />
</head>
</html>
"#
))
.create_async()
.await;
let headers_remote_page_mock = server
.mock("HEAD", "/profile")
.expect_at_most(1)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let remote_url = format!("{}/profile", server.url()).parse().unwrap();
let resp = client.obtain_metadata(&remote_url).await;
headers_remote_page_mock.assert_async().await;
html_remote_page_mock.assert_async().await;
assert_eq!(
resp,
Err(super::Error::NoMetadataEndpoint.into()),
"expected no metadata when authorization_endpoint is missing"
);
Ok(())
}
#[tokio::test]
async fn obtain_metadata_individual_endpoints_missing_token() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let html_remote_page_mock = server
.mock("GET", "/profile")
.expect_at_most(1)
.with_body(format!(
r#"
<html>
<head>
<link rel="authorization_endpoint" href="{server_url}/auth" />
</head>
</html>
"#
))
.create_async()
.await;
let headers_remote_page_mock = server
.mock("HEAD", "/profile")
.expect_at_most(1)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let remote_url = format!("{}/profile", server.url()).parse().unwrap();
let resp = client.obtain_metadata(&remote_url).await;
headers_remote_page_mock.assert_async().await;
html_remote_page_mock.assert_async().await;
assert_eq!(
resp,
Err(super::Error::NoMetadataEndpoint.into()),
"expected no metadata when token_endpoint is missing"
);
Ok(())
}
#[tokio::test]
async fn obtain_metadata_individual_endpoints_relative_urls() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let _server_url = server.url();
let html_remote_page_mock = server
.mock("GET", "/profile")
.expect_at_most(1)
.with_body(
r#"
<html>
<head>
<link rel="authorization_endpoint" href="auth" />
<link rel="token_endpoint" href="token" />
</head>
</html>
"#,
)
.create_async()
.await;
let headers_remote_page_mock = server
.mock("HEAD", "/profile")
.expect_at_most(1)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let remote_url = format!("{}/profile", server.url()).parse().unwrap();
let resp = client.obtain_metadata(&remote_url).await;
headers_remote_page_mock.assert_async().await;
html_remote_page_mock.assert_async().await;
let metadata = resp?;
assert_eq!(metadata.issuer, server.url().parse().unwrap());
assert_eq!(
metadata.authorization_endpoint,
format!("{}/auth", server.url()).parse().unwrap()
);
assert_eq!(
metadata.token_endpoint,
format!("{}/token", server.url()).parse().unwrap()
);
Ok(())
}
#[tokio::test]
async fn obtain_metadata_fallback_priority() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let metadata = super::ServerMetadata {
issuer: server.url().parse().unwrap(),
authorization_endpoint: format!("{server_url}/endpoints/auth").parse().unwrap(),
token_endpoint: format!("{server_url}/endpoints/token").parse().unwrap(),
ticket_endpoint: None,
introspection_endpoint: None,
revocation_endpoint: None,
code_challenge_methods_supported: super::ServerMetadata::recommended_code_challenge_methods(),
scopes_supported: super::Scopes::minimal(),
};
let html_remote_page_mock = server
.mock("GET", "/profile")
.expect_at_most(1)
.with_body(format!(
r#"
<html>
<head>
<link rel="indieauth-metadata" href="{server_url}/metadata" />
<link rel="authorization_endpoint" href="{server_url}/auth" />
<link rel="token_endpoint" href="{server_url}/token" />
</head>
</html>
"#
))
.create_async()
.await;
let headers_remote_page_mock = server
.mock("HEAD", "/profile")
.expect_at_most(1)
.create_async()
.await;
let metadata_endpoint_mock = server
.mock("GET", "/metadata")
.with_header(CONTENT_TYPE, CONTENT_TYPE_JSON)
.with_body(serde_json::json!(metadata).to_string())
.expect_at_most(1)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let remote_url = format!("{}/profile", server.url()).parse().unwrap();
let resp = client.obtain_metadata(&remote_url).await;
headers_remote_page_mock.assert_async().await;
metadata_endpoint_mock.assert_async().await;
html_remote_page_mock.assert_async().await;
assert_eq!(resp, Ok(metadata));
Ok(())
}
#[tokio::test]
async fn obtain_metadata_via_endpoint_headers() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let var_name = ServerMetadata {
issuer: server.url().parse().unwrap(),
authorization_endpoint: format!("{server_url}/endpoints/auth").parse().unwrap(),
token_endpoint: format!("{server_url}/endpoints/token").parse().unwrap(),
ticket_endpoint: format!("{server_url}/endpoints/ticket").parse().ok(),
introspection_endpoint: format!("{server_url}/endpoints/token").parse().ok(),
revocation_endpoint: None,
code_challenge_methods_supported: ServerMetadata::recommended_code_challenge_methods(),
scopes_supported: Scopes::minimal(),
};
let metadata = var_name;
let html_remote_page_mock = server
.mock("GET", "/profile")
.expect_at_most(0)
.create_async()
.await;
let headers_remote_page_mock = server
.mock("HEAD", "/profile")
.with_header(
LINK,
&format!(
r#"<{}/metadata>; rel="{}""#,
server.url(),
super::Client::<crate::http::reqwest::Client>::LINK_REL
),
)
.expect_at_most(1)
.create_async()
.await;
let metadata_endpoint_mock = server
.mock("GET", "/metadata")
.with_header(CONTENT_TYPE, CONTENT_TYPE_JSON)
.with_body(serde_json::json!(metadata).to_string())
.expect_at_most(1)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let remote_url = format!("{}/profile", server.url()).parse().unwrap();
let resp = client.obtain_metadata(&remote_url).await;
headers_remote_page_mock.assert_async().await;
html_remote_page_mock.assert_async().await;
metadata_endpoint_mock.assert_async().await;
assert_eq!(
resp,
Ok(metadata),
"expected no metadata anywhere due to lack of an endpoint"
);
Ok(())
}
#[test]
fn server_metadata_new_authorization_request_url() -> miette::Result<()> {
let issuer: Url = "https://example.com".parse().unwrap();
let metadata = ServerMetadata {
authorization_endpoint: format!("{}/auth", issuer.as_str()).parse().unwrap(),
token_endpoint: format!("{}/token", issuer.as_str()).parse().unwrap(),
ticket_endpoint: format!("{}/endpoints/ticket", issuer.as_str()).parse().ok(),
introspection_endpoint: format!("{}/introspect", issuer.as_str()).parse().ok(),
revocation_endpoint: None,
code_challenge_methods_supported: vec!["S256".to_string()],
scopes_supported: Scopes::minimal(),
issuer,
};
let (code_challenge, code_challenge_method) =
CodeChallenge::generate(super::CodeChallengeMethod::S256)?;
let formed_url = metadata.new_authorization_request_url(
super::AuthorizationRequestFields {
client_id: ClientId::new("http://client.example.com")?,
redirect_uri: "http://client.example.com/redirect"
.parse::<Url>()
.into_diagnostic()?
.into(),
state: "nu-state".to_string(),
challenge: code_challenge,
challenge_method: code_challenge_method,
scope: Default::default(),
},
Default::default(),
)?;
assert!(
!formed_url.query().unwrap_or_default().contains("scope="),
"does not includes an empty scope string"
);
assert!(
formed_url
.query()
.unwrap_or_default()
.contains("state=nu-state"),
"includes the provided state value"
);
Ok(())
}
#[test]
fn endpoint_discovery_classic_variant() {
let auth_url: Url = "https://example.com/auth".parse().unwrap();
let token_url: Url = "https://example.com/token".parse().unwrap();
let ticket_url: Url = "https://example.com/ticket".parse().unwrap();
let discovery = super::EndpointDiscovery::Classic {
authorization: auth_url.clone(),
token: token_url.clone(),
ticket: Some(ticket_url.clone()),
};
match discovery {
super::EndpointDiscovery::Classic {
authorization,
token,
ticket,
} => {
assert_eq!(authorization, auth_url);
assert_eq!(token, token_url);
assert_eq!(ticket, Some(ticket_url));
}
_ => panic!("Expected Classic variant"),
}
}
#[test]
fn endpoint_discovery_metadata_variant() {
let metadata_url: Url = "https://example.com/.well-known/oauth-authorization-server"
.parse()
.unwrap();
let discovery = super::EndpointDiscovery::Metadata {
metadata: metadata_url.clone(),
};
match discovery {
super::EndpointDiscovery::Metadata { metadata } => {
assert_eq!(metadata, metadata_url);
}
_ => panic!("Expected Metadata variant"),
}
}
#[test]
fn client_builder_with_classic_discovery() -> miette::Result<()> {
let auth_url: Url = "https://example.com/auth".parse().unwrap();
let token_url: Url = "https://example.com/token".parse().unwrap();
let discovery = super::EndpointDiscovery::Classic {
authorization: auth_url,
token: token_url,
ticket: None,
};
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.discovery(discovery.clone())
.build()?;
assert_eq!(
client.discovery,
Some(discovery),
"discovery field should be set"
);
Ok(())
}
#[test]
fn client_builder_with_metadata_discovery() -> miette::Result<()> {
let metadata_url: Url = "https://example.com/.well-known/oauth-authorization-server"
.parse()
.unwrap();
let discovery = super::EndpointDiscovery::Metadata {
metadata: metadata_url,
};
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.discovery(discovery.clone())
.build()?;
assert_eq!(
client.discovery,
Some(discovery),
"discovery field should be set"
);
Ok(())
}
#[test]
fn client_builder_without_discovery() -> miette::Result<()> {
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("http://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
assert_eq!(client.discovery, None, "discovery field should be None");
Ok(())
}
#[tokio::test]
async fn introspect_token_active() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let introspection_mock = server
.mock("POST", "/introspect")
.match_header("content-type", "application/x-www-form-urlencoded")
.match_header("accept", "application/json")
.match_body("token=test_access_token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{
"active": true,
"scope": "read create",
"client_id": "https://example.com",
"me": "https://user.example.com",
"exp": 1735689600,
"iat": 1735603200
}"#)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("https://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let introspection_endpoint = format!("{}/introspect", server.url()).parse().unwrap();
let response = client.introspect_token(&introspection_endpoint, "test_access_token").await?;
introspection_mock.assert_async().await;
assert_eq!(response.active, true, "token should be active");
assert_eq!(
response.scope,
Some(super::Scopes::from_str("read create")?),
"scope should match"
);
assert_eq!(
response.client_id,
Some(super::ClientId::new("https://example.com")?),
"client_id should match"
);
assert_eq!(
response.me,
Some("https://user.example.com".parse().unwrap()),
"me should match"
);
assert_eq!(response.exp, Some(1735689600), "exp should match");
assert_eq!(response.iat, Some(1735603200), "iat should match");
Ok(())
}
#[tokio::test]
async fn introspect_token_inactive() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let introspection_mock = server
.mock("POST", "/introspect")
.match_header("content-type", "application/x-www-form-urlencoded")
.match_header("accept", "application/json")
.match_body("token=invalid_token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"active": false}"#)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("https://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let introspection_endpoint = format!("{}/introspect", server.url()).parse().unwrap();
let response = client.introspect_token(&introspection_endpoint, "invalid_token").await?;
introspection_mock.assert_async().await;
assert_eq!(response.active, false, "token should be inactive");
assert_eq!(response.scope, None, "scope should be None");
assert_eq!(response.client_id, None, "client_id should be None");
Ok(())
}
#[tokio::test]
async fn revoke_token_success() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let revocation_mock = server
.mock("POST", "/revoke")
.match_header("content-type", "application/x-www-form-urlencoded")
.match_body("token=test_token&token_type_hint=access_token")
.with_status(200)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("https://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let revocation_endpoint = format!("{}/revoke", server.url()).parse().unwrap();
let result = client.revoke_token(
&revocation_endpoint,
"test_token",
Some(super::TokenTypeHint::AccessToken)
).await;
revocation_mock.assert_async().await;
assert!(result.is_ok(), "revocation should succeed");
Ok(())
}
#[tokio::test]
async fn revoke_token_without_hint() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let revocation_mock = server
.mock("POST", "/revoke")
.match_header("content-type", "application/x-www-form-urlencoded")
.match_body("token=test_token")
.with_status(200)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("https://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let revocation_endpoint = format!("{}/revoke", server.url()).parse().unwrap();
let result = client.revoke_token(
&revocation_endpoint,
"test_token",
None
).await;
revocation_mock.assert_async().await;
assert!(result.is_ok(), "revocation should succeed");
Ok(())
}
#[tokio::test]
async fn revoke_token_with_refresh_token_hint() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let revocation_mock = server
.mock("POST", "/revoke")
.match_header("content-type", "application/x-www-form-urlencoded")
.match_body("token=refresh_token_value&token_type_hint=refresh_token")
.with_status(200)
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("https://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let revocation_endpoint = format!("{}/revoke", server.url()).parse().unwrap();
let result = client.revoke_token(
&revocation_endpoint,
"refresh_token_value",
Some(super::TokenTypeHint::RefreshToken)
).await;
revocation_mock.assert_async().await;
assert!(result.is_ok(), "revocation should succeed");
Ok(())
}
#[tokio::test]
async fn refresh_token_success() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let token_mock = server
.mock("POST", "/token")
.match_header("content-type", "application/x-www-form-urlencoded")
.match_header("accept", "application/json")
.match_body("grant_type=refresh_token&refresh_token=test_refresh_token&client_id=https://example.com")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(format!(r#"{{
"token_type": "Bearer",
"access_token": "new_access_token",
"scope": "read create",
"me": "{}/user",
"expires_in": 3600
}}"#, server_url))
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("https://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let token_endpoint = format!("{}/token", server.url()).parse().unwrap();
let refresh_fields = super::RefreshTokenFields {
refresh_token: "test_refresh_token".to_string(),
client_id: super::ClientId::new("https://example.com")?,
scope: None,
};
let response = client.refresh_token::<()>(
&token_endpoint,
refresh_fields
).await?;
token_mock.assert_async().await;
match response {
super::RedemptionResponse::Claim(claim) => {
assert_eq!(claim.access_token, "new_access_token", "access_token should match");
assert_eq!(
claim.scope,
super::Scopes::from_str("read create")?,
"scope should match"
);
assert_eq!(
claim.me,
format!("{}/user", server_url).parse().unwrap(),
"me should match"
);
assert_eq!(claim.expires_in, 3600, "expires_in should match");
}
super::RedemptionResponse::Error(err) => {
panic!("Expected successful response, got error: {:?}", err);
}
}
Ok(())
}
#[tokio::test]
async fn refresh_token_with_scope_restriction() -> miette::Result<()> {
let mut server = mockito::Server::new_async().await;
let server_url = server.url();
let token_mock = server
.mock("POST", "/token")
.match_header("content-type", "application/x-www-form-urlencoded")
.match_header("accept", "application/json")
.match_body("grant_type=refresh_token&refresh_token=test_refresh_token&client_id=https://example.com&scope=read")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(format!(r#"{{
"token_type": "Bearer",
"access_token": "new_access_token",
"scope": "read",
"me": "{}/user",
"expires_in": 3600
}}"#, server_url))
.create_async()
.await;
let client = super::Client::<crate::http::reqwest::Client>::builder()
.id("https://example.com")
.client(crate::http::reqwest::Client::default())
.build()?;
let token_endpoint = format!("{}/token", server.url()).parse().unwrap();
let refresh_fields = super::RefreshTokenFields {
refresh_token: "test_refresh_token".to_string(),
client_id: super::ClientId::new("https://example.com")?,
scope: Some(super::Scopes::from_str("read")?),
};
let response = client.refresh_token::<()>(
&token_endpoint,
refresh_fields
).await?;
token_mock.assert_async().await;
match response {
super::RedemptionResponse::Claim(claim) => {
assert_eq!(
claim.scope,
super::Scopes::from_str("read")?,
"scope should be restricted to 'read'"
);
}
super::RedemptionResponse::Error(err) => {
panic!("Expected successful response, got error: {:?}", err);
}
}
Ok(())
}
#[test]
fn token_type_hint_serialization() {
assert_eq!(
super::TokenTypeHint::AccessToken.to_string(),
"access_token"
);
assert_eq!(
super::TokenTypeHint::RefreshToken.to_string(),
"refresh_token"
);
}
#[test]
fn refresh_token_fields_query_parameters() -> miette::Result<()> {
let fields = super::RefreshTokenFields {
refresh_token: "test_token".to_string(),
client_id: super::ClientId::new("https://example.com")?,
scope: Some(super::Scopes::from_str("read create")?),
};
let params = fields.into_query_parameters();
assert_eq!(params.len(), 4, "should have 4 parameters");
assert!(params.contains(&("grant_type".to_string(), "refresh_token".to_string())));
assert!(params.contains(&("refresh_token".to_string(), "test_token".to_string())));
assert!(params.contains(&("client_id".to_string(), "https://example.com".to_string())));
assert!(params.contains(&("scope".to_string(), "read create".to_string())));
Ok(())
}
#[test]
fn refresh_token_fields_query_parameters_without_scope() -> miette::Result<()> {
let fields = super::RefreshTokenFields {
refresh_token: "test_token".to_string(),
client_id: super::ClientId::new("https://example.com")?,
scope: None,
};
let params = fields.into_query_parameters();
assert_eq!(params.len(), 3, "should have 3 parameters (no scope)");
assert!(params.contains(&("grant_type".to_string(), "refresh_token".to_string())));
assert!(params.contains(&("refresh_token".to_string(), "test_token".to_string())));
assert!(params.contains(&("client_id".to_string(), "https://example.com".to_string())));
Ok(())
}