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