From c5f8cdf91b8c9b7c651e7408a7e600505da99635 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Sat, 11 Apr 2026 22:37:46 +0200 Subject: [PATCH 1/3] Replace Newtonsoft.Json with System.Text.Json --- .../FirebasePushNotificationSettings.cs | 13 +- .../Internals/DictionaryJsonConverter.cs | 254 ++++++++++-------- .../Model/NotificationAction.cs | 20 +- .../Model/NotificationCategory.cs | 15 +- .../Model/NotificationMessage.cs | 12 +- .../Model/Queues/PersistentQueue.cs | 19 +- .../Plugin.FirebasePushNotifications.csproj | 4 - ReleaseNotes.txt | 1 + .../Model/NotificationCategoryTests.cs | 30 ++- .../Model/NotificationMessageTests.cs | 2 +- .../Model/Queues/PersistentQueue.UnitTests.cs | 5 +- 11 files changed, 217 insertions(+), 158 deletions(-) diff --git a/Plugin.FirebasePushNotifications/FirebasePushNotificationSettings.cs b/Plugin.FirebasePushNotifications/FirebasePushNotificationSettings.cs index 13887aaf..beaa82a8 100644 --- a/Plugin.FirebasePushNotifications/FirebasePushNotificationSettings.cs +++ b/Plugin.FirebasePushNotifications/FirebasePushNotificationSettings.cs @@ -1,11 +1,16 @@ using System.Diagnostics; -using Newtonsoft.Json; +using System.Text.Json; using Plugin.FirebasePushNotifications.Extensions; namespace Plugin.FirebasePushNotifications { public class FirebasePushNotificationPreferences : IFirebasePushNotificationPreferences { + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }; + private readonly IPreferences preferences; public FirebasePushNotificationPreferences(IPreferences preferences) @@ -29,7 +34,7 @@ public void Set(string key, T value) if (value is not string serializedValue) { - serializedValue = JsonConvert.SerializeObject(value); + serializedValue = JsonSerializer.Serialize(value, JsonSerializerOptions); } this.preferences.Set(key, serializedValue); @@ -60,7 +65,7 @@ public T Get(string key, T defaultValue = default) } else { - value = JsonConvert.DeserializeObject(serializedValue); + value = JsonSerializer.Deserialize(serializedValue, JsonSerializerOptions); } } catch (Exception ex) @@ -114,4 +119,4 @@ public void ClearAll() } } } -} \ No newline at end of file +} diff --git a/Plugin.FirebasePushNotifications/Internals/DictionaryJsonConverter.cs b/Plugin.FirebasePushNotifications/Internals/DictionaryJsonConverter.cs index d829a097..b9b6dad5 100644 --- a/Plugin.FirebasePushNotifications/Internals/DictionaryJsonConverter.cs +++ b/Plugin.FirebasePushNotifications/Internals/DictionaryJsonConverter.cs @@ -1,7 +1,7 @@ -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; namespace Plugin.FirebasePushNotifications.Internals { @@ -11,12 +11,11 @@ namespace Plugin.FirebasePushNotifications.Internals /// public static class DictionaryJsonConverter { - private static readonly JsonSerializer DefaultJsonSerializer = new JsonSerializer() + private static readonly JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions { - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - DateFormatHandling = DateFormatHandling.IsoDateFormat, - NullValueHandling = NullValueHandling.Ignore, - Converters = { new IsoDateTimeConverter() } + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, }; /// @@ -26,41 +25,36 @@ public static class DictionaryJsonConverter /// Flat dictionary with path as key and value as string. public static IDictionary Flatten(T source) { - return Flatten(source, DefaultJsonSerializer); + return Flatten(source, DefaultJsonSerializerOptions); } /// /// Creates a flat dictionary for of type . /// /// Source object. - /// Custom JsonSerializer used to serialize . + /// Custom options used to serialize . /// Flat dictionary with path as key and value as string. - public static IDictionary Flatten(T source, JsonSerializer jsonSerializer) + public static IDictionary Flatten(T source, JsonSerializerOptions jsonSerializerOptions) { - var jObject = JObject.FromObject(source, jsonSerializer); - return Flatten(jObject); + var jsonObject = JsonSerializer.SerializeToNode(source, jsonSerializerOptions) as JsonObject ?? new JsonObject(); + return Flatten(jsonObject); } /// - /// Creates a flat dictionary for of type . + /// Creates a flat dictionary for of type . /// /// Source object. /// Flat dictionary with path as key and value as string. - public static IDictionary Flatten(JObject source) + public static IDictionary Flatten(JsonObject source) { - var jTokens = source - .Descendants() - .Where(p => !p.Any()); + var results = new Dictionary(); - var results = jTokens - .Select(jToken => - { - var value = (jToken as JValue)?.Value?.ToString(); - var key = jToken.Path; - return new KeyValuePair(key, value); - }) - .ToDictionary(x => x.Key, x => x.Value); + if (source == null) + { + return results; + } + FlattenInternal(source, null, results); return results; } @@ -72,16 +66,16 @@ public static IDictionary Flatten(JObject source) /// Target object. public static T Unflatten(IDictionary dictionary) { - var jObject = Unflatten(dictionary); - return jObject.ToObject(DefaultJsonSerializer); + var jsonObject = Unflatten(dictionary); + return jsonObject.Deserialize(DefaultJsonSerializerOptions); } /// /// The source dictionary. - /// A custom json serializer. - public static T Unflatten(IDictionary dictionary, JsonSerializer jsonSerializer) + /// A custom json serializer options instance. + public static T Unflatten(IDictionary dictionary, JsonSerializerOptions jsonSerializerOptions) { - return (T)Unflatten(dictionary, typeof(T), jsonSerializer); + return (T)Unflatten(dictionary, typeof(T), jsonSerializerOptions); } /// @@ -89,85 +83,28 @@ public static T Unflatten(IDictionary dictionary, JsonSeriali /// /// The source dictionary. /// The target type. - /// A custom json serializer. - public static object Unflatten(IDictionary dictionary, Type targetType, JsonSerializer jsonSerializer) + /// A custom json serializer options instance. + public static object Unflatten(IDictionary dictionary, Type targetType, JsonSerializerOptions jsonSerializerOptions) { - var jObject = Unflatten(dictionary); - return jObject.ToObject(targetType, jsonSerializer); + var jsonObject = Unflatten(dictionary); + return jsonObject.Deserialize(targetType, jsonSerializerOptions); } /// - /// Creates a from a given flat . + /// Creates a from a given flat . /// /// The source dictionary. /// The structured result. - public static JObject Unflatten(IDictionary dictionary) + public static JsonObject Unflatten(IDictionary dictionary) { - JContainer result = null; - var setting = new JsonMergeSettings { MergeArrayHandling = MergeArrayHandling.Merge }; - foreach (var pathValue in dictionary) - { - if (result == null) - { - result = UnflattenSingle(pathValue); - } - else - { - result.Merge(UnflattenSingle(pathValue), setting); - } - } + var root = new JsonObject(); - return result as JObject; - } - - private static JContainer UnflattenSingle(KeyValuePair keyValue) - { - var path = keyValue.Key; - var value = keyValue.Value; - var pathSegments = SplitPath(path); - - JContainer lastItem = null; - - // Build from leaf to root - foreach (var pathSegment in pathSegments.Reverse()) + foreach (var pathValue in dictionary) { - var type = GetJsonTokenType(pathSegment); - switch (type) - { - case JTokenType.Object: - var obj = new JObject(); - if (null == lastItem) - { - obj.Add(pathSegment, value); - } - else - { - obj.Add(pathSegment, lastItem); - } - - lastItem = obj; - break; - case JTokenType.Array: - var array = new JArray(); - var index = GetArrayIndex(pathSegment); - array = FillEmpty(array, index); - if (lastItem == null) - { - array[index] = value; - } - else - { - array[index] = lastItem; - } - - lastItem = array; - break; - default: - throw new NotSupportedException($"UnflattenSingle does not support type {type}."); - } + InsertPathValue(root, pathValue.Key, pathValue.Value); } - return lastItem; + return root; } public static IEnumerable SplitPath(string path) @@ -179,29 +116,126 @@ public static IEnumerable SplitPath(string path) } } - private static JArray FillEmpty(JArray array, int index) + private static void FlattenInternal(JsonNode node, string currentPath, IDictionary results) { - for (var i = 0; i <= index; i++) + switch (node) { - array.Add(null); + case JsonObject jsonObject: + foreach (var property in jsonObject) + { + var nextPath = string.IsNullOrEmpty(currentPath) + ? property.Key + : $"{currentPath}.{property.Key}"; + + FlattenInternal(property.Value, nextPath, results); + } + + break; + + case JsonArray jsonArray: + for (var i = 0; i < jsonArray.Count; i++) + { + var nextPath = $"{currentPath}[{i}]"; + FlattenInternal(jsonArray[i], nextPath, results); + } + + break; + + case JsonValue jsonValue: + results[currentPath] = GetJsonValueAsString(jsonValue); + break; + + case null: + results[currentPath] = null; + break; } + } - return array; + private static string GetElementStringValue(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Null => null, + _ => element.ToString(), + }; } - private static JTokenType GetJsonTokenType(string pathSegment) + private static string GetJsonValueAsString(JsonValue jsonValue) { - return int.TryParse(pathSegment, out _) ? JTokenType.Array : JTokenType.Object; + if (jsonValue.TryGetValue(out var stringValue)) + { + return stringValue; + } + + if (jsonValue.TryGetValue(out var jsonElement)) + { + return GetElementStringValue(jsonElement); + } + + return jsonValue.ToJsonString().Trim('"'); } - private static int GetArrayIndex(string pathSegment) + private static void InsertPathValue(JsonObject root, string path, string value) { - if (int.TryParse(pathSegment, out var result)) + var pathSegments = SplitPath(path).ToArray(); + JsonNode current = root; + + for (var i = 0; i < pathSegments.Length; i++) { - return result; + var segment = pathSegments[i]; + var isLast = i == pathSegments.Length - 1; + var isArraySegment = int.TryParse(segment, out var arrayIndex); + + if (isArraySegment) + { + var array = current as JsonArray; + if (array == null) + { + throw new NotSupportedException($"Path segment '{segment}' is an array index but current node is not an array."); + } + + EnsureArraySize(array, arrayIndex); + + if (isLast) + { + array[arrayIndex] = JsonValue.Create(value); + } + else + { + var nextSegmentIsArray = int.TryParse(pathSegments[i + 1], out _); + array[arrayIndex] ??= nextSegmentIsArray ? new JsonArray() : new JsonObject(); + current = array[arrayIndex]; + } + } + else + { + var obj = current as JsonObject; + if (obj == null) + { + throw new NotSupportedException($"Path segment '{segment}' is an object key but current node is not an object."); + } + + if (isLast) + { + obj[segment] = JsonValue.Create(value); + } + else + { + var nextSegmentIsArray = int.TryParse(pathSegments[i + 1], out _); + obj[segment] ??= nextSegmentIsArray ? new JsonArray() : new JsonObject(); + current = obj[segment]; + } + } } + } - throw new Exception("Unable to parse array index: " + pathSegment); + private static void EnsureArraySize(JsonArray array, int index) + { + while (array.Count <= index) + { + array.Add(null); + } } } -} \ No newline at end of file +} diff --git a/Plugin.FirebasePushNotifications/Model/NotificationAction.cs b/Plugin.FirebasePushNotifications/Model/NotificationAction.cs index d35b09ee..9473fc1a 100644 --- a/Plugin.FirebasePushNotifications/Model/NotificationAction.cs +++ b/Plugin.FirebasePushNotifications/Model/NotificationAction.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Plugin.FirebasePushNotifications { @@ -8,14 +8,14 @@ public class NotificationAction { [JsonConstructor] public NotificationAction( - [JsonProperty("id")] string id, - [JsonProperty("title")] string title, - [JsonProperty("type")] NotificationActionType notificationActionType, - [JsonProperty("icon")] string icon) + string id, + string title, + NotificationActionType type, + string icon) { this.Id = id ?? throw new ArgumentNullException(nameof(id)); this.Title = title ?? throw new ArgumentNullException(nameof(title)); - this.Type = notificationActionType; + this.Type = type; this.Icon = icon; } @@ -24,16 +24,16 @@ public NotificationAction(string id, string title, NotificationActionType notifi { } - [JsonProperty("id")] + [JsonPropertyName("id")] public string Id { get; } - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; } - [JsonProperty("type")] + [JsonPropertyName("type")] public NotificationActionType Type { get; } - [JsonProperty("icon")] + [JsonPropertyName("icon")] public string Icon { get; } public override string ToString() diff --git a/Plugin.FirebasePushNotifications/Model/NotificationCategory.cs b/Plugin.FirebasePushNotifications/Model/NotificationCategory.cs index 02124f46..e4021595 100644 --- a/Plugin.FirebasePushNotifications/Model/NotificationCategory.cs +++ b/Plugin.FirebasePushNotifications/Model/NotificationCategory.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Plugin.FirebasePushNotifications { @@ -19,9 +19,9 @@ public NotificationCategory( [JsonConstructor] public NotificationCategory( - [JsonProperty("categoryId")] string categoryId, - [JsonProperty("actions")] NotificationAction[] actions, - [JsonProperty("type")] NotificationCategoryType type) + string categoryId, + NotificationAction[] actions, + NotificationCategoryType type) { this.CategoryId = categoryId ?? throw new ArgumentNullException(nameof(categoryId)); @@ -42,21 +42,20 @@ public NotificationCategory( /// /// Identifier of the notification category. /// - [JsonProperty("categoryId")] + [JsonPropertyName("categoryId")] public string CategoryId { get; } /// /// Notification actions which belong to this notification category. /// - [JsonProperty("actions")] + [JsonPropertyName("actions")] public NotificationAction[] Actions { get; } /// /// Notification category type, used to display special-purpose /// notification categories. Default is . /// - [JsonProperty("type")] + [JsonPropertyName("type")] public NotificationCategoryType Type { get; } - } } diff --git a/Plugin.FirebasePushNotifications/Model/NotificationMessage.cs b/Plugin.FirebasePushNotifications/Model/NotificationMessage.cs index 6c689ea8..dc661342 100644 --- a/Plugin.FirebasePushNotifications/Model/NotificationMessage.cs +++ b/Plugin.FirebasePushNotifications/Model/NotificationMessage.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; using Plugin.FirebasePushNotifications.Internals; using Plugin.FirebasePushNotifications.Extensions; @@ -21,28 +21,28 @@ public NotificationMessage(IDictionary data) this.Data = data ?? new Dictionary(); } - [JsonProperty(Constants.NotificationTitleKey)] + [JsonPropertyName(Constants.NotificationTitleKey)] public string Title { get => this.Data.GetValueOrDefault(Constants.NotificationTitleKey); set => this.Data[Constants.NotificationTitleKey] = value; } - [JsonProperty(Constants.NotificationBodyKey)] + [JsonPropertyName(Constants.NotificationBodyKey)] public string Body { get => this.Data.GetValueOrDefault(Constants.NotificationBodyKey); set => this.Data[Constants.NotificationBodyKey] = value; } - [JsonProperty(Constants.NotificationTagKey)] + [JsonPropertyName(Constants.NotificationTagKey)] public string Tag { get => this.Data.GetValueOrDefault(Constants.NotificationTagKey); set => this.Data[Constants.NotificationTagKey] = value; } - [JsonProperty(Constants.NotificationDataKey)] + [JsonPropertyName(Constants.NotificationDataKey)] public IDictionary Data { get; private set; } = new Dictionary(); public override string ToString() @@ -51,4 +51,4 @@ public override string ToString() return string.Join($",{Environment.NewLine}", dict.Select(d => $"{{{d.Key}, {d.Value ?? "null"}}}")); } } -} \ No newline at end of file +} diff --git a/Plugin.FirebasePushNotifications/Model/Queues/PersistentQueue.cs b/Plugin.FirebasePushNotifications/Model/Queues/PersistentQueue.cs index fba98d3b..17613c97 100644 --- a/Plugin.FirebasePushNotifications/Model/Queues/PersistentQueue.cs +++ b/Plugin.FirebasePushNotifications/Model/Queues/PersistentQueue.cs @@ -1,8 +1,9 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Newtonsoft.Json; using Plugin.FirebasePushNotifications.Internals; namespace Plugin.FirebasePushNotifications.Model.Queues @@ -20,7 +21,7 @@ internal class PersistentQueue : IQueue private readonly ILogger logger; private readonly IQueue internalQueue; private readonly IFileInfo fileInfo; - private readonly JsonSerializerSettings jsonSerializerSettings; + private readonly JsonSerializerOptions jsonSerializerOptions; private readonly object lockObj = new object(); internal PersistentQueue( @@ -30,14 +31,14 @@ internal PersistentQueue( this.logger = logger ?? new NullLogger>(); this.fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); - this.jsonSerializerSettings = new JsonSerializerSettings + this.jsonSerializerOptions = new JsonSerializerOptions { - NullValueHandling = NullValueHandling.Ignore, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + ReferenceHandler = ReferenceHandler.IgnoreCycles, + PropertyNameCaseInsensitive = true, }; this.internalQueue = this.ReadQueueFile(this.fileInfo); - this.logger = logger; } /// @@ -135,7 +136,7 @@ private IQueue ReadQueueFile(IFileInfo fileInfo) var json = streamReader.ReadToEnd(); if (!string.IsNullOrEmpty(json)) { - items = JsonConvert.DeserializeObject>(json, this.jsonSerializerSettings); + items = JsonSerializer.Deserialize>(json, this.jsonSerializerOptions); } } } @@ -166,7 +167,7 @@ private void WriteQueueFile(IFileInfo fileInfo, IQueue queue) using (var writer = fileInfo.CreateText()) { var array = queue.ToArray(); - var json = JsonConvert.SerializeObject(array, this.jsonSerializerSettings); + var json = JsonSerializer.Serialize(array, this.jsonSerializerOptions); writer.Write(json); } } @@ -176,4 +177,4 @@ private void WriteQueueFile(IFileInfo fileInfo, IQueue queue) } } } -} \ No newline at end of file +} diff --git a/Plugin.FirebasePushNotifications/Plugin.FirebasePushNotifications.csproj b/Plugin.FirebasePushNotifications/Plugin.FirebasePushNotifications.csproj index ced1bafb..e128a0fc 100644 --- a/Plugin.FirebasePushNotifications/Plugin.FirebasePushNotifications.csproj +++ b/Plugin.FirebasePushNotifications/Plugin.FirebasePushNotifications.csproj @@ -55,10 +55,6 @@ true - - - - diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 37fa82f8..5d514de2 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -8,6 +8,7 @@ - Use AddOnCompleteListener for asynchronous tasks in Android. - Remove support for net7.0 and net8.0. - Add support for net9.0 and net10.0. +- Replace Newtonsoft.Json with System.Text.Json. - Bug fixes and refactorings. 3.2 diff --git a/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationCategoryTests.cs b/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationCategoryTests.cs index f64474a7..f1fb7875 100644 --- a/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationCategoryTests.cs +++ b/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationCategoryTests.cs @@ -1,10 +1,15 @@ -using FluentAssertions; -using Newtonsoft.Json; +using System.Text.Json; +using FluentAssertions; namespace Plugin.FirebasePushNotifications.Tests.Model { public class NotificationCategoryTests { + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }; + [Fact] public void ShouldCreateNotificationCategory() { @@ -33,7 +38,7 @@ public void ShouldSerializeNotificationCategory() var notificationCategory = new NotificationCategory("category1", notificationActions); // Act - var notificationCategoryJson = JsonConvert.SerializeObject(notificationCategory); + var notificationCategoryJson = JsonSerializer.Serialize(notificationCategory, JsonSerializerOptions); // Assert notificationCategoryJson.Should().Be( @@ -61,7 +66,7 @@ public void ShouldDeserializeNotificationCategory() "\"type\":2}"; // Act - var notificationCategory = JsonConvert.DeserializeObject(notificationCategoryJson); + var notificationCategory = JsonSerializer.Deserialize(notificationCategoryJson, JsonSerializerOptions); // Assert notificationCategory.CategoryId.Should().Be("category1"); @@ -74,5 +79,22 @@ public void ShouldDeserializeNotificationCategory() var action2 = notificationCategory.Actions.ElementAt(1); action2.Should().BeEquivalentTo(new NotificationAction("action2", "title2", NotificationActionType.Destructive)); } + + [Fact] + public void ShouldDeserializeNotificationAction() + { + // Arrange + const string json = "{\"id\":\"action1\",\"title\":\"title1\",\"type\":1,\"icon\":null}"; + + // Act + var action = JsonSerializer.Deserialize(json, JsonSerializerOptions); + + // Assert + action.Should().NotBeNull(); + action.Id.Should().Be("action1"); + action.Title.Should().Be("title1"); + action.Type.Should().Be(NotificationActionType.Foreground); + action.Icon.Should().BeNull(); + } } } diff --git a/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationMessageTests.cs b/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationMessageTests.cs index a111a920..ba632cea 100644 --- a/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationMessageTests.cs +++ b/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationMessageTests.cs @@ -6,7 +6,7 @@ namespace Plugin.FirebasePushNotifications.Tests.Model public class NotificationMessageTests { [Fact] - public void ShouldCreatenotificationMessage_WithTitleAndBody() + public void ShouldCreateNotificationMessage_WithTitleAndBody() { // Act const string title = "Title"; diff --git a/Tests/Plugin.FirebasePushNotifications.Tests/Model/Queues/PersistentQueue.UnitTests.cs b/Tests/Plugin.FirebasePushNotifications.Tests/Model/Queues/PersistentQueue.UnitTests.cs index 965fcdb5..fda2cea5 100644 --- a/Tests/Plugin.FirebasePushNotifications.Tests/Model/Queues/PersistentQueue.UnitTests.cs +++ b/Tests/Plugin.FirebasePushNotifications.Tests/Model/Queues/PersistentQueue.UnitTests.cs @@ -89,7 +89,7 @@ public class ValidQueueContentTestData : TheoryData public ValidQueueContentTestData() { this.Add("[]", 0); - this.Add("[{Id: 1},{Id: 2}]", 2); + this.Add("[{\"Id\":1},{\"Id\":2}]", 2); } } @@ -124,6 +124,7 @@ public InvalidQueueContentTestData() this.Add(""); this.Add(" "); this.Add("invalid content"); + this.Add("[{Id: 1},{Id: 2}]"); } } @@ -283,4 +284,4 @@ private static string[] GetTextFromMemoryStreams(IEnumerable creat .ToArray(); } } -} \ No newline at end of file +} From c27dfcdcc346a7e12dee33a430fa523974852787 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Sat, 11 Apr 2026 22:37:59 +0200 Subject: [PATCH 2/3] Add unit tests for NotificationAction --- .../Model/NotificationActionTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationActionTests.cs diff --git a/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationActionTests.cs b/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationActionTests.cs new file mode 100644 index 00000000..e08b29fd --- /dev/null +++ b/Tests/Plugin.FirebasePushNotifications.Tests/Model/NotificationActionTests.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using FluentAssertions; + +namespace Plugin.FirebasePushNotifications.Tests.Model +{ + public class NotificationActionTests + { + private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }; + + [Fact] + public void ShouldCreateNotificationAction() + { + // Act + var notificationAction = new NotificationAction("action1", "title1", NotificationActionType.Foreground); + + // Assert + notificationAction.Id.Should().Be("action1"); + notificationAction.Title.Should().Be("title1"); + notificationAction.Type.Should().Be(NotificationActionType.Foreground); + notificationAction.Icon.Should().BeNull(); + } + + [Fact] + public void ShouldSerializeNotificationAction() + { + // Arrange + var notificationAction = new NotificationAction("action1", "title1", NotificationActionType.Foreground); + + // Act + var notificationActionJson = JsonSerializer.Serialize(notificationAction, JsonSerializerOptions); + + // Assert + notificationActionJson.Should().Be("{\"id\":\"action1\",\"title\":\"title1\",\"type\":1,\"icon\":null}"); + } + + [Fact] + public void ShouldDeserializeNotificationAction() + { + // Arrange + const string notificationActionJson = "{\"Id\":\"action1\",\"Title\":\"title1\",\"Type\":1,\"Icon\":null}"; + + // Act + var notificationAction = JsonSerializer.Deserialize(notificationActionJson, JsonSerializerOptions); + + // Assert + notificationAction.Should().NotBeNull(); + notificationAction.Should().BeEquivalentTo(new NotificationAction("action1", "title1", NotificationActionType.Foreground)); + } + } +} From 72894757aa436827ecb0da9eda728af9a884a401 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Sat, 11 Apr 2026 22:45:49 +0200 Subject: [PATCH 3/3] Update xml comment --- .../Platforms/Android/Channels/INotificationChannels.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Plugin.FirebasePushNotifications/Platforms/Android/Channels/INotificationChannels.cs b/Plugin.FirebasePushNotifications/Platforms/Android/Channels/INotificationChannels.cs index 5267f542..98907a1d 100644 --- a/Plugin.FirebasePushNotifications/Platforms/Android/Channels/INotificationChannels.cs +++ b/Plugin.FirebasePushNotifications/Platforms/Android/Channels/INotificationChannels.cs @@ -72,10 +72,8 @@ public partial interface INotificationChannels /// /// Creates notification channels from given . + /// See also: /// - /// - /// If the already exist, they're updated. - /// /// The notification channel requests. void CreateNotificationChannels([NotNull] NotificationChannelRequest[] notificationChannelRequests);