1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-01 13:43:23 +01:00

PM-10563: Notification Center API

This commit is contained in:
Maciej Zieniuk 2024-10-01 13:24:35 +01:00
parent b196c8bfb9
commit ef32d33489
No known key found for this signature in database
GPG Key ID: 9CACE59F1272ACD9
7 changed files with 256 additions and 10 deletions

View File

@ -0,0 +1,62 @@
#nullable enable
using Bit.Api.Models.Response;
using Bit.Api.NotificationCenter.Models.Request;
using Bit.Api.NotificationCenter.Models.Response;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Models.Filter;
using Bit.Core.NotificationCenter.Queries.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.NotificationCenter.Controllers;
[Route("notifications")]
[Authorize("Application")]
public class NotificationsController : Controller
{
private readonly IGetNotificationsForUserQuery _getNotificationsForUserQuery;
private readonly IMarkNotificationDeletedCommand _markNotificationDeletedCommand;
private readonly IMarkNotificationReadCommand _markNotificationReadCommand;
public NotificationsController(
IGetNotificationsForUserQuery getNotificationsForUserQuery,
IMarkNotificationDeletedCommand markNotificationDeletedCommand,
IMarkNotificationReadCommand markNotificationReadCommand)
{
_getNotificationsForUserQuery = getNotificationsForUserQuery;
_markNotificationDeletedCommand = markNotificationDeletedCommand;
_markNotificationReadCommand = markNotificationReadCommand;
}
[HttpGet("")]
public async Task<ListResponseModel<NotificationResponseModel>> List(
[FromQuery] NotificationFilterRequestModel filter)
{
var notificationStatusFilter = new NotificationStatusFilter
{
Read = filter.ReadStatusFilter,
Deleted = filter.DeletedStatusFilter
};
var notifications = await _getNotificationsForUserQuery.GetByUserIdStatusFilterAsync(notificationStatusFilter);
var filteredNotifications = notifications
.Where(n => n.RevisionDate >= filter.Start && n.RevisionDate < filter.End)
.Take(filter.PageSize);
var responses = filteredNotifications.Select(n => new NotificationResponseModel(n));
return new ListResponseModel<NotificationResponseModel>(responses);
}
[HttpPatch("{id}/delete")]
public async Task MarkAsDeleted([FromRoute] Guid id)
{
await _markNotificationDeletedCommand.MarkDeletedAsync(id);
}
[HttpPatch("{id}/read")]
public async Task MarkAsRead([FromRoute] Guid id)
{
await _markNotificationReadCommand.MarkReadAsync(id);
}
}

View File

@ -0,0 +1,29 @@
namespace Bit.Api.NotificationCenter.Models.Request;
public class NotificationFilterRequestModel
{
/// <summary>
/// Filters notifications by read status. When not set, includes notifications without a status.
/// </summary>
public bool? ReadStatusFilter { get; set; }
/// <summary>
/// Filters notifications by deleted status. When not set, includes notifications without a status.
/// </summary>
public bool? DeletedStatusFilter { get; set; }
/// <summary>
/// The start date. Must be less than the end date. Inclusive.
/// </summary>
public DateTime Start { get; set; } = DateTime.MinValue;
/// <summary>
/// The end date. Must be greater than the start date. Not inclusive.
/// </summary>
public DateTime End { get; set; } = DateTime.MaxValue;
/// <summary>
/// Number of items to return. Defaults to 10.
/// </summary>
public int PageSize { get; set; } = 10;
}

View File

@ -0,0 +1,40 @@
#nullable enable
using Bit.Core.Models.Api;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
namespace Bit.Api.NotificationCenter.Models.Response;
public class NotificationResponseModel : ResponseModel
{
private const string _objectName = "notification";
public NotificationResponseModel(Notification notification, string obj = _objectName)
: base(obj)
{
if (notification == null)
{
throw new ArgumentNullException(nameof(notification));
}
Id = notification.Id;
Priority = notification.Priority;
Title = notification.Title;
Body = notification.Body;
Date = notification.RevisionDate;
}
public NotificationResponseModel() : base(_objectName)
{
}
public Guid Id { get; set; }
public Priority Priority { get; set; }
public string? Title { get; set; }
public string? Body { get; set; }
public DateTime Date { get; set; }
}

View File

@ -0,0 +1,28 @@
using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Queries;
using Bit.Core.NotificationCenter.Queries.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.NotificationCenter;
public static class NotificationCenterServiceCollectionExtensions
{
public static void AddNotificationCenterServices(this IServiceCollection services)
{
// Authorization Handlers
services.AddScoped<IAuthorizationHandler, NotificationAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, NotificationStatusAuthorizationHandler>();
// Commands
services.AddScoped<ICreateNotificationCommand, CreateNotificationCommand>();
services.AddScoped<ICreateNotificationStatusCommand, CreateNotificationStatusCommand>();
services.AddScoped<IMarkNotificationDeletedCommand, MarkNotificationDeletedCommand>();
services.AddScoped<IMarkNotificationReadCommand, MarkNotificationReadCommand>();
services.AddScoped<IUpdateNotificationCommand, UpdateNotificationCommand>();
// Queries
services.AddScoped<IGetNotificationsForUserQuery, GetNotificationsForUserQuery>();
services.AddScoped<IGetNotificationStatusForUserQuery, GetNotificationStatusForUserQuery>();
}
}

