mod common;
use std::{
sync::{
atomic::{AtomicU8, Ordering},
mpsc, Arc,
},
time::Duration,
};
use common::*;
// These tests are all ignored by default because they involve authenticating with the Ditto Cloud.
// Find and remove all of the #[ignore = "Auth tests disabled by default"] attributes to run them.
/// App ID from the Ditto Cloud Portal. Must be a valid UUID.
static APP_ID: &str = "6eff5920-8ab1-43f4-8803-8187f0d389fe";
/// Provider to use when attempting a login.
static PROVIDER: &str = "portal";
/// Token from "Playground Mode Details" section on Settings page for the app ID above.
static PLAYGROUND_TOKEN: &str = "3f03f821-9f0a-4424-8186-3aaaaa23b05c";
/// JWT token scraped from portal request to endpoint:
/// `https://<appID>.cloud.ditto.live/_ditto/auth/login`
static LOGIN_TOKEN: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik9UZEJSVFJDUVRSRU1qTTJNakpHTlVORU1UUTVPRVU0TXpoRU1qazVRa0k0UkRRMk5EVTJNZyJ9.eyJpc3MiOiJodHRwczovL2RpdHRvLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwNDc5ODQ2OTkzNzk4NTQ5MjY5MiIsImF1ZCI6WyJodHRwczovL3BvcnRhbGFwaS5kaXR0by5saXZlIiwiaHR0cHM6Ly9kaXR0by5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNjgwNTEzNzE2LCJleHAiOjE2ODA2MDAxMTYsImF6cCI6ImVnTjNMRU9OZVUyR1hzemZ2RVkwc1NTbWM5RzcyZHB5Iiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCJ9.lkvRobdxCHcjze9OvOTHfdeMEdvz4lhpi6kRYQSq3vhs6rznzarbsivefzHaPKiwoZPRgaRf-EO7BRBbpIz6fOFlKhFmMdNWOqhqNr4STRmC8K4nXx1CM1us8LRu5AZlzm8HjszHIpzHlNfneEXXV7QvDLAZnN_SoEh-Sc_Ue9_16UxSsZna6qaE7q-ZTaz7MBoDYi-B0PLn79t2VuzrsrQfsyhdj7Hoy7hXG1CzrLAlM0RXQMXHtZDELiDVftaqnYvc2EuuPbY496-UBSiGGHxRrHucHrzPPqgzyWprzcOfsUyCqWb3mbiS0GYkWOBx2fKpppQZpvpNstEUk-vvCQ";
/// An expired token for testing auth errors.
static EXPIRED_LOGIN_TOKEN: &str = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik9UZEJSVFJDUVRSRU1qTTJNakpHTlVORU1UUTVPRVU0TXpoRU1qazVRa0k0UkRRMk5EVTJNZyJ9.eyJpc3MiOiJodHRwczovL2RpdHRvLmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw2MTU1NDhkYjE4MGUxNjAwNmFmNWE3NTkiLCJhdWQiOlsiaHR0cHM6Ly9wb3J0YWxhcGkuZGl0dG8ubGl2ZSIsImh0dHBzOi8vZGl0dG8uYXV0aDAuY29tL3VzZXJpbmZvIl0sImlhdCI6MTY0MjQ0MTA3NywiZXhwIjoxNjQyNTI3NDc3LCJhenAiOiJlZ04zTEVPTmVVMkdYc3pmdkVZMHNTU21jOUc3MmRweSIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwifQ.mCQC3pILToK9MhZXsoe3DCw4f1uD0KEpfj2i7yRCrt6R81-7yWUZb7F5OXDGkVApX4FzI12w13L_Bzk6Nx1NxJVFa_8Tmb40Fthy5UW7JjANL0itEIBM15QH_rb9Z1YVXfnSHXn3rTRaqhZeRyPZjoIMeP43WB1mDzg-JLPsxw_BHhmTeS1kkvcDATxyT21H2IyE4xZXwQHwxpiXRv9UaE_rtP4qL13oHT46wm3Isa6rXT2wJmPoq9eXFkdZXvvNJgE_xCeXlAz_GURfNsnJQn0BExzzI8k3Ti66ac-q-48vXbzv9zESfWiJQnsHXfFAbAn2pQacXThb9NYKR-hPPQ";
#[ignore = "Auth tests disabled by default"]
#[test]
fn login_with_token_logs_in_given_a_valid_token() {
let (auth_required_tx, auth_required_rx) = mpsc::sync_channel::<()>(1);
let auth_event_handler = AuthEventHandler::new(auth_required_tx);
let ditto = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OnlineWithAuthentication::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
auth_event_handler.clone(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
auth_required_rx.recv().unwrap();
let res = ditto
.authenticator()
.unwrap()
.login_with_token_and_feedback(LOGIN_TOKEN, PROVIDER);
assert!(res.is_ok());
assert!(ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_some());
}
#[ignore = "Auth tests disabled by default"]
#[test]
fn login_with_token_fails_if_token_is_expired() {
let (auth_required_tx, auth_required_rx) = mpsc::sync_channel::<()>(1);
let auth_event_handler = AuthEventHandler::new(auth_required_tx);
let ditto = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OnlineWithAuthentication::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
auth_event_handler.clone(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
auth_required_rx.recv().unwrap();
let res = ditto
.authenticator()
.unwrap()
.login_with_token_and_feedback(EXPIRED_LOGIN_TOKEN, PROVIDER);
assert!(res.is_err());
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
}
#[ignore = "Auth tests disabled by default"]
#[test]
fn login_with_token_fails_if_token_is_invalid() {
let (auth_required_tx, auth_required_rx) = mpsc::sync_channel::<()>(1);
let auth_event_handler = AuthEventHandler::new(auth_required_tx);
let ditto = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OnlineWithAuthentication::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
auth_event_handler.clone(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
auth_required_rx.recv().unwrap();
let res = ditto
.authenticator()
.unwrap()
.login_with_token_and_feedback("an_invalid_token", PROVIDER);
assert!(res.is_err());
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
}
#[ignore = "Auth tests disabled by default"]
#[test]
fn login_with_token_fails_if_provider_is_invalid() {
let (auth_required_tx, auth_required_rx) = mpsc::sync_channel::<()>(1);
let auth_event_handler = AuthEventHandler::new(auth_required_tx);
let ditto = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OnlineWithAuthentication::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
auth_event_handler.clone(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
auth_required_rx.recv().unwrap();
let res = ditto
.authenticator()
.unwrap()
.login_with_token_and_feedback(LOGIN_TOKEN, "invalid_provider");
assert!(res.is_err());
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
}
#[ignore = "Auth tests disabled by default"]
#[test]
fn logout() {
let (auth_required_tx, auth_required_rx) = mpsc::sync_channel::<()>(1);
let auth_event_handler = AuthEventHandler::new(auth_required_tx);
let ditto = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OnlineWithAuthentication::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
auth_event_handler.clone(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
auth_required_rx.recv().unwrap();
let res = ditto
.authenticator()
.unwrap()
.login_with_token_and_feedback(LOGIN_TOKEN, PROVIDER);
assert!(res.is_ok());
assert!(ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_some());
let cleanup_block_times_called = Arc::new(AtomicU8::new(0));
let cleanup_block_times_called_1 = cleanup_block_times_called.clone();
let res = ditto.authenticator().unwrap().logout(|_| {
cleanup_block_times_called_1.fetch_add(1, Ordering::SeqCst);
});
assert!(res.is_ok());
assert_eq!(cleanup_block_times_called.load(Ordering::SeqCst), 1);
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
}
#[ignore = "Auth tests disabled by default"]
#[test]
fn logout_calls_cleanup_block_even_if_not_logged_in() {
let (auth_required_tx, _) = mpsc::sync_channel::<()>(1);
let auth_event_handler = AuthEventHandler::new(auth_required_tx);
let ditto = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OnlineWithAuthentication::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
auth_event_handler.clone(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
let cleanup_block_times_called = Arc::new(AtomicU8::new(0));
let cleanup_block_times_called_1 = cleanup_block_times_called.clone();
let res = ditto.authenticator().unwrap().logout(|_| {
cleanup_block_times_called_1.fetch_add(1, Ordering::SeqCst);
});
assert!(res.is_ok());
assert_eq!(cleanup_block_times_called.load(Ordering::SeqCst), 1);
}
#[ignore = "Auth tests disabled by default"]
#[test]
fn status_is_authenticated_when_starting_an_instance_reusing_an_authenticated_directory() {
let ditto_dir = tempfile::tempdir().unwrap();
let ditto_dir_path = ditto_dir.path();
{
let (auth_required_tx, auth_required_rx) = mpsc::sync_channel::<()>(1);
let auth_event_handler = AuthEventHandler::new(auth_required_tx);
let ditto = Ditto::builder()
.with_root(Arc::new(PersistentRoot::new(ditto_dir_path).unwrap()))
.with_identity(|ditto_root| {
identity::OnlineWithAuthentication::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
auth_event_handler.clone(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
assert!(!ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_none());
auth_required_rx.recv().unwrap();
let res = ditto
.authenticator()
.unwrap()
.login_with_token_and_feedback(LOGIN_TOKEN, PROVIDER);
assert!(res.is_ok());
assert!(ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_some());
}
// We've got a new `AuthEventHandler` so the `authentication_required_times_called` counter is
// reset
let (auth_required_tx, _) = mpsc::sync_channel::<()>(1);
let auth_event_handler = AuthEventHandler::new(auth_required_tx);
let ditto = Ditto::builder()
.with_root(Arc::new(PersistentRoot::new(ditto_dir_path).unwrap()))
.with_identity(|ditto_root| {
identity::OnlineWithAuthentication::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
auth_event_handler.clone(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
assert!(ditto.authenticator().unwrap().is_authenticated());
assert!(ditto.authenticator().unwrap().user_id().is_some());
assert_eq!(
auth_event_handler
.authentication_required_times_called
.load(Ordering::SeqCst),
0
);
}
#[ignore = "Auth tests disabled by default"]
#[test]
fn online_playground_authenticates_and_syncs() {
let ditto1 = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OnlinePlayground::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
PLAYGROUND_TOKEN.to_string(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
let ditto2 = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OnlinePlayground::new(
ditto_root,
AppId::from_uuid(uuid::Uuid::parse_str(APP_ID).unwrap()),
PLAYGROUND_TOKEN.to_string(),
true,
None,
)
})
.unwrap()
.with_minimum_log_level(LogLevel::Error)
.build()
.unwrap();
let unique_collection_name = uuid::Uuid::new_v4().to_string();
let collection1 = ditto1
.store()
.collection(unique_collection_name.as_str())
.unwrap();
let collection2 = ditto2
.store()
.collection(unique_collection_name.as_str())
.unwrap();
assert!(ditto1.start_sync().is_ok());
assert!(ditto2.start_sync().is_ok());
let now = std::time::Instant::now();
while now.elapsed() < std::time::Duration::from_secs(10) {
if ditto1.authenticator().unwrap().is_authenticated()
&& ditto2.authenticator().unwrap().is_authenticated()
{
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
assert!(
ditto1.authenticator().unwrap().is_authenticated()
&& ditto2.authenticator().unwrap().is_authenticated(),
"at least one of the Ditto instances failed to authenticate"
);
let subscription1 = collection1.find_all().subscribe();
let subscription2 = collection2.find_all().subscribe();
let yellow_car_id_string = uuid::Uuid::new_v4().to_string();
let yellow_car_doc_id = DocumentId::new(&yellow_car_id_string.as_str()).unwrap();
assert!(collection1
.find_by_id(yellow_car_doc_id.clone())
.exec()
.is_err());
assert!(collection2
.find_by_id(yellow_car_doc_id.clone())
.exec()
.is_err());
let _doc_id = collection1
.upsert(serde_json::json!({"color": "yellow", "_id": yellow_car_doc_id}))
.unwrap();
let found_doc = collection1
.find_by_id(yellow_car_doc_id.clone())
.exec()
.unwrap();
assert_eq!(found_doc.get::<String>("color").unwrap(), "yellow");
let (tx, rx) = mpsc::sync_channel(1);
let handler = move |doc: Option<BoxedDocument>, _event: SingleDocumentLiveQueryEvent| {
if doc.is_some() {
tx.send(()).unwrap();
}
};
let car_at_ditto2_live_query = collection2
.find_by_id(yellow_car_doc_id.clone())
.observe_local(handler)
.unwrap();
rx.recv_timeout(Duration::from_secs(10)).unwrap();
let found_doc = collection2.find_by_id(yellow_car_doc_id).exec().unwrap();
assert_eq!(found_doc.get::<String>("color").unwrap(), "yellow");
subscription1.stop();
subscription2.stop();
car_at_ditto2_live_query.stop();
}
#[derive(Clone)]
pub struct AuthEventHandler {
/// Count of how many times `authentication_required` has been called
pub authentication_required_times_called: Arc<AtomicU8>,
/// Count of how many times `authentication_expiring_soon` has been called
pub authentication_expiring_soon_times_called: Arc<AtomicU8>,
/// A sync_channel sender to send a message to when there has been a call 1to
/// `authentication_required`
auth_required_tx: Arc<mpsc::SyncSender<()>>,
}
impl AuthEventHandler {
pub fn new(auth_required_tx: mpsc::SyncSender<()>) -> Self {
Self {
authentication_required_times_called: Arc::new(AtomicU8::new(0)),
authentication_expiring_soon_times_called: Arc::new(AtomicU8::new(0)),
auth_required_tx: Arc::new(auth_required_tx),
}
}
}
impl DittoAuthenticationEventHandler for AuthEventHandler {
fn authentication_required(&self, _auth: DittoAuthenticator) {
self.authentication_required_times_called
.fetch_add(1, Ordering::SeqCst);
self.auth_required_tx.send(()).unwrap();
}
fn authentication_expiring_soon(
&self,
_auth: DittoAuthenticator,
_seconds_remaining: Duration,
) {
self.authentication_expiring_soon_times_called
.fetch_add(1, Ordering::SeqCst);
}
}