bloop-protocol 1.1.0

Core implementation of the Bloop wire protocol
//! Integration tests exercising the derive macros the way a protocol
//! extension would use them.

use bloop_protocol::{
    ClientMessage, DataHash, Decode, Encode, MessageSet, MessageSetError, NfcUid, Payload,
    RawMessage,
};

#[derive(Clone, Debug, Decode, Encode, Eq, Payload, PartialEq)]
#[bloop(opcode = 0x80)]
struct StatusRequest;

#[derive(Clone, Debug, Decode, Encode, Eq, Payload, PartialEq)]
#[bloop(opcode = 0x82)]
struct RegisterTag {
    name: String,
    nfc_uid: NfcUid,
}

#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq)]
struct StatusEntry {
    name: String,
    audio_hash: Option<DataHash>,
}

#[derive(Clone, Debug, Decode, Encode, Eq, Payload, PartialEq)]
#[bloop(opcode = 0x81)]
struct StatusReport {
    #[bloop(count = u8)]
    entries: Vec<StatusEntry>,
    #[bloop(count = u32)]
    blob: Vec<u8>,
}

#[derive(Clone, Debug, Eq, MessageSet, PartialEq)]
enum TestRequest {
    Status(StatusRequest),
    RegisterTag(RegisterTag),
}

#[derive(Clone, Debug, Eq, MessageSet, PartialEq)]
enum TestResponse {
    Status(StatusReport),
}

#[test]
fn unit_message_round_trips() {
    let raw = TestRequest::Status(StatusRequest).encode().unwrap();

    assert_eq!(raw.message_type, 0x80);
    assert!(raw.payload.is_empty());

    assert_eq!(
        TestRequest::decode(&raw).unwrap(),
        TestRequest::Status(StatusRequest)
    );
}

#[test]
fn field_message_has_expected_layout() {
    let request = RegisterTag {
        name: "ab".to_string(),
        nfc_uid: NfcUid::try_from(&[1u8, 2, 3, 4][..]).unwrap(),
    };

    let raw = TestRequest::RegisterTag(request.clone()).encode().unwrap();

    assert_eq!(raw.message_type, 0x82);
    assert_eq!(raw.payload, [2, b'a', b'b', 4, 1, 2, 3, 4]);

    assert_eq!(
        TestRequest::decode(&raw).unwrap(),
        TestRequest::RegisterTag(request)
    );
}

#[test]
fn counted_fields_use_their_prefix_width() {
    let report = StatusReport {
        entries: vec![StatusEntry {
            name: "x".to_string(),
            audio_hash: None,
        }],
        blob: vec![7, 8],
    };

    let raw = TestResponse::Status(report.clone()).encode().unwrap();

    assert_eq!(raw.message_type, 0x81);
    assert_eq!(raw.payload, [1, 1, b'x', 0, 2, 0, 0, 0, 7, 8]);

    assert_eq!(
        TestResponse::decode(&raw).unwrap(),
        TestResponse::Status(report)
    );
}

#[test]
fn unknown_opcode_is_reported() {
    let raw = RawMessage::new(0x90, vec![]);
    let error = TestRequest::decode(&raw).unwrap_err();

    assert!(matches!(error, MessageSetError::UnknownOpcode(0x90)));
}

#[test]
fn malformed_payload_is_reported() {
    let raw = RawMessage::new(0x82, vec![5, b'a']);
    let error = TestRequest::decode(&raw).unwrap_err();

    assert!(matches!(
        error,
        MessageSetError::Malformed { opcode: 0x82, .. }
    ));
}

#[test]
fn trailing_bytes_are_malformed() {
    let raw = RawMessage::new(0x80, vec![1]);
    let error = TestRequest::decode(&raw).unwrap_err();

    assert!(matches!(
        error,
        MessageSetError::Malformed { opcode: 0x80, .. }
    ));
}

#[test]
fn from_impls_are_generated() {
    let request: TestRequest = StatusRequest.into();
    assert_eq!(request, TestRequest::Status(StatusRequest));
    assert_eq!(request.opcode(), 0x80);
}

#[derive(Clone, Debug, Eq, MessageSet, PartialEq)]
enum ComposedResponse<Ext> {
    Status(StatusReport),
    Custom(Ext),
}

#[test]
fn user_defined_catch_all_delegates() {
    let report = StatusReport {
        entries: vec![],
        blob: vec![],
    };

    let raw = TestResponse::Status(report.clone()).encode().unwrap();
    let decoded = ComposedResponse::<RawMessage>::decode(&raw).unwrap();
    assert_eq!(decoded, ComposedResponse::Status(report));

    let unknown = RawMessage::new(0x90, vec![1]);
    let decoded = ComposedResponse::<RawMessage>::decode(&unknown).unwrap();
    assert_eq!(decoded, ComposedResponse::Custom(unknown));
}

#[test]
fn extension_set_plugs_into_the_standard_set() {
    let raw = TestRequest::Status(StatusRequest).encode().unwrap();
    let decoded = ClientMessage::<TestRequest>::decode(&raw).unwrap();

    assert_eq!(
        decoded,
        ClientMessage::Custom(TestRequest::Status(StatusRequest))
    );

    let standard = RawMessage::new(0x05, vec![]);
    let decoded = ClientMessage::<TestRequest>::decode(&standard).unwrap();
    assert!(matches!(decoded, ClientMessage::Ping(_)));

    let unknown = RawMessage::new(0x90, vec![]);
    let error = ClientMessage::<TestRequest>::decode(&unknown).unwrap_err();
    assert!(matches!(error, MessageSetError::UnknownOpcode(0x90)));
}