1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00
Files
server/test/Core.Test/Services/DeviceServiceTests.cs
Justin Baur e5159a3ba2 [PM-19659] Clean up Notifications code (#6244)
* Move PushType to Platform Folder

- Move the PushType next to the rest of push notification code
- Specifically exclude it from needing Platform code review
- Add tests establishing rules Platform has for usage of this enum, making it safe to have no owner

* Move NotificationHub code into Platform/Push directory

* Update NotificationHub namespace imports

* Add attribute for storing push type metadata

* Rename Push Engines to have PushEngine suffix

* Move Push Registration items to their own directory

* Push code move

* Add expected usage comment

* Add Push feature registration method

- Make method able to be called multipes times with no ill effects

* Add Push Registration service entrypoint and tests

* Use new service entrypoints

* Test changes
2025-08-26 13:30:37 -04:00

312 lines
13 KiB
C#

using System.Runtime.CompilerServices;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.PushRegistration;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class DeviceServiceTests
{
[Theory]
[BitAutoData]
public async Task SaveAsync_IdProvided_UpdatedRevisionDateAndPushRegistration(Guid id, Guid userId,
Guid organizationId1, Guid organizationId2, Guid installationId,
OrganizationUserOrganizationDetails organizationUserOrganizationDetails1,
OrganizationUserOrganizationDetails organizationUserOrganizationDetails2)
{
organizationUserOrganizationDetails1.OrganizationId = organizationId1;
organizationUserOrganizationDetails2.OrganizationId = organizationId2;
var deviceRepo = Substitute.For<IDeviceRepository>();
var pushRepo = Substitute.For<IPushRegistrationService>();
var organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType?>())
.Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]);
var globalSettings = Substitute.For<IGlobalSettings>();
globalSettings.Installation.Id.Returns(installationId);
var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings);
var device = new Device
{
Id = id,
Name = "test device",
Type = DeviceType.Android,
UserId = userId,
PushToken = "testToken",
Identifier = "testid"
};
await deviceService.SaveAsync(device);
Assert.True(device.RevisionDate - DateTime.UtcNow < TimeSpan.FromSeconds(1));
await pushRepo.Received(1).CreateOrUpdateRegistrationAsync(Arg.Is<PushRegistrationData>(v => v.Token == "testToken"), id.ToString(),
userId.ToString(), "testid", DeviceType.Android,
Arg.Do<IEnumerable<string>>(organizationIds =>
{
var organizationIdsList = organizationIds.ToList();
Assert.Equal(2, organizationIdsList.Count);
Assert.Contains(organizationId1.ToString(), organizationIdsList);
Assert.Contains(organizationId2.ToString(), organizationIdsList);
}), installationId);
}
[Theory]
[BitAutoData]
public async Task SaveAsync_IdNotProvided_CreatedAndPushRegistration(Guid userId, Guid organizationId1,
Guid organizationId2, Guid installationId,
OrganizationUserOrganizationDetails organizationUserOrganizationDetails1,
OrganizationUserOrganizationDetails organizationUserOrganizationDetails2)
{
organizationUserOrganizationDetails1.OrganizationId = organizationId1;
organizationUserOrganizationDetails2.OrganizationId = organizationId2;
var deviceRepo = Substitute.For<IDeviceRepository>();
var pushRepo = Substitute.For<IPushRegistrationService>();
var organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType?>())
.Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]);
var globalSettings = Substitute.For<IGlobalSettings>();
globalSettings.Installation.Id.Returns(installationId);
var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings);
var device = new Device
{
Name = "test device",
Type = DeviceType.Android,
UserId = userId,
PushToken = "testToken",
Identifier = "testid"
};
await deviceService.SaveAsync(device);
await pushRepo.Received(1).CreateOrUpdateRegistrationAsync(Arg.Is<PushRegistrationData>(v => v.Token == "testToken"),
Arg.Do<string>(id => Guid.TryParse(id, out var _)), userId.ToString(), "testid", DeviceType.Android,
Arg.Do<IEnumerable<string>>(organizationIds =>
{
var organizationIdsList = organizationIds.ToList();
Assert.Equal(2, organizationIdsList.Count);
Assert.Contains(organizationId1.ToString(), organizationIdsList);
Assert.Contains(organizationId2.ToString(), organizationIdsList);
}), installationId);
}
/// <summary>
/// Story: A user chose to keep trust in one of their current trusted devices, but not in another one of their
/// devices. We will rotate the trust of the currently signed in device as well as the device they chose but will
/// remove the trust of the device they didn't give new keys for.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateDevicesTrustAsync_Works(
SutProvider<DeviceService> sutProvider,
Guid currentUserId,
Device deviceOne,
Device deviceTwo,
Device deviceThree)
{
SetupOldTrust(deviceOne);
SetupOldTrust(deviceTwo);
SetupOldTrust(deviceThree);
deviceOne.Identifier = "current_device";
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(currentUserId)
.Returns(new List<Device> { deviceOne, deviceTwo, deviceThree, });
var currentDeviceModel = new DeviceKeysUpdateRequestModel
{
EncryptedPublicKey = "current_encrypted_public_key",
EncryptedUserKey = "current_encrypted_user_key",
};
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>
{
new OtherDeviceKeysUpdateRequestModel
{
DeviceId = deviceTwo.Id,
EncryptedPublicKey = "encrypted_public_key_two",
EncryptedUserKey = "encrypted_user_key_two",
},
};
await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel,
alteredDeviceModels);
// Updating trust, "current" or "other" only needs to change the EncryptedPublicKey & EncryptedUserKey
await sutProvider.GetDependency<IDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<Device>(d =>
d.Id == deviceOne.Id &&
d.EncryptedPublicKey == "current_encrypted_public_key" &&
d.EncryptedUserKey == "current_encrypted_user_key" &&
d.EncryptedPrivateKey == "old_private_deviceOne"));
await sutProvider.GetDependency<IDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<Device>(d =>
d.Id == deviceTwo.Id &&
d.EncryptedPublicKey == "encrypted_public_key_two" &&
d.EncryptedUserKey == "encrypted_user_key_two" &&
d.EncryptedPrivateKey == "old_private_deviceTwo"));
// Clearing trust should remove all key values
await sutProvider.GetDependency<IDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<Device>(d =>
d.Id == deviceThree.Id &&
d.EncryptedPublicKey == null &&
d.EncryptedUserKey == null &&
d.EncryptedPrivateKey == null));
// Should have recieved a total of 3 calls, the ones asserted above
await sutProvider.GetDependency<IDeviceRepository>()
.Received(3)
.UpsertAsync(Arg.Any<Device>());
static void SetupOldTrust(Device device, [CallerArgumentExpression(nameof(device))] string expression = null)
{
device.EncryptedPublicKey = $"old_public_{expression}";
device.EncryptedPrivateKey = $"old_private_{expression}";
device.EncryptedUserKey = $"old_user_{expression}";
}
}
/// <summary>
/// Story: This could result from a poor implementation of this method, if they attempt add trust to a device
/// that doesn't already have trust. They would have to create brand new values and for that values to be accurate
/// they would technically have all the values needed to trust a device, that is why we don't consider this bad
/// enough to throw but do skip it because we'd rather keep number of ways for trust to be added to the endpoint we
/// already have.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateDevicesTrustAsync_DoesNotUpdateUntrustedDevices(
SutProvider<DeviceService> sutProvider,
Guid currentUserId,
Device deviceOne,
Device deviceTwo)
{
deviceOne.Identifier = "current_device";
// Make deviceTwo untrusted
deviceTwo.EncryptedUserKey = string.Empty;
deviceTwo.EncryptedPublicKey = string.Empty;
deviceTwo.EncryptedPrivateKey = string.Empty;
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(currentUserId)
.Returns(new List<Device> { deviceOne, deviceTwo, });
var currentDeviceModel = new DeviceKeysUpdateRequestModel
{
EncryptedPublicKey = "current_encrypted_public_key",
EncryptedUserKey = "current_encrypted_user_key",
};
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>
{
new OtherDeviceKeysUpdateRequestModel
{
DeviceId = deviceTwo.Id,
EncryptedPublicKey = "encrypted_public_key_two",
EncryptedUserKey = "encrypted_user_key_two",
},
};
await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel,
alteredDeviceModels);
// Check that UpsertAsync was called for the trusted device
await sutProvider.GetDependency<IDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<Device>(d =>
d.Id == deviceOne.Id &&
d.EncryptedPublicKey == "current_encrypted_public_key" &&
d.EncryptedUserKey == "current_encrypted_user_key"));
// Check that UpsertAsync was not called for the untrusted device
await sutProvider.GetDependency<IDeviceRepository>()
.DidNotReceive()
.UpsertAsync(Arg.Is<Device>(d => d.Id == deviceTwo.Id));
}
/// <summary>
/// Story: This should only happen if someone were to take the access token from a different device and try to rotate
/// a device that they don't actually have.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateDevicesTrustAsync_ThrowsNotFoundException_WhenCurrentDeviceIdentifierDoesNotExist(
SutProvider<DeviceService> sutProvider,
Guid currentUserId,
Device deviceOne,
Device deviceTwo)
{
deviceOne.Identifier = "some_other_device";
deviceTwo.Identifier = "another_device";
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(currentUserId)
.Returns(new List<Device> { deviceOne, deviceTwo, });
var currentDeviceModel = new DeviceKeysUpdateRequestModel
{
EncryptedPublicKey = "current_encrypted_public_key",
EncryptedUserKey = "current_encrypted_user_key",
};
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel,
Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>()));
}
/// <summary>
/// Story: This should only happen from a poorly implemented user of this method but important to enforce someone
/// using the method correctly, a device should only be rotated intentionally and including it as both the current
/// device and one of the users other device would mean they could rotate it twice and we aren't sure
/// which one they would want to win out.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateDevicesTrustAsync_ThrowsBadRequestException_WhenCurrentDeviceIsIncludedInAlteredDevices(
SutProvider<DeviceService> sutProvider,
Guid currentUserId,
Device deviceOne,
Device deviceTwo)
{
deviceOne.Identifier = "current_device";
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(currentUserId)
.Returns(new List<Device> { deviceOne, deviceTwo, });
var currentDeviceModel = new DeviceKeysUpdateRequestModel
{
EncryptedPublicKey = "current_encrypted_public_key",
EncryptedUserKey = "current_encrypted_user_key",
};
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>
{
new OtherDeviceKeysUpdateRequestModel
{
DeviceId = deviceOne.Id, // current device is included in alteredDevices
EncryptedPublicKey = "encrypted_public_key_one",
EncryptedUserKey = "encrypted_user_key_one",
},
};
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel,
alteredDeviceModels));
}
}