use super::super::generic::GenericOAuthConfig;
use super::super::providers::OAuthProvider;
use super::super::resolved::ResolvedGenericOAuth;
use super::*;
use serde::Deserialize;
use serde_json::json;
const NOW: i64 = 2_000_000_000_123;
fn provider(fallback: Option<f64>) -> ResolvedProvider {
ResolvedProvider {
config: OAuthProvider::google("duration-client", "duration-client-secret"),
generic: Some(ResolvedGenericOAuth {
config: GenericOAuthConfig {
access_token_expires_in: fallback,
..Default::default()
},
issuer: None,
is_oidc: false,
verifier: None,
}),
}
}
#[test]
fn oauth_expiry_adds_submilliseconds_before_timeclip() -> AuthResult<()> {
for (now, seconds, expected) in [
(NOW, -0.0005, NOW - 1),
(NOW, 0.0005, NOW),
(NOW, 1.5, NOW + 1500),
(NOW, 0.0, NOW),
(-NOW, 0.0005, -NOW + 1),
(-NOW, -0.0005, -NOW),
] {
let now = DateTime::from_timestamp_millis(now)
.ok_or_else(|| AuthError::internal("OAuth test clock is out of range"))?;
assert_eq!(expiry_from(now, seconds)?.milliseconds(), expected as f64);
}
Ok(())
}
#[derive(Deserialize)]
struct Fixture {
version: String,
cases: Vec<Case>,
}
#[derive(Deserialize)]
struct Case {
backend: String,
scenario: Scenario,
now: i64,
helpers: Vec<Helper>,
}
#[derive(Deserialize)]
struct Scenario {
name: String,
#[serde(rename = "fallbackDuration")]
fallback: Option<Value>,
}
#[derive(Deserialize)]
struct Helper {
operation: String,
outcome: Value,
events: Vec<Value>,
}
fn observed_tokens(tokens: OAuthTokenSet, exchange: bool) -> AuthResult<Value> {
let mut observed = serde_json::Map::from_iter([
("tokenType".into(), json!(tokens.token_type)),
("accessToken".into(), json!(tokens.access_token)),
("refreshToken".into(), json!(tokens.refresh_token)),
("idToken".into(), json!(tokens.id_token)),
("scopes".into(), json!(tokens.scopes)),
]);
for (name, value) in [
("accessTokenExpiresAt", tokens.access_token_expires_at),
("refreshTokenExpiresAt", tokens.refresh_token_expires_at),
] {
if let Some(date) = value.field_value().as_date() {
let _ = observed.insert(
name.into(),
json!({"type": "date", "value": FieldValue::Date(date.clone()).json()?}),
);
} else if exchange {
let _ = observed.insert(name.into(), json!({"type": "undefined"}));
}
}
if let Some(raw) = tokens.raw.json()? {
let _ = observed.insert("raw".into(), raw);
}
Ok(Value::Object(observed))
}
#[test]
fn oauth_duration_helpers_match_all_captured_token_fields() -> AuthResult<()> {
let fixture: Fixture = serde_json::from_str(include_str!(
"../../../../../tests/fixtures/oauth-token-duration-1.7.6.json"
))?;
assert_eq!(fixture.version, "1.7.6");
assert_eq!(fixture.cases.len(), 4);
for case in fixture.cases {
let now = DateTime::from_timestamp_millis(case.now)
.ok_or_else(|| AuthError::internal("Captured OAuth clock is out of range"))?;
let fallback = match case.scenario.fallback {
None => None,
Some(value) if value == json!({"type": "number", "value": "NaN"}) => Some(f64::NAN),
Some(value) => return Err(AuthError::internal(format!("Unknown fallback: {value}"))),
};
assert_eq!(case.helpers.len(), 2);
for helper in case.helpers {
let exchange = match helper.operation.as_str() {
"exchange" => true,
"refresh" => false,
operation => {
return Err(AuthError::internal(format!("Unknown grant: {operation}")));
}
};
let response = helper
.events
.iter()
.find(|event| event.get("kind").and_then(Value::as_str) == Some("token.http"))
.and_then(|event| event.get("response"))
.and_then(|response| response.get("body"))
.and_then(Value::as_str)
.ok_or_else(|| AuthError::internal("Missing captured OAuth token response"))?;
let mut tokens =
parse_token_response_with_clock(serde_json::from_str(response)?, || now)?;
if !exchange {
tokens.raw = Default::default();
}
let tokens = apply_default_expiry(tokens, &provider(fallback))?;
assert_eq!(
json!({"kind": "returned", "value": observed_tokens(tokens, exchange)?}),
helper.outcome,
"{}/{}/{}",
case.backend,
case.scenario.name,
helper.operation,
);
}
}
Ok(())
}
#[test]
fn oauth_falsy_fallback_omits_expiry_without_replacing_supplied_dates() -> AuthResult<()> {
let supplied = FieldDate::from_milliseconds(NOW as f64);
for fallback in [None, Some(0.0), Some(-0.0), Some(f64::NAN)] {
let tokens = apply_default_expiry(
OAuthTokenSet {
access_token: (Some("duration-access".into())).into(),
refresh_token_expires_at: (Some(supplied.clone())).into(),
..Default::default()
},
&provider(fallback),
)?;
assert!(tokens.access_token_expires_at.is_undefined());
assert_eq!(
tokens.access_token.field_value().as_str(),
Some("duration-access")
);
assert!(
tokens
.refresh_token_expires_at
.field_value()
.as_date()
.is_some_and(|date| date.same_object(&supplied))
);
}
for fallback in [f64::NAN, f64::INFINITY, -0.0005, 300.0] {
let tokens = apply_default_expiry(
OAuthTokenSet {
access_token_expires_at: (Some(supplied.clone())).into(),
..Default::default()
},
&provider(Some(fallback)),
)?;
assert!(
tokens
.access_token_expires_at
.field_value()
.as_date()
.is_some_and(|date| date.same_object(&supplied))
);
}
let tokens = apply_default_expiry(OAuthTokenSet::default(), &provider(Some(f64::INFINITY)))?;
assert!(
tokens
.access_token_expires_at
.field_value()
.as_date()
.unwrap()
.milliseconds()
.is_nan()
);
Ok(())
}