From 055df2544f25041b63e28014d7b3144419266dec Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Sat, 22 Sep 2018 22:32:36 -0700 Subject: [PATCH 01/50] Drafting the FCM API --- .../Messaging/FirebaseMessageClientTest.cs | 94 ++++++++++++ .../Messaging/FirebaseMessagingTest.cs | 69 +++++++++ .../Messaging/MessagingTest.cs | 140 ++++++++++++++++++ .../Messaging/FirebaseMessaging.cs | 103 +++++++++++++ .../Messaging/FirebaseMessagingClient.cs | 89 +++++++++++ .../FirebaseAdmin/Messaging/Message.cs | 92 ++++++++++++ 6 files changed, 587 insertions(+) create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs create mode 100644 FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessagingTest.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs new file mode 100644 index 00000000..d48d2469 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs @@ -0,0 +1,94 @@ +// 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 Xunit; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Http; +using Google.Apis.Json; +using FirebaseAdmin.Tests; + +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, "")); + } + + [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 Send() + { + var handler = new MockMessageHandler() + { + Response = new 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, false, default(CancellationToken)); + Assert.Equal("test-response", response); + var req = NewtonsoftJsonSerializer.Instance.Deserialize(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, true, default(CancellationToken)); + Assert.Equal("test-response", response); + req = NewtonsoftJsonSerializer.Instance.Deserialize(handler.Request); + Assert.Equal("test-topic", req.Message.Topic); + Assert.True(req.ValidateOnly); + Assert.Equal(2, handler.Calls); + } + } + + internal sealed class SendRequest + { + [Newtonsoft.Json.JsonProperty("message")] + public Message Message { get; set; } + + [Newtonsoft.Json.JsonProperty("validate_only")] + public bool ValidateOnly { get; set; } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs new file mode 100644 index 00000000..94596bcd --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs @@ -0,0 +1,69 @@ +// 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.Tasks; +using Xunit; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Json; +using FirebaseAdmin.Tests; + +namespace FirebaseAdmin.Messaging.Tests +{ + public class FirebaseMessagingTest + { + 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.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.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(); + var message = new Message() + { + Topic = "test-topic", + }; + await Assert.ThrowsAsync( + async () => await messaging.SendAsync(message)); + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessagingTest.cs new file mode 100644 index 00000000..ef3832b9 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessagingTest.cs @@ -0,0 +1,140 @@ +// 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 Newtonsoft.Json.Linq; +using Xunit; +using FirebaseAdmin.Messaging; +using Google.Apis.Json; + +namespace FirebaseAdmin.Messaging.Tests +{ + public class MessagingTest + { + [Fact] + public void MessageWithoutTarget() + { + Assert.Throws(() => new Message().Validate()); + } + + [Fact] + public void EmptyMessage() + { + var message = new Message() + { + Token = "test-token" + }; + AssertJsonEquals(new JObject(){{"token", "test-token"}}, message); + + message = new Message() + { + Topic = "test-topic" + }; + AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); + + message = new Message() + { + Condition = "test-condition" + }; + AssertJsonEquals(new JObject(){{"condition", "test-condition"}}, message); + } + + [Fact] + public void MultipleTargets() + { + var message = new Message() + { + Token = "test-token", + Topic = "test-topic", + }; + Assert.Throws(() => message.Validate()); + + message = new Message() + { + Token = "test-token", + Condition = "test-condition", + }; + Assert.Throws(() => message.Validate()); + + message = new Message() + { + Condition = "test-condition", + Topic = "test-topic", + }; + Assert.Throws(() => message.Validate()); + + message = new Message() + { + Token = "test-token", + Topic = "test-topic", + Condition = "test-condition", + }; + Assert.Throws(() => message.Validate()); + } + + [Fact] + public void DataMessage() + { + var message = new Message() + { + Topic = "test-topic", + Data = new Dictionary() + { + { "k1", "v1" }, + { "k2", "v2" }, + }, + }; + AssertJsonEquals(new JObject() + { + {"topic", "test-topic"}, + {"data", new JObject(){{"k1", "v1"}, {"k2", "v2"}}}, + }, message); + } + + [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.Validate()); + } + } + + [Fact] + public void PrefixedTopicName() + { + var message = new Message() + { + Topic = "/topics/test-topic" + }; + AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); + } + + private void AssertJsonEquals(JObject expected, Message actual) + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.Validate()); + var parsed = JObject.Parse(json); + Assert.True(JToken.DeepEquals(expected, parsed)); + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs new file mode 100644 index 00000000..45d9bc77 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -0,0 +1,103 @@ +// 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 Google.Apis.Http; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Something. + /// + public sealed class FirebaseMessaging: IFirebaseService + { + private readonly FirebaseMessagingClient _messagingClient; + + private FirebaseMessaging(FirebaseApp app) + { + _messagingClient = new FirebaseMessagingClient( + new HttpClientFactory(), app.Options.Credential, app.GetProjectId()); + } + + /// + /// Something. + /// + public async Task SendAsync(Message message) + { + return await SendAsync(message, false); + } + + /// + /// Something. + /// + public async Task SendAsync(Message message, CancellationToken cancellationToken) + { + return await SendAsync(message, false, cancellationToken); + } + + /// + /// Something. + /// + public async Task SendAsync(Message message, bool dryRun) + { + return await SendAsync(message, dryRun, default(CancellationToken)); + } + + /// + /// Something. + /// + public async Task SendAsync(Message message, bool dryRun, CancellationToken cancellationToken) + { + return await _messagingClient.SendAsync(message, dryRun, cancellationToken).ConfigureAwait(false); + } + + void IFirebaseService.Delete() + { + _messagingClient.Dispose(); + } + + /// + /// Something. + /// + public static FirebaseMessaging DefaultInstance + { + get + { + var app = FirebaseApp.DefaultInstance; + if (app == null) + { + return null; + } + return GetMessaging(app); + } + } + + /// + /// Something. + /// + public static FirebaseMessaging GetMessaging(FirebaseApp app) + { + if (app == null) + { + throw new ArgumentNullException("App argument must not be null."); + } + return app.GetOrInit(typeof(FirebaseMessaging).Name, () => + { + return new FirebaseMessaging(app); + }); + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs new file mode 100644 index 00000000..62f8ea38 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -0,0 +1,89 @@ +// 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.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Http; +using Google.Apis.Json; +using Google.Apis.Util; + +namespace FirebaseAdmin.Messaging +{ + internal sealed class FirebaseMessagingClient: IDisposable + { + private const string FcmUrl = "https://fcm.googleapis.com/v1/projects/{0}/messages:send"; + + private readonly ConfigurableHttpClient _httpClient; + private readonly string _sendUrl; + + internal FirebaseMessagingClient(HttpClientFactory clientFactory, GoogleCredential credential, string projectId) + { + if (string.IsNullOrEmpty(projectId)) + { + throw new FirebaseException( + "Project ID is required to access messaging service. Use a service account credential or " + + "set the project ID explicitly via AppOptions. Alternatively you can also " + + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); + } + _httpClient = clientFactory.ThrowIfNull(nameof(clientFactory)).CreateAuthorizedHttpClient(credential); + _sendUrl = string.Format(FcmUrl, projectId); + } + + public async Task SendAsync(Message message, bool dryRun, CancellationToken cancellationToken) + { + var payload = new Dictionary() + { + { "message", message.ThrowIfNull(nameof(message)).Validate() }, + }; + if (dryRun) + { + payload["validate_only"] = true; + } + try + { + var response = await _httpClient.PostJsonAsync(_sendUrl, payload, cancellationToken) + .ConfigureAwait(false); + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + var error = "Response status code does not indicate success: " + + $"{(int) response.StatusCode} ({response.StatusCode})" + + $"{Environment.NewLine}{json}"; + throw new FirebaseException(error); + } + var parsed = NewtonsoftJsonSerializer.Instance.Deserialize(json); + return parsed.Name; + } + catch (HttpRequestException e) + { + throw new FirebaseException("Error while calling the FCM service.", e); + } + } + + public void Dispose() + { + _httpClient.Dispose(); + } + } + + internal sealed class SendResponse + { + [Newtonsoft.Json.JsonProperty("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs new file mode 100644 index 00000000..7b03b48f --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -0,0 +1,92 @@ +// 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 System.Text.RegularExpressions; +using Newtonsoft.Json; +using Google.Apis.Json; +using Google.Apis.Util; +using FirebaseAdmin; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Something. + /// + public sealed class Message + { + /// + /// Something. + /// + [JsonProperty("token")] + public string Token { internal get; set; } + + /// + /// Something. + /// + [JsonProperty("topic")] + public string Topic { internal get; set; } + + /// + /// Something. + /// + [JsonProperty("condition")] + public string Condition { internal get; set; } + + /// + /// Something. + /// + [JsonProperty("data")] + public IDictionary Data { internal get; set; } + + internal Message Validate() + { + var list = new List() + { + Token, Topic, Condition, + }; + var targets = list.FindAll((target) => !string.IsNullOrEmpty(target)); + if (targets.Count != 1) + { + throw new ArgumentException("Exactly one of Token, Topic or Condition is required."); + } + return new Message() + { + Token = Token, + Topic = ValidateTopic(Topic), + Condition = Condition, + Data = Data, + }; + } + + private static string ValidateTopic(string topic) + { + if (string.IsNullOrEmpty(topic)) + { + return null; + } + if (topic.StartsWith("/topics/")) + { + topic = topic.Substring("/topics/".Length); + } + if (!Regex.IsMatch(topic, "^[a-zA-Z0-9-_.~%]+$")) + { + throw new ArgumentException("Malformed topic name."); + } + return topic; + } + } +} \ No newline at end of file From 72236d096a478c50ac6d8f66bd1af970c60cfcba Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Sun, 23 Sep 2018 03:05:12 -0700 Subject: [PATCH 02/50] More tests and code cleanup --- .../Messaging/FirebaseMessageClientTest.cs | 42 +++++++++++++++---- .../Messaging/FirebaseMessagingClient.cs | 20 +++++---- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs index d48d2469..e8b7bbf2 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.Net; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -32,22 +33,26 @@ public class FirebaseMessagingClientTest public void NoProjectId() { var clientFactory = new HttpClientFactory(); - Assert.Throws(() => new FirebaseMessagingClient(clientFactory, mockCredential, null)); - Assert.Throws(() => new FirebaseMessagingClient(clientFactory, mockCredential, "")); + Assert.Throws( + () => new FirebaseMessagingClient(clientFactory, mockCredential, null)); + Assert.Throws( + () => new FirebaseMessagingClient(clientFactory, mockCredential, "")); } [Fact] public void NoCredential() { var clientFactory = new HttpClientFactory(); - Assert.Throws(() => new FirebaseMessagingClient(clientFactory, null, "test-project")); + Assert.Throws( + () => new FirebaseMessagingClient(clientFactory, null, "test-project")); } [Fact] public void NoClientFactory() { var clientFactory = new HttpClientFactory(); - Assert.Throws(() => new FirebaseMessagingClient(null, mockCredential, "test-project")); + Assert.Throws( + () => new FirebaseMessagingClient(null, mockCredential, "test-project")); } [Fact] @@ -57,7 +62,7 @@ public async Task Send() { Response = new SendResponse() { - Name = "test-response", + Name = "test-response", }, }; var factory = new MockHttpClientFactory(handler); @@ -66,7 +71,7 @@ public async Task Send() { Topic = "test-topic" }; - var response = await client.SendAsync(message, false, default(CancellationToken)); + var response = await client.SendAsync(message); Assert.Equal("test-response", response); var req = NewtonsoftJsonSerializer.Instance.Deserialize(handler.Request); Assert.Equal("test-topic", req.Message.Topic); @@ -74,13 +79,36 @@ public async Task Send() Assert.Equal(1, handler.Calls); // Send in dryRun mode. - response = await client.SendAsync(message, true, default(CancellationToken)); + response = await client.SendAsync(message, dryRun: true); Assert.Equal("test-response", response); req = NewtonsoftJsonSerializer.Instance.Deserialize(handler.Request); Assert.Equal("test-topic", req.Message.Topic); Assert.True(req.ValidateOnly); Assert.Equal(2, handler.Calls); } + + [Fact] + public async Task HttpError() + { + 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 = NewtonsoftJsonSerializer.Instance.Deserialize(handler.Request); + Assert.Equal("test-topic", req.Message.Topic); + Assert.False(req.ValidateOnly); + Assert.Equal(1, handler.Calls); + } } internal sealed class SendRequest diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs index 62f8ea38..129b5027 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -31,20 +31,24 @@ internal sealed class FirebaseMessagingClient: IDisposable private readonly ConfigurableHttpClient _httpClient; private readonly string _sendUrl; - internal FirebaseMessagingClient(HttpClientFactory clientFactory, GoogleCredential credential, string projectId) + internal FirebaseMessagingClient( + HttpClientFactory clientFactory, GoogleCredential credential, string projectId) { if (string.IsNullOrEmpty(projectId)) { throw new FirebaseException( - "Project ID is required to access messaging service. Use a service account credential or " - + "set the project ID explicitly via AppOptions. Alternatively you can also " - + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); + "Project ID is required to access messaging service. Use a service account " + + "credential or set the project ID explicitly via AppOptions. Alternatively " + + "you can set the project ID via the GOOGLE_CLOUD_PROJECT environment " + + "variable."); } - _httpClient = clientFactory.ThrowIfNull(nameof(clientFactory)).CreateAuthorizedHttpClient(credential); + _httpClient = clientFactory.ThrowIfNull(nameof(clientFactory)) + .CreateAuthorizedHttpClient(credential); _sendUrl = string.Format(FcmUrl, projectId); } - public async Task SendAsync(Message message, bool dryRun, CancellationToken cancellationToken) + public async Task SendAsync(Message message, + bool dryRun = false, CancellationToken cancellationToken = default(CancellationToken)) { var payload = new Dictionary() { @@ -56,8 +60,8 @@ public async Task SendAsync(Message message, bool dryRun, CancellationTo } try { - var response = await _httpClient.PostJsonAsync(_sendUrl, payload, cancellationToken) - .ConfigureAwait(false); + var response = await _httpClient.PostJsonAsync( + _sendUrl, payload, cancellationToken).ConfigureAwait(false); var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (!response.IsSuccessStatusCode) { From 65e3f1218c7df45e866a43e0fb4f990d58bd72a0 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Sun, 23 Sep 2018 20:18:46 -0700 Subject: [PATCH 03/50] Added Notification type; Renamed MessagingTest to MessageTest --- .../Messaging/FirebaseMessageClientTest.cs | 9 ---- .../{MessagingTest.cs => MessageTest.cs} | 32 ++++++++++- .../Messaging/FirebaseMessaging.cs | 54 ++++++++++++++++--- .../Messaging/FirebaseMessagingClient.cs | 21 +++++--- .../FirebaseAdmin/Messaging/Message.cs | 24 +++++++-- .../FirebaseAdmin/Messaging/Notification.cs | 36 +++++++++++++ 6 files changed, 145 insertions(+), 31 deletions(-) rename FirebaseAdmin/FirebaseAdmin.Tests/Messaging/{MessagingTest.cs => MessageTest.cs} (81%) create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs index e8b7bbf2..daae1942 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs @@ -110,13 +110,4 @@ public async Task HttpError() Assert.Equal(1, handler.Calls); } } - - internal sealed class SendRequest - { - [Newtonsoft.Json.JsonProperty("message")] - public Message Message { get; set; } - - [Newtonsoft.Json.JsonProperty("validate_only")] - public bool ValidateOnly { get; set; } - } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs similarity index 81% rename from FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessagingTest.cs rename to FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index ef3832b9..7440d3b7 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -21,7 +21,7 @@ namespace FirebaseAdmin.Messaging.Tests { - public class MessagingTest + public class MessageTest { [Fact] public void MessageWithoutTarget() @@ -130,11 +130,39 @@ public void PrefixedTopicName() AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, 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"}, + } + }, + }; + AssertJsonEquals(expected, message); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.Validate()); var parsed = JObject.Parse(json); - Assert.True(JToken.DeepEquals(expected, parsed)); + Assert.True( + JToken.DeepEquals(expected, parsed), + $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}"); } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index 45d9bc77..a39557f7 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -20,7 +20,8 @@ namespace FirebaseAdmin.Messaging { /// - /// Something. + /// This is the entry point to all server-side Firebase Cloud Messaging (FCM) operations. You + /// can get an instance of this class via FirebaseMessaging.DefaultInstance. /// public sealed class FirebaseMessaging: IFirebaseService { @@ -33,32 +34,66 @@ private FirebaseMessaging(FirebaseApp app) } /// - /// Something. + /// Sends a message via the FCM service. /// + /// A task that completes with a message ID string, which represents a + /// successful handoff to FCM. + /// If the message argument is null. + /// If an error occurs while sending the message. + /// The message to be sent. Must not be null. public async Task SendAsync(Message message) { return await SendAsync(message, false); } /// - /// Something. + /// Sends a message via the FCM service. /// + /// A task that completes with a message ID string, which represents a + /// successful handoff to FCM. + /// If the message argument is null. + /// If an error occurs while sending the message. + /// The message to be sent. Must not be null. + /// A cancellation token to monitor the asynchronous + /// operation. public async Task SendAsync(Message message, CancellationToken cancellationToken) { return await SendAsync(message, false, cancellationToken); } /// - /// Something. + /// Sends a message via the FCM service. + /// If the option is set to true, the message will not be + /// actually sent. Instead, FCM performs all the necessary validations, and emulates the + /// send operation. /// + /// A task that completes with a message ID string, which represents a + /// successful handoff to FCM. + /// If the message argument is null. + /// If an error occurs while sending the message. + /// The message to be sent. Must not be null. + /// A boolean indicating whether to perform a dry run (validation + /// only) of the send. public async Task SendAsync(Message message, bool dryRun) { return await SendAsync(message, dryRun, default(CancellationToken)); } /// - /// Something. + /// Sends a message via the FCM service. + /// If the option is set to true, the message will not be + /// actually sent. Instead, FCM performs all the necessary validations, and emulates the + /// send operation. /// + /// A task that completes with a message ID string, which represents a + /// successful handoff to FCM. + /// If the message argument is null. + /// If an error occurs while sending the message. + /// The message to be sent. Must not be null. + /// A boolean indicating whether to perform a dry run (validation + /// only) of the send. + /// A cancellation token to monitor the asynchronous + /// operation. public async Task SendAsync(Message message, bool dryRun, CancellationToken cancellationToken) { return await _messagingClient.SendAsync(message, dryRun, cancellationToken).ConfigureAwait(false); @@ -70,7 +105,8 @@ void IFirebaseService.Delete() } /// - /// Something. + /// The messaging instance associated with the default Firebase app. This property is + /// null if the default app doesn't yet exist. /// public static FirebaseMessaging DefaultInstance { @@ -86,8 +122,12 @@ public static FirebaseMessaging DefaultInstance } /// - /// Something. + /// Returns the messaging instance for the specified app. /// + /// The instance associated with the specified + /// app. + /// If the app argument is null. + /// An app instance. public static FirebaseMessaging GetMessaging(FirebaseApp app) { if (app == null) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs index 129b5027..fb2a797f 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -13,7 +13,6 @@ // limitations under the License. using System; -using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -50,18 +49,15 @@ internal FirebaseMessagingClient( public async Task SendAsync(Message message, bool dryRun = false, CancellationToken cancellationToken = default(CancellationToken)) { - var payload = new Dictionary() + var request = new SendRequest() { - { "message", message.ThrowIfNull(nameof(message)).Validate() }, + Message = message.ThrowIfNull(nameof(message)).Validate(), + ValidateOnly = dryRun, }; - if (dryRun) - { - payload["validate_only"] = true; - } try { var response = await _httpClient.PostJsonAsync( - _sendUrl, payload, cancellationToken).ConfigureAwait(false); + _sendUrl, request, cancellationToken).ConfigureAwait(false); var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (!response.IsSuccessStatusCode) { @@ -85,6 +81,15 @@ public void Dispose() } } + internal sealed class SendRequest + { + [Newtonsoft.Json.JsonProperty("message")] + public Message Message { get; set; } + + [Newtonsoft.Json.JsonProperty("validate_only")] + public bool ValidateOnly { get; set; } + } + internal sealed class SendResponse { [Newtonsoft.Json.JsonProperty("name")] diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 7b03b48f..ac12108e 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -24,34 +24,47 @@ namespace FirebaseAdmin.Messaging { /// - /// Something. + /// Represents a message that can be sent via Firebase Cloud Messaging (FCM). Contains payload + /// information as well as the recipient information. The recipient information must be + /// specified by setting exactly one of the , or + /// fields. /// public sealed class Message { /// - /// Something. + /// The registration token of the device to which the message should be sent. /// [JsonProperty("token")] public string Token { internal get; set; } /// - /// Something. + /// The name of the FCM topic to which the message should be sent. Topic names may + /// contain the /topics/ prefix. /// [JsonProperty("topic")] public string Topic { internal get; set; } /// - /// Something. + /// The FCM condition to which the message should be sent. Must be a valid condition + /// string such as "'foo' in topics". /// [JsonProperty("condition")] public string Condition { internal get; set; } /// - /// Something. + /// A collection of key-value pairs that will be added to the message as data fields. Keys + /// and the values must not be null. /// [JsonProperty("data")] public IDictionary Data { internal get; set; } + /// + /// The information to be included in + /// the message. + /// + [JsonProperty("notification")] + public Notification Notification { internal get; set; } + internal Message Validate() { var list = new List() @@ -69,6 +82,7 @@ internal Message Validate() Topic = ValidateTopic(Topic), Condition = Condition, Data = Data, + Notification = Notification, }; } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs new file mode 100644 index 00000000..b57da79a --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs @@ -0,0 +1,36 @@ +// 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 Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents the notification parameters that can be included in a . + /// + public sealed class Notification + { + /// + /// Title of the notification. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// Body of the notification. + /// + [JsonProperty("body")] + public string Body { get; set; } + } +} \ No newline at end of file From 8e7ee5553b52892f0448d1849b0f2c3e0db5155d Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Sun, 23 Sep 2018 20:52:51 -0700 Subject: [PATCH 04/50] Cancellation test --- .../Messaging/FirebaseMessagingTest.cs | 26 +++++++++--- .../Messaging/MessageTest.cs | 25 +++-------- .../Messaging/FirebaseMessaging.cs | 42 +++++++++++++------ 3 files changed, 55 insertions(+), 38 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs index 94596bcd..b36aabcb 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.Threading; using System.Threading.Tasks; using Xunit; using Google.Apis.Auth.OAuth2; @@ -21,7 +22,7 @@ namespace FirebaseAdmin.Messaging.Tests { - public class FirebaseMessagingTest + public class FirebaseMessagingTest: IDisposable { private static readonly GoogleCredential mockCredential = GoogleCredential.FromFile("./resources/service_account.json"); @@ -58,12 +59,25 @@ public async Task UseAfterDelete() var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential}); FirebaseMessaging messaging = FirebaseMessaging.DefaultInstance; app.Delete(); - var message = new Message() - { - Topic = "test-topic", - }; await Assert.ThrowsAsync( - async () => await messaging.SendAsync(message)); + 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(); } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 7440d3b7..2636da7e 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -32,22 +32,13 @@ public void MessageWithoutTarget() [Fact] public void EmptyMessage() { - var message = new Message() - { - Token = "test-token" - }; + var message = new Message(){Token = "test-token"}; AssertJsonEquals(new JObject(){{"token", "test-token"}}, message); - message = new Message() - { - Topic = "test-topic" - }; + message = new Message(){Topic = "test-topic"}; AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); - message = new Message() - { - Condition = "test-condition" - }; + message = new Message(){Condition = "test-condition"}; AssertJsonEquals(new JObject(){{"condition", "test-condition"}}, message); } @@ -112,10 +103,7 @@ public void InvalidTopicNames() }; foreach (var topic in topics) { - var message = new Message() - { - Topic = topic - }; + var message = new Message(){Topic = topic}; Assert.Throws(() => message.Validate()); } } @@ -123,10 +111,7 @@ public void InvalidTopicNames() [Fact] public void PrefixedTopicName() { - var message = new Message() - { - Topic = "/topics/test-topic" - }; + var message = new Message(){Topic = "/topics/test-topic"}; AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index a39557f7..620b32f8 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -34,11 +34,15 @@ private FirebaseMessaging(FirebaseApp app) } /// - /// Sends a message via the FCM service. + /// Sends a message to the FCM service for delivery. The message gets validated both by + /// the Admin SDK, and the remote FCM service. A successful return value indicates + /// that the message has been successfully sent to FCM, where it has been accepted by the + /// FCM service. /// - /// A task that completes with a message ID string, which represents a + /// A task that completes with a message ID string, which represents /// successful handoff to FCM. /// If the message argument is null. + /// If the message contains any invalid fields. /// If an error occurs while sending the message. /// The message to be sent. Must not be null. public async Task SendAsync(Message message) @@ -47,11 +51,15 @@ public async Task SendAsync(Message message) } /// - /// Sends a message via the FCM service. + /// Sends a message to the FCM service for delivery. The message gets validated both by + /// the Admin SDK, and the remote FCM service. A successful return value indicates + /// that the message has been successfully sent to FCM, where it has been accepted by the + /// FCM service. /// - /// A task that completes with a message ID string, which represents a + /// A task that completes with a message ID string, which represents /// successful handoff to FCM. /// If the message argument is null. + /// If the message contains any invalid fields. /// If an error occurs while sending the message. /// The message to be sent. Must not be null. /// A cancellation token to monitor the asynchronous @@ -62,14 +70,19 @@ public async Task SendAsync(Message message, CancellationToken cancellat } /// - /// Sends a message via the FCM service. + /// Sends a message to the FCM service for delivery. The message gets validated both by + /// the Admin SDK, and the remote FCM service. A successful return value indicates + /// that the message has been successfully sent to FCM, where it has been accepted by the + /// FCM service. /// If the option is set to true, the message will not be - /// actually sent. Instead, FCM performs all the necessary validations, and emulates the - /// send operation. + /// actually sent to the recipients. Instead, the FCM service performs all the necessary + /// validations, and emulates the send operation. This is a good way to check if a + /// certain message will be accepted by FCM for delivery. /// - /// A task that completes with a message ID string, which represents a + /// A task that completes with a message ID string, which represents /// successful handoff to FCM. /// If the message argument is null. + /// If the message contains any invalid fields. /// If an error occurs while sending the message. /// The message to be sent. Must not be null. /// A boolean indicating whether to perform a dry run (validation @@ -80,14 +93,19 @@ public async Task SendAsync(Message message, bool dryRun) } /// - /// Sends a message via the FCM service. + /// Sends a message to the FCM service for delivery. The message gets validated both by + /// the Admin SDK, and the remote FCM service. A successful return value indicates + /// that the message has been successfully sent to FCM, where it has been accepted by the + /// FCM service. /// If the option is set to true, the message will not be - /// actually sent. Instead, FCM performs all the necessary validations, and emulates the - /// send operation. + /// actually sent to the recipients. Instead, the FCM service performs all the necessary + /// validations, and emulates the send operation. This is a good way to check if a + /// certain message will be accepted by FCM for delivery. /// - /// A task that completes with a message ID string, which represents a + /// A task that completes with a message ID string, which represents /// successful handoff to FCM. /// If the message argument is null. + /// If the message contains any invalid fields. /// If an error occurs while sending the message. /// The message to be sent. Must not be null. /// A boolean indicating whether to perform a dry run (validation From d9b840e9a99fa4555f4f30dd7bb5511ecfe40a56 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 26 Sep 2018 22:06:56 -0700 Subject: [PATCH 05/50] Adjusted line length --- .../Messaging/FirebaseMessaging.cs | 30 ++++++++++++------- .../FirebaseAdmin/Messaging/Message.cs | 3 +- .../FirebaseAdmin/Messaging/Notification.cs | 4 +-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index 620b32f8..b8619d1c 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -42,8 +42,10 @@ private FirebaseMessaging(FirebaseApp app) /// A task that completes with a message ID string, which represents /// successful handoff to FCM. /// If the message argument is null. - /// If the message contains any invalid fields. - /// If an error occurs while sending the message. + /// If the message contains any invalid + /// fields. + /// If an error occurs while sending the + /// message. /// The message to be sent. Must not be null. public async Task SendAsync(Message message) { @@ -59,8 +61,10 @@ public async Task SendAsync(Message message) /// A task that completes with a message ID string, which represents /// successful handoff to FCM. /// If the message argument is null. - /// If the message contains any invalid fields. - /// If an error occurs while sending the message. + /// If the message contains any invalid + /// fields. + /// If an error occurs while sending the + /// message. /// The message to be sent. Must not be null. /// A cancellation token to monitor the asynchronous /// operation. @@ -82,8 +86,10 @@ public async Task SendAsync(Message message, CancellationToken cancellat /// A task that completes with a message ID string, which represents /// successful handoff to FCM. /// If the message argument is null. - /// If the message contains any invalid fields. - /// If an error occurs while sending the message. + /// If the message contains any invalid + /// fields. + /// If an error occurs while sending the + /// message. /// The message to be sent. Must not be null. /// A boolean indicating whether to perform a dry run (validation /// only) of the send. @@ -105,16 +111,20 @@ public async Task SendAsync(Message message, bool dryRun) /// A task that completes with a message ID string, which represents /// successful handoff to FCM. /// If the message argument is null. - /// If the message contains any invalid fields. - /// If an error occurs while sending the message. + /// If the message contains any invalid + /// fields. + /// If an error occurs while sending the + /// message. /// The message to be sent. Must not be null. /// A boolean indicating whether to perform a dry run (validation /// only) of the send. /// A cancellation token to monitor the asynchronous /// operation. - public async Task SendAsync(Message message, bool dryRun, CancellationToken cancellationToken) + public async Task SendAsync( + Message message, bool dryRun, CancellationToken cancellationToken) { - return await _messagingClient.SendAsync(message, dryRun, cancellationToken).ConfigureAwait(false); + return await _messagingClient.SendAsync( + message, dryRun, cancellationToken).ConfigureAwait(false); } void IFirebaseService.Delete() diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index ac12108e..7753bede 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -74,7 +74,8 @@ internal Message Validate() var targets = list.FindAll((target) => !string.IsNullOrEmpty(target)); if (targets.Count != 1) { - throw new ArgumentException("Exactly one of Token, Topic or Condition is required."); + throw new ArgumentException( + "Exactly one of Token, Topic or Condition is required."); } return new Message() { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs index b57da79a..0a284d3b 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs @@ -25,12 +25,12 @@ public sealed class Notification /// Title of the notification. /// [JsonProperty("title")] - public string Title { get; set; } + public string Title { internal get; set; } /// /// Body of the notification. /// [JsonProperty("body")] - public string Body { get; set; } + public string Body { internal get; set; } } } \ No newline at end of file From e241df0ccc9181b971202877140b5be4e7c1fc80 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Sat, 29 Sep 2018 20:05:42 -0700 Subject: [PATCH 06/50] Added integration test case --- .../FirebaseAuthTest.cs | 1 - .../FirebaseMessagingTest.cs | 46 +++++++++++++++++++ .../Messaging/FirebaseMessaging.cs | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs index 435f3f59..bbea2b2f 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs @@ -18,7 +18,6 @@ using System.Text; using System.Threading.Tasks; using Xunit; -using FirebaseAdmin; using FirebaseAdmin.Auth; using Google.Apis.Auth.OAuth2; using Google.Apis.Util; diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs new file mode 100644 index 00000000..a6c9059f --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -0,0 +1,46 @@ +// 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.Text.RegularExpressions; +using System.Threading.Tasks; +using Xunit; +using FirebaseAdmin.Messaging; + +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", + }, + }; + var id = await FirebaseMessaging.DefaultInstance.SendAsync(message, dryRun: true); + Assert.True(!string.IsNullOrEmpty(id)); + Assert.Matches(new Regex("^projects/.*/messages/.*$"), id); + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index b8619d1c..3c2111ea 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -129,7 +129,7 @@ public async Task SendAsync( void IFirebaseService.Delete() { - _messagingClient.Dispose(); + _messagingClient.Dispose(); } /// From 43c119532c8a324d4b1edfc2093638b86e2ccc3a Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Sun, 30 Sep 2018 07:15:43 -0700 Subject: [PATCH 07/50] Added AndroidConfig type; Improved validation logic --- .../FirebaseMessagingTest.cs | 7 + .../Messaging/MessageTest.cs | 84 ++++++++++ .../FirebaseAdmin/Messaging/AndroidConfig.cs | 146 ++++++++++++++++++ .../Messaging/FirebaseMessagingClient.cs | 2 +- .../FirebaseAdmin/Messaging/Message.cs | 87 +++++++---- 5 files changed, 294 insertions(+), 32 deletions(-) create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index a6c9059f..fe4bf88a 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -12,6 +12,7 @@ // 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 Xunit; @@ -37,6 +38,12 @@ public async Task Send() Title = "Title", Body = "Body", }, + AndroidConfig = new AndroidConfig() + { + Priority = Priority.NORMAL, + Ttl = TimeSpan.FromHours(1), + RestrictedPackageName = "com.google.firebase.testing", + }, }; var id = await FirebaseMessaging.DefaultInstance.SendAsync(message, dryRun: true); Assert.True(!string.IsNullOrEmpty(id)); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 2636da7e..216e2355 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -141,6 +141,90 @@ public void Notification() AssertJsonEquals(expected, message); } + [Fact] + public void AndroidConfig() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + CollapseKey = "collapse-key", + Priority = Priority.HIGH, + Ttl = TimeSpan.FromMilliseconds(10), + RestrictedPackageName = "test-pkg-name", + Data = new Dictionary() + { + { "k1", "v1" }, + { "k2", "v2" }, + }, + }, + }; + 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"}}}, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void AndroidConfigFullSecondsTTL() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + Ttl = TimeSpan.FromHours(1), + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "android", new JObject() + { + { "ttl", "3600s" }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void AndroidConfigInvalidTTL() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + Ttl = TimeSpan.FromHours(-1), + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "android", new JObject() + { + { "ttl", "3600s" }, + } + }, + }; + Assert.Throws(() => message.Validate()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.Validate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs new file mode 100644 index 00000000..1e8e8a58 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -0,0 +1,146 @@ +// 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 Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents the Android-specific options that can be included in a . + /// + public sealed class AndroidConfig + { + /// + /// A collapse key for the message. Collapse key serves as an identifier for a group of + /// messages that can be collapsed, so that only the last message gets sent when delivery can be + /// resumed. A maximum of 4 different collapse keys may be active at any given time. + /// + public string CollapseKey { internal get; set; } + + /// + /// The priority of the message. + /// + public Priority Priority { internal get; set; } + + /// + /// The time-to-live duration of the message. + /// + public TimeSpan Ttl { internal get; set; } + + /// + /// The package name of the application where the registration tokens must match in order + /// to receive the message. + /// + public string RestrictedPackageName { internal get; set; } + + /// + /// A collection of key-value pairs that will be added to the message as data fields. Keys + /// and the values must not be null. When set, overrides any data fields set on the top-level + /// . + /// + public IDictionary Data { internal get; set; } + + internal ValidatedAndroidConfig Validate() + { + return new ValidatedAndroidConfig() + { + CollapseKey = this.CollapseKey, + Priority = this.PriorityString, + Ttl = this.TTLString, + RestrictedPackageName = this.RestrictedPackageName, + Data = this.Data, + }; + } + + private string PriorityString + { + get + { + switch (Priority) + { + case Messaging.Priority.HIGH: + return "high"; + case Messaging.Priority.NORMAL: + return "normal"; + default: + return null; + } + } + } + + private string TTLString + { + get + { + if (Ttl == null) + { + return null; + } + var totalSeconds = Ttl.TotalSeconds; + if (totalSeconds < 0) + { + throw new ArgumentException("TTL must not be negative."); + } + var seconds = (long) Math.Floor(totalSeconds); + var subsecondNanos = (long) ((totalSeconds - seconds) * 1e9); + if (subsecondNanos > 0) + { + return String.Format("{0}.{1:D9}s", seconds, subsecondNanos); + } + return String.Format("{0}s", seconds); + } + } + } + + internal sealed class ValidatedAndroidConfig + { + [JsonProperty("collapse_key")] + internal string CollapseKey { get; set; } + + [JsonProperty("priority")] + internal string Priority { get; set; } + + [JsonProperty("ttl")] + internal string Ttl { get; set; } + + [JsonProperty("restricted_package_name")] + internal string RestrictedPackageName { get; set; } + + [JsonProperty("data")] + internal IDictionary Data { get; set; } + } + + /// + /// Something. + /// + public enum Priority + { + /// + /// Something. + /// + UNSPECIFIED = 0, + + /// + /// Something. + /// + HIGH = 1, + + /// + /// Something. + /// + NORMAL = 2, + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs index fb2a797f..5def4f65 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -84,7 +84,7 @@ public void Dispose() internal sealed class SendRequest { [Newtonsoft.Json.JsonProperty("message")] - public Message Message { get; set; } + public ValidatedMessage Message { get; set; } [Newtonsoft.Json.JsonProperty("validate_only")] public bool ValidateOnly { get; set; } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 7753bede..563f1a8a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -34,38 +34,37 @@ public sealed class Message /// /// The registration token of the device to which the message should be sent. /// - [JsonProperty("token")] - public string Token { internal get; set; } + public string Token { private get; set; } /// /// The name of the FCM topic to which the message should be sent. Topic names may /// contain the /topics/ prefix. /// - [JsonProperty("topic")] - public string Topic { internal get; set; } + public string Topic { private get; set; } /// /// The FCM condition to which the message should be sent. Must be a valid condition /// string such as "'foo' in topics". /// - [JsonProperty("condition")] - public string Condition { internal get; set; } + public string Condition { private get; set; } /// /// A collection of key-value pairs that will be added to the message as data fields. Keys /// and the values must not be null. /// - [JsonProperty("data")] - public IDictionary Data { internal get; set; } + public IDictionary Data { private get; set; } /// - /// The information to be included in - /// the message. + /// The notification information to be included in the message. /// - [JsonProperty("notification")] - public Notification Notification { internal get; set; } + public Notification Notification { private get; set; } - internal Message Validate() + /// + /// The Android-specific information to be included in the message. + /// + public AndroidConfig AndroidConfig { private get; set; } + + internal ValidatedMessage Validate() { var list = new List() { @@ -77,31 +76,57 @@ internal Message Validate() throw new ArgumentException( "Exactly one of Token, Topic or Condition is required."); } - return new Message() + return new ValidatedMessage() { - Token = Token, - Topic = ValidateTopic(Topic), - Condition = Condition, - Data = Data, - Notification = Notification, + Token = this.Token, + Topic = this.ValidatedTopic, + Condition = this.Condition, + Data = this.Data, + Notification = this.Notification, + AndroidConfig = this.AndroidConfig?.Validate(), }; } - private static string ValidateTopic(string topic) + private string ValidatedTopic { - if (string.IsNullOrEmpty(topic)) - { - return null; - } - if (topic.StartsWith("/topics/")) + get { - topic = topic.Substring("/topics/".Length); + if (string.IsNullOrEmpty(Topic)) + { + return null; + } + var topic = Topic; + if (topic.StartsWith("/topics/")) + { + topic = topic.Substring("/topics/".Length); + } + if (!Regex.IsMatch(topic, "^[a-zA-Z0-9-_.~%]+$")) + { + throw new ArgumentException("Malformed topic name."); + } + return topic; } - if (!Regex.IsMatch(topic, "^[a-zA-Z0-9-_.~%]+$")) - { - throw new ArgumentException("Malformed topic name."); - } - return topic; } } + + internal sealed class ValidatedMessage + { + [JsonProperty("token")] + internal string Token { get; set; } + + [JsonProperty("topic")] + internal string Topic { get; set; } + + [JsonProperty("condition")] + internal string Condition { get; set; } + + [JsonProperty("data")] + internal IDictionary Data { get; set; } + + [JsonProperty("notification")] + internal Notification Notification { get; set; } + + [JsonProperty("android")] + internal ValidatedAndroidConfig AndroidConfig { get; set; } + } } \ No newline at end of file From 91188a0b2c1d1d0a60efa9fde00a4371d304528c Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Sun, 30 Sep 2018 21:45:49 -0700 Subject: [PATCH 08/50] Making priority nullable --- .../FirebaseAdmin/Messaging/AndroidConfig.cs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 1e8e8a58..eb94ae7a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -33,7 +33,7 @@ public sealed class AndroidConfig /// /// The priority of the message. /// - public Priority Priority { internal get; set; } + public Priority? Priority { internal get; set; } /// /// The time-to-live duration of the message. @@ -59,7 +59,7 @@ internal ValidatedAndroidConfig Validate() { CollapseKey = this.CollapseKey, Priority = this.PriorityString, - Ttl = this.TTLString, + Ttl = this.TtlString, RestrictedPackageName = this.RestrictedPackageName, Data = this.Data, }; @@ -81,7 +81,7 @@ private string PriorityString } } - private string TTLString + private string TtlString { get { @@ -124,23 +124,18 @@ internal sealed class ValidatedAndroidConfig } /// - /// Something. + /// Priority levels that can be set on an . /// public enum Priority - { - /// - /// Something. - /// - UNSPECIFIED = 0, - + { /// - /// Something. + /// High priority message. /// - HIGH = 1, + HIGH, /// - /// Something. + /// Normal priority message. /// - NORMAL = 2, + NORMAL, } } \ No newline at end of file From 588b658d48dbbeffaf0b18c4eb3476433892e5de Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 6 Dec 2018 14:13:50 -0800 Subject: [PATCH 09/50] Renamed Ttl to TimeToLive; Renamed Piority enum constants; Using IEnumerable instead of IDictionary --- .../FirebaseMessagingTest.cs | 4 ++-- .../Messaging/MessageTest.cs | 8 +++---- .../FirebaseAdmin/Messaging/AndroidConfig.cs | 22 +++++++++---------- .../FirebaseAdmin/Messaging/Message.cs | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index fe4bf88a..c23e4adb 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -40,8 +40,8 @@ public async Task Send() }, AndroidConfig = new AndroidConfig() { - Priority = Priority.NORMAL, - Ttl = TimeSpan.FromHours(1), + Priority = Priority.Normal, + TimeToLive = TimeSpan.FromHours(1), RestrictedPackageName = "com.google.firebase.testing", }, }; diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 216e2355..b2c5dc3c 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -150,8 +150,8 @@ public void AndroidConfig() AndroidConfig = new AndroidConfig() { CollapseKey = "collapse-key", - Priority = Priority.HIGH, - Ttl = TimeSpan.FromMilliseconds(10), + Priority = Priority.High, + TimeToLive = TimeSpan.FromMilliseconds(10), RestrictedPackageName = "test-pkg-name", Data = new Dictionary() { @@ -185,7 +185,7 @@ public void AndroidConfigFullSecondsTTL() Topic = "test-topic", AndroidConfig = new AndroidConfig() { - Ttl = TimeSpan.FromHours(1), + TimeToLive = TimeSpan.FromHours(1), }, }; var expected = new JObject() @@ -209,7 +209,7 @@ public void AndroidConfigInvalidTTL() Topic = "test-topic", AndroidConfig = new AndroidConfig() { - Ttl = TimeSpan.FromHours(-1), + TimeToLive = TimeSpan.FromHours(-1), }, }; var expected = new JObject() diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index eb94ae7a..93bf137b 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -38,7 +38,7 @@ public sealed class AndroidConfig /// /// The time-to-live duration of the message. /// - public TimeSpan Ttl { internal get; set; } + public TimeSpan TimeToLive { internal get; set; } /// /// The package name of the application where the registration tokens must match in order @@ -51,7 +51,7 @@ public sealed class AndroidConfig /// and the values must not be null. When set, overrides any data fields set on the top-level /// . /// - public IDictionary Data { internal get; set; } + public IEnumerable> Data { internal get; set; } internal ValidatedAndroidConfig Validate() { @@ -59,7 +59,7 @@ internal ValidatedAndroidConfig Validate() { CollapseKey = this.CollapseKey, Priority = this.PriorityString, - Ttl = this.TtlString, + TimeToLive = this.TtlString, RestrictedPackageName = this.RestrictedPackageName, Data = this.Data, }; @@ -71,9 +71,9 @@ private string PriorityString { switch (Priority) { - case Messaging.Priority.HIGH: + case Messaging.Priority.High: return "high"; - case Messaging.Priority.NORMAL: + case Messaging.Priority.Normal: return "normal"; default: return null; @@ -85,11 +85,11 @@ private string TtlString { get { - if (Ttl == null) + if (TimeToLive == null) { return null; } - var totalSeconds = Ttl.TotalSeconds; + var totalSeconds = TimeToLive.TotalSeconds; if (totalSeconds < 0) { throw new ArgumentException("TTL must not be negative."); @@ -114,13 +114,13 @@ internal sealed class ValidatedAndroidConfig internal string Priority { get; set; } [JsonProperty("ttl")] - internal string Ttl { get; set; } + internal string TimeToLive { get; set; } [JsonProperty("restricted_package_name")] internal string RestrictedPackageName { get; set; } [JsonProperty("data")] - internal IDictionary Data { get; set; } + internal IEnumerable> Data { get; set; } } /// @@ -131,11 +131,11 @@ public enum Priority /// /// High priority message. /// - HIGH, + High, /// /// Normal priority message. /// - NORMAL, + Normal, } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 563f1a8a..34a1da2c 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -52,7 +52,7 @@ public sealed class Message /// A collection of key-value pairs that will be added to the message as data fields. Keys /// and the values must not be null. /// - public IDictionary Data { private get; set; } + public IEnumerable> Data { private get; set; } /// /// The notification information to be included in the message. @@ -121,7 +121,7 @@ internal sealed class ValidatedMessage internal string Condition { get; set; } [JsonProperty("data")] - internal IDictionary Data { get; set; } + internal IEnumerable> Data { get; set; } [JsonProperty("notification")] internal Notification Notification { get; set; } From 3f275f56e549ad738628580f2c863ed1067ddb84 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 6 Dec 2018 14:41:43 -0800 Subject: [PATCH 10/50] Using readonly dict when possible --- FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs | 4 ++-- FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 93bf137b..155aa7a2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -51,7 +51,7 @@ public sealed class AndroidConfig /// and the values must not be null. When set, overrides any data fields set on the top-level /// . /// - public IEnumerable> Data { internal get; set; } + public IReadOnlyDictionary Data { internal get; set; } internal ValidatedAndroidConfig Validate() { @@ -120,7 +120,7 @@ internal sealed class ValidatedAndroidConfig internal string RestrictedPackageName { get; set; } [JsonProperty("data")] - internal IEnumerable> Data { get; set; } + internal IReadOnlyDictionary Data { get; set; } } /// diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 34a1da2c..5c4f4cb9 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -14,7 +14,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; using Google.Apis.Json; @@ -52,7 +51,7 @@ public sealed class Message /// A collection of key-value pairs that will be added to the message as data fields. Keys /// and the values must not be null. /// - public IEnumerable> Data { private get; set; } + public IReadOnlyDictionary Data { private get; set; } /// /// The notification information to be included in the message. @@ -121,7 +120,7 @@ internal sealed class ValidatedMessage internal string Condition { get; set; } [JsonProperty("data")] - internal IEnumerable> Data { get; set; } + internal IReadOnlyDictionary Data { get; set; } [JsonProperty("notification")] internal Notification Notification { get; set; } From 97710dd5231d83ebdcf83e1e583dc9a74378214c Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 6 Dec 2018 14:44:44 -0800 Subject: [PATCH 11/50] Adding newline at eof --- .../FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs | 2 +- .../FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs | 2 +- .../FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs | 2 +- FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs | 2 +- .../FirebaseAdmin/Messaging/FirebaseMessagingClient.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index c23e4adb..9573cfba 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -50,4 +50,4 @@ public async Task Send() Assert.Matches(new Regex("^projects/.*/messages/.*$"), id); } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs index daae1942..2a6742df 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs @@ -110,4 +110,4 @@ public async Task HttpError() Assert.Equal(1, handler.Calls); } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs index b36aabcb..22e62801 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs @@ -80,4 +80,4 @@ public void Dispose() FirebaseApp.DeleteAll(); } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index b2c5dc3c..0ecc1cab 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -234,4 +234,4 @@ private void AssertJsonEquals(JObject expected, Message actual) $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}"); } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 155aa7a2..707fd116 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -138,4 +138,4 @@ public enum Priority /// Normal, } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index 3c2111ea..ffe621f1 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -168,4 +168,4 @@ public static FirebaseMessaging GetMessaging(FirebaseApp app) }); } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs index 5def4f65..5f2b673a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -95,4 +95,4 @@ internal sealed class SendResponse [Newtonsoft.Json.JsonProperty("name")] public string Name { get; set; } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 5c4f4cb9..87504a06 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -128,4 +128,4 @@ internal sealed class ValidatedMessage [JsonProperty("android")] internal ValidatedAndroidConfig AndroidConfig { get; set; } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs index 0a284d3b..9348982a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs @@ -33,4 +33,4 @@ public sealed class Notification [JsonProperty("body")] public string Body { internal get; set; } } -} \ No newline at end of file +} From e3628563d7522eabcca20dc4a0c524561f7605f4 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 6 Dec 2018 16:28:20 -0800 Subject: [PATCH 12/50] Making getters public as per proposal --- .../FirebaseAdmin/Messaging/AndroidConfig.cs | 10 +++++----- FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs | 12 ++++++------ .../FirebaseAdmin/Messaging/Notification.cs | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 707fd116..98dab67f 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -28,30 +28,30 @@ public sealed class AndroidConfig /// messages that can be collapsed, so that only the last message gets sent when delivery can be /// resumed. A maximum of 4 different collapse keys may be active at any given time. /// - public string CollapseKey { internal get; set; } + public string CollapseKey { get; set; } /// /// The priority of the message. /// - public Priority? Priority { internal get; set; } + public Priority? Priority { get; set; } /// /// The time-to-live duration of the message. /// - public TimeSpan TimeToLive { internal get; set; } + public TimeSpan TimeToLive { get; set; } /// /// The package name of the application where the registration tokens must match in order /// to receive the message. /// - public string RestrictedPackageName { internal get; set; } + public string RestrictedPackageName { get; set; } /// /// A collection of key-value pairs that will be added to the message as data fields. Keys /// and the values must not be null. When set, overrides any data fields set on the top-level /// . /// - public IReadOnlyDictionary Data { internal get; set; } + public IReadOnlyDictionary Data { get; set; } internal ValidatedAndroidConfig Validate() { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 87504a06..50f2235f 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -33,35 +33,35 @@ public sealed class Message /// /// The registration token of the device to which the message should be sent. /// - public string Token { private get; set; } + public string Token { get; set; } /// /// The name of the FCM topic to which the message should be sent. Topic names may /// contain the /topics/ prefix. /// - public string Topic { private get; set; } + public string Topic { get; set; } /// /// The FCM condition to which the message should be sent. Must be a valid condition /// string such as "'foo' in topics". /// - public string Condition { private get; set; } + public string Condition { get; set; } /// /// A collection of key-value pairs that will be added to the message as data fields. Keys /// and the values must not be null. /// - public IReadOnlyDictionary Data { private get; set; } + public IReadOnlyDictionary Data { get; set; } /// /// The notification information to be included in the message. /// - public Notification Notification { private get; set; } + public Notification Notification { get; set; } /// /// The Android-specific information to be included in the message. /// - public AndroidConfig AndroidConfig { private get; set; } + public AndroidConfig AndroidConfig { get; set; } internal ValidatedMessage Validate() { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs index 9348982a..804ea0e2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs @@ -25,12 +25,12 @@ public sealed class Notification /// Title of the notification. /// [JsonProperty("title")] - public string Title { internal get; set; } + public string Title { get; set; } /// /// Body of the notification. /// [JsonProperty("body")] - public string Body { internal get; set; } + public string Body { get; set; } } } From 61fc77ee4766de3b47a1e38997c768d284b96e40 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 12 Dec 2018 12:14:49 -0800 Subject: [PATCH 13/50] Responding to code review comments --- .../Messaging/FirebaseMessageClientTest.cs | 12 ++++++------ .../Messaging/FirebaseMessagingTest.cs | 2 ++ .../FirebaseAdmin/Messaging/FirebaseMessaging.cs | 6 ++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs index 2a6742df..30a0630c 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs @@ -16,10 +16,10 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using Xunit; using Google.Apis.Auth.OAuth2; using Google.Apis.Http; -using Google.Apis.Json; using FirebaseAdmin.Tests; namespace FirebaseAdmin.Messaging.Tests @@ -56,7 +56,7 @@ public void NoClientFactory() } [Fact] - public async Task Send() + public async Task SendAsync() { var handler = new MockMessageHandler() { @@ -73,7 +73,7 @@ public async Task Send() }; var response = await client.SendAsync(message); Assert.Equal("test-response", response); - var req = NewtonsoftJsonSerializer.Instance.Deserialize(handler.Request); + var req = JsonConvert.DeserializeObject(handler.Request); Assert.Equal("test-topic", req.Message.Topic); Assert.False(req.ValidateOnly); Assert.Equal(1, handler.Calls); @@ -81,14 +81,14 @@ public async Task Send() // Send in dryRun mode. response = await client.SendAsync(message, dryRun: true); Assert.Equal("test-response", response); - req = NewtonsoftJsonSerializer.Instance.Deserialize(handler.Request); + 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 HttpError() + public async Task HttpErrorAsync() { var handler = new MockMessageHandler() { @@ -104,7 +104,7 @@ public async Task HttpError() var ex = await Assert.ThrowsAsync( async () => await client.SendAsync(message)); Assert.Contains("not json", ex.Message); - var req = NewtonsoftJsonSerializer.Instance.Deserialize(handler.Request); + 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 index 22e62801..b345b2bb 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs @@ -38,6 +38,7 @@ 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); @@ -48,6 +49,7 @@ 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)); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index ffe621f1..612b1f75 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -92,7 +92,8 @@ public async Task SendAsync(Message message, CancellationToken cancellat /// message. /// The message to be sent. Must not be null. /// A boolean indicating whether to perform a dry run (validation - /// only) of the send. + /// only) of the send. If set to true, the message will be sent to the FCM backend service, + /// but it will not be delivered to any actual recipients. public async Task SendAsync(Message message, bool dryRun) { return await SendAsync(message, dryRun, default(CancellationToken)); @@ -117,7 +118,8 @@ public async Task SendAsync(Message message, bool dryRun) /// message. /// The message to be sent. Must not be null. /// A boolean indicating whether to perform a dry run (validation - /// only) of the send. + /// only) of the send. If set to true, the message will be sent to the FCM backend service, + /// but it will not be delivered to any actual recipients. /// A cancellation token to monitor the asynchronous /// operation. public async Task SendAsync( From 8aba0636fda43b2ddae7b94a138040609b78ce6b Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 12 Dec 2018 14:05:38 -0800 Subject: [PATCH 14/50] More docs updates --- FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs | 4 ++++ .../FirebaseAdmin/Messaging/FirebaseMessagingClient.cs | 4 ++-- FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 98dab67f..ca5be3a1 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -65,6 +65,10 @@ internal ValidatedAndroidConfig Validate() }; } + /// + /// String representation of the as accepted by the FCM backend + /// service. + /// private string PriorityString { get diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs index 5f2b673a..fbbfa079 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -18,8 +18,8 @@ using System.Threading.Tasks; using Google.Apis.Auth.OAuth2; using Google.Apis.Http; -using Google.Apis.Json; using Google.Apis.Util; +using Newtonsoft.Json; namespace FirebaseAdmin.Messaging { @@ -66,7 +66,7 @@ public async Task SendAsync(Message message, + $"{Environment.NewLine}{json}"; throw new FirebaseException(error); } - var parsed = NewtonsoftJsonSerializer.Instance.Deserialize(json); + var parsed = JsonConvert.DeserializeObject(json); return parsed.Name; } catch (HttpRequestException e) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 50f2235f..a8fc0238 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -63,6 +63,11 @@ public sealed class Message /// public AndroidConfig AndroidConfig { get; set; } + /// + /// Validates the content and structure of this message instance, and converts it into the + /// type. This return type can be safely serialized into + /// a JSON string that is acceptable to the FCM backend service. + /// internal ValidatedMessage Validate() { var list = new List() @@ -86,6 +91,11 @@ internal ValidatedMessage Validate() }; } + /// + /// Validated and formatted representation of the . Checks for any + /// illegal characters in the topic name, and removes the /topics/ prefix if + /// present. + /// private string ValidatedTopic { get From 1b60f4869a647a5a421253a28d667f15af798b77 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 13 Dec 2018 14:56:21 -0800 Subject: [PATCH 15/50] Implemented AndroidNotification API --- .../Messaging/MessageTest.cs | 85 +++++++- .../FirebaseAdmin/Messaging/AndroidConfig.cs | 9 + .../Messaging/AndroidNotification.cs | 184 ++++++++++++++++++ 3 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 0ecc1cab..c7e2dbd4 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -158,6 +158,21 @@ public void AndroidConfig() { "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() @@ -170,7 +185,24 @@ public void AndroidConfig() { "priority", "high" }, { "ttl", "0.010000000s" }, { "restricted_package_name", "test-pkg-name" }, - {"data", new JObject(){{"k1", "v1"}, {"k2", "v2"}}}, + { "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" }, + } + }, } }, }; @@ -225,6 +257,57 @@ public void AndroidConfigInvalidTTL() Assert.Throws(() => message.Validate()); } + [Fact] + public void AndroidNotificationInvalidColor() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + Notification = new AndroidNotification() + { + Color = "not-a-color" + }, + }, + }; + Assert.Throws(() => message.Validate()); + } + + [Fact] + public void AndroidNotificationInvalidTitleLocArgs() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + Notification = new AndroidNotification() + { + TitleLocArgs = new List(){"arg"}, + }, + }, + }; + Assert.Throws(() => message.Validate()); + } + + [Fact] + public void AndroidNotificationInvalidBodyLocArgs() + { + var message = new Message() + { + Topic = "test-topic", + AndroidConfig = new AndroidConfig() + { + Notification = new AndroidNotification() + { + BodyLocArgs = new List(){"arg"}, + }, + }, + }; + Assert.Throws(() => message.Validate()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.Validate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index ca5be3a1..0e21a9c0 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -53,6 +53,11 @@ public sealed class AndroidConfig /// public IReadOnlyDictionary Data { get; set; } + /// + /// The Android notification to be included in the message. + /// + public AndroidNotification Notification { get; set; } + internal ValidatedAndroidConfig Validate() { return new ValidatedAndroidConfig() @@ -62,6 +67,7 @@ internal ValidatedAndroidConfig Validate() TimeToLive = this.TtlString, RestrictedPackageName = this.RestrictedPackageName, Data = this.Data, + Notification = this.Notification?.Validate(), }; } @@ -125,6 +131,9 @@ internal sealed class ValidatedAndroidConfig [JsonProperty("data")] internal IReadOnlyDictionary Data { get; set; } + + [JsonProperty("notification")] + internal ValidatedAndroidNotification Notification { get; set; } } /// diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs new file mode 100644 index 00000000..868a2f2b --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -0,0 +1,184 @@ +// 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 System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents the Android-specific notification options that can be included in a + /// . + /// + public sealed class AndroidNotification + { + + /// + /// The title of the Android notification. When provided, overrides the title set + /// via . + /// + public string Title { get; set; } + + /// + /// The title of the Android notification. When provided, overrides the title set + /// via . + /// + public string Body { get; set; } + + /// + /// The icon of the Android notification. + /// + public string Icon { get; set; } + + /// + /// The notification icon color. Must be of the form #RRGGBB. + /// + public string Color { get; set; } + + /// + /// The sound to be played when the device receives the notification. + /// + public string Sound { get; set; } + + /// + /// The notification tag. This is an identifier used to replace existing notifications in + /// the notification drawer. If not specified, each request creates a new notification. + /// + public string Tag { get; set; } + + /// + /// The action associated with a user click on the notification. If specified, an activity + /// with a matching Intent Filter is launched when a user clicks on the notification. + /// + public string ClickAction { get; set; } + + /// + /// Sets the key of the title string in the app's string resources to use to localize the + /// title text. + /// . + /// + public string TitleLocKey { get; set; } + + /// + /// The collection of resource key strings that will be used in place of the format + /// specifiers in . + /// + public IEnumerable TitleLocArgs { get; set; } + + /// + /// Sets the key of the body string in the app's string resources to use to localize the + /// body text. + /// . + /// + public string BodyLocKey { get; set; } + + /// + /// The collection of resource key strings that will be used in place of the format + /// specifiers in . + /// + public IEnumerable BodyLocArgs { get; set; } + + /// + /// Sets the Android notification channel ID (new in Android O). The app must create a + /// channel with this channel ID before any notification with this channel ID is received. + /// If you don't send this channel ID in the request, or if the channel ID provided has + /// not yet been created by the app, FCM uses the channel ID specified in the app manifest. + /// + public string ChannelId { get; set; } + + /// + /// Validates the content and structure of this notification, and converts it into the + /// type. This return type can be safely + /// serialized into a JSON string that is acceptable to the FCM backend service. + /// + internal ValidatedAndroidNotification Validate() + { + if (Color != null) { + if (!Regex.Match(Color, "^#[0-9a-fA-F]{6}$").Success) + { + throw new ArgumentException("Color must be in the form #RRGGBB"); + } + } + if (TitleLocArgs != null && TitleLocArgs.Any()) { + if (string.IsNullOrEmpty(TitleLocKey)) + { + throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs"); + } + } + if (BodyLocArgs != null && BodyLocArgs.Any()) { + if (string.IsNullOrEmpty(BodyLocKey)) + { + throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs"); + } + } + return new ValidatedAndroidNotification() + { + Title = this.Title, + Body = this.Body, + Icon = this.Icon, + Color = this.Color, + Sound = this.Sound, + Tag = this.Tag, + ClickAction = this.ClickAction, + TitleLocKey = this.TitleLocKey, + TitleLocArgs = this.TitleLocArgs, + BodyLocKey = this.BodyLocKey, + BodyLocArgs = this.BodyLocArgs, + ChannelId = this.ChannelId, + }; + } + } + + internal sealed class ValidatedAndroidNotification + { + [JsonProperty("title")] + internal string Title { get; set; } + + [JsonProperty("body")] + internal string Body { get; set; } + + [JsonProperty("icon")] + internal string Icon { get; set; } + + [JsonProperty("color")] + internal string Color { get; set; } + + [JsonProperty("sound")] + internal string Sound { get; set; } + + [JsonProperty("tag")] + internal string Tag { get; set; } + + [JsonProperty("click_action")] + internal string ClickAction { get; set; } + + [JsonProperty("title_loc_key")] + internal string TitleLocKey { get; set; } + + [JsonProperty("title_loc_args")] + internal IEnumerable TitleLocArgs { get; set; } + + [JsonProperty("body_loc_key")] + internal string BodyLocKey { get; set; } + + [JsonProperty("body_loc_args")] + internal IEnumerable BodyLocArgs { get; set; } + + [JsonProperty("channel_id")] + internal string ChannelId { get; set; } + } +} From d83ec1ed617b6b1f978e600bfc1aebf272279fab Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 14 Dec 2018 13:38:17 -0800 Subject: [PATCH 16/50] Added more documentation for the internal types/methods --- .../FirebaseAdmin/Messaging/AndroidConfig.cs | 9 +++++ .../Messaging/FirebaseMessagingClient.cs | 33 ++++++++++++++++++- .../FirebaseAdmin/Messaging/Message.cs | 4 +++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index ca5be3a1..769bc2fb 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -85,6 +85,11 @@ private string PriorityString } } + /// + /// String representation of as accepted by the FCM backend + /// service. The string ends in the suffix "s" (indicating seconds) and is preceded + /// by the number of seconds, with nanoseconds expressed as fractional seconds. + /// private string TtlString { get @@ -109,6 +114,10 @@ private string TtlString } } + /// + /// Represents a validated Android configuration that can be serialized into the JSON format + /// accepted by the FCM backend service. + /// internal sealed class ValidatedAndroidConfig { [JsonProperty("collapse_key")] diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs index fbbfa079..645afb7a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -23,6 +23,10 @@ namespace FirebaseAdmin.Messaging { + /// + /// A client for making authorized HTTP calls to the FCM backend service. Handles request + /// serialization, response parsing, and HTTP error handling. + /// internal sealed class FirebaseMessagingClient: IDisposable { private const string FcmUrl = "https://fcm.googleapis.com/v1/projects/{0}/messages:send"; @@ -46,7 +50,26 @@ internal FirebaseMessagingClient( _sendUrl = string.Format(FcmUrl, projectId); } - public async Task SendAsync(Message message, + /// + /// Sends a message to the FCM service for delivery. The message gets validated both by + /// the Admin SDK, and the remote FCM service. A successful return value indicates + /// that the message has been successfully sent to FCM, where it has been accepted by the + /// FCM service. + /// + /// A task that completes with a message ID string, which represents + /// successful handoff to FCM. + /// If the message argument is null. + /// If the message contains any invalid + /// fields. + /// If an error occurs while sending the + /// message. + /// The message to be sent. Must not be null. + /// A boolean indicating whether to perform a dry run (validation + /// only) of the send. If set to true, the message will be sent to the FCM backend service, + /// but it will not be delivered to any actual recipients. + /// A cancellation token to monitor the asynchronous + /// operation. + internal async Task SendAsync(Message message, bool dryRun = false, CancellationToken cancellationToken = default(CancellationToken)) { var request = new SendRequest() @@ -81,6 +104,10 @@ public void Dispose() } } + /// + /// Represents the envelope message accepted by the FCM backend service, including the message + /// payload and other options like validate_only. + /// internal sealed class SendRequest { [Newtonsoft.Json.JsonProperty("message")] @@ -90,6 +117,10 @@ internal sealed class SendRequest public bool ValidateOnly { get; set; } } + /// + /// Represents the response messages sent by the FCM backend service. Primarily consists of the + /// message ID (Name) that indicates success handoff to FCM. + /// internal sealed class SendResponse { [Newtonsoft.Json.JsonProperty("name")] diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index a8fc0238..b41ad493 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -118,6 +118,10 @@ private string ValidatedTopic } } + /// + /// Represents a validated message that can be serialized into the JSON format accepted by the + /// FCM backend service. + /// internal sealed class ValidatedMessage { [JsonProperty("token")] From 9f9b0a00a46846bb1f998d6e34f64faeab11bc60 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 14 Dec 2018 13:40:07 -0800 Subject: [PATCH 17/50] Added documentation to internal types --- FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 868a2f2b..63e0fddf 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -143,6 +143,10 @@ internal ValidatedAndroidNotification Validate() } } + /// + /// Represents a validated Android notification that can be serialized into the JSON format + /// accepted by the FCM backend service. + /// internal sealed class ValidatedAndroidNotification { [JsonProperty("title")] From 9e46d4cca3a9acb9a0d9ff874b4159142cd24e8d Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 18 Dec 2018 16:41:31 -0800 Subject: [PATCH 18/50] Implemented Webpush API for FCM --- .../FirebaseMessagingTest.cs | 2 +- .../Messaging/MessageTest.cs | 195 ++++++++++++++- .../FirebaseAdmin/Messaging/Message.cs | 15 +- .../FirebaseAdmin/Messaging/WebpushConfig.cs | 74 ++++++ .../Messaging/WebpushNotification.cs | 234 ++++++++++++++++++ 5 files changed, 510 insertions(+), 10 deletions(-) create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index 9573cfba..102ab711 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -38,7 +38,7 @@ public async Task Send() Title = "Title", Body = "Body", }, - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { Priority = Priority.Normal, TimeToLive = TimeSpan.FromHours(1), diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index c7e2dbd4..cee6147d 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -147,7 +147,7 @@ public void AndroidConfig() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { CollapseKey = "collapse-key", Priority = Priority.High, @@ -215,7 +215,7 @@ public void AndroidConfigFullSecondsTTL() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { TimeToLive = TimeSpan.FromHours(1), }, @@ -239,7 +239,7 @@ public void AndroidConfigInvalidTTL() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { TimeToLive = TimeSpan.FromHours(-1), }, @@ -263,7 +263,7 @@ public void AndroidNotificationInvalidColor() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { Notification = new AndroidNotification() { @@ -280,7 +280,7 @@ public void AndroidNotificationInvalidTitleLocArgs() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { Notification = new AndroidNotification() { @@ -297,7 +297,7 @@ public void AndroidNotificationInvalidBodyLocArgs() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { Notification = new AndroidNotification() { @@ -308,6 +308,189 @@ public void AndroidNotificationInvalidBodyLocArgs() Assert.Throws(() => message.Validate()); } + [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}, + } + }, + } + }, + }; + 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"}, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [Fact] + public void WebpushConfigDuplicateKeys() + { + var message = new Message() + { + Topic = "test-topic", + Webpush = new WebpushConfig() + { + Notification = new WebpushNotification() + { + Title = "title", + CustomData = new Dictionary(){{"title", "other"}}, + }, + }, + }; + Assert.Throws(() => message.Validate()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.Validate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index b41ad493..16c29f09 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -61,7 +61,12 @@ public sealed class Message /// /// The Android-specific information to be included in the message. /// - public AndroidConfig AndroidConfig { get; set; } + public AndroidConfig Android { get; set; } + + /// + /// The Webpush-specific information to be included in the message. + /// + public WebpushConfig Webpush { get; set; } /// /// Validates the content and structure of this message instance, and converts it into the @@ -87,7 +92,8 @@ internal ValidatedMessage Validate() Condition = this.Condition, Data = this.Data, Notification = this.Notification, - AndroidConfig = this.AndroidConfig?.Validate(), + Android = this.Android?.Validate(), + Webpush = this.Webpush?.Validate(), }; } @@ -140,6 +146,9 @@ internal sealed class ValidatedMessage internal Notification Notification { get; set; } [JsonProperty("android")] - internal ValidatedAndroidConfig AndroidConfig { get; set; } + internal ValidatedAndroidConfig Android { get; set; } + + [JsonProperty("webpush")] + internal ValidatedWebpushConfig Webpush { get; set; } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs new file mode 100644 index 00000000..86ecde20 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs @@ -0,0 +1,74 @@ +// 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 Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents the Webpush protocol options that can be included in a . + /// + public sealed class WebpushConfig + { + /// + /// Webpush HTTP headers. Refer + /// Webpush specification for supported headers. + /// + public IReadOnlyDictionary Headers { get; set; } + + /// + /// Webpush data fields. When set, overrides any data fields set via + /// . + /// + public IReadOnlyDictionary Data { get; set; } + + /// + /// The Webpush notification to be included in the message. + /// + public WebpushNotification Notification { get; set; } + + /// + /// Validates the content and structure of this Webpush configuration, and converts it into + /// the type. This return type can be safely + /// serialized into a JSON string that is acceptable to the FCM backend service. + /// + internal ValidatedWebpushConfig Validate() + { + return new ValidatedWebpushConfig() + { + Headers = this.Headers, + Data = this.Data, + Notification = this.Notification?.Validate(), + }; + } + } + + /// + /// Represents a validated Webpush configuration that can be serialized into the JSON format + /// accepted by the FCM backend service. + /// + internal sealed class ValidatedWebpushConfig + { + [JsonProperty("headers")] + public IReadOnlyDictionary Headers { get; set; } + + [JsonProperty("data")] + public IReadOnlyDictionary Data { get; set; } + + [JsonProperty("notification")] + internal IReadOnlyDictionary Notification { get; set; } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs new file mode 100644 index 00000000..a3333792 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -0,0 +1,234 @@ +// 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 Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents the Webpush-specific notification options that can be included in a + /// . Supports most standard options defined in the + /// + /// Web Notification specification + /// + public sealed class WebpushNotification + { + /// + /// Title text of the notification. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// Body text of the notification. + /// + public string Body { get; set; } + + /// + /// The URL to the icon of the notification. + /// + public string Icon { get; set; } + + /// + /// The URL of the image used to represent the notification when there is not enough space + /// to display the notification itself. + /// + public string Badge { get; set; } + + /// + /// Any arbitrary data that should be associated with the notification. + /// + public object Data { get; set; } + + /// + /// The direction in which to display the notification. + /// + public Direction? Direction { get; set; } + + /// + /// Converts the property into a string value that can be included + /// in the json output. + /// + internal string DirectionString + { + get + { + switch (Direction) + { + case Messaging.Direction.Auto: + return "auto"; + case Messaging.Direction.LeftToRight: + return "ltr"; + case Messaging.Direction.RightToLeft: + return "rtl"; + default: + return null; + } + } + } + + /// + /// The URL of an image to be displayed in the notification. + /// + public string Image { get; set; } + + /// + /// The language of the notification. + /// + public string Language { get; set; } + + /// + /// Whether the user should be notified after a new notification replaces an old one. + /// + public bool? Renotify { get; set; } + + /// + /// Whether a notification should remain active until the user clicks or dismisses it, + /// rather than closing automatically. + /// + public bool? RequireInteraction { get; set; } + + /// + /// Whether the notification should be silent. + /// + public bool? Silent { get; set; } + + /// + /// An identifying tag for the notification. + /// + public string Tag { get; set; } + + /// + /// A timestamp value in milliseconds on the notification. + /// + public long? TimestampMillis { get; set; } + + /// + /// A vibration pattern for the receiving device's vibration hardware to emit when the + /// notification fires. + /// + public int[] Vibrate { get; set; } + + /// + /// A collection of arbitrary key-value data to be included in the notification. + /// + public IReadOnlyDictionary CustomData; + + /// + /// A collection of notification actions to be associated with the notification. + /// + [JsonProperty("actions")] + public IEnumerable Actions; + + private delegate void AddString(string key, string value); + private delegate void AddObject(string key, object value); + + /// + /// Validates the content and structure of this Webpush notification, and converts it into + /// a dictionary. + /// + internal IReadOnlyDictionary Validate() + { + var result = new Dictionary(); + AddString addString = delegate(string key, string value) + { + if (!string.IsNullOrEmpty(value)) + { + result[key] = value; + } + }; + AddObject addObject = delegate(string key, object value) + { + if (value != null) + { + result[key] = value; + } + }; + addString("title", Title); + addString("body", Body); + addString("icon", Icon); + addString("image", Image); + addString("lang", Language); + addString("tag", Tag); + addString("dir", DirectionString); + addString("badge", Badge); + addObject("renotify", Renotify); + addObject("requireInteraction", RequireInteraction); + addObject("silent", Silent); + addObject("actions", Actions); + addObject("vibrate", Vibrate); + addObject("timestamp", TimestampMillis); + addObject("data", Data); + if (CustomData != null) + { + foreach (var entry in CustomData) + { + if (result.ContainsKey(entry.Key)) + { + throw new ArgumentException($"Multiple specification for key {entry.Key}"); + } + addObject(entry.Key, entry.Value); + } + } + return result; + } + } + + /// + /// Represents an action available to users when the notification is presented. + /// + public sealed class Action + { + /// + /// Action name. + /// + [JsonProperty("action")] + public string ActionName { get; set; } + + /// + /// Title text. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// Icon URL. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + } + + /// + /// Different directions a notification can be displayed in. + /// + public enum Direction + { + /// + /// Direction automatically determined. + /// + Auto, + + /// + /// Left to right. + /// + LeftToRight, + + /// + /// Right to left. + /// + RightToLeft, + } +} From bceb99592180e4db46e17492b1141c9a06a31124 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 19 Dec 2018 13:33:12 -0800 Subject: [PATCH 19/50] Refactored the FirebaseMessaging types --- .../FirebaseMessagingTest.cs | 2 +- .../Messaging/MessageTest.cs | 88 ++++++++--- FirebaseAdmin/FirebaseAdmin/Extensions.cs | 15 ++ .../FirebaseAdmin/Messaging/AndroidConfig.cs | 146 ++++++++++-------- .../Messaging/AndroidNotification.cs | 101 +++++------- .../Messaging/FirebaseMessagingClient.cs | 4 +- .../FirebaseAdmin/Messaging/Message.cs | 114 ++++++-------- .../FirebaseAdmin/Messaging/Notification.cs | 13 ++ 8 files changed, 266 insertions(+), 217 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index 9573cfba..102ab711 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -38,7 +38,7 @@ public async Task Send() Title = "Title", Body = "Body", }, - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { Priority = Priority.Normal, TimeToLive = TimeSpan.FromHours(1), diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index c7e2dbd4..871b0d17 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -26,7 +26,7 @@ public class MessageTest [Fact] public void MessageWithoutTarget() { - Assert.Throws(() => new Message().Validate()); + Assert.Throws(() => new Message().CopyAndValidate()); } [Fact] @@ -50,21 +50,21 @@ public void MultipleTargets() Token = "test-token", Topic = "test-topic", }; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); message = new Message() { Token = "test-token", Condition = "test-condition", }; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); message = new Message() { Condition = "test-condition", Topic = "test-topic", }; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); message = new Message() { @@ -72,7 +72,19 @@ public void MultipleTargets() Topic = "test-topic", Condition = "test-condition", }; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void MessageDeserialization() + { + var original = new Message() + { + Topic = "test-topic", + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Topic, copy.Topic); } [Fact] @@ -104,7 +116,7 @@ public void InvalidTopicNames() foreach (var topic in topics) { var message = new Message(){Topic = topic}; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); } } @@ -147,7 +159,7 @@ public void AndroidConfig() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { CollapseKey = "collapse-key", Priority = Priority.High, @@ -209,13 +221,37 @@ public void AndroidConfig() AssertJsonEquals(expected, message); } + [Fact] + public void AndroidConfigMinimal() + { + var message = new Message() + { + Topic = "test-topic", + Android = new AndroidConfig() + { + RestrictedPackageName = "test-pkg-name", + }, + }; + var expected = new JObject() + { + {"topic", "test-topic"}, + { + "android", new JObject() + { + { "restricted_package_name", "test-pkg-name" }, + } + }, + }; + AssertJsonEquals(expected, message); + } + [Fact] public void AndroidConfigFullSecondsTTL() { var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { TimeToLive = TimeSpan.FromHours(1), }, @@ -226,7 +262,7 @@ public void AndroidConfigFullSecondsTTL() { "android", new JObject() { - { "ttl", "3600s" }, + { "ttl", "3600s" }, } }, }; @@ -239,7 +275,7 @@ public void AndroidConfigInvalidTTL() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { TimeToLive = TimeSpan.FromHours(-1), }, @@ -250,11 +286,25 @@ public void AndroidConfigInvalidTTL() { "android", new JObject() { - { "ttl", "3600s" }, + { "ttl", "3600s" }, } }, }; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); + } + + [Fact] + public void AndroidConfigDeserialization() + { + var original = new AndroidConfig() + { + TimeToLive = TimeSpan.FromSeconds(10.5), + Priority = Priority.High, + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Priority, copy.Priority); + Assert.Equal(original.TimeToLive, copy.TimeToLive); } [Fact] @@ -263,7 +313,7 @@ public void AndroidNotificationInvalidColor() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { Notification = new AndroidNotification() { @@ -271,7 +321,7 @@ public void AndroidNotificationInvalidColor() }, }, }; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); } [Fact] @@ -280,7 +330,7 @@ public void AndroidNotificationInvalidTitleLocArgs() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { Notification = new AndroidNotification() { @@ -288,7 +338,7 @@ public void AndroidNotificationInvalidTitleLocArgs() }, }, }; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); } [Fact] @@ -297,7 +347,7 @@ public void AndroidNotificationInvalidBodyLocArgs() var message = new Message() { Topic = "test-topic", - AndroidConfig = new AndroidConfig() + Android = new AndroidConfig() { Notification = new AndroidNotification() { @@ -305,12 +355,12 @@ public void AndroidNotificationInvalidBodyLocArgs() }, }, }; - Assert.Throws(() => message.Validate()); + Assert.Throws(() => message.CopyAndValidate()); } private void AssertJsonEquals(JObject expected, Message actual) { - var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.Validate()); + var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); var parsed = JObject.Parse(json); Assert.True( JToken.DeepEquals(expected, parsed), diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index db5a2a02..3a014b14 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -85,5 +86,19 @@ public static long UnixTimestamp(this IClock clock) { return (long) (clock.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; } + + /// + /// Creates a shallow copy of a collection of key-value pairs. + /// + public static IReadOnlyDictionary Copy( + this IEnumerable> source) + { + var copy = new Dictionary(); + foreach (var entry in source) + { + copy[entry.Key] = entry.Value; + } + return copy; + } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 3be35959..32f61aaa 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -28,54 +28,21 @@ public sealed class AndroidConfig /// messages that can be collapsed, so that only the last message gets sent when delivery can be /// resumed. A maximum of 4 different collapse keys may be active at any given time. /// + [JsonProperty("collapse_key")] public string CollapseKey { get; set; } /// /// The priority of the message. /// + [JsonIgnore] public Priority? Priority { get; set; } - /// - /// The time-to-live duration of the message. - /// - public TimeSpan TimeToLive { get; set; } - - /// - /// The package name of the application where the registration tokens must match in order - /// to receive the message. - /// - public string RestrictedPackageName { get; set; } - - /// - /// A collection of key-value pairs that will be added to the message as data fields. Keys - /// and the values must not be null. When set, overrides any data fields set on the top-level - /// . - /// - public IReadOnlyDictionary Data { get; set; } - - /// - /// The Android notification to be included in the message. - /// - public AndroidNotification Notification { get; set; } - - internal ValidatedAndroidConfig Validate() - { - return new ValidatedAndroidConfig() - { - CollapseKey = this.CollapseKey, - Priority = this.PriorityString, - TimeToLive = this.TtlString, - RestrictedPackageName = this.RestrictedPackageName, - Data = this.Data, - Notification = this.Notification?.Validate(), - }; - } - /// /// String representation of the as accepted by the FCM backend /// service. /// - private string PriorityString + [JsonProperty("priority")] + internal string PriorityString { get { @@ -89,14 +56,33 @@ private string PriorityString return null; } } + set + { + switch (value) + { + case "high": + Priority = Messaging.Priority.High; + return; + case "normal": + Priority = Messaging.Priority.High; + return; + } + } } + /// + /// The time-to-live duration of the message. + /// + [JsonIgnore] + public TimeSpan? TimeToLive { get; set; } + /// /// String representation of as accepted by the FCM backend /// service. The string ends in the suffix "s" (indicating seconds) and is preceded /// by the number of seconds, with nanoseconds expressed as fractional seconds. /// - private string TtlString + [JsonProperty("ttl")] + internal string TtlString { get { @@ -104,11 +90,7 @@ private string TtlString { return null; } - var totalSeconds = TimeToLive.TotalSeconds; - if (totalSeconds < 0) - { - throw new ArgumentException("TTL must not be negative."); - } + var totalSeconds = TimeToLive.Value.TotalSeconds; var seconds = (long) Math.Floor(totalSeconds); var subsecondNanos = (long) ((totalSeconds - seconds) * 1e9); if (subsecondNanos > 0) @@ -117,44 +99,78 @@ private string TtlString } return String.Format("{0}s", seconds); } + set + { + var segments = value.TrimEnd('s').Split('.'); + var seconds = Int64.Parse(segments[0]); + var ttl = TimeSpan.FromSeconds(seconds); + if (segments.Length == 2) + { + var subsecondNanos = Int64.Parse(segments[1].TrimStart('0')); + ttl = ttl.Add(TimeSpan.FromMilliseconds(subsecondNanos / 1e6)); + } + TimeToLive = ttl; + } } - } - - /// - /// Represents a validated Android configuration that can be serialized into the JSON format - /// accepted by the FCM backend service. - /// - internal sealed class ValidatedAndroidConfig - { - [JsonProperty("collapse_key")] - internal string CollapseKey { get; set; } - - [JsonProperty("priority")] - internal string Priority { get; set; } - - [JsonProperty("ttl")] - internal string TimeToLive { get; set; } + /// + /// The package name of the application where the registration tokens must match in order + /// to receive the message. + /// [JsonProperty("restricted_package_name")] - internal string RestrictedPackageName { get; set; } + public string RestrictedPackageName { get; set; } + /// + /// A collection of key-value pairs that will be added to the message as data fields. Keys + /// and the values must not be null. When set, overrides any data fields set on the top-level + /// . + /// [JsonProperty("data")] - internal IReadOnlyDictionary Data { get; set; } + public IReadOnlyDictionary Data { get; set; } + /// + /// The Android notification to be included in the message. + /// [JsonProperty("notification")] - internal ValidatedAndroidNotification Notification { get; set; } - } + public AndroidNotification Notification { get; set; } + + /// + /// Copies this Android config, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. + /// + internal AndroidConfig CopyAndValidate() + { + // Copy and validate the leaf-level properties + var copy = new AndroidConfig() + { + CollapseKey = this.CollapseKey, + Priority = this.Priority, + TimeToLive = this.TimeToLive, + RestrictedPackageName = this.RestrictedPackageName, + Data = this.Data?.Copy(), + }; + var totalSeconds = copy.TimeToLive?.TotalSeconds ?? 0; + if (totalSeconds < 0) + { + throw new ArgumentException("TTL must not be negative."); + } + + // Copy and validate the child properties + copy.Notification = this.Notification?.CopyAndValidate(); + return copy; + } + } /// /// Priority levels that can be set on an . /// public enum Priority - { + { /// /// High priority message. /// High, - + /// /// Normal priority message. /// diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 63e0fddf..891317c5 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -31,39 +31,46 @@ public sealed class AndroidNotification /// The title of the Android notification. When provided, overrides the title set /// via . /// + [JsonProperty("title")] public string Title { get; set; } /// /// The title of the Android notification. When provided, overrides the title set /// via . /// + [JsonProperty("body")] public string Body { get; set; } /// /// The icon of the Android notification. /// + [JsonProperty("icon")] public string Icon { get; set; } /// /// The notification icon color. Must be of the form #RRGGBB. /// + [JsonProperty("color")] public string Color { get; set; } /// /// The sound to be played when the device receives the notification. /// + [JsonProperty("sound")] public string Sound { get; set; } /// /// The notification tag. This is an identifier used to replace existing notifications in /// the notification drawer. If not specified, each request creates a new notification. /// + [JsonProperty("tag")] public string Tag { get; set; } /// /// The action associated with a user click on the notification. If specified, an activity /// with a matching Intent Filter is launched when a user clicks on the notification. /// + [JsonProperty("click_action")] public string ClickAction { get; set; } /// @@ -71,12 +78,14 @@ public sealed class AndroidNotification /// title text. /// . /// + [JsonProperty("title_loc_key")] public string TitleLocKey { get; set; } /// /// The collection of resource key strings that will be used in place of the format /// specifiers in . /// + [JsonProperty("title_loc_args")] public IEnumerable TitleLocArgs { get; set; } /// @@ -84,12 +93,14 @@ public sealed class AndroidNotification /// body text. /// . /// + [JsonProperty("body_loc_key")] public string BodyLocKey { get; set; } /// /// The collection of resource key strings that will be used in place of the format /// specifiers in . /// + [JsonProperty("body_loc_args")] public IEnumerable BodyLocArgs { get; set; } /// @@ -98,34 +109,16 @@ public sealed class AndroidNotification /// If you don't send this channel ID in the request, or if the channel ID provided has /// not yet been created by the app, FCM uses the channel ID specified in the app manifest. /// + [JsonProperty("channel_id")] public string ChannelId { get; set; } /// - /// Validates the content and structure of this notification, and converts it into the - /// type. This return type can be safely - /// serialized into a JSON string that is acceptable to the FCM backend service. + /// Copies this notification, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. /// - internal ValidatedAndroidNotification Validate() + internal AndroidNotification CopyAndValidate() { - if (Color != null) { - if (!Regex.Match(Color, "^#[0-9a-fA-F]{6}$").Success) - { - throw new ArgumentException("Color must be in the form #RRGGBB"); - } - } - if (TitleLocArgs != null && TitleLocArgs.Any()) { - if (string.IsNullOrEmpty(TitleLocKey)) - { - throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs"); - } - } - if (BodyLocArgs != null && BodyLocArgs.Any()) { - if (string.IsNullOrEmpty(BodyLocKey)) - { - throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs"); - } - } - return new ValidatedAndroidNotification() + var copy = new AndroidNotification() { Title = this.Title, Body = this.Body, @@ -140,49 +133,25 @@ internal ValidatedAndroidNotification Validate() BodyLocArgs = this.BodyLocArgs, ChannelId = this.ChannelId, }; + if (copy.Color != null && !Regex.Match(copy.Color, "^#[0-9a-fA-F]{6}$").Success) + { + throw new ArgumentException("Color must be in the form #RRGGBB"); + } + if (copy.TitleLocArgs != null && copy.TitleLocArgs.Any()) + { + if (string.IsNullOrEmpty(copy.TitleLocKey)) + { + throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs"); + } + } + if (copy.BodyLocArgs != null && copy.BodyLocArgs.Any()) + { + if (string.IsNullOrEmpty(copy.BodyLocKey)) + { + throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs"); + } + } + return copy; } } - - /// - /// Represents a validated Android notification that can be serialized into the JSON format - /// accepted by the FCM backend service. - /// - internal sealed class ValidatedAndroidNotification - { - [JsonProperty("title")] - internal string Title { get; set; } - - [JsonProperty("body")] - internal string Body { get; set; } - - [JsonProperty("icon")] - internal string Icon { get; set; } - - [JsonProperty("color")] - internal string Color { get; set; } - - [JsonProperty("sound")] - internal string Sound { get; set; } - - [JsonProperty("tag")] - internal string Tag { get; set; } - - [JsonProperty("click_action")] - internal string ClickAction { get; set; } - - [JsonProperty("title_loc_key")] - internal string TitleLocKey { get; set; } - - [JsonProperty("title_loc_args")] - internal IEnumerable TitleLocArgs { get; set; } - - [JsonProperty("body_loc_key")] - internal string BodyLocKey { get; set; } - - [JsonProperty("body_loc_args")] - internal IEnumerable BodyLocArgs { get; set; } - - [JsonProperty("channel_id")] - internal string ChannelId { get; set; } - } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs index 645afb7a..001327fa 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -74,7 +74,7 @@ internal async Task SendAsync(Message message, { var request = new SendRequest() { - Message = message.ThrowIfNull(nameof(message)).Validate(), + Message = message.ThrowIfNull(nameof(message)).CopyAndValidate(), ValidateOnly = dryRun, }; try @@ -111,7 +111,7 @@ public void Dispose() internal sealed class SendRequest { [Newtonsoft.Json.JsonProperty("message")] - public ValidatedMessage Message { get; set; } + public Message Message { get; set; } [Newtonsoft.Json.JsonProperty("validate_only")] public bool ValidateOnly { get; set; } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index b41ad493..606c9d24 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -33,46 +33,82 @@ public sealed class Message /// /// The registration token of the device to which the message should be sent. /// + [JsonProperty("token")] public string Token { get; set; } /// /// The name of the FCM topic to which the message should be sent. Topic names may /// contain the /topics/ prefix. /// + [JsonIgnore] public string Topic { get; set; } + /// + /// Formatted representation of the . Removes the /topics/ + /// prefix if present. This is what's ultimately sent to the FCM service. + /// + [JsonProperty("topic")] + internal string UnprefixedTopic + { + get + { + if (Topic != null && Topic.StartsWith("/topics/")) + { + return Topic.Substring("/topics/".Length); + } + return Topic; + } + set + { + Topic = value; + } + } + /// /// The FCM condition to which the message should be sent. Must be a valid condition /// string such as "'foo' in topics". /// + [JsonProperty("condition")] public string Condition { get; set; } /// /// A collection of key-value pairs that will be added to the message as data fields. Keys /// and the values must not be null. /// + [JsonProperty("data")] public IReadOnlyDictionary Data { get; set; } /// /// The notification information to be included in the message. /// + [JsonProperty("notification")] public Notification Notification { get; set; } /// /// The Android-specific information to be included in the message. /// - public AndroidConfig AndroidConfig { get; set; } + [JsonProperty("android")] + public AndroidConfig Android { get; set; } /// - /// Validates the content and structure of this message instance, and converts it into the - /// type. This return type can be safely serialized into - /// a JSON string that is acceptable to the FCM backend service. + /// Copies this message, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. Each property is + /// copied before validated to make sure the original is not modified in the user + /// code post-validation. /// - internal ValidatedMessage Validate() + internal Message CopyAndValidate() { + // Copy and validate the leaf-level properties + var copy = new Message() + { + Token = this.Token, + Topic = this.Topic, + Condition = this.Condition, + Data = this.Data?.Copy(), + }; var list = new List() { - Token, Topic, Condition, + copy.Token, copy.Topic, copy.Condition, }; var targets = list.FindAll((target) => !string.IsNullOrEmpty(target)); if (targets.Count != 1) @@ -80,66 +116,16 @@ internal ValidatedMessage Validate() throw new ArgumentException( "Exactly one of Token, Topic or Condition is required."); } - return new ValidatedMessage() + var topic = copy.UnprefixedTopic; + if (topic != null && !Regex.IsMatch(topic, "^[a-zA-Z0-9-_.~%]+$")) { - Token = this.Token, - Topic = this.ValidatedTopic, - Condition = this.Condition, - Data = this.Data, - Notification = this.Notification, - AndroidConfig = this.AndroidConfig?.Validate(), - }; - } - - /// - /// Validated and formatted representation of the . Checks for any - /// illegal characters in the topic name, and removes the /topics/ prefix if - /// present. - /// - private string ValidatedTopic - { - get - { - if (string.IsNullOrEmpty(Topic)) - { - return null; - } - var topic = Topic; - if (topic.StartsWith("/topics/")) - { - topic = topic.Substring("/topics/".Length); - } - if (!Regex.IsMatch(topic, "^[a-zA-Z0-9-_.~%]+$")) - { - throw new ArgumentException("Malformed topic name."); - } - return topic; + throw new ArgumentException("Malformed topic name."); } - } - } - /// - /// Represents a validated message that can be serialized into the JSON format accepted by the - /// FCM backend service. - /// - internal sealed class ValidatedMessage - { - [JsonProperty("token")] - internal string Token { get; set; } - - [JsonProperty("topic")] - internal string Topic { get; set; } - - [JsonProperty("condition")] - internal string Condition { get; set; } - - [JsonProperty("data")] - internal IReadOnlyDictionary Data { get; set; } - - [JsonProperty("notification")] - internal Notification Notification { get; set; } - - [JsonProperty("android")] - internal ValidatedAndroidConfig AndroidConfig { get; set; } + // Copy and validate the child properties + copy.Notification = this.Notification?.CopyAndValidate(); + copy.Android = this.Android?.CopyAndValidate(); + return copy; + } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs index 804ea0e2..431b3785 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs @@ -32,5 +32,18 @@ public sealed class Notification /// [JsonProperty("body")] public string Body { get; set; } + + /// + /// Copies this notification. There is nothing to be validated in this class, but we use + /// the same method name as in other classes in this namespace. + /// + internal Notification CopyAndValidate() + { + return new Notification() + { + Title = this.Title, + Body = this.Body, + }; + } } } From 0803839e35c522a09ab8b15201c1c436dd2830b5 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 19 Dec 2018 13:52:13 -0800 Subject: [PATCH 20/50] Shallow copying list properties --- FirebaseAdmin/FirebaseAdmin/Extensions.cs | 10 +++++++++- .../FirebaseAdmin/Messaging/AndroidNotification.cs | 6 +++--- FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs | 7 +++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index 3a014b14..fb1ad2e7 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -48,7 +48,7 @@ public static ServiceAccountCredential ToServiceAccountCredential( /// /// Creates a default (unauthenticated) from the /// factory. - /// + /// public static ConfigurableHttpClient CreateDefaultHttpClient( this HttpClientFactory clientFactory) { @@ -100,5 +100,13 @@ public static IReadOnlyDictionary Copy( } return copy; } + + /// + /// Creates a shallow copy of a collection of items. + /// + public static IEnumerable Copy(this IEnumerable source) + { + return new List(source); + } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 891317c5..5d520ba2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -26,7 +26,7 @@ namespace FirebaseAdmin.Messaging /// public sealed class AndroidNotification { - + /// /// The title of the Android notification. When provided, overrides the title set /// via . @@ -128,9 +128,9 @@ internal AndroidNotification CopyAndValidate() Tag = this.Tag, ClickAction = this.ClickAction, TitleLocKey = this.TitleLocKey, - TitleLocArgs = this.TitleLocArgs, + TitleLocArgs = this.TitleLocArgs?.Copy(), BodyLocKey = this.BodyLocKey, - BodyLocArgs = this.BodyLocArgs, + BodyLocArgs = this.BodyLocArgs?.Copy(), ChannelId = this.ChannelId, }; if (copy.Color != null && !Regex.Match(copy.Color, "^#[0-9a-fA-F]{6}$").Success) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 606c9d24..baab5924 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -18,7 +18,6 @@ using Newtonsoft.Json; using Google.Apis.Json; using Google.Apis.Util; -using FirebaseAdmin; namespace FirebaseAdmin.Messaging { @@ -92,9 +91,9 @@ internal string UnprefixedTopic /// /// Copies this message, and validates the content of it to ensure that it can be - /// serialized into the JSON format expected by the FCM service. Each property is - /// copied before validated to make sure the original is not modified in the user - /// code post-validation. + /// serialized into the JSON format expected by the FCM service. Each property is copied + /// before validation to guard against the original being modified in the user code + /// post-validation. /// internal Message CopyAndValidate() { From 08153a7e44fca5b971d6a806f50d2a984ca2fd03 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 19 Dec 2018 17:44:08 -0800 Subject: [PATCH 21/50] Fixed CustomData deserialization --- .../Messaging/MessageTest.cs | 43 ++++++++++++++- .../Messaging/WebpushNotification.cs | 53 +++++++++++-------- 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 228a88a1..61cd0f53 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -16,8 +16,8 @@ using System.Collections.Generic; using Newtonsoft.Json.Linq; using Xunit; -using FirebaseAdmin.Messaging; using Google.Apis.Json; +using Newtonsoft.Json; namespace FirebaseAdmin.Messaging.Tests { @@ -541,6 +541,24 @@ public void WebpushConfigDuplicateKeys() Assert.Throws(() => message.CopyAndValidate()); } + [Fact] + public void WebpushNotificationDeserialization() + { + var original = new WebpushNotification() + { + Direction = Direction.LeftToRight, + 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.Direction, copy.Direction); + Assert.Equal(original.CustomData, copy.CustomData); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); @@ -550,4 +568,27 @@ private void AssertJsonEquals(JObject expected, Message actual) $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}"); } } + + public class Foo + { + [JsonExtensionData(ReadData = true, WriteData = true)] + private IDictionary _data = new Dictionary(); + + [JsonIgnore] + public IReadOnlyDictionary CustomData + { + get + { + return _data.Copy(); + } + set + { + _data = new Dictionary(); + foreach (var entry in value) + { + _data[entry.Key] = entry.Value; + } + } + } + } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index 7668d9e5..9e1e2df0 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -85,6 +85,21 @@ internal string DirectionString return null; } } + set + { + switch (value) + { + case "auto": + Direction = Messaging.Direction.Auto; + return; + case "ltr": + Direction = Messaging.Direction.LeftToRight; + return; + case "rtl": + Direction = Messaging.Direction.RightToLeft; + return; + } + } } /// @@ -141,30 +156,26 @@ internal string DirectionString /// A collection of arbitrary key-value data to be included in the notification. /// [JsonIgnore] - public IReadOnlyDictionary CustomData; - - /// - /// A copy of exposed an IDictionary so it - /// works with JsonExtensionData annotation. - /// - [JsonExtensionData] - internal IDictionary ExtensionCustomData + public IReadOnlyDictionary CustomData { - get + get { return ExtensionCustomData; } + set { - if (CustomData?.Count > 0) + ExtensionCustomData = new Dictionary(); + foreach (var entry in value) { - var result = new Dictionary(); - foreach (var entry in CustomData) - { - result[entry.Key] = entry.Value; - } - return result; + ExtensionCustomData[entry.Key] = entry.Value; } - return null; } } + /// + /// A copy of exposed an IDictionary so it + /// works with JsonExtensionData annotation. + /// + [JsonExtensionData] + private Dictionary ExtensionCustomData; + /// /// A collection of notification actions to be associated with the notification. /// @@ -196,14 +207,14 @@ internal WebpushNotification CopyAndValidate() Data = this.Data, }; - var customDataCopy = this.CustomData?.Copy(); - if (customDataCopy?.Count > 0) + var customData = this.CustomData; + if (customData?.Count > 0) { var serializer = NewtonsoftJsonSerializer.Instance; // Serialize the notification without CustomData for validation. var json = serializer.Serialize(copy); var dict = serializer.Deserialize>(json); - foreach (var entry in customDataCopy) + foreach (var entry in customData) { if (dict.ContainsKey(entry.Key)) { @@ -211,7 +222,7 @@ internal WebpushNotification CopyAndValidate() $"Multiple specifications for WebpushNotification key: {entry.Key}"); } } - copy.CustomData = customDataCopy; + copy.CustomData = customData; } return copy; } From 0166f5f596260f4159a70966f617dbdfacfcec60 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 19 Dec 2018 22:08:35 -0800 Subject: [PATCH 22/50] Exposing CustomData as IDictionary --- .../Messaging/MessageTest.cs | 61 ++++++++----------- FirebaseAdmin/FirebaseAdmin/Extensions.cs | 8 --- .../Messaging/AndroidNotification.cs | 4 +- .../Messaging/WebpushNotification.cs | 39 ++++++------ 4 files changed, 44 insertions(+), 68 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 61cd0f53..c1814575 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -75,18 +75,6 @@ public void MultipleTargets() Assert.Throws(() => message.CopyAndValidate()); } - [Fact] - public void MessageDeserialization() - { - var original = new Message() - { - Topic = "test-topic", - }; - var json = NewtonsoftJsonSerializer.Instance.Serialize(original); - var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); - Assert.Equal(original.Topic, copy.Topic); - } - [Fact] public void DataMessage() { @@ -127,6 +115,23 @@ public void PrefixedTopicName() AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); } + [Fact] + public void MessageDeserialization() + { + var original = new Message() + { + Topic = "test-topic", + 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); + } + [Fact] public void Notification() { @@ -298,13 +303,20 @@ 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" }, + }, }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); Assert.Equal(original.Priority, copy.Priority); Assert.Equal(original.TimeToLive, copy.TimeToLive); + Assert.Equal(original.Data, copy.Data); } [Fact] @@ -546,7 +558,6 @@ public void WebpushNotificationDeserialization() { var original = new WebpushNotification() { - Direction = Direction.LeftToRight, CustomData = new Dictionary() { {"custom-key1", "custom-data"}, @@ -555,7 +566,6 @@ public void WebpushNotificationDeserialization() }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); - Assert.Equal(original.Direction, copy.Direction); Assert.Equal(original.CustomData, copy.CustomData); } @@ -568,27 +578,4 @@ private void AssertJsonEquals(JObject expected, Message actual) $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}"); } } - - public class Foo - { - [JsonExtensionData(ReadData = true, WriteData = true)] - private IDictionary _data = new Dictionary(); - - [JsonIgnore] - public IReadOnlyDictionary CustomData - { - get - { - return _data.Copy(); - } - set - { - _data = new Dictionary(); - foreach (var entry in value) - { - _data[entry.Key] = entry.Value; - } - } - } - } } diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index fb1ad2e7..0c71f7f1 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -100,13 +100,5 @@ public static IReadOnlyDictionary Copy( } return copy; } - - /// - /// Creates a shallow copy of a collection of items. - /// - public static IEnumerable Copy(this IEnumerable source) - { - return new List(source); - } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 5d520ba2..417efe70 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -128,9 +128,9 @@ internal AndroidNotification CopyAndValidate() Tag = this.Tag, ClickAction = this.ClickAction, TitleLocKey = this.TitleLocKey, - TitleLocArgs = this.TitleLocArgs?.Copy(), + TitleLocArgs = this.TitleLocArgs?.ToList(), BodyLocKey = this.BodyLocKey, - BodyLocArgs = this.BodyLocArgs?.Copy(), + BodyLocArgs = this.BodyLocArgs?.ToList(), ChannelId = this.ChannelId, }; if (copy.Color != null && !Regex.Match(copy.Color, "^#[0-9a-fA-F]{6}$").Success) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index 9e1e2df0..f75276ec 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Google.Apis.Json; using Newtonsoft.Json; @@ -153,28 +154,12 @@ internal string DirectionString public int[] Vibrate { get; set; } /// - /// A collection of arbitrary key-value data to be included in the notification. - /// - [JsonIgnore] - public IReadOnlyDictionary CustomData - { - get { return ExtensionCustomData; } - set - { - ExtensionCustomData = new Dictionary(); - foreach (var entry in value) - { - ExtensionCustomData[entry.Key] = entry.Value; - } - } - } - - /// - /// A copy of exposed an IDictionary so it - /// works with JsonExtensionData annotation. + /// A collection of arbitrary key-value data to be included in the notification. This is + /// exposed as an to support correct + /// deserialization of custom properties. /// [JsonExtensionData] - private Dictionary ExtensionCustomData; + public IDictionary CustomData { get; set; } /// /// A collection of notification actions to be associated with the notification. @@ -201,7 +186,7 @@ internal WebpushNotification CopyAndValidate() Renotify = this.Renotify, RequireInteraction = this.RequireInteraction, Silent = this.Silent, - Actions = this.Actions?.Copy(), + Actions = this.Actions?.Select((item, _) => new Action(item)).ToList(), Vibrate = this.Vibrate, TimestampMillis = this.TimestampMillis, Data = this.Data, @@ -250,6 +235,18 @@ public sealed class Action /// [JsonProperty("icon")] public string Icon { get; set; } + + /// + /// Creates a new Action instance. + /// + public Action() { } + + internal Action(Action action) + { + ActionName = action.ActionName; + Title = action.Title; + Icon = action.Icon; + } } /// From 7acaaf696a2000bb98f22095254cf3c6dd51699e Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 20 Dec 2018 00:02:24 -0800 Subject: [PATCH 23/50] Minor cleanup of internal properties --- .../FirebaseAdmin.Tests/Messaging/MessageTest.cs | 12 ++---------- .../FirebaseAdmin/Messaging/AndroidConfig.cs | 4 ++-- FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs | 2 +- .../FirebaseAdmin/Messaging/WebpushNotification.cs | 2 +- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index c1814575..34512d2f 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -285,16 +285,6 @@ public void AndroidConfigInvalidTTL() TimeToLive = TimeSpan.FromHours(-1), }, }; - var expected = new JObject() - { - {"topic", "test-topic"}, - { - "android", new JObject() - { - { "ttl", "3600s" }, - } - }, - }; Assert.Throws(() => message.CopyAndValidate()); } @@ -314,6 +304,8 @@ public void AndroidConfigDeserialization() }; 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); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 32f61aaa..e3438197 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -42,7 +42,7 @@ public sealed class AndroidConfig /// service. /// [JsonProperty("priority")] - internal string PriorityString + private string PriorityString { get { @@ -82,7 +82,7 @@ internal string PriorityString /// by the number of seconds, with nanoseconds expressed as fractional seconds. /// [JsonProperty("ttl")] - internal string TtlString + private string TtlString { get { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 0ec4098f..6635761e 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -47,7 +47,7 @@ public sealed class Message /// prefix if present. This is what's ultimately sent to the FCM service. /// [JsonProperty("topic")] - internal string UnprefixedTopic + private string UnprefixedTopic { get { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index f75276ec..618f1949 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -70,7 +70,7 @@ public sealed class WebpushNotification /// in the json output. /// [JsonProperty("dir")] - internal string DirectionString + private string DirectionString { get { From 7cc6f1ff5f3f11470b1c05b28245cf8d5e3c69d8 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 20 Dec 2018 13:37:29 -0800 Subject: [PATCH 24/50] More test cases --- .../Messaging/MessageTest.cs | 440 ++++++++++++++---- .../Messaging/WebpushNotification.cs | 13 +- 2 files changed, 346 insertions(+), 107 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 34512d2f..0fd03535 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json.Linq; using Xunit; using Google.Apis.Json; @@ -23,12 +24,6 @@ namespace FirebaseAdmin.Messaging.Tests { public class MessageTest { - [Fact] - public void MessageWithoutTarget() - { - Assert.Throws(() => new Message().CopyAndValidate()); - } - [Fact] public void EmptyMessage() { @@ -43,36 +38,10 @@ public void EmptyMessage() } [Fact] - public void MultipleTargets() + public void PrefixedTopicName() { - 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()); + var message = new Message(){Topic = "/topics/test-topic"}; + AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); } [Fact] @@ -95,24 +64,29 @@ public void DataMessage() } [Fact] - public void InvalidTopicNames() + public void Notification() { - var topics = new List() + var message = new Message() { - "/topics/", "/foo/bar", "foo bar", + Topic = "test-topic", + Notification = new Notification() + { + Title = "title", + Body = "body", + }, }; - foreach (var topic in topics) + var expected = new JObject() { - var message = new Message(){Topic = topic}; - Assert.Throws(() => message.CopyAndValidate()); - } - } - - [Fact] - public void PrefixedTopicName() - { - var message = new Message(){Topic = "/topics/test-topic"}; - AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); + {"topic", "test-topic"}, + { + "notification", new JObject() + { + {"title", "title"}, + {"body", "body"}, + } + }, + }; + AssertJsonEquals(expected, message); } [Fact] @@ -121,41 +95,102 @@ public void MessageDeserialization() var original = new Message() { Topic = "test-topic", - Data = new Dictionary() + Data = new Dictionary(){{ "key", "value" }}, + Notification = new Notification() { - { "key", "value" }, + Title = "title", + Body = "body", + }, + Android = new AndroidConfig() + { + RestrictedPackageName = "test-pkg-name", + }, + 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.Webpush.Data, copy.Webpush.Data); } [Fact] - public void Notification() + public void MessageCopy() + { + var original = new Message() + { + Topic = "test-topic", + Data = new Dictionary(), + Notification = new Notification(), + Android = new AndroidConfig(), + 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.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", - Notification = new Notification() - { - Title = "title", - Body = "body", - }, }; - var expected = new JObject() + Assert.Throws(() => message.CopyAndValidate()); + + message = new Message() { - {"topic", "test-topic"}, - { - "notification", new JObject() - { - {"title", "title"}, - {"body", "body"}, - } - }, + Token = "test-token", + Condition = "test-condition", }; - AssertJsonEquals(expected, message); + 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] @@ -232,20 +267,12 @@ public void AndroidConfigMinimal() var message = new Message() { Topic = "test-topic", - Android = new AndroidConfig() - { - RestrictedPackageName = "test-pkg-name", - }, + Android = new AndroidConfig(), }; var expected = new JObject() { {"topic", "test-topic"}, - { - "android", new JObject() - { - { "restricted_package_name", "test-pkg-name" }, - } - }, + {"android", new JObject()}, }; AssertJsonEquals(expected, message); } @@ -274,20 +301,6 @@ public void AndroidConfigFullSecondsTTL() AssertJsonEquals(expected, message); } - [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 AndroidConfigDeserialization() { @@ -301,6 +314,10 @@ public void AndroidConfigDeserialization() { { "key", "value" }, }, + Notification = new AndroidNotification() + { + Title = "title", + }, }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); @@ -309,6 +326,86 @@ public void AndroidConfigDeserialization() 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] @@ -491,6 +588,22 @@ public void WebpushConfig() 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()}, + }; + AssertJsonEquals(expected, message); + } + [Fact] public void WebpushConfigMinimalNotification() { @@ -528,21 +641,46 @@ public void WebpushConfigMinimalNotification() } [Fact] - public void WebpushConfigDuplicateKeys() + public void WebpushConfigDeserialization() { - var message = new Message() + var original = new WebpushConfig() { - Topic = "test-topic", - Webpush = new WebpushConfig() + Headers = new Dictionary() { - Notification = new WebpushNotification() - { - Title = "title", - CustomData = new Dictionary(){{"title", "other"}}, - }, + {"header1", "header-value1"}, + {"header2", "header-value2"}, + }, + Data = new Dictionary() + { + {"key1", "value1"}, + {"key2", "value2"}, + }, + Notification = new WebpushNotification() + { + Title = "title", }, }; - Assert.Throws(() => message.CopyAndValidate()); + 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] @@ -550,6 +688,38 @@ 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"}, @@ -558,9 +728,77 @@ public void WebpushNotificationDeserialization() }; 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()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index 618f1949..c6c49d3f 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -153,6 +153,12 @@ private string DirectionString [JsonProperty("vibrate")] public int[] Vibrate { get; set; } + /// + /// A collection of notification actions to be associated with the notification. + /// + [JsonProperty("actions")] + public IEnumerable Actions; + /// /// A collection of arbitrary key-value data to be included in the notification. This is /// exposed as an to support correct @@ -161,12 +167,6 @@ private string DirectionString [JsonExtensionData] public IDictionary CustomData { get; set; } - /// - /// A collection of notification actions to be associated with the notification. - /// - [JsonProperty("actions")] - public IEnumerable Actions; - /// /// Copies this Webpush notification, and validates the content of it to ensure that it can /// be serialized into the JSON format expected by the FCM service. @@ -199,6 +199,7 @@ internal WebpushNotification CopyAndValidate() // Serialize the notification without CustomData for validation. var json = serializer.Serialize(copy); var dict = serializer.Deserialize>(json); + customData = new Dictionary(customData); foreach (var entry in customData) { if (dict.ContainsKey(entry.Key)) From 86002eb8c4decff2ad96498d72cddae82a2bb654 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 9 Jan 2019 16:21:21 -0800 Subject: [PATCH 25/50] Handling invalid priority values during deserialization --- FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 32f61aaa..49c68553 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -66,6 +66,10 @@ internal string PriorityString case "normal": Priority = Messaging.Priority.High; return; + default: + throw new FirebaseException( + $"Invalid priority value: {value}. Only 'high' and 'normal'" + + " are allowed."); } } } From b1de01f52f179e95cb1027b08635d3fa8c52d736 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 9 Jan 2019 16:44:46 -0800 Subject: [PATCH 26/50] Handling invalid direction values during deserialization --- FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index c6c49d3f..10dd7fb3 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -99,6 +99,9 @@ private string DirectionString case "rtl": Direction = Messaging.Direction.RightToLeft; return; + default: + throw new FirebaseException($"Invalid direction value: {value}. Only " + + "'auto', 'rtl' and 'ltr' are allowed."); } } } From c3b4499b11a1e1c25473d095225579696ce2e817 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 15 Jan 2019 17:25:34 -0800 Subject: [PATCH 27/50] Implementing APNs support --- .../FirebaseAdmin/Messaging/ApnsConfig.cs | 26 +++ FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 180 ++++++++++++++++++ .../FirebaseAdmin/Messaging/ApsAlert.cs | 75 ++++++++ .../FirebaseAdmin/Messaging/CriticalSound.cs | 63 ++++++ 4 files changed, 344 insertions(+) create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs new file mode 100644 index 00000000..acb7ab27 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -0,0 +1,26 @@ +// 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 Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + public sealed class ApnsConfig + { + public IReadOnlyDictionary Headers; + + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs new file mode 100644 index 00000000..0a0ad144 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -0,0 +1,180 @@ +// 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 Google.Apis.Json; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + public sealed class Aps + { + [JsonIgnore] + public ApsAlert Alert { get; set; } + + [JsonIgnore] + public string AlertString { get; set; } + + [JsonProperty("alert")] + private object AlertObject + { + get + { + if (Alert != null) + { + return Alert; + } + else if (!string.IsNullOrEmpty(AlertString)) + { + return AlertString; + } + return null; + } + set + { + if (value.GetType() == typeof(string)) + { + AlertString = (string) value; + } + else + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(value); + Alert = NewtonsoftJsonSerializer.Instance.Deserialize(json); + } + } + } + + [JsonProperty("badge")] + public int Badge { get; set; } + + [JsonIgnore] + public string Sound { get; set; } + + [JsonIgnore] + public CriticalSound CriticalSound { get; set; } + + [JsonProperty("sound")] + private object SoundObject + { + get + { + if (CriticalSound != null) + { + return CriticalSound; + } + else if (!string.IsNullOrEmpty(Sound)) + { + return Sound; + } + return null; + } + set + { + if (value.GetType() == typeof(string)) + { + Sound = (string) value; + } + else + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(value); + CriticalSound = NewtonsoftJsonSerializer.Instance.Deserialize(json); + } + } + } + + [JsonIgnore] + public bool ContentAvailable { get; set; } + + [JsonProperty("content-available")] + private int? ContentAvailableInt + { + get + { + if (ContentAvailable) + { + return 1; + } + return null; + } + set + { + ContentAvailable = (value == 1); + } + } + + [JsonIgnore] + public bool MutableContent { get; set; } + + [JsonProperty("mutable-content")] + private int? MutableContentInt + { + get + { + if (MutableContent) + { + return 1; + } + return null; + } + set + { + MutableContent = (value == 1); + } + } + + [JsonProperty("category")] + public string Category { get; set; } + + [JsonProperty("thread-id")] + public string ThreadId { get; set; } + + /// + /// A collection of arbitrary key-value data to be included in the Aps dictionary. This is + /// exposed as an to support correct + /// deserialization of custom properties. + /// + [JsonExtensionData] + public IDictionary CustomData { get; set; } + + internal Aps CopyAndValidate() + { + var copy = new Aps + { + AlertString = this.AlertString, + Badge = this.Badge, + Sound = this.Sound, + ContentAvailable = this.ContentAvailable, + MutableContent = this.MutableContent, + Category = this.Category, + ThreadId = this.ThreadId, + }; + var apsAlert = this.Alert; + if (apsAlert != null && !string.IsNullOrEmpty(copy.AlertString)) + { + throw new ArgumentException("Multiple specifications for alert (Alert and AlertString"); + } + var criticalSound = this.CriticalSound; + if (criticalSound != null && !string.IsNullOrEmpty(copy.Sound)) + { + throw new ArgumentException("Multiple specifications for sound (CriticalSound and Sound"); + } + + // Copy and validate the child properties + copy.Alert = apsAlert?.CopyAndValidate(); + copy.CriticalSound = criticalSound?.CopyAndValidate(); + return copy; + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs new file mode 100644 index 00000000..1a863156 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -0,0 +1,75 @@ +// 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 Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + public sealed class ApsAlert + { + [JsonProperty("title")] + public string Title { get; set; } + + [JsonProperty("subtitle")] + public string Subtitle { get; set; } + + [JsonProperty("body")] + public string Body { get; set; } + + [JsonProperty("loc-key")] + public string LocKey { get; set; } + + [JsonProperty("loc-args")] + public IEnumerable LocArgs { get; set; } + + [JsonProperty("title-loc-key")] + public string TitleLocKey { get; set; } + + [JsonProperty("title-loc-args")] + public IEnumerable TitleLocArgs { get; set; } + + [JsonProperty("subtitle-loc-key")] + public string SubtitleLocKey { get; set; } + + [JsonProperty("subtitle-loc-args")] + public IEnumerable SubtitleLocArgs { get; set; } + + [JsonProperty("action-loc-key")] + public string ActionLocKey { get; set; } + + [JsonProperty("launch-image")] + public string LaunchImage { get; set; } + + internal ApsAlert CopyAndValidate() + { + return new ApsAlert() + { + Title = this.Title, + Subtitle = this.Subtitle, + Body = this.Body, + LocKey = this.LocKey, + LocArgs = this.LocArgs?.ToList(), + TitleLocKey = this.TitleLocKey, + TitleLocArgs = this.TitleLocArgs?.ToList(), + SubtitleLocKey = this.SubtitleLocKey, + SubtitleLocArgs = this.SubtitleLocArgs?.ToList(), + ActionLocKey = this.ActionLocKey, + LaunchImage = this.LaunchImage, + }; + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs new file mode 100644 index 00000000..fdf319ef --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -0,0 +1,63 @@ +// 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 Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + public sealed class CriticalSound + { + [JsonIgnore] + public bool Critical { get; set; } + + [JsonProperty("critical")] + private int? CriticalInt + { + get + { + if (Critical) + { + return 1; + } + return null; + } + set + { + Critical = (value == 1); + } + } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("volume")] + public double Volume { get; set; } + + internal CriticalSound CopyAndValidate() + { + if (Volume < 0 || Volume > 1) + { + throw new ArgumentException("Volume must be in the interval [0, 1]"); + } + return new CriticalSound() + { + Critical = this.Critical, + Name = this.Name, + Volume = this.Volume, + } + } + } +} \ No newline at end of file From 693eac5a7f9a8f7c353a4614b8b848b40ff96c7c Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 16 Jan 2019 17:41:55 -0800 Subject: [PATCH 28/50] Added other APNS types --- .../Messaging/MessageTest.cs | 75 ++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 88 +++++++++++ FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 137 ++++++++++++++---- .../FirebaseAdmin/Messaging/ApsAlert.cs | 51 +++++++ .../FirebaseAdmin/Messaging/CriticalSound.cs | 25 +++- .../FirebaseAdmin/Messaging/Message.cs | 7 + 6 files changed, 354 insertions(+), 29 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 0fd03535..b2f58b10 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -799,6 +799,81 @@ public void WebpushNotificationDuplicateKeys() 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}, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index acb7ab27..8211d4ec 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -14,13 +14,101 @@ using System; using System.Collections.Generic; +using System.Linq; +using Google.Apis.Json; using Newtonsoft.Json; namespace FirebaseAdmin.Messaging { + /// + /// Represents the APNS-specific options that can be included in a . Refer + /// to + /// APNs documentation for various headers and payload fields supported by APNS. + /// public sealed class ApnsConfig { + /// + /// A collection of APNs headers. + /// + [JsonProperty("headers")] public IReadOnlyDictionary Headers; + /// + /// The aps dictionary to be included in the APNs payload. + /// + [JsonIgnore] + public Aps Aps; + + /// + /// APNs payload as accepted by the FCM backend servers. + /// + [JsonProperty("payload")] + private IReadOnlyDictionary Payload + { + get + { + var aps = this.Aps; + if (aps == null) + { + throw new ArgumentException("Aps must not be null in ApnsConfig."); + } + var payload = new Dictionary() + { + { "aps", aps }, + }; + var customData = this.CustomData; + if (customData != null) + { + if (customData.ContainsKey("aps")) + { + throw new ArgumentException( + "Multiple specifications for Apns payload key: aps"); + } + payload = payload.Concat(customData).ToDictionary(x=>x.Key, x=>x.Value); + } + return payload; + } + set + { + var copy = value.ToDictionary(x=>x.Key, x=>x.Value); + object aps; + if (copy.TryGetValue("aps", out aps)) + { + copy.Remove("aps"); + if (aps.GetType() == typeof(Aps)) + { + this.Aps = (Aps) aps; + } + else + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(aps); + this.Aps = NewtonsoftJsonSerializer.Instance.Deserialize(json); + } + } + this.CustomData = copy; + } + } + + /// + /// A collection of arbitrary key-value data to be included in the APNs payload. + /// + [JsonIgnore] + public IReadOnlyDictionary CustomData { get; set; } + + /// + /// Copies this APNs config, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. + /// + + internal ApnsConfig CopyAndValidate() + { + var copy = new ApnsConfig() + { + Headers = this.Headers.Copy(), + Payload = this.Payload, + }; + copy.Aps = copy.Aps?.CopyAndValidate(); + return copy; + } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index 0a0ad144..add0cb5c 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -19,11 +19,24 @@ namespace FirebaseAdmin.Messaging { + /// + /// Represents the + /// aps dictionary that is part of every APNs message. + /// public sealed class Aps { + /// + /// An advanced alert configuration to be included in the message. It is an error to set + /// both and properties together. + /// [JsonIgnore] public ApsAlert Alert { get; set; } + /// + /// The alert text to be included in the message. To specify a more advanced alert + /// configuration, use the property instead. It is an error to set + /// both and properties together. + /// [JsonIgnore] public string AlertString { get; set; } @@ -32,22 +45,32 @@ private object AlertObject { get { - if (Alert != null) + object alert = this.AlertString; + if (string.IsNullOrEmpty(alert as string)) { - return Alert; + alert = this.Alert; } - else if (!string.IsNullOrEmpty(AlertString)) + else if (this.Alert != null) { - return AlertString; + throw new ArgumentException( + "Multiple specifications for alert (Alert and AlertString"); } - return null; + return alert; } set { - if (value.GetType() == typeof(string)) + if (value == null) + { + return; + } + else if (value.GetType() == typeof(string)) { AlertString = (string) value; } + else if (value.GetType() == typeof(ApsAlert)) + { + Alert = (ApsAlert) value; + } else { var json = NewtonsoftJsonSerializer.Instance.Serialize(value); @@ -56,12 +79,26 @@ private object AlertObject } } + /// + /// The badge to be displayed with the message. Set to 0 to remove the badge. When not + /// specified, the badge will remain unchanged. + /// [JsonProperty("badge")] - public int Badge { get; set; } + public int? Badge { get; set; } + /// + /// The name of a sound file in your app's main bundle or in the + /// Library/Sounds folder of your app's container directory. Specify the + /// string default to play the system sound. It is an error to set both + /// and properties together. + /// [JsonIgnore] public string Sound { get; set; } + /// + /// The critical alert sound to be played with the message. It is an error to set both + /// and properties together. + /// [JsonIgnore] public CriticalSound CriticalSound { get; set; } @@ -70,22 +107,32 @@ private object SoundObject { get { - if (CriticalSound != null) + object sound = this.Sound; + if (string.IsNullOrEmpty(sound as string)) { - return CriticalSound; + sound = this.CriticalSound; } - else if (!string.IsNullOrEmpty(Sound)) + else if (this.CriticalSound != null) { - return Sound; + throw new ArgumentException( + "Multiple specifications for sound (CriticalSound and Sound"); } - return null; + return sound; } set { - if (value.GetType() == typeof(string)) + if (value == null) + { + return; + } + else if (value.GetType() == typeof(string)) { Sound = (string) value; } + else if (value.GetType() == typeof(CriticalSound)) + { + CriticalSound = (CriticalSound) value; + } else { var json = NewtonsoftJsonSerializer.Instance.Serialize(value); @@ -94,9 +141,16 @@ private object SoundObject } } + /// + /// Specifies whether to configure a background update notification. + /// [JsonIgnore] public bool ContentAvailable { get; set; } + /// + /// Integer representation of the property, which is how + /// APNs expects it. + /// [JsonProperty("content-available")] private int? ContentAvailableInt { @@ -114,9 +168,17 @@ private int? ContentAvailableInt } } + /// + /// Specifies whether to set the mutable-content property on the message. When + /// set, this property allows clients to modify the notification via app extensions. + /// [JsonIgnore] public bool MutableContent { get; set; } + /// + /// Integer representation of the property, which is how + /// APNs expects it. + /// [JsonProperty("mutable-content")] private int? MutableContentInt { @@ -134,46 +196,65 @@ private int? MutableContentInt } } + /// + /// The type of the notification. + /// [JsonProperty("category")] public string Category { get; set; } + /// + /// An app-specific identifier for grouping notifications. + /// [JsonProperty("thread-id")] public string ThreadId { get; set; } /// - /// A collection of arbitrary key-value data to be included in the Aps dictionary. This is - /// exposed as an to support correct - /// deserialization of custom properties. + /// A collection of arbitrary key-value data to be included in the aps + /// dictionary. This is exposed as an to support + /// correct deserialization of custom properties. /// [JsonExtensionData] public IDictionary CustomData { get; set; } + /// + /// Copies this Aps dictionary, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM and APNs services. + /// internal Aps CopyAndValidate() { var copy = new Aps { - AlertString = this.AlertString, + AlertObject = this.AlertObject, Badge = this.Badge, - Sound = this.Sound, + SoundObject = this.SoundObject, ContentAvailable = this.ContentAvailable, MutableContent = this.MutableContent, Category = this.Category, ThreadId = this.ThreadId, }; - var apsAlert = this.Alert; - if (apsAlert != null && !string.IsNullOrEmpty(copy.AlertString)) - { - throw new ArgumentException("Multiple specifications for alert (Alert and AlertString"); - } - var criticalSound = this.CriticalSound; - if (criticalSound != null && !string.IsNullOrEmpty(copy.Sound)) + + var customData = this.CustomData; + if (customData?.Count > 0) { - throw new ArgumentException("Multiple specifications for sound (CriticalSound and Sound"); + var serializer = NewtonsoftJsonSerializer.Instance; + // Serialize the notification without CustomData for validation. + var json = serializer.Serialize(copy); + var dict = serializer.Deserialize>(json); + customData = new Dictionary(customData); + foreach (var entry in customData) + { + if (dict.ContainsKey(entry.Key)) + { + throw new ArgumentException( + $"Multiple specifications for Aps key: {entry.Key}"); + } + } + copy.CustomData = customData; } // Copy and validate the child properties - copy.Alert = apsAlert?.CopyAndValidate(); - copy.CriticalSound = criticalSound?.CopyAndValidate(); + copy.Alert = copy.Alert?.CopyAndValidate(); + copy.CriticalSound = copy.CriticalSound?.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs index 1a863156..9394eb3b 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -19,41 +19,92 @@ namespace FirebaseAdmin.Messaging { + /// + /// Represents the + /// alert property that can be included in the aps dictionary of an APNs + /// payload. + /// public sealed class ApsAlert { + /// + /// The title of the alert. When provided, overrides the title set via + /// . + /// [JsonProperty("title")] public string Title { get; set; } + /// + /// The subtitle of the alert. + /// [JsonProperty("subtitle")] public string Subtitle { get; set; } + /// + /// The body of the alert. When provided, overrides the body set via + /// . + /// [JsonProperty("body")] public string Body { get; set; } + /// + /// The key of the body string in the app's string resources to use to localize the body + /// text. + /// [JsonProperty("loc-key")] public string LocKey { get; set; } + /// + /// Resource key strings that will be used in place of the format specifiers in + /// . + /// [JsonProperty("loc-args")] public IEnumerable LocArgs { get; set; } + /// + /// The key of the title string in the app's string resources to use to localize the title + /// text. + /// [JsonProperty("title-loc-key")] public string TitleLocKey { get; set; } + /// + /// Resource key strings that will be used in place of the format specifiers in + /// . + /// [JsonProperty("title-loc-args")] public IEnumerable TitleLocArgs { get; set; } + /// + /// The key of the subtitle string in the app's string resources to use to localize the + /// subtitle text. + /// [JsonProperty("subtitle-loc-key")] public string SubtitleLocKey { get; set; } + /// + /// Resource key strings that will be used in place of the format specifiers in + /// . + /// [JsonProperty("subtitle-loc-args")] public IEnumerable SubtitleLocArgs { get; set; } + /// + /// The key of the text in the app's string resources to use to localize the action button + /// text. + /// [JsonProperty("action-loc-key")] public string ActionLocKey { get; set; } + /// + /// The launch image for the notification action. + /// [JsonProperty("launch-image")] public string LaunchImage { get; set; } + /// + /// Copies this alert dictionary, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM and APNs services. + /// internal ApsAlert CopyAndValidate() { return new ApsAlert() diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index fdf319ef..2c689ca4 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -18,11 +18,21 @@ namespace FirebaseAdmin.Messaging { + /// + /// The sound configuration for APNs critical alerts. + /// public sealed class CriticalSound { + /// + /// Sets the critical alert flag on the sound configuration. + /// [JsonIgnore] public bool Critical { get; set; } + /// + /// Integer representation of the property, which is how + /// APNs expects it. + /// [JsonProperty("critical")] private int? CriticalInt { @@ -40,12 +50,25 @@ private int? CriticalInt } } + /// + /// The name of a sound file in your app's main bundle or in the + /// Library/Sounds folder of your app's container directory. Specify the + /// string default to play the system sound. + /// [JsonProperty("name")] public string Name { get; set; } + /// + /// The volume for the critical alert's sound. Must be a value between 0.0 (silent) and + /// 1.0 (full volume). + /// [JsonProperty("volume")] public double Volume { get; set; } + /// + /// Copies this critical sound configuration, and validates the content of it to ensure + /// that it can be serialized into the JSON format expected by the FCM and APNs services. + /// internal CriticalSound CopyAndValidate() { if (Volume < 0 || Volume > 1) @@ -57,7 +80,7 @@ internal CriticalSound CopyAndValidate() Critical = this.Critical, Name = this.Name, Volume = this.Volume, - } + }; } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 6635761e..64c226c8 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -95,6 +95,12 @@ private string UnprefixedTopic [JsonProperty("webpush")] public WebpushConfig Webpush { get; set; } + /// + /// The APNs-specific information to be included in the message. + /// + [JsonProperty("apns")] + public ApnsConfig Apns { get; set; } + /// /// Copies this message, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM service. Each property is copied @@ -131,6 +137,7 @@ internal Message CopyAndValidate() copy.Notification = this.Notification?.CopyAndValidate(); copy.Android = this.Android?.CopyAndValidate(); copy.Webpush = this.Webpush?.CopyAndValidate(); + copy.Apns = this.Apns?.CopyAndValidate(); return copy; } } From a4edaf142b8929bd0b1394da40a6a43d759e7db8 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 16 Jan 2019 22:28:42 -0800 Subject: [PATCH 29/50] Cleaning up the Apns impl --- .../Messaging/MessageTest.cs | 59 +++++++++++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 37 +++++++----- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 31 +++++----- .../FirebaseAdmin/Messaging/CriticalSound.cs | 5 +- 4 files changed, 100 insertions(+), 32 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index b2f58b10..d6bd3ddd 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -874,6 +874,65 @@ public void ApnsConfig() 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 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()); + } + private void AssertJsonEquals(JObject expected, Message actual) { var json = NewtonsoftJsonSerializer.Instance.Serialize(actual.CopyAndValidate()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 8211d4ec..6bcd32c9 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -47,24 +47,17 @@ private IReadOnlyDictionary Payload { get { + var payload = this.CustomData?.ToDictionary(e => e.Key, e => e.Value) + ?? new Dictionary(); var aps = this.Aps; - if (aps == null) + if (aps != null) { - throw new ArgumentException("Aps must not be null in ApnsConfig."); - } - var payload = new Dictionary() - { - { "aps", aps }, - }; - var customData = this.CustomData; - if (customData != null) - { - if (customData.ContainsKey("aps")) + if (payload.ContainsKey("aps")) { throw new ArgumentException( "Multiple specifications for Apns payload key: aps"); } - payload = payload.Concat(customData).ToDictionary(x=>x.Key, x=>x.Value); + payload["aps"] = aps; } return payload; } @@ -104,10 +97,24 @@ internal ApnsConfig CopyAndValidate() { var copy = new ApnsConfig() { - Headers = this.Headers.Copy(), - Payload = this.Payload, + Headers = this.Headers?.Copy(), + CustomData = this.CustomData?.Copy(), }; - copy.Aps = copy.Aps?.CopyAndValidate(); + var aps = this.Aps; + if (copy.CustomData != null && copy.CustomData.ContainsKey("aps")) + { + if (aps != null) + { + throw new ArgumentException( + "Multiple specifications for Apns payload key: aps"); + } + } + else if (aps == null) + { + throw new ArgumentException( + "Aps dictionary is required in ApnsConfig"); + } + copy.Aps = aps?.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index add0cb5c..335e358d 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Google.Apis.Json; using Newtonsoft.Json; @@ -25,6 +26,8 @@ namespace FirebaseAdmin.Messaging /// public sealed class Aps { + private static readonly NewtonsoftJsonSerializer Serializer = NewtonsoftJsonSerializer.Instance; + /// /// An advanced alert configuration to be included in the message. It is an error to set /// both and properties together. @@ -73,8 +76,8 @@ private object AlertObject } else { - var json = NewtonsoftJsonSerializer.Instance.Serialize(value); - Alert = NewtonsoftJsonSerializer.Instance.Deserialize(json); + var json = Serializer.Serialize(value); + Alert = Serializer.Deserialize(json); } } } @@ -135,8 +138,8 @@ private object SoundObject } else { - var json = NewtonsoftJsonSerializer.Instance.Serialize(value); - CriticalSound = NewtonsoftJsonSerializer.Instance.Deserialize(json); + var json = Serializer.Serialize(value); + CriticalSound = Serializer.Deserialize(json); } } } @@ -226,33 +229,31 @@ internal Aps CopyAndValidate() { AlertObject = this.AlertObject, Badge = this.Badge, - SoundObject = this.SoundObject, ContentAvailable = this.ContentAvailable, MutableContent = this.MutableContent, Category = this.Category, + SoundObject = this.SoundObject, ThreadId = this.ThreadId, }; - var customData = this.CustomData; + var customData = this.CustomData?.ToDictionary(e => e.Key, e => e.Value); if (customData?.Count > 0) { var serializer = NewtonsoftJsonSerializer.Instance; // Serialize the notification without CustomData for validation. var json = serializer.Serialize(copy); - var dict = serializer.Deserialize>(json); - customData = new Dictionary(customData); - foreach (var entry in customData) + var standardApsProperties = serializer.Deserialize>(json); + var duplicates = customData.Keys + .Where(customKey => standardApsProperties.ContainsKey(customKey)) + .ToList(); + if (duplicates.Any()) { - if (dict.ContainsKey(entry.Key)) - { - throw new ArgumentException( - $"Multiple specifications for Aps key: {entry.Key}"); - } + throw new ArgumentException( + $"Multiple specifications for Aps keys: {string.Join(",", duplicates)}"); } copy.CustomData = customData; } - // Copy and validate the child properties copy.Alert = copy.Alert?.CopyAndValidate(); copy.CriticalSound = copy.CriticalSound?.CopyAndValidate(); return copy; diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index 2c689ca4..f4bfac67 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -71,7 +71,8 @@ private int? CriticalInt /// internal CriticalSound CopyAndValidate() { - if (Volume < 0 || Volume > 1) + var volume = this.Volume; + if (volume < 0 || volume > 1) { throw new ArgumentException("Volume must be in the interval [0, 1]"); } @@ -79,7 +80,7 @@ internal CriticalSound CopyAndValidate() { Critical = this.Critical, Name = this.Name, - Volume = this.Volume, + Volume = volume, }; } } From 31e317b5e14c581c7742174fbca7467af71b8977 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 16 Jan 2019 23:51:23 -0800 Subject: [PATCH 30/50] More clean up and tests --- .../Messaging/MessageTest.cs | 288 ++++++++++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 19 +- .../FirebaseAdmin/Messaging/CriticalSound.cs | 20 +- 3 files changed, 304 insertions(+), 23 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index d6bd3ddd..cccd2ca4 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -874,6 +874,214 @@ public void ApnsConfig() 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()}, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [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}, + } + }, + } + }, + } + }, + } + }, + }; + 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"}, + } + }, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [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}, + } + }, + } + }, + } + }, + }; + 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}, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + [Fact] public void ApnsNoAps() { @@ -933,6 +1141,86 @@ public void ApnsDuplicateApsAlerts() Assert.Throws(() => message.CopyAndValidate()); } + [Fact] + public void ApnsDuplicateSounds() + { + 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()); diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 6bcd32c9..5fa881bd 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -92,29 +92,18 @@ private IReadOnlyDictionary Payload /// Copies this APNs config, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM service. /// - internal ApnsConfig CopyAndValidate() { var copy = new ApnsConfig() { Headers = this.Headers?.Copy(), - CustomData = this.CustomData?.Copy(), + Payload = this.Payload, }; - var aps = this.Aps; - if (copy.CustomData != null && copy.CustomData.ContainsKey("aps")) - { - if (aps != null) - { - throw new ArgumentException( - "Multiple specifications for Apns payload key: aps"); - } - } - else if (aps == null) + if (copy.Aps == null) { - throw new ArgumentException( - "Aps dictionary is required in ApnsConfig"); + throw new ArgumentException("Aps dictionary is required in ApnsConfig"); } - copy.Aps = aps?.CopyAndValidate(); + copy.Aps = copy.Aps.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index f4bfac67..636fe5b7 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -63,7 +63,7 @@ private int? CriticalInt /// 1.0 (full volume). /// [JsonProperty("volume")] - public double Volume { get; set; } + public double? Volume { get; set; } /// /// Copies this critical sound configuration, and validates the content of it to ensure @@ -71,17 +71,21 @@ private int? CriticalInt /// internal CriticalSound CopyAndValidate() { - var volume = this.Volume; - if (volume < 0 || volume > 1) - { - throw new ArgumentException("Volume must be in the interval [0, 1]"); - } - return new CriticalSound() + var copy = new CriticalSound() { Critical = this.Critical, Name = this.Name, - Volume = volume, + Volume = this.Volume, }; + if (string.IsNullOrEmpty(copy.Name)) + { + throw new ArgumentException("Name must be specified for CriticalSound"); + } + if (copy.Volume < 0 || copy.Volume > 1) + { + throw new ArgumentException("Volume of CriticalSound must be in the interval [0, 1]"); + } + return copy; } } } \ No newline at end of file From 0eade9c1785ff04281e954642ee9db370243a0c3 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 17 Jan 2019 01:16:05 -0800 Subject: [PATCH 31/50] Using more Linq; More tests --- .../Messaging/MessageTest.cs | 16 ++++++++++++- .../Messaging/AndroidNotification.cs | 16 ++++--------- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 17 ++++--------- .../Messaging/WebpushNotification.cs | 24 +++++++++---------- 4 files changed, 36 insertions(+), 37 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index cccd2ca4..3de2c00c 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -1120,6 +1120,20 @@ public void ApnsDuplicateAps() 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() { @@ -1142,7 +1156,7 @@ public void ApnsDuplicateApsAlerts() } [Fact] - public void ApnsDuplicateSounds() + public void ApnsDuplicateApsSounds() { var message = new Message() { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 417efe70..24e546fa 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -135,21 +135,15 @@ internal AndroidNotification CopyAndValidate() }; if (copy.Color != null && !Regex.Match(copy.Color, "^#[0-9a-fA-F]{6}$").Success) { - throw new ArgumentException("Color must be in the form #RRGGBB"); + throw new ArgumentException("Color must be in the form #RRGGBB."); } - if (copy.TitleLocArgs != null && copy.TitleLocArgs.Any()) + if (copy.TitleLocArgs?.Any() == true && string.IsNullOrEmpty(copy.TitleLocKey)) { - if (string.IsNullOrEmpty(copy.TitleLocKey)) - { - throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs"); - } + throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs."); } - if (copy.BodyLocArgs != null && copy.BodyLocArgs.Any()) + if (copy.BodyLocArgs?.Any() == true && string.IsNullOrEmpty(copy.BodyLocKey)) { - if (string.IsNullOrEmpty(copy.BodyLocKey)) - { - throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs"); - } + throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs."); } return copy; } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index 335e358d..a8bbd4c2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -159,11 +159,7 @@ private int? ContentAvailableInt { get { - if (ContentAvailable) - { - return 1; - } - return null; + return ContentAvailable ? 1 : (int?) null; } set { @@ -187,11 +183,7 @@ private int? MutableContentInt { get { - if (MutableContent) - { - return 1; - } - return null; + return MutableContent ? 1 : (int?) null; } set { @@ -240,11 +232,10 @@ internal Aps CopyAndValidate() if (customData?.Count > 0) { var serializer = NewtonsoftJsonSerializer.Instance; - // Serialize the notification without CustomData for validation. var json = serializer.Serialize(copy); - var standardApsProperties = serializer.Deserialize>(json); + var standardProperties = serializer.Deserialize>(json); var duplicates = customData.Keys - .Where(customKey => standardApsProperties.ContainsKey(customKey)) + .Where(customProperty => standardProperties.ContainsKey(customProperty)) .ToList(); if (duplicates.Any()) { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index 10dd7fb3..602d5c01 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -100,8 +100,9 @@ private string DirectionString Direction = Messaging.Direction.RightToLeft; return; default: - throw new FirebaseException($"Invalid direction value: {value}. Only " - + "'auto', 'rtl' and 'ltr' are allowed."); + throw new FirebaseException( + $"Invalid direction value: {value}. Only 'auto', 'rtl' and 'ltr' " + + "are allowed."); } } } @@ -195,21 +196,20 @@ internal WebpushNotification CopyAndValidate() Data = this.Data, }; - var customData = this.CustomData; + var customData = this.CustomData?.ToDictionary(e => e.Key, e => e.Value); if (customData?.Count > 0) { var serializer = NewtonsoftJsonSerializer.Instance; - // Serialize the notification without CustomData for validation. var json = serializer.Serialize(copy); - var dict = serializer.Deserialize>(json); - customData = new Dictionary(customData); - foreach (var entry in customData) + var standardProperties = serializer.Deserialize>(json); + var duplicates = customData.Keys + .Where(customProperty => standardProperties.ContainsKey(customProperty)) + .ToList(); + if (duplicates.Any()) { - if (dict.ContainsKey(entry.Key)) - { - throw new ArgumentException( - $"Multiple specifications for WebpushNotification key: {entry.Key}"); - } + throw new ArgumentException( + "Multiple specifications for WebpushNotification keys: " + + string.Join(",", duplicates)); } copy.CustomData = customData; } From 6124d4590eea439d09e406c62a6d1023573b4fc3 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 17 Jan 2019 18:17:56 -0800 Subject: [PATCH 32/50] Fixed Apns deserialization --- .../Messaging/MessageTest.cs | 244 ++++++++++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 86 +++--- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 2 +- .../FirebaseAdmin/Messaging/ApsAlert.cs | 15 +- .../Messaging/WebpushNotification.cs | 2 +- 5 files changed, 312 insertions(+), 37 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 3de2c00c..e7b1837a 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -903,6 +903,63 @@ public void ApnsConfigMinimal() 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 ApnsConfigCustomApsDeserialization() + { + var original = new ApnsConfig() + { + Headers = new Dictionary() + { + {"k1", "v1"}, + {"k2", "v2"}, + }, + CustomData = new Dictionary() + { + { + "aps", new Dictionary() + { + {"alert", "alert-text"}, + } + }, + {"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); + } + [Fact] public void ApnsCriticalSound() { @@ -994,6 +1051,170 @@ public void ApnsCriticalSoundMinimal() AssertJsonEquals(expected, message); } + [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"}, + } + }, + } + }, + } + }, + } + }, + }; + 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(){} + }, + } + }, + } + }, + } + }, + }; + AssertJsonEquals(expected, message); + } + + [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() { @@ -1244,4 +1465,27 @@ private void AssertJsonEquals(JObject expected, Message actual) $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}"); } } + + internal class Hello + { + + [JsonIgnore] + internal Aps Aps { get; set; } + + [JsonProperty("payload")] + internal ApnsPayload Payload + { + get + { + return new ApnsPayload() + { + Aps = this.Aps, + }; + } + set + { + this.Aps = value.Aps; + } + } + } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 5fa881bd..a9b4efb9 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -27,58 +27,47 @@ namespace FirebaseAdmin.Messaging /// public sealed class ApnsConfig { + private ApnsPayload _payload = new ApnsPayload(); + /// /// A collection of APNs headers. /// [JsonProperty("headers")] - public IReadOnlyDictionary Headers; + public IReadOnlyDictionary Headers { get; set; } /// /// The aps dictionary to be included in the APNs payload. /// [JsonIgnore] - public Aps Aps; + public Aps Aps + { + get + { + return Payload.Aps; + } + set + { + Payload.Aps = value; + } + } /// /// APNs payload as accepted by the FCM backend servers. /// [JsonProperty("payload")] - private IReadOnlyDictionary Payload + private ApnsPayload Payload { get { - var payload = this.CustomData?.ToDictionary(e => e.Key, e => e.Value) - ?? new Dictionary(); - var aps = this.Aps; - if (aps != null) + if (_payload.Aps != null && _payload.CustomData?.ContainsKey("aps") == true) { - if (payload.ContainsKey("aps")) - { - throw new ArgumentException( - "Multiple specifications for Apns payload key: aps"); - } - payload["aps"] = aps; + throw new ArgumentException("Multiple specifications for ApnsConfig key: aps"); } - return payload; + return _payload; } set { - var copy = value.ToDictionary(x=>x.Key, x=>x.Value); - object aps; - if (copy.TryGetValue("aps", out aps)) - { - copy.Remove("aps"); - if (aps.GetType() == typeof(Aps)) - { - this.Aps = (Aps) aps; - } - else - { - var json = NewtonsoftJsonSerializer.Instance.Serialize(aps); - this.Aps = NewtonsoftJsonSerializer.Instance.Deserialize(json); - } - } - this.CustomData = copy; + _payload = value; } } @@ -86,7 +75,17 @@ private IReadOnlyDictionary Payload /// A collection of arbitrary key-value data to be included in the APNs payload. /// [JsonIgnore] - public IReadOnlyDictionary CustomData { get; set; } + public IDictionary CustomData + { + get + { + return Payload.CustomData; + } + set + { + Payload.CustomData = value; + } + } /// /// Copies this APNs config, and validates the content of it to ensure that it can be @@ -97,13 +96,32 @@ internal ApnsConfig CopyAndValidate() var copy = new ApnsConfig() { Headers = this.Headers?.Copy(), - Payload = this.Payload, + Payload = this.Payload.CopyAndValidate(), + }; + return copy; + } + } + + internal class ApnsPayload + { + [JsonProperty("aps")] + public Aps Aps { get; set; } + + [JsonExtensionData] + public IDictionary CustomData { get; set; } + + internal ApnsPayload CopyAndValidate() + { + var copy = new ApnsPayload() + { + CustomData = this.CustomData?.ToDictionary(e => e.Key, e => e.Value), }; - if (copy.Aps == null) + var aps = this.Aps; + if (aps == null && copy.CustomData?.ContainsKey("aps") == false) { throw new ArgumentException("Aps dictionary is required in ApnsConfig"); } - copy.Aps = copy.Aps.CopyAndValidate(); + copy.Aps = aps?.CopyAndValidate(); return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index a8bbd4c2..585f4452 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -235,7 +235,7 @@ internal Aps CopyAndValidate() var json = serializer.Serialize(copy); var standardProperties = serializer.Deserialize>(json); var duplicates = customData.Keys - .Where(customProperty => standardProperties.ContainsKey(customProperty)) + .Where(customKey => standardProperties.ContainsKey(customKey)) .ToList(); if (duplicates.Any()) { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs index 9394eb3b..f84ab225 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -107,7 +107,7 @@ public sealed class ApsAlert /// internal ApsAlert CopyAndValidate() { - return new ApsAlert() + var copy = new ApsAlert() { Title = this.Title, Subtitle = this.Subtitle, @@ -121,6 +121,19 @@ internal ApsAlert CopyAndValidate() ActionLocKey = this.ActionLocKey, LaunchImage = this.LaunchImage, }; + if (copy.TitleLocArgs?.Any() == true && string.IsNullOrEmpty(copy.TitleLocKey)) + { + throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs."); + } + if (copy.SubtitleLocArgs?.Any() == true && string.IsNullOrEmpty(copy.SubtitleLocKey)) + { + throw new ArgumentException("SubtitleLocKey is required when specifying SubtitleLocArgs."); + } + if (copy.LocArgs?.Any() == true && string.IsNullOrEmpty(copy.LocKey)) + { + throw new ArgumentException("LocKey is required when specifying LocArgs."); + } + return copy; } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index 602d5c01..9c9b1f81 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -203,7 +203,7 @@ internal WebpushNotification CopyAndValidate() var json = serializer.Serialize(copy); var standardProperties = serializer.Deserialize>(json); var duplicates = customData.Keys - .Where(customProperty => standardProperties.ContainsKey(customProperty)) + .Where(customKey => standardProperties.ContainsKey(customKey)) .ToList(); if (duplicates.Any()) { From f4fe28b99a201e1033c8b42ec655f0bfe1dcfe45 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Thu, 17 Jan 2019 21:19:47 -0800 Subject: [PATCH 33/50] Adding more APNS tests --- .../Messaging/MessageTest.cs | 79 +++++++++++++------ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 2 +- 2 files changed, 57 insertions(+), 24 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index e7b1837a..a5033d9b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -930,6 +930,22 @@ public void ApnsConfigDeserialization() 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() { @@ -946,6 +962,8 @@ public void ApnsConfigCustomApsDeserialization() "aps", new Dictionary() { {"alert", "alert-text"}, + {"custom-key1", "custom-data"}, + {"custom-key2", true}, } }, {"custom-key3", "custom-data"}, @@ -958,6 +976,12 @@ public void ApnsConfigCustomApsDeserialization() 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] @@ -1155,6 +1179,38 @@ public void ApnsApsAlertMinimal() 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 ApnsApsAlertInvalidTitleLocArgs() { @@ -1465,27 +1521,4 @@ private void AssertJsonEquals(JObject expected, Message actual) $"Expected: {expected.ToString()}\nActual: {parsed.ToString()}"); } } - - internal class Hello - { - - [JsonIgnore] - internal Aps Aps { get; set; } - - [JsonProperty("payload")] - internal ApnsPayload Payload - { - get - { - return new ApnsPayload() - { - Aps = this.Aps, - }; - } - set - { - this.Aps = value.Aps; - } - } - } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index a9b4efb9..39df3b4a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -102,7 +102,7 @@ internal ApnsConfig CopyAndValidate() } } - internal class ApnsPayload + internal sealed class ApnsPayload { [JsonProperty("aps")] public Aps Aps { get; set; } From 9b50ff9275bac8420239234a2f00d980a750dc32 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Sun, 20 Jan 2019 23:15:09 -0800 Subject: [PATCH 34/50] Adding Stylecop based linting --- FirebaseAdmin/FirebaseAdmin/AppOptions.cs | 24 ++-- .../FirebaseAdmin/Auth/FirebaseAuth.cs | 80 ++++++------ .../FirebaseAdmin/Auth/FirebaseToken.cs | 24 ++-- .../Auth/FirebaseTokenFactory.cs | 80 +++++++----- .../Auth/FirebaseTokenVerifier.cs | 21 +++- .../FirebaseAdmin/Auth/HttpPublicKeySource.cs | 6 +- FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs | 15 ++- FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs | 10 +- FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs | 12 +- .../Auth/ServiceAccountSigner.cs | 4 +- FirebaseAdmin/FirebaseAdmin/Extensions.cs | 7 +- .../FirebaseAdmin/FirebaseAdmin.csproj | 2 + FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs | 116 +++++++++--------- .../FirebaseAdmin/FirebaseException.cs | 10 +- stylecop.ruleset | 30 +++++ 16 files changed, 254 insertions(+), 189 deletions(-) create mode 100644 stylecop.ruleset diff --git a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs index e5ff1fbc..e7a31aaa 100644 --- a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs +++ b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs @@ -25,6 +25,18 @@ namespace FirebaseAdmin /// public sealed class AppOptions { + /// + /// Initializes a new instance of the class. + /// + public AppOptions() {} + + internal AppOptions(AppOptions options) + { + Credential = options.Credential; + ProjectId = options.ProjectId; + ServiceAccountId = options.ServiceAccountId; + } + /// /// used to authorize an app. All service calls made by /// the app will be authorized using this. @@ -44,17 +56,5 @@ public sealed class AppOptions /// 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 46426122..756fa246 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -26,20 +26,56 @@ namespace FirebaseAdmin.Auth public sealed class FirebaseAuth: IFirebaseService { private readonly FirebaseApp _app; - private bool _deleted; private readonly Lazy _tokenFactory; private readonly Lazy _idTokenVerifier; - private readonly Object _lock = new Object(); + private readonly object _lock = new object(); + private bool _deleted; private FirebaseAuth(FirebaseApp app) { _app = app; - _tokenFactory = new Lazy(() => + _tokenFactory = new Lazy(() => FirebaseTokenFactory.Create(_app), true); - _idTokenVerifier = new Lazy(() => + _idTokenVerifier = new Lazy(() => FirebaseTokenVerifier.CreateIDTokenVerifier(_app), true); } + /// + /// 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); + }); + } + /// /// Creates a Firebase custom token for the given user ID. This token can then be sent /// back to a client application to be used with the @@ -247,41 +283,5 @@ void IFirebaseService.Delete() } } } - - /// - /// 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); - }); - } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs index e9c0c7e3..2e8487b6 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs @@ -23,6 +23,17 @@ namespace FirebaseAdmin.Auth /// public sealed class FirebaseToken { + 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; + } + /// /// The issuer claim that identifies the principal that issued the JWT. /// @@ -48,7 +59,7 @@ public sealed class FirebaseToken /// 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. /// @@ -59,17 +70,6 @@ public sealed class FirebaseToken /// 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 diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs index 79e3b4ab..f6f6b6ac 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs @@ -17,7 +17,7 @@ using System.Collections.Immutable; using System.Text; using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; using System.Runtime.CompilerServices; using Google.Apis.Auth; using Google.Apis.Http; @@ -39,14 +39,28 @@ 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; @@ -57,6 +71,31 @@ public FirebaseTokenFactory(ISigner signer, IClock clock) _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( string uid, IDictionary developerClaims = null, @@ -85,9 +124,9 @@ 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 payload = new CustomTokenPayload() @@ -97,7 +136,7 @@ public async Task CreateCustomTokenAsync( Subject = keyId, Audience = FirebaseAudience, IssuedAtTimeSeconds = issued, - ExpirationTimeSeconds = issued + TokenDurationSeconds, + ExpirationTimeSeconds = issued + TokenDurationSeconds, }; if (developerClaims != null && developerClaims.Count > 0) { @@ -111,31 +150,6 @@ public void Dispose() { _signer.Dispose(); } - - 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); - } } internal class CustomTokenPayload: JsonWebToken.Payload diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs index e96b3349..be1704d8 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; @@ -43,7 +44,6 @@ 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; @@ -52,7 +52,7 @@ internal sealed class FirebaseTokenVerifier private readonly IClock _clock; private readonly IPublicKeySource _keySource; - public FirebaseTokenVerifier(FirebaseTokenVerifierArgs args) + internal FirebaseTokenVerifier(FirebaseTokenVerifierArgs args) { ProjectId = args.ProjectId.ThrowIfNullOrEmpty(nameof(args.ProjectId)); _shortName = args.ShortName.ThrowIfNullOrEmpty(nameof(args.ShortName)); @@ -71,7 +71,9 @@ public FirebaseTokenVerifier(FirebaseTokenVerifierArgs args) } } - public async Task VerifyTokenAsync( + public string ProjectId { get; } + + internal async Task VerifyTokenAsync( string token, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(token)) @@ -94,7 +96,7 @@ public async Task VerifyTokenAsync( 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 " + "token."; @@ -140,7 +142,7 @@ public async Task VerifyTokenAsync( { error = $"Firebase {_shortName} has a subject claim longer than 128 characters."; } - + if (error != null) { throw new FirebaseException(error); @@ -162,6 +164,7 @@ 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.")] private async Task VerifySignatureAsync( string[] segments, string keyId, CancellationToken cancellationToken) { @@ -179,7 +182,7 @@ private async Task VerifySignatureAsync( 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 @@ -218,11 +221,17 @@ internal static FirebaseTokenVerifier CreateIDTokenVerifier(FirebaseApp app) 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/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs index ffbbd9ff..ec9269ca 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs @@ -48,12 +48,12 @@ 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 string _certUrl; private readonly IClock _clock; private readonly HttpClientFactory _clientFactory; + private DateTime _expirationTime; + private IReadOnlyList _cachedKeys; public HttpPublicKeySource(string certUrl, IClock clock, HttpClientFactory clientFactory) { diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs index dec55cd4..6bc627b1 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs @@ -36,7 +36,8 @@ 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; @@ -86,11 +87,14 @@ private void ThrowIfError(HttpResponseMessage response, string content) var result = NewtonsoftJsonSerializer.Instance.Deserialize(content); error = result?.Error.Message; } - catch (Exception) {} // Ignore any errors encountered while parsing the originl error. + 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})" + + $"{(int)response.StatusCode} ({response.StatusCode})" + $"{Environment.NewLine}{content}"; } throw new FirebaseException(error); @@ -175,8 +179,9 @@ internal sealed class FixedAccountIAMSigner : IAMSigner { private readonly string _keyId; - public FixedAccountIAMSigner(HttpClientFactory clientFactory, GoogleCredential credential, - string keyId): base(clientFactory, credential) + public FixedAccountIAMSigner( + HttpClientFactory clientFactory, GoogleCredential credential, string keyId) + : base(clientFactory, credential) { _keyId = keyId.ThrowIfNullOrEmpty(nameof(keyId)); } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs index 2d63798e..dcacb8ee 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ISigner.cs @@ -22,7 +22,7 @@ namespace FirebaseAdmin.Auth /// Represents an object can be used to cryptographically sign data. Mainly used for signing /// custom JWT tokens issued to Firebase users. /// - internal interface ISigner: IDisposable + internal interface ISigner : IDisposable { /// /// Returns the ID (client email) of the service account used to sign payloads. diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs b/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs index 7fb5ab74..8fc8f9ec 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs @@ -50,13 +50,13 @@ private static string UrlSafeBase64Encode(byte[] bytes) return base64Value.TrimEnd('=').Replace('+', '-').Replace('/', '_'); } - public static string Base64Decode(string input) + internal static string Base64Decode(string input) { var raw = Base64DecodeToBytes(input); return Encoding.UTF8.GetString(raw); } - public static byte[] Base64DecodeToBytes(string input) + internal static byte[] Base64DecodeToBytes(string input) { // undo the url safe replacements input = input.Replace('-', '+').Replace('_', '/'); @@ -68,8 +68,10 @@ public static byte[] Base64DecodeToBytes(string input) return Convert.FromBase64String(input); } - public static async Task CreateSignedJwtAsync( - object header, object payload, ISigner signer, + internal static async Task CreateSignedJwtAsync( + object header, + object payload, + ISigner signer, CancellationToken cancellationToken = default(CancellationToken)) { string encodedHeader = Encode(header); diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs b/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs index e1779633..a29f05a4 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs @@ -27,6 +27,12 @@ namespace FirebaseAdmin.Auth /// internal sealed class PublicKey { + public PublicKey(string keyId, RSAKey rsa) + { + Id = keyId; + RSA = rsa; + } + /// /// The unique identifier of this key. /// @@ -37,11 +43,5 @@ internal sealed class PublicKey /// the public key. /// public RSAKey RSA { get; } - - public PublicKey(string keyId, RSAKey rsa) - { - Id = keyId; - RSA = rsa; - } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs index adfc064d..ddd2614b 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs @@ -23,7 +23,7 @@ namespace FirebaseAdmin.Auth /// An implementation that uses service account credentials to sign /// data. Uses the private key present in the credential to produce signatures. /// - internal sealed class ServiceAccountSigner: ISigner + internal sealed class ServiceAccountSigner : ISigner { private readonly ServiceAccountCredential _credential; @@ -48,6 +48,6 @@ public ServiceAccountSigner(ServiceAccountCredential credential) return Task.FromResult(Convert.FromBase64String(signature)); } - public void Dispose() {} + public void Dispose() { } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index db5a2a02..b8a1d960 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -38,7 +38,7 @@ public static ServiceAccountCredential ToServiceAccountCredential( { if (credential.UnderlyingCredential is GoogleCredential) { - return ((GoogleCredential) credential.UnderlyingCredential) + return ((GoogleCredential)credential.UnderlyingCredential) .ToServiceAccountCredential(); } return credential.UnderlyingCredential as ServiceAccountCredential; @@ -47,7 +47,7 @@ public static ServiceAccountCredential ToServiceAccountCredential( /// /// Creates a default (unauthenticated) from the /// factory. - /// + /// public static ConfigurableHttpClient CreateDefaultHttpClient( this HttpClientFactory clientFactory) { @@ -83,7 +83,8 @@ public static async Task PostJsonAsync( /// public static long UnixTimestamp(this IClock clock) { - return (long) (clock.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds; + var timeSinceEpoch = clock.UtcNow.Subtract(new DateTime(1970, 1, 1)); + return (long)timeSinceEpoch.TotalSeconds; } } } diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj b/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj index b4af550e..c285ce46 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj @@ -21,11 +21,13 @@ https://github.com/Firebase/firebase-admin-dotnet git https://github.com/Firebase/firebase-admin-dotnet + ../../stylecop.ruleset + diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs index 0c06aed3..aeafabe3 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs @@ -27,9 +27,10 @@ "3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597"+ "4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f"+ "badddb9c")] -namespace FirebaseAdmin +namespace FirebaseAdmin { - internal delegate TResult ServiceFactory() where TResult: IFirebaseService; + internal delegate TResult ServiceFactory() + where TResult : IFirebaseService; /// /// This is the entry point to the Firebase Admin SDK. It holds configuration and state common @@ -43,52 +44,26 @@ public sealed class FirebaseApp private const string DefaultAppName = "[DEFAULT]"; internal static readonly IReadOnlyList DefaultScopes = ImmutableList.Create( - // Enables access to Firebase Realtime Database. - "https://www.googleapis.com/auth/firebase", - - // Enables access to the email address associated with a project. - "https://www.googleapis.com/auth/userinfo.email", - - // Enables access to Google Identity Toolkit (for user management APIs). - "https://www.googleapis.com/auth/identitytoolkit", - - // Enables access to Google Cloud Storage. - "https://www.googleapis.com/auth/devstorage.full_control", - - // Enables access to Google Cloud Firestore - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/datastore" - ); + "https://www.googleapis.com/auth/firebase", // RTDB. + "https://www.googleapis.com/auth/userinfo.email", // RTDB + "https://www.googleapis.com/auth/identitytoolkit", // User management + "https://www.googleapis.com/auth/devstorage.full_control", // Cloud Storage + "https://www.googleapis.com/auth/cloud-platform", // Cloud Firestore + "https://www.googleapis.com/auth/datastore"); private static readonly Dictionary Apps = new Dictionary(); private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); // Guards the mutable state local to an app instance. - private readonly Object _lock = new Object(); - private bool _deleted = false; + private readonly object _lock = new object(); private readonly AppOptions _options; - /// - /// A copy of the this app was created with. - /// - public AppOptions Options - { - get - { - return new AppOptions(_options); - } - } - - /// - /// Name of this app. - /// - public string Name { get; } - // A collection of stateful services initialized using this app instance (e.g. // FirebaseAuth). Services are tracked here so they can be cleaned up when the app is // deleted. private readonly Dictionary _services = new Dictionary(); + private bool _deleted = false; private FirebaseApp(AppOptions options, string name) { @@ -104,6 +79,34 @@ private FirebaseApp(AppOptions options, string name) Name = name; } + /// + /// The default app instance. This property is null if the default app instance + /// doesn't yet exist. + /// + public static FirebaseApp DefaultInstance + { + get + { + return GetInstance(DefaultAppName); + } + } + + /// + /// A copy of the this app was created with. + /// + public AppOptions Options + { + get + { + return new AppOptions(_options); + } + } + + /// + /// Name of this app. + /// + public string Name { get; } + /// /// Deletes this app instance and cleans up any state associated with it. Once an app has /// been deleted, accessing any services related to it will result in an exception. @@ -135,7 +138,8 @@ public void Delete() } } - internal T GetOrInit(string id, ServiceFactory initializer) where T : class, IFirebaseService + internal T GetOrInit(string id, ServiceFactory initializer) + where T : class, IFirebaseService { lock (_lock) { @@ -147,12 +151,20 @@ internal T GetOrInit(string id, ServiceFactory initializer) where T : clas if (!_services.TryGetValue(id, out service)) { service = initializer(); - _services.Add(id, service); + _services.Add(id, service); } - return (T) service; + return (T)service; } } + /// + /// Returns the Google Cloud Platform project ID associated with this Firebase app. If a + /// project ID is specified in , that value is returned. If not + /// attempts to determine a project ID from the used to + /// initialize the app. Looks up the GOOGLE_CLOUD_PROJECT environment variable when all + /// else fails. + /// + /// A project ID string or null. internal string GetProjectId() { if (!string.IsNullOrEmpty(Options.ProjectId)) @@ -160,14 +172,14 @@ internal string GetProjectId() return Options.ProjectId; } var projectId = Options.Credential.ToServiceAccountCredential()?.ProjectId; - if (!String.IsNullOrEmpty(projectId)) + if (!string.IsNullOrEmpty(projectId)) { return projectId; } - foreach (var variableName in new [] {"GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"}) + foreach (var variableName in new[] { "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT" }) { projectId = Environment.GetEnvironmentVariable(variableName); - if (!String.IsNullOrEmpty(projectId)) + if (!string.IsNullOrEmpty(projectId)) { return projectId; } @@ -235,7 +247,7 @@ public static FirebaseApp Create(AppOptions options, string name) { throw new ArgumentException("The default FirebaseApp already exists."); } - else + else { throw new ArgumentException($"FirebaseApp named {name} already exists."); } @@ -254,18 +266,6 @@ private static AppOptions GetOptionsFromEnvironment() }; } - /// - /// The default app instance. This property is null if the default app instance - /// doesn't yet exist. - /// - public static FirebaseApp DefaultInstance - { - get - { - return GetInstance(DefaultAppName); - } - } - /// /// Returns the app instance identified by the given name. /// @@ -279,13 +279,13 @@ public static FirebaseApp GetInstance(string name) { throw new ArgumentException("App name to lookup must not be null or empty"); } - lock (Apps) + lock (Apps) { FirebaseApp app; if (Apps.TryGetValue(name, out app)) { return app; - } + } } return null; } @@ -306,7 +306,7 @@ internal static void DeleteAll() { throw new InvalidOperationException("Failed to delete all apps"); } - } + } } } } diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseException.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseException.cs index a2032d0a..e2a52733 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseException.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseException.cs @@ -19,10 +19,12 @@ namespace FirebaseAdmin /// /// Common error type for all exceptions raised by Firebase APIs. /// - public sealed class FirebaseException: Exception + public sealed class FirebaseException : Exception { - internal FirebaseException(string message): base(message) {} - - internal FirebaseException(string message, Exception inner): base(message, inner) {} + internal FirebaseException(string message) + : base(message) { } + + internal FirebaseException(string message, Exception inner) + : base(message, inner) { } } } diff --git a/stylecop.ruleset b/stylecop.ruleset new file mode 100644 index 00000000..b1c65323 --- /dev/null +++ b/stylecop.ruleset @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From ed910d525b13b13532d9374c6bf079f953c19456 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 00:56:39 -0800 Subject: [PATCH 35/50] Added stylecop to tests --- .../Auth/FirebaseAuthTest.cs | 24 ++-- .../Auth/FirebaseTokenFactoryTest.cs | 15 +-- .../Auth/FirebaseTokenVerifierTest.cs | 37 +++--- .../Auth/HttpPublicKeySourceTest.cs | 2 +- .../FirebaseAdmin.Tests/Auth/IAMSignerTest.cs | 106 +++++++++--------- .../Auth/ServiceAccountSignerTest.cs | 4 +- .../FirebaseAdmin.Tests.csproj | 6 + .../FirebaseAdmin.Tests/FirebaseAppTest.cs | 19 ++-- .../FirebaseAdmin.Tests/MockClock.cs | 17 +-- .../FirebaseAdmin.Tests/MockMessageHandler.cs | 97 ++++++++-------- .../FirebaseAdmin/FirebaseAdmin.csproj | 4 +- stylecop_test.ruleset | 31 +++++ 12 files changed, 204 insertions(+), 158 deletions(-) create mode 100644 stylecop_test.ruleset diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs index 97a4c1f1..40983e00 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs @@ -30,7 +30,7 @@ namespace FirebaseAdmin.Auth.Tests { public class FirebaseAuthTest: IDisposable { - private static readonly GoogleCredential mockCredential = + private static readonly GoogleCredential MockCredential = GoogleCredential.FromAccessToken("test-token"); [Fact] @@ -42,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(); @@ -52,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(); @@ -62,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( @@ -75,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); } @@ -84,7 +84,7 @@ 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}, @@ -100,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( @@ -111,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")); } @@ -119,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)); @@ -130,7 +130,7 @@ public async Task VerifyIdTokenCancel() { FirebaseApp.Create(new AppOptions() { - Credential = mockCredential, + Credential = MockCredential, ProjectId = "test-project", }); var canceller = new CancellationTokenSource(); @@ -143,7 +143,7 @@ await Assert.ThrowsAnyAsync( 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]); @@ -166,7 +166,7 @@ private static void VerifyCustomToken(string token, string uid, Dictionary( 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(){ + 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]); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs index f8bfea99..b1c55f79 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs @@ -32,8 +32,11 @@ 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] @@ -63,8 +66,8 @@ public async Task ValidToken() 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 +80,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 +95,7 @@ public async Task NoKid() { var header = new Dictionary() { - {"kid", ""}, + {"kid", string.Empty}, }; var idToken = await CreateTestTokenAsync(headerOverrides: header); await Assert.ThrowsAsync( @@ -176,7 +179,7 @@ public async Task EmptySubject() { var payload = new Dictionary() { - {"sub", ""}, + {"sub", string.Empty}, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -188,7 +191,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 +203,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 +229,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); } } @@ -267,11 +270,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) { @@ -286,7 +289,7 @@ internal static async Task CreateTestTokenAsync( 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); } } @@ -298,7 +301,7 @@ internal class FileSystemPublicKeySource : IPublicKeySource public FileSystemPublicKeySource(string file) { var x509cert = new X509Certificate2(File.ReadAllBytes(file)); - var rsa = (RSA) x509cert.PublicKey.Key; + var rsa = (RSA)x509cert.PublicKey.Key; _rsa = ImmutableList.Create(new PublicKey("test-key-id", rsa)); } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs index e7563010..1a53a4a0 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs @@ -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..f454953b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs @@ -27,6 +27,57 @@ 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 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] @@ -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..8b0811b8 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs @@ -30,7 +30,7 @@ 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()); @@ -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..b936e804 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj @@ -5,6 +5,9 @@ false ../../FirebaseAdmin.snk true + true + true + ../../stylecop_test.ruleset @@ -17,6 +20,9 @@ + + all + diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs index cc525acd..9268a6ee 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs @@ -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,7 +212,7 @@ public void GetOrInitService() public void Dispose() { - FirebaseApp.DeleteAll(); + FirebaseApp.DeleteAll(); } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs index d4fb09e6..c68cf24e 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs @@ -19,15 +19,20 @@ namespace FirebaseAdmin.Tests { public class MockClock : IClock { + private object _lock = new object(); + private DateTime _utcNow; + + public MockClock() + { + Now = DateTime.Now; + } + public DateTime Now { get { return UtcNow.ToLocalTime(); } set { UtcNow = value.ToUniversalTime(); } } - private object _lock = new object(); - private DateTime _utcNow; - public DateTime UtcNow { get @@ -37,6 +42,7 @@ public DateTime UtcNow return _utcNow; } } + set { lock (_lock) @@ -45,10 +51,5 @@ public DateTime UtcNow } } } - - public MockClock() - { - Now = DateTime.Now; - } } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs index 7bc4e209..1feff6f7 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs @@ -26,65 +26,27 @@ 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); + 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) { @@ -95,7 +57,7 @@ protected override async Task SendAsyncCore( else { Request = null; - } + } var resp = new HttpResponseMessage(); string json; if (Response is byte[]) @@ -109,8 +71,8 @@ protected override async Task SendAsyncCore( else { json = NewtonsoftJsonSerializer.Instance.Serialize(Response); - } - resp.StatusCode = StatusCode; + } + resp.StatusCode = StatusCode; if (ApplyHeaders != null) { ApplyHeaders(resp.Headers); @@ -121,4 +83,43 @@ protected override async Task SendAsyncCore( 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 _calls; } + } + + protected sealed override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) + { + Interlocked.Increment(ref _calls); + return 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/FirebaseAdmin.csproj b/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj index c285ce46..117c7603 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj @@ -27,7 +27,9 @@ - + + all + diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset new file mode 100644 index 00000000..6f7fe1ed --- /dev/null +++ b/stylecop_test.ruleset @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From fccf06ccd16250845429b5567a36c09a487ed5dc Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 14:49:04 -0800 Subject: [PATCH 36/50] Fixing more lint errors --- .../FirebaseAdmin.IntegrationTests.csproj | 7 ++++- .../FirebaseAuthTest.cs | 21 ++++++++------- .../IntegrationTestUtils.cs | 3 ++- .../Auth/FirebaseAuthTest.cs | 8 +++--- .../Auth/FirebaseTokenFactoryTest.cs | 10 +++---- .../Auth/FirebaseTokenVerifierTest.cs | 26 +++++++++---------- .../FirebaseAdmin.Tests.csproj | 6 ++--- .../FirebaseAdmin.Tests/FirebaseAppTest.cs | 10 +++---- FirebaseAdmin/FirebaseAdmin/AppOptions.cs | 2 +- .../FirebaseAdmin/Auth/FirebaseAuth.cs | 2 +- .../Auth/FirebaseTokenFactory.cs | 14 +++++----- .../Auth/FirebaseTokenVerifier.cs | 2 +- .../FirebaseAdmin/Auth/HttpPublicKeySource.cs | 2 +- FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs | 10 +++---- stylecop.ruleset | 4 --- stylecop_test.ruleset | 4 --- 16 files changed, 65 insertions(+), 66 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj index c428e7b4..3edd66b0 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj @@ -2,8 +2,10 @@ netcoreapp2.0 - false + true + true + ../../stylecop_test.ruleset @@ -12,6 +14,9 @@ + + all + diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs index 435f3f59..bf8a467e 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs @@ -27,14 +27,14 @@ 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,8 +72,8 @@ 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 serviceAcct = (ServiceAccountCredential)googleCred.UnderlyingCredential; + var token = await ((ITokenAccess)googleCred).GetAccessTokenForRequestAsync(); var app = FirebaseApp.Create(new AppOptions() { Credential = GoogleCredential.FromAccessToken(token), @@ -98,12 +98,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, }); @@ -131,6 +132,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/IntegrationTestUtils.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs index 5761de85..b2be8bff 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs @@ -26,7 +26,8 @@ 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(() => { + private static readonly Lazy DefaultFirebaseApp = new Lazy(() => + { var options = new AppOptions() { Credential = GoogleCredential.FromFile(ServiceAccountFile), diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs index 40983e00..4df8974e 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs @@ -28,7 +28,7 @@ [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace FirebaseAdmin.Auth.Tests { - public class FirebaseAuthTest: IDisposable + public class FirebaseAuthTest : IDisposable { private static readonly GoogleCredential MockCredential = GoogleCredential.FromAccessToken("test-token"); @@ -87,9 +87,9 @@ public async Task CreateCustomTokenWithClaims() 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); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs index 9503a925..8d247bbb 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs @@ -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); @@ -84,7 +84,7 @@ public async Task ReservedClaims() { var developerClaims = new Dictionary() { - {key, "value"}, + { key, "value" }, }; await Assert.ThrowsAsync( async () => await factory.CreateCustomTokenAsync("user", developerClaims)); @@ -142,6 +142,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 b1c55f79..310144a6 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs @@ -28,7 +28,7 @@ namespace FirebaseAdmin.Auth.Tests { - public class FirebaseTokenVerifierTest: IDisposable + public class FirebaseTokenVerifierTest : IDisposable { private static readonly IPublicKeySource KeySource = new FileSystemPublicKeySource( "./resources/public_cert.pem"); @@ -57,7 +57,7 @@ public async Task ValidToken() { var payload = new Dictionary() { - {"foo", "bar"}, + { "foo", "bar" }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); var decoded = await TokenVerifier.VerifyTokenAsync(idToken); @@ -95,7 +95,7 @@ public async Task NoKid() { var header = new Dictionary() { - {"kid", string.Empty}, + { "kid", string.Empty }, }; var idToken = await CreateTestTokenAsync(headerOverrides: header); await Assert.ThrowsAsync( @@ -107,7 +107,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( @@ -119,7 +119,7 @@ public async Task IncorrectAlgorithm() { var header = new Dictionary() { - {"alg", "HS256"}, + { "alg", "HS256" }, }; var idToken = await CreateTestTokenAsync(headerOverrides: header); await Assert.ThrowsAsync( @@ -131,7 +131,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( @@ -143,7 +143,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( @@ -155,7 +155,7 @@ public async Task InvalidIssuer() { var payload = new Dictionary() { - {"iss", "wrong-issuer"}, + { "iss", "wrong-issuer" }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -167,7 +167,7 @@ public async Task InvalidAudience() { var payload = new Dictionary() { - {"aud", "wrong-audience"}, + { "aud", "wrong-audience" }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -179,7 +179,7 @@ public async Task EmptySubject() { var payload = new Dictionary() { - {"sub", string.Empty}, + { "sub", string.Empty }, }; var idToken = await CreateTestTokenAsync(payloadOverrides: payload); await Assert.ThrowsAsync( @@ -256,9 +256,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) { diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj index b936e804..903194ec 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj @@ -16,13 +16,13 @@ + + all + - - all - diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs index 9268a6ee..65aa0353 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs @@ -20,7 +20,7 @@ namespace FirebaseAdmin.Tests { - public class FirebaseAppTest: IDisposable + public class FirebaseAppTest : IDisposable { private static readonly AppOptions TestOptions = new AppOptions() { @@ -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 @@ -216,7 +216,7 @@ public void Dispose() } } - internal class MockService: IFirebaseService + internal class MockService : IFirebaseService { public bool Deleted { get; private set; } @@ -226,8 +226,8 @@ public void Delete() } } - internal class OtherMockService: IFirebaseService + internal class OtherMockService : IFirebaseService { - public void Delete() {} + public void Delete() { } } } diff --git a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs index e7a31aaa..1a4bf78f 100644 --- a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs +++ b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs @@ -28,7 +28,7 @@ public sealed class AppOptions /// /// Initializes a new instance of the class. /// - public AppOptions() {} + public AppOptions() { } internal AppOptions(AppOptions options) { diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index 756fa246..19fe54e0 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -23,7 +23,7 @@ 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 readonly Lazy _tokenFactory; diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs index f6f6b6ac..e4d1a969 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs @@ -24,18 +24,18 @@ using Google.Apis.Auth.OAuth2; 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"; @@ -152,7 +152,7 @@ public void Dispose() } } - internal class CustomTokenPayload: JsonWebToken.Payload + internal class CustomTokenPayload : JsonWebToken.Payload { [Newtonsoft.Json.JsonPropertyAttribute("uid")] public string Uid { get; set; } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs index be1704d8..b1fc8d5d 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs @@ -35,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 diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs index ec9269ca..acb29cd8 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs @@ -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 diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs index aeafabe3..397ef4c5 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs @@ -21,11 +21,11 @@ using Google.Apis.Logging; using Google.Apis.Auth.OAuth2; -[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey="+ -"002400000480000094000000060200000024000052534131000400000100010081328559eaab41"+ -"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02"+ -"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597"+ -"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f"+ +[assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey=" + +"002400000480000094000000060200000024000052534131000400000100010081328559eaab41" + +"055b84af73469863499d81625dcbba8d8decb298b69e0f783a0958cf471fd4f76327b85a7d4b02" + +"3003684e85e61cf15f13150008c81f0b75a252673028e530ea95d0c581378da8c6846526ab9597" + +"4c6d0bc66d2462b51af69968a0e25114bde8811e0d6ee1dc22d4a59eee6a8bba4712cba839652f" + "badddb9c")] namespace FirebaseAdmin { diff --git a/stylecop.ruleset b/stylecop.ruleset index b1c65323..5f139441 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -4,10 +4,6 @@ - - - - diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset index 6f7fe1ed..e78a40d5 100644 --- a/stylecop_test.ruleset +++ b/stylecop_test.ruleset @@ -4,10 +4,6 @@ - - - - From 03607a5ff51222a12c63fb99bd04d7a5c090dc07 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 15:36:15 -0800 Subject: [PATCH 37/50] Fixed more lint errors --- .../FirebaseAuthTest.cs | 13 +- .../IntegrationTestUtils.cs | 15 +- .../Auth/FirebaseAuthTest.cs | 12 +- .../Auth/FirebaseTokenFactoryTest.cs | 4 +- .../Auth/FirebaseTokenVerifierTest.cs | 12 +- .../Auth/HttpPublicKeySourceTest.cs | 4 +- .../FirebaseAdmin.Tests/Auth/IAMSignerTest.cs | 2 +- .../Auth/ServiceAccountSignerTest.cs | 6 +- .../FirebaseAdmin.Tests/FirebaseAppTest.cs | 2 +- .../FirebaseAdmin.Tests/MockClock.cs | 12 +- .../FirebaseAdmin.Tests/MockMessageHandler.cs | 6 +- .../FirebaseAdmin/Auth/FirebaseAuth.cs | 40 ++-- .../Auth/FirebaseTokenFactory.cs | 20 +- .../Auth/FirebaseTokenVerifier.cs | 127 +++++------ .../FirebaseAdmin/Auth/HttpPublicKeySource.cs | 42 ++-- FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs | 72 +++---- FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs | 24 +-- .../Auth/ServiceAccountSigner.cs | 8 +- FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs | 202 +++++++++--------- stylecop.ruleset | 11 +- stylecop_test.ruleset | 7 +- 21 files changed, 320 insertions(+), 321 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs index bf8a467e..d572b5f8 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAuthTest.cs @@ -17,11 +17,11 @@ 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 { @@ -74,11 +74,12 @@ 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 app = FirebaseApp.Create( + new AppOptions() + { + Credential = GoogleCredential.FromAccessToken(token), + ServiceAccountId = serviceAcct.Id, + }, "IAMSignApp"); try { var customToken = await FirebaseAuth.GetAuth(app).CreateCustomTokenAsync( diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs index b2be8bff..bee0adc0 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/IntegrationTestUtils.cs @@ -26,14 +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.Tests/Auth/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs index 4df8974e..2740a250 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs @@ -20,10 +20,10 @@ 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 @@ -141,6 +141,11 @@ await Assert.ThrowsAnyAsync( idToken, canceller.Token)); } + public void Dispose() + { + FirebaseApp.DeleteAll(); + } + private static void VerifyCustomToken(string token, string uid, Dictionary claims) { string[] segments = token.Split("."); @@ -172,10 +177,5 @@ private static void VerifyCustomToken(string token, string uid, Dictionary _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)); + 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/HttpPublicKeySourceTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/HttpPublicKeySourceTest.cs index 1a53a4a0..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 { diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs index f454953b..ea72c041 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs @@ -19,11 +19,11 @@ 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 { diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs index 8b0811b8..ab51c2a1 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 { @@ -32,8 +32,8 @@ public async Task Signer() var credential = GoogleCredential.FromFile("./resources/service_account.json"); 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)); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs index 65aa0353..1b7307a9 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs @@ -14,9 +14,9 @@ using System; using System.Threading.Tasks; -using Xunit; using FirebaseAdmin; using Google.Apis.Auth.OAuth2; +using Xunit; namespace FirebaseAdmin.Tests { diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs index c68cf24e..3d26ca34 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs @@ -19,8 +19,8 @@ namespace FirebaseAdmin.Tests { public class MockClock : IClock { - private object _lock = new object(); - private DateTime _utcNow; + private object mutex = new object(); + private DateTime utcNow; public MockClock() { @@ -37,17 +37,17 @@ 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; } } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs index 1feff6f7..ebd34534 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs @@ -90,17 +90,17 @@ protected override async Task SendAsyncCore( /// internal abstract class CountableMessageHandler : HttpMessageHandler { - private int _calls; + private int calls; public int Calls { - get { return _calls; } + get { return this.calls; } } protected sealed override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { - Interlocked.Increment(ref _calls); + Interlocked.Increment(ref this.calls); return SendAsyncCore(request, cancellationToken); } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index 19fe54e0..275aae3f 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -25,19 +25,19 @@ namespace FirebaseAdmin.Auth /// public sealed class FirebaseAuth : IFirebaseService { - private readonly FirebaseApp _app; - private readonly Lazy _tokenFactory; - private readonly Lazy _idTokenVerifier; - private readonly object _lock = new object(); - private bool _deleted; + private readonly FirebaseApp app; + private readonly Lazy tokenFactory; + private readonly Lazy idTokenVerifier; + 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); + this.app = app; + this.tokenFactory = new Lazy( + () => FirebaseTokenFactory.Create(this.app), true); + this.idTokenVerifier = new Lazy( + () => FirebaseTokenVerifier.CreateIDTokenVerifier(this.app), true); } /// @@ -207,13 +207,13 @@ 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); @@ -261,25 +261,25 @@ 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); } 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(); } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs index e4d1a969..4c0c1e43 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs @@ -15,13 +15,13 @@ 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 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=" + @@ -62,13 +62,13 @@ internal class FirebaseTokenFactory : IDisposable "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) @@ -127,8 +127,8 @@ public async Task CreateCustomTokenAsync( 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, @@ -143,12 +143,12 @@ public async Task CreateCustomTokenAsync( 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(); + signer.Dispose(); } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs index b1fc8d5d..4bf18cf1 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs @@ -44,103 +44,126 @@ internal sealed class FirebaseTokenVerifier private static readonly IReadOnlyList StandardClaims = ImmutableList.Create("iss", "aud", "exp", "iat", "sub", "uid"); - 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; + 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.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 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 + ProjectId; string error = null; if (string.IsNullOrEmpty(header.KeyId)) { 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) { - error = $"{_shortName} has incorrect audience (aud) claim. Expected {ProjectId} " + error = $"{this.shortName} has incorrect audience (aud) claim. Expected {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) @@ -164,7 +187,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", + "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) { @@ -175,7 +205,7 @@ private async Task VerifySignatureAsync( 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 @@ -190,32 +220,9 @@ 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 diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs index acb29cd8..1efeb1c7 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 @@ -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 SemaphoreSlim _lock = new SemaphoreSlim(1, 1); - private readonly string _certUrl; - private readonly IClock _clock; - private readonly HttpClientFactory _clientFactory; - private DateTime _expirationTime; - private IReadOnlyList _cachedKeys; + 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 (cachedKeys == null || clock.UtcNow >= expirationTime) { - await _lock.WaitAsync(cancellationToken).ConfigureAwait(false); + await cacheLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { - var now = _clock.UtcNow; - if (_cachedKeys == null || now >= _expirationTime) + var now = clock.UtcNow; + if (cachedKeys == null || now >= expirationTime) { - using (var httpClient = _clientFactory.CreateDefaultHttpClient()) + using (var httpClient = clientFactory.CreateDefaultHttpClient()) { - var response = await httpClient.GetAsync(_certUrl, cancellationToken) + var response = await httpClient.GetAsync(certUrl, cancellationToken) .ConfigureAwait(false); response.EnsureSuccessStatusCode(); - _cachedKeys = ParseKeys(await response.Content.ReadAsStringAsync() + cachedKeys = ParseKeys(await response.Content.ReadAsStringAsync() .ConfigureAwait(false)); var cacheControl = response.Headers.CacheControl; if (cacheControl?.MaxAge != null) { - _expirationTime = now.Add(cacheControl.MaxAge.Value) + expirationTime = now.Add(cacheControl.MaxAge.Value) .Subtract(ClockSkew); } } @@ -97,11 +97,11 @@ public async Task> GetPublicKeysAsync( } finally { - _lock.Release(); + cacheLock.Release(); } } - return _cachedKeys; + return cachedKeys; } private IReadOnlyList ParseKeys(string json) diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs index 6bc627b1..2d8b283c 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs @@ -40,13 +40,13 @@ internal class IAMSigner : ISigner 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); } @@ -62,7 +62,7 @@ public async Task SignDataAsync( }; try { - var response = await _httpClient.PostJsonAsync(url, request, cancellationToken) + var response = await httpClient.PostJsonAsync(url, request, cancellationToken) .ConfigureAwait(false); var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); ThrowIfError(response, json); @@ -75,37 +75,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 keyId.Value.ConfigureAwait(false); } catch (Exception e) { @@ -118,6 +93,11 @@ public virtual async Task GetKeyIdAsync( } } + public void Dispose() + { + httpClient.Dispose(); + } + private static async Task DiscoverServiceAccountIdAsync( HttpClientFactory clientFactory) { @@ -128,9 +108,29 @@ private static async Task DiscoverServiceAccountIdAsync( } } - public void Dispose() + private void ThrowIfError(HttpResponseMessage response, string content) { - _httpClient.Dispose(); + 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); } } @@ -177,19 +177,19 @@ internal class SignBlobErrorDetail /// internal sealed class FixedAccountIAMSigner : IAMSigner { - private readonly string _keyId; + private readonly string keyId; public FixedAccountIAMSigner( HttpClientFactory clientFactory, GoogleCredential credential, string keyId) : base(clientFactory, credential) { - _keyId = keyId.ThrowIfNullOrEmpty(nameof(keyId)); + this.keyId = keyId.ThrowIfNullOrEmpty(nameof(keyId)); } public override Task GetKeyIdAsync( CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(_keyId); + return Task.FromResult(this.keyId); } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs b/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs index 8fc8f9ec..aabd62b0 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs @@ -25,12 +25,6 @@ namespace FirebaseAdmin.Auth /// internal static class JwtUtils { - private static string Encode(object obj) - { - var json = NewtonsoftJsonSerializer.Instance.Serialize(obj); - return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(json)); - } - /// /// Decodes a single JWT segment, and deserializes it into a value of type /// . @@ -44,12 +38,6 @@ public static T Decode(string value) return NewtonsoftJsonSerializer.Instance.Deserialize(json); } - private static string UrlSafeBase64Encode(byte[] bytes) - { - var base64Value = Convert.ToBase64String(bytes); - return base64Value.TrimEnd('=').Replace('+', '-').Replace('/', '_'); - } - internal static string Base64Decode(string input) { var raw = Base64DecodeToBytes(input); @@ -87,5 +75,17 @@ internal static async Task CreateSignedJwtAsync( assertion.Append('.').Append(UrlSafeBase64Encode(signature)); return assertion.ToString(); } + + private static string Encode(object obj) + { + var json = NewtonsoftJsonSerializer.Instance.Serialize(obj); + return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(json)); + } + + private static string UrlSafeBase64Encode(byte[] bytes) + { + var base64Value = Convert.ToBase64String(bytes); + return base64Value.TrimEnd('=').Replace('+', '-').Replace('/', '_'); + } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs index ddd2614b..f47dc0ac 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs @@ -25,7 +25,7 @@ namespace FirebaseAdmin.Auth /// internal sealed class ServiceAccountSigner : ISigner { - private readonly ServiceAccountCredential _credential; + private readonly ServiceAccountCredential credential; public ServiceAccountSigner(ServiceAccountCredential credential) { @@ -33,18 +33,18 @@ public ServiceAccountSigner(ServiceAccountCredential credential) { throw new ArgumentNullException("Credential must not be null."); } - _credential = credential; + this.credential = credential; } public Task GetKeyIdAsync(CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(_credential.Id); + return Task.FromResult(credential.Id); } public Task SignDataAsync(byte[] data, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); - var signature = _credential.CreateSignature(data); + var signature = credential.CreateSignature(data); return Task.FromResult(Convert.FromBase64String(signature)); } diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs index 397ef4c5..cee07c13 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs @@ -18,8 +18,8 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using Google; -using Google.Apis.Logging; using Google.Apis.Auth.OAuth2; +using Google.Apis.Logging; [assembly: InternalsVisibleToAttribute("FirebaseAdmin.Tests,PublicKey=" + "002400000480000094000000060200000024000052534131000400000100010081328559eaab41" + @@ -41,8 +41,6 @@ internal delegate TResult ServiceFactory() /// public sealed class FirebaseApp { - private const string DefaultAppName = "[DEFAULT]"; - internal static readonly IReadOnlyList DefaultScopes = ImmutableList.Create( "https://www.googleapis.com/auth/firebase", // RTDB. "https://www.googleapis.com/auth/userinfo.email", // RTDB @@ -51,30 +49,32 @@ public sealed class FirebaseApp "https://www.googleapis.com/auth/cloud-platform", // Cloud Firestore "https://www.googleapis.com/auth/datastore"); + private const string DefaultAppName = "[DEFAULT]"; + private static readonly Dictionary Apps = new Dictionary(); private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); // Guards the mutable state local to an app instance. - private readonly object _lock = new object(); - private readonly AppOptions _options; + private readonly object appLock = new object(); + private readonly AppOptions options; // A collection of stateful services initialized using this app instance (e.g. // FirebaseAuth). Services are tracked here so they can be cleaned up when the app is // deleted. - private readonly Dictionary _services = new Dictionary(); - private bool _deleted = false; + private readonly Dictionary services = new Dictionary(); + private bool deleted = false; private FirebaseApp(AppOptions options, string name) { - _options = new AppOptions(options); - if (_options.Credential == null) + this.options = new AppOptions(options); + if (options.Credential == null) { throw new ArgumentNullException("Credential must be set"); } - if (_options.Credential.IsCreateScopedRequired) + if (options.Credential.IsCreateScopedRequired) { - _options.Credential = _options.Credential.CreateScoped(DefaultScopes); + options.Credential = options.Credential.CreateScoped(DefaultScopes); } Name = name; } @@ -98,7 +98,7 @@ public AppOptions Options { get { - return new AppOptions(_options); + return new AppOptions(this.options); } } @@ -108,80 +108,24 @@ public AppOptions Options public string Name { get; } /// - /// Deletes this app instance and cleans up any state associated with it. Once an app has - /// been deleted, accessing any services related to it will result in an exception. - /// If the app is already deleted, this method is a no-op. + /// Returns the app instance identified by the given name. /// - public void Delete() + /// The instance with the specified name or null if it + /// doesn't exist. + /// If the name argument is null or empty. + /// Name of the app to retrieve. + public static FirebaseApp GetInstance(string name) { - // Clean up local state - lock (_lock) + if (string.IsNullOrEmpty(name)) { - _deleted = true; - foreach (var entry in _services) - { - try - { - entry.Value.Delete(); - } - catch (Exception e) - { - Logger.Error(e, "Error while cleaning up service {0}", entry.Key); - } - } - _services.Clear(); + throw new ArgumentException("App name to lookup must not be null or empty"); } - // Clean up global state lock (Apps) { - Apps.Remove(Name); - } - } - - internal T GetOrInit(string id, ServiceFactory initializer) - where T : class, IFirebaseService - { - lock (_lock) - { - if (_deleted) - { - throw new InvalidOperationException("Cannot use an app after it has been deleted"); - } - IFirebaseService service; - if (!_services.TryGetValue(id, out service)) - { - service = initializer(); - _services.Add(id, service); - } - return (T)service; - } - } - - /// - /// Returns the Google Cloud Platform project ID associated with this Firebase app. If a - /// project ID is specified in , that value is returned. If not - /// attempts to determine a project ID from the used to - /// initialize the app. Looks up the GOOGLE_CLOUD_PROJECT environment variable when all - /// else fails. - /// - /// A project ID string or null. - internal string GetProjectId() - { - if (!string.IsNullOrEmpty(Options.ProjectId)) - { - return Options.ProjectId; - } - var projectId = Options.Credential.ToServiceAccountCredential()?.ProjectId; - if (!string.IsNullOrEmpty(projectId)) - { - return projectId; - } - foreach (var variableName in new[] { "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT" }) - { - projectId = Environment.GetEnvironmentVariable(variableName); - if (!string.IsNullOrEmpty(projectId)) + FirebaseApp app; + if (Apps.TryGetValue(name, out app)) { - return projectId; + return app; } } return null; @@ -258,36 +202,35 @@ public static FirebaseApp Create(AppOptions options, string name) } } - private static AppOptions GetOptionsFromEnvironment() - { - return new AppOptions() - { - Credential = GoogleCredential.GetApplicationDefault(), - }; - } - /// - /// Returns the app instance identified by the given name. + /// Deletes this app instance and cleans up any state associated with it. Once an app has + /// been deleted, accessing any services related to it will result in an exception. + /// If the app is already deleted, this method is a no-op. /// - /// The instance with the specified name or null if it - /// doesn't exist. - /// If the name argument is null or empty. - /// Name of the app to retrieve. - public static FirebaseApp GetInstance(string name) + public void Delete() { - if (string.IsNullOrEmpty(name)) + // Clean up local state + lock (this.appLock) { - throw new ArgumentException("App name to lookup must not be null or empty"); + this.deleted = true; + foreach (var entry in this.services) + { + try + { + entry.Value.Delete(); + } + catch (Exception e) + { + Logger.Error(e, "Error while cleaning up service {0}", entry.Key); + } + } + this.services.Clear(); } + // Clean up global state lock (Apps) { - FirebaseApp app; - if (Apps.TryGetValue(name, out app)) - { - return app; - } + Apps.Remove(Name); } - return null; } /// @@ -308,5 +251,62 @@ internal static void DeleteAll() } } } + + internal T GetOrInit(string id, ServiceFactory initializer) + where T : class, IFirebaseService + { + lock (this.appLock) + { + if (this.deleted) + { + throw new InvalidOperationException("Cannot use an app after it has been deleted"); + } + IFirebaseService service; + if (!this.services.TryGetValue(id, out service)) + { + service = initializer(); + this.services.Add(id, service); + } + return (T)service; + } + } + + /// + /// Returns the Google Cloud Platform project ID associated with this Firebase app. If a + /// project ID is specified in , that value is returned. If not + /// attempts to determine a project ID from the used to + /// initialize the app. Looks up the GOOGLE_CLOUD_PROJECT environment variable when all + /// else fails. + /// + /// A project ID string or null. + internal string GetProjectId() + { + if (!string.IsNullOrEmpty(Options.ProjectId)) + { + return Options.ProjectId; + } + var projectId = Options.Credential.ToServiceAccountCredential()?.ProjectId; + if (!string.IsNullOrEmpty(projectId)) + { + return projectId; + } + foreach (var variableName in new[] { "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT" }) + { + projectId = Environment.GetEnvironmentVariable(variableName); + if (!string.IsNullOrEmpty(projectId)) + { + return projectId; + } + } + return null; + } + + private static AppOptions GetOptionsFromEnvironment() + { + return new AppOptions() + { + Credential = GoogleCredential.GetApplicationDefault(), + }; + } } } diff --git a/stylecop.ruleset b/stylecop.ruleset index 5f139441..ba9d2760 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -4,14 +4,9 @@ - - - - - - - - + + + diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset index e78a40d5..9adea184 100644 --- a/stylecop_test.ruleset +++ b/stylecop_test.ruleset @@ -4,14 +4,9 @@ - - - - - - + From d16f67045e990265c0e5ed99d1f281e8be91e450 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 16:03:55 -0800 Subject: [PATCH 38/50] Fixed more lint errors --- .../Auth/FirebaseAuthTest.cs | 2 +- .../Auth/FirebaseTokenFactoryTest.cs | 2 +- .../FirebaseAdmin.Tests/Auth/IAMSignerTest.cs | 8 +- .../FirebaseAdmin/Auth/FirebaseAuth.cs | 5 ++ .../FirebaseAdmin/Auth/FirebaseToken.cs | 21 ----- .../FirebaseAdmin/Auth/FirebaseTokenArgs.cs | 40 +++++++++ .../Auth/FirebaseTokenFactory.cs | 18 ++-- .../Auth/FirebaseTokenVerifier.cs | 22 ++--- .../Auth/FirebaseTokenVerifierArgs.cs | 35 ++++++++ .../Auth/FixedAccountIAMSigner.cs | 45 ++++++++++ .../FirebaseAdmin/Auth/HttpPublicKeySource.cs | 2 + FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs | 83 +++++++------------ FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs | 1 + .../Auth/ServiceAccountSigner.cs | 1 + FirebaseAdmin/FirebaseAdmin/Extensions.cs | 1 + FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs | 20 ++++- stylecop.ruleset | 4 +- stylecop_test.ruleset | 4 +- 18 files changed, 204 insertions(+), 110 deletions(-) create mode 100644 FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenArgs.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifierArgs.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs index 2740a250..9ff1d06f 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs @@ -151,7 +151,7 @@ private static void VerifyCustomToken(string token, string uid, Dictionary(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); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs index c66051ef..8bafe5bd 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs @@ -102,7 +102,7 @@ private static void VerifyCustomToken( 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); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs index ea72c041..3bd82579 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/IAMSignerTest.cs @@ -46,14 +46,14 @@ public async Task Signer() Assert.Equal("discovered-service-account", await signer.GetKeyIdAsync()); Assert.Equal(1, handler.Calls); - handler.Response = new SignBlobResponse() + 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( + var req = NewtonsoftJsonSerializer.Instance.Deserialize( handler.Request); Assert.Equal(Convert.ToBase64String(data), req.BytesToSign); Assert.Equal(2, handler.Calls); @@ -86,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), }, @@ -98,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); diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index 275aae3f..4d5080dc 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -53,6 +53,7 @@ public static FirebaseAuth DefaultInstance { return null; } + return GetAuth(app); } } @@ -70,6 +71,7 @@ public static FirebaseAuth GetAuth(FirebaseApp app) { throw new ArgumentNullException("App argument must not be null."); } + return app.GetOrInit(typeof(FirebaseAuth).Name, () => { return new FirebaseAuth(app); @@ -213,8 +215,10 @@ public async Task CreateCustomTokenAsync( { throw new InvalidOperationException("Cannot invoke after deleting the app."); } + tokenFactory = this.tokenFactory.Value; } + return await tokenFactory.CreateCustomTokenAsync( uid, developerClaims, cancellationToken).ConfigureAwait(false); } @@ -268,6 +272,7 @@ public async Task VerifyIdTokenAsync( throw new InvalidOperationException("Cannot invoke after deleting the app."); } } + return await this.idTokenVerifier.Value.VerifyTokenAsync(idToken, cancellationToken) .ConfigureAwait(false); } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs index 2e8487b6..c60d5339 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs @@ -71,25 +71,4 @@ internal FirebaseToken(FirebaseTokenArgs args) /// public IReadOnlyDictionary Claims { get; private set; } } - - 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 4c0c1e43..ce6c85f2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs @@ -93,6 +93,7 @@ public static FirebaseTokenFactory Create(FirebaseApp app) signer = new FixedAccountIAMSigner( new HttpClientFactory(), app.Options.Credential, app.Options.ServiceAccountId); } + return new FirebaseTokenFactory(signer, SystemClock.Default); } @@ -109,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) @@ -138,10 +140,12 @@ public async Task CreateCustomTokenAsync( IssuedAtTimeSeconds = issued, ExpirationTimeSeconds = issued + TokenDurationSeconds, }; + if (developerClaims != null && developerClaims.Count > 0) { payload.Claims = developerClaims; } + return await JwtUtils.CreateSignedJwtAsync( header, payload, this.signer, cancellationToken).ConfigureAwait(false); } @@ -150,14 +154,14 @@ public void Dispose() { signer.Dispose(); } - } - internal class CustomTokenPayload : JsonWebToken.Payload - { - [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 4bf18cf1..80529c83 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs @@ -81,6 +81,7 @@ internal static FirebaseTokenVerifier CreateIDTokenVerifier(FirebaseApp app) 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() @@ -93,6 +94,7 @@ internal static FirebaseTokenVerifier CreateIDTokenVerifier(FirebaseApp app) Clock = SystemClock.Default, PublicKeySource = keySource, }; + return new FirebaseTokenVerifier(args); } @@ -103,6 +105,7 @@ internal async Task VerifyTokenAsync( { throw new ArgumentException($"{this.shortName} must not be null or empty."); } + string[] segments = token.Split('.'); if (segments.Length != 3) { @@ -179,6 +182,7 @@ await VerifySignatureAsync(segments, header.KeyId, cancellationToken) { allClaims.Remove(claim); } + payload.Claims = allClaims.ToImmutableDictionary(); return new FirebaseToken(payload); } @@ -204,6 +208,7 @@ private async Task VerifySignatureAsync( hash = hashAlg.ComputeHash( Encoding.ASCII.GetBytes($"{segments[0]}.{segments[1]}")); } + var signature = JwtUtils.Base64DecodeToBytes(segments[2]); var keys = await this.keySource.GetPublicKeysAsync(cancellationToken) .ConfigureAwait(false); @@ -224,21 +229,4 @@ private async Task VerifySignatureAsync( } } } - - 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/FixedAccountIAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs new file mode 100644 index 00000000..92123bd2 --- /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); + } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs index 1efeb1c7..5becff23 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs @@ -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 2d8b283c..0057b3e2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs @@ -60,6 +60,7 @@ public async Task SignDataAsync( { BytesToSign = Convert.ToBase64String(data), }; + try { var response = await httpClient.PostJsonAsync(url, request, cancellationToken) @@ -114,6 +115,7 @@ private void ThrowIfError(HttpResponseMessage response, string content) { return; } + string error = null; try { @@ -124,72 +126,51 @@ private void ThrowIfError(HttpResponseMessage response, string content) { // 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); } - } - - /// - /// Represents the sign request sent to the remote IAM service. - /// - internal class SignBlobRequest - { - [Newtonsoft.Json.JsonProperty("bytesToSign")] - public string BytesToSign { get; set; } - } - - /// - /// Represents the sign response sent by the remote IAM service. - /// - internal class SignBlobResponse - { - [Newtonsoft.Json.JsonProperty("signature")] - public string Signature { get; set; } - } - - /// - /// Represents an error response sent by the remote IAM service. - /// - internal class SignBlobError - { - [Newtonsoft.Json.JsonProperty("error")] - public SignBlobErrorDetail Error { get; set; } - } - /// - /// 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 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; + /// + /// Represents the sign response sent by the remote IAM service. + /// + internal class SignBlobResponse + { + [Newtonsoft.Json.JsonProperty("signature")] + public string Signature { get; set; } + } - public FixedAccountIAMSigner( - HttpClientFactory clientFactory, GoogleCredential credential, string keyId) - : base(clientFactory, credential) + /// + /// Represents an error response sent by the remote IAM service. + /// + private class SignBlobError { - this.keyId = keyId.ThrowIfNullOrEmpty(nameof(keyId)); + [Newtonsoft.Json.JsonProperty("error")] + public SignBlobErrorDetail Error { get; set; } } - public override Task GetKeyIdAsync( - CancellationToken cancellationToken = default(CancellationToken)) + /// + /// Represents the error details embedded in an IAM error response. + /// + private class SignBlobErrorDetail { - return Task.FromResult(this.keyId); + [Newtonsoft.Json.JsonProperty("message")] + public string Message { get; set; } } } } \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs b/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs index aabd62b0..b252236f 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/JwtUtils.cs @@ -53,6 +53,7 @@ internal static byte[] Base64DecodeToBytes(string input) case 2: input += "=="; break; case 3: input += "="; break; } + return Convert.FromBase64String(input); } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs index f47dc0ac..dc69f0c9 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs @@ -33,6 +33,7 @@ public ServiceAccountSigner(ServiceAccountCredential credential) { throw new ArgumentNullException("Credential must not be null."); } + this.credential = credential; } diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index b8a1d960..1eaa3590 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -41,6 +41,7 @@ public static ServiceAccountCredential ToServiceAccountCredential( return ((GoogleCredential)credential.UnderlyingCredential) .ToServiceAccountCredential(); } + return credential.UnderlyingCredential as ServiceAccountCredential; } diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs index cee07c13..8ea5ad73 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs @@ -68,14 +68,16 @@ public sealed class FirebaseApp private FirebaseApp(AppOptions options, string name) { this.options = new AppOptions(options); - if (options.Credential == null) + if (this.options.Credential == null) { throw new ArgumentNullException("Credential must be set"); } - if (options.Credential.IsCreateScopedRequired) + + if (this.options.Credential.IsCreateScopedRequired) { - options.Credential = options.Credential.CreateScoped(DefaultScopes); + this.options.Credential = this.options.Credential.CreateScoped(DefaultScopes); } + Name = name; } @@ -120,6 +122,7 @@ public static FirebaseApp GetInstance(string name) { throw new ArgumentException("App name to lookup must not be null or empty"); } + lock (Apps) { FirebaseApp app; @@ -128,6 +131,7 @@ public static FirebaseApp GetInstance(string name) return app; } } + return null; } @@ -182,6 +186,7 @@ public static FirebaseApp Create(AppOptions options, string name) { throw new ArgumentException("App name must not be null or empty"); } + options = options ?? GetOptionsFromEnvironment(); lock (Apps) { @@ -196,6 +201,7 @@ public static FirebaseApp Create(AppOptions options, string name) throw new ArgumentException($"FirebaseApp named {name} already exists."); } } + var app = new FirebaseApp(options, name); Apps.Add(name, app); return app; @@ -224,8 +230,10 @@ public void Delete() Logger.Error(e, "Error while cleaning up service {0}", entry.Key); } } + this.services.Clear(); } + // Clean up global state lock (Apps) { @@ -245,6 +253,7 @@ internal static void DeleteAll() { entry.Value.Delete(); } + if (Apps.Count > 0) { throw new InvalidOperationException("Failed to delete all apps"); @@ -261,12 +270,14 @@ internal T GetOrInit(string id, ServiceFactory initializer) { throw new InvalidOperationException("Cannot use an app after it has been deleted"); } + IFirebaseService service; if (!this.services.TryGetValue(id, out service)) { service = initializer(); this.services.Add(id, service); } + return (T)service; } } @@ -285,11 +296,13 @@ internal string GetProjectId() { return Options.ProjectId; } + var projectId = Options.Credential.ToServiceAccountCredential()?.ProjectId; if (!string.IsNullOrEmpty(projectId)) { return projectId; } + foreach (var variableName in new[] { "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT" }) { projectId = Environment.GetEnvironmentVariable(variableName); @@ -298,6 +311,7 @@ internal string GetProjectId() return projectId; } } + return null; } diff --git a/stylecop.ruleset b/stylecop.ruleset index ba9d2760..a012047b 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -6,10 +6,8 @@ + - - - diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset index 9adea184..95fa5539 100644 --- a/stylecop_test.ruleset +++ b/stylecop_test.ruleset @@ -6,9 +6,9 @@ + + - - From f99154935b55896899b3559b4e4cd85daa330e7e Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 16:29:17 -0800 Subject: [PATCH 39/50] Fixed almost everything --- .../Auth/FirebaseTokenFactoryTest.cs | 1 + .../Auth/FirebaseTokenVerifierTest.cs | 2 ++ .../FirebaseAdmin.Tests/MockMessageHandler.cs | 3 +++ FirebaseAdmin/FirebaseAdmin/AppOptions.cs | 10 +++++---- .../FirebaseAdmin/Auth/FirebaseAuth.cs | 2 +- .../FirebaseAdmin/Auth/FirebaseToken.cs | 16 +++++++------- .../Auth/FirebaseTokenVerifier.cs | 1 + FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs | 4 ++-- FirebaseAdmin/FirebaseAdmin/Extensions.cs | 21 +++++++++++++++++++ FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs | 6 +++--- stylecop.ruleset | 10 ++------- stylecop_test.ruleset | 20 +++++++----------- 12 files changed, 59 insertions(+), 37 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs index 8bafe5bd..fa954b82 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenFactoryTest.cs @@ -96,6 +96,7 @@ private static void VerifyCustomToken( { string[] segments = token.Split("."); Assert.Equal(3, segments.Length); + // verify header var header = JwtUtils.Decode(segments[0]); Assert.Equal("JWT", header.Type); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs index 8d45232e..306d3b5b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseTokenVerifierTest.cs @@ -64,6 +64,7 @@ public async Task ValidToken() 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); @@ -283,6 +284,7 @@ internal static async Task CreateTestTokenAsync( payload[entry.Key] = entry.Value; } } + return await JwtUtils.CreateSignedJwtAsync(header, payload, Signer); } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs index ebd34534..117c888a 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs @@ -58,6 +58,7 @@ protected override async Task SendAsyncCore( { Request = null; } + var resp = new HttpResponseMessage(); string json; if (Response is byte[]) @@ -72,11 +73,13 @@ protected override async Task SendAsyncCore( { json = NewtonsoftJsonSerializer.Instance.Serialize(Response); } + resp.StatusCode = StatusCode; if (ApplyHeaders != null) { ApplyHeaders(resp.Headers); } + resp.Content = new StringContent(json, Encoding.UTF8, "application/json"); var tcs = new TaskCompletionSource(); tcs.SetResult(resp); diff --git a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs index 1a4bf78f..045d9e9d 100644 --- a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs +++ b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs @@ -38,18 +38,20 @@ internal AppOptions(AppOptions options) } /// - /// used to authorize an app. All service calls made by - /// the app will be authorized using this. + /// 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 diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index 4d5080dc..8b88977a 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -41,7 +41,7 @@ private FirebaseAuth(FirebaseApp app) } /// - /// The auth instance associated with the default Firebase app. This property is + /// 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 diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs index c60d5339..d4fc1659 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs @@ -35,38 +35,40 @@ internal FirebaseToken(FirebaseTokenArgs args) } /// - /// 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; } diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs index 80529c83..ecb7ed00 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs @@ -177,6 +177,7 @@ internal async Task VerifyTokenAsync( await 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) { diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs b/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs index a29f05a4..280cb53e 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs @@ -34,12 +34,12 @@ public PublicKey(string keyId, RSAKey rsa) } /// - /// The unique identifier of this key. + /// Gets the unique identifier of this key. /// public string Id { get; } /// - /// A instance containing the contents of + /// Gets the instance containing the contents of /// the public key. /// public RSAKey RSA { get; } diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index 1eaa3590..650db9df 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -33,6 +33,9 @@ internal static class Extensions /// . Returns null if the GoogleCredential is not /// based on a service account. /// + /// A service account credential if available, or null. + /// The Google credential from which to extract service account + /// credentials. public static ServiceAccountCredential ToServiceAccountCredential( this GoogleCredential credential) { @@ -49,6 +52,9 @@ public static ServiceAccountCredential ToServiceAccountCredential( /// Creates a default (unauthenticated) from the /// factory. /// + /// An HTTP client that can be used to make unauthenticated requests. + /// The used to create + /// the HTTP client. public static ConfigurableHttpClient CreateDefaultHttpClient( this HttpClientFactory clientFactory) { @@ -59,6 +65,11 @@ public static ConfigurableHttpClient CreateDefaultHttpClient( /// Creates an authenticated from the /// factory. /// + /// An HTTP client that can be used to OAuth2 authorized requests. + /// The used to create + /// the HTTP client. + /// The Google credential that will be used to authenticate + /// outgoing HTTP requests. public static ConfigurableHttpClient CreateAuthorizedHttpClient( this HttpClientFactory clientFactory, GoogleCredential credential) { @@ -70,6 +81,14 @@ public static ConfigurableHttpClient CreateAuthorizedHttpClient( /// /// Makes a JSON POST request using the given parameters. /// + /// An representing the response to the + /// POST request. + /// Type of the object that will be serialized into JSON. + /// The used to make the request. + /// URI for the outgoing request. + /// The object that will be serialized as the JSON body. + /// A cancellation token to monitor the asynchronous + /// operation. public static async Task PostJsonAsync( this HttpClient client, string requestUri, T body, CancellationToken cancellationToken) { @@ -82,6 +101,8 @@ public static async Task PostJsonAsync( /// /// Returns a Unix-styled timestamp (seconds from epoch) from the . /// + /// Number of seconds since epoch. + /// The used to generate the timestamp. public static long UnixTimestamp(this IClock clock) { var timeSinceEpoch = clock.UtcNow.Subtract(new DateTime(1970, 1, 1)); diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs index 8ea5ad73..609f68f2 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs @@ -82,7 +82,7 @@ private FirebaseApp(AppOptions options, string name) } /// - /// The default app instance. This property is null if the default app instance + /// Gets the default app instance. This property is null if the default app instance /// doesn't yet exist. /// public static FirebaseApp DefaultInstance @@ -94,7 +94,7 @@ public static FirebaseApp DefaultInstance } /// - /// A copy of the this app was created with. + /// Gets a copy of the this app was created with. /// public AppOptions Options { @@ -105,7 +105,7 @@ public AppOptions Options } /// - /// Name of this app. + /// Gets the name of this app. /// public string Name { get; } diff --git a/stylecop.ruleset b/stylecop.ruleset index a012047b..4b810243 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -7,13 +7,7 @@ - - - - - - - - + + \ No newline at end of file diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset index 95fa5539..7593a30c 100644 --- a/stylecop_test.ruleset +++ b/stylecop_test.ruleset @@ -4,19 +4,15 @@ - - + + - - - - - - - - - - + + + + + + \ No newline at end of file From 6bcc4d6231ff93d57a237386598e61badb552436 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 16:33:01 -0800 Subject: [PATCH 40/50] Linter test commit --- .travis.yml | 1 + stylecop.ruleset | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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/stylecop.ruleset b/stylecop.ruleset index 4b810243..da07239c 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -6,7 +6,7 @@ - + From 9108c266a2b5aa54e416a7d50469f45834c570b8 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 16:37:47 -0800 Subject: [PATCH 41/50] Fixing lint error --- stylecop.ruleset | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stylecop.ruleset b/stylecop.ruleset index da07239c..4b810243 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -6,7 +6,7 @@ - + From 1b9558aedf6a1af626413c2f1f4c6218901b1ce7 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 19:51:53 -0800 Subject: [PATCH 42/50] Enabled lint rule for 'this' checks --- .../Auth/ServiceAccountSignerTest.cs | 2 +- .../FirebaseAdmin.Tests/FirebaseAppTest.cs | 2 +- .../FirebaseAdmin.Tests/MockClock.cs | 6 ++--- .../FirebaseAdmin.Tests/MockMessageHandler.cs | 24 +++++++++---------- FirebaseAdmin/FirebaseAdmin/AppOptions.cs | 6 ++--- .../FirebaseAdmin/Auth/FirebaseAuth.cs | 8 +++---- .../FirebaseAdmin/Auth/FirebaseToken.cs | 14 +++++------ .../Auth/FirebaseTokenFactory.cs | 2 +- .../Auth/FirebaseTokenVerifier.cs | 13 ++++------ .../FirebaseAdmin/Auth/HttpPublicKeySource.cs | 20 ++++++++-------- FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs | 10 ++++---- FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs | 4 ++-- .../Auth/ServiceAccountSigner.cs | 12 ++++------ FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs | 10 ++++---- stylecop.ruleset | 1 - stylecop_test.ruleset | 1 - 16 files changed, 63 insertions(+), 72 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs index ab51c2a1..cf76f125 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Auth/ServiceAccountSignerTest.cs @@ -36,7 +36,7 @@ public async Task Signer() "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] diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs index 1b7307a9..5a453821 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAppTest.cs @@ -222,7 +222,7 @@ internal class MockService : IFirebaseService public void Delete() { - Deleted = true; + this.Deleted = true; } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs index 3d26ca34..f68f7785 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockClock.cs @@ -24,13 +24,13 @@ public class MockClock : IClock public MockClock() { - Now = DateTime.Now; + this.Now = DateTime.Now; } public DateTime Now { - get { return UtcNow.ToLocalTime(); } - set { UtcNow = value.ToUniversalTime(); } + get { return this.UtcNow.ToLocalTime(); } + set { this.UtcNow = value.ToUniversalTime(); } } public DateTime UtcNow diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs index 117c888a..b6456e79 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs @@ -34,7 +34,7 @@ internal class MockMessageHandler : CountableMessageHandler { public MockMessageHandler() { - StatusCode = HttpStatusCode.OK; + this.StatusCode = HttpStatusCode.OK; } public delegate void SetHeaders(HttpResponseHeaders header); @@ -52,32 +52,32 @@ protected override async Task SendAsyncCore( { 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); + json = NewtonsoftJsonSerializer.Instance.Serialize(this.Response); } - resp.StatusCode = StatusCode; - if (ApplyHeaders != null) + resp.StatusCode = this.StatusCode; + if (this.ApplyHeaders != null) { - ApplyHeaders(resp.Headers); + this.ApplyHeaders(resp.Headers); } resp.Content = new StringContent(json, Encoding.UTF8, "application/json"); @@ -104,7 +104,7 @@ protected sealed override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Interlocked.Increment(ref this.calls); - return SendAsyncCore(request, cancellationToken); + return this.SendAsyncCore(request, cancellationToken); } protected abstract Task SendAsyncCore( diff --git a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs index 045d9e9d..9d529ac5 100644 --- a/FirebaseAdmin/FirebaseAdmin/AppOptions.cs +++ b/FirebaseAdmin/FirebaseAdmin/AppOptions.cs @@ -32,9 +32,9 @@ public AppOptions() { } internal AppOptions(AppOptions options) { - Credential = options.Credential; - ProjectId = options.ProjectId; - ServiceAccountId = options.ServiceAccountId; + this.Credential = options.Credential; + this.ProjectId = options.ProjectId; + this.ServiceAccountId = options.ServiceAccountId; } /// diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index 8b88977a..a2226a04 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -111,7 +111,7 @@ public static FirebaseAuth GetAuth(FirebaseApp app) /// 128 characters. public async Task CreateCustomTokenAsync(string uid) { - return await CreateCustomTokenAsync(uid, default(CancellationToken)); + return await this.CreateCustomTokenAsync(uid, default(CancellationToken)); } /// @@ -150,7 +150,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); } /// @@ -177,7 +177,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)); } /// @@ -241,7 +241,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)); } /// diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs index d4fc1659..9cd4cc23 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseToken.cs @@ -25,13 +25,13 @@ public sealed class FirebaseToken { 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; + 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; } /// diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs index ce6c85f2..411c97fa 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenFactory.cs @@ -152,7 +152,7 @@ public async Task CreateCustomTokenAsync( public void Dispose() { - signer.Dispose(); + this.signer.Dispose(); } internal class CustomTokenPayload : JsonWebToken.Payload diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs index ecb7ed00..fc9fc1b6 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs @@ -38,9 +38,6 @@ internal sealed class FirebaseTokenVerifier 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 - private const string Sha256Oid = "2.16.840.1.101.3.4.2.1"; - private static readonly IReadOnlyList StandardClaims = ImmutableList.Create("iss", "aud", "exp", "iat", "sub", "uid"); @@ -54,7 +51,7 @@ internal sealed class FirebaseTokenVerifier internal FirebaseTokenVerifier(FirebaseTokenVerifierArgs args) { - ProjectId = args.ProjectId.ThrowIfNullOrEmpty(nameof(args.ProjectId)); + 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)); @@ -118,7 +115,7 @@ internal async Task VerifyTokenAsync( + "project as the credential used to initialize this SDK."; var verifyTokenMessage = $"See {this.url} for details on how to retrieve a value " + $"{this.shortName}."; - var issuer = this.issuer + ProjectId; + var issuer = this.issuer + this.ProjectId; string error = null; if (string.IsNullOrEmpty(header.KeyId)) { @@ -142,9 +139,9 @@ internal async Task VerifyTokenAsync( 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 = $"{this.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) @@ -174,7 +171,7 @@ internal async Task VerifyTokenAsync( 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]); diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs index 5becff23..6851d190 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/HttpPublicKeySource.cs @@ -66,26 +66,26 @@ public HttpPublicKeySource(string certUrl, IClock clock, HttpClientFactory clien public async Task> GetPublicKeysAsync( CancellationToken cancellationToken = default(CancellationToken)) { - if (cachedKeys == null || clock.UtcNow >= expirationTime) + if (this.cachedKeys == null || this.clock.UtcNow >= this.expirationTime) { - await cacheLock.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 { - cacheLock.Release(); + this.cacheLock.Release(); } } - return cachedKeys; + return this.cachedKeys; } private IReadOnlyList ParseKeys(string json) diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs index 0057b3e2..9051c343 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs @@ -54,7 +54,7 @@ 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 { @@ -63,10 +63,10 @@ public async Task SignDataAsync( 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); } @@ -81,7 +81,7 @@ public virtual async Task GetKeyIdAsync( { try { - return await keyId.Value.ConfigureAwait(false); + return await this.keyId.Value.ConfigureAwait(false); } catch (Exception e) { @@ -96,7 +96,7 @@ public virtual async Task GetKeyIdAsync( public void Dispose() { - httpClient.Dispose(); + this.httpClient.Dispose(); } private static async Task DiscoverServiceAccountIdAsync( diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs b/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs index 280cb53e..483f5f7c 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/PublicKey.cs @@ -29,8 +29,8 @@ internal sealed class PublicKey { public PublicKey(string keyId, RSAKey rsa) { - Id = keyId; - RSA = rsa; + this.Id = keyId; + this.RSA = rsa; } /// diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs index dc69f0c9..79b782e7 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/ServiceAccountSigner.cs @@ -16,6 +16,7 @@ using System.Threading; using System.Threading.Tasks; using Google.Apis.Auth.OAuth2; +using Google.Apis.Util; namespace FirebaseAdmin.Auth { @@ -29,23 +30,18 @@ internal sealed class ServiceAccountSigner : ISigner public ServiceAccountSigner(ServiceAccountCredential credential) { - if (credential == null) - { - throw new ArgumentNullException("Credential must not be null."); - } - - this.credential = credential; + this.credential = credential.ThrowIfNull(nameof(credential)); } public Task GetKeyIdAsync(CancellationToken cancellationToken = default(CancellationToken)) { - return Task.FromResult(credential.Id); + return Task.FromResult(this.credential.Id); } public Task SignDataAsync(byte[] data, CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); - var signature = credential.CreateSignature(data); + var signature = this.credential.CreateSignature(data); return Task.FromResult(Convert.FromBase64String(signature)); } diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs index 609f68f2..9f12ecd2 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseApp.cs @@ -78,7 +78,7 @@ private FirebaseApp(AppOptions options, string name) this.options.Credential = this.options.Credential.CreateScoped(DefaultScopes); } - Name = name; + this.Name = name; } /// @@ -237,7 +237,7 @@ public void Delete() // Clean up global state lock (Apps) { - Apps.Remove(Name); + Apps.Remove(this.Name); } } @@ -292,12 +292,12 @@ internal T GetOrInit(string id, ServiceFactory initializer) /// A project ID string or null. internal string GetProjectId() { - if (!string.IsNullOrEmpty(Options.ProjectId)) + if (!string.IsNullOrEmpty(this.Options.ProjectId)) { - return Options.ProjectId; + return this.Options.ProjectId; } - var projectId = Options.Credential.ToServiceAccountCredential()?.ProjectId; + var projectId = this.Options.Credential.ToServiceAccountCredential()?.ProjectId; if (!string.IsNullOrEmpty(projectId)) { return projectId; diff --git a/stylecop.ruleset b/stylecop.ruleset index 4b810243..fe083603 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -4,7 +4,6 @@ - diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset index 7593a30c..eb44502a 100644 --- a/stylecop_test.ruleset +++ b/stylecop_test.ruleset @@ -4,7 +4,6 @@ - From 477ecd08fbc0772e1af9be1a6951aee89b3add2d Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 22:06:42 -0800 Subject: [PATCH 43/50] Enabled all possible rules; Added linting to snippets; --- .../FirebaseAdmin.IntegrationTests.csproj | 1 - .../FirebaseAdmin.Snippets.csproj | 5 +++++ .../FirebaseAppSnippets.cs | 16 ++++++++-------- .../FirebaseAuthSnippets.cs | 8 ++++---- .../FirebaseAdmin.Tests.csproj | 1 - FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs | 3 +++ FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj | 1 + FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs | 3 +++ stylecop.json | 8 ++++++++ stylecop.ruleset | 1 - stylecop_test.ruleset | 3 +++ 11 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 stylecop.json diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj index 3edd66b0..d146599a 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseAdmin.IntegrationTests.csproj @@ -4,7 +4,6 @@ netcoreapp2.0 false true - true ../../stylecop_test.ruleset 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/FirebaseAdmin.Tests.csproj b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj index 903194ec..8641fa7b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj +++ b/FirebaseAdmin/FirebaseAdmin.Tests/FirebaseAdmin.Tests.csproj @@ -6,7 +6,6 @@ ../../FirebaseAdmin.snk true true - true ../../stylecop_test.ruleset diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs index a2226a04..11de31bf 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs @@ -277,6 +277,9 @@ public async Task VerifyIdTokenAsync( .ConfigureAwait(false); } + /// + /// Deletes this service instance. + /// void IFirebaseService.Delete() { lock (this.authLock) diff --git a/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj b/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj index 117c7603..5f6c5bbc 100644 --- a/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj +++ b/FirebaseAdmin/FirebaseAdmin/FirebaseAdmin.csproj @@ -30,6 +30,7 @@ all + diff --git a/FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs b/FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs index d6075f4a..3407ef9f 100644 --- a/FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs +++ b/FirebaseAdmin/FirebaseAdmin/IFirebaseService.cs @@ -20,6 +20,9 @@ namespace FirebaseAdmin /// internal interface IFirebaseService { + /// + /// Cleans up any state associated with this service making it unsuitable for further use. + /// void Delete(); } } diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 00000000..b38dbb7c --- /dev/null +++ b/stylecop.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "documentInternalElements": false + } + } +} \ No newline at end of file diff --git a/stylecop.ruleset b/stylecop.ruleset index fe083603..98c8be6f 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -6,7 +6,6 @@ - \ No newline at end of file diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset index eb44502a..c088d3c0 100644 --- a/stylecop_test.ruleset +++ b/stylecop_test.ruleset @@ -7,11 +7,14 @@ + + + \ No newline at end of file From ad58b40bdb37a124d2cb1b96d4e93903529d9ca1 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Mon, 21 Jan 2019 22:15:04 -0800 Subject: [PATCH 44/50] Added missing newlines at eof --- FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs | 2 +- stylecop.json | 2 +- stylecop.ruleset | 2 +- stylecop_test.ruleset | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs index 92123bd2..8398d9fb 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FixedAccountIAMSigner.cs @@ -42,4 +42,4 @@ public override Task GetKeyIdAsync( return Task.FromResult(this.keyId); } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs index 9051c343..f7d810bd 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/IAMSigner.cs @@ -173,4 +173,4 @@ private class SignBlobErrorDetail public string Message { get; set; } } } -} \ No newline at end of file +} diff --git a/stylecop.json b/stylecop.json index b38dbb7c..56f05687 100644 --- a/stylecop.json +++ b/stylecop.json @@ -5,4 +5,4 @@ "documentInternalElements": false } } -} \ No newline at end of file +} diff --git a/stylecop.ruleset b/stylecop.ruleset index 98c8be6f..8a4c10a4 100644 --- a/stylecop.ruleset +++ b/stylecop.ruleset @@ -8,4 +8,4 @@ - \ No newline at end of file + diff --git a/stylecop_test.ruleset b/stylecop_test.ruleset index c088d3c0..aaef9717 100644 --- a/stylecop_test.ruleset +++ b/stylecop_test.ruleset @@ -17,4 +17,4 @@ - \ No newline at end of file + From ee8cfd3cf47225360d7db50c2ef34b7b4f7de99b Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Tue, 22 Jan 2019 16:21:15 -0800 Subject: [PATCH 45/50] Added more unit tests --- .../Messaging/MessageTest.cs | 45 +++++++++++++++++++ .../FirebaseAdmin/Messaging/ApnsConfig.cs | 11 ++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index a5033d9b..b211aa0b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -105,6 +105,13 @@ public void MessageDeserialization() { RestrictedPackageName = "test-pkg-name", }, + Apns = new ApnsConfig() + { + Aps = new Aps() + { + AlertString = "test-alert", + }, + }, Webpush = new WebpushConfig() { Data = new Dictionary(){{ "key", "value" }}, @@ -118,6 +125,7 @@ public void MessageDeserialization() 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); } @@ -130,6 +138,7 @@ public void MessageCopy() Data = new Dictionary(), Notification = new Notification(), Android = new AndroidConfig(), + Apns = new ApnsConfig(), Webpush = new WebpushConfig(), }; var copy = original.CopyAndValidate(); @@ -137,6 +146,7 @@ public void MessageCopy() 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); } @@ -1075,6 +1085,22 @@ public void ApnsCriticalSoundMinimal() 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() { @@ -1211,6 +1237,25 @@ public void ApsAlertDeserialization() 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() { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 39df3b4a..7a360b76 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -102,14 +102,21 @@ internal ApnsConfig CopyAndValidate() } } + /// + /// The APNs payload object as expected by the FCM backend service. + /// internal sealed class ApnsPayload { [JsonProperty("aps")] - public Aps Aps { get; set; } + internal Aps Aps { get; set; } [JsonExtensionData] - public IDictionary CustomData { get; set; } + internal IDictionary CustomData { get; set; } + /// + /// Copies this APNs payload, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. + /// internal ApnsPayload CopyAndValidate() { var copy = new ApnsPayload() From ae1e80c2c8309d6f406f4857318475775dabcb50 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 23 Jan 2019 13:50:31 -0800 Subject: [PATCH 46/50] Adding newline at eod --- FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs | 2 +- FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 7a360b76..158016d2 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -132,4 +132,4 @@ internal ApnsPayload CopyAndValidate() return copy; } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index 585f4452..7c64dae9 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -250,4 +250,4 @@ internal Aps CopyAndValidate() return copy; } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs index f84ab225..c48c6012 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -136,4 +136,4 @@ internal ApsAlert CopyAndValidate() return copy; } } -} \ No newline at end of file +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index 636fe5b7..de59f1d0 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -88,4 +88,4 @@ internal CriticalSound CopyAndValidate() return copy; } } -} \ No newline at end of file +} From e6bb4d2a6bc93d20a36f79ee3042be3a6f67b2b4 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Wed, 23 Jan 2019 15:08:46 -0800 Subject: [PATCH 47/50] Adding required constant --- FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs index fc9fc1b6..172fa03e 100644 --- a/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs +++ b/FirebaseAdmin/FirebaseAdmin/Auth/FirebaseTokenVerifier.cs @@ -38,6 +38,9 @@ internal sealed class FirebaseTokenVerifier 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 + private const string Sha256Oid = "2.16.840.1.101.3.4.2.1"; + private static readonly IReadOnlyList StandardClaims = ImmutableList.Create("iss", "aud", "exp", "iat", "sub", "uid"); From bbbc92566a4d93892af40d8fbcd56588a68387e6 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 25 Jan 2019 15:20:01 -0800 Subject: [PATCH 48/50] Fixing lint errors --- .../Messaging/FirebaseMessageClientTest.cs | 11 +- .../Messaging/MessageTest.cs | 14 +- FirebaseAdmin/FirebaseAdmin/Extensions.cs | 1 + .../FirebaseAdmin/Messaging/Action.cs | 54 +++++ .../FirebaseAdmin/Messaging/AndroidConfig.cs | 119 +++++------ .../Messaging/AndroidNotification.cs | 55 ++--- .../FirebaseAdmin/Messaging/ApnsConfig.cs | 92 +++++---- FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs | 163 ++++++++------- .../FirebaseAdmin/Messaging/ApsAlert.cs | 41 ++-- .../FirebaseAdmin/Messaging/CriticalSound.cs | 45 +++-- .../FirebaseAdmin/Messaging/Direction.cs | 37 ++++ .../Messaging/FirebaseMessaging.cs | 91 +++++---- .../Messaging/FirebaseMessagingClient.cs | 70 ++++--- .../FirebaseAdmin/Messaging/Message.cs | 70 ++++--- .../FirebaseAdmin/Messaging/Notification.cs | 4 +- .../FirebaseAdmin/Messaging/Priority.cs | 32 +++ .../FirebaseAdmin/Messaging/WebpushConfig.cs | 7 +- .../Messaging/WebpushNotification.cs | 189 +++++++----------- 18 files changed, 603 insertions(+), 492 deletions(-) create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/Action.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/Direction.cs create mode 100644 FirebaseAdmin/FirebaseAdmin/Messaging/Priority.cs diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs index 30a0630c..37eafe2c 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs @@ -60,7 +60,7 @@ public async Task SendAsync() { var handler = new MockMessageHandler() { - Response = new SendResponse() + Response = new FirebaseMessagingClient.SendResponse() { Name = "test-response", }, @@ -73,7 +73,8 @@ public async Task SendAsync() }; var response = await client.SendAsync(message); Assert.Equal("test-response", response); - var req = JsonConvert.DeserializeObject(handler.Request); + var req = JsonConvert.DeserializeObject( + handler.Request); Assert.Equal("test-topic", req.Message.Topic); Assert.False(req.ValidateOnly); Assert.Equal(1, handler.Calls); @@ -81,7 +82,8 @@ public async Task SendAsync() // Send in dryRun mode. response = await client.SendAsync(message, dryRun: true); Assert.Equal("test-response", response); - req = JsonConvert.DeserializeObject(handler.Request); + req = JsonConvert.DeserializeObject( + handler.Request); Assert.Equal("test-topic", req.Message.Topic); Assert.True(req.ValidateOnly); Assert.Equal(2, handler.Calls); @@ -104,7 +106,8 @@ public async Task HttpErrorAsync() var ex = await Assert.ThrowsAsync( async () => await client.SendAsync(message)); Assert.Contains("not json", ex.Message); - var req = JsonConvert.DeserializeObject(handler.Request); + 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/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index b211aa0b..3deb2363 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -27,20 +27,20 @@ public class MessageTest [Fact] public void EmptyMessage() { - var message = new Message(){Token = "test-token"}; - AssertJsonEquals(new JObject(){{"token", "test-token"}}, message); + var message = new Message() { Token = "test-token" }; + AssertJsonEquals(new JObject() { { "token", "test-token" } }, message); - message = new Message(){Topic = "test-topic"}; - AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); + message = new Message() { Topic = "test-topic" }; + AssertJsonEquals(new JObject() { { "topic", "test-topic" } }, message); - message = new Message(){Condition = "test-condition"}; - AssertJsonEquals(new JObject(){{"condition", "test-condition"}}, message); + message = new Message() { Condition = "test-condition" }; + AssertJsonEquals(new JObject() { { "condition", "test-condition" } }, message); } [Fact] public void PrefixedTopicName() { - var message = new Message(){Topic = "/topics/test-topic"}; + var message = new Message() { Topic = "/topics/test-topic" }; AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); } diff --git a/FirebaseAdmin/FirebaseAdmin/Extensions.cs b/FirebaseAdmin/FirebaseAdmin/Extensions.cs index f0442a4a..d8d6f354 100644 --- a/FirebaseAdmin/FirebaseAdmin/Extensions.cs +++ b/FirebaseAdmin/FirebaseAdmin/Extensions.cs @@ -121,6 +121,7 @@ public static IReadOnlyDictionary Copy( { copy[entry.Key] = entry.Value; } + return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Action.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Action.cs new file mode 100644 index 00000000..09ff996b --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Action.cs @@ -0,0 +1,54 @@ +// 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 Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Represents an action available to users when the notification is presented. + /// + public sealed class Action + { + /// + /// Initializes a new instance of the class. + /// + public Action() { } + + internal Action(Action action) + { + this.ActionName = action.ActionName; + this.Title = action.Title; + this.Icon = action.Icon; + } + + /// + /// Gets or sets the name of the Action. + /// + [JsonProperty("action")] + public string ActionName { get; set; } + + /// + /// Gets or sets the title text. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// Gets or sets the icon URL. + /// + [JsonProperty("icon")] + public string Icon { get; set; } + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs index 37b79cb3..9ead102d 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidConfig.cs @@ -24,29 +24,58 @@ namespace FirebaseAdmin.Messaging public sealed class AndroidConfig { /// - /// A collapse key for the message. Collapse key serves as an identifier for a group of - /// messages that can be collapsed, so that only the last message gets sent when delivery can be - /// resumed. A maximum of 4 different collapse keys may be active at any given time. + /// Gets or sets a collapse key for the message. Collapse key serves as an identifier for a + /// group of messages that can be collapsed, so that only the last message gets sent when + /// delivery can be resumed. A maximum of 4 different collapse keys may be active at any + /// given time. /// [JsonProperty("collapse_key")] public string CollapseKey { get; set; } /// - /// The priority of the message. + /// Gets or sets the priority of the message. /// [JsonIgnore] public Priority? Priority { get; set; } /// - /// String representation of the as accepted by the FCM backend - /// service. + /// Gets or sets the time-to-live duration of the message. + /// + [JsonIgnore] + public TimeSpan? TimeToLive { get; set; } + + /// + /// Gets or sets the package name of the application where the registration tokens must + /// match in order to receive the message. + /// + [JsonProperty("restricted_package_name")] + public string RestrictedPackageName { get; set; } + + /// + /// Gets or sets a collection of key-value pairs that will be added to the message as data + /// fields. Keys and the values must not be null. When set, overrides any data fields set + /// on the top-level + /// . + /// + [JsonProperty("data")] + public IReadOnlyDictionary Data { get; set; } + + /// + /// Gets or sets the Android notification to be included in the message. + /// + [JsonProperty("notification")] + public AndroidNotification Notification { get; set; } + + /// + /// Gets or sets the string representation of as accepted by the FCM + /// backend service. /// [JsonProperty("priority")] private string PriorityString { get { - switch (Priority) + switch (this.Priority) { case Messaging.Priority.High: return "high"; @@ -56,15 +85,16 @@ private string PriorityString return null; } } + set { switch (value) { case "high": - Priority = Messaging.Priority.High; + this.Priority = Messaging.Priority.High; return; case "normal": - Priority = Messaging.Priority.High; + this.Priority = Messaging.Priority.High; return; default: throw new FirebaseException( @@ -75,69 +105,46 @@ private string PriorityString } /// - /// The time-to-live duration of the message. - /// - [JsonIgnore] - public TimeSpan? TimeToLive { get; set; } - - /// - /// String representation of as accepted by the FCM backend - /// service. The string ends in the suffix "s" (indicating seconds) and is preceded - /// by the number of seconds, with nanoseconds expressed as fractional seconds. + /// Gets or sets the string representation of as accepted by the + /// FCM backend service. The string ends in the suffix "s" (indicating seconds) and is + /// preceded by the number of seconds, with nanoseconds expressed as fractional seconds. /// [JsonProperty("ttl")] private string TtlString { get { - if (TimeToLive == null) + if (this.TimeToLive == null) { return null; } - var totalSeconds = TimeToLive.Value.TotalSeconds; - var seconds = (long) Math.Floor(totalSeconds); - var subsecondNanos = (long) ((totalSeconds - seconds) * 1e9); + + var totalSeconds = this.TimeToLive.Value.TotalSeconds; + var seconds = (long)Math.Floor(totalSeconds); + var subsecondNanos = (long)((totalSeconds - seconds) * 1e9); if (subsecondNanos > 0) { - return String.Format("{0}.{1:D9}s", seconds, subsecondNanos); + return string.Format("{0}.{1:D9}s", seconds, subsecondNanos); } - return String.Format("{0}s", seconds); + + return string.Format("{0}s", seconds); } + set { var segments = value.TrimEnd('s').Split('.'); - var seconds = Int64.Parse(segments[0]); + var seconds = long.Parse(segments[0]); var ttl = TimeSpan.FromSeconds(seconds); if (segments.Length == 2) { - var subsecondNanos = Int64.Parse(segments[1].TrimStart('0')); + var subsecondNanos = long.Parse(segments[1].TrimStart('0')); ttl = ttl.Add(TimeSpan.FromMilliseconds(subsecondNanos / 1e6)); } - TimeToLive = ttl; + + this.TimeToLive = ttl; } } - /// - /// The package name of the application where the registration tokens must match in order - /// to receive the message. - /// - [JsonProperty("restricted_package_name")] - public string RestrictedPackageName { get; set; } - - /// - /// A collection of key-value pairs that will be added to the message as data fields. Keys - /// and the values must not be null. When set, overrides any data fields set on the top-level - /// . - /// - [JsonProperty("data")] - public IReadOnlyDictionary Data { get; set; } - - /// - /// The Android notification to be included in the message. - /// - [JsonProperty("notification")] - public AndroidNotification Notification { get; set; } - /// /// Copies this Android config, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM service. @@ -164,20 +171,4 @@ internal AndroidConfig CopyAndValidate() return copy; } } - - /// - /// Priority levels that can be set on an . - /// - public enum Priority - { - /// - /// High priority message. - /// - High, - - /// - /// Normal priority message. - /// - Normal, - } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs index 24e546fa..926733ac 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/AndroidNotification.cs @@ -26,88 +26,88 @@ namespace FirebaseAdmin.Messaging /// public sealed class AndroidNotification { - /// - /// The title of the Android notification. When provided, overrides the title set - /// via . + /// Gets or sets the title of the Android notification. When provided, overrides the title + /// set via . /// [JsonProperty("title")] public string Title { get; set; } /// - /// The title of the Android notification. When provided, overrides the title set - /// via . + /// Gets or sets the title of the Android notification. When provided, overrides the title + /// set via . /// [JsonProperty("body")] public string Body { get; set; } /// - /// The icon of the Android notification. + /// Gets or sets the icon of the Android notification. /// [JsonProperty("icon")] public string Icon { get; set; } /// - /// The notification icon color. Must be of the form #RRGGBB. + /// Gets or sets the notification icon color. Must be of the form #RRGGBB. /// [JsonProperty("color")] public string Color { get; set; } /// - /// The sound to be played when the device receives the notification. + /// Gets or sets the sound to be played when the device receives the notification. /// [JsonProperty("sound")] public string Sound { get; set; } /// - /// The notification tag. This is an identifier used to replace existing notifications in - /// the notification drawer. If not specified, each request creates a new notification. + /// Gets or sets the notification tag. This is an identifier used to replace existing + /// notifications in the notification drawer. If not specified, each request creates a new + /// notification. /// [JsonProperty("tag")] public string Tag { get; set; } /// - /// The action associated with a user click on the notification. If specified, an activity - /// with a matching Intent Filter is launched when a user clicks on the notification. + /// Gets or sets the action associated with a user click on the notification. If specified, + /// an activity with a matching Intent Filter is launched when a user clicks on the + /// notification. /// [JsonProperty("click_action")] public string ClickAction { get; set; } /// - /// Sets the key of the title string in the app's string resources to use to localize the - /// title text. - /// . + /// Gets or sets the key of the title string in the app's string resources to use to + /// localize the title text. /// [JsonProperty("title_loc_key")] public string TitleLocKey { get; set; } /// - /// The collection of resource key strings that will be used in place of the format - /// specifiers in . + /// Gets or sets the collection of resource key strings that will be used in place of the + /// format specifiers in . /// [JsonProperty("title_loc_args")] public IEnumerable TitleLocArgs { get; set; } /// - /// Sets the key of the body string in the app's string resources to use to localize the - /// body text. - /// . + /// Gets or sets the key of the body string in the app's string resources to use to + /// localize the body text. /// [JsonProperty("body_loc_key")] public string BodyLocKey { get; set; } /// - /// The collection of resource key strings that will be used in place of the format - /// specifiers in . + /// Gets or sets the collection of resource key strings that will be used in place of the + /// format specifiers in . /// [JsonProperty("body_loc_args")] public IEnumerable BodyLocArgs { get; set; } /// - /// Sets the Android notification channel ID (new in Android O). The app must create a - /// channel with this channel ID before any notification with this channel ID is received. - /// If you don't send this channel ID in the request, or if the channel ID provided has - /// not yet been created by the app, FCM uses the channel ID specified in the app manifest. + /// Gets or sets the Android notification channel ID (new in Android O). The app must + /// create a channel with this channel ID before any notification with this channel ID is + /// received. If you don't send this channel ID in the request, or if the channel ID + /// provided has not yet been created by the app, FCM uses the channel ID specified in the + /// app manifest. /// [JsonProperty("channel_id")] public string ChannelId { get; set; } @@ -137,14 +137,17 @@ internal AndroidNotification CopyAndValidate() { throw new ArgumentException("Color must be in the form #RRGGBB."); } + if (copy.TitleLocArgs?.Any() == true && string.IsNullOrEmpty(copy.TitleLocKey)) { throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs."); } + if (copy.BodyLocArgs?.Any() == true && string.IsNullOrEmpty(copy.BodyLocKey)) { throw new ArgumentException("BodyLocKey is required when specifying BodyLocArgs."); } + return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs index 158016d2..2b70f7e5 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApnsConfig.cs @@ -27,63 +27,68 @@ namespace FirebaseAdmin.Messaging /// public sealed class ApnsConfig { - private ApnsPayload _payload = new ApnsPayload(); + private ApnsPayload payload = new ApnsPayload(); /// - /// A collection of APNs headers. + /// Gets or sets the APNs headers. /// [JsonProperty("headers")] public IReadOnlyDictionary Headers { get; set; } /// - /// The aps dictionary to be included in the APNs payload. + /// Gets or sets the aps dictionary to be included in the APNs payload. /// [JsonIgnore] public Aps Aps { get { - return Payload.Aps; + return this.Payload.Aps; } + set { - Payload.Aps = value; + this.Payload.Aps = value; } } /// - /// APNs payload as accepted by the FCM backend servers. + /// Gets or sets a collection of arbitrary key-value data that will be included in the APNs + /// payload. /// - [JsonProperty("payload")] - private ApnsPayload Payload + [JsonIgnore] + public IDictionary CustomData { get { - if (_payload.Aps != null && _payload.CustomData?.ContainsKey("aps") == true) - { - throw new ArgumentException("Multiple specifications for ApnsConfig key: aps"); - } - return _payload; + return this.Payload.CustomData; } + set { - _payload = value; + this.Payload.CustomData = value; } } /// - /// A collection of arbitrary key-value data to be included in the APNs payload. + /// Gets or sets the APNs payload as accepted by the FCM backend servers. /// - [JsonIgnore] - public IDictionary CustomData + [JsonProperty("payload")] + private ApnsPayload Payload { get { - return Payload.CustomData; + if (this.payload.Aps != null && this.payload.CustomData?.ContainsKey("aps") == true) + { + throw new ArgumentException("Multiple specifications for ApnsConfig key: aps"); + } + + return this.payload; } + set { - Payload.CustomData = value; + this.payload = value; } } @@ -100,36 +105,37 @@ internal ApnsConfig CopyAndValidate() }; return copy; } - } - - /// - /// The APNs payload object as expected by the FCM backend service. - /// - internal sealed class ApnsPayload - { - [JsonProperty("aps")] - internal Aps Aps { get; set; } - - [JsonExtensionData] - internal IDictionary CustomData { get; set; } /// - /// Copies this APNs payload, and validates the content of it to ensure that it can be - /// serialized into the JSON format expected by the FCM service. + /// The APNs payload object as expected by the FCM backend service. /// - internal ApnsPayload CopyAndValidate() + private class ApnsPayload { - var copy = new ApnsPayload() - { - CustomData = this.CustomData?.ToDictionary(e => e.Key, e => e.Value), - }; - var aps = this.Aps; - if (aps == null && copy.CustomData?.ContainsKey("aps") == false) + [JsonProperty("aps")] + internal Aps Aps { get; set; } + + [JsonExtensionData] + internal IDictionary CustomData { get; set; } + + /// + /// Copies this APNs payload, and validates the content of it to ensure that it can be + /// serialized into the JSON format expected by the FCM service. + /// + internal ApnsPayload CopyAndValidate() { - throw new ArgumentException("Aps dictionary is required in ApnsConfig"); + var copy = new ApnsPayload() + { + CustomData = this.CustomData?.ToDictionary(e => e.Key, e => e.Value), + }; + var aps = this.Aps; + if (aps == null && copy.CustomData?.ContainsKey("aps") == false) + { + throw new ArgumentException("Aps dictionary is required in ApnsConfig"); + } + + copy.Aps = aps?.CopyAndValidate(); + return copy; } - copy.Aps = aps?.CopyAndValidate(); - return copy; } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs index 7c64dae9..3b7bf69f 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Aps.cs @@ -29,20 +29,82 @@ public sealed class Aps private static readonly NewtonsoftJsonSerializer Serializer = NewtonsoftJsonSerializer.Instance; /// - /// An advanced alert configuration to be included in the message. It is an error to set - /// both and properties together. + /// Gets or sets an advanced alert configuration to be included in the message. It is an + /// error to set both and properties + /// together. /// [JsonIgnore] public ApsAlert Alert { get; set; } /// - /// The alert text to be included in the message. To specify a more advanced alert - /// configuration, use the property instead. It is an error to set - /// both and properties together. + /// Gets or sets the alert text to be included in the message. To specify a more advanced + /// alert configuration, use the property instead. It is an error to + /// set both and properties together. /// [JsonIgnore] public string AlertString { get; set; } + /// + /// Gets or sets the badge to be displayed with the message. Set to 0 to remove the badge. + /// When not specified, the badge will remain unchanged. + /// + [JsonProperty("badge")] + public int? Badge { get; set; } + + /// + /// Gets or sets the name of a sound file in your app's main bundle or in the + /// Library/Sounds folder of your app's container directory. Specify the + /// string default to play the system sound. It is an error to set both + /// and properties together. + /// + [JsonIgnore] + public string Sound { get; set; } + + /// + /// Gets or sets the critical alert sound to be played with the message. It is an error to + /// set both and properties together. + /// + [JsonIgnore] + public CriticalSound CriticalSound { get; set; } + + /// + /// Gets or sets a value indicating whether to configure a background update notification. + /// + [JsonIgnore] + public bool ContentAvailable { get; set; } + + /// + /// Gets or sets a value indicating whether to include the mutable-content property + /// in the message. When set, this property allows clients to modify the notification via + /// app extensions. + /// + [JsonIgnore] + public bool MutableContent { get; set; } + + /// + /// Gets or sets the type of the notification. + /// + [JsonProperty("category")] + public string Category { get; set; } + + /// + /// Gets or sets the app-specific identifier for grouping notifications. + /// + [JsonProperty("thread-id")] + public string ThreadId { get; set; } + + /// + /// Gets or sets a collection of arbitrary key-value data to be included in the aps + /// dictionary. This is exposed as an to support + /// correct deserialization of custom properties. + /// + [JsonExtensionData] + public IDictionary CustomData { get; set; } + + /// + /// Gets or sets the alert configuration of the aps dictionary. Read from either + /// or property. + /// [JsonProperty("alert")] private object AlertObject { @@ -58,8 +120,10 @@ private object AlertObject throw new ArgumentException( "Multiple specifications for alert (Alert and AlertString"); } + return alert; } + set { if (value == null) @@ -68,43 +132,24 @@ private object AlertObject } else if (value.GetType() == typeof(string)) { - AlertString = (string) value; + this.AlertString = (string)value; } else if (value.GetType() == typeof(ApsAlert)) { - Alert = (ApsAlert) value; + this.Alert = (ApsAlert)value; } else { var json = Serializer.Serialize(value); - Alert = Serializer.Deserialize(json); + this.Alert = Serializer.Deserialize(json); } } } /// - /// The badge to be displayed with the message. Set to 0 to remove the badge. When not - /// specified, the badge will remain unchanged. + /// Gets or sets the sound configuration of the aps dictionary. Read from either + /// or property. /// - [JsonProperty("badge")] - public int? Badge { get; set; } - - /// - /// The name of a sound file in your app's main bundle or in the - /// Library/Sounds folder of your app's container directory. Specify the - /// string default to play the system sound. It is an error to set both - /// and properties together. - /// - [JsonIgnore] - public string Sound { get; set; } - - /// - /// The critical alert sound to be played with the message. It is an error to set both - /// and properties together. - /// - [JsonIgnore] - public CriticalSound CriticalSound { get; set; } - [JsonProperty("sound")] private object SoundObject { @@ -120,8 +165,10 @@ private object SoundObject throw new ArgumentException( "Multiple specifications for sound (CriticalSound and Sound"); } + return sound; } + set { if (value == null) @@ -130,87 +177,56 @@ private object SoundObject } else if (value.GetType() == typeof(string)) { - Sound = (string) value; + this.Sound = (string)value; } else if (value.GetType() == typeof(CriticalSound)) { - CriticalSound = (CriticalSound) value; + this.CriticalSound = (CriticalSound)value; } else { var json = Serializer.Serialize(value); - CriticalSound = Serializer.Deserialize(json); + this.CriticalSound = Serializer.Deserialize(json); } } } /// - /// Specifies whether to configure a background update notification. - /// - [JsonIgnore] - public bool ContentAvailable { get; set; } - - /// - /// Integer representation of the property, which is how - /// APNs expects it. + /// Gets or sets the integer representation of the property, + /// which is how APNs expects it. /// [JsonProperty("content-available")] private int? ContentAvailableInt { get { - return ContentAvailable ? 1 : (int?) null; + return this.ContentAvailable ? 1 : (int?)null; } + set { - ContentAvailable = (value == 1); + this.ContentAvailable = value == 1; } } /// - /// Specifies whether to set the mutable-content property on the message. When - /// set, this property allows clients to modify the notification via app extensions. - /// - [JsonIgnore] - public bool MutableContent { get; set; } - - /// - /// Integer representation of the property, which is how - /// APNs expects it. + /// Gets or sets the integer representation of the property, + /// which is how APNs expects it. /// [JsonProperty("mutable-content")] private int? MutableContentInt { get { - return MutableContent ? 1 : (int?) null; + return this.MutableContent ? 1 : (int?)null; } + set { - MutableContent = (value == 1); + this.MutableContent = value == 1; } } - /// - /// The type of the notification. - /// - [JsonProperty("category")] - public string Category { get; set; } - - /// - /// An app-specific identifier for grouping notifications. - /// - [JsonProperty("thread-id")] - public string ThreadId { get; set; } - - /// - /// A collection of arbitrary key-value data to be included in the aps - /// dictionary. This is exposed as an to support - /// correct deserialization of custom properties. - /// - [JsonExtensionData] - public IDictionary CustomData { get; set; } - /// /// Copies this Aps dictionary, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM and APNs services. @@ -242,6 +258,7 @@ internal Aps CopyAndValidate() throw new ArgumentException( $"Multiple specifications for Aps keys: {string.Join(",", duplicates)}"); } + copy.CustomData = customData; } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs index c48c6012..17c09472 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ApsAlert.cs @@ -21,82 +21,82 @@ namespace FirebaseAdmin.Messaging { /// /// Represents the - /// alert property that can be included in the aps dictionary of an APNs + /// alert property that can be included in the aps dictionary of an APNs /// payload. /// public sealed class ApsAlert { /// - /// The title of the alert. When provided, overrides the title set via + /// Gets or sets the title of the alert. When provided, overrides the title set via /// . /// [JsonProperty("title")] public string Title { get; set; } /// - /// The subtitle of the alert. + /// Gets or sets the subtitle of the alert. /// [JsonProperty("subtitle")] public string Subtitle { get; set; } /// - /// The body of the alert. When provided, overrides the body set via + /// Gets or sets the body of the alert. When provided, overrides the body set via /// . /// [JsonProperty("body")] public string Body { get; set; } /// - /// The key of the body string in the app's string resources to use to localize the body - /// text. + /// Gets or sets the key of the body string in the app's string resources to use to + /// localize the body text. /// [JsonProperty("loc-key")] public string LocKey { get; set; } /// - /// Resource key strings that will be used in place of the format specifiers in - /// . + /// Gets or sets the resource key strings that will be used in place of the format + /// specifiers in . /// [JsonProperty("loc-args")] public IEnumerable LocArgs { get; set; } /// - /// The key of the title string in the app's string resources to use to localize the title - /// text. + /// Gets or sets the key of the title string in the app's string resources to use to + /// localize the title text. /// [JsonProperty("title-loc-key")] public string TitleLocKey { get; set; } /// - /// Resource key strings that will be used in place of the format specifiers in - /// . + /// Gets or sets the resource key strings that will be used in place of the format + /// specifiers in . /// [JsonProperty("title-loc-args")] public IEnumerable TitleLocArgs { get; set; } /// - /// The key of the subtitle string in the app's string resources to use to localize the - /// subtitle text. + /// Gets or sets the key of the subtitle string in the app's string resources to use to + /// localize the subtitle text. /// [JsonProperty("subtitle-loc-key")] public string SubtitleLocKey { get; set; } /// - /// Resource key strings that will be used in place of the format specifiers in - /// . + /// Gets or sets the resource key strings that will be used in place of the format + /// specifiers in . /// [JsonProperty("subtitle-loc-args")] public IEnumerable SubtitleLocArgs { get; set; } /// - /// The key of the text in the app's string resources to use to localize the action button - /// text. + /// Gets or sets the key of the text in the app's string resources to use to localize the + /// action button text. /// [JsonProperty("action-loc-key")] public string ActionLocKey { get; set; } /// - /// The launch image for the notification action. + /// Gets or sets the launch image for the notification action. /// [JsonProperty("launch-image")] public string LaunchImage { get; set; } @@ -125,14 +125,17 @@ internal ApsAlert CopyAndValidate() { throw new ArgumentException("TitleLocKey is required when specifying TitleLocArgs."); } + if (copy.SubtitleLocArgs?.Any() == true && string.IsNullOrEmpty(copy.SubtitleLocKey)) { throw new ArgumentException("SubtitleLocKey is required when specifying SubtitleLocArgs."); } + if (copy.LocArgs?.Any() == true && string.IsNullOrEmpty(copy.LocKey)) { throw new ArgumentException("LocKey is required when specifying LocArgs."); } + return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs index de59f1d0..11555fec 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/CriticalSound.cs @@ -24,47 +24,50 @@ namespace FirebaseAdmin.Messaging public sealed class CriticalSound { /// - /// Sets the critical alert flag on the sound configuration. + /// Gets or sets a value indicating whether to set the critical alert flag on the sound + /// configuration. /// [JsonIgnore] public bool Critical { get; set; } /// - /// Integer representation of the property, which is how - /// APNs expects it. + /// Gets or sets the name of the sound to be played. This should be a sound file in your + /// app's main bundle or in the Library/Sounds folder of your app's container + /// directory. Specify the string default to play the system sound. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets the volume for the critical alert's sound. Must be a value between 0.0 + /// (silent) and 1.0 (full volume). + /// + [JsonProperty("volume")] + public double? Volume { get; set; } + + /// + /// Gets or sets the integer representation of the property, which + /// is how APNs expects it. /// [JsonProperty("critical")] private int? CriticalInt { get { - if (Critical) + if (this.Critical) { return 1; } + return null; } + set { - Critical = (value == 1); + this.Critical = value == 1; } } - /// - /// The name of a sound file in your app's main bundle or in the - /// Library/Sounds folder of your app's container directory. Specify the - /// string default to play the system sound. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// The volume for the critical alert's sound. Must be a value between 0.0 (silent) and - /// 1.0 (full volume). - /// - [JsonProperty("volume")] - public double? Volume { get; set; } - /// /// Copies this critical sound configuration, and validates the content of it to ensure /// that it can be serialized into the JSON format expected by the FCM and APNs services. @@ -81,10 +84,12 @@ internal CriticalSound CopyAndValidate() { throw new ArgumentException("Name must be specified for CriticalSound"); } + if (copy.Volume < 0 || copy.Volume > 1) { throw new ArgumentException("Volume of CriticalSound must be in the interval [0, 1]"); } + return copy; } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Direction.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Direction.cs new file mode 100644 index 00000000..ce95dda5 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Direction.cs @@ -0,0 +1,37 @@ +// 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. + +namespace FirebaseAdmin.Messaging +{ + /// + /// Different directions a notification can be displayed in. + /// + public enum Direction + { + /// + /// Direction automatically determined. + /// + Auto, + + /// + /// Left to right. + /// + LeftToRight, + + /// + /// Right to left. + /// + RightToLeft, + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index 612b1f75..9f46cad0 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -23,16 +23,54 @@ namespace FirebaseAdmin.Messaging /// This is the entry point to all server-side Firebase Cloud Messaging (FCM) operations. You /// can get an instance of this class via FirebaseMessaging.DefaultInstance. /// - public sealed class FirebaseMessaging: IFirebaseService + public sealed class FirebaseMessaging : IFirebaseService { - private readonly FirebaseMessagingClient _messagingClient; + private readonly FirebaseMessagingClient messagingClient; private FirebaseMessaging(FirebaseApp app) { - _messagingClient = new FirebaseMessagingClient( + this.messagingClient = new FirebaseMessagingClient( new HttpClientFactory(), app.Options.Credential, app.GetProjectId()); } + /// + /// Gets the messaging instance associated with the default Firebase app. This property is + /// null if the default app doesn't yet exist. + /// + public static FirebaseMessaging DefaultInstance + { + get + { + var app = FirebaseApp.DefaultInstance; + if (app == null) + { + return null; + } + + return GetMessaging(app); + } + } + + /// + /// Returns the messaging instance for the specified app. + /// + /// The instance associated with the specified + /// app. + /// If the app argument is null. + /// An app instance. + public static FirebaseMessaging GetMessaging(FirebaseApp app) + { + if (app == null) + { + throw new ArgumentNullException("App argument must not be null."); + } + + return app.GetOrInit(typeof(FirebaseMessaging).Name, () => + { + return new FirebaseMessaging(app); + }); + } + /// /// Sends a message to the FCM service for delivery. The message gets validated both by /// the Admin SDK, and the remote FCM service. A successful return value indicates @@ -49,7 +87,7 @@ private FirebaseMessaging(FirebaseApp app) /// The message to be sent. Must not be null. public async Task SendAsync(Message message) { - return await SendAsync(message, false); + return await this.SendAsync(message, false); } /// @@ -70,7 +108,7 @@ public async Task SendAsync(Message message) /// operation. public async Task SendAsync(Message message, CancellationToken cancellationToken) { - return await SendAsync(message, false, cancellationToken); + return await this.SendAsync(message, false, cancellationToken); } /// @@ -96,7 +134,7 @@ public async Task SendAsync(Message message, CancellationToken cancellat /// but it will not be delivered to any actual recipients. public async Task SendAsync(Message message, bool dryRun) { - return await SendAsync(message, dryRun, default(CancellationToken)); + return await this.SendAsync(message, dryRun, default(CancellationToken)); } /// @@ -125,49 +163,16 @@ public async Task SendAsync(Message message, bool dryRun) public async Task SendAsync( Message message, bool dryRun, CancellationToken cancellationToken) { - return await _messagingClient.SendAsync( + return await this.messagingClient.SendAsync( message, dryRun, cancellationToken).ConfigureAwait(false); } - void IFirebaseService.Delete() - { - _messagingClient.Dispose(); - } - /// - /// The messaging instance associated with the default Firebase app. This property is - /// null if the default app doesn't yet exist. + /// Deletes this service instance. /// - public static FirebaseMessaging DefaultInstance - { - get - { - var app = FirebaseApp.DefaultInstance; - if (app == null) - { - return null; - } - return GetMessaging(app); - } - } - - /// - /// Returns the messaging instance for the specified app. - /// - /// The instance associated with the specified - /// app. - /// If the app argument is null. - /// An app instance. - public static FirebaseMessaging GetMessaging(FirebaseApp app) + void IFirebaseService.Delete() { - if (app == null) - { - throw new ArgumentNullException("App argument must not be null."); - } - return app.GetOrInit(typeof(FirebaseMessaging).Name, () => - { - return new FirebaseMessaging(app); - }); + this.messagingClient.Dispose(); } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs index 001327fa..858c3170 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs @@ -27,14 +27,14 @@ namespace FirebaseAdmin.Messaging /// A client for making authorized HTTP calls to the FCM backend service. Handles request /// serialization, response parsing, and HTTP error handling. /// - internal sealed class FirebaseMessagingClient: IDisposable + internal sealed class FirebaseMessagingClient : IDisposable { private const string FcmUrl = "https://fcm.googleapis.com/v1/projects/{0}/messages:send"; - private readonly ConfigurableHttpClient _httpClient; - private readonly string _sendUrl; + private readonly ConfigurableHttpClient httpClient; + private readonly string sendUrl; - internal FirebaseMessagingClient( + public FirebaseMessagingClient( HttpClientFactory clientFactory, GoogleCredential credential, string projectId) { if (string.IsNullOrEmpty(projectId)) @@ -45,9 +45,10 @@ internal FirebaseMessagingClient( + "you can set the project ID via the GOOGLE_CLOUD_PROJECT environment " + "variable."); } - _httpClient = clientFactory.ThrowIfNull(nameof(clientFactory)) + + this.httpClient = clientFactory.ThrowIfNull(nameof(clientFactory)) .CreateAuthorizedHttpClient(credential); - _sendUrl = string.Format(FcmUrl, projectId); + this.sendUrl = string.Format(FcmUrl, projectId); } /// @@ -69,8 +70,10 @@ internal FirebaseMessagingClient( /// but it will not be delivered to any actual recipients. /// A cancellation token to monitor the asynchronous /// operation. - internal async Task SendAsync(Message message, - bool dryRun = false, CancellationToken cancellationToken = default(CancellationToken)) + public async Task SendAsync( + Message message, + bool dryRun = false, + CancellationToken cancellationToken = default(CancellationToken)) { var request = new SendRequest() { @@ -79,16 +82,17 @@ internal async Task SendAsync(Message message, }; try { - var response = await _httpClient.PostJsonAsync( - _sendUrl, request, cancellationToken).ConfigureAwait(false); + var response = await this.httpClient.PostJsonAsync( + this.sendUrl, request, cancellationToken).ConfigureAwait(false); var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (!response.IsSuccessStatusCode) { var error = "Response status code does not indicate success: " - + $"{(int) response.StatusCode} ({response.StatusCode})" + + $"{(int)response.StatusCode} ({response.StatusCode})" + $"{Environment.NewLine}{json}"; - throw new FirebaseException(error); + throw new FirebaseException(error); } + var parsed = JsonConvert.DeserializeObject(json); return parsed.Name; } @@ -100,30 +104,30 @@ internal async Task SendAsync(Message message, public void Dispose() { - _httpClient.Dispose(); + this.httpClient.Dispose(); } - } - /// - /// Represents the envelope message accepted by the FCM backend service, including the message - /// payload and other options like validate_only. - /// - internal sealed class SendRequest - { - [Newtonsoft.Json.JsonProperty("message")] - public Message Message { get; set; } + /// + /// Represents the envelope message accepted by the FCM backend service, including the message + /// payload and other options like validate_only. + /// + internal class SendRequest + { + [Newtonsoft.Json.JsonProperty("message")] + public Message Message { get; set; } - [Newtonsoft.Json.JsonProperty("validate_only")] - public bool ValidateOnly { get; set; } - } + [Newtonsoft.Json.JsonProperty("validate_only")] + public bool ValidateOnly { get; set; } + } - /// - /// Represents the response messages sent by the FCM backend service. Primarily consists of the - /// message ID (Name) that indicates success handoff to FCM. - /// - internal sealed class SendResponse - { - [Newtonsoft.Json.JsonProperty("name")] - public string Name { get; set; } + /// + /// Represents the response messages sent by the FCM backend service. Primarily consists of the + /// message ID (Name) that indicates success handoff to FCM. + /// + internal class SendResponse + { + [Newtonsoft.Json.JsonProperty("name")] + public string Name { get; set; } + } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 64c226c8..21c2d254 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -15,9 +15,9 @@ using System; using System.Collections.Generic; using System.Text.RegularExpressions; -using Newtonsoft.Json; using Google.Apis.Json; using Google.Apis.Util; +using Newtonsoft.Json; namespace FirebaseAdmin.Messaging { @@ -30,77 +30,80 @@ namespace FirebaseAdmin.Messaging public sealed class Message { /// - /// The registration token of the device to which the message should be sent. + /// Gets or sets the registration token of the device to which the message should be sent. /// [JsonProperty("token")] public string Token { get; set; } /// - /// The name of the FCM topic to which the message should be sent. Topic names may - /// contain the /topics/ prefix. + /// Gets or sets the name of the FCM topic to which the message should be sent. Topic names + /// may contain the /topics/ prefix. /// [JsonIgnore] public string Topic { get; set; } /// - /// Formatted representation of the . Removes the /topics/ - /// prefix if present. This is what's ultimately sent to the FCM service. - /// - [JsonProperty("topic")] - private string UnprefixedTopic - { - get - { - if (Topic != null && Topic.StartsWith("/topics/")) - { - return Topic.Substring("/topics/".Length); - } - return Topic; - } - set - { - Topic = value; - } - } - - /// - /// The FCM condition to which the message should be sent. Must be a valid condition - /// string such as "'foo' in topics". + /// Gets or sets the FCM condition to which the message should be sent. Must be a valid + /// condition string such as "'foo' in topics". /// [JsonProperty("condition")] public string Condition { get; set; } /// - /// A collection of key-value pairs that will be added to the message as data fields. Keys - /// and the values must not be null. + /// Gets or sets a collection of key-value pairs that will be added to the message as data + /// fields. Keys and the values must not be null. /// [JsonProperty("data")] public IReadOnlyDictionary Data { get; set; } /// - /// The notification information to be included in the message. + /// Gets or sets the notification information to be included in the message. /// [JsonProperty("notification")] public Notification Notification { get; set; } /// - /// The Android-specific information to be included in the message. + /// Gets or sets the Android-specific information to be included in the message. /// [JsonProperty("android")] public AndroidConfig Android { get; set; } /// - /// The Webpush-specific information to be included in the message. + /// Gets or sets the Webpush-specific information to be included in the message. /// [JsonProperty("webpush")] public WebpushConfig Webpush { get; set; } /// - /// The APNs-specific information to be included in the message. + /// Gets or sets the APNs-specific information to be included in the message. /// [JsonProperty("apns")] public ApnsConfig Apns { get; set; } + /// + /// Gets or sets the formatted representation of the . Removes the + /// /topics/ prefix if present. This is what's ultimately sent to the FCM + /// service. + /// + [JsonProperty("topic")] + private string UnprefixedTopic + { + get + { + if (this.Topic != null && this.Topic.StartsWith("/topics/")) + { + return this.Topic.Substring("/topics/".Length); + } + + return this.Topic; + } + + set + { + this.Topic = value; + } + } + /// /// Copies this message, and validates the content of it to ensure that it can be /// serialized into the JSON format expected by the FCM service. Each property is copied @@ -127,6 +130,7 @@ internal Message CopyAndValidate() throw new ArgumentException( "Exactly one of Token, Topic or Condition is required."); } + var topic = copy.UnprefixedTopic; if (topic != null && !Regex.IsMatch(topic, "^[a-zA-Z0-9-_.~%]+$")) { diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs index 431b3785..017ee08d 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Notification.cs @@ -22,13 +22,13 @@ namespace FirebaseAdmin.Messaging public sealed class Notification { /// - /// Title of the notification. + /// Gets or sets the title of the notification. /// [JsonProperty("title")] public string Title { get; set; } /// - /// Body of the notification. + /// Gets or sets the body of the notification. /// [JsonProperty("body")] public string Body { get; set; } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Priority.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Priority.cs new file mode 100644 index 00000000..e28b8587 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Priority.cs @@ -0,0 +1,32 @@ +// 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. + +namespace FirebaseAdmin.Messaging +{ + /// + /// Priority levels that can be set on an . + /// + public enum Priority + { + /// + /// High priority message. + /// + High, + + /// + /// Normal priority message. + /// + Normal, + } +} \ No newline at end of file diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs index 4900b1b4..4343c9ec 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushConfig.cs @@ -24,21 +24,22 @@ namespace FirebaseAdmin.Messaging public sealed class WebpushConfig { /// - /// Webpush HTTP headers. Refer + /// Gets or sets the Webpush HTTP headers. Refer + /// /// Webpush specification for supported headers. /// [JsonProperty("headers")] public IReadOnlyDictionary Headers { get; set; } /// - /// Webpush data fields. When set, overrides any data fields set via + /// Gets or sets the Webpush data fields. When set, overrides any data fields set via /// . /// [JsonProperty("data")] public IReadOnlyDictionary Data { get; set; } /// - /// The Webpush notification to be included in the message. + /// Gets or sets the Webpush notification that will be included in the message. /// [JsonProperty("notification")] public WebpushNotification Notification { get; set; } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs index 9c9b1f81..d41ed3a8 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/WebpushNotification.cs @@ -24,153 +24,153 @@ namespace FirebaseAdmin.Messaging /// Represents the Webpush-specific notification options that can be included in a /// . Supports most standard options defined in the /// - /// Web Notification specification + /// Web Notification specification. /// public sealed class WebpushNotification { /// - /// Title text of the notification. + /// Gets or sets the title text of the notification. /// [JsonProperty("title")] public string Title { get; set; } /// - /// Body text of the notification. + /// Gets or sets the body text of the notification. /// [JsonProperty("body")] public string Body { get; set; } /// - /// The URL to the icon of the notification. + /// Gets or sets the URL to the icon of the notification. /// [JsonProperty("icon")] public string Icon { get; set; } /// - /// The URL of the image used to represent the notification when there is not enough space - /// to display the notification itself. + /// Gets or sets the URL of the image used to represent the notification when there is not + /// enough space to display the notification itself. /// [JsonProperty("badge")] public string Badge { get; set; } /// - /// Any arbitrary data that should be associated with the notification. + /// Gets or sets some arbitrary data that will be included in the notification. /// [JsonProperty("data")] public object Data { get; set; } /// - /// The direction in which to display the notification. + /// Gets or sets the direction in which to display the notification. /// [JsonIgnore] public Direction? Direction { get; set; } /// - /// Converts the property into a string value that can be included - /// in the json output. - /// - [JsonProperty("dir")] - private string DirectionString - { - get - { - switch (Direction) - { - case Messaging.Direction.Auto: - return "auto"; - case Messaging.Direction.LeftToRight: - return "ltr"; - case Messaging.Direction.RightToLeft: - return "rtl"; - default: - return null; - } - } - set - { - switch (value) - { - case "auto": - Direction = Messaging.Direction.Auto; - return; - case "ltr": - Direction = Messaging.Direction.LeftToRight; - return; - case "rtl": - Direction = Messaging.Direction.RightToLeft; - return; - default: - throw new FirebaseException( - $"Invalid direction value: {value}. Only 'auto', 'rtl' and 'ltr' " - + "are allowed."); - } - } - } - - /// - /// The URL of an image to be displayed in the notification. + /// Gets or sets the URL of an image to be displayed in the notification. /// [JsonProperty("image")] public string Image { get; set; } /// - /// The language of the notification. + /// Gets or sets the language of the notification. /// [JsonProperty("lang")] public string Language { get; set; } /// - /// Whether the user should be notified after a new notification replaces an old one. + /// Gets or sets whether the user should be notified after a new notification replaces an + /// old one. /// [JsonProperty("renotify")] public bool? Renotify { get; set; } /// - /// Whether a notification should remain active until the user clicks or dismisses it, - /// rather than closing automatically. + /// Gets or sets whether the notification should remain active until the user clicks or + /// dismisses it, rather than closing it automatically. /// [JsonProperty("requireInteraction")] public bool? RequireInteraction { get; set; } /// - /// Whether the notification should be silent. + /// Gets or sets whether the notification should be silent. /// [JsonProperty("silent")] public bool? Silent { get; set; } /// - /// An identifying tag for the notification. + /// Gets or sets an identifying tag for the notification. /// [JsonProperty("tag")] public string Tag { get; set; } /// - /// A timestamp value in milliseconds on the notification. + /// Gets or sets the notification's timestamp value in milliseconds. /// [JsonProperty("timestamp")] public long? TimestampMillis { get; set; } /// - /// A vibration pattern for the receiving device's vibration hardware to emit when the - /// notification fires. + /// Gets or sets a vibration pattern for the receiving device's vibration hardware. /// [JsonProperty("vibrate")] public int[] Vibrate { get; set; } /// - /// A collection of notification actions to be associated with the notification. + /// Gets or sets a collection of Webpush notification actions. /// [JsonProperty("actions")] - public IEnumerable Actions; + public IEnumerable Actions { get; set; } /// - /// A collection of arbitrary key-value data to be included in the notification. This is - /// exposed as an to support correct - /// deserialization of custom properties. + /// Gets or sets the custom key-value pairs that will be included in the + /// notification. This is exposed as an to support + /// correct deserialization of custom properties. /// [JsonExtensionData] public IDictionary CustomData { get; set; } + /// + /// Gets or sets the string representation of the property. + /// + [JsonProperty("dir")] + private string DirectionString + { + get + { + switch (this.Direction) + { + case Messaging.Direction.Auto: + return "auto"; + case Messaging.Direction.LeftToRight: + return "ltr"; + case Messaging.Direction.RightToLeft: + return "rtl"; + default: + return null; + } + } + + set + { + switch (value) + { + case "auto": + this.Direction = Messaging.Direction.Auto; + return; + case "ltr": + this.Direction = Messaging.Direction.LeftToRight; + return; + case "rtl": + this.Direction = Messaging.Direction.RightToLeft; + return; + default: + throw new FirebaseException( + $"Invalid direction value: {value}. Only 'auto', 'rtl' and 'ltr' " + + "are allowed."); + } + } + } + /// /// Copies this Webpush notification, and validates the content of it to ensure that it can /// be serialized into the JSON format expected by the FCM service. @@ -211,66 +211,11 @@ internal WebpushNotification CopyAndValidate() "Multiple specifications for WebpushNotification keys: " + string.Join(",", duplicates)); } + copy.CustomData = customData; } - return copy; - } - } - /// - /// Represents an action available to users when the notification is presented. - /// - public sealed class Action - { - /// - /// Action name. - /// - [JsonProperty("action")] - public string ActionName { get; set; } - - /// - /// Title text. - /// - [JsonProperty("title")] - public string Title { get; set; } - - /// - /// Icon URL. - /// - [JsonProperty("icon")] - public string Icon { get; set; } - - /// - /// Creates a new Action instance. - /// - public Action() { } - - internal Action(Action action) - { - ActionName = action.ActionName; - Title = action.Title; - Icon = action.Icon; + return copy; } } - - /// - /// Different directions a notification can be displayed in. - /// - public enum Direction - { - /// - /// Direction automatically determined. - /// - Auto, - - /// - /// Left to right. - /// - LeftToRight, - - /// - /// Right to left. - /// - RightToLeft, - } } From b4041ff5c76c1f3123a9c0b80dd4de27d7fed2ac Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 25 Jan 2019 15:55:33 -0800 Subject: [PATCH 49/50] Fixed all lint errors --- ...Test.cs => FirebaseMessagingClientTest.cs} | 22 +- .../Messaging/FirebaseMessagingTest.cs | 23 +- .../Messaging/MessageTest.cs | 363 +++++++++--------- 3 files changed, 204 insertions(+), 204 deletions(-) rename FirebaseAdmin/FirebaseAdmin.Tests/Messaging/{FirebaseMessageClientTest.cs => FirebaseMessagingClientTest.cs} (91%) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs similarity index 91% rename from FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs rename to FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs index 37eafe2c..50d459cf 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessageClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs @@ -16,17 +16,17 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; -using Xunit; +using FirebaseAdmin.Tests; using Google.Apis.Auth.OAuth2; using Google.Apis.Http; -using FirebaseAdmin.Tests; +using Newtonsoft.Json; +using Xunit; namespace FirebaseAdmin.Messaging.Tests { public class FirebaseMessagingClientTest { - private static readonly GoogleCredential mockCredential = + private static readonly GoogleCredential MockCredential = GoogleCredential.FromAccessToken("test-token"); [Fact] @@ -34,9 +34,9 @@ public void NoProjectId() { var clientFactory = new HttpClientFactory(); Assert.Throws( - () => new FirebaseMessagingClient(clientFactory, mockCredential, null)); + () => new FirebaseMessagingClient(clientFactory, MockCredential, null)); Assert.Throws( - () => new FirebaseMessagingClient(clientFactory, mockCredential, "")); + () => new FirebaseMessagingClient(clientFactory, MockCredential, string.Empty)); } [Fact] @@ -52,7 +52,7 @@ public void NoClientFactory() { var clientFactory = new HttpClientFactory(); Assert.Throws( - () => new FirebaseMessagingClient(null, mockCredential, "test-project")); + () => new FirebaseMessagingClient(null, MockCredential, "test-project")); } [Fact] @@ -66,10 +66,10 @@ public async Task SendAsync() }, }; var factory = new MockHttpClientFactory(handler); - var client = new FirebaseMessagingClient(factory, mockCredential, "test-project"); + var client = new FirebaseMessagingClient(factory, MockCredential, "test-project"); var message = new Message() { - Topic = "test-topic" + Topic = "test-topic", }; var response = await client.SendAsync(message); Assert.Equal("test-response", response); @@ -98,10 +98,10 @@ public async Task HttpErrorAsync() Response = "not json", }; var factory = new MockHttpClientFactory(handler); - var client = new FirebaseMessagingClient(factory, mockCredential, "test-project"); + var client = new FirebaseMessagingClient(factory, MockCredential, "test-project"); var message = new Message() { - Topic = "test-topic" + Topic = "test-topic", }; var ex = await Assert.ThrowsAsync( async () => await client.SendAsync(message)); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs index b345b2bb..363ac3c7 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs @@ -15,16 +15,15 @@ using System; using System.Threading; using System.Threading.Tasks; -using Xunit; -using Google.Apis.Auth.OAuth2; -using Google.Apis.Json; using FirebaseAdmin.Tests; +using Google.Apis.Auth.OAuth2; +using Xunit; namespace FirebaseAdmin.Messaging.Tests { - public class FirebaseMessagingTest: IDisposable + public class FirebaseMessagingTest : IDisposable { - private static readonly GoogleCredential mockCredential = + private static readonly GoogleCredential MockCredential = GoogleCredential.FromFile("./resources/service_account.json"); [Fact] @@ -36,7 +35,7 @@ public void GetMessagingWithoutApp() [Fact] public void GetDefaultMessaging() { - var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential}); + var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential }); FirebaseMessaging messaging = FirebaseMessaging.DefaultInstance; Assert.NotNull(messaging); Assert.Same(messaging, FirebaseMessaging.DefaultInstance); @@ -47,7 +46,7 @@ public void GetDefaultMessaging() [Fact] public void GetMessaging() { - var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential}, "MyApp"); + var app = FirebaseApp.Create(new AppOptions() { Credential = MockCredential }, "MyApp"); FirebaseMessaging messaging = FirebaseMessaging.GetMessaging(app); Assert.NotNull(messaging); Assert.Same(messaging, FirebaseMessaging.GetMessaging(app)); @@ -58,28 +57,28 @@ public void GetMessaging() [Fact] public async Task UseAfterDelete() { - var app = FirebaseApp.Create(new AppOptions(){Credential = mockCredential}); + 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"})); + 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}); + 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)); + 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 index 3deb2363..eaee0d05 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -15,10 +15,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json.Linq; -using Xunit; using Google.Apis.Json; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; namespace FirebaseAdmin.Messaging.Tests { @@ -28,20 +28,20 @@ public class MessageTest public void EmptyMessage() { var message = new Message() { Token = "test-token" }; - AssertJsonEquals(new JObject() { { "token", "test-token" } }, message); + this.AssertJsonEquals(new JObject() { { "token", "test-token" } }, message); message = new Message() { Topic = "test-topic" }; - AssertJsonEquals(new JObject() { { "topic", "test-topic" } }, message); + this.AssertJsonEquals(new JObject() { { "topic", "test-topic" } }, message); message = new Message() { Condition = "test-condition" }; - AssertJsonEquals(new JObject() { { "condition", "test-condition" } }, message); + this.AssertJsonEquals(new JObject() { { "condition", "test-condition" } }, message); } [Fact] public void PrefixedTopicName() { var message = new Message() { Topic = "/topics/test-topic" }; - AssertJsonEquals(new JObject(){{"topic", "test-topic"}}, message); + this.AssertJsonEquals(new JObject() { { "topic", "test-topic" } }, message); } [Fact] @@ -56,10 +56,11 @@ public void DataMessage() { "k2", "v2" }, }, }; - AssertJsonEquals(new JObject() + this.AssertJsonEquals( + new JObject() { - {"topic", "test-topic"}, - {"data", new JObject(){{"k1", "v1"}, {"k2", "v2"}}}, + { "topic", "test-topic" }, + { "data", new JObject() { { "k1", "v1" }, { "k2", "v2" } } }, }, message); } @@ -77,16 +78,16 @@ public void Notification() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "notification", new JObject() { - {"title", "title"}, - {"body", "body"}, + { "title", "title" }, + { "body", "body" }, } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -95,7 +96,7 @@ public void MessageDeserialization() var original = new Message() { Topic = "test-topic", - Data = new Dictionary(){{ "key", "value" }}, + Data = new Dictionary() { { "key", "value" } }, Notification = new Notification() { Title = "title", @@ -114,7 +115,7 @@ public void MessageDeserialization() }, Webpush = new WebpushConfig() { - Data = new Dictionary(){{ "key", "value" }}, + Data = new Dictionary() { { "key", "value" } }, }, }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); @@ -198,7 +199,7 @@ public void InvalidTopicNames() }; foreach (var topic in topics) { - var message = new Message(){Topic = topic}; + var message = new Message() { Topic = topic }; Assert.Throws(() => message.CopyAndValidate()); } } @@ -230,16 +231,16 @@ public void AndroidConfig() Tag = "tag", ClickAction = "click-action", TitleLocKey = "title-loc-key", - TitleLocArgs = new List(){ "arg1", "arg2" }, + TitleLocArgs = new List() { "arg1", "arg2" }, BodyLocKey = "body-loc-key", - BodyLocArgs = new List(){ "arg3", "arg4" }, + BodyLocArgs = new List() { "arg3", "arg4" }, ChannelId = "channel-id", }, }, }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "android", new JObject() { @@ -247,7 +248,7 @@ public void AndroidConfig() { "priority", "high" }, { "ttl", "0.010000000s" }, { "restricted_package_name", "test-pkg-name" }, - { "data", new JObject(){{"k1", "v1"}, {"k2", "v2"}} }, + { "data", new JObject() { { "k1", "v1" }, { "k2", "v2" } } }, { "notification", new JObject() { @@ -259,16 +260,16 @@ public void AndroidConfig() { "tag", "tag" }, { "click_action", "click-action" }, { "title_loc_key", "title-loc-key" }, - { "title_loc_args", new JArray(){"arg1", "arg2"} }, + { "title_loc_args", new JArray() { "arg1", "arg2" } }, { "body_loc_key", "body-loc-key" }, - { "body_loc_args", new JArray(){"arg3", "arg4"} }, + { "body_loc_args", new JArray() { "arg3", "arg4" } }, { "channel_id", "channel-id" }, } }, } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -281,10 +282,10 @@ public void AndroidConfigMinimal() }; var expected = new JObject() { - {"topic", "test-topic"}, - {"android", new JObject()}, + { "topic", "test-topic" }, + { "android", new JObject() }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -300,7 +301,7 @@ public void AndroidConfigFullSecondsTTL() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "android", new JObject() { @@ -308,7 +309,7 @@ public void AndroidConfigFullSecondsTTL() } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -366,9 +367,9 @@ public void AndroidNotificationDeserialization() Tag = "tag", ClickAction = "click-action", TitleLocKey = "title-loc-key", - TitleLocArgs = new List(){ "arg1", "arg2" }, + TitleLocArgs = new List() { "arg1", "arg2" }, BodyLocKey = "body-loc-key", - BodyLocArgs = new List(){ "arg3", "arg4" }, + BodyLocArgs = new List() { "arg3", "arg4" }, ChannelId = "channel-id", }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); @@ -393,9 +394,9 @@ public void AndroidNotificationCopy() var original = new AndroidNotification() { TitleLocKey = "title-loc-key", - TitleLocArgs = new List(){ "arg1", "arg2" }, + TitleLocArgs = new List() { "arg1", "arg2" }, BodyLocKey = "body-loc-key", - BodyLocArgs = new List(){ "arg3", "arg4" }, + BodyLocArgs = new List() { "arg3", "arg4" }, }; var copy = original.CopyAndValidate(); Assert.NotSame(original, copy); @@ -403,7 +404,6 @@ public void AndroidNotificationCopy() Assert.NotSame(original.BodyLocArgs, copy.BodyLocArgs); } - [Fact] public void AndroidConfigInvalidTTL() { @@ -428,7 +428,7 @@ public void AndroidNotificationInvalidColor() { Notification = new AndroidNotification() { - Color = "not-a-color" + Color = "not-a-color", }, }, }; @@ -445,7 +445,7 @@ public void AndroidNotificationInvalidTitleLocArgs() { Notification = new AndroidNotification() { - TitleLocArgs = new List(){"arg"}, + TitleLocArgs = new List() { "arg" }, }, }, }; @@ -462,7 +462,7 @@ public void AndroidNotificationInvalidBodyLocArgs() { Notification = new AndroidNotification() { - BodyLocArgs = new List(){"arg"}, + BodyLocArgs = new List() { "arg" }, }, }, }; @@ -479,13 +479,13 @@ public void WebpushConfig() { Headers = new Dictionary() { - {"header1", "header-value1"}, - {"header2", "header-value2"}, + { "header1", "header-value1" }, + { "header2", "header-value2" }, }, Data = new Dictionary() { - {"key1", "value1"}, - {"key2", "value2"}, + { "key1", "value1" }, + { "key2", "value2" }, }, Notification = new WebpushNotification() { @@ -495,7 +495,7 @@ public void WebpushConfig() Badge = "badge", Data = new Dictionary() { - {"some", "data"}, + { "some", "data" }, }, Direction = Direction.LeftToRight, Image = "image", @@ -505,7 +505,7 @@ public void WebpushConfig() RequireInteraction = true, Renotify = true, TimestampMillis = 100, - Vibrate = new int[]{10, 5, 10}, + Vibrate = new int[] { 10, 5, 10 }, Actions = new List() { new Action() @@ -523,79 +523,79 @@ public void WebpushConfig() }, CustomData = new Dictionary() { - {"custom-key1", "custom-data"}, - {"custom-key2", true}, + { "custom-key1", "custom-data" }, + { "custom-key2", true }, }, }, }, }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "webpush", new JObject() { { "headers", new JObject() { - {"header1", "header-value1"}, - {"header2", "header-value2"}, + { "header1", "header-value1" }, + { "header2", "header-value2" }, } }, { "data", new JObject() { - {"key1", "value1"}, - {"key2", "value2"}, + { "key1", "value1" }, + { "key2", "value2" }, } }, { "notification", new JObject() { - {"title", "title"}, - {"body", "body"}, - {"icon", "icon"}, - {"badge", "badge"}, + { "title", "title" }, + { "body", "body" }, + { "icon", "icon" }, + { "badge", "badge" }, { "data", new JObject() { - {"some", "data"}, + { "some", "data" }, } }, - {"dir", "ltr"}, - {"image", "image"}, - {"lang", "language"}, - {"renotify", true}, - {"requireInteraction", true}, - {"silent", true}, - {"tag", "tag"}, - {"timestamp", 100}, - {"vibrate", new JArray(){10, 5, 10}}, + { "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"}, + { "action", "Accept" }, + { "title", "Ok" }, + { "icon", "ok-button" }, }, new JObject() { - {"action", "Reject"}, - {"title", "Cancel"}, - {"icon", "cancel-button"}, + { "action", "Reject" }, + { "title", "Cancel" }, + { "icon", "cancel-button" }, }, } }, - {"custom-key1", "custom-data"}, - {"custom-key2", true}, + { "custom-key1", "custom-data" }, + { "custom-key2", true }, } }, } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -608,10 +608,10 @@ public void WebpushConfigMinimal() }; var expected = new JObject() { - {"topic", "test-topic"}, - {"webpush", new JObject()}, + { "topic", "test-topic" }, + { "webpush", new JObject() }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -632,22 +632,22 @@ public void WebpushConfigMinimalNotification() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "webpush", new JObject() { { "notification", new JObject() { - {"title", "title"}, - {"body", "body"}, - {"icon", "icon"}, + { "title", "title" }, + { "body", "body" }, + { "icon", "icon" }, } }, } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -657,13 +657,13 @@ public void WebpushConfigDeserialization() { Headers = new Dictionary() { - {"header1", "header-value1"}, - {"header2", "header-value2"}, + { "header1", "header-value1" }, + { "header2", "header-value2" }, }, Data = new Dictionary() { - {"key1", "value1"}, - {"key2", "value2"}, + { "key1", "value1" }, + { "key2", "value2" }, }, Notification = new WebpushNotification() { @@ -704,7 +704,7 @@ public void WebpushNotificationDeserialization() Badge = "badge", Data = new Dictionary() { - {"some", "data"}, + { "some", "data" }, }, Direction = Direction.LeftToRight, Image = "image", @@ -714,7 +714,7 @@ public void WebpushNotificationDeserialization() RequireInteraction = true, Renotify = true, TimestampMillis = 100, - Vibrate = new int[]{10, 5, 10}, + Vibrate = new int[] { 10, 5, 10 }, Actions = new List() { new Action() @@ -732,8 +732,8 @@ public void WebpushNotificationDeserialization() }, CustomData = new Dictionary() { - {"custom-key1", "custom-data"}, - {"custom-key2", true}, + { "custom-key1", "custom-data" }, + { "custom-key2", true }, }, }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); @@ -742,7 +742,7 @@ public void WebpushNotificationDeserialization() 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(new JObject() { { "some", "data" } }, copy.Data); Assert.Equal(original.Direction, copy.Direction); Assert.Equal(original.Image, copy.Image); Assert.Equal(original.Language, copy.Language); @@ -761,6 +761,7 @@ public void WebpushNotificationDeserialization() Assert.Equal(originalActions[i].Title, copyActions[i].Title); Assert.Equal(originalActions[i].Icon, copyActions[i].Icon); } + Assert.Equal(original.CustomData, copy.CustomData); } @@ -780,7 +781,7 @@ public void WebpushNotificationCopy() }, CustomData = new Dictionary() { - {"custom-key1", "custom-data"}, + { "custom-key1", "custom-data" }, }, }; var copy = original.CopyAndValidate(); @@ -802,7 +803,7 @@ public void WebpushNotificationDuplicateKeys() Notification = new WebpushNotification() { Title = "title", - CustomData = new Dictionary(){{"title", "other"}}, + CustomData = new Dictionary() { { "title", "other" } }, }, }, }; @@ -819,42 +820,42 @@ public void ApnsConfig() { Headers = new Dictionary() { - {"k1", "v1"}, - {"k2", "v2"}, + { "k1", "v1" }, + { "k2", "v2" }, }, Aps = new Aps() { AlertString = "alert-text", Badge = 0, - Category = "test-category", + Category = "test-category", ContentAvailable = true, MutableContent = true, Sound = "sound-file", ThreadId = "test-thread", CustomData = new Dictionary() { - {"custom-key1", "custom-data"}, - {"custom-key2", true}, + { "custom-key1", "custom-data" }, + { "custom-key2", true }, }, }, CustomData = new Dictionary() { - {"custom-key3", "custom-data"}, - {"custom-key4", true}, + { "custom-key3", "custom-data" }, + { "custom-key4", true }, }, }, }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "apns", new JObject() { { "headers", new JObject() { - {"k1", "v1"}, - {"k2", "v2"}, + { "k1", "v1" }, + { "k2", "v2" }, } }, { @@ -863,25 +864,25 @@ public void ApnsConfig() { "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}, + { "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}, + { "custom-key3", "custom-data" }, + { "custom-key4", true }, } }, } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -897,20 +898,20 @@ public void ApnsConfigMinimal() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "apns", new JObject() { { "payload", new JObject() { - {"aps", new JObject()}, + { "aps", new JObject() }, } }, } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -920,8 +921,8 @@ public void ApnsConfigDeserialization() { Headers = new Dictionary() { - {"k1", "v1"}, - {"k2", "v2"}, + { "k1", "v1" }, + { "k2", "v2" }, }, Aps = new Aps() { @@ -929,8 +930,8 @@ public void ApnsConfigDeserialization() }, CustomData = new Dictionary() { - {"custom-key3", "custom-data"}, - {"custom-key4", true}, + { "custom-key3", "custom-data" }, + { "custom-key4", true }, }, }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); @@ -963,21 +964,21 @@ public void ApnsConfigCustomApsDeserialization() { Headers = new Dictionary() { - {"k1", "v1"}, - {"k2", "v2"}, + { "k1", "v1" }, + { "k2", "v2" }, }, CustomData = new Dictionary() { { "aps", new Dictionary() { - {"alert", "alert-text"}, - {"custom-key1", "custom-data"}, - {"custom-key2", true}, + { "alert", "alert-text" }, + { "custom-key1", "custom-data" }, + { "custom-key2", true }, } }, - {"custom-key3", "custom-data"}, - {"custom-key4", true}, + { "custom-key3", "custom-data" }, + { "custom-key4", true }, }, }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); @@ -988,8 +989,8 @@ public void ApnsConfigCustomApsDeserialization() Assert.Equal("alert-text", copy.Aps.AlertString); var customApsData = new Dictionary() { - {"custom-key1", "custom-data"}, - {"custom-key2", true}, + { "custom-key1", "custom-data" }, + { "custom-key2", true }, }; Assert.Equal(customApsData, copy.Aps.CustomData); } @@ -1015,7 +1016,7 @@ public void ApnsCriticalSound() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "apns", new JObject() { @@ -1028,9 +1029,9 @@ public void ApnsCriticalSound() { "sound", new JObject() { - {"name", "default"}, - {"critical", 1}, - {"volume", 0.5}, + { "name", "default" }, + { "critical", 1 }, + { "volume", 0.5 }, } }, } @@ -1040,7 +1041,7 @@ public void ApnsCriticalSound() } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -1053,13 +1054,13 @@ public void ApnsCriticalSoundMinimal() { Aps = new Aps() { - CriticalSound = new CriticalSound(){Name = "default"}, + CriticalSound = new CriticalSound() { Name = "default" }, }, }, }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "apns", new JObject() { @@ -1072,7 +1073,7 @@ public void ApnsCriticalSoundMinimal() { "sound", new JObject() { - {"name", "default"}, + { "name", "default" }, } }, } @@ -1082,7 +1083,7 @@ public void ApnsCriticalSoundMinimal() } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -1116,13 +1117,13 @@ public void ApnsApsAlert() ActionLocKey = "action-key", Body = "test-body", LaunchImage = "test-image", - LocArgs = new List(){"arg1", "arg2"}, + LocArgs = new List() { "arg1", "arg2" }, LocKey = "loc-key", Subtitle = "test-subtitle", - SubtitleLocArgs = new List(){"arg3", "arg4"}, + SubtitleLocArgs = new List() { "arg3", "arg4" }, SubtitleLocKey = "subtitle-key", Title = "test-title", - TitleLocArgs = new List(){"arg5", "arg6"}, + TitleLocArgs = new List() { "arg5", "arg6" }, TitleLocKey = "title-key", }, }, @@ -1130,7 +1131,7 @@ public void ApnsApsAlert() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "apns", new JObject() { @@ -1143,17 +1144,17 @@ public void ApnsApsAlert() { "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"}, + { "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" }, } }, } @@ -1163,7 +1164,7 @@ public void ApnsApsAlert() } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -1182,7 +1183,7 @@ public void ApnsApsAlertMinimal() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "apns", new JObject() { @@ -1193,7 +1194,7 @@ public void ApnsApsAlertMinimal() "aps", new JObject() { { - "alert", new JObject(){} + "alert", new JObject() }, } }, @@ -1202,7 +1203,7 @@ public void ApnsApsAlertMinimal() } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -1213,13 +1214,13 @@ public void ApsAlertDeserialization() ActionLocKey = "action-key", Body = "test-body", LaunchImage = "test-image", - LocArgs = new List(){"arg1", "arg2"}, + LocArgs = new List() { "arg1", "arg2" }, LocKey = "loc-key", Subtitle = "test-subtitle", - SubtitleLocArgs = new List(){"arg3", "arg4"}, + SubtitleLocArgs = new List() { "arg3", "arg4" }, SubtitleLocKey = "subtitle-key", Title = "test-title", - TitleLocArgs = new List(){"arg5", "arg6"}, + TitleLocArgs = new List() { "arg5", "arg6" }, TitleLocKey = "title-key", }; var json = NewtonsoftJsonSerializer.Instance.Serialize(original); @@ -1242,11 +1243,11 @@ public void ApsAlertCopy() { var original = new ApsAlert() { - LocArgs = new List(){"arg1", "arg2"}, + LocArgs = new List() { "arg1", "arg2" }, LocKey = "loc-key", - SubtitleLocArgs = new List(){"arg3", "arg4"}, + SubtitleLocArgs = new List() { "arg3", "arg4" }, SubtitleLocKey = "subtitle-key", - TitleLocArgs = new List(){"arg5", "arg6"}, + TitleLocArgs = new List() { "arg5", "arg6" }, TitleLocKey = "title-key", }; var copy = original.CopyAndValidate(); @@ -1268,7 +1269,7 @@ public void ApnsApsAlertInvalidTitleLocArgs() { Alert = new ApsAlert() { - TitleLocArgs = new List(){"arg1", "arg2"}, + TitleLocArgs = new List() { "arg1", "arg2" }, }, }, }, @@ -1288,7 +1289,7 @@ public void ApnsApsAlertInvalidSubtitleLocArgs() { Alert = new ApsAlert() { - SubtitleLocArgs = new List(){"arg1", "arg2"}, + SubtitleLocArgs = new List() { "arg1", "arg2" }, }, }, }, @@ -1308,7 +1309,7 @@ public void ApnsApsAlertInvalidLocArgs() { Alert = new ApsAlert() { - LocArgs = new List(){"arg1", "arg2"}, + LocArgs = new List() { "arg1", "arg2" }, }, }, }, @@ -1329,8 +1330,8 @@ public void ApnsCustomApsWithStandardProperties() { "aps", new Dictionary() { - {"alert", "alert-text"}, - {"badge", 42}, + { "alert", "alert-text" }, + { "badge", 42 }, } }, }, @@ -1338,7 +1339,7 @@ public void ApnsCustomApsWithStandardProperties() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "apns", new JObject() { @@ -1348,8 +1349,8 @@ public void ApnsCustomApsWithStandardProperties() { "aps", new JObject() { - {"alert", "alert-text"}, - {"badge", 42}, + { "alert", "alert-text" }, + { "badge", 42 }, } }, } @@ -1357,7 +1358,7 @@ public void ApnsCustomApsWithStandardProperties() } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -1373,8 +1374,8 @@ public void ApnsCustomApsWithCustomProperties() { "aps", new Dictionary() { - {"custom-key1", "custom-data"}, - {"custom-key2", true}, + { "custom-key1", "custom-data" }, + { "custom-key2", true }, } }, }, @@ -1382,7 +1383,7 @@ public void ApnsCustomApsWithCustomProperties() }; var expected = new JObject() { - {"topic", "test-topic"}, + { "topic", "test-topic" }, { "apns", new JObject() { @@ -1392,8 +1393,8 @@ public void ApnsCustomApsWithCustomProperties() { "aps", new JObject() { - {"custom-key1", "custom-data"}, - {"custom-key2", true}, + { "custom-key1", "custom-data" }, + { "custom-key2", true }, } }, } @@ -1401,7 +1402,7 @@ public void ApnsCustomApsWithCustomProperties() } }, }; - AssertJsonEquals(expected, message); + this.AssertJsonEquals(expected, message); } [Fact] @@ -1414,7 +1415,7 @@ public void ApnsNoAps() { CustomData = new Dictionary() { - {"test", "custom-data"}, + { "test", "custom-data" }, }, }, }; @@ -1435,7 +1436,7 @@ public void ApnsDuplicateAps() }, CustomData = new Dictionary() { - {"aps", "custom-data"}, + { "aps", "custom-data" }, }, }, }; @@ -1450,7 +1451,7 @@ public void ApsDuplicateKeys() AlertString = "alert-text", CustomData = new Dictionary() { - {"alert", "other-alert-text"}, + { "alert", "other-alert-text" }, }, }; Assert.Throws(() => aps.CopyAndValidate()); From d7f22ecf0be66c889de364ea6909e29d4580c274 Mon Sep 17 00:00:00 2001 From: hiranya911 Date: Fri, 25 Jan 2019 15:58:58 -0800 Subject: [PATCH 50/50] Last lint errors --- .../FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index 102ab711..790294d8 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -15,8 +15,8 @@ using System; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Xunit; using FirebaseAdmin.Messaging; +using Xunit; namespace FirebaseAdmin.IntegrationTests {