uptrakit_web_api_types/
profile.rs1use serde::{Deserialize, Serialize};
2use uptrakit_shared_types::SecretString;
3
4use crate::validation::{Validate, ValidationError};
5
6#[derive(Serialize, Deserialize)]
7#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
8pub struct UpdateProfileRequest {
9 #[cfg_attr(feature = "openapi", schema(example = "Jane"))]
10 pub first_name: String,
11 #[cfg_attr(feature = "openapi", schema(example = "Doe"))]
12 pub last_name: String,
13}
14
15impl Validate for UpdateProfileRequest {
16 fn validate(&self) -> Result<(), ValidationError> {
17 if self.first_name.is_empty() {
18 return Err(ValidationError {
19 field: "first_name",
20 message: "first_name must not be empty".to_string(),
21 });
22 }
23 if self.first_name.len() > 100 {
24 return Err(ValidationError {
25 field: "first_name",
26 message: "first_name must not exceed 100 characters".to_string(),
27 });
28 }
29 if self.last_name.len() > 100 {
30 return Err(ValidationError {
31 field: "last_name",
32 message: "last_name must not exceed 100 characters".to_string(),
33 });
34 }
35 Ok(())
36 }
37}
38
39#[derive(Serialize, Deserialize)]
40#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
41pub struct InitiateEmailChangeRequest {
42 #[cfg_attr(feature = "openapi", schema(example = "currentpassword123"))]
43 pub current_password: SecretString,
44 #[cfg_attr(feature = "openapi", schema(example = "newemail@example.com"))]
45 pub new_email: String,
46}
47
48impl Validate for InitiateEmailChangeRequest {
49 fn validate(&self) -> Result<(), ValidationError> {
50 if !self.new_email.contains('@') {
51 return Err(ValidationError {
52 field: "new_email",
53 message: "new_email must contain '@'".to_string(),
54 });
55 }
56 if self.new_email.len() > 254 {
57 return Err(ValidationError {
58 field: "new_email",
59 message: "new_email must not exceed 254 characters".to_string(),
60 });
61 }
62 Ok(())
63 }
64}
65
66#[derive(Serialize, Deserialize)]
67#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
68pub struct ChangePasswordRequest {
69 #[cfg_attr(feature = "openapi", schema(example = "currentpassword123"))]
70 pub current_password: SecretString,
71 #[cfg_attr(
72 feature = "openapi",
73 schema(example = "newpassword123", min_length = 8)
74 )]
75 pub new_password: SecretString,
76}
77
78impl Validate for ChangePasswordRequest {
79 fn validate(&self) -> Result<(), ValidationError> {
80 let len = self.new_password.expose_secret().len();
81 if len < 8 {
82 return Err(ValidationError {
83 field: "new_password",
84 message: "new_password must be at least 8 characters".to_string(),
85 });
86 }
87 if len > 128 {
88 return Err(ValidationError {
89 field: "new_password",
90 message: "new_password must not exceed 128 characters".to_string(),
91 });
92 }
93 Ok(())
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 #![expect(
100 clippy::assertions_on_result_states,
101 reason = "test assertions — is_ok/is_err provides readable failure messages"
102 )]
103 use super::*;
104
105 fn valid_update_profile() -> UpdateProfileRequest {
108 UpdateProfileRequest {
109 first_name: "Jane".to_string(),
110 last_name: "Doe".to_string(),
111 }
112 }
113
114 #[test]
115 fn update_profile_valid() {
116 assert!(valid_update_profile().validate().is_ok());
117 }
118
119 #[test]
120 fn update_profile_empty_first_name_fails() {
121 let mut req = valid_update_profile();
122 req.first_name = String::new();
123 let err = req.validate().unwrap_err();
124 assert_eq!(err.field, "first_name");
125 }
126
127 #[test]
128 fn update_profile_first_name_too_long() {
129 let mut req = valid_update_profile();
130 req.first_name = "a".repeat(101);
131 let err = req.validate().unwrap_err();
132 assert_eq!(err.field, "first_name");
133 }
134
135 #[test]
136 fn update_profile_last_name_too_long() {
137 let mut req = valid_update_profile();
138 req.last_name = "a".repeat(101);
139 let err = req.validate().unwrap_err();
140 assert_eq!(err.field, "last_name");
141 }
142
143 #[test]
144 fn update_profile_empty_last_name_ok() {
145 let req = UpdateProfileRequest {
146 first_name: "Jane".to_string(),
147 last_name: String::new(),
148 };
149 assert!(req.validate().is_ok());
150 }
151
152 fn valid_email_change() -> InitiateEmailChangeRequest {
155 InitiateEmailChangeRequest {
156 current_password: SecretString::new("currentpassword123"),
157 new_email: "newemail@example.com".to_string(),
158 }
159 }
160
161 #[test]
162 fn initiate_email_change_valid() {
163 assert!(valid_email_change().validate().is_ok());
164 }
165
166 #[test]
167 fn initiate_email_change_missing_at_fails() {
168 let mut req = valid_email_change();
169 req.new_email = "notanemail".to_string();
170 let err = req.validate().unwrap_err();
171 assert_eq!(err.field, "new_email");
172 }
173
174 #[test]
175 fn initiate_email_change_email_too_long() {
176 let mut req = valid_email_change();
177 req.new_email = format!("{}@x.com", "a".repeat(250));
178 let err = req.validate().unwrap_err();
179 assert_eq!(err.field, "new_email");
180 }
181
182 fn valid_change_password() -> ChangePasswordRequest {
185 ChangePasswordRequest {
186 current_password: SecretString::new("oldpassword123"),
187 new_password: SecretString::new("newpassword123"),
188 }
189 }
190
191 #[test]
192 fn change_password_valid() {
193 assert!(valid_change_password().validate().is_ok());
194 }
195
196 #[test]
197 fn change_password_too_short_fails() {
198 let mut req = valid_change_password();
199 req.new_password = SecretString::new("short");
200 let err = req.validate().unwrap_err();
201 assert_eq!(err.field, "new_password");
202 }
203
204 #[test]
205 fn change_password_too_long_fails() {
206 let mut req = valid_change_password();
207 req.new_password = SecretString::new("a".repeat(129));
208 let err = req.validate().unwrap_err();
209 assert_eq!(err.field, "new_password");
210 }
211
212 #[test]
213 fn change_password_exactly_8_chars_ok() {
214 let req = ChangePasswordRequest {
215 current_password: SecretString::new("oldpass"),
216 new_password: SecretString::new("12345678"),
217 };
218 assert!(req.validate().is_ok());
219 }
220
221 #[test]
222 fn change_password_exactly_128_chars_ok() {
223 let req = ChangePasswordRequest {
224 current_password: SecretString::new("oldpass"),
225 new_password: SecretString::new("a".repeat(128)),
226 };
227 assert!(req.validate().is_ok());
228 }
229}