mirror of
https://github.com/bitwarden/server.git
synced 2025-01-23 22:01:28 +01:00
PM-10563: Paging simplification by page number and size in database
This commit is contained in:
parent
aa7a62d586
commit
5b3a09dbe8
@ -1,13 +1,11 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Text.Json;
|
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.NotificationCenter.Models;
|
|
||||||
using Bit.Api.NotificationCenter.Models.Request;
|
using Bit.Api.NotificationCenter.Models.Request;
|
||||||
using Bit.Api.NotificationCenter.Models.Response;
|
using Bit.Api.NotificationCenter.Models.Response;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Commands.Interfaces;
|
using Bit.Core.NotificationCenter.Commands.Interfaces;
|
||||||
using Bit.Core.NotificationCenter.Models.Filter;
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -21,7 +19,7 @@ public class NotificationsController : Controller
|
|||||||
private readonly IMarkNotificationDeletedCommand _markNotificationDeletedCommand;
|
private readonly IMarkNotificationDeletedCommand _markNotificationDeletedCommand;
|
||||||
private readonly IMarkNotificationReadCommand _markNotificationReadCommand;
|
private readonly IMarkNotificationReadCommand _markNotificationReadCommand;
|
||||||
|
|
||||||
private const int RowsPerPage = 10;
|
private const int DefaultPageSize = 10;
|
||||||
|
|
||||||
public NotificationsController(
|
public NotificationsController(
|
||||||
IGetNotificationStatusDetailsForUserQuery getNotificationStatusDetailsForUserQuery,
|
IGetNotificationStatusDetailsForUserQuery getNotificationStatusDetailsForUserQuery,
|
||||||
@ -37,42 +35,28 @@ public class NotificationsController : Controller
|
|||||||
public async Task<ListResponseModel<NotificationResponseModel>> List(
|
public async Task<ListResponseModel<NotificationResponseModel>> List(
|
||||||
[FromQuery] NotificationFilterRequestModel filter)
|
[FromQuery] NotificationFilterRequestModel filter)
|
||||||
{
|
{
|
||||||
var continuationToken = ParseContinuationToken(filter.ContinuationToken);
|
var pageOptions = new PageOptions
|
||||||
|
{
|
||||||
|
ContinuationToken = filter.ContinuationToken,
|
||||||
|
PageSize = DefaultPageSize
|
||||||
|
};
|
||||||
|
|
||||||
var notificationStatusFilter = new NotificationStatusFilter
|
var notificationStatusFilter = new NotificationStatusFilter
|
||||||
{
|
{
|
||||||
Read = filter.ReadStatusFilter,
|
Read = filter.ReadStatusFilter,
|
||||||
Deleted = filter.DeletedStatusFilter
|
Deleted = filter.DeletedStatusFilter
|
||||||
};
|
};
|
||||||
|
|
||||||
var notificationStatusDetails =
|
var notificationStatusDetailsPagedResult =
|
||||||
await _getNotificationStatusDetailsForUserQuery.GetByUserIdStatusFilterAsync(notificationStatusFilter);
|
await _getNotificationStatusDetailsForUserQuery.GetByUserIdStatusFilterAsync(notificationStatusFilter,
|
||||||
|
pageOptions);
|
||||||
|
|
||||||
if (continuationToken != null)
|
var responses = notificationStatusDetailsPagedResult.Data
|
||||||
{
|
|
||||||
// Priority and CreationDate are always in descending order
|
|
||||||
notificationStatusDetails = notificationStatusDetails
|
|
||||||
.Where(n => n.Priority < continuationToken.Priority ||
|
|
||||||
(n.Priority == continuationToken.Priority && n.CreationDate < continuationToken.Date));
|
|
||||||
}
|
|
||||||
|
|
||||||
var pagedNotificationStatusDetails = notificationStatusDetails
|
|
||||||
.Take(RowsPerPage)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var responses = pagedNotificationStatusDetails
|
|
||||||
.Select(n => new NotificationResponseModel(n))
|
.Select(n => new NotificationResponseModel(n))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var nextContinuationToken = pagedNotificationStatusDetails.Count == RowsPerPage
|
|
||||||
? new NotificationContinuationToken
|
|
||||||
{
|
|
||||||
Priority = pagedNotificationStatusDetails.Last().Priority,
|
|
||||||
Date = pagedNotificationStatusDetails.Last().CreationDate
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return new ListResponseModel<NotificationResponseModel>(responses,
|
return new ListResponseModel<NotificationResponseModel>(responses,
|
||||||
CreateContinuationToken(nextContinuationToken));
|
notificationStatusDetailsPagedResult.ContinuationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{id}/delete")]
|
[HttpPatch("{id}/delete")]
|
||||||
@ -86,28 +70,4 @@ public class NotificationsController : Controller
|
|||||||
{
|
{
|
||||||
await _markNotificationReadCommand.MarkReadAsync(id);
|
await _markNotificationReadCommand.MarkReadAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NotificationContinuationToken? ParseContinuationToken(string? continuationToken)
|
|
||||||
{
|
|
||||||
if (continuationToken == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var decodedContinuationToken = CoreHelpers.Base64UrlDecodeString(continuationToken);
|
|
||||||
return JsonSerializer.Deserialize<NotificationContinuationToken>(decodedContinuationToken,
|
|
||||||
JsonHelpers.CamelCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? CreateContinuationToken(NotificationContinuationToken? notificationContinuationToken)
|
|
||||||
{
|
|
||||||
if (notificationContinuationToken == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var serializedContinuationToken = JsonSerializer.Serialize(notificationContinuationToken,
|
|
||||||
JsonHelpers.CamelCase);
|
|
||||||
return CoreHelpers.Base64UrlEncodeString(serializedContinuationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using Bit.Core.NotificationCenter.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Api.NotificationCenter.Models;
|
|
||||||
|
|
||||||
public class NotificationContinuationToken
|
|
||||||
{
|
|
||||||
public Priority Priority { get; set; }
|
|
||||||
|
|
||||||
public DateTime Date { get; set; }
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Data;
|
using Bit.Core.NotificationCenter.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Filter;
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||||
@ -21,8 +22,8 @@ public class GetNotificationStatusDetailsForUserQuery : IGetNotificationStatusDe
|
|||||||
_notificationRepository = notificationRepository;
|
_notificationRepository = notificationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(
|
public async Task<PagedResult<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(
|
||||||
NotificationStatusFilter statusFilter)
|
NotificationStatusFilter statusFilter, PageOptions pageOptions)
|
||||||
{
|
{
|
||||||
if (!_currentContext.UserId.HasValue)
|
if (!_currentContext.UserId.HasValue)
|
||||||
{
|
{
|
||||||
@ -33,6 +34,6 @@ public class GetNotificationStatusDetailsForUserQuery : IGetNotificationStatusDe
|
|||||||
|
|
||||||
// Note: only returns the user's notifications - no authorization check needed
|
// Note: only returns the user's notifications - no authorization check needed
|
||||||
return await _notificationRepository.GetByUserIdAndStatusAsync(_currentContext.UserId.Value, clientType,
|
return await _notificationRepository.GetByUserIdAndStatusAsync(_currentContext.UserId.Value, clientType,
|
||||||
statusFilter);
|
statusFilter, pageOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Data;
|
using Bit.Core.NotificationCenter.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Filter;
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
|
|
||||||
@ -6,5 +7,6 @@ namespace Bit.Core.NotificationCenter.Queries.Interfaces;
|
|||||||
|
|
||||||
public interface IGetNotificationStatusDetailsForUserQuery
|
public interface IGetNotificationStatusDetailsForUserQuery
|
||||||
{
|
{
|
||||||
Task<IEnumerable<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter);
|
Task<PagedResult<NotificationStatusDetails>> GetByUserIdStatusFilterAsync(NotificationStatusFilter statusFilter,
|
||||||
|
PageOptions pageOptions);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
using Bit.Core.NotificationCenter.Models.Data;
|
using Bit.Core.NotificationCenter.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Filter;
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
@ -22,10 +23,13 @@ public interface INotificationRepository : IRepository<Notification, Guid>
|
|||||||
/// If both <see cref="NotificationStatusFilter.Read"/> and <see cref="NotificationStatusFilter.Deleted"/>
|
/// If both <see cref="NotificationStatusFilter.Read"/> and <see cref="NotificationStatusFilter.Deleted"/>
|
||||||
/// are not set, includes notifications without a status.
|
/// are not set, includes notifications without a status.
|
||||||
/// </param>
|
/// </param>
|
||||||
|
/// <param name="pageOptions">
|
||||||
|
/// Pagination options.
|
||||||
|
/// </param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// Ordered by priority (highest to lowest) and creation date (descending).
|
/// Paged results ordered by priority (descending, highest to lowest) and creation date (descending).
|
||||||
/// Includes all fields from <see cref="Notification"/> and <see cref="NotificationStatus"/>
|
/// Includes all fields from <see cref="Notification"/> and <see cref="NotificationStatus"/>
|
||||||
/// </returns>
|
/// </returns>
|
||||||
Task<IEnumerable<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType,
|
Task<PagedResult<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId, ClientType clientType,
|
||||||
NotificationStatusFilter? statusFilter);
|
NotificationStatusFilter? statusFilter, PageOptions pageOptions);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
using Bit.Core.NotificationCenter.Models.Data;
|
using Bit.Core.NotificationCenter.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Filter;
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
@ -24,16 +25,33 @@ public class NotificationRepository : Repository<Notification, Guid>, INotificat
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId,
|
public async Task<PagedResult<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId,
|
||||||
ClientType clientType, NotificationStatusFilter? statusFilter)
|
ClientType clientType, NotificationStatusFilter? statusFilter, PageOptions pageOptions)
|
||||||
{
|
{
|
||||||
await using var connection = new SqlConnection(ConnectionString);
|
await using var connection = new SqlConnection(ConnectionString);
|
||||||
|
|
||||||
|
if (!int.TryParse(pageOptions.ContinuationToken, out var pageNumber))
|
||||||
|
{
|
||||||
|
pageNumber = 1;
|
||||||
|
}
|
||||||
|
|
||||||
var results = await connection.QueryAsync<NotificationStatusDetails>(
|
var results = await connection.QueryAsync<NotificationStatusDetails>(
|
||||||
"[dbo].[Notification_ReadByUserIdAndStatus]",
|
"[dbo].[Notification_ReadByUserIdAndStatus]",
|
||||||
new { UserId = userId, ClientType = clientType, statusFilter?.Read, statusFilter?.Deleted },
|
new
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ClientType = clientType,
|
||||||
|
statusFilter?.Read,
|
||||||
|
statusFilter?.Deleted,
|
||||||
|
PageNumber = pageNumber,
|
||||||
|
pageOptions.PageSize
|
||||||
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
return results.ToList();
|
return new PagedResult<NotificationStatusDetails>
|
||||||
|
{
|
||||||
|
Data = results.ToList(),
|
||||||
|
ContinuationToken = (pageNumber + 1).ToString()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Data;
|
using Bit.Core.NotificationCenter.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Filter;
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
using Bit.Core.NotificationCenter.Repositories;
|
using Bit.Core.NotificationCenter.Repositories;
|
||||||
@ -36,12 +37,17 @@ public class NotificationRepository : Repository<Core.NotificationCenter.Entitie
|
|||||||
return Mapper.Map<List<Core.NotificationCenter.Entities.Notification>>(notifications);
|
return Mapper.Map<List<Core.NotificationCenter.Entities.Notification>>(notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId,
|
public async Task<PagedResult<NotificationStatusDetails>> GetByUserIdAndStatusAsync(Guid userId,
|
||||||
ClientType clientType, NotificationStatusFilter? statusFilter)
|
ClientType clientType, NotificationStatusFilter? statusFilter, PageOptions pageOptions)
|
||||||
{
|
{
|
||||||
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
|
if (!int.TryParse(pageOptions.ContinuationToken, out var pageNumber))
|
||||||
|
{
|
||||||
|
pageNumber = 1;
|
||||||
|
}
|
||||||
|
|
||||||
var notificationStatusDetailsViewQuery = new NotificationStatusDetailsViewQuery(userId, clientType);
|
var notificationStatusDetailsViewQuery = new NotificationStatusDetailsViewQuery(userId, clientType);
|
||||||
|
|
||||||
var query = notificationStatusDetailsViewQuery.Run(dbContext);
|
var query = notificationStatusDetailsViewQuery.Run(dbContext);
|
||||||
@ -55,9 +61,17 @@ public class NotificationRepository : Repository<Core.NotificationCenter.Entitie
|
|||||||
select n;
|
select n;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await query
|
var results = await query
|
||||||
.OrderByDescending(n => n.Priority)
|
.OrderByDescending(n => n.Priority)
|
||||||
.ThenByDescending(n => n.CreationDate)
|
.ThenByDescending(n => n.CreationDate)
|
||||||
|
.Skip(pageOptions.PageSize * (pageNumber - 1))
|
||||||
|
.Take(pageOptions.PageSize)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
return new PagedResult<NotificationStatusDetails>
|
||||||
|
{
|
||||||
|
Data = results,
|
||||||
|
ContinuationToken = (pageNumber + 1).ToString()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ CREATE PROCEDURE [dbo].[Notification_ReadByUserIdAndStatus]
|
|||||||
@UserId UNIQUEIDENTIFIER,
|
@UserId UNIQUEIDENTIFIER,
|
||||||
@ClientType TINYINT,
|
@ClientType TINYINT,
|
||||||
@Read BIT,
|
@Read BIT,
|
||||||
@Deleted BIT
|
@Deleted BIT,
|
||||||
|
@PageNumber INT = 1,
|
||||||
|
@PageSize INT = 10
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -30,4 +32,6 @@ BEGIN
|
|||||||
(@Deleted = 0 AND n.[DeletedDate] IS NULL),
|
(@Deleted = 0 AND n.[DeletedDate] IS NULL),
|
||||||
1, 0) = 1))))
|
1, 0) = 1))))
|
||||||
ORDER BY [Priority] DESC, n.[CreationDate] DESC
|
ORDER BY [Priority] DESC, n.[CreationDate] DESC
|
||||||
|
OFFSET @PageSize * (@PageNumber - 1) ROWS
|
||||||
|
FETCH NEXT @PageSize ROWS ONLY
|
||||||
END
|
END
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Text.Json;
|
|
||||||
using Bit.Api.NotificationCenter.Controllers;
|
using Bit.Api.NotificationCenter.Controllers;
|
||||||
using Bit.Api.NotificationCenter.Models.Request;
|
using Bit.Api.NotificationCenter.Models.Request;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Commands.Interfaces;
|
using Bit.Core.NotificationCenter.Commands.Interfaces;
|
||||||
using Bit.Core.NotificationCenter.Models.Data;
|
using Bit.Core.NotificationCenter.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Filter;
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
using Bit.Core.NotificationCenter.Queries.Interfaces;
|
||||||
using Bit.Core.Test.NotificationCenter.AutoFixture;
|
using Bit.Core.Test.NotificationCenter.AutoFixture;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@ -40,8 +39,8 @@ public class NotificationsControllerTest
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
||||||
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>())
|
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
|
||||||
.Returns(notificationStatusDetailsList);
|
.Returns(new PagedResult<NotificationStatusDetails> { Data = notificationStatusDetailsList });
|
||||||
|
|
||||||
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
|
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
|
||||||
.Take(10)
|
.Take(10)
|
||||||
@ -74,13 +73,15 @@ public class NotificationsControllerTest
|
|||||||
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
||||||
.Received(1)
|
.Received(1)
|
||||||
.GetByUserIdStatusFilterAsync(Arg.Is<NotificationStatusFilter>(filter =>
|
.GetByUserIdStatusFilterAsync(Arg.Is<NotificationStatusFilter>(filter =>
|
||||||
filter.Read == readStatusFilter && filter.Deleted == deletedStatusFilter));
|
filter.Read == readStatusFilter && filter.Deleted == deletedStatusFilter),
|
||||||
|
Arg.Is<PageOptions>(pageOptions =>
|
||||||
|
pageOptions.ContinuationToken == null && pageOptions.PageSize == 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
[NotificationStatusDetailsListCustomize(19)]
|
[NotificationStatusDetailsListCustomize(19)]
|
||||||
public async Task List_PagingNoContinuationToken_ReturnedFirst10MatchingNotifications(
|
public async Task List_PagingRequestNoContinuationToken_ReturnedFirst10MatchingNotifications(
|
||||||
SutProvider<NotificationsController> sutProvider,
|
SutProvider<NotificationsController> sutProvider,
|
||||||
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
|
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
|
||||||
{
|
{
|
||||||
@ -90,8 +91,9 @@ public class NotificationsControllerTest
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
||||||
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>())
|
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
|
||||||
.Returns(notificationStatusDetailsList);
|
.Returns(new PagedResult<NotificationStatusDetails>
|
||||||
|
{ Data = notificationStatusDetailsList.Take(10).ToList(), ContinuationToken = "2" });
|
||||||
|
|
||||||
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
|
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
|
||||||
.Take(10)
|
.Take(10)
|
||||||
@ -115,22 +117,19 @@ public class NotificationsControllerTest
|
|||||||
Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);
|
Assert.Equal(expectedNotificationStatusDetails.ReadDate, notificationResponseModel.ReadDate);
|
||||||
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
|
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
|
||||||
});
|
});
|
||||||
|
Assert.Equal("2", listResponse.ContinuationToken);
|
||||||
|
|
||||||
var expectedContinuationToken = new Dictionary<string, object>
|
await sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
||||||
{
|
.Received(1)
|
||||||
{ "priority", notificationStatusDetailsList[9].Priority },
|
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(),
|
||||||
{ "date", notificationStatusDetailsList[9].CreationDate }
|
Arg.Is<PageOptions>(pageOptions =>
|
||||||
};
|
pageOptions.ContinuationToken == null && pageOptions.PageSize == 10));
|
||||||
var expectedJsonContinuationToken = JsonSerializer.Serialize(expectedContinuationToken);
|
|
||||||
var expectedBase64EncodedJsonContinuationToken =
|
|
||||||
CoreHelpers.Base64UrlEncodeString(expectedJsonContinuationToken);
|
|
||||||
Assert.Equal(expectedBase64EncodedJsonContinuationToken, listResponse.ContinuationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
[NotificationStatusDetailsListCustomize(19)]
|
[NotificationStatusDetailsListCustomize(19)]
|
||||||
public async Task List_PagingUsingContinuationToken_ReturnedLast9MatchingNotifications(
|
public async Task List_PagingRequestUsingContinuationToken_ReturnedLast9MatchingNotifications(
|
||||||
SutProvider<NotificationsController> sutProvider,
|
SutProvider<NotificationsController> sutProvider,
|
||||||
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
|
IEnumerable<NotificationStatusDetails> notificationStatusDetailsEnumerable)
|
||||||
{
|
{
|
||||||
@ -140,25 +139,15 @@ public class NotificationsControllerTest
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
sutProvider.GetDependency<IGetNotificationStatusDetailsForUserQuery>()
|
||||||
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>())
|
.GetByUserIdStatusFilterAsync(Arg.Any<NotificationStatusFilter>(), Arg.Any<PageOptions>())
|
||||||
.Returns(notificationStatusDetailsList);
|
.Returns(new PagedResult<NotificationStatusDetails>
|
||||||
|
{ Data = notificationStatusDetailsList.Skip(10).ToList() });
|
||||||
|
|
||||||
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
|
var expectedNotificationStatusDetailsMap = notificationStatusDetailsList
|
||||||
.Skip(10)
|
.Skip(10)
|
||||||
.ToDictionary(n => n.Id);
|
.ToDictionary(n => n.Id);
|
||||||
|
|
||||||
var continuationToken = new Dictionary<string, object>
|
var listResponse = await sutProvider.Sut.List(new NotificationFilterRequestModel { ContinuationToken = "2" });
|
||||||
{
|
|
||||||
{ "priority", notificationStatusDetailsList[9].Priority },
|
|
||||||
{ "date", notificationStatusDetailsList[9].CreationDate }
|
|
||||||
};
|
|
||||||
var jsonContinuationToken = JsonSerializer.Serialize(continuationToken);
|
|
||||||
var base64EncodedJsonContinuationToken = CoreHelpers.Base64UrlEncodeString(jsonContinuationToken);
|
|
||||||
|
|
||||||
var listResponse = await sutProvider.Sut.List(new NotificationFilterRequestModel
|
|
||||||
{
|
|
||||||
ContinuationToken = base64EncodedJsonContinuationToken
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.Equal("list", listResponse.Object);
|
Assert.Equal("list", listResponse.Object);
|
||||||
Assert.Equal(9, listResponse.Data.Count());
|
Assert.Equal(9, listResponse.Data.Count());
|
||||||
@ -177,6 +166,12 @@ public class NotificationsControllerTest
|
|||||||
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
|
Assert.Equal(expectedNotificationStatusDetails.DeletedDate, notificationResponseModel.DeletedDate);
|
||||||
});
|
});
|
||||||
Assert.Null(listResponse.ContinuationToken);
|
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]
|
[Theory]
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Data;
|
using Bit.Core.NotificationCenter.Models.Data;
|
||||||
using Bit.Core.NotificationCenter.Models.Filter;
|
using Bit.Core.NotificationCenter.Models.Filter;
|
||||||
using Bit.Core.NotificationCenter.Queries;
|
using Bit.Core.NotificationCenter.Queries;
|
||||||
@ -19,37 +20,49 @@ namespace Bit.Core.Test.NotificationCenter.Queries;
|
|||||||
public class GetNotificationStatusDetailsForUserQueryTest
|
public class GetNotificationStatusDetailsForUserQueryTest
|
||||||
{
|
{
|
||||||
private static void Setup(SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
|
private static void Setup(SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
|
||||||
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter statusFilter, Guid? userId)
|
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter statusFilter, Guid? userId,
|
||||||
|
PageOptions pageOptions, string? continuationToken)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<INotificationRepository>().GetByUserIdAndStatusAsync(
|
sutProvider.GetDependency<INotificationRepository>()
|
||||||
userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any<ClientType>(), statusFilter)
|
.GetByUserIdAndStatusAsync(userId.GetValueOrDefault(Guid.NewGuid()), Arg.Any<ClientType>(), statusFilter,
|
||||||
.Returns(notificationsStatusDetails);
|
pageOptions)
|
||||||
|
.Returns(new PagedResult<NotificationStatusDetails>
|
||||||
|
{
|
||||||
|
Data = notificationsStatusDetails,
|
||||||
|
ContinuationToken = continuationToken
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task GetByUserIdStatusFilterAsync_NotLoggedIn_NotFoundException(
|
public async Task GetByUserIdStatusFilterAsync_NotLoggedIn_NotFoundException(
|
||||||
SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
|
SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
|
||||||
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter)
|
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter,
|
||||||
|
PageOptions pageOptions, string? continuationToken)
|
||||||
{
|
{
|
||||||
Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, userId: null);
|
Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, userId: null, pageOptions,
|
||||||
|
continuationToken);
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(() =>
|
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||||
sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter));
|
sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter, pageOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task GetByUserIdStatusFilterAsync_NotificationsFound_Returned(
|
public async Task GetByUserIdStatusFilterAsync_NotificationsFound_Returned(
|
||||||
SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
|
SutProvider<GetNotificationStatusDetailsForUserQuery> sutProvider,
|
||||||
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter)
|
List<NotificationStatusDetails> notificationsStatusDetails, NotificationStatusFilter notificationStatusFilter,
|
||||||
|
PageOptions pageOptions, string? continuationToken)
|
||||||
{
|
{
|
||||||
Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, Guid.NewGuid());
|
Setup(sutProvider, notificationsStatusDetails, notificationStatusFilter, Guid.NewGuid(), pageOptions,
|
||||||
|
continuationToken);
|
||||||
|
|
||||||
var actualNotificationsStatusDetails =
|
var actualNotificationsStatusDetailsPagedResult =
|
||||||
await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter);
|
await sutProvider.Sut.GetByUserIdStatusFilterAsync(notificationStatusFilter, pageOptions);
|
||||||
|
|
||||||
Assert.Equal(notificationsStatusDetails, actualNotificationsStatusDetails);
|
Assert.NotNull(actualNotificationsStatusDetailsPagedResult);
|
||||||
|
Assert.Equal(notificationsStatusDetails, actualNotificationsStatusDetailsPagedResult.Data);
|
||||||
|
Assert.Equal(continuationToken, actualNotificationsStatusDetailsPagedResult.ContinuationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
-- Stored Procedure Notification_ReadByUserIdAndStatus
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Notification_ReadByUserIdAndStatus]
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@ClientType TINYINT,
|
||||||
|
@Read BIT,
|
||||||
|
@Deleted BIT,
|
||||||
|
@PageNumber INT = 1,
|
||||||
|
@PageSize INT = 10
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT n.*
|
||||||
|
FROM [dbo].[NotificationStatusDetailsView] n
|
||||||
|
LEFT JOIN [dbo].[OrganizationUserView] ou ON n.[OrganizationId] = ou.[OrganizationId]
|
||||||
|
AND ou.[UserId] = @UserId
|
||||||
|
WHERE (n.[NotificationStatusUserId] IS NULL OR n.[NotificationStatusUserId] = @UserId)
|
||||||
|
AND [ClientType] IN (0, CASE WHEN @ClientType != 0 THEN @ClientType END)
|
||||||
|
AND ([Global] = 1
|
||||||
|
OR (n.[UserId] = @UserId
|
||||||
|
AND (n.[OrganizationId] IS NULL
|
||||||
|
OR ou.[OrganizationId] IS NOT NULL))
|
||||||
|
OR (n.[UserId] IS NULL
|
||||||
|
AND ou.[OrganizationId] IS NOT NULL))
|
||||||
|
AND ((@Read IS NULL AND @Deleted IS NULL)
|
||||||
|
OR (n.[NotificationStatusUserId] IS NOT NULL
|
||||||
|
AND ((@Read IS NULL
|
||||||
|
OR IIF((@Read = 1 AND n.[ReadDate] IS NOT NULL) OR
|
||||||
|
(@Read = 0 AND n.[ReadDate] IS NULL),
|
||||||
|
1, 0) = 1)
|
||||||
|
OR (@Deleted IS NULL
|
||||||
|
OR IIF((@Deleted = 1 AND n.[DeletedDate] IS NOT NULL) OR
|
||||||
|
(@Deleted = 0 AND n.[DeletedDate] IS NULL),
|
||||||
|
1, 0) = 1))))
|
||||||
|
ORDER BY [Priority] DESC, n.[CreationDate] DESC
|
||||||
|
OFFSET @PageSize * (@PageNumber - 1) ROWS
|
||||||
|
FETCH NEXT @PageSize ROWS ONLY
|
||||||
|
END
|
||||||
|
GO
|
Loading…
Reference in New Issue
Block a user