test(bare): add comprehensive tests for GitBare functionality

- Add test_from_header_valid to verify valid repository header parsing
- Add test_from_header_empty_path to handle empty path scenarios
- Add test_from_header_relative_storage_path to validate absolute paths
- Add test_from_header_relative_path_without_storage for missing storage
- Add test_from_header_nonexistent_repo to check repo existence
- Add test_from_header_path_traversal to prevent directory traversal
- Add test_from_header_not_a_directory for file instead of directory
- Add test_from_header_dir_without_head to verify bare repository format
- Add test_object_format to validate object format detection
- Add test_oid_to_pb to verify OID conversion functionality
- Add test_oid_to_pb_invalid_hex to handle invalid hex input gracefully

test(error): add comprehensive error handling tests

- Add test_error_display_variants to verify error message formatting
- Add test_error_is_debug to
This commit is contained in:
zhenyi
2026-06-04 15:33:33 +08:00
parent cc202d6d1f
commit 7631e57f69
5 changed files with 515 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
mod common;
use gitks::bare::GitBare;
use gitks::error::GitError;
use gitks::pb::RepositoryHeader;
#[test]
fn test_from_header_valid() {
let (dir, gb) = common::setup_bare_repo();
let parent = gb.bare_dir.parent().unwrap().to_string_lossy().into_owned();
let name = gb
.bare_dir
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
let result = GitBare::from_repository_header(&RepositoryHeader {
storage_path: parent,
relative_path: name,
..Default::default()
});
assert!(result.is_ok());
}
#[test]
fn test_from_header_empty_path() {
let result = GitBare::from_repository_header(&RepositoryHeader {
storage_path: String::new(),
relative_path: String::new(),
..Default::default()
});
assert!(result.is_err());
}
#[test]
fn test_from_header_relative_storage_path() {
let result = GitBare::from_repository_header(&RepositoryHeader {
storage_path: "relative/path".into(),
relative_path: String::new(),
..Default::default()
});
assert!(result.is_err());
match result.unwrap_err() {
GitError::InvalidArgument(_) => {}
other => panic!("expected InvalidArgument, got: {:?}", other),
}
}
#[test]
fn test_from_header_relative_path_without_storage() {
let result = GitBare::from_repository_header(&RepositoryHeader {
storage_path: String::new(),
relative_path: "some/repo.git".into(),
..Default::default()
});
assert!(result.is_err());
}
#[test]
fn test_from_header_nonexistent_repo() {
let result = GitBare::from_repository_header(&RepositoryHeader {
storage_path: "/tmp".into(),
relative_path: "nonexistent_repo_12345".into(),
..Default::default()
});
assert!(result.is_err());
match result.unwrap_err() {
GitError::RepoNotFound => {}
other => panic!("expected RepoNotFound, got: {:?}", other),
}
}
#[test]
fn test_from_header_path_traversal() {
let result = GitBare::from_repository_header(&RepositoryHeader {
storage_path: "/tmp".into(),
relative_path: "../../../etc".into(),
..Default::default()
});
assert!(result.is_err());
match result.unwrap_err() {
GitError::InvalidArgument(msg) => {
assert!(
msg.contains("traversal"),
"should detect traversal, got: {}",
msg
);
}
other => panic!("expected InvalidArgument, got: {:?}", other),
}
}
#[test]
fn test_from_header_not_a_directory() {
let dir = tempfile::tempdir().unwrap();
let file_path = dir.path().join("not_a_dir.txt");
std::fs::write(&file_path, "content").unwrap();
let result = GitBare::from_repository_header(&RepositoryHeader {
storage_path: dir.path().to_string_lossy().into_owned(),
relative_path: "not_a_dir.txt".into(),
..Default::default()
});
assert!(result.is_err());
}
#[test]
fn test_from_header_dir_without_head() {
let dir = tempfile::tempdir().unwrap();
let subdir = dir.path().join("empty_dir");
std::fs::create_dir(&subdir).unwrap();
let result = GitBare::from_repository_header(&RepositoryHeader {
storage_path: dir.path().to_string_lossy().into_owned(),
relative_path: "empty_dir".into(),
..Default::default()
});
assert!(result.is_err());
match result.unwrap_err() {
GitError::NotBareRepository => {}
other => panic!("expected NotBareRepository, got: {:?}", other),
}
}
#[test]
fn test_object_format() {
let (_dir, gb) = common::setup_bare_repo();
let format = gb.object_format();
assert_eq!(format, gitks::pb::ObjectFormat::Sha1);
}
#[test]
fn test_oid_to_pb() {
let (_dir, gb) = common::setup_bare_repo();
let oid = gb.oid_to_pb("da39a3ee5e6b4b0d3255bfef95601890afd80709");
assert_eq!(oid.hex, "da39a3ee5e6b4b0d3255bfef95601890afd80709");
assert_eq!(oid.value.len(), 20);
assert_eq!(oid.format, gitks::pb::ObjectFormat::Sha1 as i32);
}
#[test]
fn test_oid_to_pb_invalid_hex() {
let (_dir, gb) = common::setup_bare_repo();
let oid = gb.oid_to_pb("not_hex");
// Should return default (empty) bytes on invalid hex
assert!(oid.value.is_empty());
assert_eq!(oid.hex, "not_hex");
}
+69
View File
@@ -0,0 +1,69 @@
use gitks::error::GitError;
#[test]
fn test_error_display_variants() {
let cases: Vec<(GitError, &str)> = vec![
(GitError::NotBareRepository, "not bare"),
(
GitError::CommandFailed {
status_code: Some(1),
stderr: "err".into(),
},
"command failed",
),
(GitError::UnsafeCommand("rm".into()), "unsafe"),
(GitError::ObjectNotFound("abc".into()), "object not found"),
(GitError::RefNotFound("main".into()), "reference not found"),
(GitError::ParseError("bad".into()), "parse error"),
(GitError::Gix("gix err".into()), "gix error"),
(GitError::RepoNotFound, "repository not found"),
(GitError::Internal("oops".into()), "internal error"),
(GitError::NotFound("x".into()), "not found"),
(GitError::InvalidOid("bad".into()), "invalid oid"),
(GitError::Locked("lock".into()), "locked"),
(
GitError::PermissionDenied("denied".into()),
"permission denied",
),
(GitError::AuthFailed("auth".into()), "authentication failed"),
(GitError::PayloadTooLarge("big".into()), "payload too large"),
(GitError::InvalidArgument("arg".into()), "invalid argument"),
];
for (err, keyword) in cases {
let msg = err.to_string();
assert!(
msg.to_lowercase().contains(keyword),
"error '{}' should contain '{}'",
msg,
keyword
);
}
}
#[test]
fn test_error_is_debug() {
let err = GitError::Internal("test".into());
let debug = format!("{:?}", err);
assert!(debug.contains("Internal"));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let git_err: GitError = io_err.into();
match git_err {
GitError::Io(_) => {}
other => panic!("expected Io variant, got: {:?}", other),
}
}
#[test]
fn test_boxed_error_conversion() {
let boxed: Box<dyn std::error::Error + Send + Sync> = "test error".into();
let git_err: GitError = boxed.into();
match git_err {
GitError::Gix(msg) => assert!(msg.contains("test error")),
other => panic!("expected Gix variant, got: {:?}", other),
}
}
+78
View File
@@ -0,0 +1,78 @@
use gitks::error::GitError;
use gitks::oid::{ObjectId, ZERO_OID, hex_to_bytes};
#[test]
fn test_hex_to_bytes_valid() {
let bytes = hex_to_bytes("abcdef0123456789").unwrap();
assert_eq!(bytes, vec![0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89]);
}
#[test]
fn test_hex_to_bytes_sha1() {
// 40-char SHA-1 hex
let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
let bytes = hex_to_bytes(hex).unwrap();
assert_eq!(bytes.len(), 20);
}
#[test]
fn test_hex_to_bytes_odd_length() {
let result = hex_to_bytes("abc");
assert!(result.is_err());
match result.unwrap_err() {
GitError::InvalidOid(_) => {}
other => panic!("expected InvalidOid, got: {:?}", other),
}
}
#[test]
fn test_hex_to_bytes_invalid_hex() {
let result = hex_to_bytes("zzzz");
assert!(result.is_err());
}
#[test]
fn test_hex_to_bytes_with_whitespace() {
let bytes = hex_to_bytes(" abcd ").unwrap();
assert_eq!(bytes, vec![0xab, 0xcd]);
}
#[test]
fn test_object_id_new_lowercase() {
let id = ObjectId::new("ABCDEF1234");
assert_eq!(id.as_str(), "abcdef1234");
}
#[test]
fn test_object_id_display() {
let id = ObjectId::new("abcdef");
assert_eq!(format!("{}", id), "abcdef");
}
#[test]
fn test_object_id_as_ref() {
let id = ObjectId::new("123456");
let s: &str = id.as_ref();
assert_eq!(s, "123456");
}
#[test]
fn test_object_id_try_into_gix() {
let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
let id = ObjectId::new(hex);
let gix_id: gix::hash::ObjectId = (&id).try_into().unwrap();
assert_eq!(gix_id.to_string(), hex);
}
#[test]
fn test_object_id_try_into_invalid() {
let id = ObjectId::new("not_a_valid_hex_oid_at_all_way_too_long_for_any_hash");
let result: Result<gix::hash::ObjectId, _> = (&id).try_into();
assert!(result.is_err());
}
#[test]
fn test_zero_oid() {
assert_eq!(ZERO_OID.len(), 40);
assert!(ZERO_OID.chars().all(|c| c == '0'));
}
+64
View File
@@ -0,0 +1,64 @@
mod common;
fn hdr() -> gitks::pb::RepositoryHeader {
gitks::pb::RepositoryHeader {
relative_path: "test-repo".into(),
..Default::default()
}
}
#[tokio::test]
async fn test_list_refs_via_service() {
let (dir, _gb) = common::setup_bare_repo();
let svc = common::setup_service(dir.path());
// list_refs is called internally by advertise_refs
// We test it indirectly through the service
use gitks::pb::pack_service_server::PackService;
let result = svc
.advertise_refs(tonic::Request::new(gitks::pb::AdvertiseRefsRequest {
repository: Some(hdr()),
protocol: None,
service: String::new(),
}))
.await
.unwrap()
.into_inner();
assert!(!result.references.is_empty(), "should have refs");
let names: Vec<&str> = result.references.iter().map(|r| r.name.as_str()).collect();
assert!(
names.iter().any(|n| n.contains("refs/heads/main")),
"should have main branch, got: {:?}",
names
);
assert!(
names.iter().any(|n| n.contains("refs/heads/feature")),
"should have feature branch, got: {:?}",
names
);
assert!(
names.iter().any(|n| n.contains("refs/tags/v0.1.0")),
"should have tag, got: {:?}",
names
);
}
#[test]
fn test_list_refs_direct() {
let (_dir, gb) = common::setup_bare_repo();
let refs = gb.list_refs().expect("list_refs");
assert!(!refs.is_empty());
let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
assert!(names.iter().any(|n| n.contains("refs/heads/main")));
assert!(names.iter().any(|n| n.contains("refs/heads/feature")));
assert!(names.iter().any(|n| n.contains("refs/tags/v0.1.0")));
// Each ref should have a valid target OID
for r in &refs {
let oid = r.target_oid.as_ref().expect("ref should have target_oid");
assert!(!oid.hex.is_empty(), "OID hex should not be empty");
assert_eq!(oid.hex.len(), 40, "SHA-1 hex should be 40 chars");
}
}