Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ public class TenantManagerTest
"...",
"..\n",
"tenant\n",
"../other",
"victim.",
"tenant.",
"tenant/1",
"tenant?foo=bar",
"tenant#frag",
"tenant%2e%2e",
"tenant%2f1",
"tenant\\1",
};

Expand Down Expand Up @@ -283,7 +282,7 @@ public async Task UpdateTenant(TestConfig config)
Assert.Single(handler.Requests);
var request = handler.Requests[0];
Assert.Equal(HttpUtils.Patch, request.Method);
var mask = "allowPasswordSignup,displayName,enableEmailLinkSignin";
var mask = "allowPasswordSignup%2CdisplayName%2CenableEmailLinkSignin";
config.AssertRequest($"tenants/tenant1?updateMask={mask}", request);

var body = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(
Expand Down Expand Up @@ -598,7 +597,7 @@ public void ListTenantsOptions(TestConfig config)
var customOptions = new ListTenantsOptions()
{
PageSize = 3,
PageToken = "custom-token",
PageToken = "custom+token=1&2#3",
};

var pagedEnumerable = auth.TenantManager.ListTenantsAsync(customOptions);
Expand All @@ -611,7 +610,8 @@ public void ListTenantsOptions(TestConfig config)
Assert.All(tenants, AssertTenant);

Assert.Equal(2, handler.Requests.Count);
config.AssertRequest("tenants?pageSize=3&pageToken=custom-token", handler.Requests[0]);
config.AssertRequest(
"tenants?pageSize=3&pageToken=custom%2Btoken%3D1%262%233", handler.Requests[0]);
config.AssertRequest("tenants?pageSize=3&pageToken=token", handler.Requests[1]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ public class OidcProviderConfigTest
"oidc.\n",
"oidc.provider\n",
"oidc.provider.",
"oidc.provider/../other",
"oidc.provider/1",
"oidc.provider?foo=bar",
"oidc.provider#frag",
"oidc.provider%2fother",
"oidc.provider%2f1",
"oidc.provider&extra=1",
};

Expand Down Expand Up @@ -314,7 +314,7 @@ public async Task UpdateConfig(ProviderTestConfig config)
Assert.Single(handler.Requests);
var request = handler.Requests[0];
Assert.Equal(ProviderTestConfig.PatchMethod, request.Method);
var mask = "clientId,clientSecret,displayName,enabled,issuer,responseType.code,responseType.idToken";
var mask = "clientId%2CclientSecret%2CdisplayName%2Cenabled%2Cissuer%2CresponseType.code%2CresponseType.idToken";
config.AssertRequest($"oauthIdpConfigs/oidc.provider?updateMask={mask}", request);

var body = NewtonsoftJsonSerializer.Instance.Deserialize<JObject>(
Expand Down Expand Up @@ -640,7 +640,7 @@ public void ListOidcOptions(ProviderTestConfig config)
var customOptions = new ListProviderConfigsOptions()
{
PageSize = 3,
PageToken = "custom-token",
PageToken = "custom+token=1&2#3",
};

var pagedEnumerable = auth.ListOidcProviderConfigsAsync(customOptions);
Expand All @@ -654,7 +654,7 @@ public void ListOidcOptions(ProviderTestConfig config)

Assert.Equal(2, handler.Requests.Count);
config.AssertRequest(
"oauthIdpConfigs?pageSize=3&pageToken=custom-token", handler.Requests[0]);
"oauthIdpConfigs?pageSize=3&pageToken=custom%2Btoken%3D1%262%233", handler.Requests[0]);
config.AssertRequest(
"oauthIdpConfigs?pageSize=3&pageToken=token", handler.Requests[1]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ public class SamlProviderConfigTest
"saml.\n",
"saml.provider\n",
"saml.provider.",
"saml.provider/../other",
"saml.provider/1",
"saml.provider?foo=bar",
"saml.provider#frag",
"saml.provider%2fother",
"saml.provider%2f1",
"saml.provider&extra=1",
};

Expand Down Expand Up @@ -315,8 +315,8 @@ public async Task UpdateConfig(ProviderTestConfig config)
this.AssertSamlProviderConfig(provider);
var request = Assert.Single(handler.Requests);
Assert.Equal(ProviderTestConfig.PatchMethod, request.Method);
var mask = "displayName,enabled,idpConfig.idpCertificates,idpConfig.idpEntityId,idpConfig.ssoUrl,"
+ "spConfig.callbackUri,spConfig.spEntityId";
var mask = "displayName%2Cenabled%2CidpConfig.idpCertificates%2CidpConfig.idpEntityId%2CidpConfig.ssoUrl%2C"
+ "spConfig.callbackUri%2CspConfig.spEntityId";
config.AssertRequest(
$"inboundSamlConfigs/saml.provider?updateMask={mask}", request);

Expand Down Expand Up @@ -606,7 +606,7 @@ public void ListSamlOptions(ProviderTestConfig config)
var customOptions = new ListProviderConfigsOptions()
{
PageSize = 3,
PageToken = "custom-token",
PageToken = "custom+token=1&2#3",
};

var pagedEnumerable = auth.ListSamlProviderConfigsAsync(customOptions);
Expand All @@ -620,7 +620,7 @@ public void ListSamlOptions(ProviderTestConfig config)

Assert.Equal(2, handler.Requests.Count);
config.AssertRequest(
"inboundSamlConfigs?pageSize=3&pageToken=custom-token", handler.Requests[0]);
"inboundSamlConfigs?pageSize=3&pageToken=custom%2Btoken%3D1%262%233", handler.Requests[0]);
config.AssertRequest(
"inboundSamlConfigs?pageSize=3&pageToken=token", handler.Requests[1]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ public void ListUsersCustomOptions(TestConfig config)
var customOptions = new ListUsersOptions()
{
PageSize = 3,
PageToken = "custom-token",
PageToken = "custom+token=1&2#3",
};

var pagedEnumerable = auth.ListUsersAsync(customOptions);
Expand All @@ -539,7 +539,8 @@ public void ListUsersCustomOptions(TestConfig config)

Assert.Equal(2, handler.Requests.Count);
config.AssertRequest(
"accounts:batchGet?maxResults=3&nextPageToken=custom-token", handler.Requests[0]);
"accounts:batchGet?maxResults=3&nextPageToken=custom%2Btoken%3D1%262%233",
handler.Requests[0]);
config.AssertRequest(
"accounts:batchGet?maxResults=3&nextPageToken=token", handler.Requests[1]);
}
Expand Down
66 changes: 66 additions & 0 deletions FirebaseAdmin/FirebaseAdmin.Tests/Util/HttpUtilsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2026, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using Xunit;

