p2panda-rs 0.2.1

All the things a panda needs
Documentation
// SPDX-License-Identifier: AGPL-3.0-or-later

#[cfg(test)]
mod tests {

    use std::convert::TryFrom;

    use rstest::{fixture, rstest};

    use crate::entry::{decode_entry, sign_and_encode, Entry, EntrySigned, LogId, SeqNum};
    use crate::hash::Hash;
    use crate::identity::KeyPair;
    use crate::message::{Message, MessageEncoded, MessageFields, MessageValue};

    struct PandaTestFixture {
        entry_signed_encoded: EntrySigned,
        key_pair: KeyPair,
        message_encoded: MessageEncoded,
        entry: Entry,
    }

    fn create_message_fields(keys: Vec<&str>, values: Vec<&str>) -> MessageFields {
        let mut fields = MessageFields::new();
        for (pos, key) in keys.iter().enumerate() {
            fields
                .add(
                    key.to_owned(),
                    MessageValue::Text(values.get(pos).unwrap().to_string()),
                )
                .unwrap();
        }
        fields
    }

    #[fixture]
    fn key_pair() -> KeyPair {
        KeyPair::new()
    }

    #[fixture]
    fn message(
        #[default(vec!["message"])] keys: Vec<&str>,
        #[default(vec!["Hello!"])] values: Vec<&str>,
    ) -> Message {
        let fields = create_message_fields(keys, values);
        Message::new_create(Hash::new_from_bytes(vec![1, 2, 3]).unwrap(), fields).unwrap()
    }

    #[fixture]
    fn entry(
        message: Message,
        #[default(SeqNum::new(1).unwrap())] seq_num: SeqNum,
        #[default(None)] backlink: Option<Hash>,
        #[default(None)] skiplink: Option<Hash>,
    ) -> Entry {
        Entry::new(
            &LogId::default(),
            Some(&message),
            skiplink.as_ref(),
            backlink.as_ref(),
            &seq_num,
        )
        .unwrap()
    }

    #[fixture]
    fn v0_1_0_fixture() -> PandaTestFixture {
        PandaTestFixture {
            entry_signed_encoded: EntrySigned::new("009cdb3a8c0c4b308173d4c3c43a67a6d013444af99acb8be6c52423746d9aa2c10101f60040190c0d1b8a9bbe5d8b94c8226cdb5d9804af3af6a0c5e34c918864370953dbc7100438f1e5cb0f34bd214c595e37fbb0727f86e9f3eccafa9ba13ed8ef77a04ef01463f550ce62f983494d0eb6051c73a5641025f355758006724e5b730f47a4454c5395eab807325ee58d69c08d66461357d0f961aee383acc3247ed6419706").unwrap(),
            message_encoded: MessageEncoded::new("a466616374696f6e6663726561746566736368656d6178843030343031643736353636373538613562366266633536316631633933366438666338366235623432656132326162316461626634306432343964323764643930363430316664653134376535336634346331303364643032613235343931366265313133653531646531303737613934366133613063313237326239623334383433376776657273696f6e01666669656c6473a26b6465736372697074696f6ea26474797065637374726576616c756571666f7220706c6179696e67206368657373646e616d65a26474797065637374726576616c7565656368657373").unwrap(),
            key_pair: KeyPair::from_private_key_str("4c21b14046f284f87f1ea4be4b973664221ad483079a68ed35a6812553b41176").unwrap(),
            entry: Entry::new(
                &LogId::new(1),
                Some(&Message::new_create(Hash::new("00401d76566758a5b6bfc561f1c936d8fc86b5b42ea22ab1dabf40d249d27dd906401fde147e53f44c103dd02a254916be113e51de1077a946a3a0c1272b9b348437").unwrap(), create_message_fields(vec!["name", "description"], vec!["chess", "for playing chess"])).unwrap()),
                None,
                None,
                &SeqNum::new(1).unwrap(),
            ).unwrap()
        }
    }

    // TODO: This test should be moved into EntrySigned once we have generalized test fixtures.
    #[rstest(message)]
    #[case(message(vec!["message"], vec!["Hello!"]))]
    #[should_panic]
    #[case(message(vec!["message"], vec!["Boo!"]))]
    #[should_panic]
    #[case(message(vec!["date"], vec!["2021-05-02T20:06:45.430Z"]))]
    #[should_panic]
    #[case(message(vec!["message", "date"], vec!["Hello!", "2021-05-02T20:06:45.430Z"]))]
    fn message_validation(entry: Entry, message: Message, key_pair: KeyPair) {
        let encoded_message = MessageEncoded::try_from(&message).unwrap();
        let signed_encoded_entry = sign_and_encode(&entry, &key_pair).unwrap();
        assert!(signed_encoded_entry
            .validate_message(&encoded_message)
            .is_ok());
    }

