From ceb7ade57da7d5ba85d44420ae9409c7c99ebb58 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Fri, 22 Nov 2024 14:20:50 +0700 Subject: [PATCH] Add integration test for syncing sena-3 --- .../Fixtures/LexboxConfig.cs | 14 +++ .../Fixtures/MercurialTestHelper.cs | 31 +++++ .../Fixtures/Sena3SyncFixture.cs | 114 ++++++++++++++++++ .../Fixtures/SyncFixture.cs | 11 +- .../FwLiteProjectSync.Tests.csproj | 11 ++ .../FwLiteProjectSync.Tests/Sena3SyncTests.cs | 100 +++++++++++++++ 6 files changed, 277 insertions(+), 4 deletions(-) create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/Fixtures/LexboxConfig.cs create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/Fixtures/MercurialTestHelper.cs create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs create mode 100644 backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/LexboxConfig.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/LexboxConfig.cs new file mode 100644 index 000000000..7385cbbc8 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/LexboxConfig.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace FwLiteProjectSync.Tests.Fixtures; + +public class LexboxConfig +{ + [Required, Url, RegularExpression(@"^.+/$", ErrorMessage = "Must end with '/'")] + public required string LexboxUrl { get; set; } + public string HgWebUrl => $"{LexboxUrl}hg/"; + [Required] + public required string LexboxUsername { get; set; } + [Required] + public required string LexboxPassword { get; set; } +} diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/MercurialTestHelper.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/MercurialTestHelper.cs new file mode 100644 index 000000000..afccd122a --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/MercurialTestHelper.cs @@ -0,0 +1,31 @@ +using SIL.CommandLineProcessing; +using SIL.PlatformUtilities; +using SIL.Progress; + +namespace FwLiteProjectSync.Tests.Fixtures; + +public static class MercurialTestHelper +{ + public static string HgCommand => + Path.Combine("Mercurial", Platform.IsWindows ? "hg.exe" : "hg"); + + private static string RunHgCommand(string repoPath, string args) + { + var result = CommandLineRunner.Run(HgCommand, args, repoPath, 120, new NullProgress()); + if (result.ExitCode == 0) return result.StandardOutput; + throw new Exception( + $"hg {args} failed.\nStdOut: {result.StandardOutput}\nStdErr: {result.StandardError}"); + + } + + public static void HgClean(string repoPath, string exclude) + { + RunHgCommand(repoPath, $"purge --no-confirm --exclude {exclude}"); + } + + public static void HgUpdate(string repoPath, string rev) + { + RunHgCommand(repoPath, $"update \"{rev}\""); + } +} + diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs new file mode 100644 index 000000000..f504a8dd2 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/Sena3SyncFixture.cs @@ -0,0 +1,114 @@ +using System.IO.Compression; +using System.Net.Http.Json; +using System.Runtime.CompilerServices; +using FwDataMiniLcmBridge; +using FwDataMiniLcmBridge.Api; +using FwDataMiniLcmBridge.LcmUtils; +using FwDataMiniLcmBridge.Tests.Fixtures; +using LcmCrdt; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MiniLcm; +using SIL.IO; +using SIL.Progress; + +namespace FwLiteProjectSync.Tests.Fixtures; + +public class Sena3Fixture : IAsyncLifetime +{ + private readonly SyncFixture syncFixture; + public CrdtFwdataProjectSyncService SyncService => + Services.GetRequiredService(); + public IServiceProvider Services => syncFixture.Services; + private readonly LexboxConfig lexboxConfig; + private readonly HttpClient http; + public CrdtMiniLcmApi CrdtApi { get; set; } = null!; + public FwDataMiniLcmApi FwDataApi { get; set; } = null!; + private bool AlreadyLoggedIn { get; set; } = false; + + public Sena3Fixture() + { + syncFixture = SyncFixture.Create(services => + { + services.AddOptions() + .BindConfiguration("LexboxConfig") + .ValidateDataAnnotations() + .ValidateOnStart(); + // TODO: How do I set default values if and only if they're not already set (e.g., via environment variables)? + services.Configure(c => + { + c.LexboxUrl = "http://localhost/"; + c.LexboxUsername = "admin"; + c.LexboxPassword = "pass"; + }); + }, nameof(Sena3Fixture)); // TODO: Or create the project name in the constructor rather than in InitializeAsync, then pass it in here + lexboxConfig = Services.GetRequiredService>().Value; + var factory = Services.GetRequiredService(); + http = factory.CreateClient(nameof(Sena3Fixture)); + } + + public async Task InitializeAsync() + { + var sena3MasterCopy = await DownloadSena3(); + var projectName = "sena-3_" + Guid.NewGuid().ToString("N"); + + var projectsFolder = Services.GetRequiredService>().Value + .ProjectsFolder; + if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true); + Directory.CreateDirectory(projectsFolder); + var fwDataProject = new FwDataProject(projectName, projectsFolder); + var fwDataProjectPath = Path.Combine(fwDataProject.ProjectsPath, fwDataProject.Name); + DirectoryHelper.Copy(sena3MasterCopy, fwDataProjectPath); + File.Move(Path.Combine(fwDataProjectPath, "sena-3.fwdata"), fwDataProject.FilePath); + + Services.GetRequiredService().LoadCache(fwDataProject); + FwDataApi = Services.GetRequiredService().GetFwDataMiniLcmApi(projectName, false); + + var crdtProjectsFolder = + Services.GetRequiredService>().Value.ProjectPath; + if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true); + Directory.CreateDirectory(crdtProjectsFolder); + var crdtProject = await Services.GetRequiredService() + .CreateProject(new(projectName, FwProjectId: FwDataApi.ProjectId, SeedNewProjectData: false)); + CrdtApi = (CrdtMiniLcmApi) await Services.OpenCrdtProject(crdtProject); + } + + public async Task DisposeAsync() + { + await syncFixture.DisposeAsync(); + } + + public async Task DownloadProjectBackupStream(string code) + { + var backupUrl = new Uri($"{lexboxConfig.LexboxUrl}api/project/backupProject/{code}"); + var result = await http.GetAsync(backupUrl); + return await result.Content.ReadAsStreamAsync(); + } + + public async Task LoginAs(string lexboxUsername, string lexboxPassword) + { + if (AlreadyLoggedIn) return; + await http.PostAsync($"{lexboxConfig.LexboxUrl}api/login", JsonContent.Create(new { EmailOrUsername=lexboxUsername, Password=lexboxPassword })); + AlreadyLoggedIn = true; + } + + public async Task DownloadSena3() + { + + var tempFolder = Path.Combine(Path.GetTempPath(), nameof(Sena3Fixture)); + var sena3MasterCopy = Path.Combine(tempFolder, "sena-3"); + if (!Directory.Exists(sena3MasterCopy) || !File.Exists(Path.Combine(sena3MasterCopy, "sena-3.fwdata"))) + { + await LoginAs(lexboxConfig.LexboxUsername, lexboxConfig.LexboxPassword); + Directory.CreateDirectory(sena3MasterCopy); + var zipStream = await DownloadProjectBackupStream("sena-3"); + ZipFile.ExtractToDirectory(zipStream, sena3MasterCopy); + MercurialTestHelper.HgUpdate(sena3MasterCopy, "tip"); + LfMergeBridge.LfMergeBridge.ReassembleFwdataFile(new NullProgress(), false, Path.Combine(sena3MasterCopy, "sena-3.fwdata")); + MercurialTestHelper.HgClean(sena3MasterCopy, "sena-3.fwdata"); + } + return sena3MasterCopy; + } +} diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs index 822da9c54..788605001 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs @@ -24,7 +24,9 @@ public class SyncFixture : IAsyncLifetime public static SyncFixture Create([CallerMemberName] string projectName = "") => new(projectName); - private SyncFixture(string projectName) + public static SyncFixture Create(Action extraServiceConfiguration, [CallerMemberName] string projectName = "") => new(projectName, extraServiceConfiguration); + + private SyncFixture(string projectName, Action? extraServiceConfiguration = null) { _projectName = projectName; var crdtServices = new ServiceCollection() @@ -34,9 +36,9 @@ private SyncFixture(string projectName) .AddFwLiteProjectSync() .Configure(c => c.ProjectsFolder = Path.Combine(".", _projectName, "FwData")) .Configure(c => c.ProjectPath = Path.Combine(".", _projectName, "LcmCrdt")) - .AddLogging(builder => builder.AddDebug()) - .BuildServiceProvider(); - _services = crdtServices.CreateAsyncScope(); + .AddLogging(builder => builder.AddDebug()); + if (extraServiceConfiguration is not null) extraServiceConfiguration(crdtServices); + _services = crdtServices.BuildServiceProvider().CreateAsyncScope(); } public SyncFixture(): this("sena-3_" + Guid.NewGuid().ToString("N")) @@ -64,6 +66,7 @@ public async Task InitializeAsync() public async Task DisposeAsync() { + // CAUTION: Do not assume that InitializeAsync() has been called await _services.DisposeAsync(); } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj b/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj index ea6572e46..fa5e2b83c 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj +++ b/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj @@ -7,6 +7,7 @@ false true + $(MSBuildProjectDirectory) @@ -28,6 +29,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -41,4 +44,12 @@ + + + + + + + + diff --git a/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs new file mode 100644 index 000000000..929bdb471 --- /dev/null +++ b/backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs @@ -0,0 +1,100 @@ +using FluentAssertions.Equivalency; +using FwLiteProjectSync.Tests.Fixtures; +using LcmCrdt; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using MiniLcm; +using MiniLcm.Models; +using SystemTextJsonPatch; + +namespace FwLiteProjectSync.Tests; + +public class Sena3SyncTests : IClassFixture, IAsyncLifetime +{ + private readonly Sena3Fixture _fixture; + private readonly CrdtFwdataProjectSyncService _syncService; + + private readonly Guid _complexEntryId = Guid.NewGuid(); + private Entry _testEntry = new Entry + { + Id = Guid.NewGuid(), + LexemeForm = { Values = { { "en", "Apple" } } }, + Note = { Values = { { "en", "this is a test note" } } }, + Senses = + [ + new Sense + { + Gloss = { Values = { { "en", "Apple" } } }, + Definition = { Values = { { "en", "a round fruit with a hard, crisp skin" } } }, + ExampleSentences = + [ + new ExampleSentence { Sentence = { Values = { { "en", "I went to the store to buy an apple." } } } } + ] + } + ] + }; + + public async Task InitializeAsync() + { + await _fixture.FwDataApi.CreateEntry(_testEntry); + await _fixture.FwDataApi.CreateEntry(new Entry() + { + Id = _complexEntryId, + LexemeForm = { { "en", "Pineapple" } }, + Components = + [ + new ComplexFormComponent() + { + Id = Guid.NewGuid(), + ComplexFormEntryId = _complexEntryId, + ComplexFormHeadword = "Pineapple", + ComponentEntryId = _testEntry.Id, + ComponentHeadword = "Apple" + } + ] + }); + } + + public async Task DisposeAsync() + { + await foreach (var entry in _fixture.FwDataApi.GetEntries()) + { + await _fixture.FwDataApi.DeleteEntry(entry.Id); + } + foreach (var entry in await _fixture.CrdtApi.GetEntries().ToArrayAsync()) + { + await _fixture.CrdtApi.DeleteEntry(entry.Id); + } + } + + public Sena3SyncTests(Sena3Fixture fixture) + { + _fixture = fixture; + _syncService = _fixture.SyncService; + } + + [Fact] + public async Task FirstSena3SyncJustDoesAnImport() + { + var crdtApi = _fixture.CrdtApi; + var fwdataApi = _fixture.FwDataApi; + await _syncService.Sync(crdtApi, fwdataApi); + + var crdtEntries = await crdtApi.GetEntries().ToArrayAsync(); + var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync(); + crdtEntries.Should().BeEquivalentTo(fwdataEntries, + options => options.For(e => e.Components).Exclude(c => c.Id) + .For(e => e.ComplexForms).Exclude(c => c.Id)); + } + + [Fact] + public async Task SecondSena3SyncDoesNothing() + { + var crdtApi = _fixture.CrdtApi; + var fwdataApi = _fixture.FwDataApi; + await _syncService.Sync(crdtApi, fwdataApi); + var secondSync = await _syncService.Sync(crdtApi, fwdataApi); + secondSync.CrdtChanges.Should().Be(0); + secondSync.FwdataChanges.Should().Be(0); + } +}