use thiserror::Error;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum CoreException {
#[error("object does not exist")]
ObjectDoesNotExist,
#[error("get() returned multiple objects")]
MultipleObjectsReturned,
#[error("suspicious operation: {0}")]
SuspiciousOperation(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("view does not exist: {0}")]
ViewDoesNotExist(String),
#[error("middleware not used: {0}")]
MiddlewareNotUsed(String),
#[error("improperly configured: {0}")]
ImproperlyConfigured(String),
#[error("field error: {0}")]
FieldError(String),
#[error("{0}")]
ValidationError(String),
#[error("suspicious file operation: {0}")]
SuspiciousFileOperation(String),
#[error("request data too big")]
RequestDataTooBig,
#[error("too many fields sent")]
TooManyFieldsSent,
#[error("request aborted")]
RequestAborted,
#[error("bad request: {0}")]
BadRequest(String),
#[error("disallowed host: {0}")]
DisallowedHost(String),
#[error("disallowed redirect: {0}")]
DisallowedRedirect(String),
}
impl CoreException {
#[must_use]
pub fn is_suspicious(&self) -> bool {
matches!(
self,
Self::SuspiciousOperation(_)
| Self::SuspiciousFileOperation(_)
| Self::DisallowedHost(_)
| Self::DisallowedRedirect(_)
)
}
}
impl From<crate::core::validators::ValidationError> for CoreException {
fn from(error: crate::core::validators::ValidationError) -> Self {
Self::ValidationError(error.message)
}
}
#[cfg(test)]
mod tests {
use super::CoreException;
use crate::core::validators::ValidationError as ValidatorValidationError;
use std::error::Error as StdError;
#[test]
fn object_does_not_exist_display_matches_django_text() {
assert_eq!(
CoreException::ObjectDoesNotExist.to_string(),
"object does not exist"
);
}
#[test]
fn multiple_objects_returned_display_matches_django_text() {
assert_eq!(
CoreException::MultipleObjectsReturned.to_string(),
"get() returned multiple objects"
);
}
#[test]
fn suspicious_operation_display_includes_message() {
assert_eq!(
CoreException::SuspiciousOperation("tampered payload".into()).to_string(),
"suspicious operation: tampered payload"
);
}
#[test]
fn permission_denied_display_includes_message() {
assert_eq!(
CoreException::PermissionDenied("missing scope".into()).to_string(),
"permission denied: missing scope"
);
}
#[test]
fn view_does_not_exist_display_includes_message() {
assert_eq!(
CoreException::ViewDoesNotExist("admin.index".into()).to_string(),
"view does not exist: admin.index"
);
}
#[test]
fn middleware_not_used_display_includes_message() {
assert_eq!(
CoreException::MiddlewareNotUsed("csrf".into()).to_string(),
"middleware not used: csrf"
);
}
#[test]
fn improperly_configured_display_includes_message() {
assert_eq!(
CoreException::ImproperlyConfigured("missing database url".into()).to_string(),
"improperly configured: missing database url"
);
}
#[test]
fn field_error_display_includes_message() {
assert_eq!(
CoreException::FieldError("unknown field".into()).to_string(),
"field error: unknown field"
);
}
#[test]
fn validation_error_display_is_raw_message() {
assert_eq!(
CoreException::ValidationError("Enter a valid value.".into()).to_string(),
"Enter a valid value."
);
}
#[test]
fn suspicious_file_operation_display_includes_message() {
assert_eq!(
CoreException::SuspiciousFileOperation("path traversal".into()).to_string(),
"suspicious file operation: path traversal"
);
}
#[test]
fn request_data_too_big_display_matches_django_text() {
assert_eq!(
CoreException::RequestDataTooBig.to_string(),
"request data too big"
);
}
#[test]
fn too_many_fields_sent_display_matches_django_text() {
assert_eq!(
CoreException::TooManyFieldsSent.to_string(),
"too many fields sent"
);
}
#[test]
fn request_aborted_display_matches_django_text() {
assert_eq!(CoreException::RequestAborted.to_string(), "request aborted");
}
#[test]
fn bad_request_display_includes_message() {
assert_eq!(
CoreException::BadRequest("malformed query".into()).to_string(),
"bad request: malformed query"
);
}
#[test]
fn disallowed_host_display_includes_message() {
assert_eq!(
CoreException::DisallowedHost("evil.example".into()).to_string(),
"disallowed host: evil.example"
);
}
#[test]
fn disallowed_redirect_display_includes_message() {
assert_eq!(
CoreException::DisallowedRedirect("https://evil.example".into()).to_string(),
"disallowed redirect: https://evil.example"
);
}
#[test]
fn suspicious_variants_are_marked_suspicious() {
assert!(CoreException::SuspiciousOperation("tampered".into()).is_suspicious());
assert!(CoreException::SuspiciousFileOperation("path traversal".into()).is_suspicious());
assert!(CoreException::DisallowedHost("evil.example".into()).is_suspicious());
assert!(CoreException::DisallowedRedirect("https://evil.example".into()).is_suspicious());
}
#[test]
fn non_suspicious_variants_are_not_marked_suspicious() {
assert!(!CoreException::ObjectDoesNotExist.is_suspicious());
assert!(!CoreException::PermissionDenied("missing scope".into()).is_suspicious());
assert!(!CoreException::BadRequest("bad body".into()).is_suspicious());
}
#[test]
fn core_exception_implements_std_error() {
fn assert_error<E: StdError>() {}
assert_error::<CoreException>();
}
#[test]
fn core_exception_has_no_error_source() {
let error = CoreException::FieldError("unknown field".into());
assert!(StdError::source(&error).is_none());
}
#[test]
fn validator_validation_error_converts_into_core_exception() {
let validation_error = ValidatorValidationError::new("Enter a valid value.", "invalid");
let exception = CoreException::from(validation_error);
assert_eq!(
exception,
CoreException::ValidationError("Enter a valid value.".into())
);
}
#[test]
fn core_exception_is_cloneable_and_equatable() {
let original = CoreException::MiddlewareNotUsed("csrf".into());
let cloned = original.clone();
assert_eq!(cloned, original);
assert_eq!(cloned, CoreException::MiddlewareNotUsed("csrf".into()));
}
}