Skip to main content

uptrakit_web_api_types/
profile.rs

1use 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    // ── UpdateProfileRequest ─────────────────────────────────────────────────
106
107    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    // ── InitiateEmailChangeRequest ───────────────────────────────────────────
153
154    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    // ── ChangePasswordRequest ────────────────────────────────────────────────
183
184    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}