test: add unit tests for RPC framing, SDK state machine, and server domain services

Add comprehensive tests across three layers:
- RPC framing: empty payloads, max boundary, truncated frames, multi-frame buffers,
  all status codes, all method ID ranges, payload-too-large for response/push
- SDK: event broadcast send/receive, multiple subscribers, clone preservation,
  conversation upsert, missing conversation, message ID roundtrip, member keys
- Server domain: auth session validation/expiry, channel creation/symmetry/validation,
  delivery peek/ack/sequence ordering/fetch-limited, key package upload/fetch/validation,
  hybrid key batch fetch, size boundary tests
- CI: MSRV (1.75) check job, macOS cross-platform build check
This commit is contained in:
2026-03-08 18:07:43 +01:00
parent e4c5868b31
commit 872695e5f1
8 changed files with 1114 additions and 0 deletions

View File

@@ -70,3 +70,97 @@ impl AuthService {
Ok(RegisterFinishResp { success: true })
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn test_service() -> AuthService {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(crate::storage::FileBackedStore::open(dir.path()).unwrap());
let mut rng = rand::rngs::OsRng;
let opaque_setup = ServerSetup::<OpaqueSuite>::new(&mut rng);
AuthService {
store,
opaque_setup: Arc::new(opaque_setup),
pending_logins: Arc::new(DashMap::new()),
sessions: Arc::new(DashMap::new()),
auth_cfg: Arc::new(AuthConfig {
required_token: None,
allow_insecure_identity_from_request: false,
}),
}
}
#[test]
fn validate_session_valid_token() {
let svc = test_service();
let token = vec![1u8; 16];
let ik = vec![2u8; 32];
svc.sessions.insert(
token.clone(),
SessionInfo {
username: "alice".to_string(),
identity_key: ik.clone(),
created_at: crate::auth::current_timestamp(),
expires_at: crate::auth::current_timestamp() + 3600,
},
);
let auth = svc.validate_session(&token).unwrap();
assert_eq!(auth.identity_key, ik);
assert_eq!(auth.token, token);
assert!(auth.device_id.is_none());
}
#[test]
fn validate_session_expired_token() {
let svc = test_service();
let token = vec![3u8; 16];
svc.sessions.insert(
token.clone(),
SessionInfo {
username: "bob".to_string(),
identity_key: vec![4u8; 32],
created_at: 0,
expires_at: 0, // already expired
},
);
assert!(svc.validate_session(&token).is_none());
// Expired session should be removed from the map
assert!(!svc.sessions.contains_key(&token));
}
#[test]
fn validate_session_missing_token() {
let svc = test_service();
assert!(svc.validate_session(&[0u8; 16]).is_none());
}
#[test]
fn validate_session_removes_expired_on_check() {
let svc = test_service();
let token = vec![5u8; 16];
svc.sessions.insert(
token.clone(),
SessionInfo {
username: "eve".to_string(),
identity_key: vec![6u8; 32],
created_at: 0,
expires_at: 1, // expired long ago
},
);
// First check: returns None and removes
assert!(svc.validate_session(&token).is_none());
// Session should be gone
assert!(svc.sessions.is_empty());
}
}

View File

