lemmy/crates/utils/src/error.rs
netbrum 483bdd592e
Replace clippy::unwrap_used in tests (#5064)
* Add LemmyResult to session_middleware tests

* Add LemmyResult to inboxes tests

* Add LemmyResult to slurs tests

* Add LemmyResult to markdown tests

* Add LemmyResult to rate_limiter tests

* Add LemmyResult to error tests

* Add LemmyResult to api_common utils tests

* Add LemmyResult to request tests

* Add LemmyResult to claims tests

* Propagate registration_applications errors

* Remove clippy::unwrap_used from community tests

* Add LemmyResult to community_view tests

* Add LemmyResult to db_schema post tests

* Add LemmyResult to site_aggregates tests

* Add LemmyResult to private_message tests

* Add LemmyResult to activity tests

* Add LemmyResult to federation_allowlist tests

* Add LemmyResult to comment_aggregates tests

* Add LemmyResult to post_report tests

* Add LemmyResult to moderator tests

* Add LemmyResult to community_aggregates tests

* Add LemmyResult to person_aggregates tests

* Add LemmyResult to language tests

* Add LemmyResult to post_aggregates tests

* Add LemmyResult to db_schema comment tests

* Add LemmyResult to actor_language tests

* Add LemmyResult to vote_view tests

* Add LemmyResult to registration_application_view tests

* Add LemmyResult to private_message_view tests

* Add LemmyResult to private_message_report_view tests

* Add LemmyResult to post_report_view tests

* Add LemmyResult to comment_report_view tests

* Add LemmyResult to sitemap tests

* Replace .expect() with .unwrap()

* Format code

* Remove clippy::unwrap_used from activity tests

* Add diesel result in db_schema tests

* Format code

* Map to_bytes() error to LemmyErrorType

* Remove clippy::unwrap_used from error tests

* Removing a few more unwraps, and cleaning up language code.

* Replace map_err with unwrap_or_default

* Replace ok_or with and_then

---------

Co-authored-by: Dessalines <tyhou13@gmx.com>
2024-10-02 08:50:21 -04:00

353 lines
9.9 KiB
Rust

use cfg_if::cfg_if;
use serde::{Deserialize, Serialize};
use std::{backtrace::Backtrace, fmt::Debug};
use strum::{Display, EnumIter};
#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)]
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
#[cfg_attr(feature = "full", ts(export))]
#[serde(tag = "error", content = "message", rename_all = "snake_case")]
#[non_exhaustive]
// TODO: order these based on the crate they belong to (utils, federation, db, api)
pub enum LemmyErrorType {
ReportReasonRequired,
ReportTooLong,
NotAModerator,
NotAnAdmin,
CantBlockYourself,
CantBlockAdmin,
CouldntUpdateUser,
PasswordsDoNotMatch,
EmailNotVerified,
EmailRequired,
CouldntUpdateComment,
CouldntUpdatePrivateMessage,
CannotLeaveAdmin,
NoLinesInHtml,
SiteMetadataPageIsNotDoctypeHtml,
PictrsResponseError(String),
PictrsPurgeResponseError(String),
PictrsCachingDisabled,
ImageUrlMissingPathSegments,
ImageUrlMissingLastPathSegment,
PictrsApiKeyNotProvided,
NoContentTypeHeader,
NotAnImageType,
NotAModOrAdmin,
NoAdmins,
NotTopAdmin,
NotTopMod,
NotLoggedIn,
NotHigherMod,
NotHigherAdmin,
SiteBan,
Deleted,
BannedFromCommunity,
PersonIsBlocked,
CommunityIsBlocked,
InstanceIsBlocked,
VoteNotAllowed,
InstanceIsPrivate,
/// Password must be between 10 and 60 characters
InvalidPassword,
SiteDescriptionLengthOverflow,
HoneypotFailed,
RegistrationApplicationIsPending,
CantEnablePrivateInstanceAndFederationTogether,
Locked,
CouldntCreateComment,
MaxCommentDepthReached,
NoCommentEditAllowed,
OnlyAdminsCanCreateCommunities,
CommunityAlreadyExists,
LanguageNotAllowed,
OnlyModsCanPostInCommunity,
CouldntUpdatePost,
NoPostEditAllowed,
EditPrivateMessageNotAllowed,
SiteAlreadyExists,
ApplicationQuestionRequired,
InvalidDefaultPostListingType,
RegistrationClosed,
RegistrationApplicationAnswerRequired,
RegistrationUsernameRequired,
EmailAlreadyExists,
UsernameAlreadyExists,
FederationForbiddenByStrictAllowList,
PersonIsBannedFromCommunity,
ObjectIsNotPublic,
InvalidCommunity,
CannotCreatePostOrCommentInDeletedOrRemovedCommunity,
CannotReceivePage,
NewPostCannotBeLocked,
OnlyLocalAdminCanRemoveCommunity,
OnlyLocalAdminCanRestoreCommunity,
NoIdGiven,
IncorrectLogin,
InvalidQuery,
ObjectNotLocal,
PostIsLocked,
PersonIsBannedFromSite(String),
InvalidVoteValue,
PageDoesNotSpecifyCreator,
NoEmailSetup,
LocalSiteNotSetup,
EmailSmtpServerNeedsAPort,
MissingAnEmail,
RateLimitError,
InvalidName,
InvalidDisplayName,
InvalidMatrixId,
InvalidPostTitle,
InvalidBodyField,
BioLengthOverflow,
AltTextLengthOverflow,
MissingTotpToken,
MissingTotpSecret,
IncorrectTotpToken,
CouldntParseTotpSecret,
CouldntGenerateTotp,
TotpAlreadyEnabled,
CouldntLikeComment,
CouldntSaveComment,
CouldntCreateReport,
CouldntResolveReport,
CommunityModeratorAlreadyExists,
CommunityUserAlreadyBanned,
CommunityBlockAlreadyExists,
CommunityFollowerAlreadyExists,
CouldntUpdateCommunityHiddenStatus,
PersonBlockAlreadyExists,
UserAlreadyExists,
CouldntLikePost,
CouldntSavePost,
CouldntMarkPostAsRead,
CouldntHidePost,
CouldntUpdateCommunity,
CouldntUpdateReplies,
CouldntUpdatePersonMentions,
PostTitleTooLong,
CouldntCreatePost,
CouldntCreatePrivateMessage,
CouldntUpdatePrivate,
SystemErrLogin,
CouldntSetAllRegistrationsAccepted,
CouldntSetAllEmailVerified,
Banned,
BlockedUrl,
CouldntGetComments,
CouldntGetPosts,
InvalidUrl,
EmailSendFailed,
Slurs,
RegistrationDenied(Option<String>),
FederationDisabled,
DomainBlocked(String),
DomainNotInAllowList(String),
FederationDisabledByStrictAllowList,
SiteNameRequired,
SiteNameLengthOverflow,
PermissiveRegex,
InvalidRegex,
CaptchaIncorrect,
CouldntCreateAudioCaptcha,
InvalidUrlScheme,
CouldntSendWebmention,
ContradictingFilters,
InstanceBlockAlreadyExists,
/// Thrown when an API call is submitted with more than 1000 array elements, see
/// [[MAX_API_PARAM_ELEMENTS]]
TooManyItems,
CommunityHasNoFollowers,
BanExpirationInPast,
InvalidUnixTime,
InvalidBotAction,
CantBlockLocalInstance,
UrlWithoutDomain,
InboxTimeout,
OauthAuthorizationInvalid,
OauthLoginFailed,
OauthRegistrationClosed,
CouldntDeleteOauthProvider,
Unknown(String),
CantDeleteSite,
UrlLengthOverflow,
PostScheduleTimeMustBeInFuture,
TooManyScheduledPosts,
NotFound,
}
cfg_if! {
if #[cfg(feature = "full")] {
use std::fmt;
pub type LemmyResult<T> = Result<T, LemmyError>;
pub struct LemmyError {
pub error_type: LemmyErrorType,
pub inner: anyhow::Error,
pub context: Backtrace,
}
/// Maximum number of items in an array passed as API parameter. See [[LemmyErrorType::TooManyItems]]
pub const MAX_API_PARAM_ELEMENTS: usize = 10_000;
impl<T> From<T> for LemmyError
where
T: Into<anyhow::Error>,
{
fn from(t: T) -> Self {
let cause = t.into();
let error_type = match cause.downcast_ref::<diesel::result::Error>() {
Some(&diesel::NotFound) => LemmyErrorType::NotFound,
_ => LemmyErrorType::Unknown(format!("{}", &cause))
};
LemmyError {
error_type,
inner: cause,
context: Backtrace::capture(),
}
}
}
impl Debug for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LemmyError")
.field("message", &self.error_type)
.field("inner", &self.inner)
.field("context", &self.context)
.finish()
}
}
impl fmt::Display for LemmyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: ", &self.error_type)?;
writeln!(f, "{}", self.inner)?;
fmt::Display::fmt(&self.context, f)
}
}
impl actix_web::error::ResponseError for LemmyError {
fn status_code(&self) -> actix_web::http::StatusCode {
if self.error_type == LemmyErrorType::IncorrectLogin {
return actix_web::http::StatusCode::UNAUTHORIZED;
}
match self.inner.downcast_ref::<diesel::result::Error>() {
Some(diesel::result::Error::NotFound) => actix_web::http::StatusCode::NOT_FOUND,
_ => actix_web::http::StatusCode::BAD_REQUEST,
}
}
fn error_response(&self) -> actix_web::HttpResponse {
actix_web::HttpResponse::build(self.status_code()).json(&self.error_type)
}
}
impl From<LemmyErrorType> for LemmyError {
fn from(error_type: LemmyErrorType) -> Self {
let inner = anyhow::anyhow!("{}", error_type);
LemmyError {
error_type,
inner,
context: Backtrace::capture(),
}
}
}
pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T>;
}
impl<T, E: Into<anyhow::Error>> LemmyErrorExt<T, E> for Result<T, E> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T> {
self.map_err(|error| LemmyError {
error_type,
inner: error.into(),
context: Backtrace::capture(),
})
}
}
pub trait LemmyErrorExt2<T> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T>;
fn into_anyhow(self) -> Result<T, anyhow::Error>;
}
impl<T> LemmyErrorExt2<T> for LemmyResult<T> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T> {
self.map_err(|mut e| {
e.error_type = error_type;
e
})
}
// this function can't be an impl From or similar because it would conflict with one of the other broad Into<> implementations
fn into_anyhow(self) -> Result<T, anyhow::Error> {
self.map_err(|e| e.inner)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use actix_web::{body::MessageBody, ResponseError};
use pretty_assertions::assert_eq;
use std::fs::read_to_string;
use strum::IntoEnumIterator;
#[test]
fn deserializes_no_message() -> LemmyResult<()> {
let err = LemmyError::from(LemmyErrorType::Banned).error_response();
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?;
assert_eq!(&json, "{\"error\":\"banned\"}");
Ok(())
}
#[test]
fn deserializes_with_message() -> LemmyResult<()> {
let reg_banned = LemmyErrorType::PersonIsBannedFromSite(String::from("reason"));
let err = LemmyError::from(reg_banned).error_response();
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?;
assert_eq!(
&json,
"{\"error\":\"person_is_banned_from_site\",\"message\":\"reason\"}"
);
Ok(())
}
#[test]
fn test_convert_diesel_errors() {
let not_found_error = LemmyError::from(diesel::NotFound);
assert_eq!(LemmyErrorType::NotFound, not_found_error.error_type);
assert_eq!(404, not_found_error.status_code());
let other_error = LemmyError::from(diesel::result::Error::NotInTransaction);
assert!(matches!(other_error.error_type, LemmyErrorType::Unknown{..}));
assert_eq!(400, other_error.status_code());
}
/// Check if errors match translations. Disabled because many are not translated at all.
#[test]
#[ignore]
fn test_translations_match() -> LemmyResult<()> {
#[derive(Deserialize)]
struct Err {
error: String,
}
let translations = read_to_string("translations/translations/en.json")?;
for e in LemmyErrorType::iter() {
let msg = serde_json::to_string(&e)?;
let msg: Err = serde_json::from_str(&msg)?;
let msg = msg.error;
assert!(translations.contains(&format!("\"{msg}\"")), "{msg}");
}
Ok(())
}
}
}
}