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}