diff --git a/src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs index 1207d839d8..c535cd3e84 100644 --- a/src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/NotificationCenter/Configurations/NotificationStatusEntityTypeConfiguration.cs @@ -13,6 +13,12 @@ public class NotificationStatusEntityTypeConfiguration : IEntityTypeConfiguratio .HasKey(ns => new { ns.UserId, ns.NotificationId }) .IsClustered(); + builder + .HasOne(ns => ns.Notification) + .WithMany() + .HasForeignKey(ns => ns.NotificationId) + .OnDelete(DeleteBehavior.Cascade); + builder.ToTable(nameof(NotificationStatus)); } } diff --git a/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql b/src/Sql/NotificationCenter/dbo/Stored Procedures/Notification_MarkAsDeletedByTask.sql new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Sql/dbo/Tables/NotificationStatus.sql b/src/Sql/dbo/Tables/NotificationStatus.sql index 2f68e2b2f7..2ccb0e0a8a 100644 --- a/src/Sql/dbo/Tables/NotificationStatus.sql +++ b/src/Sql/dbo/Tables/NotificationStatus.sql @@ -5,11 +5,10 @@ CREATE TABLE [dbo].[NotificationStatus] [ReadDate] DATETIME2 (7) NULL, [DeletedDate] DATETIME2 (7) NULL, CONSTRAINT [PK_NotificationStatus] PRIMARY KEY CLUSTERED ([NotificationId] ASC, [UserId] ASC), - CONSTRAINT [FK_NotificationStatus_Notification] FOREIGN KEY ([NotificationId]) REFERENCES [dbo].[Notification] ([Id]), + CONSTRAINT [FK_NotificationStatus_Notification] FOREIGN KEY ([NotificationId]) REFERENCES [dbo].[Notification] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_NotificationStatus_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ); GO CREATE NONCLUSTERED INDEX [IX_NotificationStatus_UserId] ON [dbo].[NotificationStatus]([UserId] ASC); - diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index 288752011f..0a186e43be 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -5,6 +5,8 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Enums; @@ -976,8 +978,11 @@ public class CipherRepositoryTests [DatabaseTheory, DatabaseData] public async Task DeleteCipherWithSecurityTaskAsync_Works( IOrganizationRepository organizationRepository, + IUserRepository userRepository, ICipherRepository cipherRepository, - ISecurityTaskRepository securityTaskRepository) + ISecurityTaskRepository securityTaskRepository, + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository) { var organization = await organizationRepository.CreateAsync(new Organization { @@ -987,6 +992,14 @@ public class CipherRepositoryTests BillingEmail = "" }); + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; await cipherRepository.CreateAsync(cipher1); @@ -1012,6 +1025,20 @@ public class CipherRepositoryTests }; await securityTaskRepository.CreateManyAsync(tasks); + var notification = await notificationRepository.CreateAsync(new Notification + { + OrganizationId = organization.Id, + UserId = user.Id, + TaskId = tasks[1].Id, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + }); + await notificationStatusRepository.CreateAsync(new NotificationStatus + { + NotificationId = notification.Id, + UserId = user.Id, + ReadDate = DateTime.UtcNow, + }); // Delete cipher with pending security task await cipherRepository.DeleteAsync(cipher1); diff --git a/util/Migrator/DbScripts/2025-06-26_01_CascadeDeleteNotificationStatus.sql b/util/Migrator/DbScripts/2025-06-26_01_CascadeDeleteNotificationStatus.sql new file mode 100644 index 0000000000..474ac14a7a --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-26_01_CascadeDeleteNotificationStatus.sql @@ -0,0 +1,10 @@ +BEGIN + IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_NotificationStatus_Notification') + BEGIN + ALTER TABLE [dbo].[NotificationStatus] DROP CONSTRAINT [FK_NotificationStatus_Notification] + END + + ALTER TABLE [dbo].[NotificationStatus] + ADD CONSTRAINT [FK_NotificationStatus_Notification] FOREIGN KEY ([NotificationId]) REFERENCES [dbo].[Notification]([Id]) ON DELETE CASCADE +END +GO