mirror of
https://github.com/bitwarden/server.git
synced 2025-02-18 02:11:22 +01:00
Merge branch 'refs/heads/main' into km/pm-10600
# Conflicts: # src/Core/NotificationHub/NotificationHubPushNotificationService.cs # src/Core/NotificationHub/NotificationHubPushRegistrationService.cs # src/Core/Services/Implementations/MultiServicePushNotificationService.cs
This commit is contained in:
commit
e9d1052545
23
.github/CODEOWNERS
vendored
23
.github/CODEOWNERS
vendored
@ -4,13 +4,22 @@
|
|||||||
#
|
#
|
||||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
|
||||||
# DevOps for Actions and other workflow changes
|
## Docker files have shared ownership ##
|
||||||
.github/workflows @bitwarden/dept-devops
|
**/Dockerfile
|
||||||
|
**/*.Dockerfile
|
||||||
|
**/.dockerignore
|
||||||
|
**/entrypoint.sh
|
||||||
|
|
||||||
# DevOps for Docker changes
|
## BRE team owns these workflows ##
|
||||||
**/Dockerfile @bitwarden/dept-devops
|
.github/workflows/publish.yml @bitwarden/dept-bre
|
||||||
**/*.Dockerfile @bitwarden/dept-devops
|
|
||||||
**/.dockerignore @bitwarden/dept-devops
|
## These are shared workflows ##
|
||||||
|
.github/workflows/_move_finalization_db_scripts.yml
|
||||||
|
.github/workflows/build.yml
|
||||||
|
.github/workflows/cleanup-after-pr.yml
|
||||||
|
.github/workflows/cleanup-rc-branch.yml
|
||||||
|
.github/workflows/release.yml
|
||||||
|
.github/workflows/repository-management.yml
|
||||||
|
|
||||||
# Database Operations for database changes
|
# Database Operations for database changes
|
||||||
src/Sql/** @bitwarden/dept-dbops
|
src/Sql/** @bitwarden/dept-dbops
|
||||||
@ -60,6 +69,6 @@ src/EventsProcessor @bitwarden/team-admin-console-dev
|
|||||||
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
|
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
|
||||||
src/Admin/Views/Tools @bitwarden/team-billing-dev
|
src/Admin/Views/Tools @bitwarden/team-billing-dev
|
||||||
|
|
||||||
# Multiple owners - DO NOT REMOVE (DevOps)
|
# Multiple owners - DO NOT REMOVE (BRE)
|
||||||
**/packages.lock.json
|
**/packages.lock.json
|
||||||
Directory.Build.props
|
Directory.Build.props
|
||||||
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -594,7 +594,7 @@ jobs:
|
|||||||
workflow_id: '_update_ephemeral_tags.yml',
|
workflow_id: '_update_ephemeral_tags.yml',
|
||||||
ref: 'main',
|
ref: 'main',
|
||||||
inputs: {
|
inputs: {
|
||||||
ephemeral_env_branch: '${{ github.head_ref }}'
|
ephemeral_env_branch: process.env.GITHUB_HEAD_REF
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
59
.github/workflows/cleanup-ephemeral-environment.yml
vendored
Normal file
59
.github/workflows/cleanup-ephemeral-environment.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
name: Ephemeral environment cleanup
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [unlabeled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-pr:
|
||||||
|
name: Validate PR
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
outputs:
|
||||||
|
config-exists: ${{ steps.validate-config.outputs.config-exists }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout PR
|
||||||
|
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||||
|
|
||||||
|
- name: Validate config exists in path
|
||||||
|
id: validate-config
|
||||||
|
run: |
|
||||||
|
if [[ -f "ephemeral-environments/$GITHUB_HEAD_REF.yaml" ]]; then
|
||||||
|
echo "Ephemeral environment config found in path, continuing."
|
||||||
|
echo "config-exists=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
cleanup-config:
|
||||||
|
name: Cleanup ephemeral environment
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs: validate-pr
|
||||||
|
if: ${{ needs.validate-pr.outputs.config-exists }}
|
||||||
|
steps:
|
||||||
|
- name: Log in to Azure - CI subscription
|
||||||
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Retrieve GitHub PAT secrets
|
||||||
|
id: retrieve-secret-pat
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-ci"
|
||||||
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Trigger Ephemeral Environment cleanup
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
script: |
|
||||||
|
await github.rest.actions.createWorkflowDispatch({
|
||||||
|
owner: 'bitwarden',
|
||||||
|
repo: 'devops',
|
||||||
|
workflow_id: '_ephemeral_environment_pr_manager.yml',
|
||||||
|
ref: 'main',
|
||||||
|
inputs: {
|
||||||
|
ephemeral_env_branch: process.env.GITHUB_HEAD_REF,
|
||||||
|
cleanup_config: true,
|
||||||
|
project: 'server'
|
||||||
|
}
|
||||||
|
})
|
6
.github/workflows/enforce-labels.yml
vendored
6
.github/workflows/enforce-labels.yml
vendored
@ -6,13 +6,13 @@ on:
|
|||||||
types: [labeled, unlabeled, opened, reopened, synchronize]
|
types: [labeled, unlabeled, opened, reopened, synchronize]
|
||||||
jobs:
|
jobs:
|
||||||
enforce-label:
|
enforce-label:
|
||||||
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') }}
|
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') || contains(github.event.*.labels.*.name, 'ephemeral-environment') }}
|
||||||
name: Enforce label
|
name: Enforce label
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check for label
|
- name: Check for label
|
||||||
run: |
|
run: |
|
||||||
echo "PRs with the hold or needs-qa labels cannot be merged"
|
echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged"
|
||||||
echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -46,7 +46,7 @@ public class PushController : Controller
|
|||||||
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
|
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
|
||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type);
|
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("add-organization")]
|
[HttpPut("add-organization")]
|
||||||
@ -54,7 +54,7 @@ public class PushController : Controller
|
|||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
|
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
|
||||||
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
|
model.Devices.Select(d => Prefix(d.Id)),
|
||||||
Prefix(model.OrganizationId));
|
Prefix(model.OrganizationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ public class PushController : Controller
|
|||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
|
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
|
||||||
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
|
model.Devices.Select(d => Prefix(d.Id)),
|
||||||
Prefix(model.OrganizationId));
|
Prefix(model.OrganizationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,12 +162,12 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId)
|
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||||
return devices
|
return devices
|
||||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||||
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type));
|
.Select(d => d.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
||||||
|
@ -1838,12 +1838,12 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId)
|
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||||
return devices
|
return devices
|
||||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||||
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type));
|
.Select(d => d.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api;
|
namespace Bit.Core.Models.Api;
|
||||||
|
|
||||||
@ -7,6 +6,4 @@ public class PushDeviceRequestModel
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
[Required]
|
|
||||||
public DeviceType Type { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api;
|
namespace Bit.Core.Models.Api;
|
||||||
|
|
||||||
@ -8,9 +7,9 @@ public class PushUpdateRequestModel
|
|||||||
public PushUpdateRequestModel()
|
public PushUpdateRequestModel()
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public PushUpdateRequestModel(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
public PushUpdateRequestModel(IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
Devices = devices.Select(d => new PushDeviceRequestModel { Id = d.Key, Type = d.Value });
|
Devices = deviceIds.Select(d => new PushDeviceRequestModel { Id = d });
|
||||||
OrganizationId = organizationId;
|
OrganizationId = organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,4 +37,25 @@ public class InstallationDeviceEntity : ITableEntity
|
|||||||
{
|
{
|
||||||
return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';
|
return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';
|
||||||
}
|
}
|
||||||
|
public static bool TryParse(string deviceId, out InstallationDeviceEntity installationDeviceEntity)
|
||||||
|
{
|
||||||
|
installationDeviceEntity = null;
|
||||||
|
var installationId = Guid.Empty;
|
||||||
|
var deviceIdGuid = Guid.Empty;
|
||||||
|
if (!IsInstallationDeviceId(deviceId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var parts = deviceId.Split("_");
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Guid.TryParse(parts[0], out installationId) || !Guid.TryParse(parts[1], out deviceIdGuid))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
installationDeviceEntity = new InstallationDeviceEntity(installationId, deviceIdGuid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
8
src/Core/NotificationHub/INotificationHubClientProxy.cs
Normal file
8
src/Core/NotificationHub/INotificationHubClientProxy.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.Azure.NotificationHubs;
|
||||||
|
|
||||||
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
|
public interface INotificationHubProxy
|
||||||
|
{
|
||||||
|
Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression);
|
||||||
|
}
|
9
src/Core/NotificationHub/INotificationHubPool.cs
Normal file
9
src/Core/NotificationHub/INotificationHubPool.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.Azure.NotificationHubs;
|
||||||
|
|
||||||
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
|
public interface INotificationHubPool
|
||||||
|
{
|
||||||
|
NotificationHubClient ClientFor(Guid comb);
|
||||||
|
INotificationHubProxy AllClients { get; }
|
||||||
|
}
|
26
src/Core/NotificationHub/NotificationHubClientProxy.cs
Normal file
26
src/Core/NotificationHub/NotificationHubClientProxy.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.Azure.NotificationHubs;
|
||||||
|
|
||||||
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
|
public class NotificationHubClientProxy : INotificationHubProxy
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<INotificationHubClient> _clients;
|
||||||
|
|
||||||
|
public NotificationHubClientProxy(IEnumerable<INotificationHubClient> clients)
|
||||||
|
{
|
||||||
|
_clients = clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(INotificationHubClient, T)[]> ApplyToAllClientsAsync<T>(Func<INotificationHubClient, Task<T>> action)
|
||||||
|
{
|
||||||
|
var tasks = _clients.Select(async c => (c, await action(c)));
|
||||||
|
return await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
// partial proxy of INotificationHubClient implementation
|
||||||
|
// Note: Any other methods that are needed can simply be delegated as done here.
|
||||||
|
public async Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression)
|
||||||
|
{
|
||||||
|
return await ApplyToAllClientsAsync(async c => await c.SendTemplateNotificationAsync(properties, tagExpression));
|
||||||
|
}
|
||||||
|
}
|
128
src/Core/NotificationHub/NotificationHubConnection.cs
Normal file
128
src/Core/NotificationHub/NotificationHubConnection.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Azure.NotificationHubs;
|
||||||
|
|
||||||
|
class NotificationHubConnection
|
||||||
|
{
|
||||||
|
public string HubName { get; init; }
|
||||||
|
public string ConnectionString { get; init; }
|
||||||
|
public bool EnableSendTracing { get; init; }
|
||||||
|
private NotificationHubClient _hubClient;
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the NotificationHubClient for this connection.
|
||||||
|
///
|
||||||
|
/// If the client is null, it will be initialized.
|
||||||
|
///
|
||||||
|
/// <throws>Exception</throws> if the connection is invalid.
|
||||||
|
/// </summary>
|
||||||
|
public NotificationHubClient HubClient
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_hubClient == null)
|
||||||
|
{
|
||||||
|
if (!IsValid)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid notification hub settings");
|
||||||
|
}
|
||||||
|
Init();
|
||||||
|
}
|
||||||
|
return _hubClient;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_hubClient = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the start date for registration.
|
||||||
|
///
|
||||||
|
/// If null, registration is always disabled.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RegistrationStartDate { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the end date for registration.
|
||||||
|
///
|
||||||
|
/// If null, registration has no end date.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RegistrationEndDate { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether all data needed to generate a connection to Notification Hub is present.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsValid
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
{
|
||||||
|
var invalid = string.IsNullOrWhiteSpace(HubName) || string.IsNullOrWhiteSpace(ConnectionString);
|
||||||
|
return !invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string LogString
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return $"HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether registration is enabled for the given comb ID.
|
||||||
|
/// This is based off of the generation time encoded in the comb ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="comb"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool RegistrationEnabled(Guid comb)
|
||||||
|
{
|
||||||
|
var combTime = CoreHelpers.DateFromComb(comb);
|
||||||
|
return RegistrationEnabled(combTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether registration is enabled for the given time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queryTime">The time to check</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public bool RegistrationEnabled(DateTime queryTime)
|
||||||
|
{
|
||||||
|
if (queryTime >= RegistrationEndDate || RegistrationStartDate == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RegistrationStartDate < queryTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NotificationHubConnection() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new NotificationHubConnection from the given settings.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static NotificationHubConnection From(GlobalSettings.NotificationHubSettings settings)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
HubName = settings.HubName,
|
||||||
|
ConnectionString = settings.ConnectionString,
|
||||||
|
EnableSendTracing = settings.EnableSendTracing,
|
||||||
|
// Comb time is not precise enough for millisecond accuracy
|
||||||
|
RegistrationStartDate = settings.RegistrationStartDate.HasValue ? Truncate(settings.RegistrationStartDate.Value, TimeSpan.FromMilliseconds(10)) : null,
|
||||||
|
RegistrationEndDate = settings.RegistrationEndDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private NotificationHubConnection Init()
|
||||||
|
{
|
||||||
|
HubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime Truncate(DateTime dateTime, TimeSpan resolution)
|
||||||
|
{
|
||||||
|
return dateTime.AddTicks(-(dateTime.Ticks % resolution.Ticks));
|
||||||
|
}
|
||||||
|
}
|
62
src/Core/NotificationHub/NotificationHubPool.cs
Normal file
62
src/Core/NotificationHub/NotificationHubPool.cs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Azure.NotificationHubs;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
|
public class NotificationHubPool : INotificationHubPool
|
||||||
|
{
|
||||||
|
private List<NotificationHubConnection> _connections { get; }
|
||||||
|
private readonly IEnumerable<INotificationHubClient> _clients;
|
||||||
|
private readonly ILogger<NotificationHubPool> _logger;
|
||||||
|
public NotificationHubPool(ILogger<NotificationHubPool> logger, GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_connections = FilterInvalidHubs(globalSettings.NotificationHubPool.NotificationHubs);
|
||||||
|
_clients = _connections.GroupBy(c => c.ConnectionString).Select(g => g.First().HubClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NotificationHubConnection> FilterInvalidHubs(IEnumerable<GlobalSettings.NotificationHubSettings> hubs)
|
||||||
|
{
|
||||||
|
List<NotificationHubConnection> result = new();
|
||||||
|
_logger.LogDebug("Filtering {HubCount} notification hubs", hubs.Count());
|
||||||
|
foreach (var hub in hubs)
|
||||||
|
{
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
if (!connection.IsValid)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid notification hub settings: {HubName}", hub.HubName ?? "hub name missing");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_logger.LogDebug("Adding notification hub: {ConnectionLogString}", connection.LogString);
|
||||||
|
result.Add(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the NotificationHubClient for the given comb ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="comb"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown when no notification hub is found for a given comb.</exception>
|
||||||
|
public NotificationHubClient ClientFor(Guid comb)
|
||||||
|
{
|
||||||
|
var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray();
|
||||||
|
if (possibleConnections.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"No valid notification hubs are available for the given comb ({comb}).\n" +
|
||||||
|
$"The comb's datetime is {CoreHelpers.DateFromComb(comb)}." +
|
||||||
|
$"Hub start and end times are configured as follows:\n" +
|
||||||
|
string.Join("\n", _connections.Select(c => $"Hub {c.HubName} - Start: {c.RegistrationStartDate}, End: {c.RegistrationEndDate}")));
|
||||||
|
}
|
||||||
|
var resolvedConnection = possibleConnections[CoreHelpers.BinForComb(comb, possibleConnections.Length)];
|
||||||
|
_logger.LogTrace("Resolved notification hub for comb {Comb} out of {HubCount} hubs.\n{ConnectionInfo}", comb, possibleConnections.Length, resolvedConnection.LogString);
|
||||||
|
return resolvedConnection.HubClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public INotificationHubProxy AllClients { get { return new NotificationHubClientProxy(_clients); } }
|
||||||
|
}
|
@ -6,46 +6,32 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Azure.NotificationHubs;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Notification = Bit.Core.NotificationCenter.Entities.Notification;
|
using Notification = Bit.Core.NotificationCenter.Entities.Notification;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
public class NotificationHubPushNotificationService : IPushNotificationService
|
public class NotificationHubPushNotificationService : IPushNotificationService
|
||||||
{
|
{
|
||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly List<NotificationHubClient> _clients = [];
|
|
||||||
private readonly bool _enableTracing = false;
|
private readonly bool _enableTracing = false;
|
||||||
|
private readonly INotificationHubPool _notificationHubPool;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public NotificationHubPushNotificationService(
|
public NotificationHubPushNotificationService(
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
IInstallationDeviceRepository installationDeviceRepository,
|
||||||
GlobalSettings globalSettings,
|
INotificationHubPool notificationHubPool,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ILogger<NotificationsApiPushNotificationService> logger)
|
ILogger<NotificationsApiPushNotificationService> logger)
|
||||||
{
|
{
|
||||||
_installationDeviceRepository = installationDeviceRepository;
|
_installationDeviceRepository = installationDeviceRepository;
|
||||||
_globalSettings = globalSettings;
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_notificationHubPool = notificationHubPool;
|
||||||
foreach (var hub in globalSettings.NotificationHubs)
|
|
||||||
{
|
|
||||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
|
||||||
hub.ConnectionString,
|
|
||||||
hub.HubName,
|
|
||||||
hub.EnableSendTracing);
|
|
||||||
_clients.Add(client);
|
|
||||||
|
|
||||||
_enableTracing = _enableTracing || hub.EnableSendTracing;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,30 +275,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
|||||||
|
|
||||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
||||||
{
|
{
|
||||||
var tasks = new List<Task<NotificationOutcome>>();
|
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
|
||||||
foreach (var client in _clients)
|
new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
var task = client.SendTemplateNotificationAsync(
|
{ "type", ((byte)type).ToString() },
|
||||||
new Dictionary<string, string>
|
{ "payload", JsonSerializer.Serialize(payload) }
|
||||||
{
|
}, tag);
|
||||||
{ "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) }
|
|
||||||
}, tag);
|
|
||||||
tasks.Add(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
|
|
||||||
if (_enableTracing)
|
if (_enableTracing)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < tasks.Count; i++)
|
foreach (var (client, outcome) in results)
|
||||||
{
|
{
|
||||||
if (_clients[i].EnableTestSend)
|
if (!client.EnableTestSend)
|
||||||
{
|
{
|
||||||
var outcome = await tasks[i];
|
continue;
|
||||||
_logger.LogInformation(
|
|
||||||
"Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
|
|
||||||
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
|
||||||
}
|
}
|
||||||
|
_logger.LogInformation("Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
|
||||||
|
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,51 +1,35 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.Azure.NotificationHubs;
|
using Microsoft.Azure.NotificationHubs;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
public class NotificationHubPushRegistrationService : IPushRegistrationService
|
public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||||
{
|
{
|
||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly INotificationHubPool _notificationHubPool;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||||
private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
|
|
||||||
|
|
||||||
public NotificationHubPushRegistrationService(
|
public NotificationHubPushRegistrationService(
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
IInstallationDeviceRepository installationDeviceRepository,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
|
INotificationHubPool notificationHubPool,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ILogger<NotificationHubPushRegistrationService> logger)
|
ILogger<NotificationHubPushRegistrationService> logger)
|
||||||
{
|
{
|
||||||
_installationDeviceRepository = installationDeviceRepository;
|
_installationDeviceRepository = installationDeviceRepository;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_notificationHubPool = notificationHubPool;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
// Is this dirty to do in the ctor?
|
|
||||||
void addHub(NotificationHubType type)
|
|
||||||
{
|
|
||||||
var hubRegistration = globalSettings.NotificationHubs.FirstOrDefault(
|
|
||||||
h => h.HubType == type && h.EnableRegistration);
|
|
||||||
if (hubRegistration != null)
|
|
||||||
{
|
|
||||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
|
||||||
hubRegistration.ConnectionString,
|
|
||||||
hubRegistration.HubName,
|
|
||||||
hubRegistration.EnableSendTracing);
|
|
||||||
_clients.Add(type, client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addHub(NotificationHubType.General);
|
|
||||||
addHub(NotificationHubType.iOS);
|
|
||||||
addHub(NotificationHubType.Android);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||||
@ -118,7 +102,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
||||||
userId, identifier, clientType);
|
userId, identifier, clientType);
|
||||||
|
|
||||||
await GetClient(type).CreateOrUpdateInstallationAsync(installation);
|
await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation);
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||||
{
|
{
|
||||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||||
@ -152,11 +136,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
installation.Templates.Add(fullTemplateId, template);
|
installation.Templates.Add(fullTemplateId, template);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
public async Task DeleteRegistrationAsync(string deviceId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await GetClient(deviceType).DeleteInstallationAsync(deviceId);
|
await ClientFor(GetComb(deviceId)).DeleteInstallationAsync(deviceId);
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||||
{
|
{
|
||||||
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
|
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
|
||||||
@ -168,34 +152,31 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices,
|
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
string organizationId)
|
|
||||||
{
|
{
|
||||||
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
||||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
|
||||||
{
|
{
|
||||||
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
|
||||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices,
|
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
string organizationId)
|
|
||||||
{
|
{
|
||||||
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Remove,
|
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove,
|
||||||
$"organizationId:{organizationId}");
|
$"organizationId:{organizationId}");
|
||||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
|
||||||
{
|
{
|
||||||
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
|
||||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PatchTagsForUserDevicesAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices,
|
private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,
|
||||||
UpdateOperationType op,
|
|
||||||
string tag)
|
string tag)
|
||||||
{
|
{
|
||||||
if (!devices.Any())
|
if (!deviceIds.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -211,12 +192,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
operation.Path += $"/{tag}";
|
operation.Path += $"/{tag}";
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var device in devices)
|
foreach (var deviceId in deviceIds)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await GetClient(device.Value)
|
await ClientFor(GetComb(deviceId)).PatchInstallationAsync(deviceId, new List<PartialUpdateOperation> { operation });
|
||||||
.PatchInstallationAsync(device.Key, new List<PartialUpdateOperation> { operation });
|
|
||||||
}
|
}
|
||||||
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
||||||
{
|
{
|
||||||
@ -225,34 +205,29 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NotificationHubClient GetClient(DeviceType deviceType)
|
private NotificationHubClient ClientFor(Guid deviceId)
|
||||||
{
|
{
|
||||||
var clientType = DeviceTypes.ToClientType(deviceType);
|
return _notificationHubPool.ClientFor(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
var hubType = clientType switch
|
private Guid GetComb(string deviceId)
|
||||||
|
{
|
||||||
|
var deviceIdString = deviceId;
|
||||||
|
InstallationDeviceEntity installationDeviceEntity;
|
||||||
|
Guid deviceIdGuid;
|
||||||
|
if (InstallationDeviceEntity.TryParse(deviceIdString, out installationDeviceEntity))
|
||||||
{
|
{
|
||||||
ClientType.Web => NotificationHubType.GeneralWeb,
|
// Strip off the installation id (PartitionId). RowKey is the ID in the Installation's table.
|
||||||
ClientType.Browser => NotificationHubType.GeneralBrowserExtension,
|
deviceIdString = installationDeviceEntity.RowKey;
|
||||||
ClientType.Desktop => NotificationHubType.GeneralDesktop,
|
|
||||||
ClientType.Mobile => deviceType switch
|
|
||||||
{
|
|
||||||
DeviceType.Android => NotificationHubType.Android,
|
|
||||||
DeviceType.iOS => NotificationHubType.iOS,
|
|
||||||
_ => NotificationHubType.General
|
|
||||||
},
|
|
||||||
_ => NotificationHubType.General
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!_clients.ContainsKey(hubType))
|
|
||||||
{
|
|
||||||
_logger.LogWarning("No hub client for '{0}'. Using general hub instead.", hubType);
|
|
||||||
hubType = NotificationHubType.General;
|
|
||||||
if (!_clients.ContainsKey(hubType))
|
|
||||||
{
|
|
||||||
throw new Exception("No general hub client found.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _clients[hubType];
|
if (Guid.TryParse(deviceIdString, out deviceIdGuid))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception($"Invalid device id {deviceId}.");
|
||||||
|
}
|
||||||
|
return deviceIdGuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,7 +6,7 @@ public interface IPushRegistrationService
|
|||||||
{
|
{
|
||||||
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||||
string identifier, DeviceType type);
|
string identifier, DeviceType type);
|
||||||
Task DeleteRegistrationAsync(string deviceId, DeviceType type);
|
Task DeleteRegistrationAsync(string deviceId);
|
||||||
Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
||||||
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
||||||
}
|
}
|
||||||
|
@ -38,13 +38,13 @@ public class DeviceService : IDeviceService
|
|||||||
public async Task ClearTokenAsync(Device device)
|
public async Task ClearTokenAsync(Device device)
|
||||||
{
|
{
|
||||||
await _deviceRepository.ClearPushTokenAsync(device.Id);
|
await _deviceRepository.ClearPushTokenAsync(device.Id);
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(Device device)
|
public async Task DeleteAsync(Device device)
|
||||||
{
|
{
|
||||||
await _deviceRepository.DeleteAsync(device);
|
await _deviceRepository.DeleteAsync(device);
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||||
|
@ -1,65 +1,32 @@
|
|||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.NotificationCenter.Entities;
|
using Bit.Core.NotificationCenter.Entities;
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
public class MultiServicePushNotificationService : IPushNotificationService
|
public class MultiServicePushNotificationService : IPushNotificationService
|
||||||
{
|
{
|
||||||
private readonly List<IPushNotificationService> _services = new List<IPushNotificationService>();
|
private readonly IEnumerable<IPushNotificationService> _services;
|
||||||
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
||||||
|
|
||||||
public MultiServicePushNotificationService(
|
public MultiServicePushNotificationService(
|
||||||
IHttpClientFactory httpFactory,
|
[FromKeyedServices("implementation")] IEnumerable<IPushNotificationService> services,
|
||||||
IDeviceRepository deviceRepository,
|
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
|
||||||
GlobalSettings globalSettings,
|
|
||||||
IHttpContextAccessor httpContextAccessor,
|
|
||||||
ILogger<MultiServicePushNotificationService> logger,
|
ILogger<MultiServicePushNotificationService> logger,
|
||||||
ILogger<RelayPushNotificationService> relayLogger,
|
GlobalSettings globalSettings)
|
||||||
ILogger<NotificationsApiPushNotificationService> hubLogger)
|
|
||||||
{
|
{
|
||||||
if (globalSettings.SelfHosted)
|
_services = services;
|
||||||
{
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
|
||||||
globalSettings.Installation?.Id != null &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
|
||||||
{
|
|
||||||
_services.Add(new RelayPushNotificationService(httpFactory, deviceRepository, globalSettings,
|
|
||||||
httpContextAccessor, relayLogger));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
|
||||||
{
|
|
||||||
_services.Add(new NotificationsApiPushNotificationService(
|
|
||||||
httpFactory, globalSettings, httpContextAccessor, hubLogger));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var generalHub =
|
|
||||||
globalSettings.NotificationHubs?.FirstOrDefault(h => h.HubType == NotificationHubType.General);
|
|
||||||
if (CoreHelpers.SettingHasValue(generalHub?.ConnectionString))
|
|
||||||
{
|
|
||||||
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
|
|
||||||
globalSettings, httpContextAccessor, hubLogger));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
|
||||||
{
|
|
||||||
_services.Add(new AzureQueuePushNotificationService(globalSettings, httpContextAccessor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_logger.LogInformation("Hub services: {Services}", _services.Count());
|
||||||
|
globalSettings?.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||||
|
@ -38,37 +38,36 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
|
|||||||
await SendAsync(HttpMethod.Post, "push/register", requestModel);
|
await SendAsync(HttpMethod.Post, "push/register", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType type)
|
public async Task DeleteRegistrationAsync(string deviceId)
|
||||||
{
|
{
|
||||||
var requestModel = new PushDeviceRequestModel
|
var requestModel = new PushDeviceRequestModel
|
||||||
{
|
{
|
||||||
Id = deviceId,
|
Id = deviceId,
|
||||||
Type = type,
|
|
||||||
};
|
};
|
||||||
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
|
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddUserRegistrationOrganizationAsync(
|
public async Task AddUserRegistrationOrganizationAsync(
|
||||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
if (!devices.Any())
|
if (!deviceIds.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||||
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserRegistrationOrganizationAsync(
|
public async Task DeleteUserRegistrationOrganizationAsync(
|
||||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
if (!devices.Any())
|
if (!deviceIds.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||||
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ namespace Bit.Core.Services;
|
|||||||
|
|
||||||
public class NoopPushRegistrationService : IPushRegistrationService
|
public class NoopPushRegistrationService : IPushRegistrationService
|
||||||
{
|
{
|
||||||
public Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
public Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
@ -15,12 +15,12 @@ public class NoopPushRegistrationService : IPushRegistrationService
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
public Task DeleteRegistrationAsync(string deviceId)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Core.Auth.Settings;
|
using Bit.Core.Auth.Settings;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Settings.LoggingSettings;
|
using Bit.Core.Settings.LoggingSettings;
|
||||||
|
|
||||||
namespace Bit.Core.Settings;
|
namespace Bit.Core.Settings;
|
||||||
@ -65,7 +64,7 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
|
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
|
||||||
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
|
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
|
||||||
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
|
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
|
||||||
public virtual List<NotificationHubSettings> NotificationHubs { get; set; } = new();
|
public virtual NotificationHubPoolSettings NotificationHubPool { get; set; } = new();
|
||||||
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
||||||
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
||||||
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
|
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
|
||||||
@ -424,7 +423,7 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public string ConnectionString
|
public string ConnectionString
|
||||||
{
|
{
|
||||||
get => _connectionString;
|
get => _connectionString;
|
||||||
set => _connectionString = value.Trim('"');
|
set => _connectionString = value?.Trim('"');
|
||||||
}
|
}
|
||||||
public string HubName { get; set; }
|
public string HubName { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -433,10 +432,32 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableSendTracing { get; set; } = false;
|
public bool EnableSendTracing { get; set; } = false;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// At least one hub configuration should have registration enabled, preferably the General hub as a safety net.
|
/// The date and time at which registration will be enabled.
|
||||||
|
///
|
||||||
|
/// **This value should not be updated once set, as it is used to determine installation location of devices.**
|
||||||
|
///
|
||||||
|
/// If null, registration is disabled.
|
||||||
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableRegistration { get; set; }
|
public DateTime? RegistrationStartDate { get; set; }
|
||||||
public NotificationHubType HubType { get; set; }
|
/// <summary>
|
||||||
|
/// The date and time at which registration will be disabled.
|
||||||
|
///
|
||||||
|
/// **This value should not be updated once set, as it is used to determine installation location of devices.**
|
||||||
|
///
|
||||||
|
/// If null, hub registration has no yet known expiry.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RegistrationEndDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationHubPoolSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// List of Notification Hub settings to use for sending push notifications.
|
||||||
|
///
|
||||||
|
/// Note that hubs on the same namespace share active device limits, so multiple namespaces should be used to increase capacity.
|
||||||
|
/// </summary>
|
||||||
|
public List<NotificationHubSettings> NotificationHubs { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class YubicoSettings
|
public class YubicoSettings
|
||||||
|
@ -76,6 +76,39 @@ public static class CoreHelpers
|
|||||||
return new Guid(guidArray);
|
return new Guid(guidArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static DateTime DateFromComb(Guid combGuid)
|
||||||
|
{
|
||||||
|
var guidArray = combGuid.ToByteArray();
|
||||||
|
var daysArray = new byte[4];
|
||||||
|
var msecsArray = new byte[4];
|
||||||
|
|
||||||
|
Array.Copy(guidArray, guidArray.Length - 6, daysArray, 2, 2);
|
||||||
|
Array.Copy(guidArray, guidArray.Length - 4, msecsArray, 0, 4);
|
||||||
|
|
||||||
|
Array.Reverse(daysArray);
|
||||||
|
Array.Reverse(msecsArray);
|
||||||
|
|
||||||
|
var days = BitConverter.ToInt32(daysArray, 0);
|
||||||
|
var msecs = BitConverter.ToInt32(msecsArray, 0);
|
||||||
|
|
||||||
|
var time = TimeSpan.FromDays(days) + TimeSpan.FromMilliseconds(msecs * 3.333333);
|
||||||
|
return new DateTime(_baseDateTicks + time.Ticks, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static long BinForComb(Guid combGuid, int binCount)
|
||||||
|
{
|
||||||
|
// From System.Web.Util.HashCodeCombiner
|
||||||
|
uint CombineHashCodes(uint h1, byte h2)
|
||||||
|
{
|
||||||
|
return (uint)(((h1 << 5) + h1) ^ h2);
|
||||||
|
}
|
||||||
|
var guidArray = combGuid.ToByteArray();
|
||||||
|
var randomArray = new byte[10];
|
||||||
|
Array.Copy(guidArray, 0, randomArray, 0, 10);
|
||||||
|
var hash = randomArray.Aggregate((uint)randomArray.Length, CombineHashCodes);
|
||||||
|
return hash % binCount;
|
||||||
|
}
|
||||||
|
|
||||||
public static string CleanCertificateThumbprint(string thumbprint)
|
public static string CleanCertificateThumbprint(string thumbprint)
|
||||||
{
|
{
|
||||||
// Clean possible garbage characters from thumbprint copy/paste
|
// Clean possible garbage characters from thumbprint copy/paste
|
||||||
|
@ -25,6 +25,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.NotificationHub;
|
||||||
using Bit.Core.OrganizationFeatures;
|
using Bit.Core.OrganizationFeatures;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Resources;
|
using Bit.Core.Resources;
|
||||||
@ -264,16 +265,30 @@ public static class ServiceCollectionExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
services.AddSingleton<IPushNotificationService, MultiServicePushNotificationService>();
|
services.AddSingleton<IPushNotificationService, MultiServicePushNotificationService>();
|
||||||
if (globalSettings.SelfHosted &&
|
if (globalSettings.SelfHosted)
|
||||||
CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
|
||||||
globalSettings.Installation?.Id != null &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
|
||||||
{
|
{
|
||||||
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
||||||
|
globalSettings.Installation?.Id != null &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
|
||||||
|
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
||||||
|
}
|
||||||
|
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IPushNotificationService, NotificationsApiPushNotificationService>("implementation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (!globalSettings.SelfHosted)
|
else if (!globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<INotificationHubPool, NotificationHubPool>();
|
||||||
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
||||||
|
services.AddKeyedSingleton<IPushNotificationService, NotificationHubPushNotificationService>("implementation");
|
||||||
|
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>("implementation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Org.BouncyCastle.Security;
|
|
||||||
|
|
||||||
namespace Bit.Test.Common.AutoFixture;
|
namespace Bit.Test.Common.AutoFixture;
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ public class ControllerCustomization : ICustomization
|
|||||||
{
|
{
|
||||||
if (!controllerType.IsAssignableTo(typeof(Controller)))
|
if (!controllerType.IsAssignableTo(typeof(Controller)))
|
||||||
{
|
{
|
||||||
throw new InvalidParameterException($"{nameof(controllerType)} must derive from {typeof(Controller).Name}");
|
throw new Exception($"{nameof(controllerType)} must derive from {typeof(Controller).Name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
_controllerType = controllerType;
|
_controllerType = controllerType;
|
||||||
|
205
test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs
Normal file
205
test/Core.Test/NotificationHub/NotificationHubConnectionTests.cs
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.NotificationHub;
|
||||||
|
|
||||||
|
public class NotificationHubConnectionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void IsValid_ConnectionStringIsNull_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = null,
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(connection.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValid_HubNameIsNull_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "Endpoint=sb://example.servicebus.windows.net/;",
|
||||||
|
HubName = null,
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(connection.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsValid_ConnectionStringAndHubNameAreNotNull_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(connection.IsValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationEnabled_QueryTimeIsBeforeStartDate_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow.AddDays(1),
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(2)
|
||||||
|
};
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = connection.RegistrationEnabled(DateTime.UtcNow);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationEnabled_QueryTimeIsAfterEndDate_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = connection.RegistrationEnabled(DateTime.UtcNow.AddDays(2));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationEnabled_NullStartDate_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = null,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = connection.RegistrationEnabled(DateTime.UtcNow);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationEnabled_QueryTimeIsBetweenStartDateAndEndDate_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = connection.RegistrationEnabled(DateTime.UtcNow.AddHours(1));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationEnabled_CombTimeIsBeforeStartDate_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow.AddDays(1),
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(2)
|
||||||
|
};
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationEnabled_CombTimeIsAfterEndDate_ReturnsFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow.AddDays(2)));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationEnabled_CombTimeIsBetweenStartDateAndEndDate_ReturnsTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var hub = new GlobalSettings.NotificationHubSettings()
|
||||||
|
{
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
};
|
||||||
|
var connection = NotificationHubConnection.From(hub);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = connection.RegistrationEnabled(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow.AddHours(1)));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
}
|
||||||
|
}
|
156
test/Core.Test/NotificationHub/NotificationHubPoolTests.cs
Normal file
156
test/Core.Test/NotificationHub/NotificationHubPoolTests.cs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
using Bit.Core.NotificationHub;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using static Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.NotificationHub;
|
||||||
|
|
||||||
|
public class NotificationHubPoolTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void NotificationHubPool_WarnsOnMissingConnectionString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var globalSettings = new GlobalSettings()
|
||||||
|
{
|
||||||
|
NotificationHubPool = new NotificationHubPoolSettings()
|
||||||
|
{
|
||||||
|
NotificationHubs = new() {
|
||||||
|
new() {
|
||||||
|
ConnectionString = null,
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var logger = Substitute.For<ILogger<NotificationHubPool>>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var sut = new NotificationHubPool(logger, globalSettings);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
logger.Received().Log(LogLevel.Warning, Arg.Any<EventId>(),
|
||||||
|
Arg.Is<object>(o => o.ToString() == "Invalid notification hub settings: hub"),
|
||||||
|
null,
|
||||||
|
Arg.Any<Func<object, Exception, string>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NotificationHubPool_WarnsOnMissingHubName()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var globalSettings = new GlobalSettings()
|
||||||
|
{
|
||||||
|
NotificationHubPool = new NotificationHubPoolSettings()
|
||||||
|
{
|
||||||
|
NotificationHubs = new() {
|
||||||
|
new() {
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = null,
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var logger = Substitute.For<ILogger<NotificationHubPool>>();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var sut = new NotificationHubPool(logger, globalSettings);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
logger.Received().Log(LogLevel.Warning, Arg.Any<EventId>(),
|
||||||
|
Arg.Is<object>(o => o.ToString() == "Invalid notification hub settings: hub name missing"),
|
||||||
|
null,
|
||||||
|
Arg.Any<Func<object, Exception, string>>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NotificationHubPool_ClientFor_ThrowsOnNoValidHubs()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var globalSettings = new GlobalSettings()
|
||||||
|
{
|
||||||
|
NotificationHubPool = new NotificationHubPoolSettings()
|
||||||
|
{
|
||||||
|
NotificationHubs = new() {
|
||||||
|
new() {
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = null,
|
||||||
|
RegistrationEndDate = null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var logger = Substitute.For<ILogger<NotificationHubPool>>();
|
||||||
|
var sut = new NotificationHubPool(logger, globalSettings);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Action act = () => sut.ClientFor(Guid.NewGuid());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Throws<InvalidOperationException>(act);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NotificationHubPool_ClientFor_ReturnsClient()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var globalSettings = new GlobalSettings()
|
||||||
|
{
|
||||||
|
NotificationHubPool = new NotificationHubPoolSettings()
|
||||||
|
{
|
||||||
|
NotificationHubs = new() {
|
||||||
|
new() {
|
||||||
|
ConnectionString = "Endpoint=sb://example.servicebus.windows.net/;SharedAccessKey=example///example=",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow.AddMinutes(-1),
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var logger = Substitute.For<ILogger<NotificationHubPool>>();
|
||||||
|
var sut = new NotificationHubPool(logger, globalSettings);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var client = sut.ClientFor(CoreHelpers.GenerateComb(Guid.NewGuid(), DateTime.UtcNow));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NotificationHubPool_AllClients_ReturnsProxy()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var globalSettings = new GlobalSettings()
|
||||||
|
{
|
||||||
|
NotificationHubPool = new NotificationHubPoolSettings()
|
||||||
|
{
|
||||||
|
NotificationHubs = new() {
|
||||||
|
new() {
|
||||||
|
ConnectionString = "connection",
|
||||||
|
HubName = "hub",
|
||||||
|
RegistrationStartDate = DateTime.UtcNow,
|
||||||
|
RegistrationEndDate = DateTime.UtcNow.AddDays(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var logger = Substitute.For<ILogger<NotificationHubPool>>();
|
||||||
|
var sut = new NotificationHubPool(logger, globalSettings);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var proxy = sut.AllClients;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(proxy);
|
||||||
|
}
|
||||||
|
}
|
40
test/Core.Test/NotificationHub/NotificationHubProxyTests.cs
Normal file
40
test/Core.Test/NotificationHub/NotificationHubProxyTests.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using AutoFixture;
|
||||||
|
using Bit.Core.NotificationHub;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Microsoft.Azure.NotificationHubs;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.NotificationHub;
|
||||||
|
|
||||||
|
public class NotificationHubProxyTests
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<INotificationHubClient> _clients;
|
||||||
|
public NotificationHubProxyTests()
|
||||||
|
{
|
||||||
|
_clients = new Fixture().WithAutoNSubstitutions().CreateMany<INotificationHubClient>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> ClientMethods =
|
||||||
|
[
|
||||||
|
[
|
||||||
|
(NotificationHubClientProxy c) => c.SendTemplateNotificationAsync(new Dictionary<string, string>() { { "key", "value" } }, "tag"),
|
||||||
|
(INotificationHubClient c) => c.SendTemplateNotificationAsync(Arg.Is<Dictionary<string, string>>((a) => a.Keys.Count == 1 && a.ContainsKey("key") && a["key"] == "value"), "tag"),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ClientMethods))]
|
||||||
|
public async void CallsAllClients(Func<NotificationHubClientProxy, Task> proxyMethod, Func<INotificationHubClient, Task> clientMethod)
|
||||||
|
{
|
||||||
|
var clients = _clients.ToArray();
|
||||||
|
var proxy = new NotificationHubClientProxy(clients);
|
||||||
|
|
||||||
|
await proxyMethod(proxy);
|
||||||
|
|
||||||
|
foreach (var client in clients)
|
||||||
|
{
|
||||||
|
await clientMethod(client.Received());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,32 +1,32 @@
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.NotificationHub;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services;
|
namespace Bit.Core.Test.NotificationHub;
|
||||||
|
|
||||||
public class NotificationHubPushNotificationServiceTests
|
public class NotificationHubPushNotificationServiceTests
|
||||||
{
|
{
|
||||||
private readonly NotificationHubPushNotificationService _sut;
|
private readonly NotificationHubPushNotificationService _sut;
|
||||||
|
|
||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly INotificationHubPool _notificationHubPool;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly ILogger<NotificationsApiPushNotificationService> _logger;
|
private readonly ILogger<NotificationsApiPushNotificationService> _logger;
|
||||||
|
|
||||||
public NotificationHubPushNotificationServiceTests()
|
public NotificationHubPushNotificationServiceTests()
|
||||||
{
|
{
|
||||||
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
|
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
|
||||||
_globalSettings = new GlobalSettings();
|
|
||||||
_httpContextAccessor = Substitute.For<IHttpContextAccessor>();
|
_httpContextAccessor = Substitute.For<IHttpContextAccessor>();
|
||||||
|
_notificationHubPool = Substitute.For<INotificationHubPool>();
|
||||||
_logger = Substitute.For<ILogger<NotificationsApiPushNotificationService>>();
|
_logger = Substitute.For<ILogger<NotificationsApiPushNotificationService>>();
|
||||||
|
|
||||||
_sut = new NotificationHubPushNotificationService(
|
_sut = new NotificationHubPushNotificationService(
|
||||||
_installationDeviceRepository,
|
_installationDeviceRepository,
|
||||||
_globalSettings,
|
_notificationHubPool,
|
||||||
_httpContextAccessor,
|
_httpContextAccessor,
|
||||||
_logger
|
_logger
|
||||||
);
|
);
|
@ -1,11 +1,11 @@
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.NotificationHub;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services;
|
namespace Bit.Core.Test.NotificationHub;
|
||||||
|
|
||||||
public class NotificationHubPushRegistrationServiceTests
|
public class NotificationHubPushRegistrationServiceTests
|
||||||
{
|
{
|
||||||
@ -15,6 +15,7 @@ public class NotificationHubPushRegistrationServiceTests
|
|||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly INotificationHubPool _notificationHubPool;
|
||||||
|
|
||||||
public NotificationHubPushRegistrationServiceTests()
|
public NotificationHubPushRegistrationServiceTests()
|
||||||
{
|
{
|
||||||
@ -22,10 +23,12 @@ public class NotificationHubPushRegistrationServiceTests
|
|||||||
_serviceProvider = Substitute.For<IServiceProvider>();
|
_serviceProvider = Substitute.For<IServiceProvider>();
|
||||||
_logger = Substitute.For<ILogger<NotificationHubPushRegistrationService>>();
|
_logger = Substitute.For<ILogger<NotificationHubPushRegistrationService>>();
|
||||||
_globalSettings = new GlobalSettings();
|
_globalSettings = new GlobalSettings();
|
||||||
|
_notificationHubPool = Substitute.For<INotificationHubPool>();
|
||||||
|
|
||||||
_sut = new NotificationHubPushRegistrationService(
|
_sut = new NotificationHubPushRegistrationService(
|
||||||
_installationDeviceRepository,
|
_installationDeviceRepository,
|
||||||
_globalSettings,
|
_globalSettings,
|
||||||
|
_notificationHubPool,
|
||||||
_serviceProvider,
|
_serviceProvider,
|
||||||
_logger
|
_logger
|
||||||
);
|
);
|
@ -1,10 +1,10 @@
|
|||||||
using Bit.Core.Repositories;
|
using AutoFixture;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using GlobalSettingsCustomization = Bit.Test.Common.AutoFixture.GlobalSettings;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Services;
|
namespace Bit.Core.Test.Services;
|
||||||
|
|
||||||
@ -12,35 +12,26 @@ public class MultiServicePushNotificationServiceTests
|
|||||||
{
|
{
|
||||||
private readonly MultiServicePushNotificationService _sut;
|
private readonly MultiServicePushNotificationService _sut;
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
|
||||||
private readonly IDeviceRepository _deviceRepository;
|
|
||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
|
||||||
private readonly GlobalSettings _globalSettings;
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
||||||
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
||||||
private readonly ILogger<RelayPushNotificationService> _relayLogger;
|
private readonly ILogger<RelayPushNotificationService> _relayLogger;
|
||||||
private readonly ILogger<NotificationsApiPushNotificationService> _hubLogger;
|
private readonly ILogger<NotificationsApiPushNotificationService> _hubLogger;
|
||||||
|
private readonly IEnumerable<IPushNotificationService> _services;
|
||||||
|
private readonly Settings.GlobalSettings _globalSettings;
|
||||||
|
|
||||||
public MultiServicePushNotificationServiceTests()
|
public MultiServicePushNotificationServiceTests()
|
||||||
{
|
{
|
||||||
_httpFactory = Substitute.For<IHttpClientFactory>();
|
|
||||||
_deviceRepository = Substitute.For<IDeviceRepository>();
|
|
||||||
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
|
|
||||||
_globalSettings = new GlobalSettings();
|
|
||||||
_httpContextAccessor = Substitute.For<IHttpContextAccessor>();
|
|
||||||
_logger = Substitute.For<ILogger<MultiServicePushNotificationService>>();
|
_logger = Substitute.For<ILogger<MultiServicePushNotificationService>>();
|
||||||
_relayLogger = Substitute.For<ILogger<RelayPushNotificationService>>();
|
_relayLogger = Substitute.For<ILogger<RelayPushNotificationService>>();
|
||||||
_hubLogger = Substitute.For<ILogger<NotificationsApiPushNotificationService>>();
|
_hubLogger = Substitute.For<ILogger<NotificationsApiPushNotificationService>>();
|
||||||
|
|
||||||
|
var fixture = new Fixture().WithAutoNSubstitutions().Customize(new GlobalSettingsCustomization());
|
||||||
|
_services = fixture.CreateMany<IPushNotificationService>();
|
||||||
|
_globalSettings = fixture.Create<Settings.GlobalSettings>();
|
||||||
|
|
||||||
_sut = new MultiServicePushNotificationService(
|
_sut = new MultiServicePushNotificationService(
|
||||||
_httpFactory,
|
_services,
|
||||||
_deviceRepository,
|
|
||||||
_installationDeviceRepository,
|
|
||||||
_globalSettings,
|
|
||||||
_httpContextAccessor,
|
|
||||||
_logger,
|
_logger,
|
||||||
_relayLogger,
|
_globalSettings
|
||||||
_hubLogger
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,33 +34,30 @@ public class CoreHelpersTests
|
|||||||
// the comb are working properly
|
// the comb are working properly
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<object[]> GenerateCombCases = new[]
|
public static IEnumerable<object[]> GuidSeedCases = [
|
||||||
{
|
[
|
||||||
new object[]
|
|
||||||
{
|
|
||||||
Guid.Parse("a58db474-43d8-42f1-b4ee-0c17647cd0c0"), // Input Guid
|
Guid.Parse("a58db474-43d8-42f1-b4ee-0c17647cd0c0"), // Input Guid
|
||||||
new DateTime(2022, 3, 12, 12, 12, 0, DateTimeKind.Utc), // Input Time
|
new DateTime(2022, 3, 12, 12, 12, 0, DateTimeKind.Utc), // Input Time
|
||||||
Guid.Parse("a58db474-43d8-42f1-b4ee-ae5600c90cc1"), // Expected Comb
|
],
|
||||||
},
|
[
|
||||||
new object[]
|
|
||||||
{
|
|
||||||
Guid.Parse("f776e6ee-511f-4352-bb28-88513002bdeb"),
|
Guid.Parse("f776e6ee-511f-4352-bb28-88513002bdeb"),
|
||||||
new DateTime(2021, 5, 10, 10, 52, 0, DateTimeKind.Utc),
|
new DateTime(2021, 5, 10, 10, 52, 0, DateTimeKind.Utc),
|
||||||
Guid.Parse("f776e6ee-511f-4352-bb28-ad2400b313c1"),
|
],
|
||||||
},
|
[
|
||||||
new object[]
|
|
||||||
{
|
|
||||||
Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011648a1"),
|
Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011648a1"),
|
||||||
new DateTime(1999, 2, 26, 16, 53, 13, DateTimeKind.Utc),
|
new DateTime(1999, 2, 26, 16, 53, 13, DateTimeKind.Utc),
|
||||||
Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011649cd"),
|
],
|
||||||
},
|
[
|
||||||
new object[]
|
|
||||||
{
|
|
||||||
Guid.Parse("bfb8f353-3b32-4a9e-bef6-24fe0b54bfb0"),
|
Guid.Parse("bfb8f353-3b32-4a9e-bef6-24fe0b54bfb0"),
|
||||||
new DateTime(2024, 10, 20, 1, 32, 16, DateTimeKind.Utc),
|
new DateTime(2024, 10, 20, 1, 32, 16, DateTimeKind.Utc),
|
||||||
Guid.Parse("bfb8f353-3b32-4a9e-bef6-b20f00195780"),
|
]
|
||||||
}
|
];
|
||||||
};
|
public static IEnumerable<object[]> GenerateCombCases = GuidSeedCases.Zip([
|
||||||
|
Guid.Parse("a58db474-43d8-42f1-b4ee-ae5600c90cc1"), // Expected Comb for each Guid Seed case
|
||||||
|
Guid.Parse("f776e6ee-511f-4352-bb28-ad2400b313c1"),
|
||||||
|
Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011649cd"),
|
||||||
|
Guid.Parse("bfb8f353-3b32-4a9e-bef6-b20f00195780"),
|
||||||
|
]).Select((zip) => new object[] { zip.Item1[0], zip.Item1[1], zip.Item2 });
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(GenerateCombCases))]
|
[MemberData(nameof(GenerateCombCases))]
|
||||||
@ -71,6 +68,31 @@ public class CoreHelpersTests
|
|||||||
Assert.Equal(expectedComb, comb);
|
Assert.Equal(expectedComb, comb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(GuidSeedCases))]
|
||||||
|
public void DateFromComb_WithComb_Success(Guid inputGuid, DateTime inputTime)
|
||||||
|
{
|
||||||
|
var comb = CoreHelpers.GenerateComb(inputGuid, inputTime);
|
||||||
|
var inverseComb = CoreHelpers.DateFromComb(comb);
|
||||||
|
|
||||||
|
Assert.Equal(inputTime, inverseComb, TimeSpan.FromMilliseconds(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("00000000-0000-0000-0000-000000000000", 1, 0)]
|
||||||
|
[InlineData("00000000-0000-0000-0000-000000000001", 1, 0)]
|
||||||
|
[InlineData("00000000-0000-0000-0000-000000000000", 500, 430)]
|
||||||
|
[InlineData("00000000-0000-0000-0000-000000000001", 500, 430)]
|
||||||
|
[InlineData("10000000-0000-0000-0000-000000000001", 500, 454)]
|
||||||
|
[InlineData("00000000-0000-0100-0000-000000000001", 500, 19)]
|
||||||
|
public void BinForComb_Success(string guidString, int nbins, int expectedBin)
|
||||||
|
{
|
||||||
|
var guid = Guid.Parse(guidString);
|
||||||
|
var bin = CoreHelpers.BinForComb(guid, nbins);
|
||||||
|
|
||||||
|
Assert.Equal(expectedBin, bin);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToGuidIdArrayTVP_Success()
|
public void ToGuidIdArrayTVP_Success()
|
||||||
|
Loading…
Reference in New Issue
Block a user