1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 20:53:16 +00:00

Split Scene and Query

This commit is contained in:
Hinton
2025-10-20 19:20:00 -04:00
parent 1daf9ad892
commit 9231ae3de1
9 changed files with 422 additions and 31 deletions

View File

@@ -0,0 +1,108 @@
using System.Net;
using Bit.SeederApi.Models.Requests;
using Xunit;
namespace Bit.SeederApi.IntegrationTest;
public class EmergencyAccessInviteQueryTests : IClassFixture<SeederApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly SeederApiApplicationFactory _factory;
public EmergencyAccessInviteQueryTests(SeederApiApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
public Task InitializeAsync()
{
return Task.CompletedTask;
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task QueryEndpoint_WithValidQueryAndArguments_ReturnsOk()
{
var testEmail = $"emergency-test-{Guid.NewGuid()}@bitwarden.com";
var response = await _client.PostAsJsonAsync("/query", new QueryRequestModel
{
Template = "EmergencyAccessInviteQuery",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
});
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<QueryResponse>();
Assert.NotNull(result);
Assert.NotNull(result.Result);
// The result should be a JSON array (even if empty for non-existent email)
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);
// For a non-existent email, we expect an empty list
Assert.Empty(urls);
}
[Fact]
public async Task QueryEndpoint_WithInvalidQueryName_ReturnsNotFound()
{
var response = await _client.PostAsJsonAsync("/query", new QueryRequestModel
{
Template = "NonExistentQuery",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = "test@example.com" })
});
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task QueryEndpoint_WithMissingRequiredField_ReturnsBadRequest()
{
// EmergencyAccessInviteQuery requires 'email' field
var response = await _client.PostAsJsonAsync("/query", new QueryRequestModel
{
Template = "EmergencyAccessInviteQuery",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { wrongField = "value" })
});
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task QueryEndpoint_VerifyQueryDoesNotCreateSeedId()
{
var testEmail = $"test-{Guid.NewGuid()}@bitwarden.com";
var response = await _client.PostAsJsonAsync("/query", new QueryRequestModel
{
Template = "EmergencyAccessInviteQuery",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
});
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<QueryResponse>();
Assert.NotNull(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

@@ -0,0 +1,214 @@
using System.Net;
using Bit.SeederApi.Models.Requests;
using Bit.SeederApi.Models.Response;
using Xunit;
namespace Bit.SeederApi.IntegrationTest;
public class SeedControllerTests : IClassFixture<SeederApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly SeederApiApplicationFactory _factory;
public SeedControllerTests(SeederApiApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
public Task InitializeAsync()
{
return Task.CompletedTask;
}
public async Task DisposeAsync()
{
// Clean up any seeded data after each test
await _client.DeleteAsync("/seed");
_client.Dispose();
}
[Fact]
public async Task SeedEndpoint_WithValidScene_ReturnsOkWithSeedId()
{
var testEmail = $"seed-test-{Guid.NewGuid()}@bitwarden.com";
var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{
Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
});
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<SeedResponseModel>();
Assert.NotNull(result);
Assert.NotEqual(Guid.Empty, result.SeedId);
Assert.NotNull(result.Result);
}
[Fact]
public async Task SeedEndpoint_WithInvalidSceneName_ReturnsNotFound()
{
var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{
Template = "NonExistentScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = "test@example.com" })
});
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task SeedEndpoint_WithMissingRequiredField_ReturnsBadRequest()
{
// SingleUserScene requires 'email' field
var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{
Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { wrongField = "value" })
});
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task DeleteEndpoint_WithValidSeedId_ReturnsOk()
{
var testEmail = $"delete-test-{Guid.NewGuid()}@bitwarden.com";
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{
Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
});
seedResponse.EnsureSuccessStatusCode();
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SeedResponseModel>();
Assert.NotNull(seedResult);
var deleteResponse = await _client.DeleteAsync($"/seed/{seedResult.SeedId}");
deleteResponse.EnsureSuccessStatusCode();
}
[Fact]
public async Task DeleteEndpoint_WithInvalidSeedId_ReturnsOkWithNull()
{
// DestroyRecipe is idempotent - returns null for non-existent seeds
var nonExistentSeedId = Guid.NewGuid();
var response = await _client.DeleteAsync($"/seed/{nonExistentSeedId}");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("null", content);
}
[Fact]
public async Task DeleteBatchEndpoint_WithValidSeedIds_ReturnsOk()
{
// Create multiple seeds
var seedIds = new List<Guid>();
for (var i = 0; i < 3; i++)
{
var testEmail = $"batch-test-{Guid.NewGuid()}@bitwarden.com";
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{
Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
});
seedResponse.EnsureSuccessStatusCode();
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SeedResponseModel>();
Assert.NotNull(seedResult);
Assert.NotNull(seedResult.SeedId);
seedIds.Add(seedResult.SeedId.Value);
}
// Delete them in batch
var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch")
{
Content = JsonContent.Create(seedIds)
};
var deleteResponse = await _client.SendAsync(request);
deleteResponse.EnsureSuccessStatusCode();
var result = await deleteResponse.Content.ReadFromJsonAsync<BatchDeleteResponse>();
Assert.NotNull(result);
Assert.Equal("Batch delete completed successfully", result.Message);
}
[Fact]
public async Task DeleteBatchEndpoint_WithSomeInvalidIds_ReturnsOk()
{
// DestroyRecipe is idempotent - batch delete succeeds even with non-existent IDs
// Create one valid seed
var testEmail = $"batch-partial-test-{Guid.NewGuid()}@bitwarden.com";
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{
Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
});
seedResponse.EnsureSuccessStatusCode();
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SeedResponseModel>();
Assert.NotNull(seedResult);
// Try to delete with mix of valid and invalid IDs
Assert.NotNull(seedResult.SeedId);
var seedIds = new List<Guid> { seedResult.SeedId.Value, Guid.NewGuid(), Guid.NewGuid() };
var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch")
{
Content = JsonContent.Create(seedIds)
};
var deleteResponse = await _client.SendAsync(request);
deleteResponse.EnsureSuccessStatusCode();
var result = await deleteResponse.Content.ReadFromJsonAsync<BatchDeleteResponse>();
Assert.NotNull(result);
Assert.Equal("Batch delete completed successfully", result.Message);
}
[Fact]
public async Task DeleteAllEndpoint_DeletesAllSeededData()
{
// Create multiple seeds
for (var i = 0; i < 2; i++)
{
var testEmail = $"deleteall-test-{Guid.NewGuid()}@bitwarden.com";
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{
Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
});
seedResponse.EnsureSuccessStatusCode();
}
// Delete all
var deleteResponse = await _client.DeleteAsync("/seed");
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
}
[Fact]
public async Task SeedEndpoint_VerifyResponseContainsSeedIdAndResult()
{
var testEmail = $"verify-response-{Guid.NewGuid()}@bitwarden.com";
var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
{
Template = "SingleUserScene",
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
});
response.EnsureSuccessStatusCode();
var jsonString = await response.Content.ReadAsStringAsync();
// Verify the response contains both SeedId and Result fields
Assert.Contains("seedId", jsonString, StringComparison.OrdinalIgnoreCase);
Assert.Contains("result", jsonString, StringComparison.OrdinalIgnoreCase);
}
private class BatchDeleteResponse
{
public string? Message { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\util\SeederApi\SeederApi.csproj" />
<ProjectReference Include="..\..\util\Seeder\Seeder.csproj" />
<ProjectReference Include="..\IntegrationTestCommon\IntegrationTestCommon.csproj" />
<Content Include="..\..\util\SeederApi\appsettings.*.json">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
using Bit.IntegrationTestCommon.Factories;
namespace Bit.SeederApi.IntegrationTest;
public class SeederApiApplicationFactory : WebApplicationFactoryBase<Program>
{
}