diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Helpers/ByteVersionMovie.cs b/tests/CommunityToolkit.Datasync.Client.Test/Helpers/ByteVersionMovie.cs new file mode 100644 index 00000000..a15447b6 --- /dev/null +++ b/tests/CommunityToolkit.Datasync.Client.Test/Helpers/ByteVersionMovie.cs @@ -0,0 +1,55 @@ +// 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.Server; +using CommunityToolkit.Datasync.TestCommon.Models; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CommunityToolkit.Datasync.Client.Test.Helpers; + +[ExcludeFromCodeCoverage] +public class ByteVersionMovie +{ + public ByteVersionMovie() + { + } + + public ByteVersionMovie(object source) + { + if (source is ITableData metadata) + { + Id = metadata.Id; + Deleted = metadata.Deleted; + UpdatedAt = metadata.UpdatedAt; + Version = [..metadata.Version]; + } + + if (source is IMovie movie) + { + BestPictureWinner = movie.BestPictureWinner; + Duration = movie.Duration; + Rating = movie.Rating; + ReleaseDate = movie.ReleaseDate; + Title = movie.Title; + Year = movie.Year; + } + } + + [Key] + public string Id { get; set; } + + [Column(TypeName = "INTEGER")] + public DateTimeOffset? UpdatedAt { get; set; } + public byte[] Version { get; set; } + public bool Deleted { get; set; } + + public bool BestPictureWinner { get; set; } + public int Duration { get; set; } + public MovieRating Rating { get; set; } = MovieRating.Unrated; + public DateOnly ReleaseDate { get; set; } + public string Title { get; set; } = string.Empty; + public int Year { get; set; } +} + diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Helpers/IntegrationDbContext.cs b/tests/CommunityToolkit.Datasync.Client.Test/Helpers/IntegrationDbContext.cs index 0ddea9d8..b4ad1049 100644 --- a/tests/CommunityToolkit.Datasync.Client.Test/Helpers/IntegrationDbContext.cs +++ b/tests/CommunityToolkit.Datasync.Client.Test/Helpers/IntegrationDbContext.cs @@ -14,6 +14,8 @@ public class IntegrationDbContext(DbContextOptions options { public DbSet Movies => Set(); + public DbSet ByteMovies => Set(); + public ServiceApplicationFactory Factory { get; set; } public SqliteConnection Connection { get; set; } @@ -28,6 +30,12 @@ protected override void OnDatasyncInitialization(DatasyncOfflineOptionsBuilder o cfg.ClientName = "movies"; cfg.Endpoint = new Uri($"/{Factory.MovieEndpoint}", UriKind.Relative); }); + + optionsBuilder.Entity(cfg => + { + cfg.ClientName = "movies"; + cfg.Endpoint = new Uri($"/{Factory.MovieEndpoint}", UriKind.Relative); + }); } protected override void Dispose(bool disposing) diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Offline/Integration_Pull_Tests.cs b/tests/CommunityToolkit.Datasync.Client.Test/Offline/Integration_Pull_Tests.cs index 1686dea8..1d5fce4d 100644 --- a/tests/CommunityToolkit.Datasync.Client.Test/Offline/Integration_Pull_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Client.Test/Offline/Integration_Pull_Tests.cs @@ -57,6 +57,22 @@ public async Task PullAsync_ViaDbSet_Works() } } + [Fact] + public async Task PullAsync_ViaDbSet_Works_ByteVersion() + { + await this.context.ByteMovies.PullAsync(); + List movies = await this.context.ByteMovies.ToListAsync(); + + movies.Count.Should().Be(248); + foreach (ByteVersionMovie movie in movies) + { + InMemoryMovie serviceMovie = GetServerEntityById(movie.Id); + serviceMovie.Should().NotBeNull() + .And.BeEquivalentTo(serviceMovie) + .And.HaveEquivalentMetadataTo(serviceMovie); + } + } + [Fact] public async Task PullAsync_ViaContext_Works() { diff --git a/tests/CommunityToolkit.Datasync.Client.Test/Offline/Integration_Push_Tests.cs b/tests/CommunityToolkit.Datasync.Client.Test/Offline/Integration_Push_Tests.cs index 1eae2959..4c6ec203 100644 --- a/tests/CommunityToolkit.Datasync.Client.Test/Offline/Integration_Push_Tests.cs +++ b/tests/CommunityToolkit.Datasync.Client.Test/Offline/Integration_Push_Tests.cs @@ -104,7 +104,7 @@ public async Task PushAsync_Complex_Situation() this.context.DatasyncOperationsQueue.Should().BeEmpty(); // Now use PullAsync() again - these should all be pulled down again - PullResult pullResults = await this.context.PullAsync(); + PullResult pullResults = await this.context.Movies.PullAsync(); pullResults.IsSuccessful.Should().BeTrue(); pullResults.Additions.Should().Be(0); pullResults.Deletions.Should().Be(0); @@ -112,6 +112,79 @@ public async Task PushAsync_Complex_Situation() pullResults.Replacements.Should().Be(moviesToReplace.Count + 1); } + [Fact] + public async Task PushAsync_ByteVersion() + { + ResetInMemoryMovies(); + + PullResult initialPullResults = await this.context.ByteMovies.PullAsync(); + initialPullResults.IsSuccessful.Should().BeTrue(); + initialPullResults.Additions.Should().Be(248); + initialPullResults.Deletions.Should().Be(0); + initialPullResults.Replacements.Should().Be(0); + this.context.ByteMovies.Should().HaveCount(248); + + // Let's add some new movies + ByteVersionMovie blackPanther = new(TestCommon.TestData.Movies.BlackPanther) { Id = Guid.NewGuid().ToString("N") }; + this.context.ByteMovies.Add(blackPanther); + await this.context.SaveChangesAsync(); + + // And remove any movie that matches some criteria + List moviesToDelete = await this.context.ByteMovies.Where(x => x.Duration > 180).ToListAsync(); + this.context.ByteMovies.RemoveRange(moviesToDelete); + await this.context.SaveChangesAsync(); + + // Then replace all the Unrated movies with a rating of NC17 + List moviesToReplace = await this.context.ByteMovies.Where(x => x.Rating == MovieRating.Unrated).ToListAsync(); + moviesToReplace.ForEach(r => + { + r.Rating = MovieRating.NC17; + r.Title = r.Title.PadLeft('-'); + this.context.ByteMovies.Update(r); + }); + await this.context.SaveChangesAsync(); + + // Check the queue. + List operations = await this.context.DatasyncOperationsQueue.ToListAsync(); + operations.Count.Should().Be(1 + moviesToDelete.Count + moviesToReplace.Count); + operations.Count(x => x.Kind is OperationKind.Add).Should().Be(1); + operations.Count(x => x.Kind is OperationKind.Delete).Should().Be(moviesToDelete.Count); + operations.Count(x => x.Kind is OperationKind.Replace).Should().Be(moviesToReplace.Count); + + // Now push the results and check what we did + PushResult pushResults = await this.context.PushAsync(); + + // This little snippet of code is to aid debugging if this test fails + if (!pushResults.IsSuccessful) + { + foreach (KeyValuePair failedRequest in pushResults.FailedRequests) + { + string id = failedRequest.Key; + ServiceResponse response = failedRequest.Value; + string jsonContent = string.Empty; + if (response.HasContent) + { + using StreamReader reader = new(response.ContentStream); + jsonContent = reader.ReadToEnd(); + } + + Console.WriteLine($"FAILED REQUEST FOR ID: {id}: {response.StatusCode}\n{jsonContent}"); + } + } + + pushResults.IsSuccessful.Should().BeTrue(); + pushResults.CompletedOperations.Should().Be(1 + moviesToDelete.Count + moviesToReplace.Count); + this.context.DatasyncOperationsQueue.Should().BeEmpty(); + + // Now use PullAsync() again - these should all be pulled down again + PullResult pullResults = await this.context.ByteMovies.PullAsync(); + pullResults.IsSuccessful.Should().BeTrue(); + pullResults.Additions.Should().Be(0); + pullResults.Deletions.Should().Be(0); + // The service always replaces additions and replacements - updating the last updatedAt. + pullResults.Replacements.Should().Be(moviesToReplace.Count + 1); + } + [Fact] public async Task PushAsync_Multithreaded() { @@ -176,7 +249,7 @@ public async Task PushAsync_Multithreaded() this.context.DatasyncOperationsQueue.Should().BeEmpty(); // Now use PullAsync() again - these should all be pulled down again - PullResult pullResults = await this.context.PullAsync(); + PullResult pullResults = await this.context.Movies.PullAsync(); pullResults.IsSuccessful.Should().BeTrue(); pullResults.Additions.Should().Be(0); pullResults.Deletions.Should().Be(0);