Skip to content

Commit d6d29f1

Browse files
committed
test(auth): cover persist-failure and lock-degrade paths
- ensure_returns_token_even_when_session_persist_fails: a read-only config dir must not fail the refresh — the caller still gets the token, the on-disk session stays stale, and the failed lock acquisition exercises the unlocked degrade. - lock_file_degrades_to_none_when_config_dir_unavailable: pointing the config dir at a regular file must yield None (proceed unlocked), not an error or a hang.
1 parent e7e42ea commit d6d29f1

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

‎src/client/jwt.rs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,40 @@ mod tests {
11871187
);
11881188
}
11891189

1190+
#[test]
1191+
fn ensure_returns_token_even_when_session_persist_fails() {
1192+
// A refreshed token must reach the caller even when session.json
1193+
// can't be written (read-only config dir): the persistence failure
1194+
// is warned about, not fatal. The lock acquisition also fails here
1195+
// (session.lock can't be created), exercising the unlocked degrade.
1196+
use std::os::unix::fs::PermissionsExt;
1197+
let (_tmp, _guard) = with_temp_config_dir();
1198+
let mut server = mockito::Server::new();
1199+
let m = server
1200+
.mock("POST", "/o/token/")
1201+
.match_body(mockito::Matcher::UrlEncoded(
1202+
"grant_type".into(),
1203+
"refresh_token".into(),
1204+
))
1205+
.with_status(200)
1206+
.with_header("content-type", "application/json")
1207+
.with_body(r#"{"access_token":"refreshed-jwt","expires_in":300,"refresh_token":"r2"}"#)
1208+
.create();
1209+
1210+
save_session(&cached_session(-10, 86400)).unwrap();
1211+
let profile = mock_profile(&server.url());
1212+
1213+
let dir = config::config_dir().unwrap();
1214+
fs::set_permissions(&dir, fs::Permissions::from_mode(0o555)).unwrap();
1215+
let result = ensure_access_token(&profile, None);
1216+
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
1217+
1218+
m.assert();
1219+
assert_eq!(result.unwrap(), "refreshed-jwt");
1220+
// The on-disk session couldn't be updated — still the old one.
1221+
assert_eq!(load_session().unwrap().access_token, "cached-jwt");
1222+
}
1223+
11901224
#[test]
11911225
fn ensure_concurrent_expiry_refreshes_once_and_shares_result() {
11921226
// Regression: when the access token expired mid-burst, N concurrent

‎src/config.rs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,25 @@ mod tests {
436436
assert_eq!(staging.workspaces[0].public_id, "ws-staging");
437437
}
438438

439+
#[test]
440+
fn lock_file_degrades_to_none_when_config_dir_unavailable() {
441+
// Locking is best-effort: when the config dir can't even be
442+
// created (here: the path is a regular file), lock_file must
443+
// degrade to None — warn and proceed unlocked — not error or hang.
444+
let (tmp, _guard) = with_temp_config_dir();
445+
let not_a_dir = tmp.path().join("not-a-dir");
446+
fs::write(&not_a_dir, "x").unwrap();
447+
// SAFETY: serialized via with_temp_config_dir's ENV_LOCK guard.
448+
unsafe {
449+
std::env::set_var("HOTDATA_CONFIG_DIR", &not_a_dir);
450+
}
451+
let lock = lock_file("config.lock");
452+
unsafe {
453+
std::env::set_var("HOTDATA_CONFIG_DIR", tmp.path());
454+
}
455+
assert!(lock.is_none());
456+
}
457+
439458
#[test]
440459
fn config_file_stays_world_readable() {
441460
// The atomic-write path must not silently flip config.yml from the

0 commit comments

Comments
 (0)