1
0
mirror of https://github.com/bitwarden/server synced 2025-12-14 15:23:42 +00:00

Fix tests

This commit is contained in:
Hinton
2025-11-14 14:31:32 +01:00
parent dff45c137d
commit d9b5ab2f67
6 changed files with 92 additions and 52 deletions

View File

@@ -0,0 +1,41 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Json;
using System.Text.Json;
namespace Bit.SeederApi.IntegrationTest;
public static class HttpClientExtensions
{
/// <summary>
/// Sends a POST request with JSON content and attaches the x-play-id header.
/// </summary>
/// <typeparam name="TValue">The type of the value to serialize.</typeparam>
/// <param name="client">The HTTP client.</param>
/// <param name="requestUri">The URI the request is sent to.</param>
/// <param name="value">The value to serialize.</param>
/// <param name="playId">The play ID to attach as x-play-id header.</param>
/// <param name="options">Options to control the behavior during serialization.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>The task object representing the asynchronous operation.</returns>
public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
this HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
TValue value,
string playId,
JsonSerializerOptions? options = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(client);
if (string.IsNullOrWhiteSpace(playId))
{
throw new ArgumentException("Play ID cannot be null or whitespace.", nameof(playId));
}
var content = JsonContent.Create(value, mediaType: null, options);
content.Headers.Remove("x-play-id");
content.Headers.Add("x-play-id", playId);
return client.PostAsync(requestUri, content, cancellationToken);
}
}

View File

@@ -4,12 +4,12 @@ using Xunit;
namespace Bit.SeederApi.IntegrationTest; namespace Bit.SeederApi.IntegrationTest;
public class EmergencyAccessInviteQueryTests : IClassFixture<SeederApiApplicationFactory>, IAsyncLifetime public class QueryControllerTests : IClassFixture<SeederApiApplicationFactory>, IAsyncLifetime
{ {
private readonly HttpClient _client; private readonly HttpClient _client;
private readonly SeederApiApplicationFactory _factory; private readonly SeederApiApplicationFactory _factory;
public EmergencyAccessInviteQueryTests(SeederApiApplicationFactory factory) public QueryControllerTests(SeederApiApplicationFactory factory)
{ {
_factory = factory; _factory = factory;
_client = _factory.CreateClient(); _client = _factory.CreateClient();
@@ -38,16 +38,11 @@ public class EmergencyAccessInviteQueryTests : IClassFixture<SeederApiApplicatio
}); });
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<QueryResponse>(); var result = await response.Content.ReadAsStringAsync();
Assert.NotNull(result); Assert.NotNull(result);
Assert.NotNull(result.Result);
// The result should be a JSON array (even if empty for non-existent email) var urls = System.Text.Json.JsonSerializer.Deserialize<List<string>>(result);
var resultElement = result.Result as System.Text.Json.JsonElement?;
Assert.NotNull(resultElement);
var urls = System.Text.Json.JsonSerializer.Deserialize<List<string>>(resultElement.Value.GetRawText());
Assert.NotNull(urls); Assert.NotNull(urls);
// For a non-existent email, we expect an empty list // For a non-existent email, we expect an empty list
Assert.Empty(urls); Assert.Empty(urls);
@@ -90,19 +85,9 @@ public class EmergencyAccessInviteQueryTests : IClassFixture<SeederApiApplicatio
}); });
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<QueryResponse>(); var result = await response.Content.ReadAsStringAsync();
Assert.NotNull(result); Assert.Equal("[]", result);
// Verify the response only has Result field, not SeedId
// (Queries are read-only and don't track entities)
var jsonString = await response.Content.ReadAsStringAsync();
Assert.DoesNotContain("seedId", jsonString, StringComparison.OrdinalIgnoreCase);
Assert.Contains("result", jsonString, StringComparison.OrdinalIgnoreCase);
} }
private class QueryResponse
{
public object? Result { get; set; }
}
} }

View File

