test: 15 cross-project integration tests — WZP ↔ featherChat verified
Identity (6 tests): - Same seed → same Ed25519/X25519 keys, same fingerprint, same display - Random seed, raw HKDF output verified BIP39 Mnemonic (3 tests): - Roundtrip both directions, identical strings CallSignal Interop (4 tests): - Offer/Answer/Hangup roundtrip through FC bincode serialization - Signal type mapping verified Auth Contract (2 tests): - Request/response shapes match between WZP and FC Uses warzone-protocol v0.0.21 as real dependency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1109
Cargo.lock
generated
1109
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -18,4 +18,15 @@ tracing = { workspace = true }
|
||||
bip39 = "2"
|
||||
hex = "0.4"
|
||||
|
||||
# featherChat identity — the source of truth for Seed, IdentityKeyPair, Fingerprint
|
||||
warzone-protocol = { path = "../../deps/featherchat/warzone/crates/warzone-protocol" }
|
||||
|
||||
[dev-dependencies]
|
||||
ed25519-dalek = { workspace = true }
|
||||
warzone-protocol = { path = "../../deps/featherchat/warzone/crates/warzone-protocol" }
|
||||
wzp-proto = { workspace = true }
|
||||
wzp-client = { path = "../wzp-client" }
|
||||
wzp-relay = { path = "../wzp-relay" }
|
||||
serde_json = "1"
|
||||
serde = { workspace = true }
|
||||
bincode = "1"
|
||||
|
||||
313
crates/wzp-crypto/tests/featherchat_compat.rs
Normal file
313
crates/wzp-crypto/tests/featherchat_compat.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
//! Cross-project compatibility tests between WZP and featherChat.
|
||||
//!
|
||||
//! Verifies:
|
||||
//! 1. Identity: same seed → same keys → same fingerprints (WZP-FC-8)
|
||||
//! 2. CallSignal: WZP SignalMessage serializes into FC CallSignal.payload correctly
|
||||
//! 3. Auth: WZP auth module request/response matches FC's /v1/auth/validate contract
|
||||
//! 4. Mnemonic: BIP39 interop between both implementations
|
||||
|
||||
use wzp_proto::KeyExchange;
|
||||
|
||||
// ─── Identity Compatibility (WZP-FC-8) ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn same_seed_same_ed25519_key() {
|
||||
let seed = [42u8; 32];
|
||||
|
||||
let wzp_kx = wzp_crypto::WarzoneKeyExchange::from_identity_seed(&seed);
|
||||
let wzp_pub = wzp_kx.identity_public_key();
|
||||
|
||||
let fc_seed = warzone_protocol::identity::Seed::from_bytes(seed);
|
||||
let fc_id = fc_seed.derive_identity();
|
||||
let fc_pub = fc_id.signing.verifying_key();
|
||||
|
||||
assert_eq!(&wzp_pub, fc_pub.as_bytes(), "Ed25519 keys must match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_seed_same_fingerprint() {
|
||||
let seed = [99u8; 32];
|
||||
|
||||
let wzp_kx = wzp_crypto::WarzoneKeyExchange::from_identity_seed(&seed);
|
||||
let wzp_fp = wzp_kx.fingerprint();
|
||||
|
||||
let fc_seed = warzone_protocol::identity::Seed::from_bytes(seed);
|
||||
let fc_fp = fc_seed.derive_identity().public_identity().fingerprint.0;
|
||||
|
||||
assert_eq!(wzp_fp, fc_fp, "Fingerprints must match");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wzp_identity_module_matches_featherchat() {
|
||||
let seed = [0xAB; 32];
|
||||
|
||||
let wzp_pub = wzp_crypto::Seed::from_bytes(seed)
|
||||
.derive_identity()
|
||||
.public_identity();
|
||||
|
||||
let fc_pub = warzone_protocol::identity::Seed::from_bytes(seed)
|
||||
.derive_identity()
|
||||
.public_identity();
|
||||
|
||||
assert_eq!(wzp_pub.signing.as_bytes(), fc_pub.signing.as_bytes());
|
||||
assert_eq!(wzp_pub.encryption.as_bytes(), fc_pub.encryption.as_bytes());
|
||||
assert_eq!(wzp_pub.fingerprint.0, fc_pub.fingerprint.0);
|
||||
assert_eq!(wzp_pub.fingerprint.to_string(), fc_pub.fingerprint.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_seed_identity_match() {
|
||||
let fc_seed = warzone_protocol::identity::Seed::generate();
|
||||
let raw = fc_seed.0;
|
||||
|
||||
let fc_fp = fc_seed.derive_identity().public_identity().fingerprint.0;
|
||||
let wzp_fp = wzp_crypto::WarzoneKeyExchange::from_identity_seed(&raw).fingerprint();
|
||||
|
||||
assert_eq!(wzp_fp, fc_fp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hkdf_derive_matches() {
|
||||
let seed = [0x55; 32];
|
||||
|
||||
let fc_ed = warzone_protocol::crypto::hkdf_derive(&seed, b"", b"warzone-ed25519", 32);
|
||||
let fc_signing = ed25519_dalek::SigningKey::from_bytes(&fc_ed.try_into().unwrap());
|
||||
let fc_pub = fc_signing.verifying_key();
|
||||
|
||||
let wzp_pub = wzp_crypto::WarzoneKeyExchange::from_identity_seed(&seed).identity_public_key();
|
||||
|
||||
assert_eq!(&wzp_pub, fc_pub.as_bytes());
|
||||
}
|
||||
|
||||
// ─── BIP39 Mnemonic Interop ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn mnemonic_roundtrip_fc_to_wzp() {
|
||||
let seed = [0x77; 32];
|
||||
let fc_mnemonic = warzone_protocol::identity::Seed::from_bytes(seed).to_mnemonic();
|
||||
let wzp_recovered = wzp_crypto::Seed::from_mnemonic(&fc_mnemonic).unwrap();
|
||||
assert_eq!(wzp_recovered.0, seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mnemonic_roundtrip_wzp_to_fc() {
|
||||
let seed = [0x33; 32];
|
||||
let wzp_mnemonic = wzp_crypto::Seed::from_bytes(seed).to_mnemonic();
|
||||
let fc_recovered = warzone_protocol::identity::Seed::from_mnemonic(&wzp_mnemonic).unwrap();
|
||||
assert_eq!(fc_recovered.0, seed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mnemonic_strings_identical() {
|
||||
let seed = [0xDE; 32];
|
||||
let fc_words = warzone_protocol::identity::Seed::from_bytes(seed).to_mnemonic();
|
||||
let wzp_words = wzp_crypto::Seed::from_bytes(seed).to_mnemonic();
|
||||
assert_eq!(fc_words, wzp_words);
|
||||
}
|
||||
|
||||
// ─── CallSignal Payload Interop ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn wzp_signal_serializes_into_fc_callsignal_payload() {
|
||||
// WZP creates a CallOffer SignalMessage
|
||||
let offer = wzp_proto::SignalMessage::CallOffer {
|
||||
identity_pub: [1u8; 32],
|
||||
ephemeral_pub: [2u8; 32],
|
||||
signature: vec![3u8; 64],
|
||||
supported_profiles: vec![wzp_proto::QualityProfile::GOOD],
|
||||
};
|
||||
|
||||
// Encode as featherChat CallSignal payload
|
||||
let payload = wzp_client::featherchat::encode_call_payload(
|
||||
&offer,
|
||||
Some("relay.example.com:4433"),
|
||||
Some("myroom"),
|
||||
);
|
||||
|
||||
// Verify it's valid JSON
|
||||
let parsed: serde_json::Value = serde_json::from_str(&payload).unwrap();
|
||||
assert!(parsed.get("signal").is_some());
|
||||
assert_eq!(parsed["relay_addr"], "relay.example.com:4433");
|
||||
assert_eq!(parsed["room"], "myroom");
|
||||
|
||||
// featherChat would put this in WireMessage::CallSignal { payload, ... }
|
||||
// Verify the FC side can create a CallSignal with this payload
|
||||
let fc_msg = warzone_protocol::message::WireMessage::CallSignal {
|
||||
id: "call-123".to_string(),
|
||||
sender_fingerprint: "abcd1234".to_string(),
|
||||
signal_type: warzone_protocol::message::CallSignalType::Offer,
|
||||
payload: payload.clone(),
|
||||
target: "peer-fingerprint".to_string(),
|
||||
};
|
||||
|
||||
// Verify it serializes with bincode (FC's wire format)
|
||||
let encoded = bincode::serialize(&fc_msg).unwrap();
|
||||
assert!(!encoded.is_empty());
|
||||
|
||||
// And deserializes back
|
||||
let decoded: warzone_protocol::message::WireMessage = bincode::deserialize(&encoded).unwrap();
|
||||
if let warzone_protocol::message::WireMessage::CallSignal {
|
||||
id, payload: p, signal_type, ..
|
||||
} = decoded
|
||||
{
|
||||
assert_eq!(id, "call-123");
|
||||
assert!(matches!(signal_type, warzone_protocol::message::CallSignalType::Offer));
|
||||
|
||||
// Decode the WZP payload back
|
||||
let wzp_payload = wzp_client::featherchat::decode_call_payload(&p).unwrap();
|
||||
assert_eq!(wzp_payload.relay_addr.unwrap(), "relay.example.com:4433");
|
||||
assert!(matches!(wzp_payload.signal, wzp_proto::SignalMessage::CallOffer { .. }));
|
||||
} else {
|
||||
panic!("expected CallSignal");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wzp_answer_round_trips_through_fc_callsignal() {
|
||||
let answer = wzp_proto::SignalMessage::CallAnswer {
|
||||
identity_pub: [10u8; 32],
|
||||
ephemeral_pub: [20u8; 32],
|
||||
signature: vec![30u8; 64],
|
||||
chosen_profile: wzp_proto::QualityProfile::DEGRADED,
|
||||
};
|
||||
|
||||
let payload = wzp_client::featherchat::encode_call_payload(&answer, None, None);
|
||||
|
||||
let fc_msg = warzone_protocol::message::WireMessage::CallSignal {
|
||||
id: "call-456".to_string(),
|
||||
sender_fingerprint: "efgh5678".to_string(),
|
||||
signal_type: warzone_protocol::message::CallSignalType::Answer,
|
||||
payload,
|
||||
target: "caller-fp".to_string(),
|
||||
};
|
||||
|
||||
let bytes = bincode::serialize(&fc_msg).unwrap();
|
||||
let decoded: warzone_protocol::message::WireMessage = bincode::deserialize(&bytes).unwrap();
|
||||
|
||||
if let warzone_protocol::message::WireMessage::CallSignal { payload, .. } = decoded {
|
||||
let wzp = wzp_client::featherchat::decode_call_payload(&payload).unwrap();
|
||||
if let wzp_proto::SignalMessage::CallAnswer { chosen_profile, .. } = wzp.signal {
|
||||
assert_eq!(chosen_profile.codec, wzp_proto::CodecId::Opus6k);
|
||||
} else {
|
||||
panic!("expected CallAnswer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wzp_hangup_round_trips_through_fc_callsignal() {
|
||||
let hangup = wzp_proto::SignalMessage::Hangup {
|
||||
reason: wzp_proto::HangupReason::Normal,
|
||||
};
|
||||
|
||||
let payload = wzp_client::featherchat::encode_call_payload(&hangup, None, None);
|
||||
let signal_type = wzp_client::featherchat::signal_to_call_type(&hangup);
|
||||
assert!(matches!(signal_type, wzp_client::featherchat::CallSignalType::Hangup));
|
||||
|
||||
let fc_msg = warzone_protocol::message::WireMessage::CallSignal {
|
||||
id: "call-789".to_string(),
|
||||
sender_fingerprint: "xyz".to_string(),
|
||||
signal_type: warzone_protocol::message::CallSignalType::Hangup,
|
||||
payload,
|
||||
target: "peer".to_string(),
|
||||
};
|
||||
|
||||
let bytes = bincode::serialize(&fc_msg).unwrap();
|
||||
let decoded: warzone_protocol::message::WireMessage = bincode::deserialize(&bytes).unwrap();
|
||||
|
||||
if let warzone_protocol::message::WireMessage::CallSignal { payload, .. } = decoded {
|
||||
let wzp = wzp_client::featherchat::decode_call_payload(&payload).unwrap();
|
||||
assert!(matches!(wzp.signal, wzp_proto::SignalMessage::Hangup { .. }));
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Auth Token Contract ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn auth_validate_request_matches_fc_contract() {
|
||||
// WZP sends: { "token": "..." }
|
||||
// FC expects: ValidateRequest { token: String }
|
||||
let wzp_request = serde_json::json!({ "token": "test-token-123" });
|
||||
let json_str = wzp_request.to_string();
|
||||
|
||||
// FC can deserialize this (same shape as their ValidateRequest)
|
||||
#[derive(serde::Deserialize)]
|
||||
struct FcValidateRequest {
|
||||
token: String,
|
||||
}
|
||||
let fc_req: FcValidateRequest = serde_json::from_str(&json_str).unwrap();
|
||||
assert_eq!(fc_req.token, "test-token-123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_validate_response_matches_wzp_expectations() {
|
||||
// FC returns: { "valid": true, "fingerprint": "...", "alias": "..." }
|
||||
// WZP expects: wzp_relay::auth::ValidateResponse
|
||||
let fc_response = serde_json::json!({
|
||||
"valid": true,
|
||||
"fingerprint": "a3f8:1b2c:3d4e:5f60:7182:93a4:b5c6:d7e8",
|
||||
"alias": "manwe",
|
||||
"eth_address": null
|
||||
});
|
||||
|
||||
let wzp_resp: wzp_relay::auth::ValidateResponse =
|
||||
serde_json::from_value(fc_response).unwrap();
|
||||
assert!(wzp_resp.valid);
|
||||
assert_eq!(
|
||||
wzp_resp.fingerprint.unwrap(),
|
||||
"a3f8:1b2c:3d4e:5f60:7182:93a4:b5c6:d7e8"
|
||||
);
|
||||
assert_eq!(wzp_resp.alias.unwrap(), "manwe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_invalid_response_matches() {
|
||||
let fc_response = serde_json::json!({ "valid": false });
|
||||
let wzp_resp: wzp_relay::auth::ValidateResponse =
|
||||
serde_json::from_value(fc_response).unwrap();
|
||||
assert!(!wzp_resp.valid);
|
||||
assert!(wzp_resp.fingerprint.is_none());
|
||||
}
|
||||
|
||||
// ─── Signal Type Mapping ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn all_signal_types_map_correctly() {
|
||||
use wzp_client::featherchat::{signal_to_call_type, CallSignalType};
|
||||
|
||||
let cases: Vec<(wzp_proto::SignalMessage, &str)> = vec![
|
||||
(
|
||||
wzp_proto::SignalMessage::CallOffer {
|
||||
identity_pub: [0; 32], ephemeral_pub: [0; 32],
|
||||
signature: vec![], supported_profiles: vec![],
|
||||
},
|
||||
"Offer",
|
||||
),
|
||||
(
|
||||
wzp_proto::SignalMessage::CallAnswer {
|
||||
identity_pub: [0; 32], ephemeral_pub: [0; 32],
|
||||
signature: vec![],
|
||||
chosen_profile: wzp_proto::QualityProfile::GOOD,
|
||||
},
|
||||
"Answer",
|
||||
),
|
||||
(
|
||||
wzp_proto::SignalMessage::IceCandidate {
|
||||
candidate: "candidate:1".to_string(),
|
||||
},
|
||||
"IceCandidate",
|
||||
),
|
||||
(
|
||||
wzp_proto::SignalMessage::Hangup {
|
||||
reason: wzp_proto::HangupReason::Normal,
|
||||
},
|
||||
"Hangup",
|
||||
),
|
||||
];
|
||||
|
||||
for (signal, expected_name) in cases {
|
||||
let ct = signal_to_call_type(&signal);
|
||||
let name = format!("{ct:?}");
|
||||
assert_eq!(name, expected_name, "signal type mapping for {expected_name}");
|
||||
}
|
||||
}
|
||||
2
deps/featherchat
vendored
2
deps/featherchat
vendored
Submodule deps/featherchat updated: 064a730b42...4a4fa9fab4
Reference in New Issue
Block a user