View File

@ -24,6 +24,7 @@ using Bit.Core.Enums;
using Bit.Core.HostedServices; using Bit.Core.HostedServices;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.NotificationCenter;
using Bit.Core.OrganizationFeatures; using Bit.Core.OrganizationFeatures;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Resources; using Bit.Core.Resources;
@ -114,6 +115,7 @@ public static class ServiceCollectionExtensions
services.AddLoginServices(); services.AddLoginServices();
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>(); services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
services.AddVaultServices(); services.AddVaultServices();
services.AddNotificationCenterServices();
} }
public static void AddTokenizers(this IServiceCollection services) public static void AddTokenizers(this IServiceCollection services)

View File

@ -0,0 +1,52 @@
#nullable enable
using Bit.Api.NotificationCenter.Controllers;
using Bit.Api.NotificationCenter.Models.Request;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Models.Filter;
using Bit.Core.NotificationCenter.Queries.Interfaces;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Api.Test.NotificationCenter.Controllers;
[ControllerCustomize(typeof(NotificationsController))]
[SutProviderCustomize]
public class NotificationsControllerTest
{
[Theory]
[BitAutoData]
[NotificationListCustomize(20)]
public async Task List_DefaultFilter_ReturnedMatchingNotifications(SutProvider<NotificationsController> sutProvider,
List<Notification> notifications)
{
sutProvider.GetDependency<IGetNotificationsForUserQuery>()
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>())
.Returns(notifications);
var expectedNotificationGroupedById = notifications
.Take(10)
.ToDictionary(n => n.Id);
var filter = new NotificationFilterRequestModel();
var listResponse = await sutProvider.Sut.List(filter);
Assert.Equal(10, listResponse.Data.Count());
Assert.All(listResponse.Data, notificationResponseModel =>
{
var expectedNotification = expectedNotificationGroupedById[notificationResponseModel.Id];
Assert.NotNull(expectedNotification);
Assert.Equal(expectedNotification.Id, notificationResponseModel.Id);
Assert.Equal(expectedNotification.Priority, notificationResponseModel.Priority);
Assert.Equal(expectedNotification.Title, notificationResponseModel.Title);
Assert.Equal(expectedNotification.Body, notificationResponseModel.Body);
Assert.Equal(expectedNotification.RevisionDate, notificationResponseModel.Date);
Assert.Equal("notification", notificationResponseModel.Object);
});
Assert.Null(listResponse.ContinuationToken);
Assert.Equal("list", listResponse.Object);
}
}

View File

@ -1,4 +1,6 @@
using AutoFixture; using AutoFixture;
using AutoFixture.Dsl;
using AutoFixture.Kernel;
using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationCenter.Entities;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -8,9 +10,12 @@ public class NotificationCustomization(bool global) : ICustomization
{ {
public void Customize(IFixture fixture) public void Customize(IFixture fixture)
{ {
fixture.Customize<Notification>(composer => fixture.Customize<Notification>(GetSpecimenBuilder);
}
public ISpecimenBuilder GetSpecimenBuilder(ICustomizationComposer<Notification> customizationComposer)
{ {
var postprocessComposer = composer.With(n => n.Id, Guid.NewGuid()) var postprocessComposer = customizationComposer.With(n => n.Id, Guid.NewGuid())
.With(n => n.Global, global); .With(n => n.Global, global);
postprocessComposer = global postprocessComposer = global
@ -20,7 +25,29 @@ public class NotificationCustomization(bool global) : ICustomization
return global return global
? postprocessComposer.Without(n => n.OrganizationId) ? postprocessComposer.Without(n => n.OrganizationId)
: postprocessComposer.With(n => n.OrganizationId, Guid.NewGuid()); : postprocessComposer.With(n => n.OrganizationId, Guid.NewGuid());
}); }
}
public class NotificationListCustomization(int count) : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<List<Notification>>(composer => composer.FromFactory(() =>
{
var notificationCustomization = new NotificationCustomization(true);
var notifications = new List<Notification>();
for (var i = 0; i < count; i++)
{
var customizationComposer = fixture.Build<Notification>();
var postprocessComposer =
customizationComposer.FromFactory(
notificationCustomization.GetSpecimenBuilder(customizationComposer));
notifications.Add(postprocessComposer.Create());
}
return notifications;
}));
} }
} }
@ -29,3 +56,9 @@ public class NotificationCustomizeAttribute(bool global = true)
{ {
public override ICustomization GetCustomization() => new NotificationCustomization(global); public override ICustomization GetCustomization() => new NotificationCustomization(global);
} }
public class NotificationListCustomizeAttribute(int count)
: BitCustomizeAttribute
{
public override ICustomization GetCustomization() => new NotificationListCustomization(count);
}