From 0595fd8c34a9710d9d8d7b220878fea8de56bb4e Mon Sep 17 00:00:00 2001 From: Adrian Hall Date: Fri, 28 Feb 2025 11:16:38 -0800 Subject: [PATCH] (#217) Added dynamic proxies tests, plus some Cosmos test fixes. (#303) --- .../CosmosTableData.cs | 2 - .../Offline/DynamicProxies_Tests.cs | 166 ++++++++++++++++++ .../CosmosDbRepository_Tests.cs | 7 +- .../CosmosTableData_Tests.cs | 42 +++++ .../Models/CosmosDbMovie.cs | 5 - .../Options/PackedKeyOptions.cs | 14 +- .../PackedKeyRepository_Tests.cs | 10 +- 7 files changed, 220 insertions(+), 26 deletions(-) create mode 100644 tests/CommunityToolkit.Datasync.Client.Test/Offline/DynamicProxies_Tests.cs create mode 100644 tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosTableData_Tests.cs diff --git a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs index 781956b..c58758c 100644 --- a/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs +++ b/src/CommunityToolkit.Datasync.Server.CosmosDb/CosmosTableData.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Datasync.Server; -using System.ComponentModel.DataAnnotations.Schema; using System.Text; using System.Text.Json.Serialization; diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Offline/DynamicProxies_Tests.cs b/tests/CommunityToolkit.Datasync.Client.Test/Offline/DynamicProxies_Tests.cs new file mode 100644 index 0000000..239209c --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Client.Test/Offline/DynamicProxies_Tests.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Client.Offline; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace CommunityToolkit.Datasync.Client.Test.Offline; + +/// +/// Tests for: https://github.com/CommunityToolkit/Datasync/issues/217 +/// See https://github.com/david1995/CommunityToolKit.Datasync-DynamicProxiesRepro/blob/main/ConsoleApp1/Program.cs +/// See https://github.com/CommunityToolkit/Datasync/issues/211 +/// +[ExcludeFromCodeCoverage] +public class DynamicProxies_Tests : IDisposable +{ + private readonly string temporaryDbPath; + private readonly string dataSource; + private bool _disposedValue; + + public DynamicProxies_Tests() + { + this.temporaryDbPath = $"{Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())}.sqlite"; + this.dataSource = $"Data Source={this.temporaryDbPath};Foreign Keys=False"; + } + + protected virtual void Dispose(bool disposing) + { + if (!this._disposedValue) + { + if (disposing) + { + // Really release the DB + GC.Collect(); + GC.WaitForPendingFinalizers(); + + // If the file exists, it should be able to be deleted now. + if (File.Exists(this.temporaryDbPath)) + { + File.Delete(this.temporaryDbPath); + } + } + + this._disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + [Fact] + public async Task OfflineDbContext_Queue_SupportsDynamicProxies() + { + SqliteConnection connection = new(this.dataSource); + connection.Open(); + + try + { + DbContextOptions dbContextOptions = new DbContextOptionsBuilder() + .UseSqlite(connection) + .UseLazyLoadingProxies() + .Options; + + string key = Guid.CreateVersion7().ToString(); + await using (DynamicProxiesTestContext context = new(dbContextOptions)) + { + context.Database.EnsureCreated(); + await context.DynamicProxiesEntities1.AddAsync(new DynamicProxiesEntity1 + { + Id = key, + Name = $"Test {DateTime.Now}", + LocalNotes = "These notes should not be serialized into DatasyncOperationsQueue", + RelatedEntity = new() { Id = Guid.NewGuid().ToString() } + }); + await context.SaveChangesAsync(); + } + + await using (DynamicProxiesTestContext context = new(dbContextOptions)) + { + DatasyncOperation operationAfterInsert = await context.DatasyncOperationsQueue.FirstAsync(o => o.ItemId == key); + operationAfterInsert.EntityType.Should().EndWith("DynamicProxiesEntity1"); + operationAfterInsert.Version.Should().Be(0); + + // The LocalNotes should not be included + operationAfterInsert.Item.Should().NotContain("\"localNotes\":"); + + // Update the entity within the DbContext + DynamicProxiesEntity1 entity = await context.DynamicProxiesEntities1.FirstAsync(e => e.Id == key); + string updatedName = $"Updated name {DateTime.Now}"; + entity.Name = updatedName; + await context.SaveChangesAsync(); + + // There should be 1 operation. + int operationsWithItemId = await context.DatasyncOperationsQueue.CountAsync(o => o.ItemId == key); + operationsWithItemId.Should().Be(1); + + // Here is the operation after edit. + DatasyncOperation operationAfterEdit = await context.DatasyncOperationsQueue.FirstAsync(o => o.ItemId == key); + operationAfterEdit.EntityType.Should().EndWith("DynamicProxiesEntity1"); + operationAfterEdit.Version.Should().Be(1); + operationAfterEdit.Item.Should().Contain($"\"name\":\"{updatedName}\""); + + // The LocalNotes should not be included + operationAfterEdit.Item.Should().NotContain("\"localNotes\":"); + } + } + finally + { + connection.Close(); + connection.Dispose(); + SqliteConnection.ClearAllPools(); + } + } +} + +public class DynamicProxiesTestContext(DbContextOptions options) : OfflineDbContext(options) +{ + public virtual DbSet DynamicProxiesEntities1 { get; set; } + + public virtual DbSet DynamicProxiesEntities2 { get; set; } + + protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder optionsBuilder) + { + optionsBuilder.Entity(typeof(DynamicProxiesEntity1), _ => { }); + optionsBuilder.Entity(typeof(DynamicProxiesEntity2), _ => { }); + } +} + +public abstract class DatasyncBase +{ + [Key, StringLength(200)] + public string Id { get; set; } = null!; + + public DateTimeOffset? UpdatedAt { get; set; } + + public string Version { get; set; } + + public bool Deleted { get; set; } +} + +public class DynamicProxiesEntity1 : DatasyncBase +{ + [StringLength(255)] + public string Name { get; set; } + + // this should not be synchronized + [JsonIgnore] + [StringLength(255)] + public string LocalNotes { get; set; } + + [StringLength(200)] + public string RelatedEntityId { get; set; } + + // this property should also not be serialized + [JsonIgnore] + public virtual DynamicProxiesEntity2 RelatedEntity { get; set; } +} + +public class DynamicProxiesEntity2 : DatasyncBase; diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs index 74d0c6f..9815f13 100644 --- a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosDbRepository_Tests.cs @@ -5,7 +5,6 @@ using CommunityToolkit.Datasync.Server.Abstractions.Json; using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models; using CommunityToolkit.Datasync.TestCommon; -using CommunityToolkit.Datasync.TestCommon.Databases; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.Linq; using System.Collections.ObjectModel; @@ -42,6 +41,7 @@ protected virtual void Dispose(bool disposing) } override protected bool CanRunLiveTests() => !string.IsNullOrEmpty(this.connectionString); + protected override async Task GetEntityAsync(string id) { try @@ -109,7 +109,7 @@ public async Task InitializeAsync() CompositeIndexes = { new Collection() - { + { new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending }, new() { Path = "/id", Order = CompositePathSortOrder.Ascending } }, @@ -121,7 +121,7 @@ public async Task InitializeAsync() } } }); - + foreach (CosmosDbMovie movie in TestCommon.TestData.Movies.OfType()) { movie.UpdatedAt = DateTimeOffset.UtcNow; @@ -134,7 +134,6 @@ public async Task InitializeAsync() this._client, new CosmosSingleTableOptions("Movies", "Movies") ); - } } diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosTableData_Tests.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosTableData_Tests.cs new file mode 100644 index 0000000..e834fc0 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/CosmosTableData_Tests.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Datasync.Common.Test; +using CommunityToolkit.Datasync.Server.CosmosDb; +using CommunityToolkit.Datasync.TestCommon; +using FluentAssertions; + +namespace CommunityToolkit.Datasync.Server.EntityFrameworkCore.Test; + +[ExcludeFromCodeCoverage] +public class CosmosTableData_Tests +{ + [Theory, ClassData(typeof(ITableData_TestData))] + public void CCosmosTableData_Equals(ITableData a, ITableData b, bool expected) + { + CCosmosTableData entity_a = a.ToTableEntity(); + CCosmosTableData entity_b = b.ToTableEntity(); + + entity_a.Equals(entity_b).Should().Be(expected); + entity_b.Equals(entity_a).Should().Be(expected); + + entity_a.Equals(null).Should().BeFalse(); + entity_b.Equals(null).Should().BeFalse(); + } + + [Fact] + public void CCosmosTableData_MetadataRoundtrips() + { + DateTimeOffset testTime = DateTimeOffset.Now; + + CCosmosTableData sut1 = new() { Id = "t1", Deleted = false, UpdatedAt = testTime, Version = [0x61, 0x62, 0x63, 0x64, 0x65] }; + sut1.Version.Should().BeEquivalentTo("abcde"u8.ToArray()); + sut1.UpdatedAt.Should().Be(testTime); + } +} + +[ExcludeFromCodeCoverage] +public class CCosmosTableData : CosmosTableData +{ +} diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs index 76d7d06..8bdb67b 100644 --- a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs +++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Models/CosmosDbMovie.cs @@ -3,11 +3,6 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Datasync.TestCommon.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace CommunityToolkit.Datasync.Server.CosmosDb.Test.Models; public class CosmosDbMovie : CosmosTableData, IMovie diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs index 00827d9..5e65eef 100644 --- a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs +++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/Options/PackedKeyOptions.cs @@ -4,20 +4,12 @@ using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models; using Microsoft.Azure.Cosmos; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace CommunityToolkit.Datasync.Server.CosmosDb.Test.Options; -public class PackedKeyOptions : CosmosSingleTableOptions +public class PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true) + : CosmosSingleTableOptions(databaseId, containerId, shouldUpdateTimestamp) { - public PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true) : base(databaseId, containerId, shouldUpdateTimestamp) - { - } - public override Func IdGenerator => (entity) => $"{Guid.NewGuid()}:{entity.Year}"; public override string GetPartitionKey(CosmosDbMovie entity, out PartitionKey partitionKey) { @@ -35,7 +27,9 @@ public override string ParsePartitionKey(string id, out PartitionKey partitionKe } if (!int.TryParse(parts[1], out int year)) + { throw new ArgumentException("Invalid ID Part"); + } partitionKey = new PartitionKey(year); return id; diff --git a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs index c71abb4..3ee6a60 100644 --- a/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Server.CosmosDb.Test/PackedKeyRepository_Tests.cs @@ -6,7 +6,6 @@ using CommunityToolkit.Datasync.Server.CosmosDb.Test.Models; using CommunityToolkit.Datasync.Server.CosmosDb.Test.Options; using CommunityToolkit.Datasync.TestCommon; -using CommunityToolkit.Datasync.TestCommon.Databases; using FluentAssertions; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.Linq; @@ -57,7 +56,9 @@ protected override async Task GetEntityAsync(string id) } if (!int.TryParse(parts[1], out int year)) + { throw new ArgumentException("Invalid ID Part"); + } return await this._container.ReadItemAsync(id, new PartitionKey(year)); } @@ -122,7 +123,7 @@ public async Task InitializeAsync() CompositeIndexes = { new Collection() - { + { new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending }, new() { Path = "/id", Order = CompositePathSortOrder.Ascending } }, @@ -134,7 +135,7 @@ public async Task InitializeAsync() } } }); - + foreach (CosmosDbMovie movie in TestData.Movies.OfType()) { movie.Id = $"{Guid.NewGuid()}:{movie.Year}"; @@ -148,7 +149,6 @@ public async Task InitializeAsync() this._client, new PackedKeyOptions("Movies", "Movies") ); - } } @@ -180,6 +180,7 @@ public async Task ReadAsync_Throws_OnMalformedId(string id) (await act.Should().ThrowAsync()).WithStatusCode(400); } + [SkippableTheory] [InlineData("BadId")] [InlineData("12345-12345")] @@ -193,5 +194,4 @@ public async Task DeleteAsync_Throws_OnMalformedIds(string id) (await act.Should().ThrowAsync()).WithStatusCode(400); (await GetEntityCountAsync()).Should().Be(TestData.Movies.Count()); } - }