1use sms_core::{
14 Headers, HttpStatus, InboundMessage, InboundRegistry, WebhookError, WebhookResponse,
15};
16
17#[derive(Clone)]
27pub struct WebhookProcessor {
28 registry: InboundRegistry,
29}
30
31impl WebhookProcessor {
32 pub fn new(registry: InboundRegistry) -> Self {
34 Self { registry }
35 }
36
37 pub fn process_webhook(
41 &self,
42 provider: &str,
43 headers: Headers,
44 body: &[u8],
45 ) -> WebhookResponse {
46 match self.process_webhook_internal(provider, headers, body) {
47 Ok(message) => WebhookResponse::success(message),
48 Err(e) => self.error_to_response(e),
49 }
50 }
51
52 fn process_webhook_internal(
53 &self,
54 provider: &str,
55 headers: Headers,
56 body: &[u8],
57 ) -> Result<InboundMessage, WebhookError> {
58 let hook = self
59 .registry
60 .get(provider)
61 .ok_or_else(|| WebhookError::ProviderNotFound(provider.to_string()))?;
62
63 hook.verify(&headers, body)
64 .map_err(|e| WebhookError::VerificationFailed(e.to_string()))?;
65
66 hook.parse_inbound(&headers, body)
67 .map_err(|e| WebhookError::ParseError(e.to_string()))
68 }
69
70 fn error_to_response(&self, error: WebhookError) -> WebhookResponse {
71 match error {
72 WebhookError::ProviderNotFound(_) => {
73 WebhookResponse::error(HttpStatus::NotFound, "unknown provider")
74 }
75 WebhookError::VerificationFailed(msg) => WebhookResponse::error(
76 HttpStatus::Unauthorized,
77 &format!("verification failed: {}", msg),
78 ),
79 WebhookError::ParseError(msg) => {
80 WebhookResponse::error(HttpStatus::BadRequest, &format!("parse error: {}", msg))
81 }
82 WebhookError::SmsError(e) => WebhookResponse::error(
83 HttpStatus::InternalServerError,
84 &format!("SMS error: {}", e),
85 ),
86 }
87 }
88}
89
90pub trait HeaderConverter {
93 type HeaderType;
95
96 fn to_generic_headers(headers: &Self::HeaderType) -> Headers;
98}
99
100pub trait ResponseConverter {
103 type ResponseType;
105
106 fn from_webhook_response(response: WebhookResponse) -> Self::ResponseType;
108}
109
110#[macro_export]
112macro_rules! implement_webhook_handler {
113 ($framework:ident, $handler_name:ident, $request_type:ty, $response_type:ty) => {
114 pub async fn $handler_name(
115 processor: &WebhookProcessor,
116 provider: String,
117 headers: impl Into<Headers>,
118 body: &[u8],
119 ) -> $response_type {
120 let response = processor.process_webhook(&provider, headers.into(), body);
121 <$response_type as ResponseConverter>::from_webhook_response(response)
122 }
123 };
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use sms_core::{InboundMessage, InboundRegistry, InboundWebhook, SmsError};
130
131 struct FakeProvider;
133
134 impl InboundWebhook for FakeProvider {
135 fn provider(&self) -> &'static str {
136 "fake"
137 }
138
139 fn parse_inbound(&self, _headers: &Headers, body: &[u8]) -> Result<InboundMessage, SmsError> {
140 let text = String::from_utf8(body.to_vec())
141 .map_err(|e| SmsError::Invalid(e.to_string()))?;
142 Ok(InboundMessage {
143 id: Some("fake-id".into()),
144 from: "+1111".into(),
145 to: "+2222".into(),
146 text,
147 timestamp: None,
148 provider: "fake",
149 raw: serde_json::json!({}),
150 })
151 }
152 }
153
154 struct FailVerifyProvider;
156
157 impl InboundWebhook for FailVerifyProvider {
158 fn provider(&self) -> &'static str {
159 "fail-verify"
160 }
161
162 fn parse_inbound(&self, _headers: &Headers, _body: &[u8]) -> Result<InboundMessage, SmsError> {
163 unreachable!("should not be called if verify fails");
164 }
165
166 fn verify(&self, _headers: &Headers, _body: &[u8]) -> Result<(), SmsError> {
167 Err(SmsError::Auth("bad signature".into()))
168 }
169 }
170
171 struct FailParseProvider;
173
174 impl InboundWebhook for FailParseProvider {
175 fn provider(&self) -> &'static str {
176 "fail-parse"
177 }
178
179 fn parse_inbound(&self, _headers: &Headers, _body: &[u8]) -> Result<InboundMessage, SmsError> {
180 Err(SmsError::Invalid("cannot parse this".into()))
181 }
182 }
183
184 fn processor_with(providers: Vec<std::sync::Arc<dyn InboundWebhook>>) -> WebhookProcessor {
185 let mut registry = InboundRegistry::new();
186 for p in providers {
187 registry = registry.with(p);
188 }
189 WebhookProcessor::new(registry)
190 }
191
192 #[test]
193 fn unknown_provider_returns_404() {
194 let processor = processor_with(vec![]);
195 let response = processor.process_webhook("unknown", vec![], b"test");
196 assert_eq!(response.status.as_u16(), 404);
197 assert!(response.body.contains("unknown provider"));
198 }
199
200 #[test]
201 fn known_provider_returns_200() {
202 let processor = processor_with(vec![std::sync::Arc::new(FakeProvider)]);
203 let response = processor.process_webhook("fake", vec![], b"hello");
204 assert_eq!(response.status.as_u16(), 200);
205 assert!(response.body.contains("fake-id"));
206 assert!(response.body.contains("hello"));
207 }
208
209 #[test]
210 fn verification_failure_returns_401() {
211 let processor = processor_with(vec![std::sync::Arc::new(FailVerifyProvider)]);
212 let response = processor.process_webhook("fail-verify", vec![], b"data");
213 assert_eq!(response.status.as_u16(), 401);
214 assert!(response.body.contains("verification failed"));
215 }
216
217 #[test]
218 fn parse_failure_returns_400() {
219 let processor = processor_with(vec![std::sync::Arc::new(FailParseProvider)]);
220 let response = processor.process_webhook("fail-parse", vec![], b"data");
221 assert_eq!(response.status.as_u16(), 400);
222 assert!(response.body.contains("parse error"));
223 }
224
225 #[test]
226 fn content_type_is_json() {
227 let processor = processor_with(vec![std::sync::Arc::new(FakeProvider)]);
228 let response = processor.process_webhook("fake", vec![], b"msg");
229 assert_eq!(response.content_type, "application/json");
230 }
231
232 #[test]
233 fn processor_passes_headers_to_provider() {
234 let processor = processor_with(vec![std::sync::Arc::new(FakeProvider)]);
237 let headers = vec![
238 ("X-Custom".to_string(), "value".to_string()),
239 ("Content-Type".to_string(), "application/json".to_string()),
240 ];
241 let response = processor.process_webhook("fake", headers, b"body");
242 assert_eq!(response.status.as_u16(), 200);
243 }
244}