diff --git a/src/Eurofurence.App.Common/Compression/ZipHelper.cs b/src/Eurofurence.App.Common/Compression/ZipHelper.cs index 2be31d3f..23e16c96 100644 --- a/src/Eurofurence.App.Common/Compression/ZipHelper.cs +++ b/src/Eurofurence.App.Common/Compression/ZipHelper.cs @@ -1,9 +1,5 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Text; +using System.IO.Compression; +using System.Text.Json; namespace Eurofurence.App.Common.Compression { @@ -14,13 +10,7 @@ public static void AddAsJson(this ZipArchive archive, string entryName, object i var entry = archive.CreateEntry(entryName, compressionLevel); var stream = entry.Open(); - var serializer = new JsonSerializer(); - - using (var streamWriter = new StreamWriter(stream, Encoding.UTF8)) - using (var textWriter = new JsonTextWriter(streamWriter)) - { - serializer.Serialize(textWriter, item); - } + JsonSerializer.Serialize(stream, item); } public static void AddAsBinary(this ZipArchive archive, string entryName, byte[] data, CompressionLevel compressionLevel = CompressionLevel.Optimal) @@ -37,13 +27,7 @@ public static T ReadAsJson(this ZipArchive archive, string entryName) var entry = archive.GetEntry(entryName); var stream = entry.Open(); - var serializer = new JsonSerializer(); - - using (var streamReader = new StreamReader(stream, Encoding.UTF8)) - using (var textReader = new JsonTextReader(streamReader)) - { - return serializer.Deserialize(textReader); - } + return JsonSerializer.Deserialize(stream); } public static byte[] ReadAsBinary(this ZipArchive archive, string entryName) diff --git a/src/Eurofurence.App.Common/Eurofurence.App.Common.csproj b/src/Eurofurence.App.Common/Eurofurence.App.Common.csproj index e2cb862c..4d4cca03 100644 --- a/src/Eurofurence.App.Common/Eurofurence.App.Common.csproj +++ b/src/Eurofurence.App.Common/Eurofurence.App.Common.csproj @@ -7,7 +7,4 @@ false false - - - \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LostAndFoundResponse.cs b/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LostAndFoundResponse.cs index 140dff86..408452ad 100644 --- a/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LostAndFoundResponse.cs +++ b/src/Eurofurence.App.Server.Services/Abstractions/Lassie/LostAndFoundResponse.cs @@ -1,32 +1,32 @@ -using Newtonsoft.Json; -using System; +using System; +using System.Text.Json.Serialization; namespace Eurofurence.App.Server.Services.Abstractions.Lassie { public class LostAndFoundResponse { - [JsonProperty("id")] + [JsonPropertyName("id")] public int Id { get; set; } - [JsonProperty("image")] + [JsonPropertyName("image")] public string ImageUrl { get; set; } - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } - [JsonProperty("description")] + [JsonPropertyName("description")] public string Description { get; set; } - [JsonProperty("status")] + [JsonPropertyName("status")] public string Status { get; set; } - [JsonProperty("lost_timestamp")] + [JsonPropertyName("lost_timestamp")] public DateTime? LostDateTimeLocal { get; set; } - [JsonProperty("found_timestamp")] + [JsonPropertyName("found_timestamp")] public DateTime? FoundDateTimeLocal { get; set; } - [JsonProperty("return_timestamp")] + [JsonPropertyName("return_timestamp")] public DateTime? ReturnDateTimeLocal { get; set; } } } diff --git a/src/Eurofurence.App.Server.Services/Eurofurence - Backup.App.Server.Services.csproj b/src/Eurofurence.App.Server.Services/Eurofurence - Backup.App.Server.Services.csproj new file mode 100644 index 00000000..45e421a7 --- /dev/null +++ b/src/Eurofurence.App.Server.Services/Eurofurence - Backup.App.Server.Services.csproj @@ -0,0 +1,36 @@ + + + net8.0 + Eurofurence.App.Server.Services + Eurofurence.App.Server.Services + false + false + false + + + TRACE;DEBUG;NETSTANDARD2_0 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Services/Lassie/LassieApiClient.cs b/src/Eurofurence.App.Server.Services/Lassie/LassieApiClient.cs index d99c854c..5085bb26 100644 --- a/src/Eurofurence.App.Server.Services/Lassie/LassieApiClient.cs +++ b/src/Eurofurence.App.Server.Services/Lassie/LassieApiClient.cs @@ -1,23 +1,26 @@ -using Eurofurence.App.Server.Services.Abstractions.Lassie; -using Newtonsoft.Json; +using System; +using Eurofurence.App.Server.Services.Abstractions.Lassie; using System.Collections.Generic; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; namespace Eurofurence.App.Server.Services.Lassie { public class LassieApiClient : ILassieApiClient { - private class DataResponseWrapper + public class DataResponseWrapper { public T[] Data { get; set; } } private LassieConfiguration _configuration; + private JsonSerializerOptions _serializerOptions; - public LassieApiClient(LassieConfiguration configuration) + public LassieApiClient(LassieConfiguration configuration, JsonSerializerOptions serializerOptions) { _configuration = configuration; + _serializerOptions = serializerOptions; } public async Task QueryLostAndFoundDbAsync(string command = "lostandfound") @@ -29,16 +32,18 @@ public async Task QueryLostAndFoundDbAsync(string comman new KeyValuePair("command", command) }; - using (var client = new HttpClient()) - { - var response = await client.PostAsync(_configuration.BaseApiUrl, new FormUrlEncodedContent(outgoingQuery)); - var content = await response.Content.ReadAsStringAsync(); + using var client = new HttpClient(); + var response = await client.PostAsync(_configuration.BaseApiUrl, new FormUrlEncodedContent(outgoingQuery)); + var content = await response.Content.ReadAsStringAsync(); + + var dataResponse = DeserializeLostAndFoundResponse(content); - var dataResponse = JsonConvert.DeserializeObject>(content, - new JsonSerializerSettings() { DateTimeZoneHandling = DateTimeZoneHandling.Local }); + return dataResponse.Data ?? Array.Empty(); + } - return dataResponse.Data ?? new LostAndFoundResponse[0]; - } + public DataResponseWrapper DeserializeLostAndFoundResponse(string content) + { + return JsonSerializer.Deserialize>(content, _serializerOptions); } } } \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Services/PushNotifications/FirebaseChannelManager.cs b/src/Eurofurence.App.Server.Services/PushNotifications/FirebaseChannelManager.cs index 9e4f102e..66bb6eee 100644 --- a/src/Eurofurence.App.Server.Services/PushNotifications/FirebaseChannelManager.cs +++ b/src/Eurofurence.App.Server.Services/PushNotifications/FirebaseChannelManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Eurofurence.App.Common.ExtensionMethods; using Eurofurence.App.Common.Results; @@ -13,7 +14,6 @@ using FirebaseAdmin.Messaging; using Google.Apis.Auth.OAuth2; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; namespace Eurofurence.App.Server.Services.PushNotifications { @@ -206,7 +206,7 @@ public Task PushSyncRequestAsync() // For Expo / React Native { "experienceId", _configuration.ExpoExperienceId }, { "scopeKey", _configuration.ExpoScopeKey }, - { "body", JsonConvert.SerializeObject(new + { "body", JsonSerializer.Serialize(new { @event = "Sync", cid = _conventionSettings.ConventionIdentifier diff --git a/src/Eurofurence.App.Server.Services/PushNotifications/WnsChannelManager.cs b/src/Eurofurence.App.Server.Services/PushNotifications/WnsChannelManager.cs index f99b4c2c..c08e93b4 100644 --- a/src/Eurofurence.App.Server.Services/PushNotifications/WnsChannelManager.cs +++ b/src/Eurofurence.App.Server.Services/PushNotifications/WnsChannelManager.cs @@ -7,8 +7,9 @@ using Eurofurence.App.Domain.Model.PushNotifications; using Eurofurence.App.Infrastructure.EntityFramework; using Eurofurence.App.Server.Services.Abstractions.PushNotifications; +using System.Text.Json; +using System.Text.Json.Nodes; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; namespace Eurofurence.App.Server.Services.PushNotifications { @@ -42,7 +43,7 @@ public async Task PushAnnouncementNotificationAsync(AnnouncementRecord announcem var recipients = GetAllRecipientsAsyncByTopic(_configuration.TargetTopic); - await SendRawAsync(recipients, JsonConvert.SerializeObject(message)); + await SendRawAsync(recipients, JsonSerializer.Serialize(message)); } public async Task SendToastAsync(string topic, string message) @@ -165,7 +166,7 @@ private IQueryable GetAllRecipientsAsyncByUid(str private Task SendRawAsync(IEnumerable recipients, object rawContent) { - return SendRawAsync(recipients, JsonConvert.SerializeObject(rawContent)); + return SendRawAsync(recipients, JsonSerializer.Serialize(rawContent)); } private async Task SendRawAsync(IEnumerable recipients, string rawContent) @@ -208,25 +209,23 @@ await _appDbContext.PushNotificationChannels.FirstOrDefaultAsync(entity => private async Task GetWnsAccessTokenAsync() { - using (var client = new HttpClient()) + using var client = new HttpClient(); + var payload = new FormUrlEncodedContent(new List> { - var payload = new FormUrlEncodedContent(new List> - { - new KeyValuePair("grant_type", "client_credentials"), - new KeyValuePair("client_id", _configuration.ClientId), - new KeyValuePair("client_secret", _configuration.ClientSecret), - new KeyValuePair("scope", "notify.windows.com") - }); + new KeyValuePair("grant_type", "client_credentials"), + new KeyValuePair("client_id", _configuration.ClientId), + new KeyValuePair("client_secret", _configuration.ClientSecret), + new KeyValuePair("scope", "notify.windows.com") + }); - var response = await client.PostAsync("https://login.live.com/accesstoken.srf", payload); - response.EnsureSuccessStatusCode(); + var response = await client.PostAsync("https://login.live.com/accesstoken.srf", payload); + response.EnsureSuccessStatusCode(); - var content = await response.Content.ReadAsStringAsync(); - var schema = new {token_type = "", access_token = "", expires_in = 0}; - var node = JsonConvert.DeserializeAnonymousType(content, schema); + var content = await response.Content.ReadAsStringAsync(); - return node.access_token; - } + var accessTokenNode = JsonNode.Parse(content)!; + + return (string)accessTokenNode["access_token"] ?? ""; } } } \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Web/Eurofurence.App.Server.Web.csproj b/src/Eurofurence.App.Server.Web/Eurofurence.App.Server.Web.csproj index d531c61d..cb01ae8e 100644 --- a/src/Eurofurence.App.Server.Web/Eurofurence.App.Server.Web.csproj +++ b/src/Eurofurence.App.Server.Web/Eurofurence.App.Server.Web.csproj @@ -28,7 +28,6 @@ - diff --git a/src/Eurofurence.App.Server.Web/Extensions/BaseFirstContractResolver.cs b/src/Eurofurence.App.Server.Web/Extensions/BaseFirstContractResolver.cs deleted file mode 100644 index ace45798..00000000 --- a/src/Eurofurence.App.Server.Web/Extensions/BaseFirstContractResolver.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Eurofurence.App.Server.Web.Extensions -{ - public class BaseFirstContractResolver : DefaultContractResolver - { - protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) - { - var properties = base.CreateProperties(type, memberSerialization); - if (properties != null) - return properties.OrderBy(p => p.DeclaringType.BaseTypesAndSelf().Count()).ToList(); - return properties; - } - } - - public static class TypeExtensions - { - public static IEnumerable BaseTypesAndSelf(this Type type) - { - while (type != null) - { - yield return type; - type = type.GetTypeInfo().BaseType; - } - } - } -} \ No newline at end of file diff --git a/src/Eurofurence.App.Server.Web/Extensions/JsonDateTimeConverter.cs b/src/Eurofurence.App.Server.Web/Extensions/JsonDateTimeConverter.cs new file mode 100644 index 00000000..0f72dc80 --- /dev/null +++ b/src/Eurofurence.App.Server.Web/Extensions/JsonDateTimeConverter.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; +using System.Text.Json; +using System.Text.Json.Serialization; +using System; + +namespace Eurofurence.App.Server.Web.Extensions +{ + public class JsonDateTimeConverter : JsonConverter + { + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(typeToConvert == typeof(DateTime)); + return DateTime.Parse(reader.GetString()!); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffK")); + } + } +} diff --git a/src/Eurofurence.App.Server.Web/Jobs/UpdateNewsJob.cs b/src/Eurofurence.App.Server.Web/Jobs/UpdateNewsJob.cs index 3e7f1f88..1659490b 100644 --- a/src/Eurofurence.App.Server.Web/Jobs/UpdateNewsJob.cs +++ b/src/Eurofurence.App.Server.Web/Jobs/UpdateNewsJob.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using Autofac.Features.AttributeFilters; using Eurofurence.App.Common.DataDiffUtils; @@ -12,8 +13,6 @@ using FluentScheduler; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Eurofurence.App.Server.Web.Jobs { @@ -75,25 +74,26 @@ public async Task ExecuteAsync() return; } + var jsonDocument = JsonDocument.Parse(response); + var records = jsonDocument.RootElement.EnumerateArray(); - var records = JsonConvert.DeserializeObject(response); var unixReference = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); var mapping = records.Select(j => new { Record = new AnnouncementRecord() { - ExternalReference = j["id"].Value(), - Area = j["news"]["type"].Value().UppercaseFirst(), - Author = j["news"]?["department"]?.Value().UppercaseFirst() ?? "Eurofurence", - Title = j["news"]["title"].Value(), - Content = j["news"]["message"].Value(), - ValidFromDateTimeUtc = unixReference.AddSeconds(j["date"].Value()).ToUniversalTime(), + ExternalReference = j.GetProperty("id").GetString(), + Area = j.GetProperty("news").GetProperty("type").GetString().UppercaseFirst(), + Author = j.GetProperty("news").GetProperty("department").GetString().UppercaseFirst() ?? "Eurofurence", + Title = j.GetProperty("news").GetProperty("title").GetString(), + Content = j.GetProperty("news").GetProperty("message").GetString(), + ValidFromDateTimeUtc = unixReference.AddSeconds(j.GetProperty("date").GetDouble()).ToUniversalTime(), ValidUntilDateTimeUtc = unixReference - .AddSeconds(j["news"]["valid_until"].Value()).ToUniversalTime(), - ImageId = GetImageIdForEntryAsync(j["id"].Value(), j["data"]?["imagedata"]?.Value()).Result + .AddSeconds(j.GetProperty("news").GetProperty("valid_until").GetDouble()).ToUniversalTime(), + ImageId = GetImageIdForEntryAsync(j.GetProperty("id").GetString(), j.GetProperty("data").GetProperty("imagedata").GetString()).Result }, - Type = j["news"]["type"].Value() + Type = j.GetProperty("news").GetProperty("type").GetString() }).ToList(); foreach (var item in mapping) diff --git a/src/Eurofurence.App.Server.Web/Startup.cs b/src/Eurofurence.App.Server.Web/Startup.cs index a7efc227..feab60c8 100644 --- a/src/Eurofurence.App.Server.Web/Startup.cs +++ b/src/Eurofurence.App.Server.Web/Startup.cs @@ -23,8 +23,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using Serilog; using Serilog.Context; using Serilog.Events; @@ -33,6 +31,8 @@ using Swashbuckle.AspNetCore.SwaggerUI; using System; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using Eurofurence.App.Infrastructure.EntityFramework; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -74,22 +74,29 @@ public void ConfigureServices(IServiceCollection services) }); services.AddMvc(options => - { - options.EnableEndpointRouting = false; - options.MaxModelValidationErrors = 0; - options.Filters.Add(new CustomValidationAttributesFilter()); - }) - .AddJsonOptions(opt => opt.JsonSerializerOptions.PropertyNamingPolicy = null) - .AddNewtonsoftJson(options => - { - options.SerializerSettings.ContractResolver = new BaseFirstContractResolver(); - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); - options.SerializerSettings.Converters.Add(new IsoDateTimeConverter { - DateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffK" + options.EnableEndpointRouting = false; + options.MaxModelValidationErrors = 0; + options.Filters.Add(new CustomValidationAttributesFilter()); + }) + .AddJsonOptions(opt => + { + opt.JsonSerializerOptions.PropertyNamingPolicy = null; + opt.JsonSerializerOptions.PropertyNameCaseInsensitive = true; + opt.JsonSerializerOptions.WriteIndented = true; + opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + opt.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter()); }); - }); + + JsonSerializerOptions serializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = null, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + Converters = { new JsonStringEnumConverter(), new JsonDateTimeConverter() } + }; + + services.AddSingleton(s => serializerOptions); services.Configure(options => { diff --git a/test/Eurofurence.App.Server.Services.Tests/LassieApiClientTests.cs b/test/Eurofurence.App.Server.Services.Tests/LassieApiClientTests.cs new file mode 100644 index 00000000..beff5aec --- /dev/null +++ b/test/Eurofurence.App.Server.Services.Tests/LassieApiClientTests.cs @@ -0,0 +1,78 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Xunit; +using Eurofurence.App.Server.Services.Abstractions.Lassie; +using Eurofurence.App.Server.Web.Extensions; +using static Eurofurence.App.Server.Services.Lassie.LassieApiClient; +using System; + +namespace Eurofurence.App.Server.Services.Tests +{ + public class LassieApiClientTests + { + public static JsonSerializerOptions SerializerOptions = new() + { + PropertyNamingPolicy = null, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + Converters = { new JsonStringEnumConverter(), new JsonDateTimeConverter() } + }; + + [Fact] + public void LassieApiClient_DeserializeLostAndFoundResponse_ValidReturnValue() + { + // Arrange + string _testData = @" + { + ""data"": [ + { + ""id"": 1925, + ""image"": null, + ""thumb"": null, + ""title"": ""Image"", + ""description"": ""invisible, watercolour, found in Schroedinger's box"", + ""status"": ""F"", + ""lost_timestamp"": null, + ""found_timestamp"": ""2023-11-24 00:27:24"", + ""return_timestamp"": null + }, + { + ""id"": 1921, + ""image"": ""https://api.lassie.furcom.org/images/lostandfound_db/fe3c6670a33d8f4199ffa95a5c23b622.png"", + ""thumb"": ""https://api.lassie.furcom.org/images/lostandfound_db/thumbnail/fe3c6670a33d8f4199ffa95a5c23b622.png"", + ""title"": ""Sense of Taste"", + ""description"": ""lost in Hotel Restaurant"", + ""status"": ""L"", + ""lost_timestamp"": ""2023-11-01 16:13:02"", + ""found_timestamp"": null, + ""return_timestamp"": null + }, + { + ""id"": 1920, + ""image"": ""https://api.lassie.furcom.org/images/lostandfound_db/bc7c343fb3807d4d41b7e04fa909aad5.png"", + ""thumb"": ""https://api.lassie.furcom.org/images/lostandfound_db/thumbnail/bc7c343fb3807d4d41b7e04fa909aad5.png"", + ""title"": ""Vape"", + ""description"": ""black / rainbow, \""GEEKVAPE\"", found at Open Stage"", + ""status"": ""F"", + ""lost_timestamp"": null, + ""found_timestamp"": ""2023-09-30 23:52:38"", + ""return_timestamp"": null + } + ] + } + "; + + var expectedLength = 3; + + // Act + var result = + JsonSerializer.Deserialize>(_testData, SerializerOptions); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.Equal(result.Data.Length, expectedLength); + Assert.Equal(new DateTime(2023, 11, 24, 0, 27, 24, DateTimeKind.Utc), result.Data[0].FoundDateTimeLocal); + } + } +} \ No newline at end of file diff --git a/test/Eurofurence.App.Server.Web.Tests/Eurofurence.App.Server.Web.Tests.csproj b/test/Eurofurence.App.Server.Web.Tests/Eurofurence.App.Server.Web.Tests.csproj new file mode 100644 index 00000000..ae47b4d9 --- /dev/null +++ b/test/Eurofurence.App.Server.Web.Tests/Eurofurence.App.Server.Web.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/Eurofurence.App.Server.Web.Tests/Extensions/JsonDateTimeConverterTests.cs b/test/Eurofurence.App.Server.Web.Tests/Extensions/JsonDateTimeConverterTests.cs new file mode 100644 index 00000000..0b524038 --- /dev/null +++ b/test/Eurofurence.App.Server.Web.Tests/Extensions/JsonDateTimeConverterTests.cs @@ -0,0 +1,42 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Eurofurence.App.Server.Web.Extensions; + +namespace Eurofurence.App.Server.Web.Tests.Extensions +{ + public class JsonDateTimeConverterTests + { + public static TheoryData TestData = + new() + { + { "{\"Data\": \"2023-11-24 00:27:24\"}", new DateTime(2023, 11, 24, 0, 27, 24, DateTimeKind.Utc)}, + { "{\"Data\": \"2023-01-01 00:00:00\"}", new DateTime(2023, 01, 01, 0, 0, 0, DateTimeKind.Utc)}, + { "{\"Data\": \"2024-02-29 12:00:00\"}", new DateTime(2024, 02, 29, 12, 0, 0, DateTimeKind.Utc)} + }; + + public static JsonSerializerOptions SerializerOptions = new() + { + PropertyNamingPolicy = null, + PropertyNameCaseInsensitive = true, + WriteIndented = true, + Converters = { new JsonStringEnumConverter(), new JsonDateTimeConverter() } + }; + + public class TestDataWrapper + { + public DateTime Data { get; set; } + } + + [Theory] + [MemberData(nameof(TestData))] + public void JsonDateTimeConverterTests_Deserialize_ValidReturnValue(string json, DateTime dateTime) + { + // Act + var result = JsonSerializer.Deserialize(json, SerializerOptions); + + // Assert + Assert.NotNull(result); + Assert.Equal(dateTime, result.Data); + } + } +} diff --git a/test/Eurofurence.App.Server.Web.Tests/GlobalUsings.cs b/test/Eurofurence.App.Server.Web.Tests/GlobalUsings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/test/Eurofurence.App.Server.Web.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/test/Eurofurence.App.Tests.Common/CollectionGameServiceTests.cs b/test/Eurofurence.App.Tests.Common/CollectionGameServiceTests.cs new file mode 100644 index 00000000..e262fb55 --- /dev/null +++ b/test/Eurofurence.App.Tests.Common/CollectionGameServiceTests.cs @@ -0,0 +1,311 @@ +using Autofac; +using Eurofurence.App.Common.Results; +using Eurofurence.App.Domain.Model.Abstractions; +using Eurofurence.App.Domain.Model.Fursuits; +using Eurofurence.App.Domain.Model.Fursuits.CollectingGame; +using Eurofurence.App.Domain.Model.Security; +using Eurofurence.App.Server.Services.Abstractions.Fursuits; +using Eurofurence.App.Server.Services.Abstractions.Telegram; +using Moq; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Eurofurence.App.Server.Services.Tests +{ + public class CollectionGameServiceTests + { + protected IContainer _container; + private readonly IEntityRepository _fursuitBadgeRepository; + private readonly IEntityRepository _fursuitParticipationRepository; + private readonly IEntityRepository _playerParticipationRepository; + private readonly IEntityRepository _regSysIdentityRepository; + private readonly IEntityRepository _tokenRepository; + private readonly ICollectingGameService _collectingGameService; + private const string INVALID_TOKEN = "INVALID-TOKEN"; + + public CollectionGameServiceTests() + { + var builder = new ContainerBuilder(); + builder.RegisterModule(new App.Tests.Common.TestLogger.AutofacModule()); + builder.RegisterModule(new App.Tests.Common.InMemoryRepository.AutofacModule()); + builder.RegisterModule(new DependencyResolution.AutofacModule(null)); + + var _telegramMessageSender = new Mock().As(); + var _configuration = new CollectionGameConfiguration(); + + builder.RegisterInstance(_telegramMessageSender.Object).As(); + builder.RegisterInstance(_configuration).As(); + + _container = builder.Build(); + + _collectingGameService = _container.Resolve(); + _fursuitParticipationRepository = _container.Resolve>(); + _playerParticipationRepository = _container.Resolve>(); + _regSysIdentityRepository = _container.Resolve>(); + _fursuitBadgeRepository = _container.Resolve>(); + _tokenRepository = _container.Resolve>(); + } + + private async Task CreateRegSysIdentityAsync() + { + var id = Guid.NewGuid(); + var record = new RegSysIdentityRecord() + { + Id = id, + Uid = $"Test:{id}", + Username = $"Test Attendee {id}", + Roles = new[] { "Attendee" }, + }; + + await _regSysIdentityRepository.InsertOneAsync(record); + return record; + } + + private async Task CreateFursuitBadgeAsync(string ownerUid) + { + var record = new FursuitBadgeRecord() + { + Id = Guid.NewGuid(), + OwnerUid = ownerUid, + Name = $"Suit of attendee {ownerUid}" + }; + + await _fursuitBadgeRepository.InsertOneAsync(record); + return record; + } + + private async Task CreateTokenAsync() + { + var id = Guid.NewGuid(); + var record = new TokenRecord() + { + Id = id, + Value = id.ToString() + }; + + await _tokenRepository.InsertOneAsync(record); + return record; + } + + [Fact(DisplayName = "Token Registration: Valid scenario")] + public async Task CollectingGameService_WhenRegisteringValidTokenForAFursuitBadge_ThenOperationsSuccessful() + { + var cgs = _container.Resolve(); + + var testUser = await CreateRegSysIdentityAsync(); + var testToken = await CreateTokenAsync(); + var testUserFursuitBadge = await CreateFursuitBadgeAsync(ownerUid: testUser.Uid); + + var result = await cgs.RegisterTokenForFursuitBadgeForOwnerAsync(testUser.Uid, testUserFursuitBadge.Id, testToken.Value); + var testUserFursuitParticipation = await _fursuitParticipationRepository.FindOneAsync(a => a.OwnerUid == testUser.Uid); + testToken = await _tokenRepository.FindOneAsync(testToken.Id); + + Assert.True(result.IsSuccessful); + Assert.NotNull(testUserFursuitParticipation); + Assert.True(testToken.IsLinked); + Assert.Equal(testToken.LinkedFursuitParticipantUid, testUserFursuitParticipation.Id); + } + + [Fact(DisplayName = "Token Registration: Passing invalid token fails")] + public async Task CollectingGameService_WhenRegisteringInvalidTokenForAFursuitBadge_ThenOperationFailsWithCodeInvalidToken() + { + var cgs = _container.Resolve(); + + var testUser = await CreateRegSysIdentityAsync(); + var testToken = INVALID_TOKEN; + var testUserFursuitBadge = await CreateFursuitBadgeAsync(ownerUid: testUser.Uid); + + Assert.NotNull(testUser); + Assert.NotNull(testUserFursuitBadge); + + var result = await cgs.RegisterTokenForFursuitBadgeForOwnerAsync(testUser.Uid, testUserFursuitBadge.Id, testToken); + var testUserFursuitParticipation = await _fursuitParticipationRepository.FindOneAsync(a => a.OwnerUid == testUser.Uid); + + Assert.False(result.IsSuccessful); + Assert.Equal("INVALID_TOKEN", result.ErrorCode); + Assert.Null(testUserFursuitParticipation); + } + + [Fact(DisplayName = "Token Registration: Passing invalid fursuitBadgeId fails")] + public async Task CollectingGameService_WhenRegisteringValidTokenForInvalidFursuitBadge_ThenOperationFailsWithCodeInvalidToken() + { + var cgs = _container.Resolve(); + + var testUser = await CreateRegSysIdentityAsync(); + var testToken = await CreateTokenAsync(); + var invalidFursuitBadgeId = Guid.NewGuid(); + + Assert.NotNull(testUser); + Assert.NotNull(testToken); + + var result = await cgs.RegisterTokenForFursuitBadgeForOwnerAsync(testUser.Uid, invalidFursuitBadgeId, testToken.Value); + + Assert.False(result.IsSuccessful); + Assert.Equal("INVALID_FURSUIT_BADGE_ID", result.ErrorCode); + } + + + private struct TwoPlayersWithOneFursuitToken + { + public RegSysIdentityRecord player1WithFursuit; + public FursuitBadgeRecord player1FursuitBadge; + public TokenRecord player1Token; + public RegSysIdentityRecord player2WithoutFursuit; + } + + private async Task SetupTwoPlayersWithOneFursuitTokenAsync() + { + var result = new TwoPlayersWithOneFursuitToken(); + + result.player1WithFursuit = await CreateRegSysIdentityAsync(); + result.player1Token = await CreateTokenAsync(); + result.player1FursuitBadge = await CreateFursuitBadgeAsync(result.player1WithFursuit.Uid); + + await _collectingGameService.RegisterTokenForFursuitBadgeForOwnerAsync( + result.player1WithFursuit.Uid, + result.player1FursuitBadge.Id, + result.player1Token.Value + ); + + result.player2WithoutFursuit = await CreateRegSysIdentityAsync(); + + return result; + } + + [Fact(DisplayName = "Collect Token: Valid scenario")] + public async Task CollectingGameService_WhenCollectingValidToken_ThenOperationIsSuccessful() + { + var setup = await SetupTwoPlayersWithOneFursuitTokenAsync(); + + var result = await _collectingGameService.CollectTokenForPlayerAsync( + setup.player2WithoutFursuit.Uid, + setup.player1Token.Value + ); + + var player1FursuitParticipation = await _fursuitParticipationRepository.FindOneAsync(a => a.OwnerUid == setup.player1WithFursuit.Uid); + var player2PlayerParticipation = await _playerParticipationRepository.FindOneAsync(a => a.PlayerUid == setup.player2WithoutFursuit.Uid); + + Assert.True(result.IsSuccessful); + Assert.Equal(1, player1FursuitParticipation.CollectionCount); + Assert.Equal(1, player2PlayerParticipation.CollectionCount); + Assert.Contains(player1FursuitParticipation.CollectionEntries, a => a.PlayerParticipationUid == setup.player2WithoutFursuit.Uid); + Assert.Contains(player2PlayerParticipation.CollectionEntries, a => a.FursuitParticipationUid == player1FursuitParticipation.Id); + } + + [Fact(DisplayName = "Collect Token: Collecting valid token twice fails")] + public async Task CollectingGameService_WhenCollectingValidTokenMultipleTimes_ThenOperationFails() + { + var setup = await SetupTwoPlayersWithOneFursuitTokenAsync(); + + await _collectingGameService.CollectTokenForPlayerAsync( + setup.player2WithoutFursuit.Uid, + setup.player1Token.Value + ); + + var result = await _collectingGameService.CollectTokenForPlayerAsync( + setup.player2WithoutFursuit.Uid, + setup.player1Token.Value + ); + + Assert.False(result.IsSuccessful); + Assert.Equal("INVALID_TOKEN_ALREADY_COLLECTED", result.ErrorCode); + } + + + [Fact(DisplayName = "Collect Token: Collecting an invalid token fails")] + public async Task CollectingGameService_WhenCollectingInvalidToken_ThenOperationFails() + { + var setup = await SetupTwoPlayersWithOneFursuitTokenAsync(); + + var result = await _collectingGameService.CollectTokenForPlayerAsync( + setup.player2WithoutFursuit.Uid, + INVALID_TOKEN + ); + + Assert.False(result.IsSuccessful); + Assert.Equal("INVALID_TOKEN", result.ErrorCode); + } + + [Fact(DisplayName = "Collect Token: Collecting your own suit fails")] + public async Task CollectingGameService_WhenCollectingValidTokenOfYourOwnSuit_ThenOperationFails() + { + var setup = await SetupTwoPlayersWithOneFursuitTokenAsync(); + + var result = await _collectingGameService.CollectTokenForPlayerAsync( + setup.player1WithFursuit.Uid, + setup.player1Token.Value + ); + + Assert.False(result.IsSuccessful); + Assert.Equal("INVALID_TOKEN_OWN_SUIT", result.ErrorCode); + } + + [Fact(DisplayName = "Collect Token: Collecting too many wrong tokens leads to a ban")] + public async Task CollectingGameService_WhenCollectingManyInvalidTokenOfYourOwnSuit_ThenPlayerIsBanned() + { + var setup = await SetupTwoPlayersWithOneFursuitTokenAsync(); + + IResult result = null; + for (int i = 0; i < 20; i++) + result = await _collectingGameService.CollectTokenForPlayerAsync( + setup.player1WithFursuit.Uid, + INVALID_TOKEN + ); + + Assert.False(result.IsSuccessful); + Assert.Equal("BANNED", result.ErrorCode); + } + + [Fact(DisplayName = "Scoreboard: For players/fursuits with identical collection count, the earlier achiever ranks higher")] + public async Task CollectingGameService_WhenPlayersOrFursuitsHaveSameCollectionCount_ThenWhoeverAchievedItFirstRanksHigher() + { + var players = new RegSysIdentityRecord[3]; + var tokens = new TokenRecord[3]; + var fursuitBadges = new FursuitBadgeRecord[3]; + + // 3 players, each with suit + for (int i = 0; i < 3; i++) + { + players[i] = await CreateRegSysIdentityAsync(); + tokens[i] = await CreateTokenAsync(); + fursuitBadges[i] = await CreateFursuitBadgeAsync(players[i].Uid); + + await _collectingGameService.RegisterTokenForFursuitBadgeForOwnerAsync( + players[i].Uid, fursuitBadges[i].Id, tokens[i].Value); + } + + // Each catches everyone else. + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + if (j == i) continue; + + var result = await _collectingGameService.CollectTokenForPlayerAsync( + players[i].Uid, tokens[j].Value); + + Assert.True(result.IsSuccessful); + } + } + + var playerScoreboard = await _collectingGameService.GetPlayerScoreboardEntriesAsync(5); + var fursuitScoreboard = await _collectingGameService.GetFursuitScoreboardEntriesAsync(5); + + // 1 catches first, then 2, then 3 + Assert.Equal(3, playerScoreboard.Value.Length); + Assert.True(playerScoreboard.Value.All(a => a.CollectionCount == 2)); + Assert.Equal(players[0].Username, playerScoreboard.Value[0].Name); + Assert.Equal(players[1].Username, playerScoreboard.Value[1].Name); + Assert.Equal(players[2].Username, playerScoreboard.Value[2].Name); + + // 3 gets the 2 catches first (by 1 + 2), then 1 (by 2 + 3), then 2 + Assert.Equal(3, fursuitScoreboard.Value.Length); + Assert.True(fursuitScoreboard.Value.All(a => a.CollectionCount == 2)); + Assert.Equal(fursuitBadges[2].Id, fursuitScoreboard.Value[0].BadgeId); + Assert.Equal(fursuitBadges[0].Id, fursuitScoreboard.Value[1].BadgeId); + Assert.Equal(fursuitBadges[1].Id, fursuitScoreboard.Value[2].BadgeId); + } + } +}