Skip to content

Commit

Permalink
(#217) Added dynamic proxies tests, plus some Cosmos test fixes. (#303)
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianhall authored Feb 28, 2025
1 parent 9a0f507 commit 0595fd8
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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
/// </summary>
[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<DynamicProxiesTestContext> dbContextOptions = new DbContextOptionsBuilder<DynamicProxiesTestContext>()
.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<DynamicProxiesEntity1> DynamicProxiesEntities1 { get; set; }

public virtual DbSet<DynamicProxiesEntity2> 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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +41,7 @@ protected virtual void Dispose(bool disposing)
}

override protected bool CanRunLiveTests() => !string.IsNullOrEmpty(this.connectionString);

protected override async Task<CosmosDbMovie> GetEntityAsync(string id)
{
try
Expand Down Expand Up @@ -109,7 +109,7 @@ public async Task InitializeAsync()
CompositeIndexes =
{
new Collection<CompositePath>()
{
{
new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
},
Expand All @@ -121,7 +121,7 @@ public async Task InitializeAsync()
}
}
});

foreach (CosmosDbMovie movie in TestCommon.TestData.Movies.OfType<CosmosDbMovie>())
{
movie.UpdatedAt = DateTimeOffset.UtcNow;
Expand All @@ -134,7 +134,6 @@ public async Task InitializeAsync()
this._client,
new CosmosSingleTableOptions<CosmosDbMovie>("Movies", "Movies")
);

}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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>();
CCosmosTableData entity_b = b.ToTableEntity<CCosmosTableData>();

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
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CosmosDbMovie>
public class PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true)
: CosmosSingleTableOptions<CosmosDbMovie>(databaseId, containerId, shouldUpdateTimestamp)
{
public PackedKeyOptions(string databaseId, string containerId, bool shouldUpdateTimestamp = true) : base(databaseId, containerId, shouldUpdateTimestamp)
{
}

public override Func<CosmosDbMovie, string> IdGenerator => (entity) => $"{Guid.NewGuid()}:{entity.Year}";
public override string GetPartitionKey(CosmosDbMovie entity, out PartitionKey partitionKey)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -57,7 +56,9 @@ protected override async Task<CosmosDbMovie> GetEntityAsync(string id)
}

if (!int.TryParse(parts[1], out int year))
{
throw new ArgumentException("Invalid ID Part");
}

return await this._container.ReadItemAsync<CosmosDbMovie>(id, new PartitionKey(year));
}
Expand Down Expand Up @@ -122,7 +123,7 @@ public async Task InitializeAsync()
CompositeIndexes =
{
new Collection<CompositePath>()
{
{
new() { Path = "/updatedAt", Order = CompositePathSortOrder.Ascending },
new() { Path = "/id", Order = CompositePathSortOrder.Ascending }
},
Expand All @@ -134,7 +135,7 @@ public async Task InitializeAsync()
}
}
});

foreach (CosmosDbMovie movie in TestData.Movies.OfType<CosmosDbMovie>())
{
movie.Id = $"{Guid.NewGuid()}:{movie.Year}";
Expand All @@ -148,7 +149,6 @@ public async Task InitializeAsync()
this._client,
new PackedKeyOptions("Movies", "Movies")
);

}
}

Expand Down Expand Up @@ -180,6 +180,7 @@ public async Task ReadAsync_Throws_OnMalformedId(string id)

(await act.Should().ThrowAsync<HttpException>()).WithStatusCode(400);
}

[SkippableTheory]
[InlineData("BadId")]
[InlineData("12345-12345")]
Expand All @@ -193,5 +194,4 @@ public async Task DeleteAsync_Throws_OnMalformedIds(string id)
(await act.Should().ThrowAsync<HttpException>()).WithStatusCode(400);
(await GetEntityCountAsync()).Should().Be(TestData.Movies.Count<CosmosDbMovie>());
}

}

0 comments on commit 0595fd8

Please sign in to comment.