//! The Sync section, driven through the router. //! //! The section exists because somebody measured `commands/sync.rs` per command //! rather than per file, so the tests worth reading are the ones that pin what //! is here and what is not. A later pass that "finishes" this section by adding //! Sync Now would be undoing a measurement, not filling a gap. use std::sync::Arc; use quasi_http::Serves as _; use quasi_router::{Outcome, Params, Request}; use crate::quasi::router; use crate::state::AppState; async fn state() -> Arc { let (state, _) = crate::test_utils::setup_test_state().await; state } fn html(state: &AppState, path: &str) -> String { let response = router() .handle(state, Request::get(path).carrying(Params::new())) .expect("the route answers"); match &response.outcome { Outcome::Screen(screen) => quasi_webview::Webview::new().screen(screen), Outcome::Fragment { node, .. } => quasi_webview::Webview::new().fragment(node), other => panic!("expected content, got {other:?}"), } } #[tokio::test] async fn sync_is_a_section_of_settings() { let state = state().await; let markup = html(&state, "/settings/sync"); assert!(markup.contains("Cloud Sync"), "{markup}"); assert!(markup.contains("/settings/sync"), "the sidebar offers it"); } #[tokio::test] async fn a_device_with_no_sync_says_so_rather_than_drawing_an_empty_form() { // A test state has no sync client. Drawing dashes and disabled switches // would be a section pretending to be about something. let state = state().await; assert!( state.read_recovering().is_none(), "the fixture has no client" ); let markup = html(&state, "/settings/sync"); assert!(markup.contains("not set up"), "{markup}"); assert!(!markup.contains("Minutes between syncs"), "{markup}"); assert!(!markup.contains("Disconnect"), "{markup}"); } #[tokio::test] async fn the_settings_are_written_even_with_no_client() { // The switch and the interval are `sync_state` rows the scheduler reads. // They are not the client's, so they are writable whether or not sync is // set up, and this is the half that proves the section is local. let state = state().await; let response = router() .handle( &state, Request::post("/settings/sync/interval") .sending(Params::new().with(super::INTERVAL, "30")), ) .expect("the route answers"); assert!(matches!(response.outcome, Outcome::Fragment { .. })); let written = crate::syncstore::sync_state::get_sync_states_batch(&state.db, &[super::INTERVAL]) .expect("read back"); assert_eq!(written.get(super::INTERVAL).map(String::as_str), Some("30")); } #[tokio::test] async fn a_silly_interval_is_clamped_rather_than_refused() { // What every other numeric control on this screen does. let state = state().await; router() .handle( &state, Request::post("/settings/sync/interval") .sending(Params::new().with(super::INTERVAL, "999999")), ) .expect("the route answers"); let written = crate::syncstore::sync_state::get_sync_states_batch(&state.db, &[super::INTERVAL]) .expect("read back"); assert_eq!( written.get(super::INTERVAL).map(String::as_str), Some("1440") ); } #[tokio::test] async fn turning_automatic_syncing_off_writes_the_row_the_scheduler_reads() { let state = state().await; router() .handle( &state, Request::post("/settings/sync/auto") .sending(Params::new().with(super::AUTO_SYNC, "disabled")), ) .expect("the route answers"); let written = crate::syncstore::sync_state::get_sync_states_batch(&state.db, &[super::AUTO_SYNC]) .expect("read back"); assert_eq!(written.get(super::AUTO_SYNC).map(String::as_str), Some("0")); // Absent means on, so turning it back on has to write the row rather than // clear it: the scheduler reads `is_none_or(|v| v == "1")`. router() .handle( &state, Request::post("/settings/sync/auto") .sending(Params::new().with(super::AUTO_SYNC, "enabled")), ) .expect("the route answers"); let written = crate::syncstore::sync_state::get_sync_states_batch(&state.db, &[super::AUTO_SYNC]) .expect("read back"); assert_eq!(written.get(super::AUTO_SYNC).map(String::as_str), Some("1")); } #[tokio::test] async fn the_controls_that_need_a_server_are_absent() { // Pinned so they read as a measurement rather than an unfinished section. // Each of these is a conversation with a server, which a synchronous route // handler cannot have; see the module header and quasicoherent 82273265. let state = state().await; let markup = html(&state, "/settings/sync"); for absent in ["Sync Now", "Set up encryption", "Subscribe", "Connect"] { assert!(!markup.contains(absent), "should not offer: {absent}"); } }