@@ -29,21 +29,21 @@ public class SeedControllerTests : IClassFixture<SeederApiApplicationFactory>, I
} }
[Fact] [Fact]
public async Task SeedEndpoint_WithValidScene_ReturnsOkWithSeedId() public async Task SeedEndpoint_WithValidScene_ReturnsOk()
{ {
var testEmail = $"seed-test-{Guid.NewGuid()}@bitwarden.com"; var testEmail = $"seed-test-{Guid.NewGuid()}@bitwarden.com";
var playId = Guid.NewGuid().ToString();
var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{ {
Template = "SingleUserScene", Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
}); }, playId);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<SceneResponseModel>(); var result = await response.Content.ReadFromJsonAsync<SceneResponseModel>();
Assert.NotNull(result); Assert.NotNull(result);
Assert.NotEqual(Guid.Empty, result.SeedId);
Assert.NotNull(result.MangleMap); Assert.NotNull(result.MangleMap);
Assert.Null(result.Result); Assert.Null(result.Result);
} }
@@ -74,60 +74,63 @@ public class SeedControllerTests : IClassFixture<SeederApiApplicationFactory>, I
} }
[Fact] [Fact]
public async Task DeleteEndpoint_WithValidSeedId_ReturnsOk() public async Task DeleteEndpoint_WithValidPlayId_ReturnsOk()
{ {
var testEmail = $"delete-test-{Guid.NewGuid()}@bitwarden.com"; var testEmail = $"delete-test-{Guid.NewGuid()}@bitwarden.com";
var playId = Guid.NewGuid().ToString();
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{ {
Template = "SingleUserScene", Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
}); }, playId);
seedResponse.EnsureSuccessStatusCode(); seedResponse.EnsureSuccessStatusCode();
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>(); var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>();
Assert.NotNull(seedResult); Assert.NotNull(seedResult);
var deleteResponse = await _client.DeleteAsync($"/seed/{seedResult.SeedId}"); var deleteResponse = await _client.DeleteAsync($"/seed/{playId}");
deleteResponse.EnsureSuccessStatusCode(); deleteResponse.EnsureSuccessStatusCode();
} }
[Fact] [Fact]
public async Task DeleteEndpoint_WithInvalidSeedId_ReturnsOkWithNull() public async Task DeleteEndpoint_WithInvalidPlayId_ReturnsOk()
{ {
// DestroyRecipe is idempotent - returns null for non-existent seeds // DestroyRecipe is idempotent - returns null for non-existent play IDs
var nonExistentSeedId = Guid.NewGuid(); var nonExistentPlayId = Guid.NewGuid().ToString();
var response = await _client.DeleteAsync($"/seed/{nonExistentSeedId}"); var response = await _client.DeleteAsync($"/seed/{nonExistentPlayId}");
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
Assert.Equal("null", content); Assert.Equal($$"""{"playId":"{{nonExistentPlayId}}"}""", content);
} }
[Fact] [Fact]
public async Task DeleteBatchEndpoint_WithValidSeedIds_ReturnsOk() public async Task DeleteBatchEndpoint_WithValidPlayIds_ReturnsOk()
{ {
// Create multiple seeds // Create multiple seeds with different play IDs
var seedIds = new List<Guid>(); var playIds = new List<string>();
for (var i = 0; i < 3; i++) for (var i = 0; i < 3; i++)
{ {
var playId = Guid.NewGuid().ToString();
playIds.Add(playId);
var testEmail = $"batch-test-{Guid.NewGuid()}@bitwarden.com"; var testEmail = $"batch-test-{Guid.NewGuid()}@bitwarden.com";
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{ {
Template = "SingleUserScene", Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
}); }, playId);
seedResponse.EnsureSuccessStatusCode(); seedResponse.EnsureSuccessStatusCode();
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>(); var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>();
Assert.NotNull(seedResult); Assert.NotNull(seedResult);
Assert.NotNull(seedResult.SeedId);
seedIds.Add(seedResult.SeedId.Value);
} }
// Delete them in batch // Delete them in batch
var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch") var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch")
{ {
Content = JsonContent.Create(seedIds) Content = JsonContent.Create(playIds)
}; };
var deleteResponse = await _client.SendAsync(request); var deleteResponse = await _client.SendAsync(request);
deleteResponse.EnsureSuccessStatusCode(); deleteResponse.EnsureSuccessStatusCode();
@@ -141,24 +144,25 @@ public class SeedControllerTests : IClassFixture<SeederApiApplicationFactory>, I
public async Task DeleteBatchEndpoint_WithSomeInvalidIds_ReturnsOk() public async Task DeleteBatchEndpoint_WithSomeInvalidIds_ReturnsOk()
{ {
// DestroyRecipe is idempotent - batch delete succeeds even with non-existent IDs // DestroyRecipe is idempotent - batch delete succeeds even with non-existent IDs
// Create one valid seed // Create one valid seed with a play ID
var validPlayId = Guid.NewGuid().ToString();
var testEmail = $"batch-partial-test-{Guid.NewGuid()}@bitwarden.com"; var testEmail = $"batch-partial-test-{Guid.NewGuid()}@bitwarden.com";
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{ {
Template = "SingleUserScene", Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
}); }, validPlayId);
seedResponse.EnsureSuccessStatusCode(); seedResponse.EnsureSuccessStatusCode();
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>(); var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>();
Assert.NotNull(seedResult); Assert.NotNull(seedResult);
// Try to delete with mix of valid and invalid IDs // Try to delete with mix of valid and invalid IDs
Assert.NotNull(seedResult.SeedId); var playIds = new List<string> { validPlayId, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() };
var seedIds = new List<Guid> { seedResult.SeedId.Value, Guid.NewGuid(), Guid.NewGuid() };
var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch") var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch")
{ {
Content = JsonContent.Create(seedIds) Content = JsonContent.Create(playIds)
}; };
var deleteResponse = await _client.SendAsync(request); var deleteResponse = await _client.SendAsync(request);
@@ -174,13 +178,17 @@ public class SeedControllerTests : IClassFixture<SeederApiApplicationFactory>, I
// Create multiple seeds // Create multiple seeds
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var playId = Guid.NewGuid().ToString();
var testEmail = $"deleteall-test-{Guid.NewGuid()}@bitwarden.com"; var testEmail = $"deleteall-test-{Guid.NewGuid()}@bitwarden.com";
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{ {
Template = "SingleUserScene", Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
}); }, playId);
var body = await seedResponse.Content.ReadAsStringAsync();
Console.WriteLine(body);
seedResponse.EnsureSuccessStatusCode(); seedResponse.EnsureSuccessStatusCode();
} }
@@ -190,21 +198,22 @@ public class SeedControllerTests : IClassFixture<SeederApiApplicationFactory>, I
} }
[Fact] [Fact]
public async Task SeedEndpoint_VerifyResponseContainsSeedIdAndResult() public async Task SeedEndpoint_VerifyResponseContainsMangleMapAndResult()
{ {
var testEmail = $"verify-response-{Guid.NewGuid()}@bitwarden.com"; var testEmail = $"verify-response-{Guid.NewGuid()}@bitwarden.com";
var playId = Guid.NewGuid().ToString();
var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{ {
Template = "SingleUserScene", Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail }) Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
}); }, playId);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var jsonString = await response.Content.ReadAsStringAsync(); var jsonString = await response.Content.ReadAsStringAsync();
// Verify the response contains both SeedId and Result fields // Verify the response contains MangleMap and Result fields
Assert.Contains("seedId", jsonString, StringComparison.OrdinalIgnoreCase); Assert.Contains("mangleMap", jsonString, StringComparison.OrdinalIgnoreCase);
Assert.Contains("result", jsonString, StringComparison.OrdinalIgnoreCase); Assert.Contains("result", jsonString, StringComparison.OrdinalIgnoreCase);
} }

