//! Refresh form encoding, response validation, and revision-aware cache behavior. use sausage::cred::oauth2::*; use std::{ sync::atomic::{AtomicUsize, Ordering}, time::{Duration, Instant}, }; use zeroize::Zeroizing; struct Fake(AtomicUsize); impl Http for Fake { async fn send(&self, request: &Request, _: Instant) -> Result>, Error> { assert_eq!(request.method, "POST"); self.0.fetch_add(1, Ordering::SeqCst); Ok(Zeroizing::new(br#"{"access_token":"SYNTHETIC","token_type":"Bearer","expires_in":3600,"scope":"one two"}"#.to_vec())) } } #[tokio::test] async fn encoding_and_cache() { let req = request( "https://tokens.example.test/token", "REFRESH&=+", "CLIENT", "SECRET&=+", ) .unwrap(); assert!( req.body.as_str() == "grant_type=refresh_token&refresh_token=REFRESH%26%3D%2B&client_id=CLIENT&client_secret=SECRET%26%3D%2B" ); assert_eq!(req.headers[1].1, req.body.len().to_string()); let fake = Fake(AtomicUsize::new(0)); let mut cache = Cache::default(); let deadline = Instant::now() + Duration::from_secs(1); let t = cache.refresh("id", 1, &req, &fake, deadline).await.unwrap(); assert_eq!(t.scopes.unwrap(), vec!["one", "two"]); cache.refresh("id", 1, &req, &fake, deadline).await.unwrap(); assert_eq!(fake.0.load(Ordering::SeqCst), 1); cache.refresh("id", 2, &req, &fake, deadline).await.unwrap(); assert_eq!(fake.0.load(Ordering::SeqCst), 2); cache.clear(); cache.refresh("id", 2, &req, &fake, deadline).await.unwrap(); assert_eq!(fake.0.load(Ordering::SeqCst), 3); for url in [ "http://tokens.example.test/token", "https://user@tokens.example.test/token", "https://tokens.example.test/token#x", ] { assert!(request(url, "r", "c", "s").is_err()); } } #[tokio::test] async fn expiry_and_failure() { struct Expired(AtomicUsize); impl Http for Expired { async fn send(&self, _: &Request, _: Instant) -> Result>, Error> { self.0.fetch_add(1, Ordering::SeqCst); Ok(Zeroizing::new( br#"{"access_token":"SYNTHETIC","token_type":"Bearer","expires_in":1}"#.to_vec(), )) } } struct Failed; impl Http for Failed { async fn send(&self, _: &Request, _: Instant) -> Result>, Error> { Err(Error) } } let req = request("https://tokens.example.test/token", "r", "c", "s").unwrap(); let mut cache = Cache::default(); let http = Expired(AtomicUsize::new(0)); let deadline = Instant::now() + Duration::from_secs(1); cache.refresh("id", 1, &req, &http, deadline).await.unwrap(); cache.refresh("id", 1, &req, &http, deadline).await.unwrap(); assert_eq!(http.0.load(Ordering::SeqCst), 2); assert!( cache .refresh("id", 2, &req, &Failed, deadline) .await .is_err() ); cache.refresh("id", 2, &req, &http, deadline).await.unwrap(); assert_eq!(http.0.load(Ordering::SeqCst), 3); } #[test] fn validation() { for data in [ br#"{"error":"invalid_grant","secret":"DO NOT ECHO"}"#.as_slice(), br#"{"access_token":"","token_type":"Bearer","expires_in":3600}"#, br#"{"access_token":"x","token_type":"MAC","expires_in":3600}"#, br#"{"access_token":"x","token_type":"Bearer","expires_in":0}"#, br#"{"access_token":"x\r\ny","token_type":"Bearer","expires_in":3600}"#, ] { let e = decode(data).err().unwrap(); assert!(!e.to_string().contains("DO NOT ECHO")); } assert!( decode(br#"{"access_token":"x","token_type":"Bearer","expires_in":18446744073709551615}"#) .is_ok() ); }