//! Shared fixtures for the integration suite: the JSON bodies the SyncKit server //! would return, and the imports every module needs. //! //! Every test module imports this with `use crate::common::*;`. A helper earns a //! place here when a second module wants it; a fixture only one module uses stays //! in that module. //! //! The server and the clients live next door in [`mockkit`](crate::mockkit), //! re-exported below so the one glob import still reaches everything. pub(crate) use base64::Engine as _; pub(crate) use chrono::Utc; pub(crate) use serde_json::json; pub(crate) use sha2::Digest as _; pub(crate) use std::sync::Arc; pub(crate) use std::time::Duration; pub(crate) use uuid::Uuid; pub(crate) use wiremock::ResponseTemplate; pub(crate) use crate::mockkit::MockKit; pub(crate) use synckit_client::{ AppId, ChangeEntry, ChangeOp, DeviceId, Hlc, SyncKitClient, SyncKitConfig, SyncKitError, UserId, }; pub(crate) fn fake_jwt(exp: i64) -> String { let header = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(r#"{"alg":"HS256","typ":"JWT"}"#); let payload = json!({ "sub": "550e8400-e29b-41d4-a716-446655440000", "app": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "exp": exp, "iat": exp - 3600, }); let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes()); let sig = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"fake-signature"); format!("{header}.{payload_b64}.{sig}") } pub(crate) fn fresh_token() -> String { fake_jwt(Utc::now().timestamp() + 3600) } pub(crate) fn test_ids() -> (UserId, AppId) { ( UserId::new(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()), AppId::new(Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap()), ) } /// Install the rustls crypto provider once. reqwest is built `rustls-no-provider`, /// so a real consumer app installs one at startup (audiofiles installs ring); these /// tests have no such app, so they install ring themselves before building a client. pub(crate) fn ensure_crypto_provider() { static PROVIDER: std::sync::Once = std::sync::Once::new(); PROVIDER.call_once(|| { // Err means a provider is already installed, which is the outcome we want. let _ = rustls::crypto::ring::default_provider().install_default(); }); } pub(crate) fn auth_response_json() -> serde_json::Value { let (user_id, app_id) = test_ids(); json!({ "token": fresh_token(), "user_id": user_id, "app_id": app_id, }) } pub(crate) fn device_json() -> serde_json::Value { let (user_id, app_id) = test_ids(); json!({ "id": Uuid::new_v4(), "app_id": app_id, "user_id": user_id, "device_name": "Test Device", "platform": "test", "last_seen_at": "2025-01-01T00:00:00Z", "created_at": "2025-01-01T00:00:00Z", }) } /// Install the `keyring_core` in-memory mock as the process-global keychain /// store, once. /// /// `rotate_key` finishes by caching the new master key through /// `keystore::store_key`, which under the `keychain` feature reaches the host's /// real secret service. That is unavailable on a headless box, and writing a /// test key into a developer's login keyring is not wanted either. The mock is a /// store like any other and `keystore::entry` prefers an already-installed one /// over the platform default, so installing it here puts the shipping keychain /// code path under test with no daemon and no host state. /// /// It is process-global and installed for the life of the binary, so a test that /// asserts on keychain contents must use its own `(app_id, user_id)` pair: the /// suite's shared [`test_ids`] entry is written by every rotation test that runs. #[cfg(feature = "keychain")] pub(crate) fn ensure_mock_keystore() { static STORE: std::sync::Once = std::sync::Once::new(); STORE.call_once(|| { keyring_core::set_default_store( keyring_core::mock::Store::new().expect("the mock keychain store"), ); }); } /// No store to install: without the `keychain` feature `keystore::store_key` is /// the no-op stub, so the same orchestration runs with the cache write neutered. #[cfg(not(feature = "keychain"))] pub(crate) fn ensure_mock_keystore() {}