diff --git a/.travis.yml b/.travis.yml
index 06088137..cf183702 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,4 +8,5 @@ matrix:
script:
- dotnet build FirebaseAdmin/FirebaseAdmin
- dotnet build FirebaseAdmin/FirebaseAdmin.Snippets
+ - dotnet build FirebaseAdmin/FirebaseAdmin.IntegrationTests
- dotnet test FirebaseAdmin/FirebaseAdmin.Tests
diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj
index c428e7b4..d146599a 100644
--- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj
+++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj
@@ -2,8 +2,9 @@
netcoreapp2.0
-
false
+ true
+ ../../stylecop_test.ruleset
@@ -12,6 +13,9 @@
+
+ all
+
diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs
index 1c56baf5..af4d0dce 100644
--- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs
@@ -17,24 +17,24 @@
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
-using Xunit;
using FirebaseAdmin;
using FirebaseAdmin.Auth;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Util;
+using Xunit;
namespace FirebaseAdmin.IntegrationTests
{
public class FirebaseAuthTest
{
- private const string VerifyCustomTokenUrl =
+ private const string VerifyCustomTokenUrl =
"https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken";
public FirebaseAuthTest()
{
IntegrationTestUtils.EnsureDefaultApp();
}
-
+
[Fact]
public async Task CreateCustomToken()
{
@@ -50,9 +50,9 @@ public async Task CreateCustomTokenWithClaims()
{
var developerClaims = new Dictionary()
{
- {"admin", true},
- {"package", "gold"},
- {"magicNumber", 42L},
+ { "admin", true },
+ { "package", "gold" },
+ { "magicNumber", 42L },
};
var customToken = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(
"testuser", developerClaims);
@@ -72,13 +72,14 @@ public async Task CreateCustomTokenWithClaims()
public async Task CreateCustomTokenWithoutServiceAccount()
{
var googleCred = FirebaseApp.DefaultInstance.Options.Credential;
- var serviceAcct = (ServiceAccountCredential) googleCred.UnderlyingCredential;
- var token = await ((ITokenAccess) googleCred).GetAccessTokenForRequestAsync();
- var app = FirebaseApp.Create(new AppOptions()
- {
- Credential = GoogleCredential.FromAccessToken(token),
- ServiceAccountId = serviceAcct.Id,
- }, "IAMSignApp");
+ var serviceAcct = (ServiceAccountCredential)googleCred.UnderlyingCredential;
+ var token = await ((ITokenAccess)googleCred).GetAccessTokenForRequestAsync();
+ var app = FirebaseApp.Create(
+ new AppOptions()
+ {
+ Credential = GoogleCredential.FromAccessToken(token),
+ ServiceAccountId = serviceAcct.Id,
+ }, "IAMSignApp");
try
{
var customToken = await FirebaseAuth.GetAuth(app).CreateCustomTokenAsync(
@@ -98,7 +99,7 @@ public async Task SetCustomUserClaims()
{
var customClaims = new Dictionary()
{
- {"admin", true}
+ { "admin", true },
};
await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync("testuser", customClaims);
@@ -126,12 +127,13 @@ private static async Task SignInWithCustomTokenAsync(string customToken)
var rb = new Google.Apis.Requests.RequestBuilder()
{
Method = Google.Apis.Http.HttpConsts.Post,
- BaseUri = new Uri(VerifyCustomTokenUrl),
+ BaseUri = new Uri(VerifyCustomTokenUrl),
};
rb.AddParameter(RequestParameterType.Query, "key", IntegrationTestUtils.GetApiKey());
var request = rb.CreateRequest();
var jsonSerializer = Google.Apis.Json.NewtonsoftJsonSerializer.Instance;
- var payload = jsonSerializer.Serialize(new SignInRequest{
+ var payload = jsonSerializer.Serialize(new SignInRequest
+ {
CustomToken = customToken,
ReturnSecureToken = true,
});
@@ -159,6 +161,6 @@ internal class SignInRequest
internal class SignInResponse
{
[Newtonsoft.Json.JsonProperty("idToken")]
- public String IdToken { get; set; }
+ public string IdToken { get; set; }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs
new file mode 100644
index 00000000..790294d8
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs
@@ -0,0 +1,53 @@
+// Copyright 2018, 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;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using FirebaseAdmin.Messaging;
+using Xunit;
+
+namespace FirebaseAdmin.IntegrationTests
+{
+ public class FirebaseMessagingTest
+ {
+ public FirebaseMessagingTest()
+ {
+ IntegrationTestUtils.EnsureDefaultApp();
+ }
+
+ [Fact]
+ public async Task Send()
+ {
+ var message = new Message()
+ {
+ Topic = "foo-bar",
+ Notification = new Notification()
+ {
+ Title = "Title",
+ Body = "Body",
+ },
+ Android = new AndroidConfig()
+ {
+ Priority = Priority.Normal,
+ TimeToLive = TimeSpan.FromHours(1),
+ RestrictedPackageName = "com.google.firebase.testing",
+ },
+ };
+ var id = await FirebaseMessaging.DefaultInstance.SendAsync(message, dryRun: true);
+ Assert.True(!string.IsNullOrEmpty(id));
+ Assert.Matches(new Regex("^projects/.*/messages/.*$"), id);
+ }
+ }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs
index 5761de85..bee0adc0 100644
--- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs
+++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs
@@ -26,13 +26,15 @@ internal static class IntegrationTestUtils
private const string ServiceAccountFile = "./resources/integration_cert.json";
private const string ApiKeyFile = "./resources/integration_apikey.txt";
- private static readonly Lazy DefaultFirebaseApp = new Lazy(() => {
- var options = new AppOptions()
+ private static readonly Lazy DefaultFirebaseApp = new Lazy(
+ () =>
{
- Credential = GoogleCredential.FromFile(ServiceAccountFile),
- };
- return FirebaseApp.Create(options);
- }, true);
+ var options = new AppOptions()
+ {
+ Credential = GoogleCredential.FromFile(ServiceAccountFile),
+ };
+ return FirebaseApp.Create(options);
+ }, true);
public static FirebaseApp EnsureDefaultApp()
{
diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj
index 2a896f89..ee80a864 100644
--- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj
+++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAdmin.Snippets.csproj
@@ -3,6 +3,8 @@
netcoreapp2.0
false
+ true
+ ../../stylecop_test.ruleset
@@ -11,6 +13,9 @@
+
+ all
+
diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAppSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAppSnippets.cs
index 106daa1a..32ca9be5 100644
--- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAppSnippets.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAppSnippets.cs
@@ -21,9 +21,9 @@
namespace FirebaseAdmin.Snippets
{
- class FirebaseAppSnippets
+ internal class FirebaseAppSnippets
{
- static void InitSdkWithServiceAccount()
+ internal static void InitSdkWithServiceAccount()
{
// [START initialize_sdk_with_service_account]
FirebaseApp.Create(new AppOptions()
@@ -33,7 +33,7 @@ static void InitSdkWithServiceAccount()
// [END initialize_sdk_with_service_account]
}
- static void InitSdkWithApplicationDefault()
+ internal static void InitSdkWithApplicationDefault()
{
// [START initialize_sdk_with_application_default]
FirebaseApp.Create(new AppOptions()
@@ -43,7 +43,7 @@ static void InitSdkWithApplicationDefault()
// [END initialize_sdk_with_application_default]
}
- static void InitSdkWithRefreshToken()
+ internal static void InitSdkWithRefreshToken()
{
// [START initialize_sdk_with_refresh_token]
FirebaseApp.Create(new AppOptions()
@@ -53,14 +53,14 @@ static void InitSdkWithRefreshToken()
// [END initialize_sdk_with_refresh_token]
}
- static void InitSdkWithDefaultConfig()
+ internal static void InitSdkWithDefaultConfig()
{
// [START initialize_sdk_with_default_config]
FirebaseApp.Create();
// [END initialize_sdk_with_default_config]
}
- static void InitDefaultApp()
+ internal static void InitDefaultApp()
{
// [START access_services_default]
// Initialize the default app
@@ -78,7 +78,7 @@ static void InitDefaultApp()
// [END access_services_default]
}
- static void InitCustomApp()
+ internal static void InitCustomApp()
{
var defaultOptions = new AppOptions()
{
@@ -107,7 +107,7 @@ static void InitCustomApp()
// [END access_services_nondefault]
}
- static void InitWithServiceAccountId()
+ internal static void InitWithServiceAccountId()
{
// [START initialize_sdk_with_service_account_id]
FirebaseApp.Create(new AppOptions()
diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs
index bb7e094e..0afb71e2 100644
--- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseAuthSnippets.cs
@@ -19,9 +19,9 @@
namespace FirebaseAdmin.Snippets
{
- class FirebaseAuthSnippets
+ internal class FirebaseAuthSnippets
{
- static async Task CreateCustomTokenAsync()
+ internal static async Task CreateCustomTokenAsync()
{
// [START custom_token]
var uid = "some-uid";
@@ -32,7 +32,7 @@ static async Task CreateCustomTokenAsync()
Console.WriteLine("Created custom token: {0}", customToken);
}
- static async Task CreateCustomTokenWithClaimsAsync()
+ internal static async Task CreateCustomTokenWithClaimsAsync()
{
// [START custom_token_with_claims]
var uid = "some-uid";
@@ -48,7 +48,7 @@ static async Task CreateCustomTokenWithClaimsAsync()
Console.WriteLine("Created custom token: {0}", customToken);
}
- static async Task VeridyIdTokenAsync(string idToken)
+ internal static async Task VeridyIdTokenAsync(string idToken)
{
// [START verify_id_token]
FirebaseToken decodedToken = await FirebaseAuth.DefaultInstance
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs
index bc8a90df..491aa9af 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs
@@ -20,15 +20,17 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Xunit;
+using FirebaseAdmin.Auth;
+using Google.Apis.Auth;
using Google.Apis.Auth.OAuth2;
+using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]
namespace FirebaseAdmin.Auth.Tests
{
- public class FirebaseAuthTest: IDisposable
+ public class FirebaseAuthTest : IDisposable
{
- private static readonly GoogleCredential mockCredential =
+ private static readonly GoogleCredential MockCredential =
GoogleCredential.FromAccessToken("test-token");
[Fact]
@@ -40,7 +42,7 @@ public void GetAuthWithoutApp()
[Fact]
public void GetDefaultAuth()
{
- var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential});
+ var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
FirebaseAuth auth = FirebaseAuth.DefaultInstance;
Assert.Same(auth, FirebaseAuth.DefaultInstance);
app.Delete();
@@ -50,7 +52,7 @@ public void GetDefaultAuth()
[Fact]
public void GetAuth()
{
- var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential}, "MyApp");
+ var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential }, "MyApp");
FirebaseAuth auth = FirebaseAuth.GetAuth(app);
Assert.Same(auth, FirebaseAuth.GetAuth(app));
app.Delete();
@@ -60,7 +62,7 @@ public void GetAuth()
[Fact]
public async Task UseAfterDelete()
{
- var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential});
+ var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
FirebaseAuth auth = FirebaseAuth.DefaultInstance;
app.Delete();
await Assert.ThrowsAsync(
@@ -73,7 +75,7 @@ await Assert.ThrowsAsync(
public async Task CreateCustomToken()
{
var cred = GoogleCredential.FromFile("./resources/service_account.json");
- FirebaseApp.Create(new AppOptions(){Credential = cred});
+ FirebaseApp.Create(new AppOptions() { Credential = cred });
var token = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1");
VerifyCustomToken(token, "user1", null);
}
@@ -82,12 +84,12 @@ public async Task CreateCustomToken()
public async Task CreateCustomTokenWithClaims()
{
var cred = GoogleCredential.FromFile("./resources/service_account.json");
- FirebaseApp.Create(new AppOptions(){Credential = cred});
+ FirebaseApp.Create(new AppOptions() { Credential = cred });
var developerClaims = new Dictionary()
{
- {"admin", true},
- {"package", "gold"},
- {"magicNumber", 42L},
+ { "admin", true },
+ { "package", "gold" },
+ { "magicNumber", 42L },
};
var token = await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync(
"user2", developerClaims);
@@ -98,7 +100,7 @@ public async Task CreateCustomTokenWithClaims()
public async Task CreateCustomTokenCancel()
{
var cred = GoogleCredential.FromFile("./resources/service_account.json");
- FirebaseApp.Create(new AppOptions(){Credential = cred});
+ FirebaseApp.Create(new AppOptions() { Credential = cred });
var canceller = new CancellationTokenSource();
canceller.Cancel();
await Assert.ThrowsAsync(
@@ -109,7 +111,7 @@ await Assert.ThrowsAsync(
[Fact]
public async Task CreateCustomTokenInvalidCredential()
{
- FirebaseApp.Create(new AppOptions(){Credential = mockCredential});
+ FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
await Assert.ThrowsAsync(
async () => await FirebaseAuth.DefaultInstance.CreateCustomTokenAsync("user1"));
}
@@ -117,7 +119,7 @@ await Assert.ThrowsAsync(
[Fact]
public async Task VerifyIdTokenNoProjectId()
{
- FirebaseApp.Create(new AppOptions(){Credential = mockCredential});
+ FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
var idToken = await FirebaseTokenVerifierTest.CreateTestTokenAsync();
await Assert.ThrowsAsync(
async () => await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(idToken));
@@ -128,7 +130,7 @@ public async Task VerifyIdTokenCancel()
{
FirebaseApp.Create(new AppOptions()
{
- Credential = mockCredential,
+ Credential = MockCredential,
ProjectId = "test-project",
});
var canceller = new CancellationTokenSource();
@@ -139,12 +141,29 @@ await Assert.ThrowsAnyAsync(
idToken, canceller.Token));
}
+ [Fact]
+ public async Task SetCustomUserClaimsNoProjectId()
+ {
+ FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
+ var customClaims = new Dictionary()
+ {
+ { "admin", true },
+ };
+ await Assert.ThrowsAsync(
+ async () => await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync("user1", customClaims));
+ }
+
+ public void Dispose()
+ {
+ FirebaseApp.DeleteAll();
+ }
+
private static void VerifyCustomToken(string token, string uid, Dictionary claims)
{
- String[] segments = token.Split(".");
+ string[] segments = token.Split(".");
Assert.Equal(3, segments.Length);
- var payload = JwtUtils.Decode(segments[1]);
+ var payload = JwtUtils.Decode(segments[1]);
Assert.Equal("client@test-project.iam.gserviceaccount.com", payload.Issuer);
Assert.Equal("client@test-project.iam.gserviceaccount.com", payload.Subject);
Assert.Equal(uid, payload.Uid);
@@ -164,28 +183,11 @@ private static void VerifyCustomToken(string token, string uid, Dictionary()
- {
- {"admin", true}
- };
- await Assert.ThrowsAsync(
- async () => await FirebaseAuth.DefaultInstance.SetCustomUserClaimsAsync("user1", customClaims));
- }
-
- public void Dispose()
- {
- FirebaseApp.DeleteAll();
- }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs
index 6ad7d462..fa954b82 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs
@@ -20,11 +20,11 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Xunit;
-using FirebaseAdmin.Tests;
using FirebaseAdmin.Auth;
+using FirebaseAdmin.Tests;
using Google.Apis.Auth;
using Google.Apis.Util;
+using Xunit;
namespace FirebaseAdmin.Auth.Tests
{
@@ -56,9 +56,9 @@ public async Task CreateCustomTokenWithClaims()
var factory = new FirebaseTokenFactory(new MockSigner(), clock);
var developerClaims = new Dictionary()
{
- {"admin", true},
- {"package", "gold"},
- {"magicNumber", 42L},
+ { "admin", true },
+ { "package", "gold" },
+ { "magicNumber", 42L },
};
var token = await factory.CreateCustomTokenAsync("user2", developerClaims);
VerifyCustomToken(token, "user2", developerClaims);
@@ -71,37 +71,39 @@ public async Task InvalidUid()
await Assert.ThrowsAsync(
async () => await factory.CreateCustomTokenAsync(null));
await Assert.ThrowsAsync(
- async () => await factory.CreateCustomTokenAsync(""));
+ async () => await factory.CreateCustomTokenAsync(string.Empty));
await Assert.ThrowsAsync(
- async () => await factory.CreateCustomTokenAsync(new String('a', 129)));
+ async () => await factory.CreateCustomTokenAsync(new string('a', 129)));
}
[Fact]
public async Task ReservedClaims()
{
var factory = new FirebaseTokenFactory(new MockSigner(), new MockClock());
- foreach(var key in FirebaseTokenFactory.ReservedClaims)
+ foreach (var key in FirebaseTokenFactory.ReservedClaims)
{
- var developerClaims = new Dictionary(){
- {key, "value"},
+ var developerClaims = new Dictionary()
+ {
+ { key, "value" },
};
await Assert.ThrowsAsync(
- async () => await factory.CreateCustomTokenAsync("user", developerClaims));
- }
+ async () => await factory.CreateCustomTokenAsync("user", developerClaims));
+ }
}
private static void VerifyCustomToken(
string token, string uid, Dictionary claims)
{
- String[] segments = token.Split(".");
+ string[] segments = token.Split(".");
Assert.Equal(3, segments.Length);
+
// verify header
var header = JwtUtils.Decode(segments[0]);
Assert.Equal("JWT", header.Type);
Assert.Equal("RS256", header.Algorithm);
// verify payload
- var payload = JwtUtils.Decode(segments[1]);
+ var payload = JwtUtils.Decode(segments[1]);
Assert.Equal(MockSigner.KeyIdString, payload.Issuer);
Assert.Equal(MockSigner.KeyIdString, payload.Subject);
Assert.Equal(uid, payload.Uid);
@@ -141,6 +143,6 @@ public Task SignDataAsync(byte[] data, CancellationToken cancellationTok
return Task.FromResult(Encoding.UTF8.GetBytes(Signature));
}
- public void Dispose() {}
+ public void Dispose() { }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs
index f8bfea99..306d3b5b 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs
@@ -20,20 +20,23 @@
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
-using Xunit;
-using Google.Apis.Auth.OAuth2;
-using Google.Apis.Util;
using FirebaseAdmin.Auth;
using FirebaseAdmin.Tests;
+using Google.Apis.Auth.OAuth2;
+using Google.Apis.Util;
+using Xunit;
namespace FirebaseAdmin.Auth.Tests
{
- public class FirebaseTokenVerifierTest: IDisposable
+ public class FirebaseTokenVerifierTest : IDisposable
{
private static readonly IPublicKeySource KeySource = new FileSystemPublicKeySource(
"./resources/public_cert.pem");
+
private static readonly IClock Clock = new MockClock();
+
private static readonly ISigner Signer = CreateTestSigner();
+
private static readonly FirebaseTokenVerifier TokenVerifier = new FirebaseTokenVerifier(
new FirebaseTokenVerifierArgs()
{
@@ -46,7 +49,7 @@ public class FirebaseTokenVerifierTest: IDisposable
PublicKeySource = KeySource,
});
- private static readonly GoogleCredential mockCredential =
+ private static readonly GoogleCredential MockCredential =
GoogleCredential.FromAccessToken("test-token");
[Fact]
@@ -54,17 +57,18 @@ public async Task ValidToken()
{
var payload = new Dictionary()
{
- {"foo", "bar"},
+ { "foo", "bar" },
};
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
var decoded = await TokenVerifier.VerifyTokenAsync(idToken);
Assert.Equal("testuser", decoded.Uid);
Assert.Equal("test-project", decoded.Audience);
Assert.Equal("testuser", decoded.Subject);
+
// The default test token created by CreateTestTokenAsync has an issue time 10 minutes
// ago, and an expiry time 50 minutes in the future.
- Assert.Equal(Clock.UnixTimestamp() - 60 * 10, decoded.IssuedAtTimeSeconds);
- Assert.Equal(Clock.UnixTimestamp() + 60 * 50, decoded.ExpirationTimeSeconds);
+ Assert.Equal(Clock.UnixTimestamp() - (60 * 10), decoded.IssuedAtTimeSeconds);
+ Assert.Equal(Clock.UnixTimestamp() + (60 * 50), decoded.ExpirationTimeSeconds);
Assert.Single(decoded.Claims);
object value;
Assert.True(decoded.Claims.TryGetValue("foo", out value));
@@ -77,7 +81,7 @@ public async Task InvalidArgument()
await Assert.ThrowsAsync(
async () => await TokenVerifier.VerifyTokenAsync(null));
await Assert.ThrowsAsync(
- async () => await TokenVerifier.VerifyTokenAsync(""));
+ async () => await TokenVerifier.VerifyTokenAsync(string.Empty));
}
[Fact]
@@ -92,7 +96,7 @@ public async Task NoKid()
{
var header = new Dictionary()
{
- {"kid", ""},
+ { "kid", string.Empty },
};
var idToken = await CreateTestTokenAsync(headerOverrides: header);
await Assert.ThrowsAsync(
@@ -104,7 +108,7 @@ public async Task IncorrectKid()
{
var header = new Dictionary()
{
- {"kid", "incorrect-key-id"},
+ { "kid", "incorrect-key-id" },
};
var idToken = await CreateTestTokenAsync(headerOverrides: header);
await Assert.ThrowsAsync(
@@ -116,7 +120,7 @@ public async Task IncorrectAlgorithm()
{
var header = new Dictionary()
{
- {"alg", "HS256"},
+ { "alg", "HS256" },
};
var idToken = await CreateTestTokenAsync(headerOverrides: header);
await Assert.ThrowsAsync(
@@ -128,7 +132,7 @@ public async Task Expired()
{
var payload = new Dictionary()
{
- {"exp", Clock.UnixTimestamp() - 60},
+ { "exp", Clock.UnixTimestamp() - 60 },
};
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
await Assert.ThrowsAsync(
@@ -140,7 +144,7 @@ public async Task InvalidIssuedAt()
{
var payload = new Dictionary()
{
- {"iat", Clock.UnixTimestamp() + 60},
+ { "iat", Clock.UnixTimestamp() + 60 },
};
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
await Assert.ThrowsAsync(
@@ -152,7 +156,7 @@ public async Task InvalidIssuer()
{
var payload = new Dictionary()
{
- {"iss", "wrong-issuer"},
+ { "iss", "wrong-issuer" },
};
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
await Assert.ThrowsAsync(
@@ -164,7 +168,7 @@ public async Task InvalidAudience()
{
var payload = new Dictionary()
{
- {"aud", "wrong-audience"},
+ { "aud", "wrong-audience" },
};
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
await Assert.ThrowsAsync(
@@ -176,7 +180,7 @@ public async Task EmptySubject()
{
var payload = new Dictionary()
{
- {"sub", ""},
+ { "sub", string.Empty },
};
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
await Assert.ThrowsAsync(
@@ -188,7 +192,7 @@ public async Task LongSubject()
{
var payload = new Dictionary()
{
- {"sub", new String('a', 129)},
+ { "sub", new string('a', 129) },
};
var idToken = await CreateTestTokenAsync(payloadOverrides: payload);
await Assert.ThrowsAsync(
@@ -200,7 +204,7 @@ public void ProjectIdFromOptions()
{
var app = FirebaseApp.Create(new AppOptions()
{
- Credential = mockCredential,
+ Credential = MockCredential,
ProjectId = "explicit-project-id",
});
var verifier = FirebaseTokenVerifier.CreateIDTokenVerifier(app);
@@ -226,14 +230,14 @@ public void ProjectIdFromEnvironment()
{
var app = FirebaseApp.Create(new AppOptions()
{
- Credential = mockCredential,
+ Credential = MockCredential,
});
var verifier = FirebaseTokenVerifier.CreateIDTokenVerifier(app);
Assert.Equal("env-project-id", verifier.ProjectId);
}
finally
{
- Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", "");
+ Environment.SetEnvironmentVariable("GOOGLE_CLOUD_PROJECT", string.Empty);
}
}
@@ -253,9 +257,9 @@ internal static async Task CreateTestTokenAsync(
{
var header = new Dictionary()
{
- {"alg", "RS256"},
- {"typ", "jwt"},
- {"kid", "test-key-id"},
+ { "alg", "RS256" },
+ { "typ", "jwt" },
+ { "kid", "test-key-id" },
};
if (headerOverrides != null)
{
@@ -267,11 +271,11 @@ internal static async Task CreateTestTokenAsync(
var payload = new Dictionary()
{
- {"sub", "testuser"},
- {"iss", "https://securetoken.google.com/test-project"},
- {"aud", "test-project"},
- {"iat", Clock.UnixTimestamp() - 60 * 10},
- {"exp", Clock.UnixTimestamp() + 60 * 50},
+ { "sub", "testuser" },
+ { "iss", "https://securetoken.google.com/test-project" },
+ { "aud", "test-project" },
+ { "iat", Clock.UnixTimestamp() - (60 * 10) },
+ { "exp", Clock.UnixTimestamp() + (60 * 50) },
};
if (payloadOverrides != null)
{
@@ -280,32 +284,33 @@ internal static async Task CreateTestTokenAsync(
payload[entry.Key] = entry.Value;
}
}
+
return await JwtUtils.CreateSignedJwtAsync(header, payload, Signer);
}
private static ISigner CreateTestSigner()
{
var credential = GoogleCredential.FromFile("./resources/service_account.json");
- var serviceAccount = (ServiceAccountCredential) credential.UnderlyingCredential;
+ var serviceAccount = (ServiceAccountCredential)credential.UnderlyingCredential;
return new ServiceAccountSigner(serviceAccount);
}
}
internal class FileSystemPublicKeySource : IPublicKeySource
{
- private IReadOnlyList _rsa;
+ private IReadOnlyList rsa;
public FileSystemPublicKeySource(string file)
{
var x509cert = new X509Certificate2(File.ReadAllBytes(file));
- var rsa = (RSA) x509cert.PublicKey.Key;
- _rsa = ImmutableList.Create(new PublicKey("test-key-id", rsa));
+ var rsa = (RSA)x509cert.PublicKey.Key;
+ this.rsa = ImmutableList.Create(new PublicKey("test-key-id", rsa));
}
public Task> GetPublicKeysAsync(
CancellationToken cancellationToken)
{
- return Task.FromResult(_rsa);
+ return Task.FromResult(this.rsa);
}
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs
index 3b666c49..c3114fe2 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseUserManagerTest.cs
@@ -14,25 +14,26 @@
using System;
using System.Collections.Generic;
-using Xunit;
-using Google.Apis.Auth.OAuth2;
-using FirebaseAdmin.Tests;
-using System.Threading.Tasks;
using System.Net;
+using System.Threading.Tasks;
+using FirebaseAdmin.Tests;
+using Google.Apis.Auth.OAuth2;
+using Xunit;
namespace FirebaseAdmin.Auth.Tests
{
public class FirebaseUserManagerTest
{
- private static readonly GoogleCredential mockCredential =
+ private const string MockProjectId = "project1";
+
+ private static readonly GoogleCredential MockCredential =
GoogleCredential.FromAccessToken("test-token");
- private const string mockProjectId = "project1";
[Fact]
public void InvalidUidForUserRecord()
{
Assert.Throws(() => new UserRecord(null));
- Assert.Throws(() => new UserRecord(""));
+ Assert.Throws(() => new UserRecord(string.Empty));
Assert.Throws(() => new UserRecord(new string('a', 129)));
}
@@ -41,18 +42,20 @@ public void ReservedClaims()
{
foreach (var key in FirebaseTokenFactory.ReservedClaims)
{
- var customClaims = new Dictionary(){
- {key, "value"},
+ var customClaims = new Dictionary()
+ {
+ { key, "value" },
};
- Assert.Throws(() => new UserRecord("user1") { CustomClaims = customClaims});
+ Assert.Throws(() => new UserRecord("user1") { CustomClaims = customClaims });
}
}
[Fact]
public void EmptyClaims()
{
- var emptyClaims = new Dictionary(){
- {"", "value"},
+ var emptyClaims = new Dictionary()
+ {
+ { string.Empty, "value" },
};
Assert.Throws(() => new UserRecord("user1") { CustomClaims = emptyClaims });
}
@@ -73,18 +76,19 @@ public async Task UpdateUser()
{
var handler = new MockMessageHandler()
{
- Response = new UserRecord("user1")
+ Response = new UserRecord("user1"),
};
var factory = new MockHttpClientFactory(handler);
var userManager = new FirebaseUserManager(
new FirebaseUserManagerArgs
{
- Credential = mockCredential,
- ProjectId = mockProjectId,
- ClientFactory = factory
+ Credential = MockCredential,
+ ProjectId = MockProjectId,
+ ClientFactory = factory,
});
- var customClaims = new Dictionary(){
- {"admin", true},
+ var customClaims = new Dictionary()
+ {
+ { "admin", true },
};
await userManager.UpdateUserAsync(new UserRecord("user1") { CustomClaims = customClaims });
@@ -95,18 +99,19 @@ public async Task UpdateUserIncorrectResponseObject()
{
var handler = new MockMessageHandler()
{
- Response = new object()
+ Response = new object(),
};
var factory = new MockHttpClientFactory(handler);
var userManager = new FirebaseUserManager(
new FirebaseUserManagerArgs
{
- Credential = mockCredential,
- ProjectId = mockProjectId,
- ClientFactory = factory
+ Credential = MockCredential,
+ ProjectId = MockProjectId,
+ ClientFactory = factory,
});
- var customClaims = new Dictionary(){
- {"admin", true},
+ var customClaims = new Dictionary()
+ {
+ { "admin", true },
};
await Assert.ThrowsAsync(
@@ -118,18 +123,19 @@ public async Task UpdateUserIncorrectResponseUid()
{
var handler = new MockMessageHandler()
{
- Response = new UserRecord("testuser")
+ Response = new UserRecord("testuser"),
};
var factory = new MockHttpClientFactory(handler);
var userManager = new FirebaseUserManager(
new FirebaseUserManagerArgs
{
- Credential = mockCredential,
- ProjectId = mockProjectId,
- ClientFactory = factory
+ Credential = MockCredential,
+ ProjectId = MockProjectId,
+ ClientFactory = factory,
});
- var customClaims = new Dictionary(){
- {"admin", true},
+ var customClaims = new Dictionary()
+ {
+ { "admin", true },
};
await Assert.ThrowsAsync(
@@ -141,18 +147,19 @@ public async Task UpdateUserHttpError()
{
var handler = new MockMessageHandler()
{
- StatusCode = HttpStatusCode.InternalServerError
+ StatusCode = HttpStatusCode.InternalServerError,
};
var factory = new MockHttpClientFactory(handler);
var userManager = new FirebaseUserManager(
new FirebaseUserManagerArgs
{
- Credential = mockCredential,
- ProjectId = mockProjectId,
- ClientFactory = factory
+ Credential = MockCredential,
+ ProjectId = MockProjectId,
+ ClientFactory = factory,
});
- var customClaims = new Dictionary(){
- {"admin", true},
+ var customClaims = new Dictionary()
+ {
+ { "admin", true },
};
await Assert.ThrowsAsync(
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs
index e7563010..cf4406b4 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs
@@ -18,9 +18,9 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
-using Xunit;
-using FirebaseAdmin.Tests;
using FirebaseAdmin.Auth;
+using FirebaseAdmin.Tests;
+using Xunit;
namespace FirebaseAdmin.Auth.Tests
{
@@ -92,7 +92,7 @@ public void InvalidArguments()
Assert.Throws(
() => new HttpPublicKeySource(null, clock, clientFactory));
Assert.Throws(
- () => new HttpPublicKeySource("", clock, clientFactory));
+ () => new HttpPublicKeySource(string.Empty, clock, clientFactory));
Assert.Throws(
() => new HttpPublicKeySource("https://example.com/certs", null, clientFactory));
Assert.Throws(
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs
index 20815944..3bd82579 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs
@@ -19,14 +19,65 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using FirebaseAdmin.Tests;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Http;
using Google.Apis.Json;
using Xunit;
-using FirebaseAdmin.Tests;
namespace FirebaseAdmin.Auth.Tests
{
+ public class IAMSignerTest
+ {
+ [Fact]
+ public async Task Signer()
+ {
+ var bytes = Encoding.UTF8.GetBytes("signature");
+ var handler = new MockMessageHandler()
+ {
+ Response = "discovered-service-account",
+ };
+ var factory = new MockHttpClientFactory(handler);
+ var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token"));
+ Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync());
+ Assert.Equal(1, handler.Calls);
+
+ // should only fetch account once
+ Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync());
+ Assert.Equal(1, handler.Calls);
+
+ handler.Response = new IAMSigner.SignBlobResponse()
+ {
+ Signature = Convert.ToBase64String(bytes),
+ };
+ byte[] data = Encoding.UTF8.GetBytes("Hello world");
+ byte[] signature = await signer.SignDataAsync(data);
+ Assert.Equal(bytes, signature);
+ var req = NewtonsoftJsonSerializer.Instance.Deserialize(
+ handler.Request);
+ Assert.Equal(Convert.ToBase64String(data), req.BytesToSign);
+ Assert.Equal(2, handler.Calls);
+ }
+
+ [Fact]
+ public async Task AccountDiscoveryError()
+ {
+ var bytes = Encoding.UTF8.GetBytes("signature");
+ var handler = new MockMessageHandler()
+ {
+ StatusCode = HttpStatusCode.InternalServerError,
+ };
+ var factory = new MockHttpClientFactory(handler);
+ var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token"));
+ await Assert.ThrowsAsync(
+ async () => await signer.GetKeyIdAsync());
+ Assert.Equal(1, handler.Calls);
+ await Assert.ThrowsAsync(
+ async () => await signer.GetKeyIdAsync());
+ Assert.Equal(1, handler.Calls);
+ }
+ }
+
public class FixedAccountIAMSignerTest
{
[Fact]
@@ -35,7 +86,7 @@ public async Task Signer()
var bytes = Encoding.UTF8.GetBytes("signature");
var handler = new MockMessageHandler()
{
- Response = new SignBlobResponse()
+ Response = new IAMSigner.SignBlobResponse()
{
Signature = Convert.ToBase64String(bytes),
},
@@ -47,7 +98,7 @@ public async Task Signer()
byte[] data = Encoding.UTF8.GetBytes("Hello world");
byte[] signature = await signer.SignDataAsync(data);
Assert.Equal(bytes, signature);
- var req = NewtonsoftJsonSerializer.Instance.Deserialize(
+ var req = NewtonsoftJsonSerializer.Instance.Deserialize(
handler.Request);
Assert.Equal(Convert.ToBase64String(data), req.BytesToSign);
Assert.Equal(1, handler.Calls);
@@ -59,7 +110,7 @@ public async Task WelformedSignError()
var handler = new MockMessageHandler()
{
StatusCode = HttpStatusCode.InternalServerError,
- Response = @"{""error"": {""message"": ""test reason""}}"
+ Response = @"{""error"": {""message"": ""test reason""}}",
};
var factory = new MockHttpClientFactory(handler);
var signer = new FixedAccountIAMSigner(
@@ -77,7 +128,7 @@ public async Task UnexpectedSignError()
var handler = new MockMessageHandler()
{
StatusCode = HttpStatusCode.InternalServerError,
- Response = "not json"
+ Response = "not json",
};
var factory = new MockHttpClientFactory(handler);
var signer = new FixedAccountIAMSigner(
@@ -89,55 +140,4 @@ public async Task UnexpectedSignError()
Assert.Contains("not json", ex.Message);
}
}
-
- public class IAMSignerTest
- {
- [Fact]
- public async Task Signer()
- {
- var bytes = Encoding.UTF8.GetBytes("signature");
- var handler = new MockMessageHandler()
- {
- Response = "discovered-service-account",
- };
- var factory = new MockHttpClientFactory(handler);
- var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token"));
- Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync());
- Assert.Equal(1, handler.Calls);
-
- // should only fetch account once
- Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync());
- Assert.Equal(1, handler.Calls);
-
- handler.Response = new SignBlobResponse()
- {
- Signature = Convert.ToBase64String(bytes),
- };
- byte[] data = Encoding.UTF8.GetBytes("Hello world");
- byte[] signature = await signer.SignDataAsync(data);
- Assert.Equal(bytes, signature);
- var req = NewtonsoftJsonSerializer.Instance.Deserialize(
- handler.Request);
- Assert.Equal(Convert.ToBase64String(data), req.BytesToSign);
- Assert.Equal(2, handler.Calls);
- }
-
- [Fact]
- public async Task AccountDiscoveryError()
- {
- var bytes = Encoding.UTF8.GetBytes("signature");
- var handler = new MockMessageHandler()
- {
- StatusCode = HttpStatusCode.InternalServerError,
- };
- var factory = new MockHttpClientFactory(handler);
- var signer = new IAMSigner(factory, GoogleCredential.FromAccessToken("token"));
- await Assert.ThrowsAsync(
- async () => await signer.GetKeyIdAsync());
- Assert.Equal(1, handler.Calls);
- await Assert.ThrowsAsync(
- async () => await signer.GetKeyIdAsync());
- Assert.Equal(1, handler.Calls);
- }
- }
}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs
index 2874a79f..cf76f125 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs
@@ -18,9 +18,9 @@
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
-using Xunit;
using FirebaseAdmin.Auth;
using Google.Apis.Auth.OAuth2;
+using Xunit;
namespace FirebaseAdmin.Auth.Tests
{
@@ -30,13 +30,13 @@ public class ServiceAccountSignerTest
public async Task Signer()
{
var credential = GoogleCredential.FromFile("./resources/service_account.json");
- var serviceAccount = (ServiceAccountCredential) credential.UnderlyingCredential;
+ var serviceAccount = (ServiceAccountCredential)credential.UnderlyingCredential;
var signer = new ServiceAccountSigner(serviceAccount);
- Assert.Equal("client@test-project.iam.gserviceaccount.com",
- await signer.GetKeyIdAsync());
+ Assert.Equal(
+ "client@test-project.iam.gserviceaccount.com", await signer.GetKeyIdAsync());
byte[] data = Encoding.UTF8.GetBytes("Hello world");
byte[] signature = signer.SignDataAsync(data).Result;
- Assert.True(Verify(data, signature));
+ Assert.True(this.Verify(data, signature));
}
[Fact]
@@ -48,7 +48,7 @@ public void NullCredential()
private bool Verify(byte[] data, byte[] signature)
{
var x509cert = new X509Certificate2(File.ReadAllBytes("./resources/public_cert.pem"));
- var rsa = (RSA) x509cert.PublicKey.Key;
+ var rsa = (RSA)x509cert.PublicKey.Key;
return rsa.VerifyData(
data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj
index fd4c9036..8641fa7b 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj
@@ -5,6 +5,8 @@
false
../../FirebaseAdmin.snk
true
+ true
+ ../../stylecop_test.ruleset
@@ -13,6 +15,9 @@
+
+ all
+
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs
index cc525acd..5a453821 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs
@@ -14,13 +14,13 @@
using System;
using System.Threading.Tasks;
-using Xunit;
using FirebaseAdmin;
using Google.Apis.Auth.OAuth2;
+using Xunit;
namespace FirebaseAdmin.Tests
{
- public class FirebaseAppTest: IDisposable
+ public class FirebaseAppTest : IDisposable
{
private static readonly AppOptions TestOptions = new AppOptions()
{
@@ -96,7 +96,7 @@ public void CreateAppOptions()
};
var app = FirebaseApp.Create(options);
Assert.Equal("[DEFAULT]", app.Name);
-
+
var copy = app.Options;
Assert.NotSame(options, copy);
Assert.Same(credential, copy.Credential);
@@ -114,7 +114,7 @@ public void ServiceAccountCredentialScoping()
};
var app = FirebaseApp.Create(options);
Assert.Equal("[DEFAULT]", app.Name);
-
+
var copy = app.Options;
Assert.NotSame(options, copy);
Assert.NotSame(credential, copy.Credential);
@@ -139,7 +139,7 @@ public void ApplicationDefaultCredentials()
}
finally
{
- Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", "");
+ Environment.SetEnvironmentVariable("GOOGLE_APPLICATION_CREDENTIALS", string.Empty);
}
}
@@ -169,7 +169,7 @@ public void GetProjectIdFromServiceAccount()
[Fact]
public void GetProjectIdFromEnvironment()
{
- foreach (var name in new string[]{"GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"})
+ foreach (var name in new string[] { "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT" })
{
Environment.SetEnvironmentVariable(name, "env-project");
try
@@ -180,7 +180,7 @@ public void GetProjectIdFromEnvironment()
}
finally
{
- Environment.SetEnvironmentVariable(name, "");
+ Environment.SetEnvironmentVariable(name, string.Empty);
}
}
}
@@ -196,14 +196,15 @@ public void GetOrInitService()
var service1 = app.GetOrInit("MockService", factory);
var service2 = app.GetOrInit("MockService", factory);
Assert.Same(service1, service2);
- Assert.Throws(() => {
+ Assert.Throws(() =>
+ {
app.GetOrInit("MockService", () => { return new OtherMockService(); });
});
-
+
Assert.False(service1.Deleted);
app.Delete();
Assert.True(service1.Deleted);
- Assert.Throws(() =>
+ Assert.Throws(() =>
{
app.GetOrInit("MockService", factory);
});
@@ -211,22 +212,22 @@ public void GetOrInitService()
public void Dispose()
{
- FirebaseApp.DeleteAll();
+ FirebaseApp.DeleteAll();
}
}
- internal class MockService: IFirebaseService
+ internal class MockService : IFirebaseService
{
public bool Deleted { get; private set; }
public void Delete()
{
- Deleted = true;
+ this.Deleted = true;
}
}
- internal class OtherMockService: IFirebaseService
+ internal class OtherMockService : IFirebaseService
{
- public void Delete() {}
+ public void Delete() { }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs
new file mode 100644
index 00000000..50d459cf
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs
@@ -0,0 +1,116 @@
+// Copyright 2018, 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;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Tests;
+using Google.Apis.Auth.OAuth2;
+using Google.Apis.Http;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace FirebaseAdmin.Messaging.Tests
+{
+ public class FirebaseMessagingClientTest
+ {
+ private static readonly GoogleCredential MockCredential =
+ GoogleCredential.FromAccessToken("test-token");
+
+ [Fact]
+ public void NoProjectId()
+ {
+ var clientFactory = new HttpClientFactory();
+ Assert.Throws(
+ () => new FirebaseMessagingClient(clientFactory, MockCredential, null));
+ Assert.Throws(
+ () => new FirebaseMessagingClient(clientFactory, MockCredential, string.Empty));
+ }
+
+ [Fact]
+ public void NoCredential()
+ {
+ var clientFactory = new HttpClientFactory();
+ Assert.Throws(
+ () => new FirebaseMessagingClient(clientFactory, null, "test-project"));
+ }
+
+ [Fact]
+ public void NoClientFactory()
+ {
+ var clientFactory = new HttpClientFactory();
+ Assert.Throws(
+ () => new FirebaseMessagingClient(null, MockCredential, "test-project"));
+ }
+
+ [Fact]
+ public async Task SendAsync()
+ {
+ var handler = new MockMessageHandler()
+ {
+ Response = new FirebaseMessagingClient.SendResponse()
+ {
+ Name = "test-response",
+ },
+ };
+ var factory = new MockHttpClientFactory(handler);
+ var client = new FirebaseMessagingClient(factory, MockCredential, "test-project");
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ };
+ var response = await client.SendAsync(message);
+ Assert.Equal("test-response", response);
+ var req = JsonConvert.DeserializeObject(
+ handler.Request);
+ Assert.Equal("test-topic", req.Message.Topic);
+ Assert.False(req.ValidateOnly);
+ Assert.Equal(1, handler.Calls);
+
+ // Send in dryRun mode.
+ response = await client.SendAsync(message, dryRun: true);
+ Assert.Equal("test-response", response);
+ req = JsonConvert.DeserializeObject(
+ handler.Request);
+ Assert.Equal("test-topic", req.Message.Topic);
+ Assert.True(req.ValidateOnly);
+ Assert.Equal(2, handler.Calls);
+ }
+
+ [Fact]
+ public async Task HttpErrorAsync()
+ {
+ var handler = new MockMessageHandler()
+ {
+ StatusCode = HttpStatusCode.InternalServerError,
+ Response = "not json",
+ };
+ var factory = new MockHttpClientFactory(handler);
+ var client = new FirebaseMessagingClient(factory, MockCredential, "test-project");
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ };
+ var ex = await Assert.ThrowsAsync(
+ async () => await client.SendAsync(message));
+ Assert.Contains("not json", ex.Message);
+ var req = JsonConvert.DeserializeObject(
+ handler.Request);
+ Assert.Equal("test-topic", req.Message.Topic);
+ Assert.False(req.ValidateOnly);
+ Assert.Equal(1, handler.Calls);
+ }
+ }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs
new file mode 100644
index 00000000..363ac3c7
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs
@@ -0,0 +1,84 @@
+// Copyright 2018, 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;
+using System.Threading;
+using System.Threading.Tasks;
+using FirebaseAdmin.Tests;
+using Google.Apis.Auth.OAuth2;
+using Xunit;
+
+namespace FirebaseAdmin.Messaging.Tests
+{
+ public class FirebaseMessagingTest : IDisposable
+ {
+ private static readonly GoogleCredential MockCredential =
+ GoogleCredential.FromFile("./resources/service_account.json");
+
+ [Fact]
+ public void GetMessagingWithoutApp()
+ {
+ Assert.Null(FirebaseMessaging.DefaultInstance);
+ }
+
+ [Fact]
+ public void GetDefaultMessaging()
+ {
+ var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
+ FirebaseMessaging messaging = FirebaseMessaging.DefaultInstance;
+ Assert.NotNull(messaging);
+ Assert.Same(messaging, FirebaseMessaging.DefaultInstance);
+ app.Delete();
+ Assert.Null(FirebaseMessaging.DefaultInstance);
+ }
+
+ [Fact]
+ public void GetMessaging()
+ {
+ var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential }, "MyApp");
+ FirebaseMessaging messaging = FirebaseMessaging.GetMessaging(app);
+ Assert.NotNull(messaging);
+ Assert.Same(messaging, FirebaseMessaging.GetMessaging(app));
+ app.Delete();
+ Assert.Throws(() => FirebaseMessaging.GetMessaging(app));
+ }
+
+ [Fact]
+ public async Task UseAfterDelete()
+ {
+ var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential });
+ FirebaseMessaging messaging = FirebaseMessaging.DefaultInstance;
+ app.Delete();
+ await Assert.ThrowsAsync(
+ async () => await messaging.SendAsync(new Message() { Topic = "test-topic" }));
+ }
+
+ [Fact]
+ public async Task SendMessageCancel()
+ {
+ var cred = GoogleCredential.FromFile("./resources/service_account.json");
+ FirebaseApp.Create(new AppOptions() { Credential = cred });
+ var canceller = new CancellationTokenSource();
+ canceller.Cancel();
+ await Assert.ThrowsAsync(
+ async () => await FirebaseMessaging.DefaultInstance.SendAsync(
+ new Message() { Topic = "test-topic" }, canceller.Token));
+ }
+
+ public void Dispose()
+ {
+ FirebaseApp.DeleteAll();
+ }
+ }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs
new file mode 100644
index 00000000..eaee0d05
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs
@@ -0,0 +1,1570 @@
+// Copyright 2018, 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;
+using System.Collections.Generic;
+using System.Linq;
+using Google.Apis.Json;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace FirebaseAdmin.Messaging.Tests
+{
+ public class MessageTest
+ {
+ [Fact]
+ public void EmptyMessage()
+ {
+ var message = new Message() { Token = "test-token" };
+ this.AssertJsonEquals(new JObject() { { "token", "test-token" } }, message);
+
+ message = new Message() { Topic = "test-topic" };
+ this.AssertJsonEquals(new JObject() { { "topic", "test-topic" } }, message);
+
+ message = new Message() { Condition = "test-condition" };
+ this.AssertJsonEquals(new JObject() { { "condition", "test-condition" } }, message);
+ }
+
+ [Fact]
+ public void PrefixedTopicName()
+ {
+ var message = new Message() { Topic = "/topics/test-topic" };
+ this.AssertJsonEquals(new JObject() { { "topic", "test-topic" } }, message);
+ }
+
+ [Fact]
+ public void DataMessage()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Data = new Dictionary()
+ {
+ { "k1", "v1" },
+ { "k2", "v2" },
+ },
+ };
+ this.AssertJsonEquals(
+ new JObject()
+ {
+ { "topic", "test-topic" },
+ { "data", new JObject() { { "k1", "v1" }, { "k2", "v2" } } },
+ }, message);
+ }
+
+ [Fact]
+ public void Notification()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Notification = new Notification()
+ {
+ Title = "title",
+ Body = "body",
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "notification", new JObject()
+ {
+ { "title", "title" },
+ { "body", "body" },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void MessageDeserialization()
+ {
+ var original = new Message()
+ {
+ Topic = "test-topic",
+ Data = new Dictionary() { { "key", "value" } },
+ Notification = new Notification()
+ {
+ Title = "title",
+ Body = "body",
+ },
+ Android = new AndroidConfig()
+ {
+ RestrictedPackageName = "test-pkg-name",
+ },
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ AlertString = "test-alert",
+ },
+ },
+ Webpush = new WebpushConfig()
+ {
+ Data = new Dictionary() { { "key", "value" } },
+ },
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.Topic, copy.Topic);
+ Assert.Equal(original.Data, copy.Data);
+ Assert.Equal(original.Notification.Title, copy.Notification.Title);
+ Assert.Equal(original.Notification.Body, copy.Notification.Body);
+ Assert.Equal(
+ original.Android.RestrictedPackageName, copy.Android.RestrictedPackageName);
+ Assert.Equal(original.Apns.Aps.AlertString, copy.Apns.Aps.AlertString);
+ Assert.Equal(original.Webpush.Data, copy.Webpush.Data);
+ }
+
+ [Fact]
+ public void MessageCopy()
+ {
+ var original = new Message()
+ {
+ Topic = "test-topic",
+ Data = new Dictionary(),
+ Notification = new Notification(),
+ Android = new AndroidConfig(),
+ Apns = new ApnsConfig(),
+ Webpush = new WebpushConfig(),
+ };
+ var copy = original.CopyAndValidate();
+ Assert.NotSame(original, copy);
+ Assert.NotSame(original.Data, copy.Data);
+ Assert.NotSame(original.Notification, copy.Notification);
+ Assert.NotSame(original.Android, copy.Android);
+ Assert.NotSame(original.Apns, copy.Apns);
+ Assert.NotSame(original.Webpush, copy.Webpush);
+ }
+
+ [Fact]
+ public void MessageWithoutTarget()
+ {
+ Assert.Throws(() => new Message().CopyAndValidate());
+ }
+
+ [Fact]
+ public void MultipleTargets()
+ {
+ var message = new Message()
+ {
+ Token = "test-token",
+ Topic = "test-topic",
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+
+ message = new Message()
+ {
+ Token = "test-token",
+ Condition = "test-condition",
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+
+ message = new Message()
+ {
+ Condition = "test-condition",
+ Topic = "test-topic",
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+
+ message = new Message()
+ {
+ Token = "test-token",
+ Topic = "test-topic",
+ Condition = "test-condition",
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void InvalidTopicNames()
+ {
+ var topics = new List()
+ {
+ "/topics/", "/foo/bar", "foo bar",
+ };
+ foreach (var topic in topics)
+ {
+ var message = new Message() { Topic = topic };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+ }
+
+ [Fact]
+ public void AndroidConfig()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Android = new AndroidConfig()
+ {
+ CollapseKey = "collapse-key",
+ Priority = Priority.High,
+ TimeToLive = TimeSpan.FromMilliseconds(10),
+ RestrictedPackageName = "test-pkg-name",
+ Data = new Dictionary()
+ {
+ { "k1", "v1" },
+ { "k2", "v2" },
+ },
+ Notification = new AndroidNotification()
+ {
+ Title = "title",
+ Body = "body",
+ Icon = "icon",
+ Color = "#112233",
+ Sound = "sound",
+ Tag = "tag",
+ ClickAction = "click-action",
+ TitleLocKey = "title-loc-key",
+ TitleLocArgs = new List() { "arg1", "arg2" },
+ BodyLocKey = "body-loc-key",
+ BodyLocArgs = new List() { "arg3", "arg4" },
+ ChannelId = "channel-id",
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "android", new JObject()
+ {
+ { "collapse_key", "collapse-key" },
+ { "priority", "high" },
+ { "ttl", "0.010000000s" },
+ { "restricted_package_name", "test-pkg-name" },
+ { "data", new JObject() { { "k1", "v1" }, { "k2", "v2" } } },
+ {
+ "notification", new JObject()
+ {
+ { "title", "title" },
+ { "body", "body" },
+ { "icon", "icon" },
+ { "color", "#112233" },
+ { "sound", "sound" },
+ { "tag", "tag" },
+ { "click_action", "click-action" },
+ { "title_loc_key", "title-loc-key" },
+ { "title_loc_args", new JArray() { "arg1", "arg2" } },
+ { "body_loc_key", "body-loc-key" },
+ { "body_loc_args", new JArray() { "arg3", "arg4" } },
+ { "channel_id", "channel-id" },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void AndroidConfigMinimal()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Android = new AndroidConfig(),
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ { "android", new JObject() },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void AndroidConfigFullSecondsTTL()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Android = new AndroidConfig()
+ {
+ TimeToLive = TimeSpan.FromHours(1),
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "android", new JObject()
+ {
+ { "ttl", "3600s" },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void AndroidConfigDeserialization()
+ {
+ var original = new AndroidConfig()
+ {
+ CollapseKey = "collapse-key",
+ RestrictedPackageName = "test-pkg-name",
+ TimeToLive = TimeSpan.FromSeconds(10.5),
+ Priority = Priority.High,
+ Data = new Dictionary()
+ {
+ { "key", "value" },
+ },
+ Notification = new AndroidNotification()
+ {
+ Title = "title",
+ },
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.CollapseKey, copy.CollapseKey);
+ Assert.Equal(original.RestrictedPackageName, copy.RestrictedPackageName);
+ Assert.Equal(original.Priority, copy.Priority);
+ Assert.Equal(original.TimeToLive, copy.TimeToLive);
+ Assert.Equal(original.Data, copy.Data);
+ Assert.Equal(original.Notification.Title, copy.Notification.Title);
+ }
+
+ [Fact]
+ public void AndroidConfigCopy()
+ {
+ var original = new AndroidConfig()
+ {
+ Data = new Dictionary(),
+ Notification = new AndroidNotification(),
+ };
+ var copy = original.CopyAndValidate();
+ Assert.NotSame(original, copy);
+ Assert.NotSame(original.Data, copy.Data);
+ Assert.NotSame(original.Notification, copy.Notification);
+ }
+
+ [Fact]
+ public void AndroidNotificationDeserialization()
+ {
+ var original = new AndroidNotification()
+ {
+ Title = "title",
+ Body = "body",
+ Icon = "icon",
+ Color = "#112233",
+ Sound = "sound",
+ Tag = "tag",
+ ClickAction = "click-action",
+ TitleLocKey = "title-loc-key",
+ TitleLocArgs = new List() { "arg1", "arg2" },
+ BodyLocKey = "body-loc-key",
+ BodyLocArgs = new List() { "arg3", "arg4" },
+ ChannelId = "channel-id",
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.Title, copy.Title);
+ Assert.Equal(original.Body, copy.Body);
+ Assert.Equal(original.Icon, copy.Icon);
+ Assert.Equal(original.Color, copy.Color);
+ Assert.Equal(original.Sound, copy.Sound);
+ Assert.Equal(original.Tag, copy.Tag);
+ Assert.Equal(original.ClickAction, copy.ClickAction);
+ Assert.Equal(original.TitleLocKey, copy.TitleLocKey);
+ Assert.Equal(original.TitleLocArgs, copy.TitleLocArgs);
+ Assert.Equal(original.BodyLocKey, copy.BodyLocKey);
+ Assert.Equal(original.BodyLocArgs, copy.BodyLocArgs);
+ Assert.Equal(original.ChannelId, copy.ChannelId);
+ }
+
+ [Fact]
+ public void AndroidNotificationCopy()
+ {
+ var original = new AndroidNotification()
+ {
+ TitleLocKey = "title-loc-key",
+ TitleLocArgs = new List() { "arg1", "arg2" },
+ BodyLocKey = "body-loc-key",
+ BodyLocArgs = new List() { "arg3", "arg4" },
+ };
+ var copy = original.CopyAndValidate();
+ Assert.NotSame(original, copy);
+ Assert.NotSame(original.TitleLocArgs, copy.TitleLocArgs);
+ Assert.NotSame(original.BodyLocArgs, copy.BodyLocArgs);
+ }
+
+ [Fact]
+ public void AndroidConfigInvalidTTL()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Android = new AndroidConfig()
+ {
+ TimeToLive = TimeSpan.FromHours(-1),
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void AndroidNotificationInvalidColor()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Android = new AndroidConfig()
+ {
+ Notification = new AndroidNotification()
+ {
+ Color = "not-a-color",
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void AndroidNotificationInvalidTitleLocArgs()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Android = new AndroidConfig()
+ {
+ Notification = new AndroidNotification()
+ {
+ TitleLocArgs = new List() { "arg" },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void AndroidNotificationInvalidBodyLocArgs()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Android = new AndroidConfig()
+ {
+ Notification = new AndroidNotification()
+ {
+ BodyLocArgs = new List() { "arg" },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void WebpushConfig()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Webpush = new WebpushConfig()
+ {
+ Headers = new Dictionary()
+ {
+ { "header1", "header-value1" },
+ { "header2", "header-value2" },
+ },
+ Data = new Dictionary()
+ {
+ { "key1", "value1" },
+ { "key2", "value2" },
+ },
+ Notification = new WebpushNotification()
+ {
+ Title = "title",
+ Body = "body",
+ Icon = "icon",
+ Badge = "badge",
+ Data = new Dictionary()
+ {
+ { "some", "data" },
+ },
+ Direction = Direction.LeftToRight,
+ Image = "image",
+ Language = "language",
+ Tag = "tag",
+ Silent = true,
+ RequireInteraction = true,
+ Renotify = true,
+ TimestampMillis = 100,
+ Vibrate = new int[] { 10, 5, 10 },
+ Actions = new List()
+ {
+ new Action()
+ {
+ ActionName = "Accept",
+ Title = "Ok",
+ Icon = "ok-button",
+ },
+ new Action()
+ {
+ ActionName = "Reject",
+ Title = "Cancel",
+ Icon = "cancel-button",
+ },
+ },
+ CustomData = new Dictionary()
+ {
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ },
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "webpush", new JObject()
+ {
+ {
+ "headers", new JObject()
+ {
+ { "header1", "header-value1" },
+ { "header2", "header-value2" },
+ }
+ },
+ {
+ "data", new JObject()
+ {
+ { "key1", "value1" },
+ { "key2", "value2" },
+ }
+ },
+ {
+ "notification", new JObject()
+ {
+ { "title", "title" },
+ { "body", "body" },
+ { "icon", "icon" },
+ { "badge", "badge" },
+ {
+ "data", new JObject()
+ {
+ { "some", "data" },
+ }
+ },
+ { "dir", "ltr" },
+ { "image", "image" },
+ { "lang", "language" },
+ { "renotify", true },
+ { "requireInteraction", true },
+ { "silent", true },
+ { "tag", "tag" },
+ { "timestamp", 100 },
+ { "vibrate", new JArray() { 10, 5, 10 } },
+ {
+ "actions", new JArray()
+ {
+ new JObject()
+ {
+ { "action", "Accept" },
+ { "title", "Ok" },
+ { "icon", "ok-button" },
+ },
+ new JObject()
+ {
+ { "action", "Reject" },
+ { "title", "Cancel" },
+ { "icon", "cancel-button" },
+ },
+ }
+ },
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void WebpushConfigMinimal()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Webpush = new WebpushConfig(),
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ { "webpush", new JObject() },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void WebpushConfigMinimalNotification()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Webpush = new WebpushConfig()
+ {
+ Notification = new WebpushNotification()
+ {
+ Title = "title",
+ Body = "body",
+ Icon = "icon",
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "webpush", new JObject()
+ {
+ {
+ "notification", new JObject()
+ {
+ { "title", "title" },
+ { "body", "body" },
+ { "icon", "icon" },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void WebpushConfigDeserialization()
+ {
+ var original = new WebpushConfig()
+ {
+ Headers = new Dictionary()
+ {
+ { "header1", "header-value1" },
+ { "header2", "header-value2" },
+ },
+ Data = new Dictionary()
+ {
+ { "key1", "value1" },
+ { "key2", "value2" },
+ },
+ Notification = new WebpushNotification()
+ {
+ Title = "title",
+ },
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.Headers, copy.Headers);
+ Assert.Equal(original.Data, copy.Data);
+ Assert.Equal(original.Notification.Title, copy.Notification.Title);
+ }
+
+ [Fact]
+ public void WebpushConfigCopy()
+ {
+ var original = new WebpushConfig()
+ {
+ Headers = new Dictionary(),
+ Data = new Dictionary(),
+ Notification = new WebpushNotification(),
+ };
+ var copy = original.CopyAndValidate();
+ Assert.NotSame(original, copy);
+ Assert.NotSame(original.Headers, copy.Headers);
+ Assert.NotSame(original.Data, copy.Data);
+ Assert.NotSame(original.Notification, copy.Notification);
+ }
+
+ [Fact]
+ public void WebpushNotificationDeserialization()
+ {
+ var original = new WebpushNotification()
+ {
+ Title = "title",
+ Body = "body",
+ Icon = "icon",
+ Badge = "badge",
+ Data = new Dictionary()
+ {
+ { "some", "data" },
+ },
+ Direction = Direction.LeftToRight,
+ Image = "image",
+ Language = "language",
+ Tag = "tag",
+ Silent = true,
+ RequireInteraction = true,
+ Renotify = true,
+ TimestampMillis = 100,
+ Vibrate = new int[] { 10, 5, 10 },
+ Actions = new List()
+ {
+ new Action()
+ {
+ ActionName = "Accept",
+ Title = "Ok",
+ Icon = "ok-button",
+ },
+ new Action()
+ {
+ ActionName = "Reject",
+ Title = "Cancel",
+ Icon = "cancel-button",
+ },
+ },
+ CustomData = new Dictionary()
+ {
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ },
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.Title, copy.Title);
+ Assert.Equal(original.Body, copy.Body);
+ Assert.Equal(original.Icon, copy.Icon);
+ Assert.Equal(original.Badge, copy.Badge);
+ Assert.Equal(new JObject() { { "some", "data" } }, copy.Data);
+ Assert.Equal(original.Direction, copy.Direction);
+ Assert.Equal(original.Image, copy.Image);
+ Assert.Equal(original.Language, copy.Language);
+ Assert.Equal(original.Tag, copy.Tag);
+ Assert.Equal(original.Silent, copy.Silent);
+ Assert.Equal(original.RequireInteraction, copy.RequireInteraction);
+ Assert.Equal(original.Renotify, copy.Renotify);
+ Assert.Equal(original.TimestampMillis, copy.TimestampMillis);
+ Assert.Equal(original.Vibrate, copy.Vibrate);
+ var originalActions = original.Actions.ToList();
+ var copyActions = original.Actions.ToList();
+ Assert.Equal(originalActions.Count, copyActions.Count);
+ for (int i = 0; i < originalActions.Count; i++)
+ {
+ Assert.Equal(originalActions[i].ActionName, copyActions[i].ActionName);
+ Assert.Equal(originalActions[i].Title, copyActions[i].Title);
+ Assert.Equal(originalActions[i].Icon, copyActions[i].Icon);
+ }
+
+ Assert.Equal(original.CustomData, copy.CustomData);
+ }
+
+ [Fact]
+ public void WebpushNotificationCopy()
+ {
+ var original = new WebpushNotification()
+ {
+ Actions = new List()
+ {
+ new Action()
+ {
+ ActionName = "Accept",
+ Title = "Ok",
+ Icon = "ok-button",
+ },
+ },
+ CustomData = new Dictionary()
+ {
+ { "custom-key1", "custom-data" },
+ },
+ };
+ var copy = original.CopyAndValidate();
+ Assert.NotSame(original, copy);
+ Assert.NotSame(original.Actions, copy.Actions);
+ Assert.NotSame(original.Actions.First(), copy.Actions.First());
+ Assert.NotSame(original.CustomData, copy.CustomData);
+ Assert.Equal(original.CustomData, copy.CustomData);
+ }
+
+ [Fact]
+ public void WebpushNotificationDuplicateKeys()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Webpush = new WebpushConfig()
+ {
+ Notification = new WebpushNotification()
+ {
+ Title = "title",
+ CustomData = new Dictionary() { { "title", "other" } },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsConfig()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Headers = new Dictionary()
+ {
+ { "k1", "v1" },
+ { "k2", "v2" },
+ },
+ Aps = new Aps()
+ {
+ AlertString = "alert-text",
+ Badge = 0,
+ Category = "test-category",
+ ContentAvailable = true,
+ MutableContent = true,
+ Sound = "sound-file",
+ ThreadId = "test-thread",
+ CustomData = new Dictionary()
+ {
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ },
+ },
+ CustomData = new Dictionary()
+ {
+ { "custom-key3", "custom-data" },
+ { "custom-key4", true },
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "apns", new JObject()
+ {
+ {
+ "headers", new JObject()
+ {
+ { "k1", "v1" },
+ { "k2", "v2" },
+ }
+ },
+ {
+ "payload", new JObject()
+ {
+ {
+ "aps", new JObject()
+ {
+ { "alert", "alert-text" },
+ { "badge", 0 },
+ { "category", "test-category" },
+ { "content-available", 1 },
+ { "mutable-content", 1 },
+ { "sound", "sound-file" },
+ { "thread-id", "test-thread" },
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ }
+ },
+ { "custom-key3", "custom-data" },
+ { "custom-key4", true },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void ApnsConfigMinimal()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps(),
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "apns", new JObject()
+ {
+ {
+ "payload", new JObject()
+ {
+ { "aps", new JObject() },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void ApnsConfigDeserialization()
+ {
+ var original = new ApnsConfig()
+ {
+ Headers = new Dictionary()
+ {
+ { "k1", "v1" },
+ { "k2", "v2" },
+ },
+ Aps = new Aps()
+ {
+ AlertString = "alert-text",
+ },
+ CustomData = new Dictionary()
+ {
+ { "custom-key3", "custom-data" },
+ { "custom-key4", true },
+ },
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.Headers, copy.Headers);
+ Assert.Equal(original.CustomData, copy.CustomData);
+ Assert.Equal(original.Aps.AlertString, copy.Aps.AlertString);
+ }
+
+ [Fact]
+ public void ApnsConfigCopy()
+ {
+ var original = new ApnsConfig()
+ {
+ Headers = new Dictionary(),
+ Aps = new Aps(),
+ CustomData = new Dictionary(),
+ };
+ var copy = original.CopyAndValidate();
+ Assert.NotSame(original, copy);
+ Assert.NotSame(original.Headers, copy.Headers);
+ Assert.NotSame(original.Aps, copy.Aps);
+ Assert.NotSame(original.CustomData, copy.CustomData);
+ }
+
+ [Fact]
+ public void ApnsConfigCustomApsDeserialization()
+ {
+ var original = new ApnsConfig()
+ {
+ Headers = new Dictionary()
+ {
+ { "k1", "v1" },
+ { "k2", "v2" },
+ },
+ CustomData = new Dictionary()
+ {
+ {
+ "aps", new Dictionary()
+ {
+ { "alert", "alert-text" },
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ }
+ },
+ { "custom-key3", "custom-data" },
+ { "custom-key4", true },
+ },
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.Headers, copy.Headers);
+ original.CustomData.Remove("aps");
+ Assert.Equal(original.CustomData, copy.CustomData);
+ Assert.Equal("alert-text", copy.Aps.AlertString);
+ var customApsData = new Dictionary()
+ {
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ };
+ Assert.Equal(customApsData, copy.Aps.CustomData);
+ }
+
+ [Fact]
+ public void ApnsCriticalSound()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ CriticalSound = new CriticalSound()
+ {
+ Name = "default",
+ Critical = true,
+ Volume = 0.5,
+ },
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "apns", new JObject()
+ {
+ {
+ "payload", new JObject()
+ {
+ {
+ "aps", new JObject()
+ {
+ {
+ "sound", new JObject()
+ {
+ { "name", "default" },
+ { "critical", 1 },
+ { "volume", 0.5 },
+ }
+ },
+ }
+ },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void ApnsCriticalSoundMinimal()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ CriticalSound = new CriticalSound() { Name = "default" },
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "apns", new JObject()
+ {
+ {
+ "payload", new JObject()
+ {
+ {
+ "aps", new JObject()
+ {
+ {
+ "sound", new JObject()
+ {
+ { "name", "default" },
+ }
+ },
+ }
+ },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void ApnsCriticalSoundDeserialization()
+ {
+ var original = new CriticalSound()
+ {
+ Name = "default",
+ Volume = 0.5,
+ Critical = true,
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.Name, copy.Name);
+ Assert.Equal(original.Volume.Value, copy.Volume.Value);
+ Assert.Equal(original.Critical, copy.Critical);
+ }
+
+ [Fact]
+ public void ApnsApsAlert()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ Alert = new ApsAlert()
+ {
+ ActionLocKey = "action-key",
+ Body = "test-body",
+ LaunchImage = "test-image",
+ LocArgs = new List() { "arg1", "arg2" },
+ LocKey = "loc-key",
+ Subtitle = "test-subtitle",
+ SubtitleLocArgs = new List() { "arg3", "arg4" },
+ SubtitleLocKey = "subtitle-key",
+ Title = "test-title",
+ TitleLocArgs = new List() { "arg5", "arg6" },
+ TitleLocKey = "title-key",
+ },
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "apns", new JObject()
+ {
+ {
+ "payload", new JObject()
+ {
+ {
+ "aps", new JObject()
+ {
+ {
+ "alert", new JObject()
+ {
+ { "action-loc-key", "action-key" },
+ { "body", "test-body" },
+ { "launch-image", "test-image" },
+ { "loc-args", new JArray() { "arg1", "arg2" } },
+ { "loc-key", "loc-key" },
+ { "subtitle", "test-subtitle" },
+ { "subtitle-loc-args", new JArray() { "arg3", "arg4" } },
+ { "subtitle-loc-key", "subtitle-key" },
+ { "title", "test-title" },
+ { "title-loc-args", new JArray() { "arg5", "arg6" } },
+ { "title-loc-key", "title-key" },
+ }
+ },
+ }
+ },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void ApnsApsAlertMinimal()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ Alert = new ApsAlert(),
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "apns", new JObject()
+ {
+ {
+ "payload", new JObject()
+ {
+ {
+ "aps", new JObject()
+ {
+ {
+ "alert", new JObject()
+ },
+ }
+ },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void ApsAlertDeserialization()
+ {
+ var original = new ApsAlert()
+ {
+ ActionLocKey = "action-key",
+ Body = "test-body",
+ LaunchImage = "test-image",
+ LocArgs = new List() { "arg1", "arg2" },
+ LocKey = "loc-key",
+ Subtitle = "test-subtitle",
+ SubtitleLocArgs = new List() { "arg3", "arg4" },
+ SubtitleLocKey = "subtitle-key",
+ Title = "test-title",
+ TitleLocArgs = new List() { "arg5", "arg6" },
+ TitleLocKey = "title-key",
+ };
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(original);
+ var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json);
+ Assert.Equal(original.ActionLocKey, copy.ActionLocKey);
+ Assert.Equal(original.Body, copy.Body);
+ Assert.Equal(original.LaunchImage, copy.LaunchImage);
+ Assert.Equal(original.LocArgs, copy.LocArgs);
+ Assert.Equal(original.LocKey, copy.LocKey);
+ Assert.Equal(original.Subtitle, copy.Subtitle);
+ Assert.Equal(original.SubtitleLocArgs, copy.SubtitleLocArgs);
+ Assert.Equal(original.SubtitleLocKey, copy.SubtitleLocKey);
+ Assert.Equal(original.Title, copy.Title);
+ Assert.Equal(original.TitleLocArgs, copy.TitleLocArgs);
+ Assert.Equal(original.TitleLocKey, copy.TitleLocKey);
+ }
+
+ [Fact]
+ public void ApsAlertCopy()
+ {
+ var original = new ApsAlert()
+ {
+ LocArgs = new List() { "arg1", "arg2" },
+ LocKey = "loc-key",
+ SubtitleLocArgs = new List() { "arg3", "arg4" },
+ SubtitleLocKey = "subtitle-key",
+ TitleLocArgs = new List() { "arg5", "arg6" },
+ TitleLocKey = "title-key",
+ };
+ var copy = original.CopyAndValidate();
+ Assert.NotSame(original, copy);
+ Assert.NotSame(original.LocArgs, copy.LocArgs);
+ Assert.NotSame(original.SubtitleLocArgs, copy.SubtitleLocArgs);
+ Assert.NotSame(original.TitleLocArgs, copy.TitleLocArgs);
+ }
+
+ [Fact]
+ public void ApnsApsAlertInvalidTitleLocArgs()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ Alert = new ApsAlert()
+ {
+ TitleLocArgs = new List() { "arg1", "arg2" },
+ },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsApsAlertInvalidSubtitleLocArgs()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ Alert = new ApsAlert()
+ {
+ SubtitleLocArgs = new List() { "arg1", "arg2" },
+ },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsApsAlertInvalidLocArgs()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ Alert = new ApsAlert()
+ {
+ LocArgs = new List() { "arg1", "arg2" },
+ },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsCustomApsWithStandardProperties()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ CustomData = new Dictionary()
+ {
+ {
+ "aps", new Dictionary()
+ {
+ { "alert", "alert-text" },
+ { "badge", 42 },
+ }
+ },
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "apns", new JObject()
+ {
+ {
+ "payload", new JObject()
+ {
+ {
+ "aps", new JObject()
+ {
+ { "alert", "alert-text" },
+ { "badge", 42 },
+ }
+ },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void ApnsCustomApsWithCustomProperties()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ CustomData = new Dictionary()
+ {
+ {
+ "aps", new Dictionary()
+ {
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ }
+ },
+ },
+ },
+ };
+ var expected = new JObject()
+ {
+ { "topic", "test-topic" },
+ {
+ "apns", new JObject()
+ {
+ {
+ "payload", new JObject()
+ {
+ {
+ "aps", new JObject()
+ {
+ { "custom-key1", "custom-data" },
+ { "custom-key2", true },
+ }
+ },
+ }
+ },
+ }
+ },
+ };
+ this.AssertJsonEquals(expected, message);
+ }
+
+ [Fact]
+ public void ApnsNoAps()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ CustomData = new Dictionary()
+ {
+ { "test", "custom-data" },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsDuplicateAps()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ AlertString = "alert-text",
+ },
+ CustomData = new Dictionary()
+ {
+ { "aps", "custom-data" },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApsDuplicateKeys()
+ {
+ var aps = new Aps()
+ {
+ AlertString = "alert-text",
+ CustomData = new Dictionary()
+ {
+ { "alert", "other-alert-text" },
+ },
+ };
+ Assert.Throws(() => aps.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsDuplicateApsAlerts()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ AlertString = "alert-text",
+ Alert = new ApsAlert()
+ {
+ Body = "other-alert-text",
+ },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsDuplicateApsSounds()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ Sound = "default",
+ CriticalSound = new CriticalSound()
+ {
+ Name = "other=sound",
+ },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsInvalidCriticalSoundNoName()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ CriticalSound = new CriticalSound(),
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsInvalidCriticalSoundVolumeTooLow()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ CriticalSound = new CriticalSound()
+ {
+ Name = "default",
+ Volume = -0.1,
+ },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ [Fact]
+ public void ApnsInvalidCriticalSoundVolumeTooHigh()
+ {
+ var message = new Message()
+ {
+ Topic = "test-topic",
+ Apns = new ApnsConfig()
+ {
+ Aps = new Aps()
+ {
+ CriticalSound = new CriticalSound()
+ {
+ Name = "default",
+ Volume = 1.1,
+ },
+ },
+ },
+ };
+ Assert.Throws(() => message.CopyAndValidate());
+ }
+
+ private void AssertJsonEquals(JObject expected, Message actual)
+ {
+ var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate());
+ var parsed = JObject.Parse(json);
+ Assert.True(
+ JToken.DeepEquals(expected, parsed),
+ $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}");
+ }
+ }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs
index d4fb09e6..f68f7785 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs
@@ -19,36 +19,37 @@ namespace FirebaseAdmin.Tests
{
public class MockClock : IClock
{
- public DateTime Now
+ private object mutex = new object();
+ private DateTime utcNow;
+
+ public MockClock()
{
- get { return UtcNow.ToLocalTime(); }
- set { UtcNow = value.ToUniversalTime(); }
+ this.Now = DateTime.Now;
}
- private object _lock = new object();
- private DateTime _utcNow;
+ public DateTime Now
+ {
+ get { return this.UtcNow.ToLocalTime(); }
+ set { this.UtcNow = value.ToUniversalTime(); }
+ }
public DateTime UtcNow
{
get
{
- lock (_lock)
+ lock (this.mutex)
{
- return _utcNow;
+ return this.utcNow;
}
}
+
set
{
- lock (_lock)
+ lock (this.mutex)
{
- _utcNow = value;
+ this.utcNow = value;
}
}
}
-
- public MockClock()
- {
- Now = DateTime.Now;
- }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs
index 7bc4e209..b6456e79 100644
--- a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs
+++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs
@@ -26,99 +26,103 @@
namespace FirebaseAdmin.Tests
{
- internal class MockHttpClientFactory : HttpClientFactory
- {
- private HttpMessageHandler Handler { get; set; }
-
- public MockHttpClientFactory(HttpMessageHandler handler)
- {
- Handler = handler;
- }
-
- protected override HttpMessageHandler CreateHandler(CreateHttpClientArgs args)
- {
- return Handler;
- }
- }
-
///
/// An implementation that counts the number of requests
- /// processed.
+ /// and facilitates mocking HTTP interactions locally.
///
- internal abstract class CountableMessageHandler : HttpMessageHandler
+ internal class MockMessageHandler : CountableMessageHandler
{
- private int _calls;
-
- public int Calls
- {
- get { return _calls; }
- }
-
- sealed protected override Task SendAsync(
- HttpRequestMessage request, CancellationToken cancellationToken)
+ public MockMessageHandler()
{
- Interlocked.Increment(ref _calls);
- return SendAsyncCore(request, cancellationToken);
+ this.StatusCode = HttpStatusCode.OK;
}
- protected abstract Task SendAsyncCore(
- HttpRequestMessage request, CancellationToken cancellationToken);
- }
+ public delegate void SetHeaders(HttpResponseHeaders header);
- ///
- /// An implementation that counts the number of requests
- /// and facilitates mocking HTTP interactions locally.
- ///
- internal class MockMessageHandler : CountableMessageHandler
- {
public string Request { get; private set; }
-
+
public HttpStatusCode StatusCode { get; set; }
- public Object Response { get; set; }
- public delegate void SetHeaders(HttpResponseHeaders header);
+ public object Response { get; set; }
public SetHeaders ApplyHeaders { get; set; }
- public MockMessageHandler()
- {
- StatusCode = HttpStatusCode.OK;
- }
-
protected override async Task SendAsyncCore(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Content != null)
{
- Request = await request.Content.ReadAsStringAsync();
+ this.Request = await request.Content.ReadAsStringAsync();
}
else
{
- Request = null;
- }
+ this.Request = null;
+ }
+
var resp = new HttpResponseMessage();
string json;
- if (Response is byte[])
+ if (this.Response is byte[])
{
- json = Encoding.UTF8.GetString(Response as byte[]);
+ json = Encoding.UTF8.GetString(this.Response as byte[]);
}
- else if (Response is string)
+ else if (this.Response is string)
{
- json = Response as string;
+ json = this.Response as string;
}
else
{
- json = NewtonsoftJsonSerializer.Instance.Serialize(Response);
- }
- resp.StatusCode = StatusCode;
- if (ApplyHeaders != null)
+ json = NewtonsoftJsonSerializer.Instance.Serialize(this.Response);
+ }
+
+ resp.StatusCode = this.StatusCode;
+ if (this.ApplyHeaders != null)
{
- ApplyHeaders(resp.Headers);
+ this.ApplyHeaders(resp.Headers);
}
+
resp.Content = new StringContent(json, Encoding.UTF8, "application/json");
var tcs = new TaskCompletionSource();
tcs.SetResult(resp);
return await tcs.Task;
}
}
+
+ ///
+ /// An implementation that counts the number of requests
+ /// processed.
+ ///
+ internal abstract class CountableMessageHandler : HttpMessageHandler
+ {
+ private int calls;
+
+ public int Calls
+ {
+ get { return this.calls; }
+ }
+
+ protected sealed override Task SendAsync(
+ HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ Interlocked.Increment(ref this.calls);
+ return this.SendAsyncCore(request, cancellationToken);
+ }
+
+ protected abstract Task SendAsyncCore(
+ HttpRequestMessage request, CancellationToken cancellationToken);
+ }
+
+ internal class MockHttpClientFactory : HttpClientFactory
+ {
+ private readonly HttpMessageHandler handler;
+
+ public MockHttpClientFactory(HttpMessageHandler handler)
+ {
+ this.handler = handler;
+ }
+
+ protected override HttpMessageHandler CreateHandler(CreateHttpClientArgs args)
+ {
+ return this.handler;
+ }
+ }
}
diff --git a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs
index e5ff1fbc..9d529ac5 100644
--- a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs
+++ b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs
@@ -26,35 +26,37 @@ namespace FirebaseAdmin
public sealed class AppOptions
{
///
- /// used to authorize an app. All service calls made by
- /// the app will be authorized using this.
+ /// Initializes a new instance of the class.
+ ///
+ public AppOptions() { }
+
+ internal AppOptions(AppOptions options)
+ {
+ this.Credential = options.Credential;
+ this.ProjectId = options.ProjectId;
+ this.ServiceAccountId = options.ServiceAccountId;
+ }
+
+ ///
+ /// Gets or sets the used to authorize an app. All service
+ /// calls made by the app will be authorized using this.
///
public GoogleCredential Credential { get; set; }
///
- /// The Google Cloud Platform project ID that should be associated with an app.
+ /// Gets or sets the Google Cloud Platform project ID that should be associated with an
+ /// app.
///
public string ProjectId { get; set; }
///
- /// The unique ID of the service account that should be associated with an app.
+ /// Gets or sets the unique ID of the service account that should be associated with an
+ /// app.
/// This is used to
/// create custom auth tokens when service account credentials are not available. The
/// service account ID can be found in the client_email field of the service account
/// JSON.
///
public string ServiceAccountId { get; set; }
-
- ///
- /// Creates a new instance.
- ///
- public AppOptions() {}
-
- internal AppOptions(AppOptions options)
- {
- Credential = options.Credential;
- ProjectId = options.ProjectId;
- ServiceAccountId = options.ServiceAccountId;
- }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs
index bdea0d95..65910fbb 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs
@@ -23,24 +23,62 @@ namespace FirebaseAdmin.Auth
/// This is the entry point to all server-side Firebase Authentication operations. You can
/// get an instance of this class via FirebaseAuth.DefaultInstance.
///
- public sealed class FirebaseAuth: IFirebaseService
+ public sealed class FirebaseAuth : IFirebaseService
{
- private readonly FirebaseApp _app;
- private bool _deleted;
- private readonly Lazy _tokenFactory;
- private readonly Lazy _idTokenVerifier;
- private readonly Lazy _userManager;
- private readonly Object _lock = new Object();
+ private readonly FirebaseApp app;
+ private readonly Lazy tokenFactory;
+ private readonly Lazy idTokenVerifier;
+ private readonly Lazy userManager;
+ private readonly object authLock = new object();
+ private bool deleted;
private FirebaseAuth(FirebaseApp app)
{
- _app = app;
- _tokenFactory = new Lazy(() =>
- FirebaseTokenFactory.Create(_app), true);
- _idTokenVerifier = new Lazy(() =>
- FirebaseTokenVerifier.CreateIDTokenVerifier(_app), true);
- _userManager = new Lazy(() =>
- FirebaseUserManager.Create(_app));
+ this.app = app;
+ this.tokenFactory = new Lazy(
+ () => FirebaseTokenFactory.Create(this.app), true);
+ this.idTokenVerifier = new Lazy(
+ () => FirebaseTokenVerifier.CreateIDTokenVerifier(this.app), true);
+ this.userManager = new Lazy(() =>
+ FirebaseUserManager.Create(this.app));
+ }
+
+ ///
+ /// Gets the auth instance associated with the default Firebase app. This property is
+ /// null if the default app doesn't yet exist.
+ ///
+ public static FirebaseAuth DefaultInstance
+ {
+ get
+ {
+ var app = FirebaseApp.DefaultInstance;
+ if (app == null)
+ {
+ return null;
+ }
+
+ return GetAuth(app);
+ }
+ }
+
+ ///
+ /// Returns the auth instance for the specified app.
+ ///
+ /// The instance associated with the specified
+ /// app.
+ /// If the app argument is null.
+ /// An app instance.
+ public static FirebaseAuth GetAuth(FirebaseApp app)
+ {
+ if (app == null)
+ {
+ throw new ArgumentNullException("App argument must not be null.");
+ }
+
+ return app.GetOrInit(typeof(FirebaseAuth).Name, () =>
+ {
+ return new FirebaseAuth(app);
+ });
}
///
@@ -76,7 +114,7 @@ private FirebaseAuth(FirebaseApp app)
/// 128 characters.
public async Task CreateCustomTokenAsync(string uid)
{
- return await CreateCustomTokenAsync(uid, default(CancellationToken));
+ return await this.CreateCustomTokenAsync(uid, default(CancellationToken));
}
///
@@ -115,7 +153,7 @@ public async Task CreateCustomTokenAsync(string uid)
public async Task CreateCustomTokenAsync(
string uid, CancellationToken cancellationToken)
{
- return await CreateCustomTokenAsync(uid, null, cancellationToken);
+ return await this.CreateCustomTokenAsync(uid, null, cancellationToken);
}
///
@@ -142,7 +180,7 @@ public async Task CreateCustomTokenAsync(
public async Task CreateCustomTokenAsync(
string uid, IDictionary developerClaims)
{
- return await CreateCustomTokenAsync(uid, developerClaims, default(CancellationToken));
+ return await this.CreateCustomTokenAsync(uid, developerClaims, default(CancellationToken));
}
///
@@ -174,14 +212,16 @@ public async Task CreateCustomTokenAsync(
CancellationToken cancellationToken)
{
FirebaseTokenFactory tokenFactory;
- lock (_lock)
+ lock (this.authLock)
{
- if (_deleted)
+ if (this.deleted)
{
throw new InvalidOperationException("Cannot invoke after deleting the app.");
}
- tokenFactory = _tokenFactory.Value;
+
+ tokenFactory = this.tokenFactory.Value;
}
+
return await tokenFactory.CreateCustomTokenAsync(
uid, developerClaims, cancellationToken).ConfigureAwait(false);
}
@@ -204,7 +244,7 @@ public async Task CreateCustomTokenAsync(
/// A Firebase ID token string to parse and verify.
public async Task VerifyIdTokenAsync(string idToken)
{
- return await VerifyIdTokenAsync(idToken, default(CancellationToken));
+ return await this.VerifyIdTokenAsync(idToken, default(CancellationToken));
}
///
@@ -228,26 +268,28 @@ public async Task VerifyIdTokenAsync(string idToken)
public async Task VerifyIdTokenAsync(
string idToken, CancellationToken cancellationToken)
{
- lock (_lock)
+ lock (this.authLock)
{
- if (_deleted)
+ if (this.deleted)
{
throw new InvalidOperationException("Cannot invoke after deleting the app.");
}
}
- return await _idTokenVerifier.Value.VerifyTokenAsync(idToken, cancellationToken)
+
+ return await this.idTokenVerifier.Value.VerifyTokenAsync(idToken, cancellationToken)
.ConfigureAwait(false);
}
///
- /// Sets the specified custom claims on an existing user account. A null claims value
+ /// Sets the specified custom claims on an existing user account. A null claims value
/// removes any claims currently set on the user account. The claims should serialize into
/// a valid JSON string. The serialized claims must not be larger than 1000 characters.
///
+ /// A task that completes when the claims have been set.
/// If is null, empty or longer
- /// than 128 characters. Or, if the serialized is larger than 1000
+ /// than 128 characters. Or, if the serialized is larger than 1000
/// characters.
- /// The user ID string for the custom claims will be set. Must not be null
+ /// The user ID string for the custom claims will be set. Must not be null
/// or longer than 128 characters.
///
/// The claims to be stored on the user account, and made
@@ -255,9 +297,9 @@ public async Task VerifyIdTokenAsync(
/// serialization it should not be larger than 1000 characters.
public async Task SetCustomUserClaimsAsync(string uid, IReadOnlyDictionary claims)
{
- lock (_lock)
+ lock (this.authLock)
{
- if (_deleted)
+ if (this.deleted)
{
throw new InvalidOperationException("Cannot invoke after deleting the app.");
}
@@ -265,58 +307,30 @@ public async Task SetCustomUserClaimsAsync(string uid, IReadOnlyDictionary
+ /// Deletes this service instance.
+ ///
void IFirebaseService.Delete()
{
- lock (_lock)
+ lock (this.authLock)
{
- _deleted = true;
- if (_tokenFactory.IsValueCreated)
+ this.deleted = true;
+ if (this.tokenFactory.IsValueCreated)
{
- _tokenFactory.Value.Dispose();
+ this.tokenFactory.Value.Dispose();
}
- }
- }
- ///
- /// The auth instance associated with the default Firebase app. This property is
- /// null if the default app doesn't yet exist.
- ///
- public static FirebaseAuth DefaultInstance
- {
- get
- {
- var app = FirebaseApp.DefaultInstance;
- if (app == null)
+ if (this.userManager.IsValueCreated)
{
- return null;
+ this.userManager.Value.Dispose();
}
- return GetAuth(app);
}
}
-
- ///
- /// Returns the auth instance for the specified app.
- ///
- /// The instance associated with the specified
- /// app.
- /// If the app argument is null.
- /// An app instance.
- public static FirebaseAuth GetAuth(FirebaseApp app)
- {
- if (app == null)
- {
- throw new ArgumentNullException("App argument must not be null.");
- }
- return app.GetOrInit(typeof(FirebaseAuth).Name, () =>
- {
- return new FirebaseAuth(app);
- });
- }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
index e9c0c7e3..9cd4cc23 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs
@@ -23,73 +23,54 @@ namespace FirebaseAdmin.Auth
///
public sealed class FirebaseToken
{
+ internal FirebaseToken(FirebaseTokenArgs args)
+ {
+ this.Issuer = args.Issuer;
+ this.Subject = args.Subject;
+ this.Audience = args.Audience;
+ this.ExpirationTimeSeconds = args.ExpirationTimeSeconds;
+ this.IssuedAtTimeSeconds = args.IssuedAtTimeSeconds;
+ this.Uid = args.Subject;
+ this.Claims = args.Claims;
+ }
+
///
- /// The issuer claim that identifies the principal that issued the JWT.
+ /// Gets the issuer claim that identifies the principal that issued the JWT.
///
public string Issuer { get; private set; }
///
- /// The subject claim identifying the principal that is the subject of the JWT.
+ /// Gets the subject claim identifying the principal that is the subject of the JWT.
///
public string Subject { get; private set; }
///
- /// The audience claim that identifies the audience that the JWT is intended for.
+ /// Gets the audience claim that identifies the audience that the JWT is intended for.
///
public string Audience { get; private set; }
///
- /// The expiration time claim that identifies the expiration time (in seconds)
+ /// Gets the expiration time claim that identifies the expiration time (in seconds)
/// on or after which the token MUST NOT be accepted for processing.
///
public long ExpirationTimeSeconds { get; private set; }
///
- /// The issued at claim that identifies the time (in seconds) at which the JWT was issued.
+ /// Gets the issued at claim that identifies the time (in seconds) at which the JWT was
+ /// issued.
///
public long IssuedAtTimeSeconds { get; private set; }
-
+
///
- /// User ID of the user to which this ID token belongs. This is same as Subject.
+ /// Gets the User ID of the user to which this ID token belongs. This is same as
+ /// .
///
public string Uid { get; private set; }
///
- /// A read-only dictionary of all other claims present in the JWT. This can be used to
+ /// Gets Aall other claims present in the JWT as a readonly dictionary. This can be used to
/// access custom claims of the token.
///
public IReadOnlyDictionary Claims { get; private set; }
-
- internal FirebaseToken(FirebaseTokenArgs args)
- {
- Issuer = args.Issuer;
- Subject = args.Subject;
- Audience = args.Audience;
- ExpirationTimeSeconds = args.ExpirationTimeSeconds;
- IssuedAtTimeSeconds = args.IssuedAtTimeSeconds;
- Uid = args.Subject;
- Claims = args.Claims;
- }
- }
-
- internal sealed class FirebaseTokenArgs
- {
- [JsonProperty("iss")]
- public string Issuer { get; set; }
-
- [JsonProperty("sub")]
- public string Subject { get; set; }
-
- [JsonProperty("aud")]
- public string Audience { get; set; }
-
- [JsonProperty("exp")]
- public long ExpirationTimeSeconds { get; set; }
-
- [JsonProperty("iat")]
- public long IssuedAtTimeSeconds { get; set; }
-
- [JsonIgnore]
- public IReadOnlyDictionary Claims { get; set; }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenArgs.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenArgs.cs
new file mode 100644
index 00000000..027feb6b
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenArgs.cs
@@ -0,0 +1,40 @@
+// Copyright 2018, 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 Newtonsoft.Json;
+
+namespace FirebaseAdmin.Auth
+{
+ internal sealed class FirebaseTokenArgs
+ {
+ [JsonProperty("iss")]
+ public string Issuer { get; set; }
+
+ [JsonProperty("sub")]
+ public string Subject { get; set; }
+
+ [JsonProperty("aud")]
+ public string Audience { get; set; }
+
+ [JsonProperty("exp")]
+ public long ExpirationTimeSeconds { get; set; }
+
+ [JsonProperty("iat")]
+ public long IssuedAtTimeSeconds { get; set; }
+
+ [JsonIgnore]
+ public IReadOnlyDictionary Claims { get; set; }
+ }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs
index 79e3b4ab..411c97fa 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs
@@ -15,46 +15,86 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
-using System.Threading.Tasks;
-using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
using Google.Apis.Auth;
-using Google.Apis.Http;
using Google.Apis.Auth.OAuth2;
+using Google.Apis.Http;
using Google.Apis.Util;
-[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey="+
-"002400000480000094000000060200000024000052534131000400000100010081328559eaab41"+
-"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02"+
-"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597"+
-"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f"+
+[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey=" +
+"002400000480000094000000060200000024000052534131000400000100010081328559eaab41" +
+"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02" +
+"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597" +
+"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f" +
"badddb9c")]
namespace FirebaseAdmin.Auth
{
///
/// A helper class that creates Firebase custom tokens.
///
- internal class FirebaseTokenFactory: IDisposable
+ internal class FirebaseTokenFactory : IDisposable
{
public const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
+ "google.identity.identitytoolkit.v1.IdentityToolkit";
-
+
public const int TokenDurationSeconds = 3600;
public static readonly DateTime UnixEpoch = new DateTime(
1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
public static readonly ImmutableList ReservedClaims = ImmutableList.Create(
- "acr", "amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash",
- "exp", "firebase", "iat", "iss", "jti", "nbf", "nonce", "sub"
- );
+ "acr",
+ "amr",
+ "at_hash",
+ "aud",
+ "auth_time",
+ "azp",
+ "cnf",
+ "c_hash",
+ "exp",
+ "firebase",
+ "iat",
+ "iss",
+ "jti",
+ "nbf",
+ "nonce",
+ "sub");
- private readonly ISigner _signer;
- private readonly IClock _clock;
+ private readonly ISigner signer;
+ private readonly IClock clock;
public FirebaseTokenFactory(ISigner signer, IClock clock)
{
- _signer = signer.ThrowIfNull(nameof(signer));
- _clock = clock.ThrowIfNull(nameof(clock));
+ this.signer = signer.ThrowIfNull(nameof(signer));
+ this.clock = clock.ThrowIfNull(nameof(clock));
+ }
+
+ public static FirebaseTokenFactory Create(FirebaseApp app)
+ {
+ ISigner signer = null;
+ var serviceAccount = app.Options.Credential.ToServiceAccountCredential();
+ if (serviceAccount != null)
+ {
+ // If the app was initialized with a service account, use it to sign
+ // tokens locally.
+ signer = new ServiceAccountSigner(serviceAccount);
+ }
+ else if (string.IsNullOrEmpty(app.Options.ServiceAccountId))
+ {
+ // If no service account ID is specified, attempt to discover one and invoke the
+ // IAM service with it.
+ signer = new IAMSigner(new HttpClientFactory(), app.Options.Credential);
+ }
+ else
+ {
+ // If a service account ID is specified, invoke the IAM service with it.
+ signer = new FixedAccountIAMSigner(
+ new HttpClientFactory(), app.Options.Credential, app.Options.ServiceAccountId);
+ }
+
+ return new FirebaseTokenFactory(signer, SystemClock.Default);
}
public async Task CreateCustomTokenAsync(
@@ -70,6 +110,7 @@ public async Task CreateCustomTokenAsync(
{
throw new ArgumentException("uid must not be longer than 128 characters");
}
+
if (developerClaims != null)
{
foreach (var entry in developerClaims)
@@ -85,11 +126,11 @@ public async Task CreateCustomTokenAsync(
var header = new JsonWebSignature.Header()
{
Algorithm = "RS256",
- Type = "JWT"
+ Type = "JWT",
};
-
- var issued = (int)(_clock.UtcNow - UnixEpoch).TotalSeconds;
- var keyId = await _signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
+
+ var issued = (int)(this.clock.UtcNow - UnixEpoch).TotalSeconds;
+ var keyId = await this.signer.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
var payload = new CustomTokenPayload()
{
Uid = uid,
@@ -97,53 +138,30 @@ public async Task CreateCustomTokenAsync(
Subject = keyId,
Audience = FirebaseAudience,
IssuedAtTimeSeconds = issued,
- ExpirationTimeSeconds = issued + TokenDurationSeconds,
+ ExpirationTimeSeconds = issued + TokenDurationSeconds,
};
+
if (developerClaims != null && developerClaims.Count > 0)
{
payload.Claims = developerClaims;
}
+
return await JwtUtils.CreateSignedJwtAsync(
- header, payload, _signer, cancellationToken).ConfigureAwait(false);
+ header, payload, this.signer, cancellationToken).ConfigureAwait(false);
}
public void Dispose()
{
- _signer.Dispose();
+ this.signer.Dispose();
}
- public static FirebaseTokenFactory Create(FirebaseApp app)
+ internal class CustomTokenPayload : JsonWebToken.Payload
{
- ISigner signer = null;
- var serviceAccount = app.Options.Credential.ToServiceAccountCredential();
- if (serviceAccount != null)
- {
- // If the app was initialized with a service account, use it to sign
- // tokens locally.
- signer = new ServiceAccountSigner(serviceAccount);
- }
- else if (string.IsNullOrEmpty(app.Options.ServiceAccountId))
- {
- // If no service account ID is specified, attempt to discover one and invoke the
- // IAM service with it.
- signer = new IAMSigner(new HttpClientFactory(), app.Options.Credential);
- }
- else
- {
- // If a service account ID is specified, invoke the IAM service with it.
- signer = new FixedAccountIAMSigner(
- new HttpClientFactory(), app.Options.Credential, app.Options.ServiceAccountId);
- }
- return new FirebaseTokenFactory(signer, SystemClock.Default);
- }
- }
+ [Newtonsoft.Json.JsonPropertyAttribute("uid")]
+ public string Uid { get; set; }
- internal class CustomTokenPayload: JsonWebToken.Payload
- {
- [Newtonsoft.Json.JsonPropertyAttribute("uid")]
- public string Uid { get; set; }
-
- [Newtonsoft.Json.JsonPropertyAttribute("claims")]
- public IDictionary Claims { get; set; }
+ [Newtonsoft.Json.JsonPropertyAttribute("claims")]
+ public IDictionary Claims { get; set; }
+ }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs
index e96b3349..172fa03e 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs
@@ -15,6 +15,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
@@ -34,7 +35,7 @@ internal sealed class FirebaseTokenVerifier
private const string IdTokenCertUrl = "https://www.googleapis.com/robot/v1/metadata/x509/"
+ "securetoken@system.gserviceaccount.com";
- private const string FirebaseAudience ="https://identitytoolkit.googleapis.com/"
+ private const string FirebaseAudience = "https://identitytoolkit.googleapis.com/"
+ "google.identity.identitytoolkit.v1.IdentityToolkit";
// See http://oid-info.com/get/2.16.840.1.101.3.4.2.1
@@ -43,117 +44,146 @@ internal sealed class FirebaseTokenVerifier
private static readonly IReadOnlyList StandardClaims =
ImmutableList.Create("iss", "aud", "exp", "iat", "sub", "uid");
- public string ProjectId { get; }
- private readonly string _shortName;
- private readonly string _articledShortName;
- private readonly string _operation;
- private readonly string _url;
- private readonly string _issuer;
- private readonly IClock _clock;
- private readonly IPublicKeySource _keySource;
-
- public FirebaseTokenVerifier(FirebaseTokenVerifierArgs args)
+ private readonly string shortName;
+ private readonly string articledShortName;
+ private readonly string operation;
+ private readonly string url;
+ private readonly string issuer;
+ private readonly IClock clock;
+ private readonly IPublicKeySource keySource;
+
+ internal FirebaseTokenVerifier(FirebaseTokenVerifierArgs args)
{
- ProjectId = args.ProjectId.ThrowIfNullOrEmpty(nameof(args.ProjectId));
- _shortName = args.ShortName.ThrowIfNullOrEmpty(nameof(args.ShortName));
- _operation = args.Operation.ThrowIfNullOrEmpty(nameof(args.Operation));
- _url = args.Url.ThrowIfNullOrEmpty(nameof(args.Url));
- _issuer = args.Issuer.ThrowIfNullOrEmpty(nameof(args.Issuer));
- _clock = args.Clock.ThrowIfNull(nameof(args.Clock));
- _keySource = args.PublicKeySource.ThrowIfNull(nameof(args.PublicKeySource));
- if ("aeiou".Contains(_shortName.ToLower().Substring(0, 1)))
+ this.ProjectId = args.ProjectId.ThrowIfNullOrEmpty(nameof(args.ProjectId));
+ this.shortName = args.ShortName.ThrowIfNullOrEmpty(nameof(args.ShortName));
+ this.operation = args.Operation.ThrowIfNullOrEmpty(nameof(args.Operation));
+ this.url = args.Url.ThrowIfNullOrEmpty(nameof(args.Url));
+ this.issuer = args.Issuer.ThrowIfNullOrEmpty(nameof(args.Issuer));
+ this.clock = args.Clock.ThrowIfNull(nameof(args.Clock));
+ this.keySource = args.PublicKeySource.ThrowIfNull(nameof(args.PublicKeySource));
+ if ("aeiou".Contains(this.shortName.ToLower().Substring(0, 1)))
{
- _articledShortName = $"an {_shortName}";
+ this.articledShortName = $"an {this.shortName}";
}
else
{
- _articledShortName = $"a {_shortName}";
+ this.articledShortName = $"a {this.shortName}";
}
}
- public async Task VerifyTokenAsync(
+ public string ProjectId { get; }
+
+ internal static FirebaseTokenVerifier CreateIDTokenVerifier(FirebaseApp app)
+ {
+ var projectId = app.GetProjectId();
+ if (string.IsNullOrEmpty(projectId))
+ {
+ throw new ArgumentException(
+ "Must initialize FirebaseApp with a project ID to verify ID tokens.");
+ }
+
+ var keySource = new HttpPublicKeySource(
+ IdTokenCertUrl, SystemClock.Default, new HttpClientFactory());
+ var args = new FirebaseTokenVerifierArgs()
+ {
+ ProjectId = projectId,
+ ShortName = "ID token",
+ Operation = "VerifyIdTokenAsync()",
+ Url = "https://firebase.google.com/docs/auth/admin/verify-id-tokens",
+ Issuer = "https://securetoken.google.com/",
+ Clock = SystemClock.Default,
+ PublicKeySource = keySource,
+ };
+
+ return new FirebaseTokenVerifier(args);
+ }
+
+ internal async Task VerifyTokenAsync(
string token, CancellationToken cancellationToken = default(CancellationToken))
{
if (string.IsNullOrEmpty(token))
{
- throw new ArgumentException($"{_shortName} must not be null or empty.");
+ throw new ArgumentException($"{this.shortName} must not be null or empty.");
}
+
string[] segments = token.Split('.');
if (segments.Length != 3)
{
- throw new FirebaseException($"Incorrect number of segments in ${_shortName}.");
+ throw new FirebaseException($"Incorrect number of segments in ${this.shortName}.");
}
var header = JwtUtils.Decode(segments[0]);
var payload = JwtUtils.Decode(segments[1]);
- var projectIdMessage = $"Make sure the {_shortName} comes from the same Firebase "
+ var projectIdMessage = $"Make sure the {this.shortName} comes from the same Firebase "
+ "project as the credential used to initialize this SDK.";
- var verifyTokenMessage = $"See {_url} for details on how to retrieve a value "
- + $"{_shortName}.";
- var issuer = _issuer + ProjectId;
+ var verifyTokenMessage = $"See {this.url} for details on how to retrieve a value "
+ + $"{this.shortName}.";
+ var issuer = this.issuer + this.ProjectId;
string error = null;
if (string.IsNullOrEmpty(header.KeyId))
{
- if (FirebaseAudience == payload.Audience)
+ if (payload.Audience == FirebaseAudience)
{
- error = $"{_operation} expects {_articledShortName}, but was given a custom "
+ error = $"{this.operation} expects {this.articledShortName}, but was given a custom "
+ "token.";
}
else if (header.Algorithm == "HS256")
{
- error = $"{_operation} expects {_articledShortName}, but was given a legacy "
+ error = $"{this.operation} expects {this.articledShortName}, but was given a legacy "
+ "custom token.";
}
else
{
- error = $"Firebase {_shortName} has no 'kid' claim.";
+ error = $"Firebase {this.shortName} has no 'kid' claim.";
}
}
else if (header.Algorithm != "RS256")
{
- error = $"Firebase {_shortName} has incorrect algorithm. Expected RS256 but got "
+ error = $"Firebase {this.shortName} has incorrect algorithm. Expected RS256 but got "
+ $"{header.Algorithm}. {verifyTokenMessage}";
}
- else if (ProjectId != payload.Audience)
+ else if (this.ProjectId != payload.Audience)
{
- error = $"{_shortName} has incorrect audience (aud) claim. Expected {ProjectId} "
+ error = $"{this.shortName} has incorrect audience (aud) claim. Expected {this.ProjectId} "
+ $"but got {payload.Audience}. {projectIdMessage} {verifyTokenMessage}";
}
else if (payload.Issuer != issuer)
{
- error = $"{_shortName} has incorrect issuer (iss) claim. Expected {issuer} but "
+ error = $"{this.shortName} has incorrect issuer (iss) claim. Expected {issuer} but "
+ $"got {payload.Issuer}. {projectIdMessage} {verifyTokenMessage}";
}
- else if (payload.IssuedAtTimeSeconds > _clock.UnixTimestamp())
+ else if (payload.IssuedAtTimeSeconds > this.clock.UnixTimestamp())
{
- error = $"Firebase {_shortName} issued at future timestamp";
+ error = $"Firebase {this.shortName} issued at future timestamp";
}
- else if (payload.ExpirationTimeSeconds < _clock.UnixTimestamp())
+ else if (payload.ExpirationTimeSeconds < this.clock.UnixTimestamp())
{
- error = $"Firebase {_shortName} expired at {payload.ExpirationTimeSeconds}";
+ error = $"Firebase {this.shortName} expired at {payload.ExpirationTimeSeconds}";
}
else if (string.IsNullOrEmpty(payload.Subject))
{
- error = $"Firebase {_shortName} has no or empty subject (sub) claim.";
+ error = $"Firebase {this.shortName} has no or empty subject (sub) claim.";
}
else if (payload.Subject.Length > 128)
{
- error = $"Firebase {_shortName} has a subject claim longer than 128 characters.";
+ error = $"Firebase {this.shortName} has a subject claim longer than 128 characters.";
}
-
+
if (error != null)
{
throw new FirebaseException(error);
}
- await VerifySignatureAsync(segments, header.KeyId, cancellationToken)
+ await this.VerifySignatureAsync(segments, header.KeyId, cancellationToken)
.ConfigureAwait(false);
var allClaims = JwtUtils.Decode>(segments[1]);
+
// Remove standard claims, so that only custom claims would remain.
foreach (var claim in StandardClaims)
{
allClaims.Remove(claim);
}
+
payload.Claims = allClaims.ToImmutableDictionary();
return new FirebaseToken(payload);
}
@@ -162,6 +192,14 @@ await VerifySignatureAsync(segments, header.KeyId, cancellationToken)
/// Verifies the integrity of a JWT by validating its signature. The JWT must be specified
/// as an array of three segments (header, body and signature).
///
+ [SuppressMessage(
+ "StyleCop.Analyzers",
+ "SA1009:ClosingParenthesisMustBeSpacedCorrectly",
+ Justification = "Use of directives.")]
+ [SuppressMessage(
+ "StyleCop.Analyzers",
+ "SA1111:ClosingParenthesisMustBeOnLineOfLastParameter",
+ Justification = "Use of directives.")]
private async Task VerifySignatureAsync(
string[] segments, string keyId, CancellationToken cancellationToken)
{
@@ -171,15 +209,16 @@ private async Task VerifySignatureAsync(
hash = hashAlg.ComputeHash(
Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}"));
}
+
var signature = JwtUtils.Base64DecodeToBytes(segments[2]);
- var keys = await _keySource.GetPublicKeysAsync(cancellationToken)
+ var keys = await this.keySource.GetPublicKeysAsync(cancellationToken)
.ConfigureAwait(false);
var verified = keys.Any(key =>
#if NETSTANDARD1_5 || NETSTANDARD2_0
key.Id == keyId && key.RSA.VerifyHash(
hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
#elif NET45
- key.Id == keyId &&
+ key.Id == keyId &&
((RSACryptoServiceProvider) key.RSA).VerifyHash(hash, Sha256Oid, signature)
#else
#error Unsupported target
@@ -187,42 +226,8 @@ private async Task VerifySignatureAsync(
);
if (!verified)
{
- throw new FirebaseException($"Failed to verify {_shortName} signature.");
+ throw new FirebaseException($"Failed to verify {this.shortName} signature.");
}
}
-
- internal static FirebaseTokenVerifier CreateIDTokenVerifier(FirebaseApp app)
- {
- var projectId = app.GetProjectId();
- if (string.IsNullOrEmpty(projectId))
- {
- throw new ArgumentException(
- "Must initialize FirebaseApp with a project ID to verify ID tokens.");
- }
- var keySource = new HttpPublicKeySource(
- IdTokenCertUrl, SystemClock.Default, new HttpClientFactory());
- var args = new FirebaseTokenVerifierArgs()
- {
- ProjectId = projectId,
- ShortName = "ID token",
- Operation = "VerifyIdTokenAsync()",
- Url = "https://firebase.google.com/docs/auth/admin/verify-id-tokens",
- Issuer = "https://securetoken.google.com/",
- Clock = SystemClock.Default,
- PublicKeySource = keySource,
- };
- return new FirebaseTokenVerifier(args);
- }
- }
-
- internal sealed class FirebaseTokenVerifierArgs
- {
- public string ProjectId { get; set; }
- public string ShortName { get; set; }
- public string Operation { get; set; }
- public string Url { get; set; }
- public string Issuer { get; set; }
- public IClock Clock { get; set; }
- public IPublicKeySource PublicKeySource { get; set; }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifierArgs.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifierArgs.cs
new file mode 100644
index 00000000..d79524b7
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifierArgs.cs
@@ -0,0 +1,35 @@
+// Copyright 2018, 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 Google.Apis.Util;
+
+namespace FirebaseAdmin.Auth
+{
+ internal sealed class FirebaseTokenVerifierArgs
+ {
+ public string ProjectId { get; set; }
+
+ public string ShortName { get; set; }
+
+ public string Operation { get; set; }
+
+ public string Url { get; set; }
+
+ public string Issuer { get; set; }
+
+ public IClock Clock { get; set; }
+
+ public IPublicKeySource PublicKeySource { get; set; }
+ }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs
index 3a3e86fc..805d6347 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManager.cs
@@ -12,32 +12,51 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-using Google.Apis.Auth.OAuth2;
-using Google.Apis.Http;
-using Newtonsoft.Json.Linq;
using System;
using System.Net.Http;
using System.Threading.Tasks;
+using Google.Apis.Auth.OAuth2;
+using Google.Apis.Http;
+using Newtonsoft.Json.Linq;
namespace FirebaseAdmin.Auth
{
///
- /// FirebaseUserManager provides methods for interacting with the
+ /// FirebaseUserManager provides methods for interacting with the
///
- /// Google Identity Toolkit via its REST API. This class does not hold any mutable state,
+ /// Google Identity Toolkit via its REST API. This class does not hold any mutable state,
/// and is thread safe.
///
internal class FirebaseUserManager : IDisposable
{
- private const string ID_TOOLKIT_URL = "https://identitytoolkit.googleapis.com/v1/projects/{0}";
+ private const string IdTooklitUrl = "https://identitytoolkit.googleapis.com/v1/projects/{0}";
- private readonly ConfigurableHttpClient _httpClient;
- private readonly string _baseUrl;
+ private readonly ConfigurableHttpClient httpClient;
+ private readonly string baseUrl;
internal FirebaseUserManager(FirebaseUserManagerArgs args)
{
- _httpClient = args.ClientFactory.CreateAuthorizedHttpClient(args.Credential);
- _baseUrl = string.Format(ID_TOOLKIT_URL, args.ProjectId);
+ this.httpClient = args.ClientFactory.CreateAuthorizedHttpClient(args.Credential);
+ this.baseUrl = string.Format(IdTooklitUrl, args.ProjectId);
+ }
+
+ public static FirebaseUserManager Create(FirebaseApp app)
+ {
+ var projectId = app.GetProjectId();
+ if (string.IsNullOrEmpty(projectId))
+ {
+ throw new ArgumentException(
+ "Must initialize FirebaseApp with a project ID to manage users.");
+ }
+
+ var args = new FirebaseUserManagerArgs
+ {
+ ClientFactory = new HttpClientFactory(),
+ Credential = app.Options.Credential,
+ ProjectId = projectId,
+ };
+
+ return new FirebaseUserManager(args);
}
///
@@ -48,7 +67,7 @@ internal FirebaseUserManager(FirebaseUserManagerArgs args)
public async Task UpdateUserAsync(UserRecord user)
{
var updatePath = "/accounts:update";
- var resopnse = await PostAsync(updatePath, user);
+ var resopnse = await this.PostAsync(updatePath, user);
try
{
@@ -64,13 +83,18 @@ public async Task UpdateUserAsync(UserRecord user)
}
}
+ public void Dispose()
+ {
+ this.httpClient.Dispose();
+ }
+
private async Task PostAsync(string path, UserRecord user)
{
- var requestUri = $"{_baseUrl}{path}";
+ var requestUri = $"{this.baseUrl}{path}";
HttpResponseMessage response = null;
try
{
- response = await _httpClient.PostJsonAsync(requestUri, user, default);
+ response = await this.httpClient.PostJsonAsync(requestUri, user, default);
var json = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
@@ -90,36 +114,5 @@ private async Task PostAsync(string path, UserRecord user)
throw new FirebaseException("Error while calling Firebase Auth service", e);
}
}
-
- internal static FirebaseUserManager Create(FirebaseApp app)
- {
- var projectId = app.GetProjectId();
- if (string.IsNullOrEmpty(projectId))
- {
- throw new ArgumentException(
- "Must initialize FirebaseApp with a project ID to manage users.");
- }
-
- var args = new FirebaseUserManagerArgs
- {
- ClientFactory = new HttpClientFactory(),
- Credential = app.Options.Credential,
- ProjectId = projectId,
- };
-
- return new FirebaseUserManager(args);
- }
-
- public void Dispose()
- {
- _httpClient.Dispose();
- }
- }
-
- internal sealed class FirebaseUserManagerArgs
- {
- public HttpClientFactory ClientFactory { get; set; }
- public GoogleCredential Credential { get; set; }
- public string ProjectId { get; set; }
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManagerArgs.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManagerArgs.cs
new file mode 100644
index 00000000..034f4ffc
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseUserManagerArgs.cs
@@ -0,0 +1,28 @@
+// Copyright 2019, 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 Google.Apis.Auth.OAuth2;
+using Google.Apis.Http;
+
+namespace FirebaseAdmin.Auth
+{
+ internal sealed class FirebaseUserManagerArgs
+ {
+ public HttpClientFactory ClientFactory { get; set; }
+
+ public GoogleCredential Credential { get; set; }
+
+ public string ProjectId { get; set; }
+ }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs
new file mode 100644
index 00000000..8398d9fb
--- /dev/null
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs
@@ -0,0 +1,45 @@
+// Copyright 2018, 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.Threading;
+using System.Threading.Tasks;
+using Google.Apis.Auth.OAuth2;
+using Google.Apis.Http;
+using Google.Apis.Util;
+
+namespace FirebaseAdmin.Auth
+{
+ ///
+ /// An implementation that uses the IAM service to sign data. Unlike
+ /// this class does not attempt to auto discover a service account ID.
+ /// Insterad it must be initialized with a fixed service account ID string.
+ ///
+ internal sealed class FixedAccountIAMSigner : IAMSigner
+ {
+ private readonly string keyId;
+
+ public FixedAccountIAMSigner(
+ HttpClientFactory clientFactory, GoogleCredential credential, string keyId)
+ : base(clientFactory, credential)
+ {
+ this.keyId = keyId.ThrowIfNullOrEmpty(nameof(keyId));
+ }
+
+ public override Task GetKeyIdAsync(
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ return Task.FromResult(this.keyId);
+ }
+ }
+}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs
index ffbbd9ff..6851d190 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs
@@ -21,8 +21,8 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using Google.Apis.Json;
using Google.Apis.Http;
+using Google.Apis.Json;
using Google.Apis.Util;
#if NETSTANDARD1_5 || NETSTANDARD2_0
@@ -40,7 +40,7 @@ namespace FirebaseAdmin.Auth
/// HTTP server. Retrieved keys are cached in memory according to the HTTP cache-control
/// directive.
///
- internal sealed class HttpPublicKeySource: IPublicKeySource
+ internal sealed class HttpPublicKeySource : IPublicKeySource
{
// Default clock skew used by most GCP libraries. This interval is subtracted from the
// cache expiry time, before any expiration checks. This helps correct for minor
@@ -48,44 +48,44 @@ internal sealed class HttpPublicKeySource: IPublicKeySource
// pre-emptively refreshed instead of waiting until the last second.
private static readonly TimeSpan ClockSkew = new TimeSpan(hours: 0, minutes: 5, seconds: 0);
- private readonly string _certUrl;
- private IReadOnlyList _cachedKeys;
- private DateTime _expirationTime;
- private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
- private readonly IClock _clock;
- private readonly HttpClientFactory _clientFactory;
+ private readonly SemaphoreSlim cacheLock = new SemaphoreSlim(1, 1);
+ private readonly string certUrl;
+ private readonly IClock clock;
+ private readonly HttpClientFactory clientFactory;
+ private DateTime expirationTime;
+ private IReadOnlyList cachedKeys;
public HttpPublicKeySource(string certUrl, IClock clock, HttpClientFactory clientFactory)
{
- _certUrl = certUrl.ThrowIfNullOrEmpty(nameof(certUrl));
- _clock = clock.ThrowIfNull(nameof(clock));
- _clientFactory = clientFactory.ThrowIfNull(nameof(clientFactory));
- _expirationTime = clock.UtcNow;
+ this.certUrl = certUrl.ThrowIfNullOrEmpty(nameof(certUrl));
+ this.clock = clock.ThrowIfNull(nameof(clock));
+ this.clientFactory = clientFactory.ThrowIfNull(nameof(clientFactory));
+ this.expirationTime = clock.UtcNow;
}
public async Task> GetPublicKeysAsync(
CancellationToken cancellationToken = default(CancellationToken))
{
- if (_cachedKeys == null || _clock.UtcNow >= _expirationTime)
+ if (this.cachedKeys == null || this.clock.UtcNow >= this.expirationTime)
{
- await _lock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ await this.cacheLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
- var now = _clock.UtcNow;
- if (_cachedKeys == null || now >= _expirationTime)
+ var now = this.clock.UtcNow;
+ if (this.cachedKeys == null || now >= this.expirationTime)
{
- using (var httpClient = _clientFactory.CreateDefaultHttpClient())
+ using (var httpClient = this.clientFactory.CreateDefaultHttpClient())
{
- var response = await httpClient.GetAsync(_certUrl, cancellationToken)
+ var response = await httpClient.GetAsync(this.certUrl, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- _cachedKeys = ParseKeys(await response.Content.ReadAsStringAsync()
+ this.cachedKeys = this.ParseKeys(await response.Content.ReadAsStringAsync()
.ConfigureAwait(false));
var cacheControl = response.Headers.CacheControl;
if (cacheControl?.MaxAge != null)
{
- _expirationTime = now.Add(cacheControl.MaxAge.Value)
+ this.expirationTime = now.Add(cacheControl.MaxAge.Value)
.Subtract(ClockSkew);
}
}
@@ -97,11 +97,11 @@ public async Task> GetPublicKeysAsync(
}
finally
{
- _lock.Release();
+ this.cacheLock.Release();
}
}
- return _cachedKeys;
+ return this.cachedKeys;
}
private IReadOnlyList ParseKeys(string json)
@@ -112,6 +112,7 @@ private IReadOnlyList ParseKeys(string json)
{
throw new InvalidDataException("No public keys present in the response.");
}
+
var builder = ImmutableList.CreateBuilder();
foreach (var entry in rawKeys)
{
@@ -126,6 +127,7 @@ private IReadOnlyList ParseKeys(string json)
#endif
builder.Add(new PublicKey(entry.Key, rsa));
}
+
return builder.ToImmutableList();
}
}
diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs
index dec55cd4..f7d810bd 100644
--- a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs
+++ b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs
@@ -36,16 +36,17 @@ internal class IAMSigner : ISigner
{
private const string SignBlobUrl =
"https://iam.googleapis.com/v1/projects/-/serviceAccounts/{0}:signBlob";
- private const string MetadataServerUrl =
+
+ private const string MetadataServerUrl =
"http://metadata/computeMetadata/v1/instance/service-accounts/default/email";
- private readonly ConfigurableHttpClient _httpClient;
- private readonly Lazy> _keyId;
+ private readonly ConfigurableHttpClient httpClient;
+ private readonly Lazy> keyId;
public IAMSigner(HttpClientFactory clientFactory, GoogleCredential credential)
{
- _httpClient = clientFactory.CreateAuthorizedHttpClient(credential);
- _keyId = new Lazy>(
+ this.httpClient = clientFactory.CreateAuthorizedHttpClient(credential);
+ this.keyId = new Lazy>(
async () => await DiscoverServiceAccountIdAsync(clientFactory)
.ConfigureAwait(false), true);
}
@@ -53,18 +54,19 @@ public IAMSigner(HttpClientFactory clientFactory, GoogleCredential credential)
public async Task SignDataAsync(
byte[] data, CancellationToken cancellationToken = default(CancellationToken))
{
- var keyId = await GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
+ var keyId = await this.GetKeyIdAsync(cancellationToken).ConfigureAwait(false);
var url = string.Format(SignBlobUrl, keyId);
var request = new SignBlobRequest
{
BytesToSign = Convert.ToBase64String(data),
};
+
try
{
- var response = await _httpClient.PostJsonAsync(url, request, cancellationToken)
+ var response = await this.httpClient.PostJsonAsync(url, request, cancellationToken)
.ConfigureAwait(false);
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
- ThrowIfError(response, json);
+ this.ThrowIfError(response, json);
var parsed = NewtonsoftJsonSerializer.Instance.Deserialize(json);
return Convert.FromBase64String(parsed.Signature);
}
@@ -74,34 +76,12 @@ public async Task SignDataAsync(
}
}
- private void ThrowIfError(HttpResponseMessage response, string content)
- {
- if (response.IsSuccessStatusCode)
- {
- return;
- }
- string error = null;
- try
- {
- var result = NewtonsoftJsonSerializer.Instance.Deserialize(content);
- error = result?.Error.Message;
- }
- catch (Exception) {} // Ignore any errors encountered while parsing the originl error.
- if (string.IsNullOrEmpty(error))
- {
- error = "Response status code does not indicate success: "
- + $"{(int) response.StatusCode} ({response.StatusCode})"
- + $"{Environment.NewLine}{content}";
- }
- throw new FirebaseException(error);
- }
-
public virtual async Task GetKeyIdAsync(
CancellationToken cancellationToken = default(CancellationToken))
{
try
{
- return await _keyId.Value.ConfigureAwait(false);
+ return await this.keyId.Value.ConfigureAwait(false);
}
catch (Exception e)
{
@@ -114,6 +94,11 @@ public virtual async Task GetKeyIdAsync(
}
}
+ public void Dispose()
+ {
+ this.httpClient.Dispose();
+ }
+
private static async Task DiscoverServiceAccountIdAsync(
HttpClientFactory clientFactory)
{
@@ -124,67 +109,68 @@ private static async Task DiscoverServiceAccountIdAsync(
}
}
- public void Dispose()
+ private void ThrowIfError(HttpResponseMessage response, string content)
{
- _httpClient.Dispose();
- }
- }
+ if (response.IsSuccessStatusCode)
+ {
+ return;
+ }
- ///
- /// Represents the sign request sent to the remote IAM service.
- ///
- internal class SignBlobRequest
- {
- [Newtonsoft.Json.JsonProperty("bytesToSign")]
- public string BytesToSign { get; set; }
- }
+ string error = null;
+ try
+ {
+ var result = NewtonsoftJsonSerializer.Instance.Deserialize(content);
+ error = result?.Error.Message;
+ }
+ catch (Exception)
+ {
+ // Ignore any errors encountered while parsing the originl error.
+ }
- ///
- /// Represents the sign response sent by the remote IAM service.
- ///
- internal class SignBlobResponse
- {
- [Newtonsoft.Json.JsonProperty("signature")]
- public string Signature { get; set; }
- }
+ if (string.IsNullOrEmpty(error))
+ {
+ error = "Response status code does not indicate success: "
+ + $"{(int)response.StatusCode} ({response.StatusCode})"
+ + $"{Environment.NewLine}{content}";
+ }
- ///
- /// Represents an error response sent by the remote IAM service.
- ///
- internal class SignBlobError
- {
- [Newtonsoft.Json.JsonProperty("error")]
- public SignBlobErrorDetail Error { get; set; }
- }
+ throw new FirebaseException(error);
+ }
- ///
- /// Represents the error details embedded in an IAM error response.
- ///
- internal class SignBlobErrorDetail
- {
- [Newtonsoft.Json.JsonProperty("message")]
- public string Message { get; set; }
- }
+ ///
+ /// Represents the sign request sent to the remote IAM service.
+ ///
+ internal class SignBlobRequest
+ {
+ [Newtonsoft.Json.JsonProperty("bytesToSign")]
+ public string BytesToSign { get; set; }
+ }
- ///
- /// An