Skip to content

Commit 20e9c13

Browse files
authored
Add base64 tests (#3220)
Based on feedback from #3214
1 parent 91cd633 commit 20e9c13

File tree

1 file changed

+212
-1
lines changed

1 file changed

+212
-1
lines changed

sdk/core/typespec_client_core/src/base64.rs

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
// cspell:ignore Hdvcmxk
4+
// cspell:ignore Hdvcmxk unpadded
55

66
//! Base64 encoding and decoding functions.
77
@@ -332,3 +332,214 @@ pub mod option {
332332
<Option<String>>::serialize(&encoded, serializer)
333333
}
334334
}
335+
336+
#[cfg(test)]
337+
mod tests {
338+
use super::{
339+
decode, decode_url_safe, deserialize, deserialize_url_safe, encode, encode_url_safe,
340+
option, serialize, serialize_url_safe,
341+
};
342+
use serde::{Deserialize, Serialize};
343+
344+
#[test]
345+
fn standard_encode() {
346+
assert_eq!(encode(b"Hello, world!"), "SGVsbG8sIHdvcmxkIQ==");
347+
assert_eq!(encode(b""), "");
348+
assert_eq!(encode(b"f"), "Zg==");
349+
assert_eq!(encode(b"fo"), "Zm8=");
350+
assert_eq!(encode(b"foo"), "Zm9v");
351+
}
352+
353+
#[test]
354+
fn standard_decode() {
355+
assert_eq!(decode("SGVsbG8sIHdvcmxkIQ==").unwrap(), b"Hello, world!");
356+
assert_eq!(decode("").unwrap(), b"");
357+
assert_eq!(decode("Zg==").unwrap(), b"f");
358+
assert_eq!(decode("Zm8=").unwrap(), b"fo");
359+
assert_eq!(decode("Zm9v").unwrap(), b"foo");
360+
}
361+
362+
#[test]
363+
fn url_safe_encode() {
364+
assert_eq!(encode_url_safe(b"Hello, world!"), "SGVsbG8sIHdvcmxkIQ");
365+
assert_eq!(encode_url_safe(b""), "");
366+
assert_eq!(encode_url_safe(b"f"), "Zg");
367+
assert_eq!(encode_url_safe(b"fo"), "Zm8");
368+
assert_eq!(encode_url_safe(b"foo"), "Zm9v");
369+
370+
// Verify no padding in base64url encoding
371+
assert!(!encode_url_safe(b"f").contains('='));
372+
assert!(!encode_url_safe(b"fo").contains('='));
373+
assert!(!encode_url_safe(b"foo").contains('='));
374+
}
375+
376+
#[test]
377+
fn url_safe_decode() {
378+
assert_eq!(
379+
decode_url_safe("SGVsbG8sIHdvcmxkIQ").unwrap(),
380+
b"Hello, world!"
381+
);
382+
assert_eq!(decode_url_safe("").unwrap(), b"");
383+
assert_eq!(decode_url_safe("Zg").unwrap(), b"f");
384+
assert_eq!(decode_url_safe("Zm8").unwrap(), b"fo");
385+
assert_eq!(decode_url_safe("Zm9v").unwrap(), b"foo");
386+
}
387+
388+
#[test]
389+
fn roundtrip_standard() {
390+
let data = b"The quick brown fox jumps over the lazy dog";
391+
assert_eq!(decode(encode(data)).unwrap(), data);
392+
}
393+
394+
#[test]
395+
fn roundtrip_url_safe() {
396+
let data = b"The quick brown fox jumps over the lazy dog";
397+
assert_eq!(decode_url_safe(encode_url_safe(data)).unwrap(), data);
398+
}
399+
400+
#[derive(Serialize, Deserialize)]
401+
struct TestStruct {
402+
#[serde(serialize_with = "serialize", deserialize_with = "deserialize")]
403+
data: Vec<u8>,
404+
}
405+
406+
#[test]
407+
fn serde_standard() {
408+
let original = TestStruct {
409+
data: b"test data".to_vec(),
410+
};
411+
let json = serde_json::to_string(&original).unwrap();
412+
assert!(json.contains("dGVzdCBkYXRh"));
413+
let deserialized: TestStruct = serde_json::from_str(&json).unwrap();
414+
assert_eq!(deserialized.data, original.data);
415+
}
416+
417+
#[derive(Serialize, Deserialize)]
418+
struct TestStructUrlSafe {
419+
#[serde(
420+
serialize_with = "serialize_url_safe",
421+
deserialize_with = "deserialize_url_safe"
422+
)]
423+
data: Vec<u8>,
424+
}
425+
426+
#[test]
427+
fn serde_url_safe() {
428+
let original = TestStructUrlSafe {
429+
data: b"test data".to_vec(),
430+
};
431+
let json = serde_json::to_string(&original).unwrap();
432+
assert!(json.contains("dGVzdCBkYXRh"));
433+
let deserialized: TestStructUrlSafe = serde_json::from_str(&json).unwrap();
434+
assert_eq!(deserialized.data, original.data);
435+
}
436+
437+
#[derive(Serialize, Deserialize)]
438+
struct TestOptionalStruct {
439+
#[serde(
440+
serialize_with = "option::serialize",
441+
deserialize_with = "option::deserialize"
442+
)]
443+
data: Option<Vec<u8>>,
444+
}
445+
446+
#[test]
447+
fn serde_option_some() {
448+
let original = TestOptionalStruct {
449+
data: Some(b"test data".to_vec()),
450+
};
451+
let json = serde_json::to_string(&original).unwrap();
452+
assert!(json.contains("dGVzdCBkYXRh"));
453+
let deserialized: TestOptionalStruct = serde_json::from_str(&json).unwrap();
454+
assert_eq!(deserialized.data, original.data);
455+
}
456+
457+
#[test]
458+
fn serde_option_none() {
459+
let original = TestOptionalStruct { data: None };
460+
let json = serde_json::to_string(&original).unwrap();
461+
assert!(json.contains("null"));
462+
let deserialized: TestOptionalStruct = serde_json::from_str(&json).unwrap();
463+
assert_eq!(deserialized.data, None);
464+
}
465+
466+
#[derive(Serialize, Deserialize)]
467+
struct TestOptionalStructUrlSafe {
468+
#[serde(
469+
serialize_with = "option::serialize_url_safe",
470+
deserialize_with = "option::deserialize_url_safe"
471+
)]
472+
data: Option<Vec<u8>>,
473+
}
474+
475+
#[test]
476+
fn serde_option_url_safe_some() {
477+
let original = TestOptionalStructUrlSafe {
478+
data: Some(b"test data".to_vec()),
479+
};
480+
let json = serde_json::to_string(&original).unwrap();
481+
assert!(json.contains("dGVzdCBkYXRh"));
482+
let deserialized: TestOptionalStructUrlSafe = serde_json::from_str(&json).unwrap();
483+
assert_eq!(deserialized.data, original.data);
484+
}
485+
486+
#[test]
487+
fn serde_option_url_safe_none() {
488+
let original = TestOptionalStructUrlSafe { data: None };
489+
let json = serde_json::to_string(&original).unwrap();
490+
assert!(json.contains("null"));
491+
let deserialized: TestOptionalStructUrlSafe = serde_json::from_str(&json).unwrap();
492+
assert_eq!(deserialized.data, None);
493+
}
494+
495+
#[test]
496+
fn padding_behavior_differences() {
497+
// base64url encoding never produces padding
498+
let encoded_url_safe = encode_url_safe(b"f");
499+
assert!(!encoded_url_safe.contains('='));
500+
501+
// standard base64 encoding produces padding when needed
502+
let encoded_standard = encode(b"f");
503+
assert!(encoded_standard.contains('='));
504+
505+
// Both decoders accept both padded and unpadded input (DecodePaddingMode::Indifferent)
506+
assert_eq!(decode_url_safe("Zg==").unwrap(), b"f"); // padded input accepted
507+
assert_eq!(decode_url_safe("Zg").unwrap(), b"f"); // unpadded input accepted
508+
assert_eq!(decode("Zg==").unwrap(), b"f"); // padded input accepted
509+
assert_eq!(decode("Zg").unwrap(), b"f"); // unpadded input accepted
510+
}
511+
512+
#[test]
513+
fn decode_truly_invalid_fails() {
514+
// Characters outside base64 alphabet should fail
515+
assert!(decode("Zg!@").is_err());
516+
assert!(decode_url_safe("Zg!@").is_err());
517+
518+
// Invalid length for certain inputs should fail
519+
assert!(decode("Z").is_err()); // single character
520+
assert!(decode_url_safe("Z").is_err());
521+
}
522+
523+
#[test]
524+
fn decode_cross_alphabet_characters_fail() {
525+
// Test data containing base64url-specific characters (- and _) should fail standard base64 decoding
526+
// These strings contain characters that are valid in base64url but not in standard base64
527+
assert!(decode("SGVs-bG8sIHdvcmxkIQ").is_err()); // contains '-' (base64url char 62)
528+
assert!(decode("SGVs_bG8sIHdvcmxkIQ").is_err()); // contains '_' (base64url char 63)
529+
530+
// Test data containing standard base64-specific characters (+ and /) should fail base64url decoding
531+
// These strings contain characters that are valid in standard base64 but not in base64url
532+
assert!(decode_url_safe("SGVs+bG8sIHdvcmxkIQ").is_err()); // contains '+' (base64 char 62)
533+
assert!(decode_url_safe("SGVs/bG8sIHdvcmxkIQ").is_err()); // contains '/' (base64 char 63)
534+
}
535+
536+
#[test]
537+
fn decode_invalid_standard() {
538+
assert!(decode("invalid!@#$").is_err());
539+
}
540+
541+
#[test]
542+
fn decode_invalid_url_safe() {
543+
assert!(decode_url_safe("invalid!@#$").is_err());
544+
}
545+
}

0 commit comments

Comments
 (0)