From f3f81deb9883792c5f6213bde7a0ff9420f70d53 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Wed, 2 Oct 2024 19:23:19 +0200 Subject: [PATCH] [PM-11123] Service layer for Notification Center (#4741) * PM-11123: Service layer * PM-11123: Service layer for Notification Center * PM-11123: Throw error on unsupported requirement * PM-11123: Missing await * PM-11123: Cleanup * PM-11123: Unit Test coverage * PM-11123: Flipping the authorization logic to be exact match of fail, formatting * PM-11123: Async warning * PM-11123: Using AuthorizeOrThrowAsync, removal of redundant set new id * PM-11123: UT typo * PM-11123: UT fix --- .../NotificationAuthorizationHandler.cs | 68 +++ .../Authorization/NotificationOperations.cs | 19 + .../NotificationStatusAuthorizationHandler.cs | 57 +++ .../NotificationStatusOperations.cs | 19 + .../Commands/CreateNotificationCommand.cs | 36 ++ .../CreateNotificationStatusCommand.cs | 47 ++ .../Interfaces/ICreateNotificationCommand.cs | 9 + .../ICreateNotificationStatusCommand.cs | 9 + .../IMarkNotificationDeletedCommand.cs | 7 + .../IMarkNotificationReadCommand.cs | 7 + .../Interfaces/IUpdateNotificationCommand.cs | 9 + .../MarkNotificationDeletedCommand.cs | 74 ++++ .../Commands/MarkNotificationReadCommand.cs | 74 ++++ .../Commands/UpdateNotificationCommand.cs | 47 ++ .../GetNotificationStatusForUserQuery.cs | 47 ++ .../Queries/GetNotificationsForUserQuery.cs | 37 ++ .../IGetNotificationStatusForUserQuery.cs | 9 + .../IGetNotificationsForUserQuery.cs | 10 + .../NotificationAuthorizationHandlerTest.cs | 419 ++++++++++++++++++ ...ificationStatusAuthorizationHandlerTest.cs | 179 ++++++++ .../AutoFixture/NotificationFixtures.cs | 31 ++ .../AutoFixture/NotificationStatusFixtures.cs | 19 + .../Commands/CreateNotificationCommandTest.cs | 59 +++ .../CreateNotificationStatusCommandTest.cs | 89 ++++ .../MarkNotificationDeletedCommandTest.cs | 151 +++++++ .../MarkNotificationReadCommandTest.cs | 151 +++++++ .../Commands/UpdateNotificationCommandTest.cs | 95 ++++ .../GetNotificationStatusForUserTest.cs | 85 ++++ .../GetNotificationsForUserQueryTest.cs | 55 +++ 29 files changed, 1918 insertions(+) create mode 100644 src/Core/NotificationCenter/Authorization/NotificationAuthorizationHandler.cs create mode 100644 src/Core/NotificationCenter/Authorization/NotificationOperations.cs create mode 100644 src/Core/NotificationCenter/Authorization/NotificationStatusAuthorizationHandler.cs create mode 100644 src/Core/NotificationCenter/Authorization/NotificationStatusOperations.cs create mode 100644 src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationStatusCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationDeletedCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationReadCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/Interfaces/IUpdateNotificationCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs create mode 100644 src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs create mode 100644 src/Core/NotificationCenter/Queries/GetNotificationStatusForUserQuery.cs create mode 100644 src/Core/NotificationCenter/Queries/GetNotificationsForUserQuery.cs create mode 100644 src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusForUserQuery.cs create mode 100644 src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationsForUserQuery.cs create mode 100644 test/Core.Test/NotificationCenter/Authorization/NotificationAuthorizationHandlerTest.cs create mode 100644 test/Core.Test/NotificationCenter/Authorization/NotificationStatusAuthorizationHandlerTest.cs create mode 100644 test/Core.Test/NotificationCenter/AutoFixture/NotificationFixtures.cs create mode 100644 test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusFixtures.cs create mode 100644 test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs create mode 100644 test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs create mode 100644 test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs create mode 100644 test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs create mode 100644 test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs create mode 100644 test/Core.Test/NotificationCenter/Queries/GetNotificationStatusForUserTest.cs create mode 100644 test/Core.Test/NotificationCenter/Queries/GetNotificationsForUserQueryTest.cs diff --git a/src/Core/NotificationCenter/Authorization/NotificationAuthorizationHandler.cs b/src/Core/NotificationCenter/Authorization/NotificationAuthorizationHandler.cs new file mode 100644 index 000000000..6e06c92db --- /dev/null +++ b/src/Core/NotificationCenter/Authorization/NotificationAuthorizationHandler.cs @@ -0,0 +1,68 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Authorization; + +public class NotificationAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + + public NotificationAuthorizationHandler(ICurrentContext currentContext) + { + _currentContext = currentContext; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + NotificationOperationsRequirement requirement, + Notification notification) + { + if (!_currentContext.UserId.HasValue) + { + return; + } + + var authorized = requirement switch + { + not null when requirement == NotificationOperations.Read => CanRead(notification), + not null when requirement == NotificationOperations.Create => await CanCreate(notification), + not null when requirement == NotificationOperations.Update => await CanUpdate(notification), + _ => throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement)) + }; + + if (authorized) + { + context.Succeed(requirement); + } + } + + private bool CanRead(Notification notification) + { + var userMatching = !notification.UserId.HasValue || notification.UserId.Value == _currentContext.UserId!.Value; + var organizationMatching = !notification.OrganizationId.HasValue || + _currentContext.GetOrganization(notification.OrganizationId.Value) != null; + + return notification.Global || (userMatching && organizationMatching); + } + + private async Task CanCreate(Notification notification) + { + var organizationPermissionsMatching = !notification.OrganizationId.HasValue || + await _currentContext.AccessReports(notification.OrganizationId.Value); + var userNoOrganizationMatching = !notification.UserId.HasValue || notification.OrganizationId.HasValue || + notification.UserId.Value == _currentContext.UserId!.Value; + + return !notification.Global && organizationPermissionsMatching && userNoOrganizationMatching; + } + + private async Task CanUpdate(Notification notification) + { + var organizationPermissionsMatching = !notification.OrganizationId.HasValue || + await _currentContext.AccessReports(notification.OrganizationId.Value); + var userNoOrganizationMatching = !notification.UserId.HasValue || notification.OrganizationId.HasValue || + notification.UserId.Value == _currentContext.UserId!.Value; + + return !notification.Global && organizationPermissionsMatching && userNoOrganizationMatching; + } +} diff --git a/src/Core/NotificationCenter/Authorization/NotificationOperations.cs b/src/Core/NotificationCenter/Authorization/NotificationOperations.cs new file mode 100644 index 000000000..5a67805b4 --- /dev/null +++ b/src/Core/NotificationCenter/Authorization/NotificationOperations.cs @@ -0,0 +1,19 @@ +#nullable enable +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.NotificationCenter.Authorization; + +public class NotificationOperationsRequirement : OperationAuthorizationRequirement +{ + public NotificationOperationsRequirement(string name) + { + Name = name; + } +} + +public static class NotificationOperations +{ + public static readonly NotificationOperationsRequirement Read = new(nameof(Read)); + public static readonly NotificationOperationsRequirement Create = new(nameof(Create)); + public static readonly NotificationOperationsRequirement Update = new(nameof(Update)); +} diff --git a/src/Core/NotificationCenter/Authorization/NotificationStatusAuthorizationHandler.cs b/src/Core/NotificationCenter/Authorization/NotificationStatusAuthorizationHandler.cs new file mode 100644 index 000000000..df018c081 --- /dev/null +++ b/src/Core/NotificationCenter/Authorization/NotificationStatusAuthorizationHandler.cs @@ -0,0 +1,57 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Authorization; + +public class NotificationStatusAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + + public NotificationStatusAuthorizationHandler(ICurrentContext currentContext) + { + _currentContext = currentContext; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + NotificationStatusOperationsRequirement requirement, + NotificationStatus notificationStatus) + { + if (!_currentContext.UserId.HasValue) + { + return Task.CompletedTask; + } + + var authorized = requirement switch + { + not null when requirement == NotificationStatusOperations.Read => CanRead(notificationStatus), + not null when requirement == NotificationStatusOperations.Create => CanCreate(notificationStatus), + not null when requirement == NotificationStatusOperations.Update => CanUpdate(notificationStatus), + _ => throw new ArgumentException("Unsupported operation requirement type provided.", nameof(requirement)) + }; + + if (authorized) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + + private bool CanRead(NotificationStatus notificationStatus) + { + return notificationStatus.UserId == _currentContext.UserId!.Value; + } + + private bool CanCreate(NotificationStatus notificationStatus) + { + return notificationStatus.UserId == _currentContext.UserId!.Value; + } + + private bool CanUpdate(NotificationStatus notificationStatus) + { + return notificationStatus.UserId == _currentContext.UserId!.Value; + } +} diff --git a/src/Core/NotificationCenter/Authorization/NotificationStatusOperations.cs b/src/Core/NotificationCenter/Authorization/NotificationStatusOperations.cs new file mode 100644 index 000000000..e097d67cb --- /dev/null +++ b/src/Core/NotificationCenter/Authorization/NotificationStatusOperations.cs @@ -0,0 +1,19 @@ +#nullable enable +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.NotificationCenter.Authorization; + +public class NotificationStatusOperationsRequirement : OperationAuthorizationRequirement +{ + public NotificationStatusOperationsRequirement(string name) + { + Name = name; + } +} + +public static class NotificationStatusOperations +{ + public static readonly NotificationStatusOperationsRequirement Read = new(nameof(Read)); + public static readonly NotificationStatusOperationsRequirement Create = new(nameof(Create)); + public static readonly NotificationStatusOperationsRequirement Update = new(nameof(Update)); +} diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs new file mode 100644 index 000000000..4f76950a3 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/CreateNotificationCommand.cs @@ -0,0 +1,36 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class CreateNotificationCommand : ICreateNotificationCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + + public CreateNotificationCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + } + + public async Task CreateAsync(Notification notification) + { + notification.CreationDate = notification.RevisionDate = DateTime.UtcNow; + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, + NotificationOperations.Create); + + return await _notificationRepository.CreateAsync(notification); + } +} diff --git a/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs b/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs new file mode 100644 index 000000000..fcd61ceeb --- /dev/null +++ b/src/Core/NotificationCenter/Commands/CreateNotificationStatusCommand.cs @@ -0,0 +1,47 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class CreateNotificationStatusCommand : ICreateNotificationStatusCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + private readonly INotificationStatusRepository _notificationStatusRepository; + + public CreateNotificationStatusCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + _notificationStatusRepository = notificationStatusRepository; + } + + public async Task CreateAsync(NotificationStatus notificationStatus) + { + var notification = await _notificationRepository.GetByIdAsync(notificationStatus.NotificationId); + if (notification == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, + NotificationOperations.Read); + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, + NotificationStatusOperations.Create); + + return await _notificationStatusRepository.CreateAsync(notificationStatus); + } +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs new file mode 100644 index 000000000..a3b4d894e --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationCommand.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; + +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface ICreateNotificationCommand +{ + Task CreateAsync(Notification notification); +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationStatusCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationStatusCommand.cs new file mode 100644 index 000000000..ea9695e2e --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/ICreateNotificationStatusCommand.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; + +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface ICreateNotificationStatusCommand +{ + Task CreateAsync(NotificationStatus notificationStatus); +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationDeletedCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationDeletedCommand.cs new file mode 100644 index 000000000..39bc0735c --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationDeletedCommand.cs @@ -0,0 +1,7 @@ +#nullable enable +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface IMarkNotificationDeletedCommand +{ + Task MarkDeletedAsync(Guid notificationId); +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationReadCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationReadCommand.cs new file mode 100644 index 000000000..91ce63def --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/IMarkNotificationReadCommand.cs @@ -0,0 +1,7 @@ +#nullable enable +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface IMarkNotificationReadCommand +{ + Task MarkReadAsync(Guid notificationId); +} diff --git a/src/Core/NotificationCenter/Commands/Interfaces/IUpdateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/Interfaces/IUpdateNotificationCommand.cs new file mode 100644 index 000000000..8db10e6f1 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/Interfaces/IUpdateNotificationCommand.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; + +namespace Bit.Core.NotificationCenter.Commands.Interfaces; + +public interface IUpdateNotificationCommand +{ + Task UpdateAsync(Notification notification); +} diff --git a/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs b/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs new file mode 100644 index 000000000..fed9fd046 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/MarkNotificationDeletedCommand.cs @@ -0,0 +1,74 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + private readonly INotificationStatusRepository _notificationStatusRepository; + + public MarkNotificationDeletedCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + _notificationStatusRepository = notificationStatusRepository; + } + + public async Task MarkDeletedAsync(Guid notificationId) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var notification = await _notificationRepository.GetByIdAsync(notificationId); + if (notification == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, + NotificationOperations.Read); + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId, + _currentContext.UserId.Value); + + if (notificationStatus == null) + { + notificationStatus = new NotificationStatus() + { + NotificationId = notificationId, + UserId = _currentContext.UserId.Value, + DeletedDate = DateTime.Now + }; + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, + NotificationStatusOperations.Create); + + await _notificationStatusRepository.CreateAsync(notificationStatus); + } + else + { + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, + NotificationStatusOperations.Update); + + notificationStatus.DeletedDate = DateTime.UtcNow; + + await _notificationStatusRepository.UpdateAsync(notificationStatus); + } + } +} diff --git a/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs b/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs new file mode 100644 index 000000000..936866050 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/MarkNotificationReadCommand.cs @@ -0,0 +1,74 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class MarkNotificationReadCommand : IMarkNotificationReadCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + private readonly INotificationStatusRepository _notificationStatusRepository; + + public MarkNotificationReadCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository, + INotificationStatusRepository notificationStatusRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + _notificationStatusRepository = notificationStatusRepository; + } + + public async Task MarkReadAsync(Guid notificationId) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var notification = await _notificationRepository.GetByIdAsync(notificationId); + if (notification == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notification, + NotificationOperations.Read); + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId, + _currentContext.UserId.Value); + + if (notificationStatus == null) + { + notificationStatus = new NotificationStatus() + { + NotificationId = notificationId, + UserId = _currentContext.UserId.Value, + ReadDate = DateTime.Now + }; + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, + NotificationStatusOperations.Create); + + await _notificationStatusRepository.CreateAsync(notificationStatus); + } + else + { + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, + notificationStatus, NotificationStatusOperations.Update); + + notificationStatus.ReadDate = DateTime.UtcNow; + + await _notificationStatusRepository.UpdateAsync(notificationStatus); + } + } +} diff --git a/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs b/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs new file mode 100644 index 000000000..f04947817 --- /dev/null +++ b/src/Core/NotificationCenter/Commands/UpdateNotificationCommand.cs @@ -0,0 +1,47 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands.Interfaces; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Commands; + +public class UpdateNotificationCommand : IUpdateNotificationCommand +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationRepository _notificationRepository; + + public UpdateNotificationCommand(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationRepository notificationRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationRepository = notificationRepository; + } + + public async Task UpdateAsync(Notification notificationToUpdate) + { + var notification = await _notificationRepository.GetByIdAsync(notificationToUpdate.Id); + if (notification == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, + notification, NotificationOperations.Update); + + notification.Priority = notificationToUpdate.Priority; + notification.ClientType = notificationToUpdate.ClientType; + notification.Title = notificationToUpdate.Title; + notification.Body = notificationToUpdate.Body; + notification.RevisionDate = DateTime.UtcNow; + + await _notificationRepository.ReplaceAsync(notification); + } +} diff --git a/src/Core/NotificationCenter/Queries/GetNotificationStatusForUserQuery.cs b/src/Core/NotificationCenter/Queries/GetNotificationStatusForUserQuery.cs new file mode 100644 index 000000000..b28a0444a --- /dev/null +++ b/src/Core/NotificationCenter/Queries/GetNotificationStatusForUserQuery.cs @@ -0,0 +1,47 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Queries.Interfaces; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.NotificationCenter.Queries; + +public class GetNotificationStatusForUserQuery : IGetNotificationStatusForUserQuery +{ + private readonly ICurrentContext _currentContext; + private readonly IAuthorizationService _authorizationService; + private readonly INotificationStatusRepository _notificationStatusRepository; + + public GetNotificationStatusForUserQuery(ICurrentContext currentContext, + IAuthorizationService authorizationService, + INotificationStatusRepository notificationStatusRepository) + { + _currentContext = currentContext; + _authorizationService = authorizationService; + _notificationStatusRepository = notificationStatusRepository; + } + + public async Task GetByNotificationIdAndUserIdAsync(Guid notificationId) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var notificationStatus = await _notificationStatusRepository.GetByNotificationIdAndUserIdAsync(notificationId, + _currentContext.UserId.Value); + if (notificationStatus == null) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, + notificationStatus, NotificationStatusOperations.Read); + + return notificationStatus; + } +} diff --git a/src/Core/NotificationCenter/Queries/GetNotificationsForUserQuery.cs b/src/Core/NotificationCenter/Queries/GetNotificationsForUserQuery.cs new file mode 100644 index 000000000..9354d796d --- /dev/null +++ b/src/Core/NotificationCenter/Queries/GetNotificationsForUserQuery.cs @@ -0,0 +1,37 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Models.Filter; +using Bit.Core.NotificationCenter.Queries.Interfaces; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Utilities; + +namespace Bit.Core.NotificationCenter.Queries; + +public class GetNotificationsForUserQuery : IGetNotificationsForUserQuery +{ + private readonly ICurrentContext _currentContext; + private readonly INotificationRepository _notificationRepository; + + public GetNotificationsForUserQuery(ICurrentContext currentContext, + INotificationRepository notificationRepository) + { + _currentContext = currentContext; + _notificationRepository = notificationRepository; + } + + public async Task> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter) + { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var clientType = DeviceTypes.ToClientType(_currentContext.DeviceType); + + // Note: only returns the user's notifications - no authorization check needed + return await _notificationRepository.GetByUserIdAndStatusAsync(_currentContext.UserId.Value, clientType, + statusFilter); + } +} diff --git a/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusForUserQuery.cs b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusForUserQuery.cs new file mode 100644 index 000000000..a31956535 --- /dev/null +++ b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationStatusForUserQuery.cs @@ -0,0 +1,9 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; + +namespace Bit.Core.NotificationCenter.Queries.Interfaces; + +public interface IGetNotificationStatusForUserQuery +{ + Task GetByNotificationIdAndUserIdAsync(Guid notificationId); +} diff --git a/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationsForUserQuery.cs b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationsForUserQuery.cs new file mode 100644 index 000000000..f50c7745e --- /dev/null +++ b/src/Core/NotificationCenter/Queries/Interfaces/IGetNotificationsForUserQuery.cs @@ -0,0 +1,10 @@ +#nullable enable +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Models.Filter; + +namespace Bit.Core.NotificationCenter.Queries.Interfaces; + +public interface IGetNotificationsForUserQuery +{ + Task> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter); +} diff --git a/test/Core.Test/NotificationCenter/Authorization/NotificationAuthorizationHandlerTest.cs b/test/Core.Test/NotificationCenter/Authorization/NotificationAuthorizationHandlerTest.cs new file mode 100644 index 000000000..9985d279b --- /dev/null +++ b/test/Core.Test/NotificationCenter/Authorization/NotificationAuthorizationHandlerTest.cs @@ -0,0 +1,419 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.Authorization; + +using System.Security.Claims; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +[NotificationCustomize] +public class NotificationAuthorizationHandlerTests +{ + private static void SetupUserPermission(SutProvider sutProvider, + Guid? userId = null, Guid? organizationId = null, bool canAccessReports = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organizationId.GetValueOrDefault(Guid.NewGuid())) + .Returns(new CurrentContextOrganization()); + sutProvider.GetDependency().AccessReports(organizationId.GetValueOrDefault(Guid.NewGuid())) + .Returns(canAccessReports); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_UnsupportedNotificationOperationRequirement_Throws( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = new NotificationOperationsRequirement("UnsupportedOperation"); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); + } + + [Theory] + [BitAutoData(nameof(NotificationOperations.Read))] + [BitAutoData(nameof(NotificationOperations.Create))] + [BitAutoData(nameof(NotificationOperations.Update))] + public async Task HandleAsync_NotLoggedIn_Unauthorized( + string requirementName, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, userId: null); + var requirement = new NotificationOperationsRequirement(requirementName); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData(nameof(NotificationOperations.Read))] + [BitAutoData(nameof(NotificationOperations.Create))] + [BitAutoData(nameof(NotificationOperations.Update))] + public async Task HandleAsync_ResourceEmpty_Unauthorized( + string requirementName, + SutProvider sutProvider, + ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = new NotificationOperationsRequirement(requirementName); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, null); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: true)] + public async Task HandleAsync_ReadRequirementGlobalNotification_Authorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + + var requirement = NotificationOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_ReadRequirementUserNotMatching_Unauthorized( + bool hasOrganizationId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId); + + if (!hasOrganizationId) + { + notification.OrganizationId = null; + } + + var requirement = NotificationOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_ReadRequirementOrganizationNotMatching_Unauthorized( + bool hasUserId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid()); + + if (!hasUserId) + { + notification.UserId = null; + } + + var requirement = NotificationOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData(false, true)] + [BitAutoData(true, false)] + [BitAutoData(true, true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_ReadRequirement_Authorized( + bool hasUserId, + bool hasOrganizationId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId); + + if (!hasUserId) + { + notification.UserId = null; + } + + if (!hasOrganizationId) + { + notification.OrganizationId = null; + } + + var requirement = NotificationOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: true)] + public async Task HandleAsync_CreateRequirementGlobalNotification_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId); + + notification.OrganizationId = null; + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementOrganizationNotMatching_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid()); + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementOrganizationUserNoAccessReportsPermission_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, canAccessReports: false); + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementUserNotPartOfOrganization_Authorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId); + + notification.OrganizationId = null; + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_CreateRequirementOrganizationUserCanAccessReports_Authorized( + bool hasUserId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, true); + + if (!hasUserId) + { + notification.UserId = null; + } + + var requirement = NotificationOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + // TODO + [Theory] + [BitAutoData] + [NotificationCustomize(global: true)] + public async Task HandleAsync_UpdateRequirementGlobalNotification_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid(), notification.OrganizationId); + + notification.OrganizationId = null; + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementOrganizationNotMatching_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, Guid.NewGuid()); + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementOrganizationUserNoAccessReportsPermission_Unauthorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, canAccessReports: false); + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementUserNotPartOfOrganization_Authorized( + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId); + + notification.OrganizationId = null; + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(global: false)] + public async Task HandleAsync_UpdateRequirementOrganizationUserCanAccessReports_Authorized( + bool hasUserId, + SutProvider sutProvider, + Notification notification, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notification.UserId, notification.OrganizationId, true); + + if (!hasUserId) + { + notification.UserId = null; + } + + var requirement = NotificationOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notification); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } +} diff --git a/test/Core.Test/NotificationCenter/Authorization/NotificationStatusAuthorizationHandlerTest.cs b/test/Core.Test/NotificationCenter/Authorization/NotificationStatusAuthorizationHandlerTest.cs new file mode 100644 index 000000000..a43c40ea7 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Authorization/NotificationStatusAuthorizationHandlerTest.cs @@ -0,0 +1,179 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.Authorization; + +using System.Security.Claims; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +[NotificationStatusCustomize] +public class NotificationStatusAuthorizationHandlerTests +{ + private static void SetupUserPermission(SutProvider sutProvider, + Guid? userId = null) + { + sutProvider.GetDependency().UserId.Returns(userId); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_UnsupportedNotificationOperationRequirement_Throws( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = new NotificationStatusOperationsRequirement("UnsupportedOperation"); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await Assert.ThrowsAsync(() => sutProvider.Sut.HandleAsync(context)); + } + + [Theory] + [BitAutoData(nameof(NotificationStatusOperations.Read))] + [BitAutoData(nameof(NotificationStatusOperations.Create))] + [BitAutoData(nameof(NotificationStatusOperations.Update))] + public async Task HandleAsync_NotLoggedIn_Unauthorized( + string requirementName, + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, userId: null); + var requirement = new NotificationStatusOperationsRequirement(requirementName); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData(nameof(NotificationStatusOperations.Read))] + [BitAutoData(nameof(NotificationStatusOperations.Create))] + [BitAutoData(nameof(NotificationStatusOperations.Update))] + public async Task HandleAsync_ResourceEmpty_Unauthorized( + string requirementName, + SutProvider sutProvider, + ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + var requirement = new NotificationStatusOperationsRequirement(requirementName); + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, null); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_ReadRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + + var requirement = NotificationStatusOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_ReadRequirement_Authorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notificationStatus.UserId); + + var requirement = NotificationStatusOperations.Read; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_CreateRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + + var requirement = NotificationStatusOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_CreateRequirement_Authorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notificationStatus.UserId); + + var requirement = NotificationStatusOperations.Create; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_UpdateRequirementUserNotMatching_Unauthorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, Guid.NewGuid()); + + var requirement = NotificationStatusOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory] + [BitAutoData] + public async Task HandleAsync_UpdateRequirement_Authorized( + SutProvider sutProvider, + NotificationStatus notificationStatus, ClaimsPrincipal claimsPrincipal) + { + SetupUserPermission(sutProvider, notificationStatus.UserId); + + var requirement = NotificationStatusOperations.Update; + var context = new AuthorizationHandlerContext(new List { requirement }, + claimsPrincipal, notificationStatus); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } +} diff --git a/test/Core.Test/NotificationCenter/AutoFixture/NotificationFixtures.cs b/test/Core.Test/NotificationCenter/AutoFixture/NotificationFixtures.cs new file mode 100644 index 000000000..4cdee8de9 --- /dev/null +++ b/test/Core.Test/NotificationCenter/AutoFixture/NotificationFixtures.cs @@ -0,0 +1,31 @@ +using AutoFixture; +using Bit.Core.NotificationCenter.Entities; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.AutoFixture; + +public class NotificationCustomization(bool global) : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => + { + var postprocessComposer = composer.With(n => n.Id, Guid.NewGuid()) + .With(n => n.Global, global); + + postprocessComposer = global + ? postprocessComposer.Without(n => n.UserId) + : postprocessComposer.With(n => n.UserId, Guid.NewGuid()); + + return global + ? postprocessComposer.Without(n => n.OrganizationId) + : postprocessComposer.With(n => n.OrganizationId, Guid.NewGuid()); + }); + } +} + +public class NotificationCustomizeAttribute(bool global = true) + : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new NotificationCustomization(global); +} diff --git a/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusFixtures.cs b/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusFixtures.cs new file mode 100644 index 000000000..6f7bacbe0 --- /dev/null +++ b/test/Core.Test/NotificationCenter/AutoFixture/NotificationStatusFixtures.cs @@ -0,0 +1,19 @@ +using AutoFixture; +using Bit.Core.NotificationCenter.Entities; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.AutoFixture; + +public class NotificationStatusCustomization : ICustomization +{ + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer.With(ns => ns.NotificationId, Guid.NewGuid()) + .With(ns => ns.UserId, Guid.NewGuid())); + } +} + +public class NotificationStatusCustomizeAttribute : BitCustomizeAttribute +{ + public override ICustomization GetCustomization() => new NotificationStatusCustomization(); +} diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs new file mode 100644 index 000000000..4f5842d1c --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationCommandTest.cs @@ -0,0 +1,59 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +public class CreateNotificationCommandTest +{ + private static void Setup(SutProvider sutProvider, + Notification notification, bool authorized = false) + { + sutProvider.GetDependency() + .CreateAsync(notification) + .Returns(notification); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification, + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Create))) + .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification, authorized: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notification)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_Authorized_NotificationCreated( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification, true); + + var newNotification = await sutProvider.Sut.CreateAsync(notification); + + Assert.Equal(notification, newNotification); + Assert.Equal(DateTime.UtcNow, notification.CreationDate, TimeSpan.FromMinutes(1)); + Assert.Equal(notification.CreationDate, notification.RevisionDate); + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs new file mode 100644 index 000000000..8dc852492 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/CreateNotificationStatusCommandTest.cs @@ -0,0 +1,89 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +[NotificationStatusCustomize] +public class CreateNotificationStatusCommandTest +{ + private static void Setup(SutProvider sutProvider, + Notification? notification, NotificationStatus notificationStatus, + bool authorizedNotification = false, bool authorizedCreate = false) + { + sutProvider.GetDependency() + .GetByIdAsync(notificationStatus.NotificationId) + .Returns(notification); + sutProvider.GetDependency() + .CreateAsync(notificationStatus) + .Returns(notificationStatus); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Read))) + .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus, + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Create))) + .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NotificationNotFound_NotFoundException( + SutProvider sutProvider, + NotificationStatus notificationStatus) + { + Setup(sutProvider, notification: null, notificationStatus, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NotificationReadNotAuthorized_NotFoundException( + SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notification, notificationStatus, authorizedNotification: false, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_CreateNotAuthorized_NotFoundException( + SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notification, notificationStatus, true, authorizedCreate: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(notificationStatus)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NotificationFoundAuthorized_NotificationStatusCreated( + SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notification, notificationStatus, true, true); + + var newNotificationStatus = await sutProvider.Sut.CreateAsync(notificationStatus); + + Assert.Equal(notificationStatus, newNotificationStatus); + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs new file mode 100644 index 000000000..a5bb20423 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/MarkNotificationDeletedCommandTest.cs @@ -0,0 +1,151 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +[NotificationStatusCustomize] +public class MarkNotificationDeletedCommandTest +{ + private static void Setup(SutProvider sutProvider, + Guid notificationId, Guid? userId, Notification? notification, NotificationStatus? notificationStatus, + bool authorizedNotification = false, bool authorizedCreate = false, bool authorizedUpdate = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetByIdAsync(notificationId) + .Returns(notification); + sutProvider.GetDependency() + .GetByNotificationIdAndUserIdAsync(notificationId, userId ?? Arg.Any()) + .Returns(notificationStatus); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()); + sutProvider.GetDependency() + .UpdateAsync(notificationStatus ?? Arg.Any()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Read))) + .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Create))) + .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Update))) + .Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + + sutProvider.GetDependency().ClearReceivedCalls(); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_NotificationNotFound_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_ReadRequirementNotificationNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus, authorizedNotification: false, + true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_CreateRequirementNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, + authorizedCreate: false, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_UpdateRequirementNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, + authorizedUpdate: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkDeletedAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_NotificationStatusNotFoundCreateAuthorized_NotificationStatusCreated( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true); + + await sutProvider.Sut.MarkDeletedAsync(notificationId); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(ns => + ns.NotificationId == notificationId && ns.UserId == userId && !ns.ReadDate.HasValue && + ns.DeletedDate.HasValue && DateTime.UtcNow - ns.DeletedDate.Value < TimeSpan.FromMinutes(1))); + } + + [Theory] + [BitAutoData] + public async Task MarkDeletedAsync_NotificationStatusFoundCreateAuthorized_NotificationStatusUpdated( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + var deletedDate = notificationStatus.DeletedDate; + + Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, true); + + await sutProvider.Sut.MarkDeletedAsync(notificationId); + + await sutProvider.GetDependency().Received(1) + .UpdateAsync(Arg.Is(ns => + ns.Equals(notificationStatus) && + ns.NotificationId == notificationStatus.NotificationId && ns.UserId == notificationStatus.UserId && + ns.ReadDate == notificationStatus.ReadDate && ns.DeletedDate != deletedDate && + ns.DeletedDate.HasValue && + DateTime.UtcNow - ns.DeletedDate.Value < TimeSpan.FromMinutes(1))); + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs new file mode 100644 index 000000000..f80234c07 --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/MarkNotificationReadCommandTest.cs @@ -0,0 +1,151 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +[NotificationStatusCustomize] +public class MarkNotificationReadCommandTest +{ + private static void Setup(SutProvider sutProvider, + Guid notificationId, Guid? userId, Notification? notification, NotificationStatus? notificationStatus, + bool authorizedNotification = false, bool authorizedCreate = false, bool authorizedUpdate = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetByIdAsync(notificationId) + .Returns(notification); + sutProvider.GetDependency() + .GetByNotificationIdAndUserIdAsync(notificationId, userId ?? Arg.Any()) + .Returns(notificationStatus); + sutProvider.GetDependency() + .CreateAsync(Arg.Any()); + sutProvider.GetDependency() + .UpdateAsync(notificationStatus ?? Arg.Any()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Read))) + .Returns(authorizedNotification ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Create))) + .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Update))) + .Returns(authorizedUpdate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + + sutProvider.GetDependency().ClearReceivedCalls(); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_NotificationNotFound_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_ReadRequirementNotificationNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus, authorizedNotification: false, + true, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_CreateRequirementNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, + authorizedCreate: false, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_UpdateRequirementNotAuthorized_NotFoundException( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, + authorizedUpdate: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MarkReadAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_NotificationStatusNotFoundCreateAuthorized_NotificationStatusCreated( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification) + { + Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true); + + await sutProvider.Sut.MarkReadAsync(notificationId); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(ns => + ns.NotificationId == notificationId && ns.UserId == userId && !ns.DeletedDate.HasValue && + ns.ReadDate.HasValue && DateTime.UtcNow - ns.ReadDate.Value < TimeSpan.FromMinutes(1))); + } + + [Theory] + [BitAutoData] + public async Task MarkReadAsync_NotificationStatusFoundCreateAuthorized_NotificationStatusUpdated( + SutProvider sutProvider, + Guid notificationId, Guid userId, Notification notification, NotificationStatus notificationStatus) + { + var readDate = notificationStatus.ReadDate; + + Setup(sutProvider, notificationId, userId, notification, notificationStatus, true, true, true); + + await sutProvider.Sut.MarkReadAsync(notificationId); + + await sutProvider.GetDependency().Received(1) + .UpdateAsync(Arg.Is(ns => + ns.Equals(notificationStatus) && + ns.NotificationId == notificationStatus.NotificationId && ns.UserId == notificationStatus.UserId && + ns.DeletedDate == notificationStatus.DeletedDate && ns.ReadDate != readDate && + ns.ReadDate.HasValue && + DateTime.UtcNow - ns.ReadDate.Value < TimeSpan.FromMinutes(1))); + } +} diff --git a/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs b/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs new file mode 100644 index 000000000..976d1d77a --- /dev/null +++ b/test/Core.Test/NotificationCenter/Commands/UpdateNotificationCommandTest.cs @@ -0,0 +1,95 @@ +#nullable enable +using System.Security.Claims; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Commands; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Enums; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.NotificationCenter.Commands; + +[SutProviderCustomize] +[NotificationCustomize] +public class UpdateNotificationCommandTest +{ + private static void Setup(SutProvider sutProvider, + Guid notificationId, Notification? notification, bool authorized = false) + { + sutProvider.GetDependency() + .GetByIdAsync(notificationId) + .Returns(notification); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notification ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationOperations.Update))) + .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + + sutProvider.GetDependency().ClearReceivedCalls(); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_NotificationNotFound_NotFoundException( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification.Id, notification: null, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(notification)); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Notification notification) + { + Setup(sutProvider, notification.Id, notification, authorized: false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(notification)); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_Authorized_NotificationCreated( + SutProvider sutProvider, + Notification notification) + { + notification.Priority = Priority.Medium; + notification.ClientType = ClientType.Web; + notification.Title = "Title"; + notification.Body = "Body"; + notification.RevisionDate = DateTime.UtcNow.AddMinutes(-60); + + Setup(sutProvider, notification.Id, notification, true); + + var notificationToUpdate = CoreHelpers.CloneObject(notification); + notificationToUpdate.Priority = Priority.High; + notificationToUpdate.ClientType = ClientType.Mobile; + notificationToUpdate.Title = "Updated Title"; + notificationToUpdate.Body = "Updated Body"; + notificationToUpdate.RevisionDate = DateTime.UtcNow.AddMinutes(-30); + + await sutProvider.Sut.UpdateAsync(notificationToUpdate); + + await sutProvider.GetDependency().Received(1) + .ReplaceAsync(Arg.Is(n => + // Not updated fields + n.Id == notificationToUpdate.Id && n.Global == notificationToUpdate.Global && + n.UserId == notificationToUpdate.UserId && n.OrganizationId == notificationToUpdate.OrganizationId && + n.CreationDate == notificationToUpdate.CreationDate && + // Updated fields + n.Priority == notificationToUpdate.Priority && n.ClientType == notificationToUpdate.ClientType && + n.Title == notificationToUpdate.Title && n.Body == notificationToUpdate.Body && + DateTime.UtcNow - n.RevisionDate < TimeSpan.FromMinutes(1))); + } +} diff --git a/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusForUserTest.cs b/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusForUserTest.cs new file mode 100644 index 000000000..5ae22508b --- /dev/null +++ b/test/Core.Test/NotificationCenter/Queries/GetNotificationStatusForUserTest.cs @@ -0,0 +1,85 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Queries; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.Queries; + +using System.Security.Claims; +using Bit.Core.NotificationCenter.Authorization; +using Bit.Core.NotificationCenter.Entities; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +[NotificationStatusCustomize] +public class GetNotificationStatusForUserQueryTest +{ + private static void Setup(SutProvider sutProvider, + Guid notificationId, NotificationStatus? notificationStatus, Guid? userId, bool authorized = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .GetByNotificationIdAndUserIdAsync(notificationId, userId.GetValueOrDefault(Guid.NewGuid())) + .Returns(notificationStatus); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), notificationStatus ?? Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(NotificationStatusOperations.Read))) + .Returns(authorized ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_UserNotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid notificationId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, notificationStatus, userId: null, true); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_NotificationStatusNotFound_NotFoundException( + SutProvider sutProvider, + Guid notificationId) + { + Setup(sutProvider, notificationId, notificationStatus: null, Guid.NewGuid(), true); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Guid notificationId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, notificationStatus, Guid.NewGuid(), authorized: false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId)); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_NotificationFoundAuthorized_Returned( + SutProvider sutProvider, + Guid notificationId, NotificationStatus notificationStatus) + { + Setup(sutProvider, notificationId, notificationStatus, Guid.NewGuid(), true); + + var actualNotificationStatus = await sutProvider.Sut.GetByNotificationIdAndUserIdAsync(notificationId); + + Assert.Equal(notificationStatus, actualNotificationStatus); + } +} diff --git a/test/Core.Test/NotificationCenter/Queries/GetNotificationsForUserQueryTest.cs b/test/Core.Test/NotificationCenter/Queries/GetNotificationsForUserQueryTest.cs new file mode 100644 index 000000000..75c150c8d --- /dev/null +++ b/test/Core.Test/NotificationCenter/Queries/GetNotificationsForUserQueryTest.cs @@ -0,0 +1,55 @@ +#nullable enable +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.NotificationCenter.Models.Filter; +using Bit.Core.NotificationCenter.Queries; +using Bit.Core.NotificationCenter.Repositories; +using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.NotificationCenter.Queries; + +using Bit.Core.NotificationCenter.Entities; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +[NotificationCustomize] +public class GetNotificationsForUserQueryTest +{ + private static void Setup(SutProvider sutProvider, + List notifications, NotificationStatusFilter statusFilter, Guid? userId) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetByUserIdAndStatusAsync( + userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any(), statusFilter) + .Returns(notifications); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + List notifications, NotificationStatusFilter notificationStatusFilter) + { + Setup(sutProvider, notifications, notificationStatusFilter, userId: null); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter)); + } + + [Theory] + [BitAutoData] + public async Task GetByUserIdStatusFilterAsync_NotificationsFound_Returned( + SutProvider sutProvider, + List notifications, NotificationStatusFilter notificationStatusFilter) + { + Setup(sutProvider, notifications, notificationStatusFilter, Guid.NewGuid()); + + var actualNotifications = await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter); + + Assert.Equal(notifications, actualNotifications); + } +}