1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[PM-10563] Notification Center API (#4852)

* PM-10563: Notification Center API

* PM-10563: continuation token hack

* PM-10563: Resolving merge conflicts

* PM-10563: Unit Tests

* PM-10563: Paging simplification by page number and size in database

* PM-10563: Request validation

* PM-10563: Read, Deleted status filters change

* PM-10563: Plural name for tests

* PM-10563: Request validation to always for int type

* PM-10563: Continuation Token returns null on response when no more records available

* PM-10563: Integration tests for GET

* PM-10563: Mark notification read, deleted commands date typos fix

* PM-10563: Integration tests for PATCH read, deleted

* PM-10563: Request, Response models tests

* PM-10563: EditorConfig compliance

* PM-10563: Extracting to const

* PM-10563: Update db migration script date

* PM-10563: Update migration script date
This commit is contained in:
Maciej Zieniuk
2024-12-18 15:59:50 +01:00
committed by GitHub
parent de2dc243fc
commit 21fcfcd5e8
18 changed files with 1272 additions and 39 deletions

View File

@ -0,0 +1,202 @@
#nullable enable
using Bit.Api.NotificationCenter.Controllers;
using Bit.Api.NotificationCenter.Models.Request;
using Bit.Core.Models.Data;
using Bit.Core.NotificationCenter.Commands.Interfaces;
using Bit.Core.NotificationCenter.Models.Data;
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 NotificationsControllerTests
{
[Theory]
[BitAutoData([null, null])]
[BitAutoData([null, false])]
[BitAutoData([null, true])]
[BitAutoData(false, null)]
[BitAutoData(true, null)]
[BitAutoData(false, false)]
[BitAutoData(false, true)]
[BitAutoData(true, false)]
[BitAutoData(true, true)]
[NotificationStatusDetailsListCustomize(5)]
public async Task ListAsync_StatusFilter_ReturnedMatchingNotifications(bool? readStatusFilter, bool? deletedStatusFilter,
SutProvider<NotificationsController> sutProvider,
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
{
var notificationStatusDetailsList = notificationStatusDetailsEnumerable
.OrderByDescending(n => n.Priority)
.ThenByDescending(n => n.CreationDate)
.ToList();
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
.Returns(new PagedResult<NotificationStatusDetails> { Data = notificationStatusDetailsList });
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
.Take(10)
.ToDictionary(n => n.Id);
var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel
{
ReadStatusFilter = readStatusFilter,
DeletedStatusFilter = deletedStatusFilter
});
Assert.Equal("list", listResponse.Object);
Assert.Equal(5, listResponse.Data.Count());
Assert.All(listResponse.Data, notificationResponseModel =>
{
Assert.Equal("notification", notificationResponseModel.Object);
Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));
var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];
Assert.NotNull(expectedNotificationStatusDetails);
Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);
Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);
Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);
Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);
Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);
Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
});
Assert.Null(listResponse.ContinuationToken);
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.Received(1)
.GetByUserIdStatusFilterAsync(Arg.Is<NotificationStatusFilter>(filter =>
filter.Read == readStatusFilter && filter.Deleted == deletedStatusFilter),
Arg.Is<PageOptions>(pageOptions =>
pageOptions.ContinuationToken == null && pageOptions.PageSize == 10));
}
[Theory]
[BitAutoData]
[NotificationStatusDetailsListCustomize(19)]
public async Task ListAsync_PagingRequestNoContinuationToken_ReturnedFirst10MatchingNotifications(
SutProvider<NotificationsController> sutProvider,
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
{
var notificationStatusDetailsList = notificationStatusDetailsEnumerable
.OrderByDescending(n => n.Priority)
.ThenByDescending(n => n.CreationDate)
.ToList();
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
.Returns(new PagedResult<NotificationStatusDetails>
{ Data = notificationStatusDetailsList.Take(10).ToList(), ContinuationToken = "2" });
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
.Take(10)
.ToDictionary(n => n.Id);
var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel());
Assert.Equal("list", listResponse.Object);
Assert.Equal(10, listResponse.Data.Count());
Assert.All(listResponse.Data, notificationResponseModel =>
{
Assert.Equal("notification", notificationResponseModel.Object);
Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));
var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];
Assert.NotNull(expectedNotificationStatusDetails);
Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);
Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);
Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);
Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);
Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);
Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
});
Assert.Equal("2", listResponse.ContinuationToken);
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.Received(1)
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(),
Arg.Is<PageOptions>(pageOptions =>
pageOptions.ContinuationToken == null && pageOptions.PageSize == 10));
}
[Theory]
[BitAutoData]
[NotificationStatusDetailsListCustomize(19)]
public async Task ListAsync_PagingRequestUsingContinuationToken_ReturnedLast9MatchingNotifications(
SutProvider<NotificationsController> sutProvider,
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
{
var notificationStatusDetailsList = notificationStatusDetailsEnumerable
.OrderByDescending(n => n.Priority)
.ThenByDescending(n => n.CreationDate)
.ToList();
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
.Returns(new PagedResult<NotificationStatusDetails>
{ Data = notificationStatusDetailsList.Skip(10).ToList() });
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
.Skip(10)
.ToDictionary(n => n.Id);
var listResponse = await sutProvider.Sut.ListAsync(new NotificationFilterRequestModel { ContinuationToken = "2" });
Assert.Equal("list", listResponse.Object);
Assert.Equal(9, listResponse.Data.Count());
Assert.All(listResponse.Data, notificationResponseModel =>
{
Assert.Equal("notification", notificationResponseModel.Object);
Assert.True(expectedNotificationStatusDetailsMap.ContainsKey(notificationResponseModel.Id));
var expectedNotificationStatusDetails = expectedNotificationStatusDetailsMap[notificationResponseModel.Id];
Assert.NotNull(expectedNotificationStatusDetails);
Assert.Equal(expectedNotificationStatusDetails.Id, notificationResponseModel.Id);
Assert.Equal(expectedNotificationStatusDetails.Priority, notificationResponseModel.Priority);
Assert.Equal(expectedNotificationStatusDetails.Title, notificationResponseModel.Title);
Assert.Equal(expectedNotificationStatusDetails.Body, notificationResponseModel.Body);
Assert.Equal(expectedNotificationStatusDetails.RevisionDate, notificationResponseModel.Date);
Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
});
Assert.Null(listResponse.ContinuationToken);
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
.Received(1)
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(),
Arg.Is<PageOptions>(pageOptions =>
pageOptions.ContinuationToken == "2" && pageOptions.PageSize == 10));
}
[Theory]
[BitAutoData]
public async Task MarkAsDeletedAsync_NotificationId_MarkedAsDeleted(
SutProvider<NotificationsController> sutProvider,
Guid notificationId)
{
await sutProvider.Sut.MarkAsDeletedAsync(notificationId);
await sutProvider.GetDependency<IMarkNotificationDeletedCommand>()
.Received(1)
.MarkDeletedAsync(notificationId);
}
[Theory]
[BitAutoData]
public async Task MarkAsReadAsync_NotificationId_MarkedAsRead(
SutProvider<NotificationsController> sutProvider,
Guid notificationId)
{
await sutProvider.Sut.MarkAsReadAsync(notificationId);
await sutProvider.GetDependency<IMarkNotificationReadCommand>()
.Received(1)
.MarkReadAsync(notificationId);
}
}

