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    use super::*;
100
101    // ── UpdateProfileRequest ─────────────────────────────────────────────────
102
103    fn valid_update_profile() -> UpdateProfileRequest {
104        UpdateProfileRequest {
105            first_name: "Jane".to_string(),
106            last_name: "Doe".to_string(),
107        }
108    }
109
110    #[test]
111    fn update_profile_valid() {
112        assert!(valid_update_profile().validate().is_ok());
113    }
114
115    #[test]
116    fn update_profile_empty_first_name_fails() {
117        let mut req = valid_update_profile();
118        req.first_name = String::new();
119        let err = req.validate().unwrap_err();
120        assert_eq!(err.field, "first_name");
121    }
122
123    #[test]
124    fn update_profile_first_name_too_long() {
125        let mut req = valid_update_profile();
126        req.first_name = "a".repeat(101);
127        let err = req.validate().unwrap_err();
128        assert_eq!(err.field, "first_name");
129    }
130
131    #[test]
132    fn update_profile_last_name_too_long() {
133        let mut req = valid_update_profile();
134        req.last_name = "a".repeat(101);
135        let err = req.validate().unwrap_err();
136        assert_eq!(err.field, "last_name");
137    }
138
139    #[test]
140    fn update_profile_empty_last_name_ok() {
141        let req = UpdateProfileRequest {
142            first_name: "Jane".to_string(),
143            last_name: String::new(),
144        };
145        assert!(req.validate().is_ok());
146    }
147
148    // ── InitiateEmailChangeRequest ───────────────────────────────────────────
149
150    fn valid_email_change() -> InitiateEmailChangeRequest {
151        InitiateEmailChangeRequest {
152            current_password: SecretString::new("currentpassword123"),
153            new_email: "newemail@example.com".to_string(),
154        }
155    }
156
157    #[test]
158    fn initiate_email_change_valid() {
159        assert!(valid_email_change().validate().is_ok());
160    }
161
162    #[test]
163    fn initiate_email_change_missing_at_fails() {
164        let mut req = valid_email_change();
165        req.new_email = "notanemail".to_string();
166        let err = req.validate().unwrap_err();
167        assert_eq!(err.field, "new_email");
168    }
169
170    #[test]
171    fn initiate_email_change_email_too_long() {
172        let mut req = valid_email_change();
173        req.new_email = format!("{}@x.com", "a".repeat(250));
174        let err = req.validate().unwrap_err();
175        assert_eq!(err.field, "new_email");
176    }
177
178    // ── ChangePasswordRequest ────────────────────────────────────────────────
179
180    fn valid_change_password() -> ChangePasswordRequest {
181        ChangePasswordRequest {
182            current_password: SecretString::new("oldpassword123"),
183            new_password: SecretString::new("newpassword123"),
184        }
185    }
186
187    #[test]
188    fn change_password_valid() {
189        assert!(valid_change_password().validate().is_ok());
190    }
191
192    #[test]
193    fn change_password_too_short_fails() {
194        let mut req = valid_change_password();
195        req.new_password = SecretString::new("short");
196        let err = req.validate().unwrap_err();
197        assert_eq!(err.field, "new_password");
198    }
199
200    #[test]
201    fn change_password_too_long_fails() {
202        let mut req = valid_change_password();
203        req.new_password = SecretString::new("a".repeat(129));
204        let err = req.validate().unwrap_err();
205        assert_eq!(err.field, "new_password");
206    }
207
208    #[test]
209    fn change_password_exactly_8_chars_ok() {
210        let req = ChangePasswordRequest {
211            current_password: SecretString::new("oldpass"),
212            new_password: SecretString::new("12345678"),
213        };
214        assert!(req.validate().is_ok());
215    }
216
217    #[test]
218    fn change_password_exactly_128_chars_ok() {
219        let req = ChangePasswordRequest {
220            current_password: SecretString::new("oldpass"),
221            new_password: SecretString::new("a".repeat(128)),
222        };
223        assert!(req.validate().is_ok());
224    }
225}