@@ -36,3 +36,96 @@ impl ChannelService {
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::storage::FileBackedStore;
fn test_service() -> (tempfile::TempDir, ChannelService) {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(FileBackedStore::open(dir.path()).unwrap());
let svc = ChannelService { store };
(dir, svc)
}
#[test]
fn create_channel_success() {
let (_dir, svc) = test_service();
let caller = vec![1u8; 32];
let peer = vec![2u8; 32];
let resp = svc
.create_channel(CreateChannelReq { peer_key: peer.clone() }, &caller)
.unwrap();
assert!(resp.was_new);
assert_eq!(resp.channel_id.len(), 16);
}
#[test]
fn create_channel_idempotent() {
let (_dir, svc) = test_service();
let caller = vec![1u8; 32];
let peer = vec![2u8; 32];
let resp1 = svc
.create_channel(CreateChannelReq { peer_key: peer.clone() }, &caller)
.unwrap();
assert!(resp1.was_new);
let resp2 = svc
.create_channel(CreateChannelReq { peer_key: peer.clone() }, &caller)
.unwrap();
assert!(!resp2.was_new);
assert_eq!(resp1.channel_id, resp2.channel_id);
}
#[test]
fn create_channel_symmetric() {
let (_dir, svc) = test_service();
let a = vec![1u8; 32];
let b = vec![2u8; 32];
let resp_ab = svc
.create_channel(CreateChannelReq { peer_key: b.clone() }, &a)
.unwrap();
let resp_ba = svc
.create_channel(CreateChannelReq { peer_key: a.clone() }, &b)
.unwrap();
// Same channel regardless of who initiates
assert_eq!(resp_ab.channel_id, resp_ba.channel_id);
}
#[test]
fn create_channel_rejects_invalid_peer_key_length() {
let (_dir, svc) = test_service();
let caller = vec![1u8; 32];
let err = svc
.create_channel(CreateChannelReq { peer_key: vec![1u8; 31] }, &caller)
.unwrap_err();
assert!(matches!(err, DomainError::InvalidIdentityKey(31)));
let err = svc
.create_channel(CreateChannelReq { peer_key: vec![1u8; 33] }, &caller)
.unwrap_err();
assert!(matches!(err, DomainError::InvalidIdentityKey(33)));
let err = svc
.create_channel(CreateChannelReq { peer_key: vec![] }, &caller)
.unwrap_err();
assert!(matches!(err, DomainError::InvalidIdentityKey(0)));
}
#[test]
fn create_channel_rejects_self_channel() {
let (_dir, svc) = test_service();
let me = vec![5u8; 32];
let err = svc
.create_channel(CreateChannelReq { peer_key: me.clone() }, &me)
.unwrap_err();
assert!(matches!(err, DomainError::BadParams(_)));
}
}

View File

@@ -353,4 +353,184 @@ mod tests {
let bare = device_recipient_key(&ik, &[]);
assert_eq!(bare, ik);
}
#[test]
fn peek_does_not_drain() {
let (_dir, svc) = test_service();
let ik = vec![10u8; 32];
let ch = vec![0u8; 16];
svc.enqueue(EnqueueReq {
recipient_key: ik.clone(),
payload: b"peek-me".to_vec(),
channel_id: ch.clone(),
ttl_secs: 0,
})
.unwrap();
// Peek should return the message without removing it.
let peeked = svc
.peek(PeekReq {
recipient_key: ik.clone(),
channel_id: ch.clone(),
limit: 10,
})
.unwrap();
assert_eq!(peeked.payloads.len(), 1);
assert_eq!(peeked.payloads[0].data, b"peek-me");
// Peek again — still there.
let peeked2 = svc
.peek(PeekReq {
recipient_key: ik.clone(),
channel_id: ch.clone(),
limit: 10,
})
.unwrap();
assert_eq!(peeked2.payloads.len(), 1);
// Fetch drains it.
let fetched = svc
.fetch(FetchReq {
recipient_key: ik.clone(),
channel_id: ch.clone(),
limit: 10,
})
.unwrap();
assert_eq!(fetched.payloads.len(), 1);
// Now peek returns empty.
let peeked3 = svc
.peek(PeekReq {
recipient_key: ik,
channel_id: ch,
limit: 10,
})
.unwrap();
assert!(peeked3.payloads.is_empty());
}
#[test]
fn ack_removes_messages_up_to_seq() {
let (_dir, svc) = test_service();
let ik = vec![11u8; 32];
let ch = vec![0u8; 16];
// Enqueue 3 messages (use peek to verify without draining).
for i in 0..3 {
svc.enqueue(EnqueueReq {
recipient_key: ik.clone(),
payload: format!("msg-{i}").into_bytes(),
channel_id: ch.clone(),
ttl_secs: 0,
})
.unwrap();
}
let all = svc
.peek(PeekReq {
recipient_key: ik.clone(),
channel_id: ch.clone(),
limit: 10,
})
.unwrap();
assert_eq!(all.payloads.len(), 3);
// Ack up to seq of the second message.
let ack_seq = all.payloads[1].seq;
svc.ack(AckReq {
recipient_key: ik.clone(),
channel_id: ch.clone(),
seq_up_to: ack_seq,
})
.unwrap();
// Only the third message should remain.
let remaining = svc
.peek(PeekReq {
recipient_key: ik,
channel_id: ch,
limit: 10,
})
.unwrap();
assert_eq!(remaining.payloads.len(), 1);
assert_eq!(remaining.payloads[0].data, b"msg-2");
}
#[test]
fn fetch_empty_queue() {
let (_dir, svc) = test_service();
let ik = vec![20u8; 32];
let ch = vec![0u8; 16];
let resp = svc
.fetch(FetchReq {
recipient_key: ik,
channel_id: ch,
limit: 0,
})
.unwrap();
assert!(resp.payloads.is_empty());
}
#[test]
fn enqueue_sequence_numbers_increase() {
let (_dir, svc) = test_service();
let ik = vec![30u8; 32];
let ch = vec![0u8; 16];
let r1 = svc
.enqueue(EnqueueReq {
recipient_key: ik.clone(),
payload: b"a".to_vec(),
channel_id: ch.clone(),
ttl_secs: 0,
})
.unwrap();
let r2 = svc
.enqueue(EnqueueReq {
recipient_key: ik.clone(),
payload: b"b".to_vec(),
channel_id: ch.clone(),
ttl_secs: 0,
})
.unwrap();
let r3 = svc
.enqueue(EnqueueReq {
recipient_key: ik,
payload: b"c".to_vec(),
channel_id: ch,
ttl_secs: 0,
})
.unwrap();
assert!(r2.seq > r1.seq);
assert!(r3.seq > r2.seq);
}
#[test]
fn fetch_limited_respects_limit() {
let (_dir, svc) = test_service();
let ik = vec![40u8; 32];
let ch = vec![0u8; 16];
for i in 0..5 {
svc.enqueue(EnqueueReq {
recipient_key: ik.clone(),
payload: format!("msg-{i}").into_bytes(),
channel_id: ch.clone(),
ttl_secs: 0,
})
.unwrap();
}
let resp = svc
.fetch(FetchReq {
recipient_key: ik,
channel_id: ch,
limit: 2,
})
.unwrap();
assert_eq!(resp.payloads.len(), 2);
}
}

