use super::*;
#[test]
fn a_credential_reports_its_expiry_and_whether_it_can_renew() {
let now = chrono::Utc::now().timestamp_millis();
let live = link_assistant_router::subscription::SubscriptionToken {
access_token: "a".into(),
refresh_token: Some("r".into()),
expires_at_ms: Some(now + 3 * 3_600_000),
account_id: None,
resource_url: None,
};
let report = describe_credential(&live);
assert!(report.contains("expires in"), "{report}");
assert!(report.contains("refresh token present"), "{report}");
}
#[test]
fn an_expired_credential_is_named_as_expired() {
let now = chrono::Utc::now().timestamp_millis();
let dead = link_assistant_router::subscription::SubscriptionToken {
access_token: "a".into(),
refresh_token: None,
expires_at_ms: Some(now - 3 * 3_600_000),
account_id: None,
resource_url: None,
};
let report = describe_credential(&dead);
assert!(report.contains("EXPIRED"), "{report}");
assert!(
report.contains("NO refresh token"),
"a credential that cannot be renewed must say so: {report}"
);
}
#[test]
fn an_unrecorded_expiry_is_not_reported_as_expired() {
let unknown = link_assistant_router::subscription::SubscriptionToken {
access_token: "a".into(),
refresh_token: Some("r".into()),
expires_at_ms: None,
account_id: None,
resource_url: None,
};
let report = describe_credential(&unknown);
assert!(report.contains("no recorded expiry"), "{report}");
assert!(!report.contains("EXPIRED"), "{report}");
}
#[test]
fn durations_read_at_a_glance_at_each_threshold() {
assert_eq!(humanize_minutes(45), "45 minutes");
assert_eq!(
humanize_minutes(89),
"89 minutes",
"the last minute reading"
);
assert_eq!(humanize_minutes(90), "1 hours", "the first hour reading");
assert_eq!(
humanize_minutes(119),
"1 hours",
"truncation the doc comment claims to have removed still applies here"
);
assert_eq!(humanize_minutes(120), "2 hours");
assert_eq!(
humanize_minutes(60 * 47),
"47 hours",
"the last hour reading"
);
assert_eq!(humanize_minutes(60 * 48), "2 days", "the first day reading");
}
#[tokio::test]
async fn rejected_conditional_candidate_requires_explicit_force() {
let home = tempfile::tempdir().expect("credential home");
let data = tempfile::tempdir().expect("router data");
let reader = SubscriptionReader::new(SubscriptionProvider::Qwen, home.path());
let document = r#"{"access_token":"rejected","refresh_token":"r","scope":"openid"}"#;
let error = install_candidate(
&reader,
data.path(),
document,
CredentialProbe::Rejected,
ImportPolicy {
if_absent: true,
force: false,
},
)
.await
.expect_err("rejected candidate must be refused");
assert!(error.contains("--force"), "{error}");
assert!(!home.path().join("oauth_creds.json").exists());
}
#[tokio::test]
async fn rejected_candidate_without_force_reports_existing_destination_as_present() {
let home = tempfile::tempdir().expect("credential home");
let data = tempfile::tempdir().expect("router data");
let reader = SubscriptionReader::new(SubscriptionProvider::Codex, home.path());
let existing = home.path().join("auth.json");
let current =
r#"{"auth_mode":"chatgpt","tokens":{"access_token":"current","refresh_token":"rotated"}}"#;
std::fs::write(&existing, current).expect("current credential");
let outcome = install_candidate(
&reader,
data.path(),
r#"{"auth_mode":"chatgpt","tokens":{"access_token":"rejected","refresh_token":"stale"}}"#,
CredentialProbe::Rejected,
ImportPolicy {
if_absent: true,
force: false,
},
)
.await
.expect("existing destination wins before rejection policy");
assert_eq!(
outcome,
InstallDocumentResult::AlreadyPresent(existing.clone())
);
assert_eq!(std::fs::read_to_string(existing).unwrap(), current);
}
#[tokio::test]
async fn force_does_not_overwrite_an_existing_destination() {
let home = tempfile::tempdir().expect("credential home");
let data = tempfile::tempdir().expect("router data");
let reader = SubscriptionReader::new(SubscriptionProvider::Codex, home.path());
let existing = home.path().join("auth.json");
let current =
r#"{"auth_mode":"chatgpt","tokens":{"access_token":"current","refresh_token":"rotated"}}"#;
std::fs::write(&existing, current).expect("current credential");
let outcome = install_candidate(
&reader,
data.path(),
r#"{"auth_mode":"chatgpt","tokens":{"access_token":"rejected","refresh_token":"stale"}}"#,
CredentialProbe::Rejected,
ImportPolicy {
if_absent: true,
force: true,
},
)
.await
.expect("force permits consideration of rejected candidate");
assert_eq!(
outcome,
InstallDocumentResult::AlreadyPresent(existing.clone())
);
assert_eq!(std::fs::read_to_string(existing).unwrap(), current);
}
#[tokio::test]
async fn force_installs_a_rejected_candidate_into_an_empty_destination() {
let home = tempfile::tempdir().expect("credential home");
let data = tempfile::tempdir().expect("router data");
let reader = SubscriptionReader::new(SubscriptionProvider::Codex, home.path());
let candidate = r#"{"auth_mode":"chatgpt","tokens":{"access_token":"rejected","refresh_token":"explicit"}}"#;
let outcome = install_candidate(
&reader,
data.path(),
candidate,
CredentialProbe::Rejected,
ImportPolicy {
if_absent: true,
force: true,
},
)
.await
.expect("force permits rejected candidate in empty destination");
let destination = home.path().join("auth.json");
assert_eq!(
outcome,
InstallDocumentResult::Installed(destination.clone())
);
assert_eq!(std::fs::read(destination).unwrap(), candidate.as_bytes());
}
#[tokio::test]
async fn ordinary_import_still_replaces_a_rejected_candidate() {
let home = tempfile::tempdir().expect("credential home");
let data = tempfile::tempdir().expect("router data");
let reader = SubscriptionReader::new(SubscriptionProvider::Gemini, home.path());
std::fs::write(
home.path().join("oauth_creds.json"),
r#"{"access_token":"current"}"#,
)
.expect("current credential");
let candidate = r#"{"access_token":"rejected","scope":"preserved"}"#;
let outcome = install_candidate(
&reader,
data.path(),
candidate,
CredentialProbe::Rejected,
ImportPolicy {
if_absent: false,
force: false,
},
)
.await
.expect("ordinary replacement compatibility");
assert!(matches!(outcome, InstallDocumentResult::Installed(_)));
assert_eq!(
std::fs::read_to_string(home.path().join("oauth_creds.json")).unwrap(),
candidate
);
}