openzeppelin_relayer/models/notification/
request.rs1use crate::models::{notification::Notification, ApiError, NotificationType, SecretString};
13use serde::{Deserialize, Serialize};
14use utoipa::ToSchema;
15
16#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
18#[serde(deny_unknown_fields)]
19pub struct NotificationCreateRequest {
20 #[schema(nullable = false)]
21 pub id: Option<String>,
22 #[schema(nullable = false)]
23 pub r#type: Option<NotificationType>,
24 pub url: String,
25 #[schema(nullable = false)]
27 pub signing_key: Option<String>,
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
32#[serde(deny_unknown_fields)]
33pub struct NotificationUpdateRequest {
34 #[schema(nullable = false)]
35 pub r#type: Option<NotificationType>,
36 #[schema(nullable = false)]
37 pub url: Option<String>,
38 pub signing_key: Option<String>,
43}
44
45impl TryFrom<NotificationCreateRequest> for Notification {
46 type Error = ApiError;
47
48 fn try_from(request: NotificationCreateRequest) -> Result<Self, Self::Error> {
49 let signing_key = request.signing_key.map(|s| SecretString::new(&s));
50 let id = request
51 .id
52 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
53 let notification_type = request.r#type.unwrap_or(NotificationType::Webhook);
54
55 let notification = Notification::new(id, notification_type, request.url, signing_key);
56
57 notification.validate().map_err(ApiError::from)?;
59
60 Ok(notification)
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use crate::models::NotificationRepoModel;
67
68 use super::*;
69
70 #[test]
71 fn test_valid_create_request_conversion() {
72 let request = NotificationCreateRequest {
73 id: Some("test-notification".to_string()),
74 r#type: Some(NotificationType::Webhook),
75 url: "https://example.com/webhook".to_string(),
76 signing_key: Some("a".repeat(32)), };
78
79 let result = Notification::try_from(request);
80 assert!(result.is_ok());
81
82 let notification = result.unwrap();
83 assert_eq!(notification.id, "test-notification");
84 assert_eq!(notification.notification_type, NotificationType::Webhook);
85 assert_eq!(notification.url, "https://example.com/webhook");
86 assert!(notification.signing_key.is_some());
87 }
88
89 #[test]
90 fn test_invalid_create_request_conversion() {
91 let request = NotificationCreateRequest {
92 id: Some("invalid@id".to_string()), r#type: Some(NotificationType::Webhook),
94 url: "https://example.com/webhook".to_string(),
95 signing_key: None,
96 };
97
98 let result = Notification::try_from(request);
99 assert!(result.is_err());
100
101 if let Err(ApiError::BadRequest(msg)) = result {
102 assert!(msg.contains("ID must contain only letters, numbers, dashes and underscores"));
103 } else {
104 panic!("Expected BadRequest error");
105 }
106 }
107
108 #[test]
109 fn test_signing_key_too_short() {
110 let request = NotificationCreateRequest {
111 id: Some("test-notification".to_string()),
112 r#type: Some(NotificationType::Webhook),
113 url: "https://example.com/webhook".to_string(),
114 signing_key: Some("short".to_string()), };
116
117 let result = Notification::try_from(request);
118 assert!(result.is_err());
119
120 if let Err(ApiError::BadRequest(msg)) = result {
121 assert!(msg.contains("Signing key must be at least"));
122 } else {
123 panic!("Expected BadRequest error");
124 }
125 }
126
127 #[test]
128 fn test_invalid_url() {
129 let request = NotificationCreateRequest {
130 id: Some("test-notification".to_string()),
131 r#type: Some(NotificationType::Webhook),
132 url: "not-a-url".to_string(),
133 signing_key: None,
134 };
135
136 let result = Notification::try_from(request);
137 assert!(result.is_err());
138
139 if let Err(ApiError::BadRequest(msg)) = result {
140 assert!(msg.contains("Invalid URL format"));
141 } else {
142 panic!("Expected BadRequest error");
143 }
144 }
145
146 #[test]
147 fn test_update_request_validation_domain_first() {
148 let existing_core = Notification::new(
150 "test-id".to_string(),
151 NotificationType::Webhook,
152 "https://example.com/webhook".to_string(),
153 Some(SecretString::new("existing-key")),
154 );
155
156 let update_request = NotificationUpdateRequest {
157 r#type: None,
158 url: Some("https://new-example.com/webhook".to_string()),
159 signing_key: Some("a".repeat(32)), };
161
162 let result = existing_core.apply_update(&update_request);
163 assert!(result.is_ok());
164
165 let updated = result.unwrap();
166 assert_eq!(updated.id, "test-id"); assert_eq!(updated.url, "https://new-example.com/webhook"); assert!(updated.signing_key.is_some()); }
170
171 #[test]
172 fn test_update_request_invalid_url_domain_first() {
173 let existing_core = Notification::new(
175 "test-id".to_string(),
176 NotificationType::Webhook,
177 "https://example.com/webhook".to_string(),
178 None,
179 );
180
181 let update_request = NotificationUpdateRequest {
182 r#type: None,
183 url: Some("not-a-url".to_string()), signing_key: None,
185 };
186
187 let result = existing_core.apply_update(&update_request);
188 assert!(result.is_err());
189
190 let api_error: ApiError = result.unwrap_err().into();
192 if let ApiError::BadRequest(msg) = api_error {
193 assert!(msg.contains("Invalid URL format"));
194 } else {
195 panic!("Expected BadRequest error");
196 }
197 }
198
199 #[test]
200 fn test_notification_to_repo_model() {
201 let notification = Notification::new(
202 "test-id".to_string(),
203 NotificationType::Webhook,
204 "https://example.com/webhook".to_string(),
205 Some(SecretString::new("test-key")),
206 );
207
208 let repo_model = NotificationRepoModel::from(notification);
209 assert_eq!(repo_model.id, "test-id");
210 assert_eq!(repo_model.notification_type, NotificationType::Webhook);
211 assert_eq!(repo_model.url, "https://example.com/webhook");
212 assert!(repo_model.signing_key.is_some());
213 }
214}