mirror of
https://github.com/bitwarden/server
synced 2026-01-15 15:03:34 +00:00
[PM-22263] [PM-29849] Initial PoC of seeder API (#6424)
We want to reduce the amount of business critical test data in the company. One way of doing that is to generate test data on demand prior to client side testing. Clients will request a scene to be set up with a JSON body set of options, specific to a given scene. Successful seed requests will be responded to with a mangleMap which maps magic strings present in the request to the mangled, non-colliding versions inserted into the database. This way, the server is solely responsible for understanding uniqueness requirements in the database. scenes also are able to return custom data, depending on the scene. For example, user creation would benefit from a return value of the userId for further test setup on the client side. Clients will indicate they are running tests by including a unique header, x-play-id which specifies a unique testing context. The server uses this PlayId as the seed for any mangling that occurs. This allows the client to decide it will reuse a given PlayId if the test context builds on top of previously executed tests. When a given context is no longer needed, the API user will delete all test data associated with the PlayId by calling a delete endpoint. --------- Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
@@ -26,6 +26,7 @@ public class SutProvider<TSut> : ISutProvider
|
||||
|
||||
public TSut Sut { get; private set; }
|
||||
public Type SutType => typeof(TSut);
|
||||
public IFixture Fixture => _fixture;
|
||||
|
||||
public SutProvider() : this(new Fixture()) { }
|
||||
|
||||
@@ -65,6 +66,19 @@ public class SutProvider<TSut> : ISutProvider
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and registers a dependency to be injected when the sut is created.
|
||||
/// </summary>
|
||||
/// <typeparam name="TDep">The Dependency type to create</typeparam>
|
||||
/// <param name="parameterName">The (optional) parameter name to register the dependency under</param>
|
||||
/// <returns>The created dependency value</returns>
|
||||
public TDep CreateDependency<TDep>(string parameterName = "")
|
||||
{
|
||||
var dependency = _fixture.Create<TDep>();
|
||||
SetDependency(dependency, parameterName);
|
||||
return dependency;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a dependency of the sut. Can only be called after the dependency has been set, either explicitly with
|
||||
/// <see cref="SetDependency{T}"/> or automatically with <see cref="Create"/>.
|
||||
|
||||
211
test/Core.Test/Services/PlayIdServiceTests.cs
Normal file
211
test/Core.Test/Services/PlayIdServiceTests.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PlayIdServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void InPlay_WhenPlayIdSetAndDevelopment_ReturnsTrue(
|
||||
string playId,
|
||||
SutProvider<PlayIdService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Development);
|
||||
sutProvider.Sut.PlayId = playId;
|
||||
|
||||
var result = sutProvider.Sut.InPlay(out var resultPlayId);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(playId, resultPlayId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void InPlay_WhenPlayIdSetButNotDevelopment_ReturnsFalse(
|
||||
string playId,
|
||||
SutProvider<PlayIdService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Production);
|
||||
sutProvider.Sut.PlayId = playId;
|
||||
|
||||
var result = sutProvider.Sut.InPlay(out var resultPlayId);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(playId, resultPlayId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData((string?)null)]
|
||||
[BitAutoData("")]
|
||||
public void InPlay_WhenPlayIdNullOrEmptyAndDevelopment_ReturnsFalse(
|
||||
string? playId,
|
||||
SutProvider<PlayIdService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Development);
|
||||
sutProvider.Sut.PlayId = playId;
|
||||
|
||||
var result = sutProvider.Sut.InPlay(out var resultPlayId);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(resultPlayId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void PlayId_CanGetAndSet(string playId)
|
||||
{
|
||||
var hostEnvironment = Substitute.For<IHostEnvironment>();
|
||||
var sut = new PlayIdService(hostEnvironment);
|
||||
|
||||
sut.PlayId = playId;
|
||||
|
||||
Assert.Equal(playId, sut.PlayId);
|
||||
}
|
||||
}
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class NeverPlayIdServicesTests
|
||||
{
|
||||
[Fact]
|
||||
public void InPlay_ReturnsFalse()
|
||||
{
|
||||
var sut = new NeverPlayIdServices();
|
||||
|
||||
var result = sut.InPlay(out var playId);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(playId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("test-play-id")]
|
||||
[InlineData(null)]
|
||||
public void PlayId_SetterDoesNothing_GetterReturnsNull(string? value)
|
||||
{
|
||||
var sut = new NeverPlayIdServices();
|
||||
|
||||
sut.PlayId = value;
|
||||
|
||||
Assert.Null(sut.PlayId);
|
||||
}
|
||||
}
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PlayIdSingletonServiceTests
|
||||
{
|
||||
public static IEnumerable<object[]> SutProvider()
|
||||
{
|
||||
var sutProvider = new SutProvider<PlayIdSingletonService>();
|
||||
var httpContext = sutProvider.CreateDependency<HttpContext>();
|
||||
var serviceProvider = sutProvider.CreateDependency<IServiceProvider>();
|
||||
var hostEnvironment = sutProvider.CreateDependency<IHostEnvironment>();
|
||||
var playIdService = new PlayIdService(hostEnvironment);
|
||||
sutProvider.SetDependency(playIdService);
|
||||
httpContext.RequestServices.Returns(serviceProvider);
|
||||
serviceProvider.GetService<PlayIdService>().Returns(playIdService);
|
||||
serviceProvider.GetRequiredService<PlayIdService>().Returns(playIdService);
|
||||
sutProvider.CreateDependency<IHttpContextAccessor>().HttpContext.Returns(httpContext);
|
||||
sutProvider.Create();
|
||||
return [[sutProvider]];
|
||||
}
|
||||
|
||||
private void PrepHttpContext(
|
||||
SutProvider<PlayIdSingletonService> sutProvider)
|
||||
{
|
||||
var httpContext = sutProvider.CreateDependency<HttpContext>();
|
||||
var serviceProvider = sutProvider.CreateDependency<IServiceProvider>();
|
||||
var PlayIdService = sutProvider.CreateDependency<PlayIdService>();
|
||||
httpContext.RequestServices.Returns(serviceProvider);
|
||||
serviceProvider.GetRequiredService<PlayIdService>().Returns(PlayIdService);
|
||||
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext.Returns(httpContext);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(SutProvider))]
|
||||
public void InPlay_WhenNoHttpContext_ReturnsFalse(
|
||||
SutProvider<PlayIdSingletonService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext.Returns((HttpContext?)null);
|
||||
sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Development);
|
||||
|
||||
var result = sutProvider.Sut.InPlay(out var playId);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(playId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(SutProvider))]
|
||||
public void InPlay_WhenNotDevelopment_ReturnsFalse(
|
||||
SutProvider<PlayIdSingletonService> sutProvider,
|
||||
string playIdValue)
|
||||
{
|
||||
var scopedPlayIdService = sutProvider.GetDependency<PlayIdService>();
|
||||
scopedPlayIdService.PlayId = playIdValue;
|
||||
sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Production);
|
||||
|
||||
var result = sutProvider.Sut.InPlay(out var playId);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Empty(playId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(SutProvider))]
|
||||
public void InPlay_WhenDevelopmentAndHttpContextWithPlayId_ReturnsTrue(
|
||||
SutProvider<PlayIdSingletonService> sutProvider,
|
||||
string playIdValue)
|
||||
{
|
||||
sutProvider.GetDependency<PlayIdService>().PlayId = playIdValue;
|
||||
sutProvider.GetDependency<IHostEnvironment>().EnvironmentName.Returns(Environments.Development);
|
||||
|
||||
var result = sutProvider.Sut.InPlay(out var playId);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.Equal(playIdValue, playId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(SutProvider))]
|
||||
public void PlayId_SetterSetsOnScopedService(
|
||||
SutProvider<PlayIdSingletonService> sutProvider,
|
||||
string playIdValue)
|
||||
{
|
||||
var scopedPlayIdService = sutProvider.GetDependency<PlayIdService>();
|
||||
|
||||
sutProvider.Sut.PlayId = playIdValue;
|
||||
|
||||
Assert.Equal(playIdValue, scopedPlayIdService.PlayId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(SutProvider))]
|
||||
public void PlayId_WhenNoHttpContext_GetterReturnsNull(
|
||||
SutProvider<PlayIdSingletonService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext.Returns((HttpContext?)null);
|
||||
|
||||
var result = sutProvider.Sut.PlayId;
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(SutProvider))]
|
||||
public void PlayId_WhenNoHttpContext_SetterDoesNotThrow(
|
||||
SutProvider<PlayIdSingletonService> sutProvider,
|
||||
string playIdValue)
|
||||
{
|
||||
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext.Returns((HttpContext?)null);
|
||||
|
||||
sutProvider.Sut.PlayId = playIdValue;
|
||||
}
|
||||
}
|
||||
143
test/Core.Test/Services/PlayItemServiceTests.cs
Normal file
143
test/Core.Test/Services/PlayItemServiceTests.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class PlayItemServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Record_User_WhenInPlay_RecordsPlayItem(
|
||||
string playId,
|
||||
User user,
|
||||
SutProvider<PlayItemService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IPlayIdService>()
|
||||
.InPlay(out Arg.Any<string>())
|
||||
.Returns(x =>
|
||||
{
|
||||
x[0] = playId;
|
||||
return true;
|
||||
});
|
||||
|
||||
await sutProvider.Sut.Record(user);
|
||||
|
||||
await sutProvider.GetDependency<IPlayItemRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(Arg.Is<PlayItem>(pd =>
|
||||
pd.PlayId == playId &&
|
||||
pd.UserId == user.Id &&
|
||||
pd.OrganizationId == null));
|
||||
|
||||
sutProvider.GetDependency<ILogger<PlayItemService>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString().Contains(user.Id.ToString()) && o.ToString().Contains(playId)),
|
||||
null,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Record_User_WhenNotInPlay_DoesNotRecordPlayItem(
|
||||
User user,
|
||||
SutProvider<PlayItemService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IPlayIdService>()
|
||||
.InPlay(out Arg.Any<string>())
|
||||
.Returns(x =>
|
||||
{
|
||||
x[0] = null;
|
||||
return false;
|
||||
});
|
||||
|
||||
await sutProvider.Sut.Record(user);
|
||||
|
||||
await sutProvider.GetDependency<IPlayItemRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateAsync(Arg.Any<PlayItem>());
|
||||
|
||||
sutProvider.GetDependency<ILogger<PlayItemService>>()
|
||||
.DidNotReceive()
|
||||
.Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Record_Organization_WhenInPlay_RecordsPlayItem(
|
||||
string playId,
|
||||
Organization organization,
|
||||
SutProvider<PlayItemService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IPlayIdService>()
|
||||
.InPlay(out Arg.Any<string>())
|
||||
.Returns(x =>
|
||||
{
|
||||
x[0] = playId;
|
||||
return true;
|
||||
});
|
||||
|
||||
await sutProvider.Sut.Record(organization);
|
||||
|
||||
await sutProvider.GetDependency<IPlayItemRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(Arg.Is<PlayItem>(pd =>
|
||||
pd.PlayId == playId &&
|
||||
pd.OrganizationId == organization.Id &&
|
||||
pd.UserId == null));
|
||||
|
||||
sutProvider.GetDependency<ILogger<PlayItemService>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString().Contains(organization.Id.ToString()) && o.ToString().Contains(playId)),
|
||||
null,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Record_Organization_WhenNotInPlay_DoesNotRecordPlayItem(
|
||||
Organization organization,
|
||||
SutProvider<PlayItemService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IPlayIdService>()
|
||||
.InPlay(out Arg.Any<string>())
|
||||
.Returns(x =>
|
||||
{
|
||||
x[0] = null;
|
||||
return false;
|
||||
});
|
||||
|
||||
await sutProvider.Sut.Record(organization);
|
||||
|
||||
await sutProvider.GetDependency<IPlayItemRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateAsync(Arg.Any<PlayItem>());
|
||||
|
||||
sutProvider.GetDependency<ILogger<PlayItemService>>()
|
||||
.DidNotReceive()
|
||||
.Log(
|
||||
LogLevel.Information,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Any<object>(),
|
||||
Arg.Any<Exception>(),
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,6 @@ public class DatabaseDataAttribute : DataAttribute
|
||||
|
||||
private void AddDapperServices(IServiceCollection services, Database database)
|
||||
{
|
||||
services.AddDapperRepositories(SelfHosted);
|
||||
var globalSettings = new GlobalSettings
|
||||
{
|
||||
DatabaseProvider = "sqlServer",
|
||||
@@ -141,6 +140,7 @@ public class DatabaseDataAttribute : DataAttribute
|
||||
UserRequestExpiration = TimeSpan.FromMinutes(15),
|
||||
}
|
||||
};
|
||||
services.AddDapperRepositories(SelfHosted);
|
||||
services.AddSingleton(globalSettings);
|
||||
services.AddSingleton<IGlobalSettings>(globalSettings);
|
||||
services.AddSingleton(database);
|
||||
@@ -160,7 +160,6 @@ public class DatabaseDataAttribute : DataAttribute
|
||||
private void AddEfServices(IServiceCollection services, Database database)
|
||||
{
|
||||
services.SetupEntityFramework(database.ConnectionString, database.Type);
|
||||
services.AddPasswordManagerEFRepositories(SelfHosted);
|
||||
|
||||
var globalSettings = new GlobalSettings
|
||||
{
|
||||
@@ -169,6 +168,7 @@ public class DatabaseDataAttribute : DataAttribute
|
||||
UserRequestExpiration = TimeSpan.FromMinutes(15),
|
||||
},
|
||||
};
|
||||
services.AddPasswordManagerEFRepositories(SelfHosted);
|
||||
services.AddSingleton(globalSettings);
|
||||
services.AddSingleton<IGlobalSettings>(globalSettings);
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
/// </remarks>
|
||||
public bool ManagesDatabase { get; set; } = true;
|
||||
|
||||
private readonly List<Action<IServiceCollection>> _configureTestServices = new();
|
||||
protected readonly List<Action<IServiceCollection>> _configureTestServices = new();
|
||||
private readonly List<Action<IConfigurationBuilder>> _configureAppConfiguration = new();
|
||||
|
||||
public void SubstituteService<TService>(Action<TService> mockService)
|
||||
|
||||
40
test/SeederApi.IntegrationTest/HttpClientExtensions.cs
Normal file
40
test/SeederApi.IntegrationTest/HttpClientExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
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);
|
||||
}
|
||||
}
|
||||
75
test/SeederApi.IntegrationTest/QueryControllerTest.cs
Normal file
75
test/SeederApi.IntegrationTest/QueryControllerTest.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Net;
|
||||
using Bit.SeederApi.Models.Request;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.SeederApi.IntegrationTest;
|
||||
|
||||
public class QueryControllerTests : IClassFixture<SeederApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly SeederApiApplicationFactory _factory;
|
||||
|
||||
public QueryControllerTests(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.ReadAsStringAsync();
|
||||
|
||||
Assert.NotNull(result);
|
||||
|
||||
var urls = System.Text.Json.JsonSerializer.Deserialize<List<string>>(result);
|
||||
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);
|
||||
}
|
||||
}
|
||||
222
test/SeederApi.IntegrationTest/SeedControllerTest.cs
Normal file
222
test/SeederApi.IntegrationTest/SeedControllerTest.cs
Normal file
@@ -0,0 +1,222 @@
|
||||
using System.Net;
|
||||
using Bit.SeederApi.Models.Request;
|
||||
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_ReturnsOk()
|
||||
{
|
||||
var testEmail = $"seed-test-{Guid.NewGuid()}@bitwarden.com";
|
||||
var playId = Guid.NewGuid().ToString();
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
|
||||
{
|
||||
Template = "SingleUserScene",
|
||||
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
|
||||
}, playId);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<SceneResponseModel>();
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result.MangleMap);
|
||||
Assert.Null(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_WithValidPlayId_ReturnsOk()
|
||||
{
|
||||
var testEmail = $"delete-test-{Guid.NewGuid()}@bitwarden.com";
|
||||
var playId = Guid.NewGuid().ToString();
|
||||
|
||||
var seedResponse = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
|
||||
{
|
||||
Template = "SingleUserScene",
|
||||
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
|
||||
}, playId);
|
||||
|
||||
seedResponse.EnsureSuccessStatusCode();
|
||||
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>();
|
||||
Assert.NotNull(seedResult);
|
||||
|
||||
var deleteResponse = await _client.DeleteAsync($"/seed/{playId}");
|
||||
deleteResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteEndpoint_WithInvalidPlayId_ReturnsOk()
|
||||
{
|
||||
// DestroyRecipe is idempotent - returns null for non-existent play IDs
|
||||
var nonExistentPlayId = Guid.NewGuid().ToString();
|
||||
var response = await _client.DeleteAsync($"/seed/{nonExistentPlayId}");
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Equal($$"""{"playId":"{{nonExistentPlayId}}"}""", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteBatchEndpoint_WithValidPlayIds_ReturnsOk()
|
||||
{
|
||||
// Create multiple seeds with different play IDs
|
||||
var playIds = new List<string>();
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var playId = Guid.NewGuid().ToString();
|
||||
playIds.Add(playId);
|
||||
|
||||
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 })
|
||||
}, playId);
|
||||
|
||||
seedResponse.EnsureSuccessStatusCode();
|
||||
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>();
|
||||
Assert.NotNull(seedResult);
|
||||
}
|
||||
|
||||
// Delete them in batch
|
||||
var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch")
|
||||
{
|
||||
Content = JsonContent.Create(playIds)
|
||||
};
|
||||
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 with a play ID
|
||||
var validPlayId = Guid.NewGuid().ToString();
|
||||
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 })
|
||||
}, validPlayId);
|
||||
|
||||
seedResponse.EnsureSuccessStatusCode();
|
||||
var seedResult = await seedResponse.Content.ReadFromJsonAsync<SceneResponseModel>();
|
||||
Assert.NotNull(seedResult);
|
||||
|
||||
// Try to delete with mix of valid and invalid IDs
|
||||
var playIds = new List<string> { validPlayId, Guid.NewGuid().ToString(), Guid.NewGuid().ToString() };
|
||||
var request = new HttpRequestMessage(HttpMethod.Delete, "/seed/batch")
|
||||
{
|
||||
Content = JsonContent.Create(playIds)
|
||||
};
|
||||
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 playId = Guid.NewGuid().ToString();
|
||||
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 })
|
||||
}, playId);
|
||||
|
||||
seedResponse.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// Delete all
|
||||
var deleteResponse = await _client.DeleteAsync("/seed");
|
||||
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SeedEndpoint_VerifyResponseContainsMangleMapAndResult()
|
||||
{
|
||||
var testEmail = $"verify-response-{Guid.NewGuid()}@bitwarden.com";
|
||||
var playId = Guid.NewGuid().ToString();
|
||||
|
||||
var response = await _client.PostAsJsonAsync("/seed", new SeedRequestModel
|
||||
{
|
||||
Template = "SingleUserScene",
|
||||
Arguments = System.Text.Json.JsonSerializer.SerializeToElement(new { email = testEmail })
|
||||
}, playId);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
var jsonString = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Verify the response contains MangleMap and Result fields
|
||||
Assert.Contains("mangleMap", jsonString, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("result", jsonString, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private class BatchDeleteResponse
|
||||
{
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,18 @@
|
||||
using Bit.Core.Services;
|
||||
using Bit.IntegrationTestCommon;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
|
||||
namespace Bit.SeederApi.IntegrationTest;
|
||||
|
||||
public class SeederApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
||||
{
|
||||
public SeederApiApplicationFactory()
|
||||
{
|
||||
TestDatabase = new SqliteTestDatabase();
|
||||
_configureTestServices.Add(serviceCollection =>
|
||||
{
|
||||
serviceCollection.AddSingleton<IPlayIdService, NeverPlayIdServices>();
|
||||
serviceCollection.AddHttpContextAccessor();
|
||||
});
|
||||
}
|
||||
}
|
||||
102
test/SharedWeb.Test/PlayIdMiddlewareTests.cs
Normal file
102
test/SharedWeb.Test/PlayIdMiddlewareTests.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using Bit.Core.Services;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NSubstitute;
|
||||
|
||||
namespace SharedWeb.Test;
|
||||
|
||||
public class PlayIdMiddlewareTests
|
||||
{
|
||||
private readonly PlayIdService _playIdService;
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly PlayIdMiddleware _middleware;
|
||||
|
||||
public PlayIdMiddlewareTests()
|
||||
{
|
||||
var hostEnvironment = Substitute.For<IHostEnvironment>();
|
||||
hostEnvironment.EnvironmentName.Returns(Environments.Development);
|
||||
|
||||
_playIdService = new PlayIdService(hostEnvironment);
|
||||
_next = Substitute.For<RequestDelegate>();
|
||||
_middleware = new PlayIdMiddleware(_next);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_WithValidPlayId_SetsPlayIdAndCallsNext()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Headers["x-play-id"] = "test-play-id";
|
||||
|
||||
await _middleware.Invoke(context, _playIdService);
|
||||
|
||||
Assert.Equal("test-play-id", _playIdService.PlayId);
|
||||
await _next.Received(1).Invoke(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_WithoutPlayIdHeader_CallsNext()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
|
||||
await _middleware.Invoke(context, _playIdService);
|
||||
|
||||
Assert.Null(_playIdService.PlayId);
|
||||
await _next.Received(1).Invoke(context);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("\t")]
|
||||
public async Task Invoke_WithEmptyOrWhitespacePlayId_Returns400(string playId)
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Response.Body = new MemoryStream();
|
||||
context.Request.Headers["x-play-id"] = playId;
|
||||
|
||||
await _middleware.Invoke(context, _playIdService);
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
await _next.DidNotReceive().Invoke(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_WithPlayIdExceedingMaxLength_Returns400()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Response.Body = new MemoryStream();
|
||||
var longPlayId = new string('a', 257); // Exceeds 256 character limit
|
||||
context.Request.Headers["x-play-id"] = longPlayId;
|
||||
|
||||
await _middleware.Invoke(context, _playIdService);
|
||||
|
||||
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
|
||||
await _next.DidNotReceive().Invoke(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_WithPlayIdAtMaxLength_SetsPlayIdAndCallsNext()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
var maxLengthPlayId = new string('a', 256); // Exactly 256 characters
|
||||
context.Request.Headers["x-play-id"] = maxLengthPlayId;
|
||||
|
||||
await _middleware.Invoke(context, _playIdService);
|
||||
|
||||
Assert.Equal(maxLengthPlayId, _playIdService.PlayId);
|
||||
await _next.Received(1).Invoke(context);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invoke_WithSpecialCharactersInPlayId_SetsPlayIdAndCallsNext()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Headers["x-play-id"] = "test-play_id.123";
|
||||
|
||||
await _middleware.Invoke(context, _playIdService);
|
||||
|
||||
Assert.Equal("test-play_id.123", _playIdService.PlayId);
|
||||
await _next.Received(1).Invoke(context);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user