Skip to main content

max / synckit

4.3 KB · 113 lines History Blame Raw
1 //! Shared fixtures for the integration suite: the JSON bodies the SyncKit server
2 //! would return, and the imports every module needs.
3 //!
4 //! Every test module imports this with `use crate::common::*;`. A helper earns a
5 //! place here when a second module wants it; a fixture only one module uses stays
6 //! in that module.
7 //!
8 //! The server and the clients live next door in [`mockkit`](crate::mockkit),
9 //! re-exported below so the one glob import still reaches everything.
10
11 pub(crate) use base64::Engine as _;
12 pub(crate) use chrono::Utc;
13 pub(crate) use serde_json::json;
14 pub(crate) use sha2::Digest as _;
15 pub(crate) use std::sync::Arc;
16 pub(crate) use std::time::Duration;
17 pub(crate) use uuid::Uuid;
18 pub(crate) use wiremock::ResponseTemplate;
19
20 pub(crate) use crate::mockkit::MockKit;
21
22 pub(crate) use synckit_client::{
23 AppId, ChangeEntry, ChangeOp, DeviceId, Hlc, SyncKitClient, SyncKitConfig, SyncKitError, UserId,
24 };
25
26 pub(crate) fn fake_jwt(exp: i64) -> String {
27 let header =
28 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(r#"{"alg":"HS256","typ":"JWT"}"#);
29 let payload = json!({
30 "sub": "550e8400-e29b-41d4-a716-446655440000",
31 "app": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
32 "exp": exp,
33 "iat": exp - 3600,
34 });
35 let payload_b64 =
36 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
37 let sig = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"fake-signature");
38 format!("{header}.{payload_b64}.{sig}")
39 }
40
41 pub(crate) fn fresh_token() -> String {
42 fake_jwt(Utc::now().timestamp() + 3600)
43 }
44
45 pub(crate) fn test_ids() -> (UserId, AppId) {
46 (
47 UserId::new(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()),
48 AppId::new(Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap()),
49 )
50 }
51
52 /// Install the rustls crypto provider once. reqwest is built `rustls-no-provider`,
53 /// so a real consumer app installs one at startup (audiofiles installs ring); these
54 /// tests have no such app, so they install ring themselves before building a client.
55 pub(crate) fn ensure_crypto_provider() {
56 static PROVIDER: std::sync::Once = std::sync::Once::new();
57 PROVIDER.call_once(|| {
58 // Err means a provider is already installed, which is the outcome we want.
59 let _ = rustls::crypto::ring::default_provider().install_default();
60 });
61 }
62
63 pub(crate) fn auth_response_json() -> serde_json::Value {
64 let (user_id, app_id) = test_ids();
65 json!({
66 "token": fresh_token(),
67 "user_id": user_id,
68 "app_id": app_id,
69 })
70 }
71
72 pub(crate) fn device_json() -> serde_json::Value {
73 let (user_id, app_id) = test_ids();
74 json!({
75 "id": Uuid::new_v4(),
76 "app_id": app_id,
77 "user_id": user_id,
78 "device_name": "Test Device",
79 "platform": "test",
80 "last_seen_at": "2025-01-01T00:00:00Z",
81 "created_at": "2025-01-01T00:00:00Z",
82 })
83 }
84
85 /// Install the `keyring_core` in-memory mock as the process-global keychain
86 /// store, once.
87 ///
88 /// `rotate_key` finishes by caching the new master key through
89 /// `keystore::store_key`, which under the `keychain` feature reaches the host's
90 /// real secret service. That is unavailable on a headless box, and writing a
91 /// test key into a developer's login keyring is not wanted either. The mock is a
92 /// store like any other and `keystore::entry` prefers an already-installed one
93 /// over the platform default, so installing it here puts the shipping keychain
94 /// code path under test with no daemon and no host state.
95 ///
96 /// It is process-global and installed for the life of the binary, so a test that
97 /// asserts on keychain contents must use its own `(app_id, user_id)` pair: the
98 /// suite's shared [`test_ids`] entry is written by every rotation test that runs.
99 #[cfg(feature = "keychain")]
100 pub(crate) fn ensure_mock_keystore() {
101 static STORE: std::sync::Once = std::sync::Once::new();
102 STORE.call_once(|| {
103 keyring_core::set_default_store(
104 keyring_core::mock::Store::new().expect("the mock keychain store"),
105 );
106 });
107 }
108
109 /// No store to install: without the `keychain` feature `keystore::store_key` is
110 /// the no-op stub, so the same orchestration runs with the cache write neutered.
111 #[cfg(not(feature = "keychain"))]
112 pub(crate) fn ensure_mock_keystore() {}
113