use crate::clients::HttpError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum OAuthError {
#[error("HMAC signature validation failed")]
InvalidHmac,
#[error("State parameter mismatch: expected '{expected}', received '{received}'")]
StateMismatch {
expected: String,
received: String,
},
#[error("Token exchange failed with status {status}: {message}")]
TokenExchangeFailed {
status: u16,
message: String,
},
#[error("Client credentials exchange failed with status {status}: {message}")]
ClientCredentialsFailed {
status: u16,
message: String,
},
#[error("Token refresh failed with status {status}: {message}")]
TokenRefreshFailed {
status: u16,
message: String,
},
#[error("Invalid callback: {reason}")]
InvalidCallback {
reason: String,
},
#[error("Host URL must be configured in ShopifyConfig for OAuth")]
MissingHostConfig,
#[error("Invalid JWT: {reason}")]
InvalidJwt {
reason: String,
},
#[error("Token exchange requires an embedded app configuration")]
NotEmbeddedApp,
#[error("Client credentials requires a non-embedded app configuration")]
NotPrivateApp,
#[error(transparent)]
HttpError(#[from] HttpError),
}
const _: fn() = || {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<OAuthError>();
};
#[cfg(test)]
mod tests {
use super::*;
use crate::clients::{HttpResponseError, InvalidHttpRequestError};
#[test]
fn test_invalid_hmac_formats_correctly() {
let error = OAuthError::InvalidHmac;
assert_eq!(error.to_string(), "HMAC signature validation failed");
}
#[test]
fn test_state_mismatch_includes_expected_and_received() {
let error = OAuthError::StateMismatch {
expected: "abc123".to_string(),
received: "xyz789".to_string(),
};
let message = error.to_string();
assert!(message.contains("abc123"));
assert!(message.contains("xyz789"));
assert!(message.contains("expected"));
assert!(message.contains("received"));
}
#[test]
fn test_token_exchange_failed_includes_status_and_message() {
let error = OAuthError::TokenExchangeFailed {
status: 401,
message: "Invalid client credentials".to_string(),
};
let message = error.to_string();
assert!(message.contains("401"));
assert!(message.contains("Invalid client credentials"));
}
#[test]
fn test_from_http_error_conversion() {
let http_error = HttpError::Response(HttpResponseError {
code: 500,
message: "Internal server error".to_string(),
error_reference: None,
});
let oauth_error: OAuthError = http_error.into();
match oauth_error {
OAuthError::HttpError(_) => {}
_ => panic!("Expected HttpError variant"),
}
}
#[test]
fn test_oauth_error_implements_std_error() {
let error: &dyn std::error::Error = &OAuthError::InvalidHmac;
let _ = error;
let error: &dyn std::error::Error = &OAuthError::StateMismatch {
expected: "a".to_string(),
received: "b".to_string(),
};
let _ = error;
let error: &dyn std::error::Error = &OAuthError::TokenExchangeFailed {
status: 400,
message: "test".to_string(),
};
let _ = error;
let error: &dyn std::error::Error = &OAuthError::InvalidCallback {
reason: "test".to_string(),
};
let _ = error;
let error: &dyn std::error::Error = &OAuthError::MissingHostConfig;
let _ = error;
let error: &dyn std::error::Error = &OAuthError::InvalidJwt {
reason: "test".to_string(),
};
let _ = error;
let error: &dyn std::error::Error = &OAuthError::NotEmbeddedApp;
let _ = error;
let error: &dyn std::error::Error = &OAuthError::ClientCredentialsFailed {
status: 401,
message: "test".to_string(),
};
let _ = error;
let error: &dyn std::error::Error = &OAuthError::NotPrivateApp;
let _ = error;
let error: &dyn std::error::Error = &OAuthError::TokenRefreshFailed {
status: 400,
message: "test".to_string(),
};
let _ = error;
}
#[test]
fn test_invalid_callback_includes_reason() {
let error = OAuthError::InvalidCallback {
reason: "Shop domain is invalid".to_string(),
};
assert!(error.to_string().contains("Shop domain is invalid"));
}
#[test]
fn test_missing_host_config_message() {
let error = OAuthError::MissingHostConfig;
assert!(error.to_string().contains("Host URL"));
assert!(error.to_string().contains("configured"));
}
#[test]
fn test_http_error_from_invalid_request() {
let invalid = InvalidHttpRequestError::MissingBodyType;
let http_error = HttpError::InvalidRequest(invalid);
let oauth_error: OAuthError = http_error.into();
match oauth_error {
OAuthError::HttpError(HttpError::InvalidRequest(_)) => {}
_ => panic!("Expected HttpError::InvalidRequest variant"),
}
}
#[test]
fn test_oauth_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<OAuthError>();
}
#[test]
fn test_invalid_jwt_formats_error_message_with_reason() {
let error = OAuthError::InvalidJwt {
reason: "Token expired".to_string(),
};
let message = error.to_string();
assert!(message.contains("Invalid JWT"));
assert!(message.contains("Token expired"));
}
#[test]
fn test_not_embedded_app_has_correct_error_message() {
let error = OAuthError::NotEmbeddedApp;
let message = error.to_string();
assert!(message.contains("embedded app"));
assert!(message.contains("Token exchange"));
}
#[test]
fn test_new_variants_implement_std_error() {
let invalid_jwt_error: &dyn std::error::Error = &OAuthError::InvalidJwt {
reason: "test reason".to_string(),
};
assert!(invalid_jwt_error.to_string().contains("Invalid JWT"));
let not_embedded_error: &dyn std::error::Error = &OAuthError::NotEmbeddedApp;
assert!(not_embedded_error.to_string().contains("embedded app"));
}
#[test]
fn test_new_variants_are_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<OAuthError>();
let invalid_jwt = OAuthError::InvalidJwt {
reason: "test".to_string(),
};
let not_embedded = OAuthError::NotEmbeddedApp;
std::thread::spawn(move || {
let _ = invalid_jwt;
})
.join()
.unwrap();
std::thread::spawn(move || {
let _ = not_embedded;
})
.join()
.unwrap();
}
#[test]
fn test_client_credentials_failed_formats_error_message_with_status_and_message() {
let error = OAuthError::ClientCredentialsFailed {
status: 401,
message: "Invalid client credentials".to_string(),
};
let message = error.to_string();
assert!(message.contains("Client credentials"));
assert!(message.contains("401"));
assert!(message.contains("Invalid client credentials"));
}
#[test]
fn test_not_private_app_has_correct_error_message() {
let error = OAuthError::NotPrivateApp;
let message = error.to_string();
assert!(message.contains("non-embedded"));
assert!(message.contains("Client credentials"));
}
#[test]
fn test_client_credentials_variants_implement_std_error() {
let client_creds_error: &dyn std::error::Error = &OAuthError::ClientCredentialsFailed {
status: 500,
message: "Server error".to_string(),
};
assert!(client_creds_error
.to_string()
.contains("Client credentials"));
let not_private_error: &dyn std::error::Error = &OAuthError::NotPrivateApp;
assert!(not_private_error.to_string().contains("non-embedded"));
}
#[test]
fn test_client_credentials_variants_are_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<OAuthError>();
let client_creds_failed = OAuthError::ClientCredentialsFailed {
status: 401,
message: "test".to_string(),
};
let not_private = OAuthError::NotPrivateApp;
std::thread::spawn(move || {
let _ = client_creds_failed;
})
.join()
.unwrap();
std::thread::spawn(move || {
let _ = not_private;
})
.join()
.unwrap();
}
#[test]
fn test_token_refresh_failed_formats_error_message_with_status_and_message() {
let error = OAuthError::TokenRefreshFailed {
status: 400,
message: "Invalid refresh token".to_string(),
};
let message = error.to_string();
assert!(message.contains("Token refresh"));
assert!(message.contains("400"));
assert!(message.contains("Invalid refresh token"));
}
#[test]
fn test_token_refresh_failed_with_network_error_status_zero() {
let error = OAuthError::TokenRefreshFailed {
status: 0,
message: "Network error: connection refused".to_string(),
};
let message = error.to_string();
assert!(message.contains("Token refresh"));
assert!(message.contains("0"));
assert!(message.contains("Network error"));
}
#[test]
fn test_token_refresh_failed_implements_std_error() {
let error: &dyn std::error::Error = &OAuthError::TokenRefreshFailed {
status: 401,
message: "Unauthorized".to_string(),
};
assert!(error.to_string().contains("Token refresh"));
}
#[test]
fn test_token_refresh_failed_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<OAuthError>();
let token_refresh_failed = OAuthError::TokenRefreshFailed {
status: 400,
message: "test".to_string(),
};
std::thread::spawn(move || {
let _ = token_refresh_failed;
})
.join()
.unwrap();
}
}