ironshield_types/
response.rs1use serde::{Deserialize, Serialize};
2use crate::IronShieldChallenge;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct IronShieldChallengeResponse {
10 pub solved_challenge: IronShieldChallenge,
11 pub solution: i64,
12}
13
14impl IronShieldChallengeResponse {
15 pub fn new(
25 solved_challenge: IronShieldChallenge,
26 solution: i64
27 ) -> Self {
28 Self {
29 solved_challenge,
30 solution,
31 }
32 }
33
34 pub fn concat_struct(&self) -> String {
40 format!(
41 "{}|{}",
42 self.solved_challenge.concat_struct(),
43 self.solution
44 )
45 }
46
47 pub fn from_concat_struct(concat_string: &str) -> Result<Self, String> {
62 let last_pipe_pos = concat_string.rfind('|')
64 .ok_or("Expected at least one '|' separator")?;
65
66 let challenge_part = &concat_string[..last_pipe_pos];
67 let solution_part = &concat_string[last_pipe_pos + 1..];
68
69 let solved_challenge = IronShieldChallenge::from_concat_struct(challenge_part)?;
70 let solution = solution_part.parse::<i64>()
71 .map_err(|_| "Failed to parse solution as i64")?;
72
73 Ok(Self {
74 solved_challenge,
75 solution,
76 })
77 }
78
79 pub fn to_base64url_header(&self) -> String {
97 crate::serde_utils::concat_struct_base64url_encode(&self.concat_struct())
98 }
99
100 pub fn from_base64url_header(encoded_header: &str) -> Result<Self, String> {
124 let concat_str: String = crate::serde_utils::concat_struct_base64url_decode(encoded_header.to_string())?;
126
127 Self::from_concat_struct(&concat_str)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::SigningKey;
136
137 #[test]
138 fn test_response_base64url_header_encoding_roundtrip() {
139 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
141 let challenge = IronShieldChallenge::new(
142 "test_website".to_string(),
143 100_000,
144 dummy_key,
145 [0x34; 32],
146 );
147 let response: IronShieldChallengeResponse = IronShieldChallengeResponse::new(challenge, 12345);
148
149 let encoded: String = response.to_base64url_header();
151 let decoded: IronShieldChallengeResponse = IronShieldChallengeResponse::from_base64url_header(&encoded).unwrap();
152
153 assert_eq!(response.solved_challenge.random_nonce, decoded.solved_challenge.random_nonce);
155 assert_eq!(response.solved_challenge.website_id, decoded.solved_challenge.website_id);
156 assert_eq!(response.solved_challenge.challenge_param, decoded.solved_challenge.challenge_param);
157 assert_eq!(response.solved_challenge.public_key, decoded.solved_challenge.public_key);
158 assert_eq!(response.solved_challenge.challenge_signature, decoded.solved_challenge.challenge_signature);
159 assert_eq!(response.solution, decoded.solution);
160 }
161
162 #[test]
163 fn test_response_base64url_header_invalid_data() {
164 let result: Result<IronShieldChallengeResponse, String> = IronShieldChallengeResponse::from_base64url_header("invalid-base64!");
166 assert!(result.is_err());
167 assert!(result.unwrap_err().contains("Base64 decode error"));
168
169 use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
171 let invalid_format: String = URL_SAFE_NO_PAD.encode(b"only_one_part");
172 let result: Result<IronShieldChallengeResponse, String> = IronShieldChallengeResponse::from_base64url_header(&invalid_format);
173 assert!(result.is_err());
174 assert!(result.unwrap_err().contains("Expected at least one '|' separator"));
175 }
176
177 #[test]
178 fn test_concat_struct() {
179 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
180 let challenge = IronShieldChallenge::new(
181 "test_website".to_string(),
182 100_000,
183 dummy_key,
184 [0x34; 32],
185 );
186 let response = IronShieldChallengeResponse::new(challenge.clone(), 42);
187 let concat = response.concat_struct();
188 let expected = format!("{}|{}", challenge.concat_struct(), 42);
189 assert_eq!(concat, expected);
190 }
191
192 #[test]
193 fn test_from_concat_struct() {
194 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
195 let challenge = IronShieldChallenge::new(
196 "test_website".to_string(),
197 100_000,
198 dummy_key,
199 [0x34; 32],
200 );
201 let concat = format!("{}|{}", challenge.concat_struct(), 42);
202 let response = IronShieldChallengeResponse::from_concat_struct(&concat).unwrap();
203 assert_eq!(response.solved_challenge.website_id, challenge.website_id);
204 assert_eq!(response.solved_challenge.challenge_param, challenge.challenge_param);
205 assert_eq!(response.solution, 42);
206 }
207
208 #[test]
209 fn test_from_concat_struct_edge_cases() {
210 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
211 let challenge = IronShieldChallenge::new(
212 "test_website".to_string(),
213 1,
214 dummy_key,
215 [0x00; 32],
216 );
217
218 let concat = format!("{}|{}", challenge.concat_struct(), -1);
220 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
221 assert!(result.is_ok());
222 let parsed = result.unwrap();
223 assert_eq!(parsed.solution, -1);
224
225 let concat = format!("{}|{}", challenge.concat_struct(), 0);
227 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
228 assert!(result.is_ok());
229 let parsed = result.unwrap();
230 assert_eq!(parsed.solution, 0);
231
232 let concat = format!("{}|{}", challenge.concat_struct(), i64::MAX);
234 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
235 assert!(result.is_ok());
236 let parsed = result.unwrap();
237 assert_eq!(parsed.solution, i64::MAX);
238 }
239
240 #[test]
241 fn test_from_concat_struct_error_cases() {
242 let result = IronShieldChallengeResponse::from_concat_struct("no_pipe_separator");
244 assert!(result.is_err());
245 assert!(result.unwrap_err().contains("Expected at least one '|' separator"));
246
247 let dummy_key = SigningKey::from_bytes(&[0u8; 32]);
249 let challenge = IronShieldChallenge::new(
250 "test_website".to_string(),
251 1,
252 dummy_key,
253 [0x00; 32],
254 );
255 let concat = format!("{}|{}", challenge.concat_struct(), "not_a_number");
256 let result = IronShieldChallengeResponse::from_concat_struct(&concat);
257 assert!(result.is_err());
258 assert!(result.unwrap_err().contains("Failed to parse solution as i64"));
259 }
260}