diff --git a/libvctrl_core/src/codec/binary_decoder.rs b/libvctrl_core/src/codec/binary_decoder.rs index e8a07d8..5067465 100644 --- a/libvctrl_core/src/codec/binary_decoder.rs +++ b/libvctrl_core/src/codec/binary_decoder.rs @@ -392,3 +392,274 @@ impl Decoder for BinaryDecoder { Tag::with_meta(name, target, tagger, message, meta) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codec::BinaryEncoder; + use libvctrl_handler::{Encoder, TreeEntry}; + use std::io::Cursor; + + fn hash_byte(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) + } + + fn user(name: &str, email: &str) -> Result { + UserID::new(name.to_string(), email.to_string()) + } + + fn meta(ts: i64, tz: i16) -> Result { + CommitMeta::new(ts, tz, None) + } + + #[test] + fn check_version_valid() -> Result<(), VctrlError> { + let data = [3_u8, 42]; + let rest = BinaryDecoder::check_version(&data)?; + assert_eq!(rest, &[42]); + Ok(()) + } + + #[test] + fn check_version_missing_byte() { + assert!(BinaryDecoder::check_version(&[]).is_err()); + } + + #[test] + fn check_version_unsupported() -> Result<(), VctrlError> { + let result = BinaryDecoder::check_version(&[4_u8]); + assert!(result.is_err()); + match result { + Err(VctrlError::CorruptedData(msg)) => { + assert!(msg.contains("unsupported version")); + } + _ => return Err(VctrlError::Other("expected CorruptedData".into())), + } + Ok(()) + } + + #[test] + fn read_bounded_within_limit() -> Result<(), VctrlError> { + let mut reader = Cursor::new(vec![1_u8, 2, 3]); + let data = BinaryDecoder::read_bounded(&mut reader, 10)?; + assert_eq!(data, vec![1, 2, 3]); + Ok(()) + } + + #[test] + fn read_bounded_exceeds_limit() { + let mut reader = Cursor::new(vec![1_u8, 2, 3, 4]); + assert!(BinaryDecoder::read_bounded(&mut reader, 2).is_err()); + } + + #[test] + fn require_byte_valid() -> Result<(), VctrlError> { + let value = BinaryDecoder::require_byte(&[10, 20], 1, "second byte")?; + assert_eq!(value, 20); + Ok(()) + } + + #[test] + fn require_byte_missing() { + assert!(BinaryDecoder::require_byte(&[10], 1, "second byte").is_err()); + } + + #[test] + fn require_slice_valid() -> Result<(), VctrlError> { + let data = [1, 2, 3, 4]; + let slice = BinaryDecoder::require_slice(&data, 1, 2, "middle")?; + assert_eq!(slice, &[2, 3]); + Ok(()) + } + + #[test] + fn require_slice_overflow() { + let data = [1, 2, 3]; + assert!(BinaryDecoder::require_slice(&data, usize::MAX, 2, "overflow").is_err()); + } + + #[test] + fn decode_blob_valid_roundtrip() -> Result<(), VctrlError> { + let encoder = BinaryEncoder; + let codec = BinaryDecoder; + let payload = vec![1_u8, 2, 3, 4]; + + let blob = Blob::new(payload.clone())?; + let mut buf = Vec::new(); + encoder.encode_blob(&blob, &mut buf)?; + let decoded = codec.decode_blob(Cursor::new(buf))?; + assert_eq!(decoded.data(), payload.as_slice()); + Ok(()) + } + + #[test] + fn decode_blob_invalid_version() { + let codec = BinaryDecoder; + let data = [4_u8, 0, 0, 0, 0, 0, 0, 0, 0]; + assert!(codec.decode_blob(Cursor::new(data)).is_err()); + } + + #[test] + fn decode_blob_length_mismatch() { + let codec = BinaryDecoder; + let mut data = Vec::new(); + data.push(3_u8); + data.extend_from_slice(&5_u64.to_le_bytes()); + data.push(1_u8); + assert!(codec.decode_blob(Cursor::new(data)).is_err()); + } + + #[test] + fn decode_tree_valid_roundtrip() -> Result<(), VctrlError> { + let encoder = BinaryEncoder; + let codec = BinaryDecoder; + + let hash = hash_byte(0x22)?; + let entry = TreeEntry::new("a.txt".to_string(), EntryKind::Blob, hash)?; + let tree = Tree::new(vec![entry])?; + + let mut buf = Vec::new(); + encoder.encode_tree(&tree, &mut buf)?; + let decoded = codec.decode_tree(Cursor::new(buf))?; + + let entries = decoded.entries(); + assert_eq!(entries.len(), 1); + let first = entries + .first() + .ok_or_else(|| VctrlError::Other("expected one entry".into()))?; + assert_eq!(first.name(), "a.txt"); + assert_eq!(first.kind(), EntryKind::Blob); + assert_eq!(*first.hash(), hash); + Ok(()) + } + + #[test] + fn decode_tree_unknown_kind() -> Result<(), VctrlError> { + let codec = BinaryDecoder; + let mut data = Vec::new(); + data.push(3_u8); + data.extend_from_slice(&1_u32.to_le_bytes()); + data.push(1_u8); + data.push(b'a'); + data.push(9_u8); + data.extend_from_slice(hash_byte(0x33)?.as_bytes()); + + let result = codec.decode_tree(Cursor::new(data)); + assert!(result.is_err()); + match result { + Err(VctrlError::CorruptedData(msg)) => { + assert!(msg.contains("unknown entry kind")); + } + _ => return Err(VctrlError::Other("expected CorruptedData".into())), + } + Ok(()) + } + + #[test] + fn decode_commit_valid_roundtrip() -> Result<(), VctrlError> { + let encoder = BinaryEncoder; + let codec = BinaryDecoder; + + let tree = hash_byte(0x01)?; + let parent = hash_byte(0x02)?; + let author = user("Alice", "alice@example.com")?; + let committer = user("Bob", "bob@example.com")?; + let message = "initial commit".to_string(); + let meta = meta(1_600_000_000, 0)?; + + let commit = + Commit::with_meta(tree, vec![parent], author, committer, message.clone(), meta)?; + + let mut buf = Vec::new(); + encoder.encode_commit(&commit, &mut buf)?; + let decoded = codec.decode_commit(Cursor::new(buf))?; + + assert_eq!(decoded.tree(), &tree); + let parents = decoded.parents(); + assert_eq!(parents.len(), 1); + assert_eq!(parents.first(), Some(&parent)); + assert_eq!(decoded.author().name(), "Alice"); + assert_eq!(decoded.committer().email(), "bob@example.com"); + assert_eq!(decoded.message(), message); + assert_eq!(decoded.meta().timestamp(), 1_600_000_000); + assert_eq!(decoded.meta().timezone_offset(), 0); + assert!(decoded.meta().encoding().is_none()); + Ok(()) + } + + #[test] + fn decode_commit_trailing_bytes() -> Result<(), VctrlError> { + let encoder = BinaryEncoder; + let codec = BinaryDecoder; + + let tree = hash_byte(0x01)?; + let author = user("Alice", "alice@example.com")?; + let committer = user("Bob", "bob@example.com")?; + let message = "initial commit".to_string(); + let meta = meta(1_600_000_000, 0)?; + + let commit = Commit::with_meta(tree, vec![], author, committer, message, meta)?; + let mut buf = Vec::new(); + encoder.encode_commit(&commit, &mut buf)?; + buf.push(0_u8); + + assert!(codec.decode_commit(Cursor::new(buf)).is_err()); + Ok(()) + } + + #[test] + fn decode_tag_valid_roundtrip() -> Result<(), VctrlError> { + let encoder = BinaryEncoder; + let codec = BinaryDecoder; + + let target = hash_byte(0x33)?; + let tagger = user("Tagger", "tagger@example.com")?; + let message = "v1.0".to_string(); + let meta = meta(1_600_000_000, 0)?; + + let tag = Tag::with_meta( + "v1.0".to_string(), + target, + Some(tagger), + message.clone(), + meta, + )?; + + let mut buf = Vec::new(); + encoder.encode_tag(&tag, &mut buf)?; + let decoded = codec.decode_tag(Cursor::new(buf))?; + + assert_eq!(decoded.name(), "v1.0"); + assert_eq!(decoded.target(), &target); + let decoded_tagger = decoded + .tagger() + .ok_or_else(|| VctrlError::Other("expected tagger".into()))?; + assert_eq!(decoded_tagger.name(), "Tagger"); + assert_eq!(decoded.message(), message); + assert_eq!(decoded.meta().timestamp(), 1_600_000_000); + assert_eq!(decoded.meta().timezone_offset(), 0); + assert!(decoded.meta().encoding().is_none()); + Ok(()) + } + + #[test] + fn decode_tag_invalid_tagger_presence() -> Result<(), VctrlError> { + let codec = BinaryDecoder; + let mut data = Vec::new(); + data.push(3_u8); + data.push(1_u8); + data.push(b'v'); + data.extend_from_slice(hash_byte(0x33)?.as_bytes()); + data.push(2_u8); + + let result = codec.decode_tag(Cursor::new(data)); + assert!(result.is_err()); + match result { + Err(VctrlError::CorruptedData(msg)) => { + assert!(msg.contains("invalid tagger presence")); + } + _ => return Err(VctrlError::Other("expected CorruptedData".into())), + } + Ok(()) + } +} diff --git a/libvctrl_core/src/codec/binary_encoder.rs b/libvctrl_core/src/codec/binary_encoder.rs index a7ba6bc..9bad0c1 100644 --- a/libvctrl_core/src/codec/binary_encoder.rs +++ b/libvctrl_core/src/codec/binary_encoder.rs @@ -44,9 +44,7 @@ impl Encoder for BinaryEncoder { EntryKind::Symlink => 2, EntryKind::Tree => 3, EntryKind::Submodule => 4, - _ => { - return Err(VctrlError::SerializationError("unknown entry kind".into())); - } + _ => return Err(VctrlError::SerializationError("unknown entry kind".into())), }; writer .write_all(&[kind_byte]) @@ -153,7 +151,7 @@ impl Encoder for BinaryEncoder { .write_all(enc.as_bytes()) .map_err(VctrlError::from_io)?; } - None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + None => writer.write_all(&[0_u8]).map_err(VctrlError::from_io)?, } Ok(()) } @@ -175,7 +173,7 @@ impl Encoder for BinaryEncoder { match tag.tagger() { Some(tagger) => { - writer.write_all(&[1u8]).map_err(VctrlError::from_io)?; + writer.write_all(&[1_u8]).map_err(VctrlError::from_io)?; let tagger_name = tagger.name(); writer @@ -197,7 +195,7 @@ impl Encoder for BinaryEncoder { .write_all(tagger_email.as_bytes()) .map_err(VctrlError::from_io)?; } - None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + None => writer.write_all(&[0_u8]).map_err(VctrlError::from_io)?, } let msg = tag.message(); @@ -232,8 +230,113 @@ impl Encoder for BinaryEncoder { .write_all(enc.as_bytes()) .map_err(VctrlError::from_io)?; } - None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + None => writer.write_all(&[0_u8]).map_err(VctrlError::from_io)?, } Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::codec::BinaryDecoder; + use libvctrl_handler::{CommitMeta, Decoder, Hash, TreeEntry, UserID}; + use std::io::Cursor; + + fn hash_byte(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) + } + + fn user(name: &str, email: &str) -> Result { + UserID::new(name.to_string(), email.to_string()) + } + + #[test] + fn encode_blob_exact_bytes() -> Result<(), VctrlError> { + let blob = Blob::new(vec![1_u8, 2, 3])?; + let mut buf = Vec::new(); + BinaryEncoder.encode_blob(&blob, &mut buf)?; + assert_eq!(buf, vec![3, 3, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3]); + Ok(()) + } + + #[test] + fn encode_tree_exact_prefix() -> Result<(), VctrlError> { + let hash = hash_byte(0x22)?; + let entry = TreeEntry::new("a".to_string(), EntryKind::Blob, hash)?; + let tree = Tree::new(vec![entry])?; + + let mut buf = Vec::new(); + BinaryEncoder.encode_tree(&tree, &mut buf)?; + + assert_eq!(buf.first(), Some(&3_u8)); + assert_eq!(buf.get(1..5), Some(&1_u32.to_le_bytes()[..])); + assert_eq!(buf.get(5), Some(&1_u8)); + assert_eq!(buf.get(6), Some(&b'a')); + assert_eq!(buf.get(7), Some(&0_u8)); + assert_eq!(buf.len(), 5 + 1 + 1 + 1 + 64); + Ok(()) + } + + #[test] + fn encode_commit_roundtrip_with_decoder() -> Result<(), VctrlError> { + let tree = hash_byte(0x11)?; + let parent = hash_byte(0x12)?; + let author = user("Alice", "alice@example.com")?; + let committer = user("Bob", "bob@example.com")?; + let message = "commit message".to_string(); + let meta = CommitMeta::new(123, 0, None)?; + + let commit = + Commit::with_meta(tree, vec![parent], author, committer, message.clone(), meta)?; + + let mut buf = Vec::new(); + BinaryEncoder.encode_commit(&commit, &mut buf)?; + let decoded = BinaryDecoder.decode_commit(Cursor::new(buf))?; + + assert_eq!(decoded.tree(), &tree); + assert_eq!(decoded.parents(), &[parent]); + assert_eq!(decoded.author().name(), "Alice"); + assert_eq!(decoded.committer().email(), "bob@example.com"); + assert_eq!(decoded.message(), message); + assert_eq!(decoded.meta().timestamp(), 123); + assert_eq!(decoded.meta().timezone_offset(), 0); + assert!(decoded.meta().encoding().is_none()); + Ok(()) + } + + #[test] + fn encode_tag_roundtrip_with_decoder() -> Result<(), VctrlError> { + let target = hash_byte(0x33)?; + let tagger = user("Tagger", "tagger@example.com")?; + let message = "v1.0".to_string(); + let meta = CommitMeta::new(456, 0, None)?; + + let tag = Tag::with_meta( + "v1.0".to_string(), + target, + Some(tagger), + message.clone(), + meta, + )?; + + let mut buf = Vec::new(); + BinaryEncoder.encode_tag(&tag, &mut buf)?; + let decoded = BinaryDecoder.decode_tag(Cursor::new(buf))?; + + assert_eq!(decoded.name(), "v1.0"); + assert_eq!(decoded.target(), &target); + assert_eq!( + decoded + .tagger() + .ok_or_else(|| VctrlError::Other("expected tagger".into()))? + .name(), + "Tagger" + ); + assert_eq!(decoded.message(), message); + assert_eq!(decoded.meta().timestamp(), 456); + assert_eq!(decoded.meta().timezone_offset(), 0); + assert!(decoded.meta().encoding().is_none()); + Ok(()) + } +} diff --git a/libvctrl_core/src/hash/sha512.rs b/libvctrl_core/src/hash/sha512.rs index c5f3e37..8926e9d 100644 --- a/libvctrl_core/src/hash/sha512.rs +++ b/libvctrl_core/src/hash/sha512.rs @@ -28,3 +28,49 @@ impl Hasher for Sha512Hasher { Hash::from_bytes(&digest) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn hash_empty_input() -> Result<(), VctrlError> { + let hash = Sha512Hasher.hash(Cursor::new(Vec::::new()))?; + assert_eq!( + hash.as_bytes(), + &[ + 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, + 0x80, 0x07, 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, 0x83, 0xf4, 0xa9, 0x21, + 0xd3, 0x6c, 0xe9, 0xce, 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, 0xff, 0x83, + 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f, 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, + 0xa5, 0x38, 0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e + ] + ); + Ok(()) + } + + #[test] + fn hash_abc() -> Result<(), VctrlError> { + let hash = Sha512Hasher.hash(Cursor::new(b"abc"))?; + assert_eq!( + hash.as_bytes(), + &[ + 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, + 0x41, 0x31, 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, 0x0a, 0x9e, 0xee, 0xe6, + 0x4b, 0x55, 0xd3, 0x9a, 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, 0x36, 0xba, + 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, + 0x2a, 0x9a, 0xc9, 0x4f, 0xa5, 0x4c, 0xa4, 0x9f + ] + ); + Ok(()) + } + + #[test] + fn hash_multiple_chunks() -> Result<(), VctrlError> { + let data = vec![0xAB; 8192]; + let hash = Sha512Hasher.hash(Cursor::new(data))?; + assert_eq!(hash.as_bytes().len(), 64); + Ok(()) + } +} diff --git a/libvctrl_core/src/object/blob.rs b/libvctrl_core/src/object/blob.rs index ef22996..ddc22d8 100644 --- a/libvctrl_core/src/object/blob.rs +++ b/libvctrl_core/src/object/blob.rs @@ -21,3 +21,23 @@ impl BlobBuilder { Blob::new(self.data) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn builder_empty_data_builds_ok() -> Result<(), VctrlError> { + let blob = BlobBuilder::new().build()?; + assert!(blob.data().is_empty()); + Ok(()) + } + + #[test] + fn builder_with_data_builds_ok() -> Result<(), VctrlError> { + let data = vec![1_u8, 2, 3]; + let blob = BlobBuilder::new().with_data(data.clone()).build()?; + assert_eq!(blob.data(), data.as_slice()); + Ok(()) + } +} diff --git a/libvctrl_core/src/object/commit.rs b/libvctrl_core/src/object/commit.rs index 3e15948..d1ccafc 100644 --- a/libvctrl_core/src/object/commit.rs +++ b/libvctrl_core/src/object/commit.rs @@ -80,3 +80,106 @@ impl CommitBuilder { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn hash_byte(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) + } + + fn user(name: &str, email: &str) -> Result { + UserID::new(name.to_string(), email.to_string()) + } + + #[test] + fn build_missing_tree_errors() -> Result<(), VctrlError> { + let result = CommitBuilder::new() + .author(user("A", "a@example.com")?) + .committer(user("B", "b@example.com")?) + .message("msg") + .build(); + assert!(result.is_err()); + Ok(()) + } + + #[test] + fn build_missing_author_errors() -> Result<(), VctrlError> { + let result = CommitBuilder::new() + .tree(hash_byte(0x01)?) + .committer(user("B", "b@example.com")?) + .message("msg") + .build(); + assert!(result.is_err()); + Ok(()) + } + + #[test] + fn build_missing_committer_errors() -> Result<(), VctrlError> { + let result = CommitBuilder::new() + .tree(hash_byte(0x01)?) + .author(user("A", "a@example.com")?) + .message("msg") + .build(); + assert!(result.is_err()); + Ok(()) + } + + #[test] + fn build_missing_message_errors() -> Result<(), VctrlError> { + let result = CommitBuilder::new() + .tree(hash_byte(0x01)?) + .author(user("A", "a@example.com")?) + .committer(user("B", "b@example.com")?) + .build(); + assert!(result.is_err()); + Ok(()) + } + + #[test] + fn build_valid_commit_without_meta() -> Result<(), VctrlError> { + let tree = hash_byte(0x11)?; + let parent = hash_byte(0x12)?; + let author = user("Alice", "alice@example.com")?; + let committer = user("Bob", "bob@example.com")?; + let message = "hello".to_string(); + + let commit = CommitBuilder::new() + .tree(tree) + .parent(parent) + .author(author) + .committer(committer) + .message(message.clone()) + .build()?; + + assert_eq!(commit.tree(), &tree); + assert_eq!(commit.parents(), &[parent]); + assert_eq!(commit.author().name(), "Alice"); + assert_eq!(commit.committer().name(), "Bob"); + assert_eq!(commit.message(), message); + Ok(()) + } + + #[test] + fn build_valid_commit_with_meta() -> Result<(), VctrlError> { + let tree = hash_byte(0x21)?; + let author = user("Alice", "alice@example.com")?; + let committer = user("Bob", "bob@example.com")?; + let message = "hello".to_string(); + let meta = CommitMeta::new(123, 0, Some("utf-8".to_string()))?; + + let commit = CommitBuilder::new() + .tree(tree) + .author(author) + .committer(committer) + .message(message) + .meta(meta) + .build()?; + + assert_eq!(commit.meta().timestamp(), 123); + assert_eq!(commit.meta().timezone_offset(), 0); + assert_eq!(commit.meta().encoding(), Some("utf-8")); + Ok(()) + } +} diff --git a/libvctrl_core/src/object/tag.rs b/libvctrl_core/src/object/tag.rs index a5f81f7..ca6ee1d 100644 --- a/libvctrl_core/src/object/tag.rs +++ b/libvctrl_core/src/object/tag.rs @@ -72,3 +72,78 @@ impl TagBuilder { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn hash_byte(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) + } + + fn user(name: &str, email: &str) -> Result { + UserID::new(name.to_string(), email.to_string()) + } + + #[test] + fn build_missing_name_errors() -> Result<(), VctrlError> { + let result = TagBuilder::new() + .target(hash_byte(0x01)?) + .message("msg") + .build(); + assert!(result.is_err()); + Ok(()) + } + + #[test] + fn build_missing_target_errors() { + let result = TagBuilder::new().name("v1.0").message("msg").build(); + assert!(result.is_err()); + } + + #[test] + fn build_valid_tag_without_tagger_or_meta() -> Result<(), VctrlError> { + let name = "v1.0".to_string(); + let target = hash_byte(0x22)?; + let message = "release".to_string(); + + let tag = TagBuilder::new() + .name(name.clone()) + .target(target) + .message(message.clone()) + .build()?; + + assert_eq!(tag.name(), name); + assert_eq!(tag.target(), &target); + assert!(tag.tagger().is_none()); + assert_eq!(tag.message(), message); + Ok(()) + } + + #[test] + fn build_valid_tag_with_tagger_and_meta() -> Result<(), VctrlError> { + let name = "v2.0".to_string(); + let target = hash_byte(0x23)?; + let tagger = user("Tagger", "tagger@example.com")?; + let message = "release".to_string(); + let meta = CommitMeta::new(42, 0, Some("utf-8".to_string()))?; + + let tag = TagBuilder::new() + .name(name) + .target(target) + .tagger(tagger) + .message(message) + .meta(meta) + .build()?; + + assert_eq!( + tag.tagger() + .ok_or_else(|| VctrlError::Other("expected tagger".into()))? + .name(), + "Tagger" + ); + assert_eq!(tag.meta().timestamp(), 42); + assert_eq!(tag.meta().encoding(), Some("utf-8")); + Ok(()) + } +} diff --git a/libvctrl_core/src/object/tree.rs b/libvctrl_core/src/object/tree.rs index 87bf772..4e53743 100644 --- a/libvctrl_core/src/object/tree.rs +++ b/libvctrl_core/src/object/tree.rs @@ -52,3 +52,56 @@ impl TreeEntryBuilder { TreeEntry::new(self.name, self.kind, self.hash) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn hash_byte(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) + } + + #[test] + fn tree_entry_builder_valid() -> Result<(), VctrlError> { + let hash = hash_byte(0x11)?; + let entry = TreeEntryBuilder::new("file.txt".to_string(), EntryKind::Blob, hash).build()?; + assert_eq!(entry.name(), "file.txt"); + assert_eq!(entry.kind(), EntryKind::Blob); + assert_eq!(*entry.hash(), hash); + Ok(()) + } + + #[test] + fn tree_entry_builder_empty_name_errors() -> Result<(), VctrlError> { + let hash = hash_byte(0x11)?; + let result = TreeEntryBuilder::new(String::new(), EntryKind::Blob, hash).build(); + assert!(result.is_err()); + Ok(()) + } + + #[test] + fn tree_builder_add_entry_and_build() -> Result<(), VctrlError> { + let hash = hash_byte(0x22)?; + let tree = TreeBuilder::new() + .add_entry("a".to_string(), EntryKind::Blob, hash)? + .build()?; + + let entries = tree.entries(); + assert_eq!(entries.len(), 1); + assert_eq!( + entries + .first() + .ok_or_else(|| VctrlError::Other("expected entry".into()))? + .name(), + "a" + ); + Ok(()) + } + + #[test] + fn tree_builder_build_empty_tree() -> Result<(), VctrlError> { + let tree = TreeBuilder::new().build()?; + assert!(tree.entries().is_empty()); + Ok(()) + } +} diff --git a/libvctrl_core/src/store/memory.rs b/libvctrl_core/src/store/memory.rs index 8e01e40..bf55773 100644 --- a/libvctrl_core/src/store/memory.rs +++ b/libvctrl_core/src/store/memory.rs @@ -39,3 +39,58 @@ impl ObjectStore for MemoryStore { Ok(self.objects.contains_key(hash)) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn hash_byte(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) + } + + #[test] + fn put_and_get_roundtrip() -> Result<(), VctrlError> { + let mut store = MemoryStore::new(); + let hash = hash_byte(0xAB)?; + let data = vec![10_u8, 20, 30]; + + store.put(&hash, &data)?; + { + let mut reader = store.get(&hash)?; + let mut buf = Vec::new(); + let _ = reader.read_to_end(&mut buf)?; + assert_eq!(buf, data); + } + Ok(()) + } + + #[test] + fn get_missing_object_errors() -> Result<(), VctrlError> { + let store = MemoryStore::new(); + let hash = hash_byte(0xCD)?; + let result = store.get(&hash); + assert!(matches!(result, Err(VctrlError::ObjectNotFound(_)))); + Ok(()) + } + + #[test] + fn delete_removes_object() -> Result<(), VctrlError> { + let mut store = MemoryStore::new(); + let hash = hash_byte(0xEF)?; + let data = vec![1_u8, 2, 3]; + + store.put(&hash, &data)?; + assert!(store.exists(&hash)?); + store.delete(&hash)?; + assert!(!store.exists(&hash)?); + Ok(()) + } + + #[test] + fn exists_missing_object_returns_false() -> Result<(), VctrlError> { + let store = MemoryStore::new(); + let hash = hash_byte(0x77)?; + assert!(!store.exists(&hash)?); + Ok(()) + } +} diff --git a/libvctrl_core/src/store/ref_store.rs b/libvctrl_core/src/store/ref_store.rs index fce63e5..de5f8fe 100644 --- a/libvctrl_core/src/store/ref_store.rs +++ b/libvctrl_core/src/store/ref_store.rs @@ -44,3 +44,64 @@ impl RefStore for MemoryRefStore { Ok(names.into_iter().map(Ok).collect::>().into_iter()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn hash_byte(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) + } + + #[test] + fn set_and_get_ref_roundtrip() -> Result<(), VctrlError> { + let mut store = MemoryRefStore::new(); + let hash = hash_byte(0xAB)?; + + store.set_ref("refs/heads/main", &hash)?; + let got = store.get_ref("refs/heads/main")?; + assert_eq!(got, hash); + Ok(()) + } + + #[test] + fn set_ref_invalid_name_errors() -> Result<(), VctrlError> { + let mut store = MemoryRefStore::new(); + let hash = hash_byte(0xCD)?; + assert!(store.set_ref("bad name", &hash).is_err()); + Ok(()) + } + + #[test] + fn get_ref_missing_errors() { + let store = MemoryRefStore::new(); + let result = store.get_ref("refs/heads/nope"); + assert!(matches!(result, Err(VctrlError::RefNotFound(_)))); + } + + #[test] + fn delete_ref_removes_ref() -> Result<(), VctrlError> { + let mut store = MemoryRefStore::new(); + let hash = hash_byte(0xEF)?; + store.set_ref("refs/tags/v1", &hash)?; + store.delete_ref("refs/tags/v1")?; + assert!(store.get_ref("refs/tags/v1").is_err()); + Ok(()) + } + + #[test] + fn list_refs_sorted() -> Result<(), VctrlError> { + let mut store = MemoryRefStore::new(); + let h1 = hash_byte(0x01)?; + let h2 = hash_byte(0x02)?; + store.set_ref("refs/heads/b", &h1)?; + store.set_ref("refs/heads/a", &h2)?; + + let names: Vec = store.list_refs()?.collect::>()?; + assert_eq!( + names, + vec!["refs/heads/a".to_string(), "refs/heads/b".to_string()] + ); + Ok(()) + } +} diff --git a/libvctrl_core/tests/common/mod.rs b/libvctrl_core/tests/common/mod.rs new file mode 100644 index 0000000..bee37c0 --- /dev/null +++ b/libvctrl_core/tests/common/mod.rs @@ -0,0 +1,5 @@ +use libvctrl_handler::{Hash, VctrlError}; + +pub const fn make_hash(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) +} diff --git a/libvctrl_core/tests/integration_builders.rs b/libvctrl_core/tests/integration_builders.rs new file mode 100644 index 0000000..1393e27 --- /dev/null +++ b/libvctrl_core/tests/integration_builders.rs @@ -0,0 +1,40 @@ +use libvctrl_sha512 as _; +use proptest as _; + +use libvctrl_core::object::{ + BlobBuilder, CommitBuilder, TagBuilder, TreeBuilder, TreeEntryBuilder, +}; +use libvctrl_handler::{EntryKind, UserID, VctrlError}; + +pub mod common; + +fn make_user(name: &str, email: &str) -> Result { + UserID::new(name.to_string(), email.to_string()) +} + +#[test] +fn builder_chain_public_api() -> Result<(), VctrlError> { + let hash = common::make_hash(0x77)?; + let entry = TreeEntryBuilder::new("file".to_string(), EntryKind::Blob, hash).build()?; + let _tree = TreeBuilder::new().entry(entry).build()?; + + let blob = BlobBuilder::new().with_data(vec![1_u8, 2]).build()?; + assert_eq!(blob.data(), &[1_u8, 2]); + + let commit = CommitBuilder::new() + .tree(common::make_hash(0x78)?) + .author(make_user("Alice", "alice@example.com")?) + .committer(make_user("Bob", "bob@example.com")?) + .message("builder commit") + .build()?; + assert_eq!(commit.message(), "builder commit"); + + let tag = TagBuilder::new() + .name("v1") + .target(common::make_hash(0x79)?) + .message("builder tag") + .build()?; + assert_eq!(tag.name(), "v1"); + + Ok(()) +} diff --git a/libvctrl_core/tests/integration_codec.rs b/libvctrl_core/tests/integration_codec.rs new file mode 100644 index 0000000..bdc4aaa --- /dev/null +++ b/libvctrl_core/tests/integration_codec.rs @@ -0,0 +1,113 @@ +use std::io::Cursor; + +use libvctrl_sha512 as _; +use proptest as _; + +use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; +use libvctrl_handler::{ + Blob, Commit, CommitMeta, Decoder, Encoder, EntryKind, Tag, Tree, TreeEntry, UserID, VctrlError, +}; + +pub mod common; + +fn make_user(name: &str, email: &str) -> Result { + UserID::new(name.to_string(), email.to_string()) +} + +fn make_meta(ts: i64, tz: i16) -> Result { + CommitMeta::new(ts, tz, None) +} + +#[test] +fn blob_roundtrip_through_public_api() -> Result<(), VctrlError> { + let payload = vec![9_u8, 8, 7, 6]; + let blob = Blob::new(payload.clone())?; + + let mut buf = Vec::new(); + BinaryEncoder.encode_blob(&blob, &mut buf)?; + + let decoded = BinaryDecoder.decode_blob(Cursor::new(buf))?; + assert_eq!(decoded.data(), payload.as_slice()); + + Ok(()) +} + +#[test] +fn tree_roundtrip_through_public_api() -> Result<(), VctrlError> { + let hash = common::make_hash(0x44)?; + let entry = TreeEntry::new("file.txt".to_string(), EntryKind::Executable, hash)?; + let tree = Tree::new(vec![entry])?; + + let mut buf = Vec::new(); + BinaryEncoder.encode_tree(&tree, &mut buf)?; + + let decoded = BinaryDecoder.decode_tree(Cursor::new(buf))?; + assert_eq!(decoded.entries().len(), 1); + let first = decoded + .entries() + .first() + .ok_or_else(|| VctrlError::Other("expected entry".into()))?; + assert_eq!(first.name(), "file.txt"); + assert_eq!(first.kind(), EntryKind::Executable); + assert_eq!(*first.hash(), hash); + + Ok(()) +} + +#[test] +fn commit_roundtrip_through_public_api() -> Result<(), VctrlError> { + let tree = common::make_hash(0x55)?; + let parent = common::make_hash(0x56)?; + let author = make_user("Alice", "alice@example.com")?; + let committer = make_user("Bob", "bob@example.com")?; + let message = "integration commit".to_string(); + let meta = make_meta(1_600_000_000, 0)?; + + let commit = Commit::with_meta(tree, vec![parent], author, committer, message.clone(), meta)?; + + let mut buf = Vec::new(); + BinaryEncoder.encode_commit(&commit, &mut buf)?; + + let decoded = BinaryDecoder.decode_commit(Cursor::new(buf))?; + assert_eq!(decoded.tree(), &tree); + assert_eq!(decoded.parents(), &[parent]); + assert_eq!(decoded.author().name(), "Alice"); + assert_eq!(decoded.committer().email(), "bob@example.com"); + assert_eq!(decoded.message(), message); + assert_eq!(decoded.meta().timestamp(), 1_600_000_000); + assert_eq!(decoded.meta().timezone_offset(), 0); + + Ok(()) +} + +#[test] +fn tag_roundtrip_through_public_api() -> Result<(), VctrlError> { + let target = common::make_hash(0x66)?; + let tagger = make_user("Tagger", "tagger@example.com")?; + let message = "v1.0".to_string(); + let meta = make_meta(1_600_000_000, 0)?; + + let tag = Tag::with_meta( + "v1.0".to_string(), + target, + Some(tagger), + message.clone(), + meta, + )?; + + let mut buf = Vec::new(); + BinaryEncoder.encode_tag(&tag, &mut buf)?; + + let decoded = BinaryDecoder.decode_tag(Cursor::new(buf))?; + assert_eq!(decoded.name(), "v1.0"); + assert_eq!(decoded.target(), &target); + let tagger = decoded + .tagger() + .ok_or_else(|| VctrlError::Other("expected tagger".into()))?; + assert_eq!(tagger.name(), "Tagger"); + assert_eq!(decoded.message(), message); + assert_eq!(decoded.meta().timestamp(), 1_600_000_000); + assert_eq!(decoded.meta().timezone_offset(), 0); + + Ok(()) +} diff --git a/libvctrl_core/tests/integration_hash.rs b/libvctrl_core/tests/integration_hash.rs new file mode 100644 index 0000000..3070cf2 --- /dev/null +++ b/libvctrl_core/tests/integration_hash.rs @@ -0,0 +1,26 @@ +use std::io::Cursor; + +use libvctrl_sha512 as _; +use proptest as _; + +use libvctrl_core::hash::Sha512Hasher; +use libvctrl_handler::{Hasher, VctrlError}; + +#[test] +fn sha512_hasher_public_api() -> Result<(), VctrlError> { + let hasher = Sha512Hasher; + let hash = hasher.hash(Cursor::new(b"abc"))?; + + assert_eq!( + hash.as_bytes(), + &[ + 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, + 0x41, 0x31, 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, 0x0a, 0x9e, 0xee, 0xe6, + 0x4b, 0x55, 0xd3, 0x9a, 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, 0x36, 0xba, + 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, + 0x2a, 0x9a, 0xc9, 0x4f, 0xa5, 0x4c, 0xa4, 0x9f + ] + ); + + Ok(()) +} diff --git a/libvctrl_core/tests/integration_store.rs b/libvctrl_core/tests/integration_store.rs new file mode 100644 index 0000000..a0e22c8 --- /dev/null +++ b/libvctrl_core/tests/integration_store.rs @@ -0,0 +1,72 @@ +use std::io::Read; + +use libvctrl_sha512 as _; +use proptest as _; + +use libvctrl_core::store::{MemoryRefStore, MemoryStore}; +use libvctrl_handler::{ObjectStore, RefStore, VctrlError}; + +pub mod common; + +#[test] +fn memory_store_put_get_delete_exists() -> Result<(), VctrlError> { + let mut store = MemoryStore::new(); + let hash = common::make_hash(0xAA)?; + let data = vec![1_u8, 2, 3, 4]; + + store.put(&hash, &data)?; + + { + let mut reader = store.get(&hash)?; + let mut buf = Vec::new(); + let _ = reader.read_to_end(&mut buf)?; + assert_eq!(buf, data); + } + + assert!(store.exists(&hash)?); + store.delete(&hash)?; + assert!(!store.exists(&hash)?); + + Ok(()) +} + +#[test] +fn memory_store_get_missing_errors() -> Result<(), VctrlError> { + let store = MemoryStore::new(); + let hash = common::make_hash(0xBB)?; + let result = store.get(&hash); + assert!(matches!(result, Err(VctrlError::ObjectNotFound(_)))); + Ok(()) +} + +#[test] +fn memory_ref_store_roundtrip_and_list() -> Result<(), VctrlError> { + let mut store = MemoryRefStore::new(); + let h1 = common::make_hash(0x01)?; + let h2 = common::make_hash(0x02)?; + + store.set_ref("refs/heads/main", &h1)?; + store.set_ref("refs/heads/dev", &h2)?; + + assert_eq!(store.get_ref("refs/heads/main")?, h1); + + let names: Vec = store.list_refs()?.collect::>()?; + assert_eq!( + names, + vec!["refs/heads/dev".to_string(), "refs/heads/main".to_string()] + ); + + store.delete_ref("refs/heads/dev")?; + assert!(store.get_ref("refs/heads/dev").is_err()); + + Ok(()) +} + +#[test] +fn memory_ref_store_invalid_name_errors() -> Result<(), VctrlError> { + let mut store = MemoryRefStore::new(); + let hash = common::make_hash(0x03)?; + let result = store.set_ref("bad name", &hash); + assert!(result.is_err()); + Ok(()) +}