namespace FirebaseAdmin.Util.Tests
{
public class HttpUtilsTest
{
[Fact]
public void EncodeQueryParamsNullOrEmpty()
{
Assert.Equal(string.Empty, HttpUtils.EncodeQueryParams(null));
Assert.Equal(string.Empty, HttpUtils.EncodeQueryParams(new Dictionary<string, object>()));
}

[Fact]
public void EncodeQueryParamsEscapesSpecialCharacters()
{
var queryParams = new Dictionary<string, object>()
{
{ "pageToken", "token+1=2&3#4/5" },
{ "pageSize", 100 },
{ "key with spaces", null },
};

Assert.Equal(
"?key%20with%20spaces=&pageSize=100&pageToken=token%2B1%3D2%263%234%2F5",
HttpUtils.EncodeQueryParams(queryParams));
}

[Fact]
public void EncodeQueryParamsUsesInvariantCulture()
{
var originalCulture = Thread.CurrentThread.CurrentCulture;
try
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");
var queryParams = new Dictionary<string, object>()
{
{ "ratio", 1.5 },
};

Assert.Equal("?ratio=1.5", HttpUtils.EncodeQueryParams(queryParams));
}
finally
{
Thread.CurrentThread.CurrentCulture = originalCulture;
}
}
}
}
9 changes: 8 additions & 1 deletion FirebaseAdmin/FirebaseAdmin/Util/HttpUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using Google.Apis.Json;
Expand All @@ -36,7 +37,13 @@ internal static string EncodeQueryParams(IDictionary<string, object> queryParams
if (queryParams != null && queryParams.Count > 0)
{
var list = queryParams.OrderBy(kvp => kvp.Key)
.Select(kvp => $"{kvp.Key}={kvp.Value}");
.Select(kvp =>
{
var key = Uri.EscapeDataString(kvp.Key);
var value = Uri.EscapeDataString(
Convert.ToString(kvp.Value, CultureInfo.InvariantCulture));
return $"{key}={value}";
});
queryString = "?" + string.Join("&", list);
}

Expand Down
Loading