1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[PM-10564] Push notification updates to other clients (#5057)

* PM-10600: Notification push notification

* PM-10600: Sending to specific client types for relay push notifications

* PM-10600: Sending to specific client types for other clients

* PM-10600: Send push notification on notification creation

* PM-10600: Explicit group names

* PM-10600: Id typos

* PM-10600: Revert global push notifications

* PM-10600: Added DeviceType claim

* PM-10600: Sent to organization typo

* PM-10600: UT coverage

* PM-10600: Small refactor, UTs coverage

* PM-10600: UTs coverage

* PM-10600: Startup fix

* PM-10600: Test fix

* PM-10600: Required attribute, organization group for push notification fix

* PM-10600: UT coverage

* PM-10600: Fix Mobile devices not registering to organization push notifications

We only register devices for organization push notifications when the organization is being created. This does not work, since we have a use case (Notification Center) of delivering notifications to all users of organization. This fixes it, by adding the organization id tag when device registers for push notifications.

* PM-10600: Unit Test coverage for NotificationHubPushRegistrationService

Fixed IFeatureService substitute mocking for Android tests.
Added user part of organization test with organizationId tags expectation.

* PM-10600: Unit Tests fix to NotificationHubPushRegistrationService after merge conflict

* PM-10600: Organization push notifications not sending to mobile device from self-hosted.

Self-hosted instance uses relay to register the mobile device against Bitwarden Cloud Api. Only the self-hosted server knows client's organization membership, which means it needs to pass in the organization id's information to the relay. Similarly, for Bitwarden Cloud, the organizaton id will come directly from the server.

* PM-10600: Fix self-hosted organization notification not being received by mobile device.

When mobile device registers on self-hosted through the relay, every single id, like user id, device id and now organization id needs to be prefixed with the installation id. This have been missing in the PushController that handles this for organization id.

* PM-10600: Broken NotificationsController integration test

Device type is now part of JWT access token, so the notification center results in the integration test are now scoped to client type web and all.

* PM-10600: Merge conflicts fix

* merge conflict fix

* PM-10600: Push notification with full notification center content.

Notification Center push notification now includes all the fields.

* PM-10564: Push notification updates to other clients

Cherry-picked and squashed commits:
d9711b6031 6e69c8a0ce 01c814595e 3885885d5f 1285a7e994 fcf346985f 28ff53c293 57804ae27c 1c9339b686

* null check fix

* logging using template formatting
This commit is contained in:
Maciej Zieniuk
2025-02-13 14:23:33 +01:00
committed by GitHub
parent 0c482eea32
commit c3924bbf3b
22 changed files with 646 additions and 126 deletions

View File

@ -41,6 +41,12 @@ public class CreateNotificationCommandTest
Setup(sutProvider, notification, authorized: false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(notification));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
}
[Theory]
@ -59,5 +65,8 @@ public class CreateNotificationCommandTest
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushNotificationAsync(newNotification);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
}
}

View File

@ -5,6 +5,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Push;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -50,6 +51,12 @@ public class CreateNotificationStatusCommandTest
Setup(sutProvider, notification: null, notificationStatus, true, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(notificationStatus));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -61,6 +68,12 @@ public class CreateNotificationStatusCommandTest
Setup(sutProvider, notification, notificationStatus, authorizedNotification: false, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(notificationStatus));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -72,6 +85,12 @@ public class CreateNotificationStatusCommandTest
Setup(sutProvider, notification, notificationStatus, true, authorizedCreate: false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(notificationStatus));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -85,5 +104,11 @@ public class CreateNotificationStatusCommandTest
var newNotificationStatus = await sutProvider.Sut.CreateAsync(notificationStatus);
Assert.Equal(notificationStatus, newNotificationStatus);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushNotificationStatusAsync(notification, notificationStatus);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
}

View File

