Skip to main content

shopify_sdk/webhooks/
errors.rs

1//! Webhook-specific error types for the Shopify API SDK.
2//!
3//! This module contains error types for webhook registration and verification operations.
4//!
5//! # Error Handling
6//!
7//! The SDK uses specific error types for different webhook failure scenarios:
8//!
9//! - [`WebhookError::HostNotConfigured`]: When `config.host()` is `None`
10//! - [`WebhookError::RegistrationNotFound`]: When a topic is not in the local registry
11//! - [`WebhookError::GraphqlError`]: Wrapped GraphQL errors
12//! - [`WebhookError::ShopifyError`]: For userErrors in GraphQL responses
13//! - [`WebhookError::InvalidHmac`]: When webhook signature verification fails
14//! - [`WebhookError::NoHandlerForTopic`]: When no handler is registered for a topic
15//! - [`WebhookError::PayloadParseError`]: When webhook payload JSON parsing fails
16//!
17//! # Example
18//!
19//! ```rust
20//! use shopify_sdk::webhooks::WebhookError;
21//! use shopify_sdk::rest::resources::v2025_10::common::WebhookTopic;
22//!
23//! let error = WebhookError::RegistrationNotFound {
24//!     topic: WebhookTopic::OrdersCreate,
25//! };
26//! println!("Error: {}", error);
27//! ```
28
29use crate::clients::GraphqlError;
30use crate::rest::resources::v2025_10::common::WebhookTopic;
31use thiserror::Error;
32
33/// Error type for webhook registration and verification operations.
34///
35/// This enum provides error types for webhook operations, including
36/// host configuration errors, registration lookup failures, signature
37/// verification failures, and wrapped GraphQL errors.
38///
39/// # Example
40///
41/// ```rust
42/// use shopify_sdk::webhooks::WebhookError;
43/// use shopify_sdk::rest::resources::v2025_10::common::WebhookTopic;
44///
45/// // Create a registration not found error
46/// let error = WebhookError::RegistrationNotFound {
47///     topic: WebhookTopic::OrdersCreate,
48/// };
49/// assert!(error.to_string().contains("not found"));
50/// ```
51#[derive(Debug, Error)]
52pub enum WebhookError {
53    /// Host URL is not configured in ShopifyConfig.
54    ///
55    /// This error occurs when attempting to register webhooks but
56    /// `config.host()` returns `None`. The host URL is required to
57    /// construct callback URLs for webhook subscriptions.
58    #[error("Host URL is not configured. Please set host in ShopifyConfig to register webhooks.")]
59    HostNotConfigured,
60
61    /// Webhook registration not found in the local registry.
62    ///
63    /// This error occurs when attempting to register a webhook topic
64    /// that hasn't been added to the registry via `add_registration()`.
65    #[error("Webhook registration not found for topic: {topic:?}")]
66    RegistrationNotFound {
67        /// The webhook topic that was not found.
68        topic: WebhookTopic,
69    },
70
71    /// An underlying GraphQL error occurred.
72    ///
73    /// This variant wraps [`GraphqlError`] for unified error handling.
74    #[error(transparent)]
75    GraphqlError(#[from] GraphqlError),
76
77    /// A Shopify API error occurred (from userErrors in GraphQL response).
78    ///
79    /// This error is returned when the GraphQL mutation succeeds (HTTP 200)
80    /// but Shopify returns userErrors in the response body.
81    #[error("Shopify API error: {message}")]
82    ShopifyError {
83        /// The error message from Shopify.
84        message: String,
85    },
86
87    /// Webhook subscription not found in Shopify.
88    ///
89    /// This error occurs when attempting to unregister a webhook that
90    /// doesn't exist in Shopify for the given topic.
91    #[error("Webhook subscription not found in Shopify for topic: {topic:?}")]
92    SubscriptionNotFound {
93        /// The webhook topic that was not found.
94        topic: WebhookTopic,
95    },
96
97    /// Webhook signature verification failed.
98    ///
99    /// This error occurs when the HMAC signature in the webhook request
100    /// does not match the expected signature computed from the request body.
101    /// The error message is intentionally generic to avoid leaking security details.
102    #[error("Webhook signature verification failed")]
103    InvalidHmac,
104
105    /// No handler registered for the webhook topic.
106    ///
107    /// This error occurs when attempting to process a webhook for a topic
108    /// that has no registered handler in the registry.
109    #[error("No handler registered for webhook topic: {topic}")]
110    NoHandlerForTopic {
111        /// The raw topic string that had no handler.
112        topic: String,
113    },
114
115    /// Webhook payload parsing failed.
116    ///
117    /// This error occurs when the webhook request body cannot be parsed
118    /// as valid JSON.
119    #[error("Failed to parse webhook payload: {message}")]
120    PayloadParseError {
121        /// The error message from the JSON parser.
122        message: String,
123    },
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::clients::{HttpError, HttpResponseError};
130
131    #[test]
132    fn test_host_not_configured_error_message() {
133        let error = WebhookError::HostNotConfigured;
134        let message = error.to_string();
135        assert!(message.contains("Host URL is not configured"));
136        assert!(message.contains("ShopifyConfig"));
137    }
138
139    #[test]
140    fn test_registration_not_found_error_message() {
141        let error = WebhookError::RegistrationNotFound {
142            topic: WebhookTopic::OrdersCreate,
143        };
144        let message = error.to_string();
145        assert!(message.contains("not found"));
146        assert!(message.contains("OrdersCreate"));
147    }
148
149    #[test]
150    fn test_shopify_error_message() {
151        let error = WebhookError::ShopifyError {
152            message: "Invalid callback URL".to_string(),
153        };
154        let message = error.to_string();
155        assert!(message.contains("Shopify API error"));
156        assert!(message.contains("Invalid callback URL"));
157    }
158
159    #[test]
160    fn test_from_graphql_error_conversion() {
161        let http_error = HttpError::Response(HttpResponseError {
162            code: 401,
163            message: r#"{"error":"Unauthorized"}"#.to_string(),
164            error_reference: None,
165        });
166        let graphql_error = GraphqlError::Http(http_error);
167
168        // Test From<GraphqlError> conversion
169        let webhook_error: WebhookError = graphql_error.into();
170
171        assert!(matches!(webhook_error, WebhookError::GraphqlError(_)));
172        assert!(webhook_error.to_string().contains("Unauthorized"));
173    }
174
175    #[test]
176    fn test_all_error_variants_implement_std_error() {
177        // HostNotConfigured
178        let error: &dyn std::error::Error = &WebhookError::HostNotConfigured;
179        let _ = error;
180
181        // RegistrationNotFound
182        let error: &dyn std::error::Error = &WebhookError::RegistrationNotFound {
183            topic: WebhookTopic::OrdersCreate,
184        };
185        let _ = error;
186
187        // ShopifyError
188        let error: &dyn std::error::Error = &WebhookError::ShopifyError {
189            message: "test".to_string(),
190        };
191        let _ = error;
192
193        // GraphqlError
194        let http_error = HttpError::Response(HttpResponseError {
195            code: 400,
196            message: "test".to_string(),
197            error_reference: None,
198        });
199        let error: &dyn std::error::Error =
200            &WebhookError::GraphqlError(GraphqlError::Http(http_error));
201        let _ = error;
202
203        // InvalidHmac
204        let error: &dyn std::error::Error = &WebhookError::InvalidHmac;
205        let _ = error;
206
207        // NoHandlerForTopic
208        let error: &dyn std::error::Error = &WebhookError::NoHandlerForTopic {
209            topic: "orders/create".to_string(),
210        };
211        let _ = error;
212
213        // PayloadParseError
214        let error: &dyn std::error::Error = &WebhookError::PayloadParseError {
215            message: "invalid json".to_string(),
216        };
217        let _ = error;
218    }
219
220    #[test]
221    fn test_subscription_not_found_error_message() {
222        let error = WebhookError::SubscriptionNotFound {
223            topic: WebhookTopic::ProductsUpdate,
224        };
225        let message = error.to_string();
226        assert!(message.contains("not found in Shopify"));
227        assert!(message.contains("ProductsUpdate"));
228    }
229
230    #[test]
231    fn test_invalid_hmac_error_message() {
232        let error = WebhookError::InvalidHmac;
233        let message = error.to_string();
234        assert_eq!(message, "Webhook signature verification failed");
235        // Ensure the message is generic and doesn't leak security details
236        assert!(!message.contains("key"));
237        assert!(!message.contains("secret"));
238    }
239
240    // ========================================================================
241    // Task Group 1 Tests: WebhookHandler and Error Types
242    // ========================================================================
243
244    #[test]
245    fn test_no_handler_for_topic_error_message_formatting() {
246        let error = WebhookError::NoHandlerForTopic {
247            topic: "orders/create".to_string(),
248        };
249        let message = error.to_string();
250        assert!(message.contains("No handler registered"));
251        assert!(message.contains("orders/create"));
252    }
253
254    #[test]
255    fn test_payload_parse_error_message_formatting() {
256        let error = WebhookError::PayloadParseError {
257            message: "expected value at line 1 column 1".to_string(),
258        };
259        let message = error.to_string();
260        assert!(message.contains("Failed to parse webhook payload"));
261        assert!(message.contains("expected value at line 1 column 1"));
262    }
263}