    #[rstest]
    fn entry_encoding_decoding(entry: Entry, key_pair: KeyPair) {
        // Encode Message
        let encoded_message = MessageEncoded::try_from(entry.message().unwrap()).unwrap();

        // Sign and encode Entry
        let signed_encoded_entry = sign_and_encode(&entry, &key_pair).unwrap();

        // Decode signed and encoded Entry
        let decoded_entry = decode_entry(&signed_encoded_entry, Some(&encoded_message)).unwrap();

        // All Entry and decoded Entry values should be equal
        assert_eq!(entry.log_id(), decoded_entry.log_id());
        assert_eq!(entry.message().unwrap(), decoded_entry.message().unwrap());
        assert_eq!(entry.seq_num(), decoded_entry.seq_num());
        assert_eq!(entry.backlink_hash(), decoded_entry.backlink_hash());
        assert_eq!(entry.skiplink_hash(), decoded_entry.skiplink_hash());
    }

    #[rstest]
    fn sign_and_encode_roundtrip(entry: Entry, key_pair: KeyPair) {
        // Sign a p2panda entry. For this encoding, the entry is converted into a
        // bamboo-rs-core entry, which means that it also doesn't contain the message anymore
        let entry_first_encoded = sign_and_encode(&entry, &key_pair).unwrap();

        // Make an unsigned, decoded p2panda entry from the signed and encoded form. This is adding
        // the message back
        let message_encoded = MessageEncoded::try_from(entry.message().unwrap()).unwrap();
        let entry_decoded: Entry =
            decode_entry(&entry_first_encoded, Some(&message_encoded)).unwrap();

        // Re-encode the recovered entry to be able to check that we still have the same data
        let test_entry_signed_encoded = sign_and_encode(&entry_decoded, &key_pair).unwrap();
        assert_eq!(entry_first_encoded, test_entry_signed_encoded);

        // Create second p2panda entry without skiplink as it is not required
        let entry_second = Entry::new(
            &LogId::default(),
            entry.message(),
            None,
            Some(&entry_first_encoded.hash()),
            &SeqNum::new(2).unwrap(),
        )
        .unwrap();
        assert!(sign_and_encode(&entry_second, &key_pair).is_ok());
    }

    #[rstest(fixture, case::v0_1_0(v0_1_0_fixture()))]
    fn fixture_sign_encode(fixture: PandaTestFixture) {
        // Sign and encode fixture Entry
        let entry_signed_encoded = sign_and_encode(&fixture.entry, &fixture.key_pair).unwrap();

        // fixture EntrySigned hash should equal newly encoded EntrySigned hash.
        assert_eq!(
            fixture.entry_signed_encoded.hash().as_str(),
            entry_signed_encoded.hash().as_str()
        );
    }

    #[rstest(fixture, case::v0_1_0(v0_1_0_fixture()))]
    fn fixture_decode_message(fixture: PandaTestFixture) {
        // Decode fixture MessageEncoded
        let message = Message::try_from(&fixture.message_encoded).unwrap();
        let message_fields = message.fields().unwrap();

        let fixture_message_fields = fixture.entry.message().unwrap().fields().unwrap();

        // Decoded fixture MessageEncoded values should match fixture Entry message values
        // Would be an improvement if we iterate over fields instead of using hard coded keys
        assert_eq!(
            message_fields.get("description").unwrap(),
            fixture_message_fields.get("description").unwrap()
        );

        assert_eq!(
            message_fields.get("name").unwrap(),
            fixture_message_fields.get("name").unwrap()
        );
    }

    #[rstest(fixture, case::v0_1_0(v0_1_0_fixture()))]
    fn fixture_decode_entry(fixture: PandaTestFixture) {
        // Decode fixture EntrySigned
        let entry = decode_entry(
            &fixture.entry_signed_encoded,
            Some(&fixture.message_encoded),
        )
        .unwrap();

        // Decoded Entry values should match fixture Entry values
        assert_eq!(entry.message().unwrap(), fixture.entry.message().unwrap());
        assert_eq!(entry.seq_num(), fixture.entry.seq_num());
        assert_eq!(entry.backlink_hash(), fixture.entry.backlink_hash());
        assert_eq!(entry.skiplink_hash(), fixture.entry.skiplink_hash());
        assert_eq!(entry.log_id(), fixture.entry.log_id());
    }
}