Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GQL middleware test for simultaneous db access #1182

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion backend/Testing/ApiTests/ApiTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class ApiTestBase
public string BaseUrl => TestingEnvironmentVariables.ServerBaseUrl;
private readonly SocketsHttpHandler _httpClientHandler;
public readonly HttpClient HttpClient;
public string? CurrJwt { get; private set; }

public ApiTestBase()
{
Expand Down Expand Up @@ -48,7 +49,8 @@ public virtual async Task<string> LoginAs(string user, string? password = null)
{
password ??= TestingEnvironmentVariables.DefaultPassword;
var response = await JwtHelper.ExecuteLogin(new SendReceiveAuth(user, password), HttpClient);
return JwtHelper.GetJwtFromLoginResponse(response);
CurrJwt = JwtHelper.GetJwtFromLoginResponse(response);
return CurrJwt;
}

public void ClearCookies()
Expand Down
75 changes: 75 additions & 0 deletions backend/Testing/ApiTests/GqlMiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Text.Json.Nodes;
using LexCore.Entities;
using Shouldly;
using Testing.Fixtures;
using static Testing.Services.Utils;

namespace Testing.ApiTests;

[Trait("Category", "Integration")]
public class GqlMiddlewareTests : IClassFixture<IntegrationFixture>
{
private readonly IntegrationFixture _fixture;
private readonly ApiTestBase _adminApiTester;

public GqlMiddlewareTests(IntegrationFixture fixture)
{
_fixture = fixture;
_adminApiTester = _fixture.AdminApiTester;
}

private async Task<JsonObject> QueryMyProjectsWithMembers()
{
var json = await _adminApiTester.ExecuteGql(
$$"""
query loadMyProjects {
myProjects(orderBy: [ { name: ASC } ]) {
code
id
name
users {
id
userId
role
}
}
}
""");
return json;
}

[Fact]
public async Task CanTriggerMultipleInstancesOfMiddlewareThatAccessDbSimultaneously()
{
var config1 = GetNewProjectConfig();
var config2 = GetNewProjectConfig();
var config3 = GetNewProjectConfig();

var projects = await Task.WhenAll(
RegisterProjectInLexBox(config1, _adminApiTester),
RegisterProjectInLexBox(config2, _adminApiTester),
RegisterProjectInLexBox(config3, _adminApiTester));

await using var project1 = projects[0];
await using var project2 = projects[1];
await using var project3 = projects[2];

await Task.WhenAll(
AddMemberToProject(config1, _adminApiTester, "editor", ProjectRole.Editor),
AddMemberToProject(config2, _adminApiTester, "editor", ProjectRole.Editor),
AddMemberToProject(config3, _adminApiTester, "editor", ProjectRole.Editor));

await _adminApiTester.LoginAs("editor");
// Because we assigned ProjectRole.Editor and these projects are new,
// our middlware will query the project confidentiality from the DB to determine
// if the user is allowed to view all members
var json = await QueryMyProjectsWithMembers();

json.ShouldNotBeNull();
var myProjects = json["data"]!["myProjects"]!.AsArray();
var ids = myProjects.Select(p => p!["id"]!.GetValue<Guid>());

foreach (var project in projects)
ids.ShouldContain(project.id);
}
}
6 changes: 3 additions & 3 deletions backend/Testing/ApiTests/ResetProjectRaceConditions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public async Task SimultaneousResetsDontResultIn404s()
var config3 = GetNewProjectConfig();

var projects = await Task.WhenAll(
RegisterProjectInLexBox(config1, _adminApiTester),
RegisterProjectInLexBox(config2, _adminApiTester),
RegisterProjectInLexBox(config3, _adminApiTester)
RegisterProjectInLexBox(config1, _adminApiTester, true),
RegisterProjectInLexBox(config2, _adminApiTester, true),
RegisterProjectInLexBox(config3, _adminApiTester, true)
);

await using var project1 = projects[0];
Expand Down
46 changes: 41 additions & 5 deletions backend/Testing/Services/Utils.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using LexCore.Entities;
using Quartz.Util;
using Shouldly;
using Testing.ApiTests;
Expand Down Expand Up @@ -33,7 +34,8 @@ public static ProjectConfig GetNewProjectConfig(HgProtocol? protocol = null, boo

public static async Task<LexboxProject> RegisterProjectInLexBox(
ProjectConfig config,
ApiTestBase apiTester
ApiTestBase apiTester,
bool waitForRepoReady = false
)
{
await apiTester.ExecuteGql($$"""
Expand Down Expand Up @@ -62,10 +64,42 @@ ... on DbError {
}
}
""");
await WaitForHgRefreshIntervalAsync();
if (waitForRepoReady) await WaitForHgRefreshIntervalAsync();
return new LexboxProject(apiTester, config.Id);
}

public static async Task AddMemberToProject(
ProjectConfig config,
ApiTestBase apiTester,
string usernameOrEmail,
ProjectRole role
)
{
await apiTester.ExecuteGql($$"""
mutation {
addProjectMember(input: {
projectId: "{{config.Id}}",
usernameOrEmail: "{{usernameOrEmail}}"
role: {{role.ToString().ToUpper()}}
canInvite: false
}) {
project {
id
}
errors {
__typename
... on Error {
message
}
... on InvalidEmailError {
address
}
}
}
}
""");
}

public static void ValidateSendReceiveOutput(string srOutput)
{
srOutput.ShouldNotContain("abort");
Expand Down Expand Up @@ -101,18 +135,20 @@ private static string GetNewProjectDir(string projectCode,

public record LexboxProject : IAsyncDisposable
{
public readonly Guid id;
private readonly ApiTestBase _apiTester;
private readonly Guid _id;
private readonly string _jwt;

public LexboxProject(ApiTestBase apiTester, Guid id)
{
this.id = id;
_apiTester = apiTester;
_id = id;
_jwt = apiTester.CurrJwt ?? throw new InvalidOperationException("No JWT found");
}

public async ValueTask DisposeAsync()
{
var response = await _apiTester.HttpClient.DeleteAsync($"api/project/{_id}");
var response = await _apiTester.HttpClient.DeleteAsync($"api/project/{id}?jwt={_jwt}");
response.EnsureSuccessStatusCode();
}
}
Expand Down
8 changes: 4 additions & 4 deletions backend/Testing/SyncReverseProxy/SendReceiveServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public async Task CloneConfidentialProjectAsOrgManager(HgProtocol protocol)
{
// Create a fresh project
var projectConfig = _srFixture.InitLocalFlexProjectWithRepo(protocol, isConfidential: true, LexData.SeedingData.TestOrgId);
await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester);
await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester, true);

// Push the project to the server
var sendReceiveParams = new SendReceiveParams(protocol, projectConfig);
Expand Down Expand Up @@ -97,7 +97,7 @@ public async Task ModifyProjectData(HgProtocol protocol)
{
// Create a fresh project
var projectConfig = _srFixture.InitLocalFlexProjectWithRepo();
await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester);
await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester, true);

// Push the project to the server
var sendReceiveParams = new SendReceiveParams(protocol, projectConfig);
Expand Down Expand Up @@ -127,7 +127,7 @@ public async Task SendReceiveAfterProjectReset(HgProtocol protocol)
{
// Create a fresh project
var projectConfig = _srFixture.InitLocalFlexProjectWithRepo(protocol, isConfidential: false, null, "SR_AfterReset");
await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester);
await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester, true);

var sendReceiveParams = new SendReceiveParams(protocol, projectConfig);
var srResult = _sendReceiveService.SendReceiveProject(sendReceiveParams, AdminAuth);
Expand Down Expand Up @@ -194,7 +194,7 @@ public async Task SendNewProject_Medium()
private async Task SendNewProject(int totalSizeMb, int fileCount)
{
var projectConfig = _srFixture.InitLocalFlexProjectWithRepo();
await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester);
await using var project = await RegisterProjectInLexBox(projectConfig, _adminApiTester, true);

await WaitForHgRefreshIntervalAsync();

Expand Down