diff --git a/sdk/core/azure-core/test/ut/base64_test.cpp b/sdk/core/azure-core/test/ut/base64_test.cpp index 84a945643..38859ae11 100644 --- a/sdk/core/azure-core/test/ut/base64_test.cpp +++ b/sdk/core/azure-core/test/ut/base64_test.cpp @@ -186,3 +186,225 @@ TEST(Base64, InvalidDecode) // cspell::enable } + +// Base64Url Tests +TEST(Base64Url, BasicEncode) +{ + // Test empty input + std::vector emptyData; + std::string result = _internal::Base64Url::Base64UrlEncode(emptyData); + EXPECT_EQ(result, ""); + + // Test single byte (padding should be removed) + std::vector oneByte = {0x01}; + result = _internal::Base64Url::Base64UrlEncode(oneByte); + EXPECT_EQ(result, "AQ"); // Base64 would be "AQ==", Base64Url removes padding + + // Test two bytes (padding should be removed) + std::vector twoBytes = {0x01, 0x02}; + result = _internal::Base64Url::Base64UrlEncode(twoBytes); + EXPECT_EQ(result, "AQI"); // Base64 would be "AQI=", Base64Url removes padding + + // Test three bytes (no padding needed) + std::vector threeBytes = {0x01, 0x02, 0x03}; + result = _internal::Base64Url::Base64UrlEncode(threeBytes); + EXPECT_EQ(result, "AQID"); // cspell:disable-line + + // Test data with + and / in Base64 encoding (should be replaced with - and _) + // Base64 for these bytes would contain '+' and '/', which should be replaced in Base64Url + std::vector specialChars = {0xFB, 0xEF}; + result = _internal::Base64Url::Base64UrlEncode(specialChars); + // Verify it doesn't contain + or / + EXPECT_EQ(result.find('+'), std::string::npos); + EXPECT_EQ(result.find('/'), std::string::npos); + + // Test data that generates + in Base64 + std::vector dataWithPlus = {0xFB}; // This should generate '+' in standard Base64 + result = _internal::Base64Url::Base64UrlEncode(dataWithPlus); + EXPECT_EQ(result.find('+'), std::string::npos); + EXPECT_NE(result.find('-'), std::string::npos); // Should have '-' instead + + // Test data that generates / in Base64 + std::vector dataWithSlash = {0xFF}; // This should generate '/' in standard Base64 + result = _internal::Base64Url::Base64UrlEncode(dataWithSlash); + EXPECT_EQ(result.find('/'), std::string::npos); + EXPECT_NE(result.find('_'), std::string::npos); // Should have '_' instead +} + +TEST(Base64Url, BasicDecode) +{ + // Test empty input + std::vector result = _internal::Base64Url::Base64UrlDecode(""); + EXPECT_TRUE(result.empty()); + + // Test single byte (no padding in input) + result = _internal::Base64Url::Base64UrlDecode("AQ"); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0], 0x01); + + // Test two bytes (no padding in input) + result = _internal::Base64Url::Base64UrlDecode("AQI"); + EXPECT_EQ(result.size(), 2u); + EXPECT_EQ(result[0], 0x01); + EXPECT_EQ(result[1], 0x02); + + // Test three bytes + result = _internal::Base64Url::Base64UrlDecode("AQID"); // cspell:disable-line + EXPECT_EQ(result.size(), 3u); + EXPECT_EQ(result[0], 0x01); + EXPECT_EQ(result[1], 0x02); + EXPECT_EQ(result[2], 0x03); + + // Test with URL-safe characters (- and _) + std::vector encoded = {0xFB, 0xEF}; + std::string base64UrlEncoded = _internal::Base64Url::Base64UrlEncode(encoded); + result = _internal::Base64Url::Base64UrlDecode(base64UrlEncoded); + EXPECT_EQ(result, encoded); +} + +TEST(Base64Url, RoundtripEncodeDecode) +{ + // Test various lengths to verify padding handling + for (size_t len : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000}) + { + std::vector data; + data.resize(len); + RandomBuffer(data.data(), data.size()); + + std::string encoded = _internal::Base64Url::Base64UrlEncode(data); + std::vector decoded = _internal::Base64Url::Base64UrlDecode(encoded); + + EXPECT_EQ(decoded, data) << "Roundtrip failed for length " << len; + + // Verify no padding characters in encoded output + EXPECT_EQ(encoded.find('='), std::string::npos) + << "Base64Url encoded string should not contain padding"; + + // Verify no standard Base64 special chars (+ and /) + EXPECT_EQ(encoded.find('+'), std::string::npos) + << "Base64Url encoded string should not contain '+'"; + EXPECT_EQ(encoded.find('/'), std::string::npos) + << "Base64Url encoded string should not contain '/'"; + } +} + +TEST(Base64Url, SpecialCharacterReplacement) +{ + // Create data that will produce '+' and '/' in standard Base64 + // 0x3E in 6-bit encoding = 62 = '+' in Base64, should be '-' in Base64Url + // 0x3F in 6-bit encoding = 63 = '/' in Base64, should be '_' in Base64Url + + // Pattern that generates both + and / + // We need bytes that when encoded produce these characters + std::vector data; + for (int i = 0; i < 256; i++) + { + data.push_back(static_cast(i)); + } + + std::string encoded = _internal::Base64Url::Base64UrlEncode(data); + + // Verify the encoding uses URL-safe characters + EXPECT_EQ(encoded.find('+'), std::string::npos) << "Should not contain '+'"; + EXPECT_EQ(encoded.find('/'), std::string::npos) << "Should not contain '/'"; + + // Verify roundtrip works + std::vector decoded = _internal::Base64Url::Base64UrlDecode(encoded); + EXPECT_EQ(decoded, data); +} + +TEST(Base64Url, PaddingHandling) +{ + // Test that Base64Url decode correctly adds padding + + // Length % 4 == 2 should add "==" + std::vector data1 = {0x01}; + std::string encoded1 = _internal::Base64Url::Base64UrlEncode(data1); + EXPECT_EQ(encoded1.length() % 4, 2u); + EXPECT_EQ(encoded1.find('='), std::string::npos); + std::vector decoded1 = _internal::Base64Url::Base64UrlDecode(encoded1); + EXPECT_EQ(decoded1, data1); + + // Length % 4 == 3 should add "=" + std::vector data2 = {0x01, 0x02}; + std::string encoded2 = _internal::Base64Url::Base64UrlEncode(data2); + EXPECT_EQ(encoded2.length() % 4, 3u); + EXPECT_EQ(encoded2.find('='), std::string::npos); + std::vector decoded2 = _internal::Base64Url::Base64UrlDecode(encoded2); + EXPECT_EQ(decoded2, data2); + + // Length % 4 == 0 should not add padding + std::vector data3 = {0x01, 0x02, 0x03}; + std::string encoded3 = _internal::Base64Url::Base64UrlEncode(data3); + EXPECT_EQ(encoded3.length() % 4, 0u); + std::vector decoded3 = _internal::Base64Url::Base64UrlDecode(encoded3); + EXPECT_EQ(decoded3, data3); +} + +TEST(Base64Url, InvalidDecodeInput) +{ + // Test length % 4 == 1 (invalid) + EXPECT_THROW( + _internal::Base64Url::Base64UrlDecode("A"), // cspell:disable-line + std::invalid_argument); + EXPECT_THROW( + _internal::Base64Url::Base64UrlDecode("AAAAA"), // cspell:disable-line + std::invalid_argument); + EXPECT_THROW( + _internal::Base64Url::Base64UrlDecode("AAAAAAAAA"), // cspell:disable-line + std::invalid_argument); + + // Test invalid characters (that aren't valid in Base64) + EXPECT_THROW(_internal::Base64Url::Base64UrlDecode("@@@@"), std::runtime_error); + EXPECT_THROW(_internal::Base64Url::Base64UrlDecode("A@@@"), std::runtime_error); + EXPECT_THROW(_internal::Base64Url::Base64UrlDecode("####"), std::runtime_error); +} + +TEST(Base64Url, ComparisonWithStandardBase64) +{ + // Create test data + std::vector data = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}; + + // Encode with standard Base64 + std::string base64 = Convert::Base64Encode(data); + + // Encode with Base64Url + std::string base64Url = _internal::Base64Url::Base64UrlEncode(data); + + // Base64Url should not have padding + EXPECT_NE(base64.find('='), std::string::npos); // Standard Base64 should have padding + EXPECT_EQ(base64Url.find('='), std::string::npos); // Base64Url should not + + // Both should decode to the same data + EXPECT_EQ(Convert::Base64Decode(base64), data); + EXPECT_EQ(_internal::Base64Url::Base64UrlDecode(base64Url), data); +} + +TEST(Base64Url, KnownVectors) +{ + // Test with known Base64 -> Base64Url conversions + // Standard Base64: "hello" -> "aGVsbG8=" + // Base64Url should be: "aGVsbG8" (no padding) + std::vector hello = {'h', 'e', 'l', 'l', 'o'}; + std::string encoded = _internal::Base64Url::Base64UrlEncode(hello); + EXPECT_EQ(encoded, "aGVsbG8"); // cspell:disable-line + + // Decode and verify + std::vector decoded + = _internal::Base64Url::Base64UrlDecode("aGVsbG8"); // cspell:disable-line + EXPECT_EQ(decoded, hello); + + // Test case with special character replacement needs + // Using bytes that create both + and / in standard Base64 + // Standard Base64 for {0xFB, 0xFF, 0xFE} is "+//+", which includes both + and / + std::vector specialData = {0xFB, 0xFF, 0xFE}; + std::string specialEncoded = _internal::Base64Url::Base64UrlEncode(specialData); + + // Verify URL-safe + EXPECT_EQ(specialEncoded.find('+'), std::string::npos); + EXPECT_EQ(specialEncoded.find('/'), std::string::npos); + + // Verify roundtrip + std::vector specialDecoded = _internal::Base64Url::Base64UrlDecode(specialEncoded); + EXPECT_EQ(specialDecoded, specialData); +}