#![allow(clippy::unwrap_used, reason = "tests")]
use mnesis::{ErrorId, KernelError, Version};
use mnesis_store::conflict::ConflictPredicate;
use mnesis_store::{LoadWithError, StoreError};
type TestStoreError = StoreError<std::io::Error, std::io::Error, std::io::Error>;
type TestLoadWithError =
LoadWithError<std::io::Error, std::io::Error, std::io::Error, std::io::Error>;
fn label(s: &str) -> ErrorId {
ErrorId::from_display(&s)
}
#[test]
fn conflict_display_contains_stream_id_and_versions() {
let err: TestStoreError = StoreError::Conflict {
stream_id: label("order-42"),
expected: Version::new(3),
actual: Version::new(5),
};
let msg = format!("{err}");
assert!(msg.contains("order-42"), "should contain stream_id");
assert!(msg.contains('3'), "should contain expected version");
assert!(msg.contains('5'), "should contain actual version");
assert!(msg.contains("conflict"), "should mention conflict");
assert!(
std::error::Error::source(&err).is_none(),
"Conflict variant should have no source"
);
}
#[test]
fn is_conflict_true_only_for_conflict_variant() {
let conflict: TestStoreError = StoreError::Conflict {
stream_id: label("order-42"),
expected: Version::new(3),
actual: Version::new(5),
};
assert!(
conflict.is_conflict(),
"Conflict variant must be a conflict"
);
let not_found: TestStoreError = StoreError::StreamNotFound {
stream_id: label("user-99"),
};
assert!(!not_found.is_conflict(), "StreamNotFound is not a conflict");
let decode: TestStoreError = StoreError::Decode(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"bad bytes",
));
assert!(!decode.is_conflict(), "Decode is not a conflict");
let overflow: TestStoreError = StoreError::VersionOverflow;
assert!(!overflow.is_conflict(), "VersionOverflow is not a conflict");
}
#[test]
fn conflict_predicate_trait_impl_tracks_variant() {
let conflict: TestStoreError = StoreError::Conflict {
stream_id: label("order-42"),
expected: Version::new(3),
actual: Version::new(5),
};
assert!(
<TestStoreError as ConflictPredicate>::is_conflict(&conflict),
"trait impl must report Conflict as a conflict"
);
let not_found: TestStoreError = StoreError::StreamNotFound {
stream_id: label("user-99"),
};
assert!(
!<TestStoreError as ConflictPredicate>::is_conflict(¬_found),
"trait impl must report a non-Conflict variant as not a conflict"
);
}
#[test]
fn stream_not_found_display_contains_stream_id() {
let err: TestStoreError = StoreError::StreamNotFound {
stream_id: label("user-99"),
};
let msg = format!("{err}");
assert!(msg.contains("user-99"), "should contain stream_id");
assert!(msg.contains("not found"), "should mention 'not found'");
assert!(
std::error::Error::source(&err).is_none(),
"StreamNotFound variant should have no source"
);
}
#[test]
fn encode_display_contains_inner_message() {
let inner = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad json");
let err: TestStoreError = StoreError::Encode(inner);
let msg = format!("{err}");
assert!(msg.contains("encode"), "should mention encode");
assert!(msg.contains("bad json"), "should contain inner message");
let source = std::error::Error::source(&err);
assert!(source.is_some(), "Encode variant should have a source");
let source_msg = format!("{}", source.unwrap());
assert!(
source_msg.contains("bad json"),
"source should contain inner message"
);
}
#[test]
fn decode_display_contains_inner_message() {
let inner = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad json");
let err: TestStoreError = StoreError::Decode(inner);
let msg = format!("{err}");
assert!(msg.contains("decode"), "should mention decode");
assert!(msg.contains("bad json"), "should contain inner message");
let source = std::error::Error::source(&err);
assert!(source.is_some(), "Decode variant should have a source");
let source_msg = format!("{}", source.unwrap());
assert!(
source_msg.contains("bad json"),
"source should contain inner message"
);
}
#[test]
fn adapter_display_contains_inner_message() {
let inner = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "db offline");
let err: TestStoreError = StoreError::Adapter(inner);
let msg = format!("{err}");
assert!(msg.contains("adapter"), "should mention adapter");
assert!(msg.contains("db offline"), "should contain inner message");
let source = std::error::Error::source(&err);
assert!(source.is_some(), "Adapter variant should have a source");
let source_msg = format!("{}", source.unwrap());
assert!(
source_msg.contains("db offline"),
"source should contain inner message"
);
}
#[test]
fn kernel_error_converts_to_store_error() {
let kernel_err = KernelError::VersionMismatch {
expected: Version::INITIAL,
actual: Version::new(2).unwrap(),
};
let store_err: TestStoreError = kernel_err.into();
assert!(matches!(store_err, StoreError::Kernel(_)));
let msg = format!("{store_err}");
assert!(msg.contains("kernel"), "should mention kernel");
assert!(msg.contains("mismatch"), "should mention mismatch");
}
#[test]
fn kernel_error_has_source_chain() {
let kernel_err = KernelError::RehydrationLimitExceeded { max: 100 };
let store_err: TestStoreError = kernel_err.into();
let source = std::error::Error::source(&store_err);
assert!(source.is_some(), "Kernel variant should have a source");
}
#[test]
fn load_with_upcast_display_contains_inner_message() {
let inner = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad transform");
let err: TestLoadWithError = LoadWithError::Upcast(inner);
let msg = format!("{err}");
assert!(msg.contains("upcast"), "should mention upcast");
assert!(
msg.contains("bad transform"),
"should contain inner message"
);
let source = std::error::Error::source(&err);
assert!(source.is_some(), "Upcast variant should have a source");
}
#[test]
fn conflict_display_truncates_overlong_stream_id_with_ellipsis() {
let long = "s".repeat(100);
let err: TestStoreError = StoreError::Conflict {
stream_id: ErrorId::from_display(&long),
expected: Version::new(3),
actual: Version::new(5),
};
let msg = format!("{err}");
assert!(
!msg.contains(&long),
"the untruncated stream id must not be rendered"
);
assert!(
msg.contains('…'),
"truncation must be visually signalled with '…'"
);
assert!(
msg.contains(&"s".repeat(61)),
"the truncated prefix should still be present"
);
assert!(msg.contains('3') && msg.contains('5'));
}
#[test]
fn stream_not_found_display_truncates_overlong_stream_id_with_ellipsis() {
let long = "u".repeat(200);
let err: TestStoreError = StoreError::StreamNotFound {
stream_id: ErrorId::from_display(&long),
};
let msg = format!("{err}");
assert!(
!msg.contains(&long),
"the untruncated stream id must not be rendered"
);
assert!(
msg.contains('…'),
"truncation must be visually signalled with '…'"
);
assert!(msg.contains("not found"), "should mention 'not found'");
}
#[test]
fn load_with_store_variant_wraps_store_error() {
let inner = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "db offline");
let store_err: TestStoreError = StoreError::Adapter(inner);
let load_err: TestLoadWithError = store_err.into();
let msg = format!("{load_err}");
assert!(
msg.contains("db offline"),
"Store variant should forward source"
);
let source = std::error::Error::source(&load_err);
assert!(source.is_some(), "Store variant should have a source");
}