View File

@@ -18,11 +18,11 @@ public class QueryController(ILogger<QueryController> logger, IQueryService quer
return Json(result); return Json(result);
} }
catch (SceneNotFoundException ex) catch (QueryNotFoundException ex)
{ {
return NotFound(new { Error = ex.Message }); return NotFound(new { Error = ex.Message });
} }
catch (SceneExecutionException ex) catch (QueryExecutionException ex)
{ {
logger.LogError(ex, "Error executing query: {Query}", request.Template); logger.LogError(ex, "Error executing query: {Query}", request.Template);
return BadRequest(new { Error = ex.Message, Details = ex.InnerException?.Message }); return BadRequest(new { Error = ex.Message, Details = ex.InnerException?.Message });

View File

@@ -21,6 +21,7 @@ builder.Services.AddScoped<Microsoft.AspNetCore.Identity.IPasswordHasher<Bit.Cor
builder.Services.AddSingleton<Bit.RustSDK.RustSdkService>(); builder.Services.AddSingleton<Bit.RustSDK.RustSdkService>();
builder.Services.AddScoped<Bit.Seeder.Factories.UserSeeder>(); builder.Services.AddScoped<Bit.Seeder.Factories.UserSeeder>();
builder.Services.AddScoped<ISceneService, SceneService>(); builder.Services.AddScoped<ISceneService, SceneService>();
builder.Services.AddScoped<IQueryService, QueryService>();
builder.Services.AddScoped<MangleId>(_ => new MangleId()); builder.Services.AddScoped<MangleId>(_ => new MangleId());
builder.Services.AddScenes(); builder.Services.AddScenes();
builder.Services.AddQueries(); builder.Services.AddQueries();
@@ -41,3 +42,6 @@ app.UseRouting();
app.MapControllerRoute(name: "default", pattern: "{controller=Seed}/{action=Index}/{id?}"); app.MapControllerRoute(name: "default", pattern: "{controller=Seed}/{action=Index}/{id?}");
app.Run(); app.Run();
// Make Program class accessible for integration tests
public partial class Program { }

View File

@@ -43,12 +43,13 @@ public class SceneService(
var userIds = playData.Select(pd => pd.UserId).Distinct().ToList(); var userIds = playData.Select(pd => pd.UserId).Distinct().ToList();
var organizationIds = playData.Select(pd => pd.OrganizationId).Distinct().ToList(); var organizationIds = playData.Select(pd => pd.OrganizationId).Distinct().ToList();
// Delete Users before Oraganizations to respect foreign key constraints // Delete Users before Organizations to respect foreign key constraints
if (userIds.Count > 0) if (userIds.Count > 0)
{ {
var users = databaseContext.Users.Where(u => userIds.Contains(u.Id)); var users = databaseContext.Users.Where(u => userIds.Contains(u.Id));
await userRepository.DeleteManyAsync(users); await userRepository.DeleteManyAsync(users);
} }
if (organizationIds.Count > 0) if (organizationIds.Count > 0)
{ {
var organizations = databaseContext.Organizations.Where(o => organizationIds.Contains(o.Id)); var organizations = databaseContext.Organizations.Where(o => organizationIds.Contains(o.Id));