View File

@@ -95,3 +95,241 @@ impl KeyService {
Ok(FetchHybridKeysResp { keys })
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::storage::FileBackedStore;
fn test_service() -> (tempfile::TempDir, KeyService) {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(FileBackedStore::open(dir.path()).unwrap());
let svc = KeyService { store };
(dir, svc)
}
fn test_auth() -> CallerAuth {
CallerAuth {
identity_key: vec![1u8; 32],
token: vec![0u8; 16],
device_id: None,
}
}
#[test]
fn upload_and_fetch_key_package() {
let (_dir, svc) = test_service();
let auth = test_auth();
let ik = vec![1u8; 32];
let package = vec![42u8; 128];
let resp = svc
.upload_key_package(
UploadKeyPackageReq {
identity_key: ik.clone(),
package: package.clone(),
},
&auth,
)
.unwrap();
// Fingerprint is SHA-256 of the package
assert_eq!(resp.fingerprint.len(), 32);
let fetched = svc
.fetch_key_package(FetchKeyPackageReq { identity_key: ik }, &auth)
.unwrap();
assert_eq!(fetched.package, package);
}
#[test]
fn fetch_key_package_missing() {
let (_dir, svc) = test_service();
let auth = test_auth();
let resp = svc
.fetch_key_package(
FetchKeyPackageReq { identity_key: vec![99u8; 32] },
&auth,
)
.unwrap();
assert!(resp.package.is_empty());
}
#[test]
fn upload_key_package_rejects_invalid_identity_key() {
let (_dir, svc) = test_service();
let auth = test_auth();
let err = svc
.upload_key_package(
UploadKeyPackageReq {
identity_key: vec![1u8; 31],
package: vec![1u8; 10],
},
&auth,
)
.unwrap_err();
assert!(matches!(err, DomainError::InvalidIdentityKey(31)));
}
#[test]
fn upload_key_package_rejects_empty_package() {
let (_dir, svc) = test_service();
let auth = test_auth();
let err = svc
.upload_key_package(
UploadKeyPackageReq {
identity_key: vec![1u8; 32],
package: vec![],
},
&auth,
)
.unwrap_err();
assert!(matches!(err, DomainError::EmptyPackage));
}
#[test]
fn upload_key_package_rejects_oversized() {
let (_dir, svc) = test_service();
let auth = test_auth();
let err = svc
.upload_key_package(
UploadKeyPackageReq {
identity_key: vec![1u8; 32],
package: vec![0u8; MAX_KEYPACKAGE_BYTES + 1],
},
&auth,
)
.unwrap_err();
assert!(matches!(err, DomainError::PackageTooLarge(_)));
}
#[test]
fn upload_and_fetch_hybrid_key() {
let (_dir, svc) = test_service();
let auth = test_auth();
let ik = vec![2u8; 32];
let hk = vec![0xABu8; 1184]; // ML-KEM-768 public key size
svc.upload_hybrid_key(
UploadHybridKeyReq {
identity_key: ik.clone(),
hybrid_public_key: hk.clone(),
},
&auth,
)
.unwrap();
let resp = svc
.fetch_hybrid_key(FetchHybridKeyReq { identity_key: ik }, &auth)
.unwrap();
assert_eq!(resp.hybrid_public_key, hk);
}
#[test]
fn fetch_hybrid_key_missing() {
let (_dir, svc) = test_service();
let auth = test_auth();
let resp = svc
.fetch_hybrid_key(
FetchHybridKeyReq { identity_key: vec![99u8; 32] },
&auth,
)
.unwrap();
assert!(resp.hybrid_public_key.is_empty());
}
#[test]
fn upload_hybrid_key_rejects_invalid_identity() {
let (_dir, svc) = test_service();
let auth = test_auth();
let err = svc
.upload_hybrid_key(
UploadHybridKeyReq {
identity_key: vec![1u8; 10],
hybrid_public_key: vec![1u8; 100],
},
&auth,
)
.unwrap_err();
assert!(matches!(err, DomainError::InvalidIdentityKey(10)));
}
#[test]
fn upload_hybrid_key_rejects_empty() {
let (_dir, svc) = test_service();
let auth = test_auth();
let err = svc
.upload_hybrid_key(
UploadHybridKeyReq {
identity_key: vec![1u8; 32],
hybrid_public_key: vec![],
},
&auth,
)
.unwrap_err();
assert!(matches!(err, DomainError::EmptyHybridKey));
}
#[test]
fn fetch_hybrid_keys_batch() {
let (_dir, svc) = test_service();
let auth = test_auth();
let ik1 = vec![1u8; 32];
let ik2 = vec![2u8; 32];
let ik3 = vec![3u8; 32]; // no hybrid key uploaded
svc.upload_hybrid_key(
UploadHybridKeyReq {
identity_key: ik1.clone(),
hybrid_public_key: vec![0xAAu8; 64],
},
&auth,
)
.unwrap();
svc.upload_hybrid_key(
UploadHybridKeyReq {
identity_key: ik2.clone(),
hybrid_public_key: vec![0xBBu8; 64],
},
&auth,
)
.unwrap();
let resp = svc
.fetch_hybrid_keys(
FetchHybridKeysReq {
identity_keys: vec![ik1, ik2, ik3],
},
&auth,
)
.unwrap();
assert_eq!(resp.keys.len(), 3);
assert_eq!(resp.keys[0], vec![0xAAu8; 64]);
assert_eq!(resp.keys[1], vec![0xBBu8; 64]);
assert!(resp.keys[2].is_empty()); // missing key returns empty
}
#[test]
fn upload_key_package_at_max_size() {
let (_dir, svc) = test_service();
let auth = test_auth();
// Exactly at max should succeed
let resp = svc
.upload_key_package(
UploadKeyPackageReq {
identity_key: vec![1u8; 32],
package: vec![0u8; MAX_KEYPACKAGE_BYTES],
},
&auth,
);
assert!(resp.is_ok());
}
}