mod common;
use std::path::{Path, PathBuf};
use std::time::Duration;
use common::{email, Server, PASSWORD};
use net_backend_client::protocol::auth::{AccessToken, GetAccount, RefreshToken, RegisterRequest, TokenPair};
use net_backend_client::protocol::UnixMillis;
use net_backend_client::{Client, Error, TokenFile};
fn folder(name: &str) -> PathBuf {
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("token-file").join(name);
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("test folder");
dir
}
fn fake_pair(n: u32) -> TokenPair {
TokenPair::new(AccessToken::new(format!("nbsa_fake-{n}")), UnixMillis(1_000), RefreshToken::new(format!("nbsr_fake-secret-{n}")), UnixMillis(2_000))
}
fn names(dir: &Path) -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(dir).expect("read dir").map(|e| e.expect("entry").file_name().to_string_lossy().into_owned()).collect();
names.sort();
names
}
#[test]
fn a_round_trip_on_its_own() {
let dir = folder("round-trip");
let file = TokenFile::new(dir.join("nested").join("session.json"));
let server = "https://api.example.com";
assert!(matches!(file.load(server), Ok(None)), "no file: no session");
file.save(server, &fake_pair(1)).expect("save (the missing folder is created)");
let back = file.load(server).expect("load").expect("stored");
assert_eq!((back.access_token.expose(), back.refresh_token.expose()), ("nbsa_fake-1", "nbsr_fake-secret-1"));
assert_eq!((back.access_expires_at, back.refresh_expires_at), (UnixMillis(1_000), UnixMillis(2_000)));
file.save(server, &fake_pair(2)).expect("save again");
assert_eq!(file.load(server).expect("load").map(|p| p.refresh_token.expose().to_string()).as_deref(), Some("nbsr_fake-secret-2"));
assert_eq!(names(&dir.join("nested")), vec!["session.json".to_string()]);
assert!(matches!(file.load("https://other.example.com"), Ok(None)));
file.remove().expect("remove");
assert!(!file.path().exists());
file.remove().expect("removing a missing file is fine");
}
#[test]
fn damaged_and_foreign_files_are_refused_without_quoting_them() {
let dir = folder("damaged");
let file = TokenFile::new(dir.join("session.json"));
let server = "https://api.example.com";
let damaged: [&[u8]; 5] = [
b"",
b"{\"format\":1,\"server\":\"https://api.example.com\",\"tokens\":{\"access_token\":\"nbsa_fake-leak\"",
b"[\"nbsr_fake-leak\"]",
b"{\"format\":1,\"server\":\"https://api.example.com\",\"tokens\":{\"token_type\":\"Bearer\",\"access_token\":7,\"access_expires_at\":\"nbsr_fake-leak\"}}",
&[0xff, 0xfe, 0x00, 0x41],
];
for content in damaged {
std::fs::write(file.path(), content).expect("write");
let error = file.load(server).expect_err("damaged");
let text = format!("{error} {error:?}");
assert!(matches!(error, Error::InvalidRequest(_)) && text.contains("not a valid token file"), "{text}");
assert!(!text.contains("leak"), "the content is never quoted: {text}");
}
std::fs::write(file.path(), br#"{"format":99,"server":"x","tokens":{}}"#).expect("write");
assert!(matches!(file.load(server), Err(Error::InvalidRequest(ref why)) if why.contains("format 99")));
std::fs::write(file.path(), vec![b' '; 70 * 1024]).expect("write");
assert!(matches!(file.load(server), Err(Error::InvalidRequest(ref why)) if why.contains("not a token file")));
let folder_path = dir.join("a-folder");
std::fs::create_dir_all(&folder_path).expect("folder");
let as_file = TokenFile::new(&folder_path);
assert!(as_file.load(server).is_err());
assert!(as_file.save(server, &fake_pair(1)).is_err());
assert!(!names(&dir).iter().any(|n| n.ends_with(".tmp")), "a failed save leaves no temporary file: {:?}", names(&dir));
}
#[cfg(unix)]
#[test]
fn the_file_is_owner_only_on_unix() {
use std::os::unix::fs::PermissionsExt;
let dir = folder("unix-mode");
let path = dir.join("fresh").join("session.json");
let file = TokenFile::new(&path);
file.save("https://api.example.com", &fake_pair(1)).expect("save");
let mode = |p: &Path| std::fs::metadata(p).expect("metadata").permissions().mode() & 0o777;
assert_eq!(mode(&path), 0o600);
assert_eq!(mode(&dir.join("fresh")), 0o700, "a folder it creates is owner-only too");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).expect("chmod");
file.save("https://api.example.com", &fake_pair(2)).expect("save");
assert_eq!(mode(&path), 0o600);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_client_keeps_the_file_up_to_date() {
let server = Server::start();
let dir = folder("client");
let path = dir.join("session.json");
let file = TokenFile::new(&path);
let client = Client::builder(&server.base).token_file(&path).build().expect("client");
assert!(!client.is_logged_in());
let session = client.register(RegisterRequest::new(email("filed"), PASSWORD)).await.expect("register");
let stored = file.load(&client.server_url()).expect("load").expect("written at register");
assert_eq!(stored.refresh_token.expose(), session.tokens.refresh_token.expose());
let fresh = client.refresh().await.expect("refresh");
assert_ne!(fresh.refresh_token.expose(), session.tokens.refresh_token.expose());
let stored = file.load(&client.server_url()).expect("load").expect("stored");
assert_eq!(stored.refresh_token.expose(), fresh.refresh_token.expose());
drop(client);
let resumed = Client::builder(&server.base).token_file(&path).build().expect("client");
assert!(resumed.is_logged_in());
let me = resumed.call(&GetAccount::new()).await.expect("a call with the stored session");
assert_eq!(me.id, session.account.id);
server.advance(Duration::from_secs(2 * 3600));
resumed.call(&GetAccount::new()).await.expect("refreshed, then sent");
let stored = file.load(&resumed.server_url()).expect("load").expect("stored");
assert_eq!(stored.refresh_token.expose(), resumed.tokens().expect("tokens").refresh_token.expose());
resumed.logout().await.expect("logout");
assert!(!path.exists());
assert!(names(&dir).is_empty(), "{:?}", names(&dir));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn damaged_foreign_and_explicit_starts() {
let server = Server::start();
let dir = folder("starts");
let path = dir.join("session.json");
std::fs::write(&path, b"{ not json").expect("write");
let client = Client::builder(&server.base).token_file(&path).build().expect("a damaged file does not stop the start");
assert!(!client.is_logged_in());
client.register(RegisterRequest::new(email("damaged"), PASSWORD)).await.expect("register");
assert!(TokenFile::new(&path).load(&client.server_url()).expect("valid again").is_some());
let other = Client::builder("http://127.0.0.1:9").token_file(&path).build().expect("client");
assert!(!other.is_logged_in());
let explicit = client.tokens().expect("tokens");
std::fs::remove_file(&path).expect("remove");
let client = Client::builder(&server.base).tokens(explicit.clone()).token_file(&path).build().expect("client");
assert!(client.is_logged_in());
let stored = TokenFile::new(&path).load(&client.server_url()).expect("load").expect("written at build");
assert_eq!(stored.refresh_token.expose(), explicit.refresh_token.expose());
let unreadable = Client::builder(&server.base).token_file(&dir).build();
assert!(matches!(unreadable, Err(Error::InvalidRequest(ref why)) if why.contains("token file")), "{unreadable:?}");
let client = Client::builder(&server.base).token_file(&path).build().expect("client");
client.resume(fake_pair(7));
assert_eq!(TokenFile::new(&path).load(&client.server_url()).expect("load").map(|p| p.access_token.expose().to_string()).as_deref(), Some("nbsa_fake-7"));
let ended = client.call(&GetAccount::new()).await.expect_err("an unknown refresh token is refused");
assert!(ended.needs_login(), "{ended:?}");
assert!(!path.exists(), "the ended session's file is gone");
}
#[test]
fn the_blocking_client_uses_it_too() {
let server = Server::start();
let dir = folder("blocking");
let path = dir.join("session.json");
let builder = Client::builder(&server.base).token_file(&path);
let client = net_backend_client::blocking::Client::from_builder(builder).expect("client");
client.register(RegisterRequest::new(email("blocking-file"), PASSWORD)).expect("register");
drop(client);
let client = net_backend_client::blocking::Client::from_builder(Client::builder(&server.base).token_file(&path)).expect("client");
assert!(client.is_logged_in());
client.call(&GetAccount::new()).expect("resumed");
client.logout().expect("logout");
assert!(!path.exists());
}