8e81572386
Add revision-safe service and template APIs, channel mapping, and validated admin controls. Ref: IT-1601
154 lines
10 KiB
C#
154 lines
10 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using HrynCo.NotificationService.DAL.Abstract.Providers;
|
|
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
|
using HrynCo.NotificationService.DAL.EF;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace HrynCo.NotificationService.Web.IntegrationTests;
|
|
|
|
public sealed class ServiceDeletionApiTests(TemplateDatabaseFixture fixture) : IClassFixture<TemplateDatabaseFixture>
|
|
{
|
|
[Fact]
|
|
public async Task ListsExposeRevisions_AndDefinitionsAreServiceScopedIncludingEmptyDefinitions()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null), "Empty");
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en"), "Populated");
|
|
using var scope = context.App.Services.CreateScope();
|
|
var other = new NotificationServiceInfo { Name = "OtherService" };
|
|
await scope.ServiceProvider.GetRequiredService<INotificationServiceRepository>().SaveAsync(other, true);
|
|
var services = (await context.Client.GetFromJsonAsync<JsonElement>("/api/v1/services/all")).GetProperty("data");
|
|
Assert.Equal(2, services.GetArrayLength());
|
|
Assert.All(services.EnumerateArray(), s => Assert.NotEqual(Guid.Empty, s.GetProperty("revision").GetGuid()));
|
|
var definitions = (await context.Client.GetFromJsonAsync<JsonElement>(context.ServiceRoot + "/email-template-definitions/all")).GetProperty("data");
|
|
Assert.Equal(new[] { "Empty", "Populated" }, definitions.EnumerateArray().Select(d => d.GetProperty("key").GetString()));
|
|
Assert.Empty(definitions[0].GetProperty("translations").EnumerateArray());
|
|
Assert.Single(definitions[1].GetProperty("translations").EnumerateArray());
|
|
Assert.All(definitions.EnumerateArray(), d => Assert.Equal(context.ServiceId, d.GetProperty("serviceId").GetGuid()));
|
|
var otherDefinitions = await context.Client.GetFromJsonAsync<JsonElement>($"/api/v1/services/{other.Id}/email-template-definitions/all");
|
|
Assert.Empty(otherDefinitions.GetProperty("data").EnumerateArray());
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.GetAsync($"/api/v1/services/{Guid.NewGuid()}/email-template-definitions/all")).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExplicitCleanupDeletesDefinitionsThenServiceAndLanguages_ButRetainsChannels()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await SeedChannelAsync(context);
|
|
const string key = "Delete / %2F Привіт";
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en", "uk"), key);
|
|
var definition = await context.ReadAsync(key);
|
|
var service = await ReadServiceAsync(context);
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.DeleteAsync(context.ServiceRoot + "?revision=" + service.GetProperty("revision").GetGuid())).StatusCode);
|
|
var deleted = await context.Client.DeleteAsync(context.DefinitionUrl(key) + "&revision=" + definition.GetProperty("revision").GetGuid());
|
|
Assert.Equal(HttpStatusCode.OK, deleted.StatusCode);
|
|
var result = await deleted.Content.ReadFromJsonAsync<JsonElement>();
|
|
Assert.True(result.GetProperty("success").GetBoolean());
|
|
Assert.Equal(JsonValueKind.Object, result.GetProperty("data").ValueKind);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.GetAsync(context.DefinitionUrl(key))).StatusCode);
|
|
var afterDefinitionDeletion = await ReadServiceAsync(context);
|
|
Assert.Equal(service.GetProperty("revision").GetGuid(), afterDefinitionDeletion.GetProperty("revision").GetGuid());
|
|
Assert.Equal(service.GetProperty("supportedLanguageCodes").GetRawText(), afterDefinitionDeletion.GetProperty("supportedLanguageCodes").GetRawText());
|
|
Assert.All(afterDefinitionDeletion.GetProperty("languages").EnumerateArray(), l => Assert.Equal(0, l.GetProperty("versionCount").GetInt32()));
|
|
Assert.Equal(HttpStatusCode.OK, (await context.Client.DeleteAsync(context.ServiceRoot + "?revision=" + service.GetProperty("revision").GetGuid())).StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.GetAsync(context.ServiceRoot)).StatusCode);
|
|
var all = await context.Client.GetFromJsonAsync<JsonElement>("/api/v1/services/all");
|
|
Assert.Empty(all.GetProperty("data").EnumerateArray());
|
|
using var scope = context.App.Services.CreateScope();
|
|
Assert.Single(await scope.ServiceProvider.GetRequiredService<IEmailChannelRepository>().GetAllAsync());
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.DeleteAsync(context.ServiceRoot + "?revision=" + service.GetProperty("revision").GetGuid())).StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, (await context.Client.DeleteAsync(context.DefinitionUrl(key) + "&revision=" + definition.GetProperty("revision").GetGuid())).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteIsRevisionGuarded_AndEvenEmptyDefinitionsBlockServiceRemoval()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null));
|
|
var definition = await context.ReadAsync();
|
|
var service = await ReadServiceAsync(context);
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.DeleteAsync(context.ServiceRoot + "?revision=" + service.GetProperty("revision").GetGuid())).StatusCode);
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.DeleteAsync(context.DefinitionUrl() + "&revision=" + Guid.NewGuid())).StatusCode);
|
|
using var scope = context.App.Services.CreateScope();
|
|
var repository = scope.ServiceProvider.GetRequiredService<INotificationServiceRepository>();
|
|
var edit = (await repository.GetAsync(context.ServiceId))!;
|
|
edit.Name = "RenamedService";
|
|
await repository.SaveAsync(edit, false);
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.DeleteAsync(context.ServiceRoot + "?revision=" + service.GetProperty("revision").GetGuid())).StatusCode);
|
|
Assert.Equal(HttpStatusCode.Conflict, (await context.Client.DeleteAsync(context.DefinitionUrl() + "&revision=" + definition.GetProperty("revision").GetGuid())).StatusCode);
|
|
Assert.Equal("RenamedService", (await ReadServiceAsync(context)).GetProperty("name").GetString());
|
|
Assert.Empty((await context.ReadAsync()).GetProperty("translations").EnumerateArray());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData("?revision=not-a-guid")]
|
|
[InlineData("?revision=00000000-0000-0000-0000-000000000000")]
|
|
public async Task InvalidServiceDeleteDoesNotRemoveAnything(string query)
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
var before = await ReadServiceAsync(context);
|
|
var response = await context.Client.DeleteAsync(context.ServiceRoot + query);
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
Assert.Equal("InvalidRequest", (await response.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("error").GetProperty("code").GetString());
|
|
Assert.Equal(before.GetRawText(), (await ReadServiceAsync(context)).GetRawText());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("?key=Welcome")]
|
|
[InlineData("?key=Welcome&revision=not-a-guid")]
|
|
[InlineData("?key=Welcome&revision=00000000-0000-0000-0000-000000000000")]
|
|
[InlineData("?revision=11111111-1111-1111-1111-111111111111")]
|
|
public async Task InvalidDefinitionDeleteDoesNotRemoveAnything(string query)
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en"));
|
|
var before = await context.ReadAsync();
|
|
var response = await context.Client.DeleteAsync(context.ServiceRoot + "/email-template-definitions" + query);
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
Assert.Equal("InvalidRequest", (await response.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("error").GetProperty("code").GetString());
|
|
Assert.Equal(before.GetRawText(), (await context.ReadAsync()).GetRawText());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConcurrentDefinitionUpdateAndDeleteHaveOneWinner()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
await context.UpsertAsync(DefinitionApiTestContext.Body(null, "en"));
|
|
var revision = (await context.ReadAsync()).GetProperty("revision").GetGuid();
|
|
var responses = await Task.WhenAll(context.UpsertAsync(DefinitionApiTestContext.Body(revision, "uk")), context.Client.DeleteAsync(context.DefinitionUrl() + "&revision=" + revision));
|
|
Assert.Equal(new[] { HttpStatusCode.OK, HttpStatusCode.Conflict }, responses.Select(r => r.StatusCode).Order().ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ConcurrentServiceDeleteAndDefinitionCreationCannotLeaveOrphans()
|
|
{
|
|
await using var context = await DefinitionApiTestContext.CreateAsync(fixture);
|
|
var service = await ReadServiceAsync(context);
|
|
var deletion = context.Client.DeleteAsync(context.ServiceRoot + "?revision=" + service.GetProperty("revision").GetGuid());
|
|
var creation = context.UpsertAsync(DefinitionApiTestContext.Body(null, "en"));
|
|
var responses = await Task.WhenAll(deletion, creation);
|
|
var deleted = responses[0].StatusCode;
|
|
var created = responses[1].StatusCode;
|
|
Assert.True(deleted == HttpStatusCode.OK && created == HttpStatusCode.NotFound
|
|
|| deleted == HttpStatusCode.Conflict && created == HttpStatusCode.Created);
|
|
}
|
|
|
|
private static async Task<JsonElement> ReadServiceAsync(DefinitionApiTestContext context) =>
|
|
(await context.Client.GetFromJsonAsync<JsonElement>(context.ServiceRoot)).GetProperty("data");
|
|
|
|
private static async Task SeedChannelAsync(DefinitionApiTestContext context)
|
|
{
|
|
using var scope = context.App.Services.CreateScope();
|
|
await scope.ServiceProvider.GetRequiredService<IEmailChannelRepository>().AddAsync(new EmailChannel
|
|
{
|
|
ServiceName = "TestService", EmailChannelType = EmailChannelType.Smtp,
|
|
Settings = new SmtpChannelSettings { Host = "localhost", FromEmail = "test@example.invalid" }
|
|
});
|
|
await scope.ServiceProvider.GetRequiredService<NotificationDbContext>().SaveChangesAsync();
|
|
}
|
|
}
|