From d8168589131ef4bbdf01a03712ffb78a5193c90e Mon Sep 17 00:00:00 2001 From: Kevin Hahn Date: Mon, 25 Nov 2024 15:19:54 +0700 Subject: [PATCH 01/27] make api to call fw headless sync (#1252) * add crdt sync passthrough endpoint to call crdt sync on fw headless, setup service discovery to simplify configuration * fix issue with cookie port number not working when in the host parameter * log errors from fw headless, and return a problem from the lexbox api * Add button for triggering CRDT sync * increase request timeout on the sync endpoint, change the path to not be redundant --------- Co-authored-by: Tim Haasdyk --- Tiltfile | 2 +- backend/FwHeadless/HttpClientAuthHandler.cs | 5 ++- backend/FwHeadless/Program.cs | 5 ++- .../CrdtFwdataProjectSyncService.cs | 3 +- .../LexBoxApi/Controllers/CrdtController.cs | 18 +++++++- backend/LexBoxApi/LexBoxApi.csproj | 1 + backend/LexBoxApi/LexBoxKernel.cs | 3 ++ .../LexBoxApi/Services/FwHeadlessClient.cs | 19 ++++++++ .../LexBoxApi/appsettings.Development.json | 7 +++ backend/LexBoxApi/appsettings.json | 5 +++ backend/LexCore/Sync/SyncResult.cs | 3 ++ backend/LexData/SeedingData.cs | 1 + deployment/base/lexbox-deployment.yaml | 3 ++ frontend/src/lib/forms/Button.svelte | 7 ++- frontend/src/lib/icons/Icon.svelte | 4 +- .../project/[project_code]/+page.svelte | 2 + .../[project_code]/CrdtSyncButton.svelte | 43 +++++++++++++++++++ 17 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 backend/LexBoxApi/Services/FwHeadlessClient.cs create mode 100644 backend/LexCore/Sync/SyncResult.cs create mode 100644 frontend/src/routes/(authenticated)/project/[project_code]/CrdtSyncButton.svelte diff --git a/Tiltfile b/Tiltfile index 24b320e75..061bdb63d 100644 --- a/Tiltfile +++ b/Tiltfile @@ -28,7 +28,7 @@ docker_build( context='backend', dockerfile='./backend/FwHeadless/dev.Dockerfile', only=['.'], - ignore=['LexBoxApi'], + ignore=['LexBoxApi', '**/Mercurial', '**/MercurialExtensions'], live_update=[ sync('backend', '/src/backend') ] diff --git a/backend/FwHeadless/HttpClientAuthHandler.cs b/backend/FwHeadless/HttpClientAuthHandler.cs index cc3efa799..41d364334 100644 --- a/backend/FwHeadless/HttpClientAuthHandler.cs +++ b/backend/FwHeadless/HttpClientAuthHandler.cs @@ -34,7 +34,10 @@ protected override async Task SendAsync(HttpRequestMessage private async Task SetAuthHeader(HttpRequestMessage request, CancellationToken cancellationToken, Uri lexboxUrl) { var cookieContainer = new CookieContainer(); - cookieContainer.Add(new Cookie(LexAuthConstants.AuthCookieName, await GetToken(cancellationToken), null, lexboxUrl.Authority)); + cookieContainer.Add(new Cookie(LexAuthConstants.AuthCookieName, await GetToken(cancellationToken), null, lexboxUrl.Host) + { + Port = $"\"{lexboxUrl.Port}\"" + }); request.Headers.Add("Cookie", cookieContainer.GetCookieHeader(lexboxUrl)); } diff --git a/backend/FwHeadless/Program.cs b/backend/FwHeadless/Program.cs index e4e897016..95e175463 100644 --- a/backend/FwHeadless/Program.cs +++ b/backend/FwHeadless/Program.cs @@ -4,6 +4,7 @@ using FwLiteProjectSync; using LcmCrdt; using LcmCrdt.RemoteSync; +using LexCore.Sync; using LexData; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Extensions.Options; @@ -51,7 +52,7 @@ app.Run(); -static async Task, NotFound, ProblemHttpResult>> ExecuteMergeRequest( +static async Task, NotFound, ProblemHttpResult>> ExecuteMergeRequest( ILogger logger, IServiceProvider services, SendReceiveService srService, @@ -69,7 +70,7 @@ if (dryRun) { logger.LogInformation("Dry run, not actually syncing"); - return TypedResults.Ok(new CrdtFwdataProjectSyncService.SyncResult(0, 0)); + return TypedResults.Ok(new SyncResult(0, 0)); } var projectCode = await projectLookupService.GetProjectCode(projectId); diff --git a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs index 12258dcee..ee42e1026 100644 --- a/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs +++ b/backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs @@ -1,6 +1,7 @@ using System.Text.Json; using FwDataMiniLcmBridge.Api; using LcmCrdt; +using LexCore.Sync; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MiniLcm; @@ -13,8 +14,6 @@ namespace FwLiteProjectSync; public class CrdtFwdataProjectSyncService(IOptions lcmCrdtConfig, MiniLcmImport miniLcmImport, ILogger logger) { - public record SyncResult(int CrdtChanges, int FwdataChanges); - public async Task Sync(IMiniLcmApi crdtApi, FwDataMiniLcmApi fwdataApi, bool dryRun = false) { if (crdtApi is CrdtMiniLcmApi crdt && crdt.ProjectData.FwProjectId != fwdataApi.ProjectId) diff --git a/backend/LexBoxApi/Controllers/CrdtController.cs b/backend/LexBoxApi/Controllers/CrdtController.cs index 4c4e98bb0..4051d2b52 100644 --- a/backend/LexBoxApi/Controllers/CrdtController.cs +++ b/backend/LexBoxApi/Controllers/CrdtController.cs @@ -1,11 +1,14 @@ using System.Text.Json.Serialization; -using LexBoxApi.Auth; using SIL.Harmony.Core; +using LexBoxApi.Auth; +using LexBoxApi.Auth.Attributes; using LexBoxApi.Hub; using LexBoxApi.Services; using LexCore.Entities; using LexCore.ServiceInterfaces; +using LexCore.Sync; using LexData; +using Microsoft.AspNetCore.Http.Timeouts; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; @@ -21,7 +24,8 @@ public class CrdtController( IHubContext hubContext, IPermissionService permissionService, LoggedInContext loggedInContext, - ProjectService projectService) : ControllerBase + ProjectService projectService, + FwHeadlessClient fwHeadlessClient) : ControllerBase { private DbSet ServerCommits => dbContext.Set(); @@ -90,4 +94,14 @@ public async Task> GetProjectId(string code) return Ok(projectId); } + + [HttpPost("sync/{projectId}")] + [AdminRequired] + [RequestTimeout(300_000)]//5 minutes + public async Task> ExecuteMerge(Guid projectId) + { + var result = await fwHeadlessClient.CrdtSync(projectId); + if (result is null) return Problem("Failed to sync CRDT"); + return result; + } } diff --git a/backend/LexBoxApi/LexBoxApi.csproj b/backend/LexBoxApi/LexBoxApi.csproj index baffc9fb9..238fc7e57 100644 --- a/backend/LexBoxApi/LexBoxApi.csproj +++ b/backend/LexBoxApi/LexBoxApi.csproj @@ -33,6 +33,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/backend/LexBoxApi/LexBoxKernel.cs b/backend/LexBoxApi/LexBoxKernel.cs index 347449122..a92217cf2 100644 --- a/backend/LexBoxApi/LexBoxKernel.cs +++ b/backend/LexBoxApi/LexBoxKernel.cs @@ -49,6 +49,9 @@ public static void AddLexBoxApi(this IServiceCollection services, .ValidateDataAnnotations() .ValidateOnStart(); services.AddHttpClient(); + services.AddServiceDiscovery(); + services.AddHttpClient(client => client.BaseAddress = new ("http://fwHeadless")) + .AddServiceDiscovery();//service discovery means that we lookup the hostname in Services__fwHeadless__http in config services.AddHttpContextAccessor(); services.AddMemoryCache(); services.AddScoped(); diff --git a/backend/LexBoxApi/Services/FwHeadlessClient.cs b/backend/LexBoxApi/Services/FwHeadlessClient.cs new file mode 100644 index 000000000..48324f218 --- /dev/null +++ b/backend/LexBoxApi/Services/FwHeadlessClient.cs @@ -0,0 +1,19 @@ +using LexCore.Sync; + +namespace LexBoxApi.Services; + +public class FwHeadlessClient(HttpClient httpClient, ILogger logger) +{ + public async Task CrdtSync(Guid projectId) + { + var response = await httpClient.PostAsync($"/api/crdt-sync?projectId={projectId}", null); + if (response.IsSuccessStatusCode) + return await response.Content.ReadFromJsonAsync(); + logger.LogError("Failed to sync CRDT: {StatusCode} {StatusDescription}, projectId: {ProjectId}, response: {Response}", + response.StatusCode, + response.ReasonPhrase, + projectId, + await response.Content.ReadAsStringAsync()); + return null; + } +} diff --git a/backend/LexBoxApi/appsettings.Development.json b/backend/LexBoxApi/appsettings.Development.json index 35f3ab4dc..1705db6f7 100644 --- a/backend/LexBoxApi/appsettings.Development.json +++ b/backend/LexBoxApi/appsettings.Development.json @@ -77,5 +77,12 @@ "From": "Lexbox ", "EmailRenderHost": "localhost:3000", "BaseUrl": "http://localhost:3000" + }, + "Services": { + "fwHeadless": { + "http": [ + "localhost:5275" + ] + } } } diff --git a/backend/LexBoxApi/appsettings.json b/backend/LexBoxApi/appsettings.json index 6f3e5ef2e..3ae6b1f17 100644 --- a/backend/LexBoxApi/appsettings.json +++ b/backend/LexBoxApi/appsettings.json @@ -70,5 +70,10 @@ }, "Email": { "CreateProjectEmailDestination": "lexbox_support@groups.sil.org" + }, + "Services": { + "fwHeadless": { + "http": ["fw-headless"] + } } } diff --git a/backend/LexCore/Sync/SyncResult.cs b/backend/LexCore/Sync/SyncResult.cs new file mode 100644 index 000000000..70b7d0aa6 --- /dev/null +++ b/backend/LexCore/Sync/SyncResult.cs @@ -0,0 +1,3 @@ +namespace LexCore.Sync; + +public record SyncResult(int CrdtChanges, int FwdataChanges); diff --git a/backend/LexData/SeedingData.cs b/backend/LexData/SeedingData.cs index bb7e2fb44..553699e8b 100644 --- a/backend/LexData/SeedingData.cs +++ b/backend/LexData/SeedingData.cs @@ -156,6 +156,7 @@ private async Task SeedUserData(CancellationToken cancellationToken = default) Description = "Eastern Lawa project", Code = "elawa-dev-flex", Type = ProjectType.FLEx, + FlexProjectMetadata = new(), ProjectOrigin = ProjectMigrationStatus.Migrated, LastCommit = DateTimeOffset.UtcNow, RetentionPolicy = RetentionPolicy.Dev, diff --git a/deployment/base/lexbox-deployment.yaml b/deployment/base/lexbox-deployment.yaml index 15f25a185..a7c4da81a 100644 --- a/deployment/base/lexbox-deployment.yaml +++ b/deployment/base/lexbox-deployment.yaml @@ -205,6 +205,9 @@ spec: value: /tmp/tus-test-upload - name: Tus__ResetUploadPath value: /tmp/tus-reset-upload + - name: Services__fwHeadless__http__0 + value: fw-headless + - name: otel-collector image: otel/opentelemetry-collector-contrib:0.101.0 diff --git a/frontend/src/lib/forms/Button.svelte b/frontend/src/lib/forms/Button.svelte index b2f8c1e9c..595e951ba 100644 --- a/frontend/src/lib/forms/Button.svelte +++ b/frontend/src/lib/forms/Button.svelte @@ -7,13 +7,16 @@ export let type: undefined | 'submit' = undefined; export let size: undefined | 'btn-sm' = undefined; export let disabled = false; + export let customLoader = false; diff --git a/frontend/src/lib/icons/Icon.svelte b/frontend/src/lib/icons/Icon.svelte index 99fa52775..874b49d13 100644 --- a/frontend/src/lib/icons/Icon.svelte +++ b/frontend/src/lib/icons/Icon.svelte @@ -10,6 +10,8 @@ export let size: IconSize = 'text-lg'; export let color: `text-${string}` | undefined = undefined; export let pale = false; + export let spin = false; + export let spinReverse = false; // For pixel perfect text alignment, because the svgs often contain vertical white-space export let y: string | undefined = undefined; @@ -17,5 +19,5 @@ {#if icon} - + {/if} diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte index ec4a4bb70..76ea3ef35 100644 --- a/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte +++ b/frontend/src/routes/(authenticated)/project/[project_code]/+page.svelte @@ -55,6 +55,7 @@ import { onMount } from 'svelte'; import { getSearchParamValues } from '$lib/util/query-params'; import FlexModelVersionText from '$lib/components/Projects/FlexModelVersionText.svelte'; + import CrdtSyncButton from './CrdtSyncButton.svelte'; export let data: PageData; $: user = data.user; @@ -312,6 +313,7 @@ {/if} {#if project.type === ProjectType.FlEx && $isDev} + {:else} diff --git a/frontend/src/routes/(authenticated)/project/[project_code]/CrdtSyncButton.svelte b/frontend/src/routes/(authenticated)/project/[project_code]/CrdtSyncButton.svelte new file mode 100644 index 000000000..324c40126 --- /dev/null +++ b/frontend/src/routes/(authenticated)/project/[project_code]/CrdtSyncButton.svelte @@ -0,0 +1,43 @@ + + + From a5255639533ab805088579be42fc4ac5495f1d81 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Mon, 25 Nov 2024 10:49:13 +0100 Subject: [PATCH 02/27] Migrate everything from Shouldly to FluentAssertions (#1262) * Migrate everything from Shouldly to FluentAssertions * Don't reference Testing.csproj from MiniLcm.Tests * Address feedback * Upgrade to FluentAssertions 7-prerelease * Fix assertion that's now case-sensitive --------- Co-authored-by: Robin Munn --- .../FwDataMiniLcmBridge.Tests.csproj | 6 +- .../UpdateComplexFormsTests.cs | 28 ++++----- .../FwLiteProjectSync.Tests/EntrySyncTests.cs | 4 +- .../FwLiteProjectSync.Tests.csproj | 2 +- .../LcmCrdt.Tests/Changes/ComplexFormTests.cs | 14 ++--- .../FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj | 2 +- .../LcmCrdt.Tests/SerializationTests.cs | 2 +- .../FwLite/MiniLcm.Tests/BasicApiTestsBase.cs | 2 +- .../MiniLcm.Tests/CreateEntryTestsBase.cs | 12 ++-- .../FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj | 6 +- .../MiniLcm.Tests/SemanticDomainTestsBase.cs | 8 +-- .../MiniLcm.Tests/WritingSystemIdTests.cs | 8 +-- backend/LexBoxApi/GraphQL/ProjectMutations.cs | 4 +- backend/Testing/ApiTests/ApiTestBase.cs | 13 +++-- backend/Testing/ApiTests/AuthTests.cs | 30 +++++----- backend/Testing/ApiTests/FlexJwtTests.cs | 22 ++++--- .../Testing/ApiTests/GqlMiddlewareTests.cs | 12 ++-- backend/Testing/ApiTests/HeaderTests.cs | 4 +- backend/Testing/ApiTests/InvalidRouteTests.cs | 6 +- .../ApiTests/NewProjectRaceCondition.cs | 15 +++-- .../Testing/ApiTests/OrgPermissionTests.cs | 44 +++++++------- .../ApiTests/ProjectPermissionTests.cs | 16 ++--- .../ApiTests/ResetProjectRaceConditions.cs | 22 +++---- .../Testing/Fixtures/IntegrationFixture.cs | 6 +- .../Fixtures/IntegrationFixtureTests.cs | 8 +-- .../Fixtures/Tests/ServicesFixtureTests.cs | 4 +- .../LexAuthUserOutOfSyncExtensionsTests.cs | 58 +++++++++---------- .../Testing/LexCore/CrdtServerCommitTests.cs | 8 +-- backend/Testing/LexCore/LexAuthUserTests.cs | 52 +++++++++-------- .../Testing/LexCore/PasswordHashingTests.cs | 6 +- backend/Testing/LexCore/ProjectCodeTests.cs | 6 +- .../LexCore/Services/HgServiceTests.cs | 12 ++-- .../LexCore/Services/ProjectServiceTest.cs | 17 +++--- .../Utils/ConcurrentWeakDictionaryTests.cs | 20 +++---- backend/Testing/LexCore/Utils/GqlUtils.cs | 8 +-- .../Services/CleanupResetProjectsTests.cs | 16 ++--- .../IsLanguageForgeProjectDataLoaderTests.cs | 10 ++-- backend/Testing/Services/JwtHelper.cs | 4 +- .../Testing/Services/SendReceiveService.cs | 14 ++--- backend/Testing/Services/Utils.cs | 10 ++-- backend/Testing/Services/UtilsTests.cs | 4 +- .../SyncReverseProxy/LegacyProjectApiTests.cs | 57 +++++++++--------- .../SyncReverseProxy/ProxyHgRequestTests.cs | 20 +++---- .../SyncReverseProxy/ResumableTests.cs | 24 ++++---- .../SendReceiveServiceTests.cs | 51 ++++++++-------- backend/Testing/Testing.csproj | 2 +- backend/Testing/Usings.cs | 2 +- 47 files changed, 352 insertions(+), 349 deletions(-) diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/FwDataMiniLcmBridge.Tests.csproj b/backend/FwLite/FwDataMiniLcmBridge.Tests/FwDataMiniLcmBridge.Tests.csproj index 13c53ae52..f2ff05e5d 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/FwDataMiniLcmBridge.Tests.csproj +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/FwDataMiniLcmBridge.Tests.csproj @@ -20,14 +20,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs b/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs index 7499f06e3..acc4c96bf 100644 --- a/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs +++ b/backend/FwLite/FwDataMiniLcmBridge.Tests/UpdateComplexFormsTests.cs @@ -34,7 +34,7 @@ await _api.UpdateEntry(complexForm.Id, ComplexFormComponent.FromEntries(complexForm, component))); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.Components.Should() + entry.Components.Should() .ContainSingle(c => c.ComponentEntryId == component.Id && c.ComplexFormEntryId == complexForm.Id); } @@ -63,7 +63,7 @@ await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Remove(e => e.Components, 0)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.Components.Should().BeEmpty(); + entry.Components.Should().BeEmpty(); } [Fact] @@ -92,7 +92,7 @@ await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentEntryId, component2.Id)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - var complexFormComponent = entry!.Components.Should().ContainSingle().Subject; + var complexFormComponent = entry.Components.Should().ContainSingle().Subject; complexFormComponent.ComponentEntryId.Should().Be(component2.Id); } @@ -127,7 +127,7 @@ await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentSenseId, component2SenseId)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - var complexFormComponent = entry!.Components.Should().ContainSingle().Subject; + var complexFormComponent = entry.Components.Should().ContainSingle().Subject; complexFormComponent.ComponentEntryId.Should().Be(component2.Id); complexFormComponent.ComponentSenseId.Should().Be(component2SenseId); } @@ -163,7 +163,7 @@ await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComponentSenseId, null)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.Components.Should() + entry.Components.Should() .ContainSingle(c => c.ComponentEntryId == component2.Id && c.ComponentSenseId == null); } @@ -193,7 +193,7 @@ await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.Components[0].ComplexFormEntryId, complexForm2.Id)); var entry = await _api.GetEntry(complexForm2.Id); entry.Should().NotBeNull(); - var complexFormComponent = entry!.Components.Should().ContainSingle().Subject; + var complexFormComponent = entry.Components.Should().ContainSingle().Subject; complexFormComponent.ComponentEntryId.Should().Be(component1.Id); } @@ -208,7 +208,7 @@ await _api.UpdateEntry(component.Id, ComplexFormComponent.FromEntries(complexForm, component))); var entry = await _api.GetEntry(component.Id); entry.Should().NotBeNull(); - entry!.ComplexForms.Should() + entry.ComplexForms.Should() .ContainSingle(c => c.ComponentEntryId == component.Id && c.ComplexFormEntryId == complexForm.Id); } @@ -237,7 +237,7 @@ await _api.UpdateEntry(component.Id, new UpdateObjectInput().Remove(e => e.ComplexForms, 0)); var entry = await _api.GetEntry(component.Id); entry.Should().NotBeNull(); - entry!.ComplexForms.Should().BeEmpty(); + entry.ComplexForms.Should().BeEmpty(); } [Fact] @@ -266,7 +266,7 @@ await _api.UpdateEntry(component1.Id, new UpdateObjectInput().Set(e => e.ComplexForms[0].ComplexFormEntryId, complexForm2.Id)); var entry = await _api.GetEntry(component1.Id); entry.Should().NotBeNull(); - var complexFormComponent = entry!.ComplexForms.Should().ContainSingle().Subject; + var complexFormComponent = entry.ComplexForms.Should().ContainSingle().Subject; complexFormComponent.ComplexFormEntryId.Should().Be(complexForm2.Id); } @@ -296,7 +296,7 @@ await _api.UpdateEntry(component1.Id, new UpdateObjectInput().Set(e => e.ComplexForms[0].ComponentEntryId, component2.Id)); var entry = await _api.GetEntry(component2.Id); entry.Should().NotBeNull(); - var complexFormComponent = entry!.ComplexForms.Should().ContainSingle().Subject; + var complexFormComponent = entry.ComplexForms.Should().ContainSingle().Subject; complexFormComponent.ComponentEntryId.Should().Be(component2.Id); complexFormComponent.ComplexFormEntryId.Should().Be(complexFormId); } @@ -338,7 +338,7 @@ await _api.UpdateEntry(component1.Id, new UpdateObjectInput().Set(e => e.ComplexForms[0].ComponentSenseId, component1SenseId2)); var entry = await _api.GetEntry(component1.Id); entry.Should().NotBeNull(); - var complexFormComponent = entry!.ComplexForms.Should().ContainSingle().Subject; + var complexFormComponent = entry.ComplexForms.Should().ContainSingle().Subject; complexFormComponent.ComponentEntryId.Should().Be(componentId1); complexFormComponent.ComponentSenseId.Should().Be(component1SenseId2); } @@ -353,7 +353,7 @@ await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Add(e => e.ComplexFormTypes, complexFormType)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.ComplexFormTypes.Should().ContainSingle(c => c.Id == complexFormType.Id); + entry.ComplexFormTypes.Should().ContainSingle(c => c.Id == complexFormType.Id); } [Fact] @@ -369,7 +369,7 @@ await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Remove(e => e.ComplexFormTypes, 0)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.ComplexFormTypes.Should().BeEmpty(); + entry.ComplexFormTypes.Should().BeEmpty(); } [Fact] @@ -382,6 +382,6 @@ await _api.UpdateEntry(complexForm.Id, new UpdateObjectInput().Set(e => e.ComplexFormTypes[0].Id, complexFormType2.Id)); var entry = await _api.GetEntry(complexForm.Id); entry.Should().NotBeNull(); - entry!.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(complexFormType2.Id); + entry.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(complexFormType2.Id); } } diff --git a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs index d1b264839..021587cfd 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs +++ b/backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs @@ -1,7 +1,7 @@ -using FluentAssertions.Equivalency; -using FwLiteProjectSync.Tests.Fixtures; +using FwLiteProjectSync.Tests.Fixtures; using MiniLcm.Models; using MiniLcm.SyncHelpers; +using MiniLcm.Tests; using MiniLcm.Tests.AutoFakerHelpers; using Soenneker.Utils.AutoBogus; diff --git a/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj b/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj index 1ae1063f4..c988caadc 100644 --- a/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj +++ b/backend/FwLite/FwLiteProjectSync.Tests/FwLiteProjectSync.Tests.csproj @@ -20,7 +20,7 @@ - + diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs index 5f5be9ed1..804cacdad 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ComplexFormTests.cs @@ -18,7 +18,7 @@ public async Task AddComplexFormType() await fixture.DataModel.AddChange(Guid.NewGuid(), change); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); - complexEntry!.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(change.ComplexFormType.Id); + complexEntry.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(change.ComplexFormType.Id); } [Fact] @@ -33,14 +33,14 @@ await fixture.DataModel.AddChange( ); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); - complexEntry!.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(complexFormType.Id); + complexEntry.ComplexFormTypes.Should().ContainSingle().Which.Id.Should().Be(complexFormType.Id); await fixture.DataModel.AddChange( Guid.NewGuid(), new RemoveComplexFormTypeChange(complexEntry.Id, complexFormType.Id) ); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); - complexEntry!.ComplexFormTypes.Should().BeEmpty(); + complexEntry.ComplexFormTypes.Should().BeEmpty(); } [Fact] @@ -55,12 +55,12 @@ public async Task AddEntryComponent() await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(ComplexFormComponent.FromEntries(complexEntry, rackEntry))); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); - complexEntry!.Components.Should().ContainSingle(e => e.ComponentEntryId == coatEntry.Id); + complexEntry.Components.Should().ContainSingle(e => e.ComponentEntryId == coatEntry.Id); complexEntry.Components.Should().ContainSingle(e => e.ComponentEntryId == rackEntry.Id); coatEntry = await fixture.Api.GetEntry(coatEntry.Id); coatEntry.Should().NotBeNull(); - coatEntry!.ComplexForms.Should().ContainSingle(e => e.ComplexFormEntryId == complexEntry.Id); + coatEntry.ComplexForms.Should().ContainSingle(e => e.ComplexFormEntryId == complexEntry.Id); } [Fact] @@ -74,11 +74,11 @@ public async Task DeleteEntryComponent() await fixture.DataModel.AddChange(Guid.NewGuid(), new AddEntryComponentChange(ComplexFormComponent.FromEntries(complexEntry, rackEntry))); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); - var component = complexEntry!.Components.First(); + var component = complexEntry.Components.First(); await fixture.DataModel.AddChange(Guid.NewGuid(), new DeleteChange(component.Id)); complexEntry = await fixture.Api.GetEntry(complexEntry.Id); complexEntry.Should().NotBeNull(); - complexEntry!.Components.Should().NotContain(c => c.Id == component.Id); + complexEntry.Components.Should().NotContain(c => c.Id == component.Id); } } diff --git a/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj b/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj index d955b734a..9f03df743 100644 --- a/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj +++ b/backend/FwLite/LcmCrdt.Tests/LcmCrdt.Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/backend/FwLite/LcmCrdt.Tests/SerializationTests.cs b/backend/FwLite/LcmCrdt.Tests/SerializationTests.cs index 268255141..52002507f 100644 --- a/backend/FwLite/LcmCrdt.Tests/SerializationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/SerializationTests.cs @@ -76,7 +76,7 @@ public void CanDeserializeMultiString() }; var actualMs = JsonSerializer.Deserialize(json); actualMs.Should().NotBeNull(); - actualMs!.Values.Should().ContainKey("en"); + actualMs.Values.Should().ContainKey("en"); actualMs.Should().BeEquivalentTo(expectedMs); } diff --git a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs index da63a7f4d..9b1f05144 100644 --- a/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/BasicApiTestsBase.cs @@ -202,7 +202,7 @@ public async Task GetEntry() { var entry = await Api.GetEntry(Entry1Id); entry.Should().NotBeNull(); - entry!.LexemeForm.Values.Should().NotBeEmpty(); + entry.LexemeForm.Values.Should().NotBeEmpty(); var sense = entry.Senses.Should() .NotBeEmpty($"because '{entry.LexemeForm.Values.First().Value}' should have a sense").And.Subject.First(); sense.Gloss.Values.Should().NotBeEmpty(); diff --git a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs index 477362c03..37fef2f21 100644 --- a/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/CreateEntryTestsBase.cs @@ -9,7 +9,7 @@ public async Task CanCreateEntry() { var entry = await Api.CreateEntry(new() { LexemeForm = { { "en", "test" } } }); entry.Should().NotBeNull(); - entry!.LexemeForm.Values.Should().ContainKey("en"); + entry.LexemeForm.Values.Should().ContainKey("en"); entry.LexemeForm.Values["en"].Should().Be("test"); } @@ -53,7 +53,7 @@ public async Task CanCreate_WithComponentsProperty() }); entry = await Api.GetEntry(entry.Id); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => c.ComponentEntryId == component.Id); + entry.Components.Should().ContainSingle(c => c.ComponentEntryId == component.Id); } [Fact] @@ -78,7 +78,7 @@ public async Task CanCreate_WithComplexFormsProperty() }); entry = await Api.GetEntry(entry.Id); entry.Should().NotBeNull(); - entry!.ComplexForms.Should().ContainSingle(c => c.ComplexFormEntryId == complexForm.Id); + entry.ComplexForms.Should().ContainSingle(c => c.ComplexFormEntryId == complexForm.Id); } [Fact] @@ -110,11 +110,11 @@ await Api.CreateEntry(new() var entry = await Api.GetEntry(component.Id); entry.Should().NotBeNull(); - entry!.ComplexForms.Should().ContainSingle().Which.ComponentSenseId.Should().Be(componentSenseId); + entry.ComplexForms.Should().ContainSingle().Which.ComponentSenseId.Should().Be(componentSenseId); entry = await Api.GetEntry(complexFormEntryId); entry.Should().NotBeNull(); - entry!.Components.Should().ContainSingle(c => + entry.Components.Should().ContainSingle(c => c.ComplexFormEntryId == complexFormEntryId && c.ComponentEntryId == component.Id && c.ComponentSenseId == componentSenseId); } @@ -133,6 +133,6 @@ public async Task CanCreate_WithComplexFormTypesProperty() }); entry = await Api.GetEntry(entry.Id); entry.Should().NotBeNull(); - entry!.ComplexFormTypes.Should().ContainSingle(c => c.Id == complexFormType.Id); + entry.ComplexFormTypes.Should().ContainSingle(c => c.Id == complexFormType.Id); } } diff --git a/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj b/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj index 83030857f..be07e4f4e 100644 --- a/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj +++ b/backend/FwLite/MiniLcm.Tests/MiniLcm.Tests.csproj @@ -12,14 +12,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/FwLite/MiniLcm.Tests/SemanticDomainTestsBase.cs b/backend/FwLite/MiniLcm.Tests/SemanticDomainTestsBase.cs index 6704a50f0..38b6a5f63 100644 --- a/backend/FwLite/MiniLcm.Tests/SemanticDomainTestsBase.cs +++ b/backend/FwLite/MiniLcm.Tests/SemanticDomainTestsBase.cs @@ -31,11 +31,11 @@ await Api.CreateEntry(new Entry() }); } - private Task GetEntry() + private async Task GetEntry() { - var entry = Api.GetEntry(_entryId); + var entry = await Api.GetEntry(_entryId); entry.Should().NotBeNull(); - return entry!; + return entry; } [Fact] @@ -55,7 +55,7 @@ public async Task Sense_HasSemanticDomains() { var entry = await GetEntry(); entry.Should().NotBeNull(); - var sense = entry!.Senses.First(s => s.SemanticDomains.Any()); + var sense = entry.Senses.First(s => s.SemanticDomains.Any()); sense.SemanticDomains.Should().NotBeEmpty(); sense.SemanticDomains.Should().AllSatisfy(sd => { diff --git a/backend/FwLite/MiniLcm.Tests/WritingSystemIdTests.cs b/backend/FwLite/MiniLcm.Tests/WritingSystemIdTests.cs index 6448f77b0..14290cc07 100644 --- a/backend/FwLite/MiniLcm.Tests/WritingSystemIdTests.cs +++ b/backend/FwLite/MiniLcm.Tests/WritingSystemIdTests.cs @@ -1,6 +1,4 @@ -using MiniLcm.Models; - -namespace MiniLcm.Tests; +namespace MiniLcm.Tests; public class WritingSystemIdTests { @@ -11,7 +9,7 @@ public class WritingSystemIdTests public void ValidWritingSystemId_ShouldNotThrow(string code) { var ws = new WritingSystemId(code); - ws.Should().NotBeNull(); + ws.Should().NotBe(default); } [Theory] @@ -29,6 +27,6 @@ public void InvalidWritingSystemId_ShouldThrow(string code) public void DefaultWritingSystemId_IsValid() { var ws = new WritingSystemId("default"); - ws.Should().NotBeNull(); + ws.Should().NotBe(default); } } diff --git a/backend/LexBoxApi/GraphQL/ProjectMutations.cs b/backend/LexBoxApi/GraphQL/ProjectMutations.cs index 2f6df4039..b481eb7c6 100644 --- a/backend/LexBoxApi/GraphQL/ProjectMutations.cs +++ b/backend/LexBoxApi/GraphQL/ProjectMutations.cs @@ -26,14 +26,14 @@ public enum CreateProjectResult Requested } - public record CreateProjectResponse(Guid? Id, CreateProjectResult Result); + public record CreateProjectResponse(Guid Id, CreateProjectResult Result); [Error] [Error] [Error] [UseMutationConvention] [RefreshJwt] [VerifiedEmailRequired] - public async Task CreateProject( + public async Task CreateProject( LoggedInContext loggedInContext, IPermissionService permissionService, CreateProjectInput input, diff --git a/backend/Testing/ApiTests/ApiTestBase.cs b/backend/Testing/ApiTests/ApiTestBase.cs index 1f9901865..cbf22d856 100644 --- a/backend/Testing/ApiTests/ApiTestBase.cs +++ b/backend/Testing/ApiTests/ApiTestBase.cs @@ -1,10 +1,10 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http.Json; using System.Text.Json.Nodes; +using FluentAssertions; using LexCore.Auth; using Microsoft.Extensions.Http.Resilience; using Polly; -using Shouldly; using Testing.LexCore.Utils; using Testing.Services; @@ -67,14 +67,14 @@ public async Task ExecuteGql([StringSyntax("graphql")] string gql, b var response = await HttpClient.PostAsJsonAsync($"{BaseUrl}/api/graphql{jwtParam}", new { query = gql }); if (JwtHelper.TryGetJwtFromLoginResponse(response, out var jwt)) CurrJwt = jwt; var jsonResponse = await response.Content.ReadFromJsonAsync(); - jsonResponse.ShouldNotBeNull($"for query {gql} ({(int)response.StatusCode} ({response.ReasonPhrase}))"); + jsonResponse.Should().NotBeNull($"for query {gql} ({(int)response.StatusCode} ({response.ReasonPhrase}))"); GqlUtils.ValidateGqlErrors(jsonResponse, expectGqlError); if (expectSuccessCode) - response.IsSuccessStatusCode.ShouldBeTrue($"code was {(int)response.StatusCode} ({response.ReasonPhrase})"); + response.IsSuccessStatusCode.Should().BeTrue($"code was {(int)response.StatusCode} ({response.ReasonPhrase})"); return jsonResponse; } - public async Task GetProjectLastCommit(string projectCode) + public async Task GetProjectLastCommit(string projectCode) { var jsonResult = await ExecuteGql($$""" query projectLastCommit { @@ -83,8 +83,9 @@ query projectLastCommit { } } """); - var project = jsonResult?["data"]?["projectByCode"].ShouldBeOfType(); - return project?["lastCommit"]?.ToString(); + var project = jsonResult?["data"]?["projectByCode"].Should().BeOfType().Subject; + var stringDate = project?["lastCommit"]?.ToString(); + return stringDate == null ? null : DateTimeOffset.Parse(stringDate); } public async Task StartLexboxProjectReset(string projectCode) diff --git a/backend/Testing/ApiTests/AuthTests.cs b/backend/Testing/ApiTests/AuthTests.cs index 36fa74ae5..33a065e4f 100644 --- a/backend/Testing/ApiTests/AuthTests.cs +++ b/backend/Testing/ApiTests/AuthTests.cs @@ -7,7 +7,7 @@ using LexCore.Auth; using LexSyncReverseProxy; using LfClassicData; -using Shouldly; +using FluentAssertions; using Testing.Services; namespace Testing.ApiTests; @@ -22,16 +22,16 @@ public async Task TestLoginAndVerifyDifferentUsers() var managerResponse = await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/api/user/currentUser"), HttpCompletionOption.ResponseContentRead); - managerResponse.StatusCode.ShouldBe(HttpStatusCode.OK); + managerResponse.StatusCode.Should().Be(HttpStatusCode.OK); var manager = await managerResponse.Content.ReadFromJsonAsync(); - manager.GetProperty("email").GetString().ShouldBe("manager@test.com"); + manager.GetProperty("email").GetString().Should().Be("manager@test.com"); await LoginAs("admin", TestingEnvironmentVariables.DefaultPassword); var response = await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/api/user/currentUser"), HttpCompletionOption.ResponseContentRead); var admin = await response.Content.ReadFromJsonAsync(); - admin.GetProperty("email").GetString().ShouldBe("admin@test.com"); + admin.GetProperty("email").GetString().Should().Be("admin@test.com"); } [Fact] @@ -41,20 +41,20 @@ public async Task TestGqlVerifyDifferentUsers() var query = """query testGetMe { meAuth { id email }}"""; await LoginAs("manager", TestingEnvironmentVariables.DefaultPassword); var manager = await ExecuteGql(query); - manager.ShouldNotBeNull(); - manager["data"]!["meAuth"]!["email"]!.ToString().ShouldBe("manager@test.com"); + manager.Should().NotBeNull(); + manager["data"]!["meAuth"]!["email"]!.ToString().Should().Be("manager@test.com"); await LoginAs("admin", TestingEnvironmentVariables.DefaultPassword); var admin = await ExecuteGql(query); - admin.ShouldNotBeNull(); - admin["data"]!["meAuth"]!["email"]!.ToString().ShouldBe("admin@test.com"); + admin.Should().NotBeNull(); + admin["data"]!["meAuth"]!["email"]!.ToString().Should().Be("admin@test.com"); } [Fact] public async Task NotLoggedInIsNotPermittedToCallRequiresAuthApi() { var response = await HttpClient.GetAsync($"{BaseUrl}/api/AuthTesting/requires-auth"); - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] @@ -70,7 +70,7 @@ public async Task ManagerIsForbiddenFromAdminApi() { await LoginAs("manager", TestingEnvironmentVariables.DefaultPassword); var response = await HttpClient.GetAsync($"{BaseUrl}/api/AuthTesting/requires-admin"); - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } [Fact] @@ -86,11 +86,11 @@ public async Task NoOneCanCallForgotPasswordApi() { await LoginAs("manager", TestingEnvironmentVariables.DefaultPassword); var response = await HttpClient.GetAsync($"{BaseUrl}/api/AuthTesting/requires-forgot-password"); - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); await LoginAs("admin", TestingEnvironmentVariables.DefaultPassword); response = await HttpClient.GetAsync($"{BaseUrl}/api/AuthTesting/requires-forgot-password"); - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } [Fact] @@ -99,7 +99,7 @@ public async Task ClearingCookiesWorks() await LoginAs("manager", TestingEnvironmentVariables.DefaultPassword); ClearCookies(); var response = await HttpClient.GetAsync($"{BaseUrl}/api/AuthTesting/requires-auth"); - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact(Skip = "Not working due to oauth, to solve we should setup a login via oauth to use the right jwt")] @@ -136,7 +136,7 @@ public async Task JwtWithInvalidSignatureFailsAuth() { Headers = { Authorization = new AuthenticationHeaderValue("Bearer", newJwt) } }); - response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } //these must match because auth determines the project code from the route key using the method in HgHelpers @@ -144,6 +144,6 @@ public async Task JwtWithInvalidSignatureFailsAuth() [Fact] public void RouteKeyInLfClassicRoutesMustMatchRouteKeyInProxyConstants() { - LfClassicRoutes.ProjectCodeRouteKey.ShouldBe(ProxyConstants.HgProjectCodeRouteKey); + LfClassicRoutes.ProjectCodeRouteKey.Should().Be(ProxyConstants.HgProjectCodeRouteKey); } } diff --git a/backend/Testing/ApiTests/FlexJwtTests.cs b/backend/Testing/ApiTests/FlexJwtTests.cs index 4594590a1..3f3c82108 100644 --- a/backend/Testing/ApiTests/FlexJwtTests.cs +++ b/backend/Testing/ApiTests/FlexJwtTests.cs @@ -1,9 +1,7 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Net.Http.Json; -using System.Security.Claims; +using System.Net.Http.Json; using System.Text.Json; using LexCore.Auth; -using Shouldly; +using FluentAssertions; using Testing.Services; namespace Testing.ApiTests; @@ -37,18 +35,18 @@ public async Task CanGetProjectSpecificToken() //intentionally not using the RefreshResponse class to make sure this test still fails if properties are renamed var json = await response.Content.ReadFromJsonAsync(); var projectToken = json.GetProperty("projectToken").GetString(); - projectToken.ShouldNotBeEmpty(); + projectToken.Should().NotBeNullOrEmpty(); var user = ParseUserToken(projectToken); - user.Projects.ShouldHaveSingleItem(); - user.Audience.ShouldBe(LexboxAudience.SendAndReceive); + user.Projects.Should().ContainSingle(); + user.Audience.Should().Be(LexboxAudience.SendAndReceive); var flexToken = json.GetProperty("flexToken").GetString(); - flexToken.ShouldNotBeEmpty(); + flexToken.Should().NotBeNullOrEmpty(); var flexUser = ParseUserToken(flexToken); - flexUser.Projects.ShouldBeEmpty(); - flexUser.Audience.ShouldBe(LexboxAudience.SendAndReceiveRefresh); + flexUser.Projects.Should().BeEmpty(); + flexUser.Audience.Should().Be(LexboxAudience.SendAndReceiveRefresh); - json.GetProperty("projectTokenExpiresAt").GetDateTime().ShouldNotBe(default); - json.GetProperty("flexTokenExpiresAt").GetDateTime().ShouldNotBe(default); + json.GetProperty("projectTokenExpiresAt").GetDateTime().Should().NotBe(default); + json.GetProperty("flexTokenExpiresAt").GetDateTime().Should().NotBe(default); } } diff --git a/backend/Testing/ApiTests/GqlMiddlewareTests.cs b/backend/Testing/ApiTests/GqlMiddlewareTests.cs index d6c0ed834..72074b694 100644 --- a/backend/Testing/ApiTests/GqlMiddlewareTests.cs +++ b/backend/Testing/ApiTests/GqlMiddlewareTests.cs @@ -1,6 +1,6 @@ using System.Text.Json.Nodes; using LexCore.Entities; -using Shouldly; +using FluentAssertions; using Testing.Fixtures; using static Testing.Services.Utils; @@ -80,11 +80,11 @@ await Task.WhenAll( // if the user is allowed to view all members var json = await QueryMyProjectsWithMembers(); - json.ShouldNotBeNull(); + json.Should().NotBeNull(); var myProjects = json["data"]!["myProjects"]!.AsArray(); var ids = myProjects.Select(p => p!["id"]!.GetValue()); - projects.Select(p => p.Id).ShouldBeSubsetOf(ids); + projects.Select(p => p.Id).Should().BeSubsetOf(ids); } [Fact] @@ -104,7 +104,7 @@ await _adminApiTester.ExecuteGql($$""" } } """, expectGqlError: true); // we're not a member yet - _adminApiTester.CurrJwt.ShouldBe(editorJwt); // token wasn't updated + _adminApiTester.CurrJwt.Should().Be(editorJwt); // token wasn't updated await AddMemberToProject(config, _adminApiTester, "editor", ProjectRole.Editor, _adminJwt); @@ -116,7 +116,7 @@ await _adminApiTester.ExecuteGql($$""" } } """, expectGqlError: true); // we're a member, but didn't query for users, so... - _adminApiTester.CurrJwt.ShouldBe(editorJwt); // token wasn't updated + _adminApiTester.CurrJwt.Should().Be(editorJwt); // token wasn't updated var response = await _adminApiTester.ExecuteGql($$""" query { @@ -129,6 +129,6 @@ await _adminApiTester.ExecuteGql($$""" } } """, expectGqlError: false); // we queried for users, so... - _adminApiTester.CurrJwt.ShouldNotBe(editorJwt); // token was updated + _adminApiTester.CurrJwt.Should().NotBe(editorJwt); // token was updated } } diff --git a/backend/Testing/ApiTests/HeaderTests.cs b/backend/Testing/ApiTests/HeaderTests.cs index 8c3bf2bfc..97648256e 100644 --- a/backend/Testing/ApiTests/HeaderTests.cs +++ b/backend/Testing/ApiTests/HeaderTests.cs @@ -1,5 +1,5 @@ using System.Net; -using Shouldly; +using FluentAssertions; namespace Testing.ApiTests; @@ -31,7 +31,7 @@ public async Task CheckCloudflareHeaderSizeLimit() if (response.StatusCode != HttpStatusCode.OK) failStatusCodes.Add(response.StatusCode); } - failStatusCodes.ShouldBeEmpty(); + failStatusCodes.Should().BeEmpty(); } private string RandomString(int length) diff --git a/backend/Testing/ApiTests/InvalidRouteTests.cs b/backend/Testing/ApiTests/InvalidRouteTests.cs index 6886ca88b..9894056d3 100644 --- a/backend/Testing/ApiTests/InvalidRouteTests.cs +++ b/backend/Testing/ApiTests/InvalidRouteTests.cs @@ -1,5 +1,5 @@ using System.Net; -using Shouldly; +using FluentAssertions; using Testing.Services; namespace Testing.ApiTests; @@ -11,13 +11,13 @@ public class InvalidRouteTests : ApiTestBase public async Task ApiPathRequestsShouldBeServedByDotnetForAnonymous() { var response = await HttpClient.GetAsync($"{BaseUrl}/api/login/not-exists"); - response.StatusCode.ShouldBe(HttpStatusCode.NotFound); + response.StatusCode.Should().Be(HttpStatusCode.NotFound); } [Fact] public async Task ApiBasePathRequestsShouldBeServedByDotnetForAuthenticated() { await LoginAs("manager", TestingEnvironmentVariables.DefaultPassword); var response = await HttpClient.GetAsync($"{BaseUrl}/api/login/not-exists"); - response.StatusCode.ShouldBe(HttpStatusCode.NotFound); + response.StatusCode.Should().Be(HttpStatusCode.NotFound); } } diff --git a/backend/Testing/ApiTests/NewProjectRaceCondition.cs b/backend/Testing/ApiTests/NewProjectRaceCondition.cs index 2eed7b63b..6dcd30899 100644 --- a/backend/Testing/ApiTests/NewProjectRaceCondition.cs +++ b/backend/Testing/ApiTests/NewProjectRaceCondition.cs @@ -1,5 +1,5 @@ using System.Text.Json.Nodes; -using Shouldly; +using FluentAssertions; using Testing.Services; namespace Testing.ApiTests; @@ -47,12 +47,17 @@ private async Task CreateQueryAndVerifyProject(Guid id) createProjectResponse { id } + errors { + ... on Error { + message + } + } } } """); - var project = response["data"]!["createProject"]!["createProjectResponse"].ShouldBeOfType(); - project["id"]!.GetValue().ShouldBe(id.ToString()); + var project = response["data"]!["createProject"]!["createProjectResponse"].Should().BeOfType().Subject; + project["id"]!.GetValue().Should().Be(id.ToString()); // Query a 2nd time to ensure the instability of new repos isn't causing trouble response = await ExecuteGql($$""" @@ -66,7 +71,7 @@ private async Task CreateQueryAndVerifyProject(Guid id) } """); - project = response["data"]!["projectByCode"].ShouldBeOfType(); - project["name"]!.GetValue().ShouldBe(name); + project = response["data"]!["projectByCode"].Should().BeOfType().Subject; + project["name"]!.GetValue().Should().Be(name); } } diff --git a/backend/Testing/ApiTests/OrgPermissionTests.cs b/backend/Testing/ApiTests/OrgPermissionTests.cs index fabeab52a..dc8b59873 100644 --- a/backend/Testing/ApiTests/OrgPermissionTests.cs +++ b/backend/Testing/ApiTests/OrgPermissionTests.cs @@ -1,6 +1,6 @@ using System.Text.Json.Nodes; using LexData; -using Shouldly; +using FluentAssertions; namespace Testing.ApiTests; @@ -37,42 +37,42 @@ private async Task QueryOrg(Guid orgId) private static JsonObject GetOrg(JsonObject json) { var org = json["data"]?["orgById"]?.AsObject(); - org.ShouldNotBeNull(); + org.Should().NotBeNull(); return org; } private void MustHaveOneMemberWithEmail(JsonNode org) { org["members"]!.AsArray().Where(m => m?["user"]?["email"]?.GetValue() is { Length: > 0 }) - .ShouldNotBeEmpty(); + .Should().NotBeNullOrEmpty(); } private void MustNotHaveMemberWithEmail(JsonNode org) { org["members"]!.AsArray().Where(m => m?["user"]?["email"]?.GetValue() is { Length: > 0 }) - .ShouldBeEmpty(); + .Should().BeEmpty(); } private void MustHaveOneMemberWithUsername(JsonNode org) { org["members"]!.AsArray().Where(m => m?["user"]?["username"]?.GetValue() is { Length: > 0 }) - .ShouldNotBeEmpty(); + .Should().NotBeNullOrEmpty(); } private void MustNotHaveMemberWithUsername(JsonNode org) { org["members"]!.AsArray().Where(m => m?["user"]?["username"]?.GetValue() is { Length: > 0 }) - .ShouldBeEmpty(); + .Should().BeEmpty(); } private void MustHaveUserNames(JsonNode org) { org["members"]!.AsArray() .Where(m => m?["user"]?["name"]?.GetValue() is { Length: > 0 }) - .ShouldNotBeEmpty(); + .Should().NotBeNullOrEmpty(); } private void MustContainUser(JsonNode org, Guid id) { - org["members"]!.AsArray().ShouldContain( + org["members"]!.AsArray().Should().Contain( m => m!["user"]!["id"]!.GetValue() == id, $"org: '{org["name"]}' members were: {org["members"]!.ToJsonString()}"); } @@ -81,14 +81,14 @@ private void MustHaveOnlyManagers(JsonNode org) { org["members"]!.AsArray() .Where(m => m?["role"]?.GetValue() is not "ADMIN") - .ShouldBeEmpty(); + .Should().BeEmpty(); } private void MustHaveNonManagers(JsonNode org) { org["members"]!.AsArray() .Where(m => m?["role"]?.GetValue() is not "ADMIN") - .ShouldNotBeEmpty(); + .Should().NotBeNullOrEmpty(); } [Fact] @@ -113,8 +113,8 @@ public async Task CanNotListOrgsAndListOrgUsers() """, true, false); var error = json["errors"]?.AsArray().First()?.AsObject(); - error.ShouldNotBeNull(); - error["extensions"]?["code"]?.GetValue().ShouldBe("AUTH_NOT_AUTHORIZED"); + error.Should().NotBeNull(); + error["extensions"]?["code"]?.GetValue().Should().Be("AUTH_NOT_AUTHORIZED"); } [Fact] @@ -134,8 +134,8 @@ public async Task CanNotListOrgsAndListOrgProjects() """, true, false); var error = json["errors"]?.AsArray().First()?.AsObject(); - error.ShouldNotBeNull(); - error["extensions"]?["code"]?.GetValue().ShouldBe("AUTH_NOT_AUTHORIZED"); + error.Should().NotBeNull(); + error["extensions"]?["code"]?.GetValue().Should().Be("AUTH_NOT_AUTHORIZED"); } [Fact] @@ -167,7 +167,7 @@ public async Task OrgMemberCanSeeThemselvesInOrg() { await LoginAs("editor"); var org = GetOrg(await QueryOrg(SeedingData.TestOrgId)); - org.ShouldNotBeNull(); + org.Should().NotBeNull(); MustContainUser(org, SeedingData.EditorId); } @@ -176,7 +176,7 @@ public async Task OrgMemberCanNotSeeMemberEmails() { await LoginAs("editor"); var org = GetOrg(await QueryOrg(SeedingData.TestOrgId)); - org.ShouldNotBeNull(); + org.Should().NotBeNull(); MustHaveUserNames(org); MustNotHaveMemberWithEmail(org); } @@ -186,7 +186,7 @@ public async Task OrgMemberCanNotSeeMemberUsernames() { await LoginAs("editor"); var org = GetOrg(await QueryOrg(SeedingData.TestOrgId)); - org.ShouldNotBeNull(); + org.Should().NotBeNull(); MustHaveUserNames(org); MustNotHaveMemberWithUsername(org); } @@ -203,24 +203,24 @@ public async Task NonMemberCanOnlyQueryManagers() private void MustNotShowConfidentialProjects(JsonNode org) { var projects = org["projects"]!.AsArray(); - projects.ShouldNotBeEmpty(); + projects.Should().NotBeNullOrEmpty(); projects .Where(p => p?["isConfidential"]?.GetValue() != false) - .ShouldBeEmpty(); + .Should().BeEmpty(); } private void MustContainProject(JsonNode org, Guid projectId) { var projects = org["projects"]!.AsArray(); - projects.ShouldNotBeEmpty(); - projects.ShouldContain(p => p!["id"]!.GetValue() == projectId, $"project id '{projectId}' should exist in: {projects.ToJsonString()}"); + projects.Should().NotBeNullOrEmpty(); + projects.Should().Contain(p => p!["id"]!.GetValue() == projectId, $"project id '{projectId}' should exist in: {projects.ToJsonString()}"); } private void MustNotContainProject(JsonNode org, Guid projectId) { var projects = org["projects"]!.AsArray(); if ((projects?.Count ?? 0) == 0) return; - projects!.ShouldNotContain(p => p!["id"]!.GetValue() == projectId, $"project id '{projectId}' should not exist in: {projects!.ToJsonString()}"); + projects!.Should().NotContain(p => p!["id"]!.GetValue() == projectId, $"project id '{projectId}' should not exist in: {projects!.ToJsonString()}"); } [Fact] diff --git a/backend/Testing/ApiTests/ProjectPermissionTests.cs b/backend/Testing/ApiTests/ProjectPermissionTests.cs index 36f781ffc..4a0bf5c3b 100644 --- a/backend/Testing/ApiTests/ProjectPermissionTests.cs +++ b/backend/Testing/ApiTests/ProjectPermissionTests.cs @@ -1,5 +1,5 @@ using System.Text.Json.Nodes; -using Shouldly; +using FluentAssertions; using Testing.Services; namespace Testing.ApiTests; @@ -56,27 +56,27 @@ ... on Error { private JsonObject GetProject(JsonObject json) { var project = json["data"]!["projectByCode"]?.AsObject(); - project.ShouldNotBeNull(); + project.Should().NotBeNull(); return project; } private void MustHaveMembers(JsonObject project, int? count = null) { var members = project["users"]!.AsArray(); - members.ShouldNotBeNull().ShouldNotBeEmpty(); - if (count is not null) members.Count.ShouldBe(count.Value); + members.Should().NotBeNullOrEmpty(); + if (count is not null) members.Count.Should().Be(count.Value); } private void MustNotHaveMembers(JsonObject project) { var users = project["users"]!.AsArray(); - users.ShouldBeEmpty(); + users.Should().BeEmpty(); } private void MustHaveOnlyUserAsMember(JsonObject project, Guid userId) { var users = project["users"]!.AsArray(); - users.ShouldContain(node => node!["user"]!["id"]!.GetValue() == userId, + users.Should().Contain(node => node!["user"]!["id"]!.GetValue() == userId, "user list " + users.ToJsonString()); } @@ -132,7 +132,7 @@ public async Task ConfidentialProject_NonMemberCannotSeeProject() await LoginAs("user"); var json = await QueryProject(project.Code, expectGqlError: true); var error = json["errors"]!.AsArray().First()?.AsObject(); - error.ShouldNotBeNull(); - error["extensions"]?["code"]?.GetValue().ShouldBe("AUTH_NOT_AUTHORIZED"); + error.Should().NotBeNull(); + error["extensions"]?["code"]?.GetValue().Should().Be("AUTH_NOT_AUTHORIZED"); } } diff --git a/backend/Testing/ApiTests/ResetProjectRaceConditions.cs b/backend/Testing/ApiTests/ResetProjectRaceConditions.cs index 358d96790..cffb0ae1c 100644 --- a/backend/Testing/ApiTests/ResetProjectRaceConditions.cs +++ b/backend/Testing/ApiTests/ResetProjectRaceConditions.cs @@ -1,4 +1,4 @@ -using Shouldly; +using FluentAssertions; using Testing.Fixtures; using static Testing.Services.Utils; @@ -41,9 +41,9 @@ public async Task SimultaneousResetsDontResultIn404s() var lastCommitBefore2 = await _adminApiTester.GetProjectLastCommit(config2.Code); var lastCommitBefore3 = await _adminApiTester.GetProjectLastCommit(config3.Code); - lastCommitBefore1.ShouldBeNullOrWhiteSpace(); - lastCommitBefore2.ShouldBeNullOrWhiteSpace(); - lastCommitBefore3.ShouldBeNullOrWhiteSpace(); + lastCommitBefore1.Should().BeNull(); + lastCommitBefore2.Should().BeNull(); + lastCommitBefore3.Should().BeNull(); // Reset and fill projects on server var newLastCommits = await Task.WhenAll( @@ -52,9 +52,9 @@ public async Task SimultaneousResetsDontResultIn404s() DoFullProjectResetAndVerifyLastCommit(config3.Code) ); - newLastCommits[0].ShouldNotBeNullOrWhiteSpace(); - newLastCommits[0].ShouldBe(newLastCommits[1]); - newLastCommits[0].ShouldBe(newLastCommits[2]); + newLastCommits[0].Should().NotBeNull(); + newLastCommits[0].Should().Be(newLastCommits[1]); + newLastCommits[0].Should().Be(newLastCommits[2]); // we need a short delay between resets or we'll get naming collisions on the backups of the reset projects await Task.Delay(1000); @@ -68,15 +68,15 @@ public async Task SimultaneousResetsDontResultIn404s() ); } - private async Task DoFullProjectResetAndVerifyLastCommit(string projectCode, string? expectedLastCommit = null) + private async Task DoFullProjectResetAndVerifyLastCommit(string projectCode, DateTimeOffset? expectedLastCommit = null) { await _adminApiTester.StartLexboxProjectReset(projectCode); var lastCommitBefore = await _adminApiTester.GetProjectLastCommit(projectCode); - lastCommitBefore.ShouldBeNullOrWhiteSpace(); + lastCommitBefore.Should().BeNull(); await _fixture.FinishLexboxProjectResetWithTemplateRepo(projectCode); var lastCommit = await _adminApiTester.GetProjectLastCommit(projectCode); - if (expectedLastCommit is not null) lastCommit.ShouldBe(expectedLastCommit); - else lastCommit.ShouldNotBeNullOrWhiteSpace(); + if (expectedLastCommit is not null) lastCommit.Should().Be(expectedLastCommit); + else lastCommit.Should().NotBeNull(); return lastCommit; } } diff --git a/backend/Testing/Fixtures/IntegrationFixture.cs b/backend/Testing/Fixtures/IntegrationFixture.cs index df951bb8d..77a1f282a 100644 --- a/backend/Testing/Fixtures/IntegrationFixture.cs +++ b/backend/Testing/Fixtures/IntegrationFixture.cs @@ -1,7 +1,7 @@ using System.IO.Compression; using System.Runtime.CompilerServices; using LexCore.Utils; -using Shouldly; +using FluentAssertions; using Squidex.Assets; using Testing.ApiTests; using Testing.Services; @@ -16,7 +16,7 @@ public class IntegrationFixture : IAsyncLifetime public static readonly DirectoryInfo TemplateRepo = new(Path.Join(BasePath, "_template-repo_")); public ApiTestBase AdminApiTester { get; private set; } = new(); private string? _adminJwt = null; - public string AdminJwt => _adminJwt.ShouldNotBeNull(); + public string AdminJwt => _adminJwt.Should().NotBeNull().And.Subject; static IntegrationFixture() { @@ -69,7 +69,7 @@ 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); + Directory.EnumerateFiles(projectPath.Dir).Should().Contain(projectPath.FwDataFile); } public async Task FinishLexboxProjectResetWithTemplateRepo(string projectCode) diff --git a/backend/Testing/Fixtures/IntegrationFixtureTests.cs b/backend/Testing/Fixtures/IntegrationFixtureTests.cs index 18697e956..d606fbacc 100644 --- a/backend/Testing/Fixtures/IntegrationFixtureTests.cs +++ b/backend/Testing/Fixtures/IntegrationFixtureTests.cs @@ -1,5 +1,5 @@ using Moq; -using Shouldly; +using FluentAssertions; using Testing.ApiTests; namespace Testing.Fixtures; @@ -18,7 +18,7 @@ public async Task InitCreatesARepoWithTheProject() await fixture.InitializeAsync(Mock.Of()); IntegrationFixture.TemplateRepo.EnumerateFiles() .Select(f => f.Name) - .ShouldContain("kevin-test-01.fwdata"); + .Should().Contain("kevin-test-01.fwdata"); } [Fact] @@ -27,7 +27,7 @@ public async Task CanFindTheProjectZipFile() await fixture.InitializeAsync(Mock.Of()); IntegrationFixture.TemplateRepoZip .Directory!.EnumerateFiles().Select(f => f.Name) - .ShouldContain(IntegrationFixture.TemplateRepoZip.Name); + .Should().Contain(IntegrationFixture.TemplateRepoZip.Name); } [Fact] @@ -36,6 +36,6 @@ public async Task CanInitFlexProjectRepo() await fixture.InitializeAsync(Mock.Of()); var projectConfig = fixture.InitLocalFlexProjectWithRepo(); Directory.EnumerateFiles(projectConfig.Dir) - .ShouldContain(projectConfig.FwDataFile); + .Should().Contain(projectConfig.FwDataFile); } } diff --git a/backend/Testing/Fixtures/Tests/ServicesFixtureTests.cs b/backend/Testing/Fixtures/Tests/ServicesFixtureTests.cs index 519c396f8..8369b562b 100644 --- a/backend/Testing/Fixtures/Tests/ServicesFixtureTests.cs +++ b/backend/Testing/Fixtures/Tests/ServicesFixtureTests.cs @@ -1,4 +1,4 @@ -using Shouldly; +using FluentAssertions; namespace Testing.Fixtures.Tests; @@ -13,6 +13,6 @@ public async Task CanSetupServices() await fixture.InitializeAsync(); await fixture.DisposeAsync(); }; - Should.CompleteIn(act, TimeSpan.FromSeconds(10)); + await act.Should().CompleteWithinAsync(TimeSpan.FromSeconds(10)); } } diff --git a/backend/Testing/GraphQL/LexAuthUserOutOfSyncExtensionsTests.cs b/backend/Testing/GraphQL/LexAuthUserOutOfSyncExtensionsTests.cs index b8996ea79..3df103e75 100644 --- a/backend/Testing/GraphQL/LexAuthUserOutOfSyncExtensionsTests.cs +++ b/backend/Testing/GraphQL/LexAuthUserOutOfSyncExtensionsTests.cs @@ -1,7 +1,7 @@ using LexBoxApi.GraphQL; using LexCore.Auth; using LexCore.Entities; -using Shouldly; +using FluentAssertions; namespace Testing.GraphQL; @@ -20,10 +20,10 @@ public class LexAuthUserOutOfSyncExtensionsTests public void DetectsUserAddedToProject() { var project = NewProject(); - user.IsOutOfSyncWithProject(project).ShouldBeFalse(); + user.IsOutOfSyncWithProject(project).Should().BeFalse(); project.Users.Add(new() { UserId = user.Id, Role = ProjectRole.Editor }); - user.IsOutOfSyncWithProject(project).ShouldBeTrue(); + user.IsOutOfSyncWithProject(project).Should().BeTrue(); } [Fact] @@ -32,10 +32,10 @@ public void DetectsUserRemovedFromProject() var project = NewProject(); project.Users.Add(new() { UserId = user.Id, Role = ProjectRole.Editor }); var editorUser = user with { Projects = [new AuthUserProject(ProjectRole.Editor, project.Id)] }; - editorUser.IsOutOfSyncWithProject(project).ShouldBeFalse(); + editorUser.IsOutOfSyncWithProject(project).Should().BeFalse(); project.Users.Clear(); - editorUser.IsOutOfSyncWithProject(project).ShouldBeTrue(); + editorUser.IsOutOfSyncWithProject(project).Should().BeTrue(); } [Fact] @@ -45,10 +45,10 @@ public void DetectsUserProjectRoleChanged() var projectUser = new ProjectUsers { UserId = user.Id, Role = ProjectRole.Editor }; project.Users.Add(projectUser); var editorUser = user with { Projects = [new AuthUserProject(ProjectRole.Editor, project.Id)] }; - editorUser.IsOutOfSyncWithProject(project).ShouldBeFalse(); + editorUser.IsOutOfSyncWithProject(project).Should().BeFalse(); projectUser.Role = ProjectRole.Manager; - editorUser.IsOutOfSyncWithProject(project).ShouldBeTrue(); + editorUser.IsOutOfSyncWithProject(project).Should().BeTrue(); } [Fact] @@ -58,7 +58,7 @@ public void DoesNotDetectsUserProjectRoleChangedIfRolesNotAvailable() var projectUser = new ProjectUsers { UserId = user.Id, Role = ProjectRole.Unknown }; project.Users.Add(projectUser); var editorUser = user with { Projects = [new AuthUserProject(ProjectRole.Editor, project.Id)] }; - editorUser.IsOutOfSyncWithProject(project).ShouldBeFalse(); // might be out of sync, but we can't tell + editorUser.IsOutOfSyncWithProject(project).Should().BeFalse(); // might be out of sync, but we can't tell } [Fact] @@ -69,7 +69,7 @@ public void DoesNotDetectChangesWithoutProjectUsersIfNotMyProject() project.Users = null!; var editorUser = user with { Projects = [new AuthUserProject(ProjectRole.Editor, project.Id)] }; - editorUser.IsOutOfSyncWithProject(project).ShouldBeFalse(); // might be out of sync, but we can't tell + editorUser.IsOutOfSyncWithProject(project).Should().BeFalse(); // might be out of sync, but we can't tell } [Fact] @@ -79,9 +79,9 @@ public void DetectsAddedToMyProjectWithoutProjectUsers() // simulate Users not projected in GQL query project.Users = null!; - user.IsOutOfSyncWithProject(project).ShouldBeFalse(); // might be out of sync, but we can't tell - user.IsOutOfSyncWithProject(project, isMyProject: true).ShouldBeTrue(); - user.IsOutOfSyncWithMyProjects([project]).ShouldBeTrue(); + user.IsOutOfSyncWithProject(project).Should().BeFalse(); // might be out of sync, but we can't tell + user.IsOutOfSyncWithProject(project, isMyProject: true).Should().BeTrue(); + user.IsOutOfSyncWithMyProjects([project]).Should().BeTrue(); } [Fact] @@ -92,18 +92,18 @@ public void DetectsRemovedFromMyProjectWithoutProjectUsers() project.Users = null!; var editorUser = user with { Projects = [new AuthUserProject(ProjectRole.Editor, project.Id)] }; - editorUser.IsOutOfSyncWithProject(project).ShouldBeFalse(); // might be out of sync, but we can't tell - editorUser.IsOutOfSyncWithMyProjects([]).ShouldBeTrue(); + editorUser.IsOutOfSyncWithProject(project).Should().BeFalse(); // might be out of sync, but we can't tell + editorUser.IsOutOfSyncWithMyProjects([]).Should().BeTrue(); } [Fact] public void DetectsUserAddedToOrg() { var org = NewOrg(); - user.IsOutOfSyncWithOrg(org).ShouldBeFalse(); + user.IsOutOfSyncWithOrg(org).Should().BeFalse(); org.Members.Add(new() { UserId = user.Id, Role = OrgRole.User }); - user.IsOutOfSyncWithOrg(org).ShouldBeTrue(); + user.IsOutOfSyncWithOrg(org).Should().BeTrue(); } [Fact] @@ -112,10 +112,10 @@ public void DetectsUserRemovedFromOrg() var org = NewOrg(); org.Members.Add(new() { UserId = user.Id, Role = OrgRole.User }); var editorUser = user with { Orgs = [new AuthUserOrg(OrgRole.User, org.Id)] }; - editorUser.IsOutOfSyncWithOrg(org).ShouldBeFalse(); + editorUser.IsOutOfSyncWithOrg(org).Should().BeFalse(); org.Members.Clear(); - editorUser.IsOutOfSyncWithOrg(org).ShouldBeTrue(); + editorUser.IsOutOfSyncWithOrg(org).Should().BeTrue(); } [Fact] @@ -125,10 +125,10 @@ public void DetectsUserOrgRoleChanged() var orgUser = new OrgMember { UserId = user.Id, Role = OrgRole.User }; org.Members.Add(orgUser); var editorUser = user with { Orgs = [new AuthUserOrg(OrgRole.User, org.Id)] }; - editorUser.IsOutOfSyncWithOrg(org).ShouldBeFalse(); + editorUser.IsOutOfSyncWithOrg(org).Should().BeFalse(); orgUser.Role = OrgRole.Admin; - editorUser.IsOutOfSyncWithOrg(org).ShouldBeTrue(); + editorUser.IsOutOfSyncWithOrg(org).Should().BeTrue(); } [Fact] @@ -138,7 +138,7 @@ public void DoesNotDetectsUserOrgRoleChangedIfRolesNotAvailable() var orgUser = new OrgMember { UserId = user.Id, Role = OrgRole.Unknown }; org.Members.Add(orgUser); var editorUser = user with { Orgs = [new AuthUserOrg(OrgRole.User, org.Id)] }; - editorUser.IsOutOfSyncWithOrg(org).ShouldBeFalse(); // might be out of sync, but we can't tell + editorUser.IsOutOfSyncWithOrg(org).Should().BeFalse(); // might be out of sync, but we can't tell } [Fact] @@ -147,10 +147,10 @@ public void DetectsChangesWithOrgProjects() var org = NewOrg(); var project = NewProject(); org.Projects = [project]; - user.IsOutOfSyncWithOrg(org).ShouldBeFalse(); + user.IsOutOfSyncWithOrg(org).Should().BeFalse(); project.Users.Add(new() { UserId = user.Id, Role = ProjectRole.Editor }); - user.IsOutOfSyncWithOrg(org).ShouldBeTrue(); + user.IsOutOfSyncWithOrg(org).Should().BeTrue(); } [Fact] @@ -161,7 +161,7 @@ public void DoesNotDetectChangesWithoutOrgMembersIfNotMyOrg() org.Members = null!; var editorUser = user with { Orgs = [new AuthUserOrg(OrgRole.User, org.Id)] }; - editorUser.IsOutOfSyncWithOrg(org).ShouldBeFalse(); // might be out of sync, but we can't tell + editorUser.IsOutOfSyncWithOrg(org).Should().BeFalse(); // might be out of sync, but we can't tell } [Fact] @@ -171,9 +171,9 @@ public void DetectsAddedToMyOrgWithoutOrgMembers() // simulate Members not projected in GQL query org.Members = null!; - user.IsOutOfSyncWithOrg(org).ShouldBeFalse(); // might be out of sync, but we can't tell - user.IsOutOfSyncWithOrg(org, isMyOrg: true).ShouldBeTrue(); - user.IsOutOfSyncWithMyOrgs([org]).ShouldBeTrue(); + user.IsOutOfSyncWithOrg(org).Should().BeFalse(); // might be out of sync, but we can't tell + user.IsOutOfSyncWithOrg(org, isMyOrg: true).Should().BeTrue(); + user.IsOutOfSyncWithMyOrgs([org]).Should().BeTrue(); } [Fact] @@ -184,8 +184,8 @@ public void DetectsRemovedFromMyOrgWithoutOrgMembers() org.Members = null!; var editorUser = user with { Orgs = [new AuthUserOrg(OrgRole.User, org.Id)] }; - editorUser.IsOutOfSyncWithOrg(org).ShouldBeFalse(); // might be out of sync, but we can't tell - editorUser.IsOutOfSyncWithMyOrgs([]).ShouldBeTrue(); + editorUser.IsOutOfSyncWithOrg(org).Should().BeFalse(); // might be out of sync, but we can't tell + editorUser.IsOutOfSyncWithMyOrgs([]).Should().BeTrue(); } private static Project NewProject() diff --git a/backend/Testing/LexCore/CrdtServerCommitTests.cs b/backend/Testing/LexCore/CrdtServerCommitTests.cs index 4deff4c38..31ed6c747 100644 --- a/backend/Testing/LexCore/CrdtServerCommitTests.cs +++ b/backend/Testing/LexCore/CrdtServerCommitTests.cs @@ -4,7 +4,7 @@ using LexData.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Shouldly; +using FluentAssertions; using Testing.Fixtures; namespace Testing.LexCore; @@ -71,8 +71,8 @@ public async Task CanRoundTripCommitChanges() await _dbContext.SaveChangesAsync(); var actualCommit = await _dbContext.Set().AsNoTracking().FirstAsync(c => c.Id == commitId); - actualCommit.ShouldNotBeSameAs(expectedCommit); - JsonSerializer.Serialize(actualCommit.ChangeEntities[0].Change).ShouldBe(changeJson); + actualCommit.Should().NotBeSameAs(expectedCommit); + JsonSerializer.Serialize(actualCommit.ChangeEntities[0].Change).Should().Be(changeJson); } [Fact] @@ -80,6 +80,6 @@ public void TypePropertyShouldAlwaysBeFirst() { var changeJson = """{"name":"Joe","$type":"test"}"""; var jsonChange = JsonSerializer.Deserialize(changeJson); - JsonSerializer.Serialize(jsonChange).ShouldBe("""{"$type":"test","name":"Joe"}"""); + JsonSerializer.Serialize(jsonChange).Should().Be("""{"$type":"test","name":"Joe"}"""); } } diff --git a/backend/Testing/LexCore/LexAuthUserTests.cs b/backend/Testing/LexCore/LexAuthUserTests.cs index 967db437a..ed919e901 100644 --- a/backend/Testing/LexCore/LexAuthUserTests.cs +++ b/backend/Testing/LexCore/LexAuthUserTests.cs @@ -12,7 +12,8 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; -using Shouldly; +using FluentAssertions; +using FluentAssertions.Execution; namespace Testing.LexCore; @@ -58,12 +59,13 @@ public void CanGetClaimsFromUser() var emailClaim = new Claim(LexAuthConstants.EmailClaimType, _user.Email); var roleClaim = new Claim(LexAuthConstants.RoleClaimType, _user.Role.ToString()); var projectClaim = new Claim("proj", _user.ProjectsJson); - claims.ShouldSatisfyAllConditions( - () => claims.ShouldContain(idClaim.ToString()), - () => claims.ShouldContain(emailClaim.ToString()), - () => claims.ShouldContain(roleClaim.ToString()), - () => claims.ShouldContain(projectClaim.ToString()) - ); + using (new AssertionScope()) + { + claims.Should().Contain(idClaim.ToString()); + claims.Should().Contain(emailClaim.ToString()); + claims.Should().Contain(roleClaim.ToString()); + claims.Should().Contain(projectClaim.ToString()); + } } [Fact] @@ -71,7 +73,7 @@ public void CanRoundTripClaimsThroughAPrincipal() { var claims = _user.GetPrincipal("Testing"); var newUser = LexAuthUser.FromClaimsPrincipal(claims); - newUser.ShouldBeEquivalentTo(_user); + newUser.Should().BeEquivalentTo(_user); } [Fact] @@ -83,7 +85,7 @@ public void CanRoundTripClaimsThroughJwt() var outputJwt = tokenHandler.ReadJwtToken(encodedJwt); var principal = new ClaimsPrincipal(new ClaimsIdentity(outputJwt.Claims, "Testing")); var newUser = LexAuthUser.FromClaimsPrincipal(principal); - newUser.ShouldBeEquivalentTo(_user); + newUser.Should().BeEquivalentTo(_user); } [Fact] @@ -105,11 +107,11 @@ public void CanRoundTripClaimsWhenUsingSecurityTokenDescriptor() ); var tokenHandler = new JwtSecurityTokenHandler(); var token = tokenHandler.ReadJwtToken(jwt); - token.ValidTo.ShouldBe(expires.DateTime); - token.ValidFrom.ShouldBe(issuedAt.DateTime); - token.IssuedAt.ShouldBe(issuedAt.DateTime); + token.ValidTo.Should().Be(expires.DateTime); + token.ValidFrom.Should().Be(issuedAt.DateTime); + token.IssuedAt.Should().Be(issuedAt.DateTime); //props get converted to claims, but some we want to exclude because they are used elsewhere. - token.Claims.ShouldNotContain(c => c.Type == "props.issued" || c.Type == "props.expires"); + token.Claims.Should().NotContain(c => c.Type == "props.issued" || c.Type == "props.expires"); var json = Base64UrlEncoder.Decode(token.RawPayload); LexAuthUser? newUser; @@ -122,7 +124,7 @@ public void CanRoundTripClaimsWhenUsingSecurityTokenDescriptor() throw new JsonException("Could not deserialize user, json: " + json, e); } - newUser.ShouldBeEquivalentTo(_user); + newUser.Should().BeEquivalentTo(_user); } [Fact] @@ -149,15 +151,15 @@ public void CanRoundTripFromAuthTicketToAuthTicket() jwtUserOptions ); var actualTicket = JwtTicketDataFormat.ConvertJwtToAuthTicket(jwt, JwtBearerOptions, NullLogger.Instance); - actualTicket.ShouldNotBeNull(); - actualTicket.Properties.IssuedUtc.ShouldBe(ticket.Properties.IssuedUtc); - actualTicket.Properties.ExpiresUtc.ShouldBe(ticket.Properties.ExpiresUtc); + actualTicket.Should().NotBeNull(); + actualTicket.Properties.IssuedUtc.Should().Be(ticket.Properties.IssuedUtc); + actualTicket.Properties.ExpiresUtc.Should().Be(ticket.Properties.ExpiresUtc); //order by is because the order isn't important but the assertion fails if the order is different actualTicket.Properties.Items.OrderBy(kvp => kvp.Key) - .ShouldBe(ticket.Properties.Items.OrderBy(kvp => kvp.Key)); + .Should().Equal(ticket.Properties.Items.OrderBy(kvp => kvp.Key)); var newUser = LexAuthUser.FromClaimsPrincipal(actualTicket.Principal); - newUser.ShouldBeEquivalentTo(_user); + newUser.Should().BeEquivalentTo(_user); } [Fact] @@ -169,7 +171,7 @@ public void CanRoundTripJwtFromUserThroughLexAuthService() var outputJwt = tokenHandler.ReadJwtToken(jwt); var principal = new ClaimsPrincipal(new ClaimsIdentity(outputJwt.Claims, "Testing")); var newUser = LexAuthUser.FromClaimsPrincipal(principal); - newUser.ShouldBeEquivalentTo(_user); + newUser.Should().BeEquivalentTo(_user); } private const string knownGoodJwt = @@ -182,12 +184,12 @@ public void CanParseFromKnownGoodJwt() var outputJwt = tokenHandler.ReadJwtToken(knownGoodJwt); var principal = new ClaimsPrincipal(new ClaimsIdentity(outputJwt.Claims, "Testing")); var newUser = LexAuthUser.FromClaimsPrincipal(principal); - newUser.ShouldNotBeNull(); - newUser.UpdatedDate.ShouldBe(0); + newUser.Should().NotBeNull(); + newUser.UpdatedDate.Should().Be(0); //old jwt doesn't have updated date or orgs, we're ok with that so we correct the values to make the equivalence work newUser.Orgs = [ new AuthUserOrg(OrgRole.Admin, LexData.SeedingData.TestOrgId) ]; newUser.UpdatedDate = _user.UpdatedDate; - newUser.ShouldBeEquivalentTo(_user); + newUser.Should().BeEquivalentTo(_user); } [Fact] @@ -200,7 +202,7 @@ public void CheckingJwtLength() .ToArray() }; var (jwt, _, _) = _lexAuthService.GenerateJwt(user); - jwt.Length.ShouldBeLessThan(LexAuthUser.MaxJwtLength); + jwt.Length.Should().BeLessThan(LexAuthUser.MaxJwtLength); } [Fact] @@ -223,6 +225,6 @@ public void CanRoundTripThroughRefresh() var loggedInPrincipal = new ClaimsPrincipal(new ClaimsIdentity(tokenHandler.ReadJwtToken(redirectJwt).Claims, "Testing")); var newUser = LexAuthUser.FromClaimsPrincipal(loggedInPrincipal); - newUser.ShouldBeEquivalentTo(_user with { Audience = LexboxAudience.ForgotPassword }); + newUser.Should().BeEquivalentTo(_user with { Audience = LexboxAudience.ForgotPassword }); } } diff --git a/backend/Testing/LexCore/PasswordHashingTests.cs b/backend/Testing/LexCore/PasswordHashingTests.cs index 1ce079acb..4d33b9a37 100644 --- a/backend/Testing/LexCore/PasswordHashingTests.cs +++ b/backend/Testing/LexCore/PasswordHashingTests.cs @@ -1,5 +1,5 @@ using LexCore; -using Shouldly; +using FluentAssertions; namespace Testing.LexCore; @@ -10,6 +10,6 @@ public class PasswordHashingTests [Theory] public void CanHashPassword(string pw, string salt, string hash) { - PasswordHashing.RedminePasswordHash(pw, salt, false).ShouldBe(hash); + PasswordHashing.RedminePasswordHash(pw, salt, false).Should().Be(hash); } -} \ No newline at end of file +} diff --git a/backend/Testing/LexCore/ProjectCodeTests.cs b/backend/Testing/LexCore/ProjectCodeTests.cs index 49b387bfa..6f6945adf 100644 --- a/backend/Testing/LexCore/ProjectCodeTests.cs +++ b/backend/Testing/LexCore/ProjectCodeTests.cs @@ -1,5 +1,5 @@ using LexCore.Entities; -using Shouldly; +using FluentAssertions; namespace Testing.LexCore; @@ -29,7 +29,7 @@ public void InvalidCodesThrows(string code) public void ValidCodes(string code) { var projectCode = new ProjectCode(code); - projectCode.Value.ShouldBe(code); - projectCode.ToString().ShouldBe(code); + projectCode.Value.Should().Be(code); + projectCode.ToString().Should().Be(code); } } diff --git a/backend/Testing/LexCore/Services/HgServiceTests.cs b/backend/Testing/LexCore/Services/HgServiceTests.cs index 4372635f4..e3ff8e0f1 100644 --- a/backend/Testing/LexCore/Services/HgServiceTests.cs +++ b/backend/Testing/LexCore/Services/HgServiceTests.cs @@ -1,15 +1,13 @@ using System.IO.Compression; using LexBoxApi.Services; using LexCore.Config; -using LexCore.Entities; using LexCore.Exceptions; using LexSyncReverseProxy; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; using Moq.Contrib.HttpClient; -using Shouldly; -using Testing.Fixtures; +using FluentAssertions; namespace Testing.LexCore.Services; @@ -59,7 +57,7 @@ private void CleanUpTempDir() [InlineData(HgType.resumable, LexboxResumable)] public void DetermineProjectPrefixWorks(HgType type, string expectedUrl) { - HgService.DetermineProjectUrlPrefix(type, _hgConfig).ShouldBe(expectedUrl); + HgService.DetermineProjectUrlPrefix(type, _hgConfig).Should().Be(expectedUrl); } [Theory] @@ -82,7 +80,7 @@ public void HgDatesConvertedAccurately(string? input, string? expectedStr) { DateTimeOffset? expected = expectedStr == null ? null : DateTimeOffset.Parse(expectedStr); var actual = HgService.ConvertHgDate(input); - actual.ShouldBe(expected); + actual.Should().Be(expected); } [Theory] @@ -108,7 +106,7 @@ public async Task CanFinishResetByUnZippingAnArchive(string filePath) var repoPath = Path.GetFullPath(Path.Join(_hgConfig.RepoPath, "u", code)); Directory.EnumerateFiles(repoPath, "*", SearchOption.AllDirectories) .Select(p => Path.GetRelativePath(repoPath, p)) - .ShouldHaveSingleItem().ShouldBe(Path.Join(".hg", "important-file.bin")); + .Should().ContainSingle().Which.Should().Be(Path.Join(".hg", "important-file.bin")); } [Theory] @@ -143,6 +141,6 @@ public async Task ThrowsIfNoHgFolderIsFound() stream.Position = 0; var act = () => _hgService.FinishReset(code, stream); - act.ShouldThrow(); + await act.Should().ThrowAsync(); } } diff --git a/backend/Testing/LexCore/Services/ProjectServiceTest.cs b/backend/Testing/LexCore/Services/ProjectServiceTest.cs index 48dcc9739..6accdbbe8 100644 --- a/backend/Testing/LexCore/Services/ProjectServiceTest.cs +++ b/backend/Testing/LexCore/Services/ProjectServiceTest.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using Npgsql; -using Shouldly; +using FluentAssertions; using Testing.Fixtures; namespace Testing.LexCore.Services; @@ -51,7 +51,7 @@ public async Task CanCreateProject() { var projectId = await _projectService.CreateProject( new(null, "TestProject", "Test", "test1", ProjectType.FLEx, RetentionPolicy.Test, false, null, null)); - projectId.ShouldNotBe(default); + projectId.Should().NotBe(Guid.Empty); } [Fact] @@ -61,8 +61,8 @@ public async Task CanUpdateProjectLangTags() new(null, "TestProject", "Test", "test2", ProjectType.FLEx, RetentionPolicy.Test, false, null, null)); await _projectService.UpdateProjectLangTags(projectId); var project = await _lexBoxDbContext.Projects.Include(p => p.FlexProjectMetadata).SingleAsync(p => p.Id == projectId); - project.FlexProjectMetadata.ShouldNotBeNull(); - project.FlexProjectMetadata.WritingSystems.ShouldBeEquivalentTo(_writingSystems); + project.FlexProjectMetadata.Should().NotBeNull(); + project.FlexProjectMetadata.WritingSystems.Should().BeEquivalentTo(_writingSystems); } [Fact] @@ -72,11 +72,12 @@ public async Task ShouldErrorIfCreatingAProjectWithTheSameCode() await _projectService.CreateProject( new(null, "TestProject", "Test", "test-dup-code", ProjectType.FLEx, RetentionPolicy.Test, false, null, null)); - var exception = await _projectService.CreateProject( + var act = () => _projectService.CreateProject( new(null, "Test2", "Test desc", "test-dup-code", ProjectType.Unknown, RetentionPolicy.Dev, false, null, null) - ).ShouldThrowAsync(); + ); - exception.InnerException.ShouldBeOfType() - .SqlState.ShouldBe(PostgresErrorCodes.UniqueViolation); + (await act.Should().ThrowAsync()) + .WithInnerException() + .Which.SqlState.Should().Be(PostgresErrorCodes.UniqueViolation); } } diff --git a/backend/Testing/LexCore/Utils/ConcurrentWeakDictionaryTests.cs b/backend/Testing/LexCore/Utils/ConcurrentWeakDictionaryTests.cs index 1ac10e717..58dbb9699 100644 --- a/backend/Testing/LexCore/Utils/ConcurrentWeakDictionaryTests.cs +++ b/backend/Testing/LexCore/Utils/ConcurrentWeakDictionaryTests.cs @@ -1,5 +1,5 @@ using LexCore.Utils; -using Shouldly; +using FluentAssertions; namespace Testing.LexCore.Utils; @@ -12,8 +12,8 @@ public void Add_Then_Try_Get_Value_Test() var obj = new object(); var dict = new ConcurrentWeakDictionary(); dict.Add("key", obj); - dict.TryGetValue("key", out var value).ShouldBeTrue(); - value.ShouldBe(obj); + dict.TryGetValue("key", out var value).Should().BeTrue(); + value.Should().Be(obj); } [Fact] @@ -25,9 +25,9 @@ public void GetOrAdd_New_Key_Should_Add_And_Return_New_Value_Test() var returnedValue = dictionary.GetOrAdd("key", k => value); - returnedValue.ShouldBe(value); - dictionary.TryGetValue("key", out var existingValue).ShouldBeTrue(); - existingValue.ShouldBe(value); + returnedValue.Should().Be(value); + dictionary.TryGetValue("key", out var existingValue).Should().BeTrue(); + existingValue.Should().Be(value); } [Fact] @@ -41,9 +41,9 @@ public void GetOrAdd_Existing_Key_Should_Return_Existing_Value_Test() var returnedValue = dictionary.GetOrAdd(key, k => new object()); - returnedValue.ShouldBe(value); - dictionary.TryGetValue(key, out var existingValue).ShouldBeTrue(); - existingValue.ShouldBe(value); + returnedValue.Should().Be(value); + dictionary.TryGetValue(key, out var existingValue).Should().BeTrue(); + existingValue.Should().Be(value); } private ConcurrentWeakDictionary Setup(string key) @@ -69,6 +69,6 @@ public void Add_Then_Collect_And_Check_That_Key_Is_Removed_Test() GC.WaitForPendingFinalizers(); // Check that the value for the key no longer exists. - dictionary.TryGetValue(key, out var result).ShouldBeFalse(); + dictionary.TryGetValue(key, out var result).Should().BeFalse(); } } diff --git a/backend/Testing/LexCore/Utils/GqlUtils.cs b/backend/Testing/LexCore/Utils/GqlUtils.cs index b67c4597a..de2c0eb73 100644 --- a/backend/Testing/LexCore/Utils/GqlUtils.cs +++ b/backend/Testing/LexCore/Utils/GqlUtils.cs @@ -1,5 +1,5 @@ using System.Text.Json.Nodes; -using Shouldly; +using FluentAssertions; namespace Testing.LexCore.Utils; @@ -9,13 +9,13 @@ public static void ValidateGqlErrors(JsonObject json, bool expectError = false) { if (!expectError) { - json["errors"].ShouldBeNull(); + json!["errors"]?.Should().BeNull(); if (json["data"] is JsonObject data) { foreach (var (_, resultValue) in data) { if (resultValue is JsonObject resultObject) - resultObject["errors"].ShouldBeNull(); + resultObject["errors"]?.Should().BeNull(); } } } @@ -37,7 +37,7 @@ public static void ValidateGqlErrors(JsonObject json, bool expectError = false) } } } - foundError.ShouldBeTrue(); + foundError.Should().BeTrue(); } } } diff --git a/backend/Testing/Services/CleanupResetProjectsTests.cs b/backend/Testing/Services/CleanupResetProjectsTests.cs index 6946903d5..431d2b1de 100644 --- a/backend/Testing/Services/CleanupResetProjectsTests.cs +++ b/backend/Testing/Services/CleanupResetProjectsTests.cs @@ -1,6 +1,6 @@ using LexBoxApi.Services; using LexCore.Utils; -using Shouldly; +using FluentAssertions; namespace Testing.Services; @@ -12,8 +12,8 @@ public void ResetRegexCanFindTimestampFromResetRepoName() var date = DateTimeOffset.UtcNow; var repoName = HgService.DeletedRepoName("test", HgService.ResetSoftDeleteSuffix(date)); var match = HgService.ResetProjectsRegex().Match(repoName); - match.Success.ShouldBeTrue(); - match.Groups[1].Value.ShouldBe(FileUtils.ToTimestamp(date)); + match.Success.Should().BeTrue(); + match.Groups[1].Value.Should().Be(FileUtils.ToTimestamp(date)); } [Fact] @@ -22,8 +22,8 @@ public void CanGetDateFromResetRepoName() var expected = DateTimeOffset.Now; var repoName = HgService.DeletedRepoName("test", HgService.ResetSoftDeleteSuffix(expected)); var actual = HgService.GetResetDate(repoName); - actual.ShouldNotBeNull(); - TruncateToMinutes(actual.Value).ShouldBe(TruncateToMinutes(expected)); + actual.Should().NotBeNull(); + TruncateToMinutes(actual!.Value).Should().Be(TruncateToMinutes(expected)); } private DateTimeOffset TruncateToMinutes(DateTimeOffset date) @@ -36,8 +36,8 @@ private DateTimeOffset TruncateToMinutes(DateTimeOffset date) public void ResetRegexCanFindTimestamp(string repoName, string timestamp) { var match = HgService.ResetProjectsRegex().Match(repoName); - match.Success.ShouldBeTrue(); - match.Groups[1].Value.ShouldBe(timestamp); + match.Success.Should().BeTrue(); + match.Groups[1].Value.Should().Be(timestamp); } [Theory] @@ -47,7 +47,7 @@ public void ResetRegexCanFindTimestamp(string repoName, string timestamp) public void ResetRegexDoesNotMatchNonResets(string repoName) { var match = HgService.ResetProjectsRegex().Match(repoName); - match.Success.ShouldBeFalse(); + match.Success.Should().BeFalse(); } } diff --git a/backend/Testing/Services/IsLanguageForgeProjectDataLoaderTests.cs b/backend/Testing/Services/IsLanguageForgeProjectDataLoaderTests.cs index 8f48a0929..3d16d6902 100644 --- a/backend/Testing/Services/IsLanguageForgeProjectDataLoaderTests.cs +++ b/backend/Testing/Services/IsLanguageForgeProjectDataLoaderTests.cs @@ -1,7 +1,7 @@ using LexBoxApi.GraphQL.CustomTypes; using Microsoft.Extensions.Time.Testing; using Polly; -using Shouldly; +using FluentAssertions; namespace Testing.Services; @@ -36,14 +36,14 @@ private ValueTask>> Execute(Exception? exceptio private void VerifyEmptyResult(Outcome> result) { - result.Exception.ShouldBeNull(); - result.Result.ShouldBe(new Dictionary() { { "test", false } }); + result.Exception.Should().BeNull(); + result.Result.Should().BeEquivalentTo(new Dictionary() { { "test", false } }); } private void VerifySuccessResult(Outcome> result) { - result.Exception.ShouldBeNull(); - result.Result.ShouldBe(new Dictionary() { { "test", true } }); + result.Exception.Should().BeNull(); + result.Result.Should().BeEquivalentTo(new Dictionary() { { "test", true } }); } [Fact] diff --git a/backend/Testing/Services/JwtHelper.cs b/backend/Testing/Services/JwtHelper.cs index af6a8172a..5f839c832 100644 --- a/backend/Testing/Services/JwtHelper.cs +++ b/backend/Testing/Services/JwtHelper.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Http.Resilience; using Mono.Unix.Native; using Polly; -using Shouldly; +using FluentAssertions; using Testing.ApiTests; namespace Testing.Services; @@ -58,7 +58,7 @@ public static async Task ExecuteLogin(SendReceiveAuth auth, public static string GetJwtFromLoginResponse(HttpResponseMessage response) { TryGetJwtFromLoginResponse(response, out var jwt); - jwt.ShouldNotBeNullOrEmpty(); + jwt.Should().NotBeNullOrEmpty(); return jwt; } diff --git a/backend/Testing/Services/SendReceiveService.cs b/backend/Testing/Services/SendReceiveService.cs index aabf4ea7f..778f0369b 100644 --- a/backend/Testing/Services/SendReceiveService.cs +++ b/backend/Testing/Services/SendReceiveService.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using Chorus; using Nini.Ini; -using Shouldly; +using FluentAssertions; using SIL.Progress; using Testing.Logging; using Xunit.Abstractions; @@ -86,19 +86,19 @@ public string RunCloneSendReceive(SendReceiveParams sendReceiveParams, SendRecei // Clone var cloneResult = CloneProject(sendReceiveParams, auth); - Directory.Exists(projectDir).ShouldBeTrue($"Directory {projectDir} not found. Clone response: {cloneResult}"); - Directory.EnumerateFiles(projectDir).ShouldContain(fwDataFile); + Directory.Exists(projectDir).Should().BeTrue($"Directory {projectDir} not found. Clone response: {cloneResult}"); + Directory.EnumerateFiles(projectDir).Should().Contain(fwDataFile); var fwDataFileInfo = new FileInfo(fwDataFile); - fwDataFileInfo.Length.ShouldBeGreaterThan(0); + fwDataFileInfo.Length.Should().BeGreaterThan(0); var fwDataFileOriginalLength = fwDataFileInfo.Length; // SendReceive var srResult = SendReceiveProject(sendReceiveParams, auth); - srResult.ShouldContain("no changes from others"); + srResult.Should().Contain("No changes from others"); fwDataFileInfo.Refresh(); - fwDataFileInfo.Exists.ShouldBeTrue(); - fwDataFileInfo.Length.ShouldBe(fwDataFileOriginalLength); + fwDataFileInfo.Exists.Should().BeTrue(); + fwDataFileInfo.Length.Should().Be(fwDataFileOriginalLength); return $"Clone: {cloneResult}{Environment.NewLine}SendReceive: {srResult}"; } diff --git a/backend/Testing/Services/Utils.cs b/backend/Testing/Services/Utils.cs index dfea6d1bd..d70df9fbb 100644 --- a/backend/Testing/Services/Utils.cs +++ b/backend/Testing/Services/Utils.cs @@ -2,7 +2,7 @@ using System.Text.RegularExpressions; using LexCore.Entities; using Quartz.Util; -using Shouldly; +using FluentAssertions; using Testing.ApiTests; using static Testing.Services.Constants; @@ -111,9 +111,9 @@ ... on InvalidEmailError { public static void ValidateSendReceiveOutput(string srOutput) { - srOutput.ShouldNotContain("abort"); - srOutput.ShouldNotContain("failure"); - srOutput.ShouldNotContain("error"); + srOutput.Should().NotContain("abort"); + srOutput.Should().NotContain("failure"); + srOutput.Should().NotContain("error"); } public static string ToProjectCodeFriendlyString(string name) @@ -137,7 +137,7 @@ private static string GetNewProjectDir(string projectCode, var randomIndexedId = $"{_folderIndex++}-{Guid.NewGuid().ToString().Split("-")[0]}"; //fwdata file containing folder name will be the same as the file name projectDir = Path.Join(projectDir, randomIndexedId, projectCode); - projectDir.Length.ShouldBeLessThan(150, $"Path may be too long with mercurial directories {projectDir}"); + projectDir.Length.Should().BeLessThan(150, $"Path may be too long with mercurial directories {projectDir}"); return projectDir; } } diff --git a/backend/Testing/Services/UtilsTests.cs b/backend/Testing/Services/UtilsTests.cs index 0d52a5875..81801b647 100644 --- a/backend/Testing/Services/UtilsTests.cs +++ b/backend/Testing/Services/UtilsTests.cs @@ -1,4 +1,4 @@ -using Shouldly; +using FluentAssertions; using Testing.SyncReverseProxy; namespace Testing.Services; @@ -14,6 +14,6 @@ public class UtilsTests [InlineData("SimultaneousResetsDontResultIn404S", "simultaneous-resets-dont-result-in-404-s")] public void VerifyToProjectCodeFriendlyString(string input, string expected) { - Utils.ToProjectCodeFriendlyString(input).ShouldBe(expected); + Utils.ToProjectCodeFriendlyString(input).Should().Be(expected); } } diff --git a/backend/Testing/SyncReverseProxy/LegacyProjectApiTests.cs b/backend/Testing/SyncReverseProxy/LegacyProjectApiTests.cs index 5a3b47712..ab48a04d8 100644 --- a/backend/Testing/SyncReverseProxy/LegacyProjectApiTests.cs +++ b/backend/Testing/SyncReverseProxy/LegacyProjectApiTests.cs @@ -1,9 +1,9 @@ using System.Net; using System.Net.Http.Json; -using System.Text; using System.Text.Json; using System.Text.Json.Nodes; -using Shouldly; +using FluentAssertions; +using FluentAssertions.Execution; using Testing.ApiTests; using Testing.Services; @@ -28,27 +28,28 @@ public class LegacyProjectApiTests private async Task ValidateResponse(HttpResponseMessage response) { - response.StatusCode.ShouldBe(HttpStatusCode.OK); + response.StatusCode.Should().Be(HttpStatusCode.OK); var content = await response.Content.ReadFromJsonAsync(); - content.ValueKind.ShouldBe(JsonValueKind.Array); + content.ValueKind.Should().Be(JsonValueKind.Array); var projectArray = JsonArray.Create(content); - projectArray.ShouldNotBeNull(); - projectArray.Count.ShouldBeGreaterThan(0); + projectArray.Should().NotBeNull(); + projectArray.Count.Should().BeGreaterThan(0); var project = projectArray.First(p => p?["identifier"]?.GetValue() == TestingEnvironmentVariables.ProjectCode) as JsonObject; - project.ShouldNotBeNull(); + project.Should().NotBeNull(); var projectDict = new Dictionary(project); - projectDict.ShouldSatisfyAllConditions( - () => projectDict.ShouldContainKey("identifier"), - () => projectDict.ShouldContainKey("name"), - () => projectDict.ShouldContainKey("repository"), - () => projectDict.ShouldContainKey("role") - ); - project["identifier"]!.GetValue().ShouldBe(TestingEnvironmentVariables.ProjectCode); - project["name"]!.GetValue().ShouldBe("Sena 3"); - project["repository"]!.GetValue().ShouldBe("http://public.languagedepot.org"); + using (new AssertionScope()) + { + projectDict.Should().ContainKey("identifier"); + projectDict.Should().ContainKey("name"); + projectDict.Should().ContainKey("repository"); + projectDict.Should().ContainKey("role"); + } + project["identifier"]!.GetValue().Should().Be(TestingEnvironmentVariables.ProjectCode); + project["name"]!.GetValue().Should().Be("Sena 3"); + project["repository"]!.GetValue().Should().Be("http://public.languagedepot.org"); //todo what is role for? returns unknown in my single test - project["role"]!.GetValue().ShouldNotBeEmpty(); + project["role"]!.GetValue().Should().NotBeEmpty(); } [Fact] @@ -96,13 +97,13 @@ public async Task TestInvalidPassword() $"{_baseUrl}/api/user/{TestData.User}/projects", new FormUrlEncodedContent( new[] { new KeyValuePair("password", "bad password") })); - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); var content = await response.Content.ReadFromJsonAsync(); - content.ValueKind.ShouldBe(JsonValueKind.Object); + content.ValueKind.Should().Be(JsonValueKind.Object); var responseObject = JsonObject.Create(content); - responseObject.ShouldNotBeNull(); - responseObject.ShouldContainKey("error"); - responseObject["error"]!.GetValue().ShouldBe("Bad password"); + responseObject.Should().NotBeNull(); + responseObject.Should().ContainKey("error"); + responseObject["error"]!.GetValue().Should().Be("Bad password"); } [Fact] @@ -112,13 +113,13 @@ public async Task TestInvalidUser() $"{_baseUrl}/api/user/not-a-real-user-account/projects", new FormUrlEncodedContent( new[] { new KeyValuePair("password", "doesn't matter") })); - response.StatusCode.ShouldBe(HttpStatusCode.NotFound); + response.StatusCode.Should().Be(HttpStatusCode.NotFound); var content = await response.Content.ReadFromJsonAsync(); - content.ValueKind.ShouldBe(JsonValueKind.Object); + content.ValueKind.Should().Be(JsonValueKind.Object); var responseObject = JsonObject.Create(content); - responseObject.ShouldNotBeNull(); - responseObject.ShouldContainKey("error"); - responseObject["error"]!.GetValue().ShouldBe("Unknown user"); + responseObject.Should().NotBeNull(); + responseObject.Should().ContainKey("error"); + responseObject["error"]!.GetValue().Should().Be("Unknown user"); } // LF sends lots of requests with no password/request body. Chorus might as well. @@ -127,6 +128,6 @@ public async Task TestInvalidUser() public async Task MissingPasswordReturns403() { var response = await Client.PostAsJsonAsync($"{_baseUrl}/api/user/{TestData.User}/projects", null); - response.StatusCode.ShouldBe(HttpStatusCode.Forbidden); + response.StatusCode.Should().Be(HttpStatusCode.Forbidden); } } diff --git a/backend/Testing/SyncReverseProxy/ProxyHgRequestTests.cs b/backend/Testing/SyncReverseProxy/ProxyHgRequestTests.cs index d5afda11b..7f808c870 100644 --- a/backend/Testing/SyncReverseProxy/ProxyHgRequestTests.cs +++ b/backend/Testing/SyncReverseProxy/ProxyHgRequestTests.cs @@ -3,7 +3,7 @@ using System.Net.Http.Json; using System.Text; using LexBoxApi.Auth; -using Shouldly; +using FluentAssertions; using Testing.ApiTests; using Testing.Services; @@ -18,7 +18,7 @@ public class ProxyHgRequests private void ShouldBeValidResponse(HttpResponseMessage responseMessage) { //the Basic realm part is required by the HG client, otherwise it won't request again with a basic auth header - responseMessage.Headers.WwwAuthenticate.ToString().ShouldContain("Basic realm=\""); + responseMessage.Headers.WwwAuthenticate.ToString().Should().Contain("Basic realm=\""); } [Theory] @@ -35,7 +35,7 @@ public async Task TestGet(string user) Convert.ToBase64String(Encoding.ASCII.GetBytes($"{user}:{TestData.Password}"))) } }); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.OK); + responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] @@ -50,7 +50,7 @@ public async Task TestGetPrefixHg() Convert.ToBase64String(Encoding.ASCII.GetBytes($"{TestData.User}:{TestData.Password}"))) } }); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.OK); + responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); } [Theory] @@ -59,7 +59,7 @@ public async Task TestGetPrefixHg() public async Task TestGetWithJwtInBasicAuth(string user) { var jwt = await JwtHelper.GetJwtForUser(new(user, TestData.Password)); - jwt.ShouldNotBeNullOrEmpty(); + jwt.Should().NotBeNullOrEmpty(); var responseMessage = await Client.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}/{TestingEnvironmentVariables.ProjectCode}") @@ -69,7 +69,7 @@ public async Task TestGetWithJwtInBasicAuth(string user) Authorization = new ("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"bearer:{jwt}"))) } }); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.OK); + responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] @@ -85,7 +85,7 @@ public async Task TestGetBadPassword() Convert.ToBase64String(Encoding.ASCII.GetBytes($"{TestData.User}:{password}"))) } }); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); ShouldBeValidResponse(responseMessage); } @@ -95,7 +95,7 @@ public async Task TestNoAuthResponse() var responseMessage = await Client.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}/{TestingEnvironmentVariables.ProjectCode}")); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); ShouldBeValidResponse(responseMessage); } @@ -112,9 +112,9 @@ public async Task SimpleClone() }; batchRequest.Headers.Add("x-hgarg-1", "cmds=heads+%3Bknown+nodes%3D"); var batchResponse = await Client.SendAsync(batchRequest); - batchResponse.StatusCode.ShouldBe(HttpStatusCode.OK); + batchResponse.StatusCode.Should().Be(HttpStatusCode.OK); var batchBody = await batchResponse.Content.ReadAsStringAsync(); - batchBody.ShouldEndWith(";"); + batchBody.Should().EndWith(";"); var heads = batchBody.Split('\n')[^2]; var getBundleRequest = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}/{projectCode}?cmd=getbundle") diff --git a/backend/Testing/SyncReverseProxy/ResumableTests.cs b/backend/Testing/SyncReverseProxy/ResumableTests.cs index 3c121f5a4..32593b1f9 100644 --- a/backend/Testing/SyncReverseProxy/ResumableTests.cs +++ b/backend/Testing/SyncReverseProxy/ResumableTests.cs @@ -2,7 +2,7 @@ using System.Net; using System.Net.Http.Headers; using System.Text; -using Shouldly; +using FluentAssertions; using Testing.ApiTests; using Testing.Services; @@ -28,10 +28,10 @@ public async Task IsAvailable(string user) } }, HttpCompletionOption.ResponseHeadersRead); var responseString = await responseMessage.Content.ReadAsStringAsync(); - responseString.ShouldBeNullOrEmpty(); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.OK); + responseString.Should().BeNullOrEmpty(); + responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); var headers = responseMessage.Headers.ToDictionary(kvp => kvp.Key, kvp => string.Join(',', kvp.Value), StringComparer.OrdinalIgnoreCase); - headers.ShouldContainKeyAndValue("X-HgR-Version", "3"); + headers.Should().Contain("X-HgR-Version", "3"); } [Theory] @@ -40,7 +40,7 @@ public async Task IsAvailable(string user) public async Task IsAvailableJwtInBasicAuth(string user) { var jwt = await JwtHelper.GetJwtForUser(new(user, TestData.Password)); - jwt.ShouldNotBeNullOrEmpty(); + jwt.Should().NotBeNullOrEmpty(); var responseMessage = await Client.SendAsync(new(HttpMethod.Get, $"{_baseUrl}/api/v03/isAvailable?repoId={TestingEnvironmentVariables.ProjectCode}") @@ -51,10 +51,10 @@ public async Task IsAvailableJwtInBasicAuth(string user) } }, HttpCompletionOption.ResponseHeadersRead); var responseString = await responseMessage.Content.ReadAsStringAsync(); - responseString.ShouldBeNullOrEmpty(); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.OK); + responseString.Should().BeNullOrEmpty(); + responseMessage.StatusCode.Should().Be(HttpStatusCode.OK); var headers = responseMessage.Headers.ToDictionary(kvp => kvp.Key, kvp => string.Join(',', kvp.Value), StringComparer.OrdinalIgnoreCase); - headers.ShouldContainKeyAndValue("X-HgR-Version", "3"); + headers.Should().Contain("X-HgR-Version", "3"); } [Fact] @@ -69,7 +69,7 @@ public async Task WithBadUser() Convert.ToBase64String(Encoding.ASCII.GetBytes($"not a user:doesnt matter"))) } }); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] @@ -84,7 +84,7 @@ public async Task WithBadPassword() Convert.ToBase64String(Encoding.ASCII.GetBytes($"{TestData.User}:wrong password"))) } }); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] @@ -99,7 +99,7 @@ public async Task WithBadNotValidProject() Convert.ToBase64String(Encoding.ASCII.GetBytes($"{TestData.User}:{TestData.Password}"))) } }); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } [Fact] @@ -115,6 +115,6 @@ public async Task WithUnauthorizedUser() Convert.ToBase64String(Encoding.ASCII.GetBytes($"{userWithoutPermission}:{TestData.Password}"))) } }); - responseMessage.StatusCode.ShouldBe(HttpStatusCode.Unauthorized); + responseMessage.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } } diff --git a/backend/Testing/SyncReverseProxy/SendReceiveServiceTests.cs b/backend/Testing/SyncReverseProxy/SendReceiveServiceTests.cs index be5dd69d3..8d6edc151 100644 --- a/backend/Testing/SyncReverseProxy/SendReceiveServiceTests.cs +++ b/backend/Testing/SyncReverseProxy/SendReceiveServiceTests.cs @@ -1,6 +1,6 @@ using Chorus.VcsDrivers.Mercurial; using LexBoxApi.Auth; -using Shouldly; +using FluentAssertions; using SIL.Progress; using System.Net.Http.Json; using System.Text.Json.Nodes; @@ -35,10 +35,10 @@ public SendReceiveServiceTests(ITestOutputHelper output, IntegrationFixture send public async Task VerifyHgWorking() { var version = await _sendReceiveService.GetHgVersion(); - version.ShouldStartWith("Mercurial Distributed SCM"); + version.Should().StartWith("Mercurial Distributed SCM"); _output.WriteLine("Hg version: " + version); HgRunner.Run("hg version", Environment.CurrentDirectory, 5, new XunitStringBuilderProgress(_output) { ShowVerbose = true }); - HgRepository.GetEnvironmentReadinessMessage("en").ShouldBeNull(); + HgRepository.GetEnvironmentReadinessMessage("en").Should().BeNull(); } [Theory] @@ -65,7 +65,7 @@ public async Task CloneConfidentialProjectAsOrgManager(HgProtocol protocol) // Verify pushed var lastCommitDate = await _adminApiTester.GetProjectLastCommit(projectConfig.Code); - lastCommitDate.ShouldNotBeNullOrEmpty(); + lastCommitDate.Should().NotBeNull(); } [Theory] @@ -105,11 +105,11 @@ public async Task ModifyProjectData(HgProtocol protocol) // Verify pushed and store last commit var lastCommitDate = await _adminApiTester.GetProjectLastCommit(projectConfig.Code); - lastCommitDate.ShouldNotBeNullOrEmpty(); + lastCommitDate.Should().NotBeNull(); // Modify var fwDataFileInfo = new FileInfo(sendReceiveParams.FwDataFile); - fwDataFileInfo.Length.ShouldBeGreaterThan(0); + fwDataFileInfo.Length.Should().BeGreaterThan(0); ModifyProjectHelper.ModifyProject(sendReceiveParams.FwDataFile); // Push changes @@ -117,7 +117,7 @@ public async Task ModifyProjectData(HgProtocol protocol) // Verify the push updated the last commit date var lastCommitDateAfter = await _adminApiTester.GetProjectLastCommit(projectConfig.Code); - lastCommitDateAfter.ShouldBeGreaterThan(lastCommitDate); + lastCommitDateAfter.Should().BeAfter(lastCommitDate.Value); } [Theory] @@ -137,7 +137,7 @@ public async Task SendReceiveAfterProjectReset(HgProtocol protocol) var response = await _adminApiTester.HttpClient.GetAsync(tipUri); var jsonResult = await response.Content.ReadFromJsonAsync(); var originalTip = jsonResult?["node"]?.AsValue()?.ToString(); - originalTip.ShouldNotBeNull(); + originalTip.Should().NotBeNull(); // /api/project/resetProject/{code} // /api/project/finishResetProject/{code} // leave project empty @@ -152,9 +152,8 @@ public async Task SendReceiveAfterProjectReset(HgProtocol protocol) response = await _adminApiTester.HttpClient.GetAsync(tipUri); jsonResult = await response.Content.ReadFromJsonAsync(); var emptyTip = jsonResult?["node"]?.AsValue()?.ToString(); - emptyTip.ShouldNotBeNull(); - emptyTip.ShouldNotBeEmpty(); - emptyTip.Replace("0", "").ShouldBeEmpty(); + emptyTip.Should().NotBeNullOrEmpty(); + emptyTip.Replace("0", "").Should().BeEmpty(); // Step 3: do Send/Receive if (protocol == HgProtocol.Resumable) @@ -175,8 +174,8 @@ public async Task SendReceiveAfterProjectReset(HgProtocol protocol) response = await _adminApiTester.HttpClient.GetAsync(tipUri); jsonResult = await response.Content.ReadFromJsonAsync(); var postSRTip = jsonResult?["node"]?.AsValue()?.ToString(); - postSRTip.ShouldNotBeNull(); - postSRTip.ShouldBe(originalTip); + postSRTip.Should().NotBeNull(); + postSRTip.Should().Be(originalTip); } [Fact] @@ -207,7 +206,7 @@ private async Task SendNewProject(int totalSizeMb, int fileCount) var fileName = $"test-file{i}.bin"; WriteFile(Path.Combine(sendReceiveParams.Dir, fileName), totalSizeMb / fileCount); HgRunner.Run($"hg add {fileName}", sendReceiveParams.Dir, 5, progress); - HgRunner.Run($"""hg commit -m "large file commit {i}" """, sendReceiveParams.Dir, 5, progress).ExitCode.ShouldBe(0); + HgRunner.Run($"""hg commit -m "large file commit {i}" """, sendReceiveParams.Dir, 5, progress).ExitCode.Should().Be(0); } var srResult = _sendReceiveService.SendReceiveProject(sendReceiveParams, AdminAuth); @@ -235,7 +234,7 @@ public void InvalidPassOnCloneHgWeb() var sendReceiveParams = GetParams(HgProtocol.Hgweb); var act = () => _sendReceiveService.CloneProject(sendReceiveParams, InvalidPass); - act.ShouldThrow(); + act.Should().Throw(); } [Fact] @@ -244,7 +243,7 @@ public void InvalidPassOnCloneHgResumable() var sendReceiveParams = GetParams(HgProtocol.Resumable); var act = () => _sendReceiveService.CloneProject(sendReceiveParams, InvalidPass); - act.ShouldThrow(); + act.Should().Throw(); } [Fact] @@ -254,7 +253,7 @@ public void InvalidPassOnSendReceiveHgWeb() _sendReceiveService.CloneProject(sendReceiveParams, ManagerAuth); var act = () => _sendReceiveService.SendReceiveProject(sendReceiveParams, InvalidPass); - act.ShouldThrow(); + act.Should().Throw(); } [Fact] @@ -264,7 +263,7 @@ public void InvalidPassOnSendReceiveHgResumable() _sendReceiveService.CloneProject(sendReceiveParams, ManagerAuth); var act = () => _sendReceiveService.SendReceiveProject(sendReceiveParams, InvalidPass); - act.ShouldThrow(); + act.Should().Throw(); } [Fact] @@ -272,7 +271,7 @@ public void InvalidUserCloneHgWeb() { var sendReceiveParams = GetParams(HgProtocol.Hgweb); var act = () => _sendReceiveService.CloneProject(sendReceiveParams, InvalidUser); - act.ShouldThrow(); + act.Should().Throw(); } [Fact] @@ -280,7 +279,7 @@ public void InvalidUserCloneHgResumable() { var sendReceiveParams = GetParams(HgProtocol.Resumable); var act = () => _sendReceiveService.CloneProject(sendReceiveParams, InvalidUser); - act.ShouldThrow(); + act.Should().Throw(); } [Fact] @@ -289,8 +288,8 @@ public void InvalidProjectAdminLogin() var sendReceiveParams = GetParams(HgProtocol.Hgweb, "non-existent-project"); var act = () => _sendReceiveService.CloneProject(sendReceiveParams, AdminAuth); - act.ShouldThrow(); - Directory.GetFiles(sendReceiveParams.Dir).ShouldBeEmpty(); + act.Should().Throw(); + Directory.GetFiles(sendReceiveParams.Dir).Should().BeEmpty(); } [Fact] @@ -299,8 +298,8 @@ public void InvalidProjectManagerLogin() var sendReceiveParams = GetParams(HgProtocol.Hgweb, "non-existent-project"); var act = () => _sendReceiveService.CloneProject(sendReceiveParams, ManagerAuth); - act.ShouldThrow(); - Directory.GetFiles(sendReceiveParams.Dir).ShouldBeEmpty(); + act.Should().Throw(); + Directory.GetFiles(sendReceiveParams.Dir).Should().BeEmpty(); } [Fact] @@ -309,7 +308,7 @@ public void UnauthorizedUserCloneHgWeb() var sendReceiveParams = GetParams(HgProtocol.Hgweb); var act = () => _sendReceiveService.CloneProject(sendReceiveParams, UnauthorizedUser); - act.ShouldThrow(); + act.Should().Throw(); } [Fact] @@ -318,6 +317,6 @@ public void UnauthorizedUserCloneHgResumable() var sendReceiveParams = GetParams(HgProtocol.Resumable); var act = () => _sendReceiveService.CloneProject(sendReceiveParams, UnauthorizedUser); - act.ShouldThrow(); + act.Should().Throw(); } } diff --git a/backend/Testing/Testing.csproj b/backend/Testing/Testing.csproj index 04fa2ae8c..3ccb8771e 100644 --- a/backend/Testing/Testing.csproj +++ b/backend/Testing/Testing.csproj @@ -36,7 +36,7 @@ - + diff --git a/backend/Testing/Usings.cs b/backend/Testing/Usings.cs index 8c927eb74..c802f4480 100644 --- a/backend/Testing/Usings.cs +++ b/backend/Testing/Usings.cs @@ -1 +1 @@ -global using Xunit; \ No newline at end of file +global using Xunit; From a598cae11013dfc96fab517199bcb19369b1ce3a Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Tue, 26 Nov 2024 03:18:38 +0100 Subject: [PATCH 03/27] Chore/improve e2e tests in gha (#1270) * Trigger Develop API CI/CD on frontend changes * Skip back button test in Firefox * Make playwright task more accessible. --- .github/workflows/develop-api.yaml | 2 ++ frontend/Taskfile.yml | 3 ++- frontend/tests/logout.test.ts | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/develop-api.yaml b/.github/workflows/develop-api.yaml index 04ddb8c84..17253952f 100644 --- a/.github/workflows/develop-api.yaml +++ b/.github/workflows/develop-api.yaml @@ -5,6 +5,7 @@ on: paths: - 'backend/**' - '!backend/FwLite/**' + - 'frontend/**' - '.github/workflows/lexbox-api.yaml' - '.github/workflows/deploy.yaml' - 'deployment/lexbox-deployment.yaml' @@ -14,6 +15,7 @@ on: paths: - 'backend/**' - '!backend/FwLite/**' + - 'frontend/**' - '.github/workflows/lexbox-api.yaml' - '.github/workflows/deploy.yaml' - 'deployment/lexbox-deployment.yaml' diff --git a/frontend/Taskfile.yml b/frontend/Taskfile.yml index 4c37bd215..ead41d753 100644 --- a/frontend/Taskfile.yml +++ b/frontend/Taskfile.yml @@ -21,7 +21,8 @@ tasks: - corepack enable || true - pnpm install playwright-tests: - cmd: pnpm test + aliases: [ pt ] + cmd: pnpm run test {{.CLI_ARGS}} playwright-generate-tests: cmds: diff --git a/frontend/tests/logout.test.ts b/frontend/tests/logout.test.ts index f67c92f22..dd1236a40 100644 --- a/frontend/tests/logout.test.ts +++ b/frontend/tests/logout.test.ts @@ -2,7 +2,8 @@ import {AdminDashboardPage} from './pages/adminDashboardPage'; import {loginAs} from './utils/authHelpers'; import {test} from './fixtures'; -test('Back button after logout redirects back to login page', async ({page}) => { +test('Back button after logout redirects back to login page', async ({page, browserName}) => { + test.skip(browserName === 'firefox', 'Support for Clear-Site-Data: "cache" was removed and is WIP (https://bugzilla.mozilla.org/show_bug.cgi?id=1838506)'); await loginAs(page.request, 'admin'); const adminPage = await new AdminDashboardPage(page).goto(); const drawer = await adminPage.openDrawer(); From 7eaf4f88fecbf78452e735b968b96e5e922a693e Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 26 Nov 2024 13:02:39 +0700 Subject: [PATCH 04/27] User typeahead enabled for non-admin project managers (#1237) Add a new usersICanSee GQL query, which looks for: * All users in one of my orgs * All users in one of the projects I manage * All users in one of the non-confidential projects I'm a member of Note that projects with `isConfidential = null` are treated as public (non-confidential) by this query. The "Add Project Member" typeahead is now updated to use that query, which allows project managers who aren't site admins to use it to find users whose email address they don't know. E.g. if Test Manager has Test Editor as part of project A, and he also manages project B, then when he clicks on "Add Members" in project B, he can type Test Editor's name and select him to add, without knowing his email address. --------- Co-authored-by: Tim Haasdyk --- backend/LexBoxApi/GraphQL/LexQueries.cs | 9 + .../Services/DevGqlSchemaWriterService.cs | 1 + backend/LexBoxApi/Services/UserService.cs | 18 +- .../ApiTests/UsersICanSeeQueryTests.cs | 98 +++++++ .../Fixtures/TempProjectWithoutRepo.cs | 39 +++ .../LexCore/Services/UserServiceTest.cs | 262 ++++++++++++++++++ backend/Testing/Testing.csproj | 1 + frontend/schema.graphql | 16 +- frontend/src/lib/forms/UserTypeahead.svelte | 17 +- frontend/src/lib/gql/typeahead-queries.ts | 18 +- .../org/[org_id]/AddOrgMemberModal.svelte | 5 +- .../project/[project_code]/+page.svelte | 2 +- .../[project_code]/AddProjectMember.svelte | 7 +- 13 files changed, 468 insertions(+), 25 deletions(-) create mode 100644 backend/Testing/ApiTests/UsersICanSeeQueryTests.cs create mode 100644 backend/Testing/Fixtures/TempProjectWithoutRepo.cs create mode 100644 backend/Testing/LexCore/Services/UserServiceTest.cs diff --git a/backend/LexBoxApi/GraphQL/LexQueries.cs b/backend/LexBoxApi/GraphQL/LexQueries.cs index f2326d2ef..72d947436 100644 --- a/backend/LexBoxApi/GraphQL/LexQueries.cs +++ b/backend/LexBoxApi/GraphQL/LexQueries.cs @@ -187,6 +187,15 @@ public IQueryable UsersInMyOrg(LexBoxDbContext context, LoggedInContext lo return context.Users.Where(u => u.Organizations.Any(orgMember => myOrgIds.Contains(orgMember.OrgId))); } + [UseOffsetPaging] + [UseProjection] + [UseFiltering] + [UseSorting] + public IQueryable UsersICanSee(UserService userService, LoggedInContext loggedInContext) + { + return userService.UserQueryForTypeahead(loggedInContext.User); + } + [UseProjection] [GraphQLType] public async Task OrgById(LexBoxDbContext dbContext, diff --git a/backend/LexBoxApi/Services/DevGqlSchemaWriterService.cs b/backend/LexBoxApi/Services/DevGqlSchemaWriterService.cs index a40a93c3f..c0b77997e 100644 --- a/backend/LexBoxApi/Services/DevGqlSchemaWriterService.cs +++ b/backend/LexBoxApi/Services/DevGqlSchemaWriterService.cs @@ -33,6 +33,7 @@ public static async Task GenerateGqlSchema(string[] args) .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() .AddLexGraphQL(builder.Environment, true); var host = builder.Build(); diff --git a/backend/LexBoxApi/Services/UserService.cs b/backend/LexBoxApi/Services/UserService.cs index 277eba5a1..dc3f3a29f 100644 --- a/backend/LexBoxApi/Services/UserService.cs +++ b/backend/LexBoxApi/Services/UserService.cs @@ -1,13 +1,14 @@ using System.Net.Mail; -using LexBoxApi.Auth; using LexBoxApi.Services.Email; +using LexCore.Auth; +using LexCore.Entities; using LexCore.Exceptions; using LexData; using Microsoft.EntityFrameworkCore; namespace LexBoxApi.Services; -public class UserService(LexBoxDbContext dbContext, IEmailService emailService, LexAuthService lexAuthService) +public class UserService(LexBoxDbContext dbContext, IEmailService emailService) { public async Task ForgotPassword(string email) { @@ -83,4 +84,17 @@ public static (string name, string? email, string? username) ExtractNameAndAddre } return (name, email, username); } + + public IQueryable UserQueryForTypeahead(LexAuthUser user) + { + var myOrgIds = user.Orgs.Select(o => o.OrgId).ToList(); + var myProjectIds = user.Projects.Select(p => p.ProjectId).ToList(); + var myManagedProjectIds = user.Projects.Where(p => p.Role == ProjectRole.Manager).Select(p => p.ProjectId).ToList(); + return dbContext.Users.Where(u => + u.Id == user.Id || + u.Organizations.Any(orgMember => myOrgIds.Contains(orgMember.OrgId)) || + u.Projects.Any(projMember => + myManagedProjectIds.Contains(projMember.ProjectId) || + (projMember.Project != null && projMember.Project.IsConfidential != true && myProjectIds.Contains(projMember.ProjectId)))); + } } diff --git a/backend/Testing/ApiTests/UsersICanSeeQueryTests.cs b/backend/Testing/ApiTests/UsersICanSeeQueryTests.cs new file mode 100644 index 000000000..5c23843fa --- /dev/null +++ b/backend/Testing/ApiTests/UsersICanSeeQueryTests.cs @@ -0,0 +1,98 @@ +using System.Text.Json.Nodes; +using Shouldly; +using Testing.Services; + +namespace Testing.ApiTests; + +[Trait("Category", "Integration")] +public class UsersICanSeeQueryTests : ApiTestBase +{ + private async Task QueryUsersICanSee(bool expectGqlError = false) + { + var json = await ExecuteGql( + $$""" + query { + usersICanSee(take: 10) { + totalCount + items { + id + name + } + } + } + """, + expectGqlError, expectSuccessCode: false); + return json; + } + + private async Task AddUserToProject(Guid projectId, string username) + { + await ExecuteGql( + $$""" + mutation { + addProjectMember(input: { + projectId: "{{projectId}}", + usernameOrEmail: "{{username}}", + role: EDITOR, + canInvite: false + }) { + project { + id + } + errors { + __typename + ... on Error { + message + } + } + } + } + """); + } + + private JsonArray GetUsers(JsonObject json) + { + var users = json["data"]!["usersICanSee"]!["items"]!.AsArray(); + users.ShouldNotBeNull(); + return users; + } + + private void MustHaveUser(JsonArray users, string userName) + { + users.ShouldNotBeNull().ShouldNotBeEmpty(); + users.ShouldContain(node => node!["name"]!.GetValue() == userName, + "user list " + users.ToJsonString()); + } + + private void MustNotHaveUser(JsonArray users, string userName) + { + users.ShouldNotBeNull().ShouldNotBeEmpty(); + users.ShouldNotContain(node => node!["name"]!.GetValue() == userName, + "user list " + users.ToJsonString()); + } + + [Fact] + public async Task ManagerCanSeeProjectMembersOfAllProjects() + { + await LoginAs("manager"); + await using var project = await this.RegisterProjectInLexBox(Utils.GetNewProjectConfig(isConfidential: true)); + //refresh jwt + await LoginAs("manager"); + await AddUserToProject(project.Id, "qa@test.com"); + var json = GetUsers(await QueryUsersICanSee()); + MustHaveUser(json, "Qa Admin"); + } + + [Fact] + public async Task MemberCanSeeNotProjectMembersOfConfidentialProjects() + { + await LoginAs("manager"); + await using var project = await this.RegisterProjectInLexBox(Utils.GetNewProjectConfig(isConfidential: true)); + //refresh jwt + await LoginAs("manager"); + await AddUserToProject(project.Id, "qa@test.com"); + await LoginAs("editor"); + var json = GetUsers(await QueryUsersICanSee()); + MustNotHaveUser(json, "Qa Admin"); + } +} diff --git a/backend/Testing/Fixtures/TempProjectWithoutRepo.cs b/backend/Testing/Fixtures/TempProjectWithoutRepo.cs new file mode 100644 index 000000000..81c5aa702 --- /dev/null +++ b/backend/Testing/Fixtures/TempProjectWithoutRepo.cs @@ -0,0 +1,39 @@ +using LexCore.Entities; +using LexData; +using Testing.Services; + +namespace Testing.Fixtures; + +public class TempProjectWithoutRepo(LexBoxDbContext dbContext, Project project) : IAsyncDisposable +{ + public Project Project => project; + public static async Task Create(LexBoxDbContext dbContext, bool isConfidential = false, Guid? managerId = null) + { + var config = Utils.GetNewProjectConfig(isConfidential: isConfidential); + var project = new Project + { + Name = config.Name, + Code = config.Code, + IsConfidential = config.IsConfidential, + LastCommit = null, + Organizations = [], + Users = [], + RetentionPolicy = RetentionPolicy.Test, + Type = ProjectType.FLEx, + Id = config.Id, + }; + if (managerId is Guid id) + { + project.Users.Add(new ProjectUsers { ProjectId = project.Id, UserId = id, Role = ProjectRole.Manager }); + } + dbContext.Add(project); + await dbContext.SaveChangesAsync(); + return new TempProjectWithoutRepo(dbContext, project); + } + + public async ValueTask DisposeAsync() + { + dbContext.Remove(project); + await dbContext.SaveChangesAsync(); + } +} diff --git a/backend/Testing/LexCore/Services/UserServiceTest.cs b/backend/Testing/LexCore/Services/UserServiceTest.cs new file mode 100644 index 000000000..16028cd9c --- /dev/null +++ b/backend/Testing/LexCore/Services/UserServiceTest.cs @@ -0,0 +1,262 @@ +using LexBoxApi.Services; +using LexBoxApi.Services.Email; +using LexCore.Auth; +using LexCore.Entities; +using LexData; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Testing.Fixtures; +using FluentAssertions; + +namespace Testing.LexCore.Services; + +[Collection(nameof(TestingServicesFixture))] +public class UserServiceTest : IAsyncLifetime +{ + private readonly UserService _userService; + + private readonly LexBoxDbContext _lexBoxDbContext; + private List ManagedProjects { get; } = []; + private List ManagedUsers { get; } = []; + private List ManagedOrgs { get; } = []; + + // Users created for this test + private User? Robin { get; set; } + private User? John { get; set; } + private User? Alan { get; set; } + private User? Marian { get; set; } + private User? Bishop { get; set; } + private User? Tuck { get; set; } + private User? Sheriff { get; set; } + private User? Guy { get; set; } + // Projects created for this test + private Project? Sherwood { get; set; } + private Project? Nottingham { get; set; } + // Orgs created for this test + private Organization? Outlaws { get; set; } + private Organization? LawEnforcement { get; set; } + private Organization? Church { get; set; } + + public UserServiceTest(TestingServicesFixture testing) + { + var serviceProvider = testing.ConfigureServices(s => + { + s.AddScoped(_ => Mock.Of()); + s.AddScoped(); + }); + _userService = serviceProvider.GetRequiredService(); + _lexBoxDbContext = serviceProvider.GetRequiredService(); + } + + public Task InitializeAsync() + { + Robin = CreateUser("Robin Hood"); + John = CreateUser("Little John"); + Alan = CreateUser("Alan a Dale"); + Marian = CreateUser("Maid Marian"); + Bishop = CreateUser("Bishop of Hereford"); + Tuck = CreateUser("Friar Tuck"); + Sheriff = CreateUser("Sheriff of Nottingham"); + Guy = CreateUser("Guy of Gisbourne"); + + Nottingham = CreateProject([Sheriff.Id], [Marian.Id, Tuck.Id]); + Sherwood = CreateConfidentialProject([Robin.Id, Marian.Id], [John.Id, Alan.Id, Tuck.Id]); + + Outlaws = CreateOrg([Robin.Id], [John.Id]); // Alan a Dale should *NOT* be in this org + LawEnforcement = CreateOrg([Sheriff.Id], [Guy.Id]); + Church = CreateOrg([Bishop.Id], [Tuck.Id]); + + return _lexBoxDbContext.SaveChangesAsync(); + } + + public Task DisposeAsync() + { + foreach (var project in ManagedProjects) + { + _lexBoxDbContext.Remove(project); + } + foreach (var user in ManagedUsers) + { + _lexBoxDbContext.Remove(user); + } + foreach (var org in ManagedOrgs) + { + _lexBoxDbContext.Remove(org); + } + return _lexBoxDbContext.SaveChangesAsync(); + } + + public void UserListShouldBe(IEnumerable actual, IEnumerable expected) + { + var actualNames = actual.Select(u => u.Name); + var expectedNames = expected.Select(u => u?.Name ?? ""); + actualNames.Should().BeEquivalentTo(expectedNames, options => options.WithoutStrictOrdering()); + } + + [Fact] + public async Task ManagerCanSeeAllUsersEvenInConfidentialProjects() + { + // Robin Hood is in Outlaws org (admin) and Sherwood project (private, manager) + var authUser = new LexAuthUser(Robin!); + var users = await _userService.UserQueryForTypeahead(authUser).ToArrayAsync(); + // John, who is in both the Outlaws org (user) and Sherwood project (member) is not duplicated + UserListShouldBe(users, [Robin, Marian, John, Alan, Tuck]); + } + + [Fact] + public async Task NonManagerCanNotSeeUsersInConfidentialProjects() + { + // Little John is in Outlaws org (user) and Sherwood project (private, member) + var authUser = new LexAuthUser(John!); + var users = await _userService.UserQueryForTypeahead(authUser).ToArrayAsync(); + // John can see Robin because he shares an org, but not Marian even though she's a manager of the Sherwood project + UserListShouldBe(users, [Robin, John]); + } + + [Fact] + public async Task ManagerOfOneProjectAndMemberOfAnotherPublicProjectCanSeeUsersInBoth() + { + // Maid Marian is in no orgs and two projects: Sherwood (private, manager) and Nottingham (public, member) + var authUser = new LexAuthUser(Marian!); + var users = await _userService.UserQueryForTypeahead(authUser).ToArrayAsync(); + // Marian can see everyone in both projects; Tuck is not duplicated despite being in both projects + UserListShouldBe(users, [Robin, Marian, John, Alan, Tuck, Sheriff]); + } + + [Fact] + public async Task ManagerOfOneProjectAndMemberOfAnotherConfidentialProjectCanNotSeeUsersInConfidentialProject() + { + // Sheriff of Nottingham is in LawEnforcement org (admin) and Nottingham project (pulbic, manager) + try + { + // Sheriff tries to sneak into Sherwood... + await AddUserToProject(Sherwood!, Sheriff!); + // ... but can still only see the users in Nottingham and LawEnforcement + var authUser = new LexAuthUser(Sheriff!); + var users = await _userService.UserQueryForTypeahead(authUser).ToArrayAsync(); + UserListShouldBe(users, [Sheriff, Guy, Marian, Tuck]); + } + finally + { + await RemoveUserFromProject(Sherwood!, Sheriff!); + } + } + + [Fact] + public async Task OrgAdminsInNoProjectsCanSeeOnlyTheirOrg() + { + // Bishop of Hereford is in Church org (admin) but no projects + var authUser = new LexAuthUser(Bishop!); + var users = await _userService.UserQueryForTypeahead(authUser).ToArrayAsync(); + // Bishop can only see members of Church org + UserListShouldBe(users, [Bishop, Tuck]); + } + + [Fact] + public async Task OrgMembersInNoProjectsCanSeeOnlyTheirOrg() + { + // Guy of Gisborne is in LawEnforcement org (user) but no projects + var authUser = new LexAuthUser(Guy!); + var users = await _userService.UserQueryForTypeahead(authUser).ToArrayAsync(); + // Guy can only see members of LawEnforcement org + UserListShouldBe(users, [Sheriff, Guy]); + } + + [Fact] + public async Task OrgAndProjectMembersCanSeeFellowOrgMembersAndFellowPublicProjectMembersButNotFellowPrivateProjectMembers() + { + // Friar Tuck is in Church org (user) and two projects: Nottingham (public, member) and Sherwood (private, member) + var authUser = new LexAuthUser(Tuck!); + var users = await _userService.UserQueryForTypeahead(authUser).ToArrayAsync(); + // Tuck can see everyone in Church and Nottingham, but nobody in Sherwood because it's private — though he can see Marian because he shares a public project with her + UserListShouldBe(users, [Bishop, Tuck, Sheriff, Marian]); + } + + [Fact] + public async Task MemberOfOnePrivateProjectButNoOrgsCanOnlySeeHimself() + { + // Alan a Dale is in Sherwood project (private, member) but no orgs + var authUser = new LexAuthUser(Alan!); + var users = await _userService.UserQueryForTypeahead(authUser).ToArrayAsync(); + // Alan can see himself in the Sherwood project, but nobody else because it's private + UserListShouldBe(users, [Alan]); + } + + private User CreateUser(string name) + { + var email = name.ToLowerInvariant().Replace(' ', '_') + "@example.com"; + var user = new User + { + Name = name, + Email = email, + CanCreateProjects = true, + EmailVerified = true, + IsAdmin = name.Contains("Admin"), + PasswordHash = "", + Salt = "" + }; + _lexBoxDbContext.Add(user); + ManagedUsers.Add(user); + return user; // Caller must call SaveChanges after all users and projects are added + } + + private Project CreateProject(IEnumerable managers, IEnumerable members, bool isConfidential = false) + { + var config = Testing.Services.Utils.GetNewProjectConfig(); + var project = new Project + { + Name = config.Name, + Code = config.Code, + IsConfidential = isConfidential, + LastCommit = null, + Organizations = [], + Users = [], + RetentionPolicy = RetentionPolicy.Test, + Type = ProjectType.FLEx, + Id = config.Id, + }; + project.Users.AddRange(managers.Select(userId => new ProjectUsers { UserId = userId, Role = ProjectRole.Manager })); + project.Users.AddRange(members.Select(userId => new ProjectUsers { UserId = userId, Role = ProjectRole.Editor })); + _lexBoxDbContext.Add(project); + ManagedProjects.Add(project); + return project; // Caller must call SaveChanges after all users and projects are added + } + + private Project CreateConfidentialProject(IEnumerable managers, IEnumerable members) + { + return CreateProject(managers, members, true); + } + + private async Task AddUserToProject(Project project, User user, ProjectRole role = ProjectRole.Editor) + { + var pu = project.Users.FirstOrDefault(pu => pu.UserId == user.Id); + if (pu is null) project.Users.Add(new ProjectUsers { UserId = user.Id, Role = role }); + else pu.Role = role; + await _lexBoxDbContext.SaveChangesAsync(); + } + + private async Task RemoveUserFromProject(Project project, User user) + { + var pu = project.Users.FirstOrDefault(pu => pu.UserId == user.Id); + if (pu is not null) project.Users.Remove(pu); + await _lexBoxDbContext.SaveChangesAsync(); + } + + private Organization CreateOrg(IEnumerable managers, IEnumerable members) + { + var id = Guid.NewGuid(); + var shortId = id.ToString().Split("-")[0]; + var org = new Organization + { + Name = shortId, + Members = [], + Projects = [], + }; + org.Members.AddRange(managers.Select(userId => new OrgMember { UserId = userId, Role = OrgRole.Admin })); + org.Members.AddRange(members.Select(userId => new OrgMember { UserId = userId, Role = OrgRole.User })); + _lexBoxDbContext.Add(org); + ManagedOrgs.Add(org); + return org; + } +} diff --git a/backend/Testing/Testing.csproj b/backend/Testing/Testing.csproj index 3ccb8771e..0c2ffc87a 100644 --- a/backend/Testing/Testing.csproj +++ b/backend/Testing/Testing.csproj @@ -27,6 +27,7 @@ + diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 9e136c3a6..9a315d8c3 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -192,6 +192,10 @@ type InvalidEmailError implements Error { address: String! } +type InvalidOperationError implements Error { + message: String! +} + type IsAdminResponse { value: Boolean! } @@ -441,6 +445,7 @@ type Query { orgs(where: OrganizationFilterInput @cost(weight: "10") orderBy: [OrganizationSortInput!] @cost(weight: "10")): [Organization!]! @cost(weight: "10") myOrgs(where: OrganizationFilterInput @cost(weight: "10") orderBy: [OrganizationSortInput!] @cost(weight: "10")): [Organization!]! @cost(weight: "10") usersInMyOrg(skip: Int take: Int where: UserFilterInput @cost(weight: "10") orderBy: [UserSortInput!] @cost(weight: "10")): UsersInMyOrgCollectionSegment @listSize(assumedSize: 1000, slicingArguments: [ "take" ], sizedFields: [ "items" ]) @cost(weight: "10") + usersICanSee(skip: Int take: Int where: UserFilterInput @cost(weight: "10") orderBy: [UserSortInput!] @cost(weight: "10")): UsersICanSeeCollectionSegment @listSize(assumedSize: 1000, slicingArguments: [ "take" ], sizedFields: [ "items" ]) @cost(weight: "10") orgById(orgId: UUID!): OrgById @cost(weight: "10") users(skip: Int take: Int where: UserFilterInput @cost(weight: "10") orderBy: [UserSortInput!] @cost(weight: "10")): UsersCollectionSegment @authorize(policy: "AdminRequiredPolicy") @listSize(assumedSize: 1000, slicingArguments: [ "take" ], sizedFields: [ "items" ]) @cost(weight: "10") me: MeDto @cost(weight: "10") @@ -562,6 +567,15 @@ type UsersCollectionSegment { totalCount: Int! @cost(weight: "10") } +"A segment of a collection." +type UsersICanSeeCollectionSegment { + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [User!] + totalCount: Int! @cost(weight: "10") +} + "A segment of a collection." type UsersInMyOrgCollectionSegment { "Information to aid in pagination." @@ -615,7 +629,7 @@ union LeaveProjectError = NotFoundError | LastMemberCantLeaveError union RemoveProjectFromOrgError = DbError | NotFoundError -union SendNewVerificationEmailByAdminError = NotFoundError | DbError | UniqueValueError +union SendNewVerificationEmailByAdminError = NotFoundError | DbError | InvalidOperationError union SetOrgMemberRoleError = DbError | NotFoundError | OrgMemberInvitedByEmail diff --git a/frontend/src/lib/forms/UserTypeahead.svelte b/frontend/src/lib/forms/UserTypeahead.svelte index 2f300197d..4f721f17e 100644 --- a/frontend/src/lib/forms/UserTypeahead.svelte +++ b/frontend/src/lib/forms/UserTypeahead.svelte @@ -1,6 +1,6 @@