From c222245ff60871f8a8db139acda0b487c105f08f Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Fri, 12 Apr 2024 14:30:29 +0200 Subject: [PATCH] Refactor and stabilize Send/Receive tests (#715) * Refactor Send/Receive tests * move `InitLocalFlexProjectWithRepo` into SR Fixture because it only works when the fixture is used * Refactor SendReceiveAfterProjectReset and other things * Fix mistakes --------- Co-authored-by: Kevin Hahn --- .../Controllers/ProjectController.cs | 2 +- backend/LexBoxApi/Services/HgService.cs | 23 +- backend/LexCore/Utils/FileUtils.cs | 19 + backend/Testing/ApiTests/ApiTestBase.cs | 7 +- .../ApiTests/NewProjectRaceCondition.cs | 4 +- .../Testing/Fixtures/SendReceiveFixture.cs | 63 ++++ backend/Testing/Services/Constants.cs | 11 + .../Testing/Services/SendReceiveService.cs | 46 ++- .../Services/TestingEnvironmentVariables.cs | 2 + backend/Testing/Services/Utils.cs | 130 +++++++ backend/Testing/Services/UtilsTests.cs | 17 + .../SendReceiveServiceTests.cs | 356 +++++------------- backend/Testing/Taskfile.yml | 6 +- frontend/tests/fixtures.ts | 2 +- 14 files changed, 393 insertions(+), 295 deletions(-) create mode 100644 backend/Testing/Fixtures/SendReceiveFixture.cs create mode 100644 backend/Testing/Services/Constants.cs create mode 100644 backend/Testing/Services/Utils.cs create mode 100644 backend/Testing/Services/UtilsTests.cs diff --git a/backend/LexBoxApi/Controllers/ProjectController.cs b/backend/LexBoxApi/Controllers/ProjectController.cs index 8041cf7de..984b8ac69 100644 --- a/backend/LexBoxApi/Controllers/ProjectController.cs +++ b/backend/LexBoxApi/Controllers/ProjectController.cs @@ -156,7 +156,7 @@ public async Task FinishResetProject(string code) return Ok(); } - [HttpDelete("project/{id}")] + [HttpDelete("{id}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesDefaultResponseType] diff --git a/backend/LexBoxApi/Services/HgService.cs b/backend/LexBoxApi/Services/HgService.cs index d808d25ac..ec0053a68 100644 --- a/backend/LexBoxApi/Services/HgService.cs +++ b/backend/LexBoxApi/Services/HgService.cs @@ -68,9 +68,10 @@ private void InitRepoAt(string code) { var repoDirectory = new DirectoryInfo(PrefixRepoFilePath(code)); repoDirectory.Create(); - CopyFilesRecursively( + FileUtils.CopyFilesRecursively( new DirectoryInfo("Services/HgEmptyRepo"), - repoDirectory + repoDirectory, + Permissions ); } @@ -177,24 +178,6 @@ await Task.Run(() => UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.SetUser; - private static void CopyFilesRecursively(DirectoryInfo source, DirectoryInfo target) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - target.UnixFileMode = Permissions; - foreach (var dir in source.EnumerateDirectories()) - { - CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name)); - } - - foreach (var file in source.EnumerateFiles()) - { - var destFileName = Path.Combine(target.FullName, file.Name); - var destFile = file.CopyTo(destFileName); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - destFile.UnixFileMode = Permissions; - } - } - private static void SetPermissionsRecursively(DirectoryInfo rootDir) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return; diff --git a/backend/LexCore/Utils/FileUtils.cs b/backend/LexCore/Utils/FileUtils.cs index 20cf58d6e..f5a6503f2 100644 --- a/backend/LexCore/Utils/FileUtils.cs +++ b/backend/LexCore/Utils/FileUtils.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Runtime.InteropServices; namespace LexCore.Utils; @@ -10,4 +11,22 @@ public static string ToTimestamp(DateTimeOffset dateTime) // make it file-system friendly return timestamp.Replace(':', '-'); } + + public static void CopyFilesRecursively(DirectoryInfo source, DirectoryInfo target, UnixFileMode? permissions = null) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + target.UnixFileMode = permissions ?? source.UnixFileMode; + foreach (var dir in source.EnumerateDirectories()) + { + CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name), permissions); + } + + foreach (var file in source.EnumerateFiles()) + { + var destFileName = Path.Combine(target.FullName, file.Name); + var destFile = file.CopyTo(destFileName); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + destFile.UnixFileMode = permissions ?? file.UnixFileMode; + } + } } diff --git a/backend/Testing/ApiTests/ApiTestBase.cs b/backend/Testing/ApiTests/ApiTestBase.cs index 68ea1f388..7a2070e55 100644 --- a/backend/Testing/ApiTests/ApiTestBase.cs +++ b/backend/Testing/ApiTests/ApiTestBase.cs @@ -1,8 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using System.Net; using System.Net.Http.Json; using System.Text.Json.Nodes; -using LexBoxApi.Auth; using Shouldly; using Testing.LexCore.Utils; using Testing.Services; @@ -17,7 +15,10 @@ public class ApiTestBase public ApiTestBase() { - HttpClient = new HttpClient(_httpClientHandler); + HttpClient = new HttpClient(_httpClientHandler) + { + BaseAddress = new Uri(BaseUrl) + }; } public async Task LoginAs(string user, string password) diff --git a/backend/Testing/ApiTests/NewProjectRaceCondition.cs b/backend/Testing/ApiTests/NewProjectRaceCondition.cs index dc57f98c6..8d4298a1e 100644 --- a/backend/Testing/ApiTests/NewProjectRaceCondition.cs +++ b/backend/Testing/ApiTests/NewProjectRaceCondition.cs @@ -24,8 +24,8 @@ public async Task CanCreateMultipleProjectsAndQueryThemRightAway() } finally { - await HttpClient.DeleteAsync($"{BaseUrl}/api/project/project/{project1Id}"); - await HttpClient.DeleteAsync($"{BaseUrl}/api/project/project/{project2Id}"); + await HttpClient.DeleteAsync($"{BaseUrl}/api/project/{project1Id}"); + await HttpClient.DeleteAsync($"{BaseUrl}/api/project/{project2Id}"); } } diff --git a/backend/Testing/Fixtures/SendReceiveFixture.cs b/backend/Testing/Fixtures/SendReceiveFixture.cs new file mode 100644 index 000000000..128874017 --- /dev/null +++ b/backend/Testing/Fixtures/SendReceiveFixture.cs @@ -0,0 +1,63 @@ +using System.IO.Compression; +using System.Runtime.CompilerServices; +using Chorus.VcsDrivers.Mercurial; +using LexCore.Utils; +using Shouldly; +using SIL.Progress; +using Testing.ApiTests; +using Testing.Services; +using static Testing.Services.Constants; + +namespace Testing.Fixtures; + +public class SendReceiveFixture : IAsyncLifetime +{ + private readonly DirectoryInfo _templateRepo = new(Path.Join(BasePath, "_template-repo_")); + public ApiTestBase AdminApiTester { get; } = new(); + + public async Task InitializeAsync() + { + DeletePreviousTestFiles(); + await DownloadTemplateRepo(); + await AdminApiTester.LoginAs(AdminAuth.Username, AdminAuth.Password); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; + } + + private static void DeletePreviousTestFiles() + { + if (Directory.Exists(BasePath)) Directory.Delete(BasePath, true); + } + + private async Task DownloadTemplateRepo() + { + await using var stream = await AdminApiTester.HttpClient.GetStreamAsync("https://drive.google.com/uc?export=download&id=1w357T1Ti7bDwEof4HPBUZ5gB7WSKA5O2"); + using var zip = new ZipArchive(stream); + zip.ExtractToDirectory(_templateRepo.FullName); + } + + public ProjectConfig InitLocalFlexProjectWithRepo(HgProtocol? protocol = null, [CallerMemberName] string projectName = "") + { + var projectConfig = Utils.GetNewProjectConfig(protocol, projectName); + InitLocalFlexProjectWithRepo(projectConfig); + return projectConfig; + } + + public void InitLocalFlexProjectWithRepo(ProjectPath projectPath) + { + var projectDir = Directory.CreateDirectory(projectPath.Dir); + FileUtils.CopyFilesRecursively(_templateRepo, projectDir); + File.Move(Path.Join(projectPath.Dir, "kevin-test-01.fwdata"), projectPath.FwDataFile); + Directory.EnumerateFiles(projectPath.Dir).ShouldContain(projectPath.FwDataFile); + + // hack around the fact that our send and receive won't create a repo from scratch. + var progress = new NullProgress(); + HgRunner.Run("hg init", projectPath.Dir, 1, progress); + HgRunner.Run("hg branch 7500002.7000072", projectPath.Dir, 1, progress); + HgRunner.Run($"hg add Lexicon.fwstub", projectPath.Dir, 1, progress); + HgRunner.Run("""hg commit -m "first commit" """, projectPath.Dir, 1, progress); + } +} diff --git a/backend/Testing/Services/Constants.cs b/backend/Testing/Services/Constants.cs new file mode 100644 index 000000000..0d387f4e3 --- /dev/null +++ b/backend/Testing/Services/Constants.cs @@ -0,0 +1,11 @@ +namespace Testing.Services; + +public static class Constants +{ + public static readonly string BasePath = Path.Join(Path.GetTempPath(), "SR_Tests"); + public static readonly SendReceiveAuth ManagerAuth = new("manager", TestingEnvironmentVariables.DefaultPassword); + public static readonly SendReceiveAuth AdminAuth = new("admin", TestingEnvironmentVariables.DefaultPassword); + public static readonly SendReceiveAuth InvalidPass = new("manager", "incorrect_pass"); + public static readonly SendReceiveAuth InvalidUser = new("invalid_user", TestingEnvironmentVariables.DefaultPassword); + public static readonly SendReceiveAuth UnauthorizedUser = new("user", TestingEnvironmentVariables.DefaultPassword); +} diff --git a/backend/Testing/Services/SendReceiveService.cs b/backend/Testing/Services/SendReceiveService.cs index 87ce18fb9..aabf4ea7f 100644 --- a/backend/Testing/Services/SendReceiveService.cs +++ b/backend/Testing/Services/SendReceiveService.cs @@ -1,7 +1,7 @@ using System.Diagnostics; -using System.Runtime.InteropServices; using Chorus; using Nini.Ini; +using Shouldly; using SIL.Progress; using Testing.Logging; using Xunit.Abstractions; @@ -10,9 +10,9 @@ namespace Testing.Services; public record SendReceiveAuth(string Username, string Password); -public record SendReceiveParams(string ProjectCode, string BaseUrl, string DestDir) +public record SendReceiveParams(string ProjectCode, string BaseUrl, string Dir) : ProjectPath(ProjectCode, Dir) { - public string FwDataFile { get; } = Path.Join(DestDir, $"{ProjectCode}.fwdata"); + public SendReceiveParams(HgProtocol protocol, ProjectPath project) : this(project.Code, protocol.GetTestHostName(), project.Dir) { } } public class SendReceiveService @@ -78,7 +78,32 @@ public async Task GetHgVersion() return output; } - public string CloneProject(SendReceiveParams sendReceiveParams, SendReceiveAuth auth) + public string RunCloneSendReceive(SendReceiveParams sendReceiveParams, SendReceiveAuth auth) + { + var projectDir = sendReceiveParams.Dir; + var fwDataFile = sendReceiveParams.FwDataFile; + + // Clone + var cloneResult = CloneProject(sendReceiveParams, auth); + + Directory.Exists(projectDir).ShouldBeTrue($"Directory {projectDir} not found. Clone response: {cloneResult}"); + Directory.EnumerateFiles(projectDir).ShouldContain(fwDataFile); + var fwDataFileInfo = new FileInfo(fwDataFile); + fwDataFileInfo.Length.ShouldBeGreaterThan(0); + var fwDataFileOriginalLength = fwDataFileInfo.Length; + + // SendReceive + var srResult = SendReceiveProject(sendReceiveParams, auth); + + srResult.ShouldContain("no changes from others"); + fwDataFileInfo.Refresh(); + fwDataFileInfo.Exists.ShouldBeTrue(); + fwDataFileInfo.Length.ShouldBe(fwDataFileOriginalLength); + + return $"Clone: {cloneResult}{Environment.NewLine}SendReceive: {srResult}"; + } + + public string CloneProject(SendReceiveParams sendReceiveParams, SendReceiveAuth auth, bool validateOutput = true) { var (projectCode, baseUrl, destDir) = sendReceiveParams; var (username, password) = auth; @@ -113,10 +138,16 @@ public string CloneProject(SendReceiveParams sendReceiveParams, SendReceiveAuth string cloneResult; LfMergeBridge.LfMergeBridge.Execute("Language_Forge_Clone", progress, flexBridgeOptions, out cloneResult); cloneResult += $"{Environment.NewLine}Progress out: {progress.Text}"; + + if (validateOutput) + { + Utils.ValidateSendReceiveOutput(cloneResult); + } + return cloneResult; } - public string SendReceiveProject(SendReceiveParams sendReceiveParams, SendReceiveAuth auth, string commitMessage = "Testing") + public string SendReceiveProject(SendReceiveParams sendReceiveParams, SendReceiveAuth auth, string commitMessage = "Testing", bool validateOutput = true) { var (projectCode, baseUrl, destDir) = sendReceiveParams; var (username, password) = auth; @@ -161,6 +192,11 @@ public string SendReceiveProject(SendReceiveParams sendReceiveParams, SendReceiv cloneResult += "Progress out: " + progress.Text; + if (validateOutput) + { + Utils.ValidateSendReceiveOutput(cloneResult); + } + return cloneResult; } } diff --git a/backend/Testing/Services/TestingEnvironmentVariables.cs b/backend/Testing/Services/TestingEnvironmentVariables.cs index 0f20ddedc..8b9d29e7a 100644 --- a/backend/Testing/Services/TestingEnvironmentVariables.cs +++ b/backend/Testing/Services/TestingEnvironmentVariables.cs @@ -25,6 +25,8 @@ public static class TestingEnvironmentVariables public static string ProjectCode = Environment.GetEnvironmentVariable("TEST_PROJECT_CODE") ?? "sena-3"; public static string DefaultPassword = Environment.GetEnvironmentVariable("TEST_DEFAULT_PASSWORD") ?? "pass"; + public static int HgRefreshInterval = 5000; + public static string GetTestHostName(this HgProtocol protocol) { return protocol switch diff --git a/backend/Testing/Services/Utils.cs b/backend/Testing/Services/Utils.cs new file mode 100644 index 000000000..6864c7acf --- /dev/null +++ b/backend/Testing/Services/Utils.cs @@ -0,0 +1,130 @@ +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Shouldly; +using Testing.ApiTests; +using static Testing.Services.Constants; + +namespace Testing.Services; + +public static class Utils +{ + private static int _folderIndex = 1; + + public static SendReceiveParams GetParams(HgProtocol protocol, + string? projectCode = null, + [CallerMemberName] string projectName = "") + { + projectCode ??= TestingEnvironmentVariables.ProjectCode; + var sendReceiveParams = new SendReceiveParams(projectCode, protocol.GetTestHostName(), + GetNewProjectDir(projectCode, projectName)); + return sendReceiveParams; + } + + public static ProjectConfig GetNewProjectConfig(HgProtocol? protocol = null, [CallerMemberName] string projectName = "") + { + var id = Guid.NewGuid(); + if (protocol.HasValue) projectName += $" ({protocol.Value.ToString()[..5]})"; + var projectCode = ToProjectCodeFriendlyString(projectName); + var shortId = id.ToString().Split("-")[0]; + projectCode = $"{projectCode}-{shortId}-dev-flex"; + var dir = GetNewProjectDir(projectCode, projectName); + return new ProjectConfig(id, projectName, projectCode, dir); + } + + public static async Task RegisterProjectInLexBox( + ProjectConfig config, + ApiTestBase apiTester + ) + { + await apiTester.ExecuteGql($$""" + mutation { + createProject(input: { + name: "{{config.Name}}", + type: FL_EX, + id: "{{config.Id}}", + code: "{{config.Code}}", + description: "Project created by an integration test", + retentionPolicy: DEV + }) { + createProjectResponse { + id + result + } + errors { + __typename + ... on DbError { + code + message + } + } + } + } + """); + return new LexboxProject(apiTester, config.Id); + } + + public static void ValidateSendReceiveOutput(string srOutput) + { + srOutput.ShouldNotContain("abort"); + srOutput.ShouldNotContain("failure"); + srOutput.ShouldNotContain("error"); + } + + public static string ToProjectCodeFriendlyString(string name) + { + var dashesBeforeCapitals = Regex.Replace(name, "(? { - private readonly SendReceiveAuth ManagerAuth = new("manager", TestingEnvironmentVariables.DefaultPassword); - private readonly SendReceiveAuth AdminAuth = new("admin", TestingEnvironmentVariables.DefaultPassword); - private readonly SendReceiveAuth InvalidPass = new("manager", "incorrect_pass"); - private readonly SendReceiveAuth InvalidUser = new("invalid_user", TestingEnvironmentVariables.DefaultPassword); - private readonly SendReceiveAuth UnauthorizedUser = new("user", TestingEnvironmentVariables.DefaultPassword); - private readonly ITestOutputHelper _output; + private readonly SendReceiveFixture _srFixture; + private readonly ApiTestBase _adminApiTester; private readonly SendReceiveService _sendReceiveService; - public SendReceiveServiceTests(ITestOutputHelper output) + public SendReceiveServiceTests(ITestOutputHelper output, SendReceiveFixture sendReceiveSrFixture) { _output = output; _sendReceiveService = new SendReceiveService(_output); - } - - private static readonly string BasePath = Path.Join(Path.GetTempPath(), "SR_Tests"); - private static int _folderIndex = 1; - static SendReceiveServiceTests() - { - var dirInfo = new DirectoryInfo(BasePath); - try - { - dirInfo.Delete(true); - } - catch (DirectoryNotFoundException) - { - // It's fine if it didn't exist beforehand - } - } - - - private string GetProjectDir(string projectCode, - string? identifier = null, - [CallerMemberName] string testName = "") - { - var projectDir = Path.Join(BasePath, testName); - if (identifier is not null) projectDir = Path.Join(projectDir, identifier); - //fwdata file containing folder name will be the same as the file name - projectDir = Path.Join(projectDir, _folderIndex++.ToString(), projectCode); - projectDir.Length.ShouldBeLessThan(150, "Path may be too long with mercurial directories"); - return projectDir; - } - - private SendReceiveParams GetParams(HgProtocol protocol, - string? projectCode = null, - [CallerMemberName] string testName = "") - { - projectCode ??= TestingEnvironmentVariables.ProjectCode; - var sendReceiveParams = new SendReceiveParams(projectCode, protocol.GetTestHostName(), GetProjectDir(projectCode, testName: testName)); - return sendReceiveParams; + _srFixture = sendReceiveSrFixture; + _adminApiTester = _srFixture.AdminApiTester; } [Fact] public async Task VerifyHgWorking() { - string version = await _sendReceiveService.GetHgVersion(); + var version = await _sendReceiveService.GetHgVersion(); version.ShouldStartWith("Mercurial Distributed SCM"); _output.WriteLine("Hg version: " + version); - HgRunner.Run("hg version", Environment.CurrentDirectory, 5, new XunitStringBuilderProgress(_output) {ShowVerbose = true}); + HgRunner.Run("hg version", Environment.CurrentDirectory, 5, new XunitStringBuilderProgress(_output) { ShowVerbose = true }); HgRepository.GetEnvironmentReadinessMessage("en").ShouldBeNull(); } - [Fact] - public void CloneBigProject() + [Theory] + [InlineData(HgProtocol.Hgweb)] + [InlineData(HgProtocol.Resumable)] + public void CloneBigProject(HgProtocol hgProtocol) { - RunCloneSendReceive(HgProtocol.Hgweb, AdminAuth, "elawa-dev-flex"); + var sendReceiveParams = GetParams(hgProtocol, "elawa-dev-flex"); + _sendReceiveService.RunCloneSendReceive(sendReceiveParams, AdminAuth); } [Theory] @@ -94,9 +55,9 @@ public void CloneBigProject() [InlineData(HgProtocol.Resumable, "manager")] public void CanCloneSendReceive(HgProtocol hgProtocol, string user) { - RunCloneSendReceive(hgProtocol, - new SendReceiveAuth(user, TestingEnvironmentVariables.DefaultPassword), - TestingEnvironmentVariables.ProjectCode); + var sendReceiveParams = GetParams(hgProtocol); + _sendReceiveService.RunCloneSendReceive(sendReceiveParams, + new SendReceiveAuth(user, TestingEnvironmentVariables.DefaultPassword)); } [Theory] @@ -106,37 +67,9 @@ public async Task CanCloneSendReceiveWithJwtOverBasicAuth(HgProtocol hgProtocol, { var projectCode = TestingEnvironmentVariables.ProjectCode; var jwt = await JwtHelper.GetProjectJwtForUser(new SendReceiveAuth(user, TestingEnvironmentVariables.DefaultPassword), projectCode); - - RunCloneSendReceive(hgProtocol, - new SendReceiveAuth(AuthKernel.JwtOverBasicAuthUsername, jwt), - projectCode); - } - - private void RunCloneSendReceive(HgProtocol hgProtocol, SendReceiveAuth auth, string projectCode) - { - var sendReceiveParams = new SendReceiveParams(projectCode, hgProtocol.GetTestHostName(), - GetProjectDir(projectCode, Path.Join(hgProtocol.ToString(), auth.Username))); - var projectDir = sendReceiveParams.DestDir; - var fwDataFile = sendReceiveParams.FwDataFile; - - // Clone - var cloneResult = _sendReceiveService.CloneProject(sendReceiveParams, auth); - cloneResult.ShouldNotContain("abort"); - cloneResult.ShouldNotContain("error"); - Directory.Exists(projectDir).ShouldBeTrue($"Directory {projectDir} not found. Clone response: {cloneResult}"); - Directory.EnumerateFiles(projectDir).ShouldContain(fwDataFile); - var fwDataFileInfo = new FileInfo(fwDataFile); - fwDataFileInfo.Length.ShouldBeGreaterThan(0); - var fwDataFileOriginalLength = fwDataFileInfo.Length; - - // SendReceive - var srResult = _sendReceiveService.SendReceiveProject(sendReceiveParams, auth); - srResult.ShouldNotContain("abort"); - srResult.ShouldNotContain("error"); - srResult.ShouldContain("no changes from others"); - fwDataFileInfo.Refresh(); - fwDataFileInfo.Exists.ShouldBeTrue(); - fwDataFileInfo.Length.ShouldBe(fwDataFileOriginalLength); + var sendReceiveParams = GetParams(hgProtocol, projectCode); + _sendReceiveService.RunCloneSendReceive(sendReceiveParams, + new SendReceiveAuth(AuthKernel.JwtOverBasicAuthUsername, jwt)); } [Theory] @@ -144,132 +77,70 @@ private void RunCloneSendReceive(HgProtocol hgProtocol, SendReceiveAuth auth, st [InlineData(HgProtocol.Resumable)] public async Task ModifyProjectData(HgProtocol protocol) { - var projectCode = TestingEnvironmentVariables.ProjectCode; - var apiTester = new ApiTestBase(); - var auth = AdminAuth; - await apiTester.LoginAs(auth.Username, auth.Password); - string gqlQuery = + // Create a fresh project + var projectConfig = _srFixture.InitLocalFlexProjectWithRepo(); + await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester); + + await WaitForHgRefreshIntervalAsync(); + + // Push the project to the server + var sendReceiveParams = new SendReceiveParams(protocol, projectConfig); + _sendReceiveService.SendReceiveProject(sendReceiveParams, AdminAuth); + + await WaitForLexboxMetadataUpdateAsync(); + + // Verify pushed and store last commit + var gqlQuery = $$""" query projectLastCommit { - projectByCode(code: "{{projectCode}}") { + projectByCode(code: "{{projectConfig.Code}}") { lastCommit } } """; - var jsonResult = await apiTester.ExecuteGql(gqlQuery); - var lastCommitDate = jsonResult["data"]["projectByCode"]["lastCommit"].ToString(); - - // Clone - var sendReceiveParams = GetParams(protocol, projectCode); - var cloneResult = _sendReceiveService.CloneProject(sendReceiveParams, auth); - cloneResult.ShouldNotContain("abort"); - cloneResult.ShouldNotContain("error"); + var jsonResult = await _adminApiTester.ExecuteGql(gqlQuery); + var lastCommitDate = jsonResult?["data"]?["projectByCode"]?["lastCommit"]?.ToString(); + lastCommitDate.ShouldNotBeNullOrEmpty(); + + // Modify var fwDataFileInfo = new FileInfo(sendReceiveParams.FwDataFile); fwDataFileInfo.Length.ShouldBeGreaterThan(0); ModifyProjectHelper.ModifyProject(sendReceiveParams.FwDataFile); - // Send changes - var srResult = _sendReceiveService.SendReceiveProject(sendReceiveParams, auth, "Modify project data automated test"); - srResult.ShouldNotContain("abort"); - srResult.ShouldNotContain("error"); - await Task.Delay(6000); + // Push changes + _sendReceiveService.SendReceiveProject(sendReceiveParams, AdminAuth, "Modify project data automated test"); + + await WaitForLexboxMetadataUpdateAsync(); - jsonResult = await apiTester.ExecuteGql(gqlQuery); - var lastCommitDateAfter = jsonResult["data"]["projectByCode"]["lastCommit"].ToString(); + // Verify the push updated the last commit date + jsonResult = await _adminApiTester.ExecuteGql(gqlQuery); + var lastCommitDateAfter = jsonResult?["data"]?["projectByCode"]?["lastCommit"]?.ToString(); lastCommitDateAfter.ShouldBeGreaterThan(lastCommitDate); } - [Theory] [InlineData(HgProtocol.Hgweb)] [InlineData(HgProtocol.Resumable)] public async Task SendReceiveAfterProjectReset(HgProtocol protocol) { - // Create new project on server so we don't reset our master test project - var id = Guid.NewGuid(); - var newProjectCode = $"{(protocol == HgProtocol.Hgweb ? "web": "res")}-sr-reset-{id:N}"; - var apiTester = new ApiTestBase(); - var auth = AdminAuth; - await apiTester.LoginAs(auth.Username, auth.Password); - await apiTester.ExecuteGql($$""" - mutation { - createProject(input: { - name: "Send new project test", - type: FL_EX, - id: "{{id}}", - code: "{{newProjectCode}}", - description: "A project created during a unit test to test Send/Receive operation via {{protocol}} after a project reset", - retentionPolicy: DEV - }) { - createProjectResponse { - id - result - } - errors { - __typename - ... on DbError { - code - message - } - } - } - } - """); + // Create a fresh project + var projectConfig = _srFixture.InitLocalFlexProjectWithRepo(protocol, "SR_AfterReset"); + await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester); - // Ensure newly-created project is deleted after test completes - await using var deleteProject = Defer.Async(() => apiTester.HttpClient.DeleteAsync($"{apiTester.BaseUrl}/api/project/project/{id}")); - - // Sleep 5 seconds to ensure hgweb picks up newly-created test project - await Task.Delay(TimeSpan.FromSeconds(5)); - - // Populate new project from original so we're not resetting original test project in E2E tests - // Note that this time we're cloning via hg clone rather than Chorus, because we don't want a .fwdata file yet - var progress = new NullProgress(); - var origProjectCode = TestingEnvironmentVariables.ProjectCode; - var sourceProjectDir = GetProjectDir(origProjectCode); - Directory.CreateDirectory(sourceProjectDir); - var hgwebUrl = new UriBuilder - { - Scheme = TestingEnvironmentVariables.HttpScheme, - Host = HgProtocol.Hgweb.GetTestHostName(), - UserName = auth.Username, - Password = auth.Password - }; - HgRunner.Run($"hg clone {hgwebUrl}{origProjectCode} {sourceProjectDir}", "", 45, progress); - HgRunner.Run($"hg push {hgwebUrl}{newProjectCode}", sourceProjectDir, 45, progress); - - // Sleep 5 seconds to ensure hgweb picks up newly-pushed commits - await Task.Delay(TimeSpan.FromSeconds(5)); - - // Now clone again via Chorus so that we'll hvae a .fwdata file - var sendReceiveParams = GetParams(protocol, newProjectCode); - Directory.CreateDirectory(sendReceiveParams.DestDir); - var srResult = _sendReceiveService.CloneProject(sendReceiveParams, auth); - _output.WriteLine(srResult); - srResult.ShouldNotContain("abort"); - srResult.ShouldNotContain("failure"); - srResult.ShouldNotContain("error"); - - // Delete the Chorus revision cache if resumable, otherwise Chorus will send the wrong data during S/R - // Note that HgWeb protocol does *not* have this issue - string chorusStorageFolder = Path.Join(sendReceiveParams.DestDir, "Chorus", "ChorusStorage"); - var revisionCache = new FileInfo(Path.Join(chorusStorageFolder, "revisioncache.json")); - if (revisionCache.Exists) - { - revisionCache.Delete(); - } + await WaitForHgRefreshIntervalAsync(); - // With all that setup out of the way, we can now start the actual test itself + var sendReceiveParams = new SendReceiveParams(protocol, projectConfig); + var srResult = _sendReceiveService.SendReceiveProject(sendReceiveParams, AdminAuth); // First, save the current value of `hg tip` from the original project var tipUri = new UriBuilder { Scheme = TestingEnvironmentVariables.HttpScheme, Host = TestingEnvironmentVariables.ServerHostname, - Path = $"hg/{newProjectCode}/tags", + Path = $"hg/{projectConfig.Code}/tags", Query = "?style=json" }; - var response = await apiTester.HttpClient.GetAsync(tipUri.Uri); + var response = await _adminApiTester.HttpClient.GetAsync(tipUri.Uri); var jsonResult = await response.Content.ReadFromJsonAsync(); var originalTip = jsonResult?["node"]?.AsValue()?.ToString(); originalTip.ShouldNotBeNull(); @@ -280,14 +151,13 @@ ... on DbError { // /api/project/upload-zip/{code} // upload zip file // Step 1: reset project - await apiTester.HttpClient.PostAsync($"{apiTester.BaseUrl}/api/project/resetProject/{newProjectCode}", null); - await apiTester.HttpClient.PostAsync($"{apiTester.BaseUrl}/api/project/finishResetProject/{newProjectCode}", null); + await _adminApiTester.HttpClient.PostAsync($"{_adminApiTester.BaseUrl}/api/project/resetProject/{projectConfig.Code}", null); + await _adminApiTester.HttpClient.PostAsync($"{_adminApiTester.BaseUrl}/api/project/finishResetProject/{projectConfig.Code}", null); - // Sleep 5 seconds to ensure hgweb picks up newly-reset project - await Task.Delay(TimeSpan.FromSeconds(5)); + await WaitForHgRefreshIntervalAsync(); // Step 2: verify project is now empty, i.e. tip is "0000000..." - response = await apiTester.HttpClient.GetAsync(tipUri.Uri); + response = await _adminApiTester.HttpClient.GetAsync(tipUri.Uri); jsonResult = await response.Content.ReadFromJsonAsync(); var emptyTip = jsonResult?["node"]?.AsValue()?.ToString(); emptyTip.ShouldNotBeNull(); @@ -295,99 +165,63 @@ ... on DbError { emptyTip.Replace("0", "").ShouldBeEmpty(); // Step 3: do Send/Receive - var srResultStep3 = _sendReceiveService.SendReceiveProject(sendReceiveParams, auth); + if (protocol == HgProtocol.Resumable) + { + // Delete the Chorus revision cache of resumable, otherwise Chorus will send the wrong data during S/R + var chorusStorageFolder = Path.Join(sendReceiveParams.Dir, "Chorus", "ChorusStorage"); + var revisionCache = new FileInfo(Path.Join(chorusStorageFolder, "revisioncache.json")); + if (revisionCache.Exists) + { + revisionCache.Delete(); + } + } + + var srResultStep3 = _sendReceiveService.SendReceiveProject(sendReceiveParams, AdminAuth); _output.WriteLine(srResultStep3); - srResultStep3.ShouldNotContain("abort"); - srResultStep3.ShouldNotContain("failure"); - srResultStep3.ShouldNotContain("error"); + + await WaitForHgRefreshIntervalAsync(); // Step 4: verify project tip is same hash as original project tip - response = await apiTester.HttpClient.GetAsync(tipUri.Uri); + response = await _adminApiTester.HttpClient.GetAsync(tipUri.Uri); jsonResult = await response.Content.ReadFromJsonAsync(); var postSRTip = jsonResult?["node"]?.AsValue()?.ToString(); postSRTip.ShouldNotBeNull(); postSRTip.ShouldBe(originalTip); } - [Fact] - public async Task SendNewProject() + [Theory] + [InlineData(180, 10)] + [InlineData(50, 3)] + public async Task SendNewProject(int totalSizeMb, int fileCount) { - var id = Guid.NewGuid(); - var apiTester = new ApiTestBase(); - var auth = AdminAuth; - var projectCode = "send-new-project-test-" + id.ToString("N"); - await apiTester.LoginAs(auth.Username, auth.Password); - await apiTester.ExecuteGql($$""" - mutation { - createProject(input: { - name: "Send new project test", - type: FL_EX, - id: "{{id}}", - code: "{{projectCode}}", - description: "this is a new project created during a unit test to verify we can send a new project for the first time", - retentionPolicy: DEV - }) { - createProjectResponse { - id - result - } - errors { - __typename - ... on DbError { - code - message - } - } - } - } - """); + var projectConfig = _srFixture.InitLocalFlexProjectWithRepo(); + await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester); - await using var deleteProject = Defer.Async(() => apiTester.HttpClient.DeleteAsync($"{apiTester.BaseUrl}/api/project/project/{id}")); + await WaitForHgRefreshIntervalAsync(); - var sendReceiveParams = GetParams(HgProtocol.Hgweb, projectCode); - await using (var stream = await apiTester.HttpClient.GetStreamAsync("https://drive.google.com/uc?export=download&id=1w357T1Ti7bDwEof4HPBUZ5gB7WSKA5O2")) - using(var zip = new ZipArchive(stream)) - { - zip.ExtractToDirectory(sendReceiveParams.DestDir); - } - File.Move(Path.Join(sendReceiveParams.DestDir, "kevin-test-01.fwdata"), sendReceiveParams.FwDataFile); - Directory.EnumerateFiles(sendReceiveParams.DestDir).ShouldContain(sendReceiveParams.FwDataFile); - - //hack around the fact that our send and receive won't create a repo from scratch. - var progress = new NullProgress(); - HgRunner.Run("hg init", sendReceiveParams.DestDir, 1, progress); - HgRunner.Run("hg branch 7500002.7000072", sendReceiveParams.DestDir, 1, progress); - HgRunner.Run($"hg add Lexicon.fwstub", sendReceiveParams.DestDir, 1, progress); - HgRunner.Run("""hg commit -m "first commit" """, sendReceiveParams.DestDir, 1, progress); + var sendReceiveParams = new SendReceiveParams(HgProtocol.Hgweb, projectConfig); //add a bunch of small files, must be in separate commits otherwise hg runs out of memory. But we want the push to be large - const int totalSizeMb = 180; - const int fileCount = 10; - for (int i = 1; i <= fileCount; i++) + var progress = new NullProgress(); + for (var i = 1; i <= fileCount; i++) { - var bigFileName = $"big-file{i}.bin"; - WriteBigFile(Path.Combine(sendReceiveParams.DestDir, bigFileName), totalSizeMb / fileCount); - HgRunner.Run($"hg add {bigFileName}", sendReceiveParams.DestDir, 1, progress); - HgRunner.Run($"""hg commit -m "large file commit {i}" """, sendReceiveParams.DestDir, 5, progress).ExitCode.ShouldBe(0); + var fileName = $"test-file{i}.bin"; + WriteFile(Path.Combine(sendReceiveParams.Dir, fileName), totalSizeMb / fileCount); + HgRunner.Run($"hg add {fileName}", sendReceiveParams.Dir, 1, progress); + HgRunner.Run($"""hg commit -m "large file commit {i}" """, sendReceiveParams.Dir, 5, progress).ExitCode.ShouldBe(0); } - - //attempt to prevent issue where project isn't found yet. - await Task.Delay(TimeSpan.FromSeconds(5)); - var srResult = _sendReceiveService.SendReceiveProject(sendReceiveParams, auth); + var srResult = _sendReceiveService.SendReceiveProject(sendReceiveParams, AdminAuth); _output.WriteLine(srResult); - srResult.ShouldNotContain("abort"); - srResult.ShouldNotContain("failure"); - srResult.ShouldNotContain("error"); } - private static void WriteBigFile(string path, int sizeMb) + private static void WriteFile(string path, int sizeMb) { var random = new Random(); using var file = File.Open(path, FileMode.Create); Span buffer = stackalloc byte[1024 * 1024]; - for (int i = 0; i < (sizeMb * 1024 * 1024 / buffer.Length); i++) + for (var i = 0; i < (sizeMb * 1024 * 1024 / buffer.Length); i++) { random.NextBytes(buffer); file.Write(buffer); @@ -457,7 +291,7 @@ public void InvalidProjectAdminLogin() var act = () => _sendReceiveService.CloneProject(sendReceiveParams, AdminAuth); act.ShouldThrow(); - Directory.GetFiles(sendReceiveParams.DestDir).ShouldBeEmpty(); + Directory.GetFiles(sendReceiveParams.Dir).ShouldBeEmpty(); } [Fact] @@ -467,7 +301,7 @@ public void InvalidProjectManagerLogin() var act = () => _sendReceiveService.CloneProject(sendReceiveParams, ManagerAuth); act.ShouldThrow(); - Directory.GetFiles(sendReceiveParams.DestDir).ShouldBeEmpty(); + Directory.GetFiles(sendReceiveParams.Dir).ShouldBeEmpty(); } [Fact] diff --git a/backend/Testing/Taskfile.yml b/backend/Testing/Taskfile.yml index 87cfa66a0..1e45f94fb 100644 --- a/backend/Testing/Taskfile.yml +++ b/backend/Testing/Taskfile.yml @@ -6,14 +6,16 @@ vars: tasks: unit: + vars: + FILTER: '{{default "." .CLI_ARGS}}' cmds: - - dotnet test --filter=Category!=Integration + - dotnet test --filter="Category!=Integration&{{.FILTER}}" integration: vars: FILTER: '{{default "." .CLI_ARGS}}' cmds: - - dotnet test --filter="Category=Integration&FullyQualifiedName!~Testing.Browser&{{.FILTER}}" + - dotnet test --filter="Category=Integration&FullyQualifiedName!~Testing.Browser&{{.FILTER}}" --results-directory ./test-results --logger trx integration-env: dotenv: [ local.env ] diff --git a/frontend/tests/fixtures.ts b/frontend/tests/fixtures.ts index fa5117f8b..bca423a40 100644 --- a/frontend/tests/fixtures.ts +++ b/frontend/tests/fixtures.ts @@ -114,7 +114,7 @@ export const test = base.extend({ `); const id = gqlResponse.data.createProject.createProjectResponse.id; await use({id, code, name}); - const deleteResponse = await page.request.delete(`${testEnv.serverBaseUrl}/api/project/project/${id}`); + const deleteResponse = await page.request.delete(`${testEnv.serverBaseUrl}/api/project/${id}`); expect(deleteResponse.ok()).toBeTruthy(); }, // eslint-disable-next-line no-empty-pattern