using System.Net; using System.Net.Http.Json; using HrynCo.NotificationService.DAL.Abstract.PushChannels; using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Templates; using Microsoft.Extensions.DependencyInjection; namespace HrynCo.NotificationService.Web.IntegrationTests; public sealed class PushChannelAdminTests(TemplateDatabaseFixture fixture) : IClassFixture { [Fact] public async Task TestsRequireAntiforgeryAndRejectMissingCredentialsWithoutSaving() { await using var context = await DefinitionApiTestContext.CreateAsync(fixture); using var client = context.App.CreateClient(); var html = await client.GetStringAsync("/admin/push-channels/create"); Assert.Contains("Check Firebase authorization", html); Assert.Contains("Recipient token", html); Assert.Contains("test-authorization", html); var fields = TemplateEditorTestFields.Read(html); foreach (var action in new[] { "test-authorization", "test-send" }) { var body = new { firebaseProjectId = "test-project", recipientToken = "test-token" }; Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsJsonAsync("/admin/push-channels/" + action, body)).StatusCode); using var request = new HttpRequestMessage(HttpMethod.Post, "/admin/push-channels/" + action); request.Headers.Add("RequestVerificationToken", fields["__RequestVerificationToken"]); request.Content = JsonContent.Create(body); var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = await response.Content.ReadFromJsonAsync(); Assert.False(result.GetProperty("success").GetBoolean()); Assert.DoesNotContain("test-token", await response.Content.ReadAsStringAsync()); } using var scope = context.App.Services.CreateScope(); Assert.Empty(await scope.ServiceProvider.GetRequiredService().GetAllAsync()); } [Fact] public async Task CreateAndEditKeepCredentialsOnServerAndAllowServiceReassignment() { await using var context = await DefinitionApiTestContext.CreateAsync(fixture); using var client = context.App.CreateClient(new() { AllowAutoRedirect = false }); var credentials = PushChannelTestCredentials.Create(); var response = await SaveNewAsync(client, context.ServiceId, credentials); Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); using var scope = context.App.Services.CreateScope(); var channels = scope.ServiceProvider.GetRequiredService(); var channel = Assert.Single(await channels.GetAllAsync()); var other = new NotificationServiceInfo { Name = "Invemory-Push-Test" }; await scope.ServiceProvider.GetRequiredService().SaveAsync(other, true); var html = await client.GetStringAsync($"/admin/push-channels/{channel.Id}"); Assert.DoesNotContain("PRIVATE KEY", html); Assert.DoesNotContain("test@example.invalid", html); Assert.Contains("Credentials are saved", html); var fields = TemplateEditorTestFields.Read(html); fields["ServiceId"] = other.Id.ToString(); fields["CredentialsJson"] = ""; fields["IsActive"] = "true"; Assert.Equal(HttpStatusCode.Redirect, (await client.PostAsync("/admin/push-channels/save", new FormUrlEncodedContent(fields))).StatusCode); var saved = (await channels.GetByIdAsync(channel.Id))!; Assert.Equal(other.Id, saved.ServiceId); Assert.Equal("Invemory-Push-Test", saved.ServiceName); Assert.True(System.Text.Json.Nodes.JsonNode.DeepEquals( System.Text.Json.Nodes.JsonNode.Parse(credentials), System.Text.Json.Nodes.JsonNode.Parse((await channels.GetCredentialsAsync(channel.Id))!))); Assert.Null(await channels.GetActiveForServiceAsync(context.ServiceId)); Assert.NotNull(await channels.GetActiveForServiceAsync(other.Id)); } [Theory] [InlineData("duplicate")] [InlineData("missingService")] [InlineData("emptyService")] [InlineData("invalidJson")] [InlineData("missingCredentials")] public async Task InvalidCreateDoesNotExposeSubmittedSecretOrPersistAChannel(string scenario) { await using var context = await DefinitionApiTestContext.CreateAsync(fixture); using var client = context.App.CreateClient(new() { AllowAutoRedirect = false }); var credentials = PushChannelTestCredentials.Create(); if (scenario == "duplicate") Assert.Equal(HttpStatusCode.Redirect, (await SaveNewAsync(client, context.ServiceId, credentials)).StatusCode); var serviceId = scenario switch { "missingService" => Guid.NewGuid(), "emptyService" => Guid.Empty, _ => context.ServiceId }; var secret = scenario switch { "invalidJson" => "invalid-secret-must-not-render", "missingCredentials" => "", _ => credentials }; var response = await SaveNewAsync(client, serviceId, secret); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var html = await response.Content.ReadAsStringAsync(); Assert.DoesNotContain("PRIVATE KEY", html); Assert.DoesNotContain("invalid-secret-must-not-render", html); Assert.Contains("TestService", html); using var scope = context.App.Services.CreateScope(); var channels = await scope.ServiceProvider.GetRequiredService().GetAllAsync(); Assert.Equal(scenario == "duplicate" ? 1 : 0, channels.Count); } [Fact] public async Task BlankCredentialsCannotChangeProjectAndValidationErrorsDoNotEchoSecrets() { await using var context = await DefinitionApiTestContext.CreateAsync(fixture); using var client = context.App.CreateClient(new() { AllowAutoRedirect = false }); var credentials = PushChannelTestCredentials.Create(); await SaveNewAsync(client, context.ServiceId, credentials); using var scope = context.App.Services.CreateScope(); var channels = scope.ServiceProvider.GetRequiredService(); var channel = Assert.Single(await channels.GetAllAsync()); var fields = TemplateEditorTestFields.Read(await client.GetStringAsync($"/admin/push-channels/{channel.Id}")); fields["ServiceId"] = context.ServiceId.ToString(); fields["FirebaseProjectId"] = "other-project"; fields["CredentialsJson"] = ""; var response = await client.PostAsync("/admin/push-channels/save", new FormUrlEncodedContent(fields)); Assert.Contains("Changing project requires new credentials", await response.Content.ReadAsStringAsync()); Assert.Equal("test-project", (await channels.GetByIdAsync(channel.Id))!.FirebaseProjectId); fields["FirebaseProjectId"] = ""; fields["CredentialsJson"] = credentials; response = await client.PostAsync("/admin/push-channels/save", new FormUrlEncodedContent(fields)); Assert.DoesNotContain("PRIVATE KEY", await response.Content.ReadAsStringAsync()); Assert.True(System.Text.Json.Nodes.JsonNode.DeepEquals( System.Text.Json.Nodes.JsonNode.Parse(credentials), System.Text.Json.Nodes.JsonNode.Parse((await channels.GetCredentialsAsync(channel.Id))!))); } [Fact] public async Task RenameUpdatesDisplayAndInactiveChannelBlocksServiceDeletion() { await using var context = await DefinitionApiTestContext.CreateAsync(fixture); using var client = context.App.CreateClient(new() { AllowAutoRedirect = false }); var credentials = PushChannelTestCredentials.Create(); await SaveNewAsync(client, context.ServiceId, credentials); using var scope = context.App.Services.CreateScope(); var channels = scope.ServiceProvider.GetRequiredService(); var channel = Assert.Single(await channels.GetAllAsync()); channel.IsActive = false; await channels.SaveAsync(channel, credentials, false); Assert.Null(await channels.GetActiveForServiceAsync(context.ServiceId)); var services = scope.ServiceProvider.GetRequiredService(); var service = (await services.GetAsync(context.ServiceId))!; service.Name = "Invemory-Renamed-Push"; await services.SaveAsync(service, false); Assert.Contains(service.Name, await client.GetStringAsync("/admin/push-channels")); await Assert.ThrowsAsync(() => services.DeleteAsync(service.Id, service.Revision)); } private static async Task SaveNewAsync(HttpClient client, Guid serviceId, string credentials) { var fields = TemplateEditorTestFields.Read(await client.GetStringAsync("/admin/push-channels/create")); fields["ServiceId"] = serviceId.ToString(); fields["FirebaseProjectId"] = "test-project"; fields["CredentialsJson"] = credentials; fields["IsActive"] = "true"; return await client.PostAsync("/admin/push-channels/save", new FormUrlEncodedContent(fields)); } }