ironshield_types/
response.rs1use serde::{
2 Deserialize,
3 Serialize
4};
5
6use crate::IronShieldChallenge;
7
8#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct IronShieldChallengeResponse {
15 pub solved_challenge: IronShieldChallenge,
16 pub solution: i64,
17}
18
19impl IronShieldChallengeResponse {
20 pub fn new(
30 solved_challenge: IronShieldChallenge,
31 solution: i64
32 ) -> Self {
33 Self {
34 solved_challenge,
35 solution,
36 }
37 }
38
39 pub fn concat_struct(&self) -> String {
45 format!(
46 "{}|{}",
47 self.solved_challenge.concat_struct(),
48 self.solution
49 )
50 }
51
52 pub fn from_concat_struct(concat_string: &str) -> Result<Self, String> {
67 let last_pipe_pos = concat_string.rfind('|')
69 .ok_or("Expected at least one '|' separator")?;
70
71 let challenge_part = &concat_string[..last_pipe_pos];
72 let solution_part = &concat_string[last_pipe_pos + 1..];
73
74 let solved_challenge = IronShieldChallenge::from_concat_struct(challenge_part)?;
75 let solution = solution_part.parse::<i64>()
76 .map_err(|_| "Failed to parse solution as i64")?;
77
78 Ok(Self {
79 solved_challenge,
80 solution,
81 })
82 }
83
84 pub fn to_base64url_header(&self) -> String {
102 crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
103 }
104
105 pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
129 let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
131
132 Self::from_concat_struct(&concat_str)
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::SigningKey;
141
142 #[test]
143 fn test_response_base64url_header_encoding_roundtrip() {
144 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
146 let challenge = IronShieldChallenge::new(
147 "test_website".to_string(),
148 100_000,
149 dummy_key,
150 [0x34; 32],
151 );
152 let response: IronShieldChallengeResponse = IronShieldChallengeResponse::new(challenge, 12345);
153
154 let encoded: String = response.to_base64url_header();
156 let decoded: IronShieldChallengeResponse = IronShieldChallengeResponse::from_base64url_header(&encoded).unwrap();
157
158 assert_eq!(response.solved_challenge.random_nonce, decoded.solved_challenge.random_nonce);
160 assert_eq!(response.solved_challenge.website_id, decoded.solved_challenge.website_id);
161 assert_eq!(response.solved_challenge.challenge_param, decoded.solved_challenge.challenge_param);
162 assert_eq!(response.solved_challenge.public_key, decoded.solved_challenge.public_key);
163 assert_eq!(response.solved_challenge.challenge_signature, decoded.solved_challenge.challenge_signature);
164 assert_eq!(response.solution, decoded.solution);
165 }
166
167 #[test]
168 fn test_response_base64url_header_invalid_data() {
169 let result: Result<IronShieldChallengeResponse, String> = IronShieldChallengeResponse::from_base64url_header("invalid-base64!");
171 assert!(result.is_err());
172 assert!(result.unwrap_err().contains("Base64 decode error"));
173
174 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
176 let invalid_format: String = URL_SAFE_NO_PAD.encode(b"only_one_part");
177 let result: Result<IronShieldChallengeResponse, String> = IronShieldChallengeResponse::from_base64url_header(&invalid_format);
178 assert!(result.is_err());
179 assert!(result.unwrap_err().contains("Expected at least one '|' separator"));
180 }
181
182 #[test]
183 fn test_concat_struct() {
184 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
185 let challenge = IronShieldChallenge::new(
186 "test_website".to_string(),
187 100_000,
188 dummy_key,
189 [0x34; 32],
190 );
191 let response = IronShieldChallengeResponse::new(challenge.clone(), 42);
192 let concat = response.concat_struct();
193 let expected = format!("{}|{}", challenge.concat_struct(), 42);
194 assert_eq!(concat, expected);
195 }
196
197 #[test]
198 fn test_from_concat_struct() {
199 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
200 let challenge = IronShieldChallenge::new(
201 "test_website".to_string(),
202 100_000,
203 dummy_key,
204 [0x34; 32],
205 );
206 let concat = format!("{}|{}", challenge.concat_struct(), 42);
207 let response = IronShieldChallengeResponse::from_concat_struct(&concat).unwrap();
208 assert_eq!(response.solved_challenge.website_id, challenge.website_id);
209 assert_eq!(response.solved_challenge.challenge_param, challenge.challenge_param);
210 assert_eq!(response.solution, 42);
211 }
212
213 #[test]
214 fn test_from_concat_struct_edge_cases() {
215 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
216 let challenge = IronShieldChallenge::new(
217 "test_website".to_string(),
218 1,
219 dummy_key,
220 [0x00; 32],
221 );
222
223 let concat = format!("{}|{}", challenge.concat_struct(), -1);
225 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
226 assert!(result.is_ok());
227 let parsed = result.unwrap();
228 assert_eq!(parsed.solution, -1);
229
230 let concat = format!("{}|{}", challenge.concat_struct(), 0);
232 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
233 assert!(result.is_ok());
234 let parsed = result.unwrap();
235 assert_eq!(parsed.solution, 0);
236
237 let concat = format!("{}|{}", challenge.concat_struct(), i64::MAX);
239 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
240 assert!(result.is_ok());
241 let parsed = result.unwrap();
242 assert_eq!(parsed.solution, i64::MAX);
243 }
244
245 #[test]
246 fn test_from_concat_struct_error_cases() {
247 let result = IronShieldChallengeResponse::from_concat_struct("no_pipe_separator");
249 assert!(result.is_err());
250 assert!(result.unwrap_err().contains("Expected at least one '|' separator"));
251
252 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
254 let challenge = IronShieldChallenge::new(
255 "test_website".to_string(),
256 1,
257 dummy_key,
258 [0x00; 32],
259 );
260 let concat = format!("{}|{}", challenge.concat_struct(), "not_a_number");
261 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
262 assert!(result.is_err());
263 assert!(result.unwrap_err().contains("Failed to parse solution as i64"));
264 }
265}