@ -6,6 +6,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Push;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -63,6 +64,12 @@ public class MarkNotificationDeletedCommandTest
Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -74,6 +81,12 @@ public class MarkNotificationDeletedCommandTest
Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -86,6 +99,12 @@ public class MarkNotificationDeletedCommandTest
true, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -98,6 +117,12 @@ public class MarkNotificationDeletedCommandTest
authorizedCreate: false, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -110,6 +135,12 @@ public class MarkNotificationDeletedCommandTest
authorizedUpdate: false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkDeletedAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -119,13 +150,25 @@ public class MarkNotificationDeletedCommandTest
Guid notificationId, Guid userId, Notification notification)
{
Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true);
var expectedNotificationStatus = new NotificationStatus
{
NotificationId = notificationId,
UserId = userId,
ReadDate = null,
DeletedDate = DateTime.UtcNow
};
await sutProvider.Sut.MarkDeletedAsync(notificationId);
await sutProvider.GetDependency<INotificationStatusRepository>().Received(1)
.CreateAsync(Arg.Is<NotificationStatus>(ns =>
ns.NotificationId == notificationId && ns.UserId == userId && !ns.ReadDate.HasValue &&
ns.DeletedDate.HasValue && DateTime.UtcNow - ns.DeletedDate.Value < TimeSpan.FromMinutes(1)));
.CreateAsync(Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(expectedNotificationStatus, ns)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushNotificationStatusAsync(notification,
Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(expectedNotificationStatus, ns)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -134,18 +177,30 @@ public class MarkNotificationDeletedCommandTest
SutProvider<MarkNotificationDeletedCommand> 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<INotificationStatusRepository>().Received(1)
.UpdateAsync(Arg.Is<NotificationStatus>(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)));
.UpdateAsync(Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(notificationStatus, ns)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushNotificationStatusAsync(notification,
Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(notificationStatus, ns)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
private static void AssertNotificationStatus(NotificationStatus expectedNotificationStatus,
NotificationStatus? actualNotificationStatus)
{
Assert.NotNull(actualNotificationStatus);
Assert.Equal(expectedNotificationStatus.NotificationId, actualNotificationStatus.NotificationId);
Assert.Equal(expectedNotificationStatus.UserId, actualNotificationStatus.UserId);
Assert.Equal(expectedNotificationStatus.ReadDate, actualNotificationStatus.ReadDate);
Assert.NotEqual(expectedNotificationStatus.DeletedDate, actualNotificationStatus.DeletedDate);
Assert.NotNull(actualNotificationStatus.DeletedDate);
Assert.Equal(DateTime.UtcNow, actualNotificationStatus.DeletedDate.Value, TimeSpan.FromMinutes(1));
}
}

View File

@ -6,6 +6,7 @@ using Bit.Core.NotificationCenter.Authorization;
using Bit.Core.NotificationCenter.Commands;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Push;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -63,6 +64,12 @@ public class MarkNotificationReadCommandTest
Setup(sutProvider, notificationId, userId: null, notification, notificationStatus, true, true, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -74,6 +81,12 @@ public class MarkNotificationReadCommandTest
Setup(sutProvider, notificationId, userId, notification: null, notificationStatus, true, true, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -86,6 +99,12 @@ public class MarkNotificationReadCommandTest
true, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -98,6 +117,12 @@ public class MarkNotificationReadCommandTest
authorizedCreate: false, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -110,6 +135,12 @@ public class MarkNotificationReadCommandTest
authorizedUpdate: false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.MarkReadAsync(notificationId));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -119,13 +150,25 @@ public class MarkNotificationReadCommandTest
Guid notificationId, Guid userId, Notification notification)
{
Setup(sutProvider, notificationId, userId, notification, notificationStatus: null, true, true, true);
var expectedNotificationStatus = new NotificationStatus
{
NotificationId = notificationId,
UserId = userId,
ReadDate = DateTime.UtcNow,
DeletedDate = null
};
await sutProvider.Sut.MarkReadAsync(notificationId);
await sutProvider.GetDependency<INotificationStatusRepository>().Received(1)
.CreateAsync(Arg.Is<NotificationStatus>(ns =>
ns.NotificationId == notificationId && ns.UserId == userId && !ns.DeletedDate.HasValue &&
ns.ReadDate.HasValue && DateTime.UtcNow - ns.ReadDate.Value < TimeSpan.FromMinutes(1)));
.CreateAsync(Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(expectedNotificationStatus, ns)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushNotificationStatusAsync(notification,
Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(expectedNotificationStatus, ns)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
[Theory]
@ -134,18 +177,30 @@ public class MarkNotificationReadCommandTest
SutProvider<MarkNotificationReadCommand> 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<INotificationStatusRepository>().Received(1)
.UpdateAsync(Arg.Is<NotificationStatus>(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)));
.UpdateAsync(Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(notificationStatus, ns)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushNotificationStatusAsync(notification,
Arg.Do<NotificationStatus>(ns => AssertNotificationStatus(notificationStatus, ns)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
}
private static void AssertNotificationStatus(NotificationStatus expectedNotificationStatus,
NotificationStatus? actualNotificationStatus)
{
Assert.NotNull(actualNotificationStatus);
Assert.Equal(expectedNotificationStatus.NotificationId, actualNotificationStatus.NotificationId);
Assert.Equal(expectedNotificationStatus.UserId, actualNotificationStatus.UserId);
Assert.NotEqual(expectedNotificationStatus.ReadDate, actualNotificationStatus.ReadDate);
Assert.NotNull(actualNotificationStatus.ReadDate);
Assert.Equal(DateTime.UtcNow, actualNotificationStatus.ReadDate.Value, TimeSpan.FromMinutes(1));
Assert.Equal(expectedNotificationStatus.DeletedDate, actualNotificationStatus.DeletedDate);
}
}

View File

@ -7,6 +7,7 @@ using Bit.Core.NotificationCenter.Commands;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Push;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
@ -45,6 +46,12 @@ public class UpdateNotificationCommandTest
Setup(sutProvider, notification.Id, notification: null, true);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(notification));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
}
[Theory]
@ -56,6 +63,12 @@ public class UpdateNotificationCommandTest
Setup(sutProvider, notification.Id, notification, authorized: false);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(notification));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationAsync(Arg.Any<Notification>());
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
}
[Theory]
@ -91,5 +104,11 @@ public class UpdateNotificationCommandTest
n.Priority == notificationToUpdate.Priority && n.ClientType == notificationToUpdate.ClientType &&
n.Title == notificationToUpdate.Title && n.Body == notificationToUpdate.Body &&
DateTime.UtcNow - n.RevisionDate < TimeSpan.FromMinutes(1)));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushNotificationAsync(notification);
await sutProvider.GetDependency<IPushNotificationService>()
.Received(0)
.PushNotificationStatusAsync(Arg.Any<Notification>(), Arg.Any<NotificationStatus>());
}
}