View File

@ -0,0 +1,93 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.NotificationCenter.Models.Request;
using Xunit;
namespace Bit.Api.Test.NotificationCenter.Models.Request;
public class NotificationFilterRequestModelTests
{
[Theory]
[InlineData("invalid")]
[InlineData("-1")]
[InlineData("0")]
public void Validate_ContinuationTokenInvalidNumber_Invalid(string continuationToken)
{
var model = new NotificationFilterRequestModel
{
ContinuationToken = continuationToken,
};
var result = Validate(model);
Assert.Single(result);
Assert.Contains("Continuation token must be a positive, non zero integer.", result[0].ErrorMessage);
Assert.Contains("ContinuationToken", result[0].MemberNames);
}
[Fact]
public void Validate_ContinuationTokenMaxLengthExceeded_Invalid()
{
var model = new NotificationFilterRequestModel
{
ContinuationToken = "1234567890"
};
var result = Validate(model);
Assert.Single(result);
Assert.Contains("The field ContinuationToken must be a string with a maximum length of 9.",
result[0].ErrorMessage);
Assert.Contains("ContinuationToken", result[0].MemberNames);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
[InlineData("1")]
[InlineData("123456789")]
public void Validate_ContinuationTokenCorrect_Valid(string? continuationToken)
{
var model = new NotificationFilterRequestModel
{
ContinuationToken = continuationToken
};
var result = Validate(model);
Assert.Empty(result);
}
[Theory]
[InlineData(9)]
[InlineData(1001)]
public void Validate_PageSizeInvalidRange_Invalid(int pageSize)
{
var model = new NotificationFilterRequestModel
{
PageSize = pageSize
};
var result = Validate(model);
Assert.Single(result);
Assert.Contains("The field PageSize must be between 10 and 1000.", result[0].ErrorMessage);
Assert.Contains("PageSize", result[0].MemberNames);
}
[Theory]
[InlineData(null)]
[InlineData(10)]
[InlineData(1000)]
public void Validate_PageSizeCorrect_Valid(int? pageSize)
{
var model = pageSize == null
? new NotificationFilterRequestModel()
: new NotificationFilterRequestModel
{
PageSize = pageSize.Value
};
var result = Validate(model);
Assert.Empty(result);
}
private static List<ValidationResult> Validate(NotificationFilterRequestModel model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
return results;
}
}

View File

@ -0,0 +1,43 @@
#nullable enable
using Bit.Api.NotificationCenter.Models.Response;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.NotificationCenter.Models.Data;
using Xunit;
namespace Bit.Api.Test.NotificationCenter.Models.Response;
public class NotificationResponseModelTests
{
[Fact]
public void Constructor_NotificationStatusDetailsNull_CorrectFields()
{
Assert.Throws<ArgumentNullException>(() => new NotificationResponseModel(null!));
}
[Fact]
public void Constructor_NotificationStatusDetails_CorrectFields()
{
var notificationStatusDetails = new NotificationStatusDetails
{
Id = Guid.NewGuid(),
Global = true,
Priority = Priority.High,
ClientType = ClientType.All,
Title = "Test Title",
Body = "Test Body",
RevisionDate = DateTime.UtcNow - TimeSpan.FromMinutes(3),
ReadDate = DateTime.UtcNow - TimeSpan.FromMinutes(1),
DeletedDate = DateTime.UtcNow,
};
var model = new NotificationResponseModel(notificationStatusDetails);
Assert.Equal(model.Id, notificationStatusDetails.Id);
Assert.Equal(model.Priority, notificationStatusDetails.Priority);
Assert.Equal(model.Title, notificationStatusDetails.Title);
Assert.Equal(model.Body, notificationStatusDetails.Body);
Assert.Equal(model.Date, notificationStatusDetails.RevisionDate);
Assert.Equal(model.ReadDate, notificationStatusDetails.ReadDate);
Assert.Equal(model.DeletedDate, notificationStatusDetails.DeletedDate);
}
}