1
0
mirror of https://github.com/bitwarden/server synced 2026-02-26 17:33:40 +00:00

[PM-31787] Users can access the sends after the limit was reached (#6958)

* fix file type send increment behavior

* fix text send access increment behavior

* fix & update tests

* cleanup unused service

* fix broken test constructor expecting unused service
This commit is contained in:
John Harrington
2026-02-10 07:57:43 -07:00
committed by GitHub
parent e5cf9dff2e
commit bc94934808
5 changed files with 547 additions and 24 deletions

View File

@@ -903,6 +903,106 @@ public class SendsControllerTests : IDisposable
Assert.Equal(creator.Email, response.CreatorIdentifier);
}
[Theory, AutoData]
public async Task AccessUsingAuth_WithDisabledSend_ThrowsNotFoundException(Guid sendId)
{
var send = new Send
{
Id = sendId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
Disabled = true,
AccessCount = 0,
MaxAccessCount = null
};
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
}
[Theory, AutoData]
public async Task AccessUsingAuth_WithMaxAccessCountReached_ThrowsNotFoundException(Guid sendId)
{
var send = new Send
{
Id = sendId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
Disabled = false,
AccessCount = 10,
MaxAccessCount = 10
};
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
}
[Theory, AutoData]
public async Task AccessUsingAuth_WithExpiredSend_ThrowsNotFoundException(Guid sendId)
{
var send = new Send
{
Id = sendId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired yesterday
Disabled = false,
AccessCount = 0,
MaxAccessCount = null
};
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
}
[Theory, AutoData]
public async Task AccessUsingAuth_WithDeletionDatePassed_ThrowsNotFoundException(Guid sendId)
{
var send = new Send
{
Id = sendId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
DeletionDate = DateTime.UtcNow.AddDays(-1), // Deletion date has passed
ExpirationDate = null,
Disabled = false,
AccessCount = 0,
MaxAccessCount = null
};
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _userService.DidNotReceive().GetUserByIdAsync(Arg.Any<Guid>());
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
}
[Theory, AutoData]
public async Task GetSendFileDownloadDataUsingAuth_WithValidFileId_ReturnsDownloadUrl(
Guid sendId, string fileId, string expectedUrl)
@@ -922,7 +1022,8 @@ public class SendsControllerTests : IDisposable
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
_sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl);
_nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId)
.Returns((expectedUrl, SendAccessResult.Granted));
var result = await _sut.GetSendFileDownloadDataUsingAuth(fileId);
@@ -932,7 +1033,7 @@ public class SendsControllerTests : IDisposable
Assert.Equal(fileId, response.Id);
Assert.Equal(expectedUrl, response.Url);
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _sendFileStorageService.Received(1).GetSendFileDownloadUrlAsync(send, fileId);
await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId);
}
[Theory, AutoData]
@@ -948,13 +1049,13 @@ public class SendsControllerTests : IDisposable
Assert.Equal("Could not locate send", exception.Message);
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _sendFileStorageService.DidNotReceive()
await _nonAnonymousSendCommand.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[Theory, AutoData]
public async Task GetSendFileDownloadDataUsingAuth_WithTextSend_StillReturnsResponse(
Guid sendId, string fileId, string expectedUrl)
public async Task GetSendFileDownloadDataUsingAuth_WithTextSend_ThrowsBadRequestException(
Guid sendId, string fileId)
{
var send = new Send
{
@@ -970,15 +1071,44 @@ public class SendsControllerTests : IDisposable
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
_sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl);
_nonAnonymousSendCommand
.When(x => x.GetSendFileDownloadUrlAsync(send, fileId))
.Do(x => throw new BadRequestException("Can only get a download URL for a file type of Send"));
var result = await _sut.GetSendFileDownloadDataUsingAuth(fileId);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
Assert.NotNull(result);
var objectResult = Assert.IsType<ObjectResult>(result);
var response = Assert.IsType<SendFileDownloadDataResponseModel>(objectResult.Value);
Assert.Equal(fileId, response.Id);
Assert.Equal(expectedUrl, response.Url);
Assert.Equal("Can only get a download URL for a file type of Send", exception.Message);
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId);
}
[Theory, AutoData]
public async Task GetSendFileDownloadDataUsingAuth_WithAccessDenied_ThrowsNotFoundException(
Guid sendId, string fileId)
{
var fileData = new SendFileData("Test File", "Notes", "document.pdf") { Id = fileId, Size = 2048 };
var send = new Send
{
Id = sendId,
Type = SendType.File,
Data = JsonSerializer.Serialize(fileData),
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
Disabled = false,
AccessCount = 0,
MaxAccessCount = null
};
var user = CreateUserWithSendIdClaim(sendId);
_sut.ControllerContext = CreateControllerContextWithUser(user);
_sendRepository.GetByIdAsync(sendId).Returns(send);
_nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId)
.Returns((null, SendAccessResult.Denied));
await Assert.ThrowsAsync<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
await _sendRepository.Received(1).GetByIdAsync(sendId);
await _nonAnonymousSendCommand.Received(1).GetSendFileDownloadUrlAsync(send, fileId);
}

View File

@@ -11,6 +11,7 @@ using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.SendFeatures.Commands;
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
using Bit.Core.Tools.Services;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
@@ -28,7 +29,6 @@ public class NonAnonymousSendCommandTests
private readonly ISendRepository _sendRepository;
private readonly ISendFileStorageService _sendFileStorageService;
private readonly IPushNotificationService _pushNotificationService;
private readonly ISendAuthorizationService _sendAuthorizationService;
private readonly ISendValidationService _sendValidationService;
private readonly IFeatureService _featureService;
private readonly ICurrentContext _currentContext;
@@ -42,7 +42,6 @@ public class NonAnonymousSendCommandTests
_sendRepository = Substitute.For<ISendRepository>();
_sendFileStorageService = Substitute.For<ISendFileStorageService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_sendAuthorizationService = Substitute.For<ISendAuthorizationService>();
_featureService = Substitute.For<IFeatureService>();
_sendValidationService = Substitute.For<ISendValidationService>();
_currentContext = Substitute.For<ICurrentContext>();
@@ -53,7 +52,6 @@ public class NonAnonymousSendCommandTests
_sendRepository,
_sendFileStorageService,
_pushNotificationService,
_sendAuthorizationService,
_sendValidationService,
_sendCoreHelperService,
_logger
@@ -1093,4 +1091,329 @@ public class NonAnonymousSendCommandTests
Assert.Equal("File received does not match expected file length.", exception.Message);
}
[Fact]
public async Task GetSendFileDownloadUrlAsync_WithTextSend_ThrowsBadRequest()
{
// Arrange
var send = new Send
{
Id = Guid.NewGuid(),
Type = SendType.Text,
UserId = Guid.NewGuid()
};
var fileId = "somefile123";
// Act & Assert
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
_nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId));
Assert.Equal("Can only get a download URL for a file type of Send", exception.Message);
// Verify no storage service methods were called
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[Fact]
public async Task GetSendFileDownloadUrlAsync_WithDisabledSend_ReturnsDenied()
{
// Arrange
var fileId = "file123";
var send = new Send
{
Id = Guid.NewGuid(),
Type = SendType.File,
UserId = Guid.NewGuid(),
Disabled = true,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
AccessCount = 0,
MaxAccessCount = null
};
// Act
var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);
// Assert
Assert.Null(url);
Assert.Equal(SendAccessResult.Denied, result);
// Verify no repository updates occurred
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[Fact]
public async Task GetSendFileDownloadUrlAsync_WithMaxAccessCountReached_ReturnsDenied()
{
// Arrange
var fileId = "file123";
var send = new Send
{
Id = Guid.NewGuid(),
Type = SendType.File,
UserId = Guid.NewGuid(),
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
AccessCount = 5,
MaxAccessCount = 5
};
// Act
var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);
// Assert
Assert.Null(url);
Assert.Equal(SendAccessResult.Denied, result);
// Verify no repository updates occurred
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[Fact]
public async Task GetSendFileDownloadUrlAsync_WithExpiredSend_ReturnsDenied()
{
// Arrange
var fileId = "file123";
var send = new Send
{
Id = Guid.NewGuid(),
Type = SendType.File,
UserId = Guid.NewGuid(),
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired yesterday
AccessCount = 0,
MaxAccessCount = null
};
// Act
var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);
// Assert
Assert.Null(url);
Assert.Equal(SendAccessResult.Denied, result);
// Verify no repository updates occurred
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[Fact]
public async Task GetSendFileDownloadUrlAsync_WithDeletionDatePassed_ReturnsDenied()
{
// Arrange
var fileId = "file123";
var send = new Send
{
Id = Guid.NewGuid(),
Type = SendType.File,
UserId = Guid.NewGuid(),
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(-1), // Deletion date has passed
ExpirationDate = null,
AccessCount = 0,
MaxAccessCount = null
};
// Act
var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);
// Assert
Assert.Null(url);
Assert.Equal(SendAccessResult.Denied, result);
// Verify no repository updates occurred
await _sendRepository.DidNotReceive().ReplaceAsync(Arg.Any<Send>());
await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any<Send>());
await _sendFileStorageService.DidNotReceive()
.GetSendFileDownloadUrlAsync(Arg.Any<Send>(), Arg.Any<string>());
}
[Fact]
public async Task GetSendFileDownloadUrlAsync_WithValidSend_ReturnsUrlAndIncrementsAccessCount()
{
// Arrange
var fileId = "file123";
var expectedUrl = "https://download.example.com/file123";
var send = new Send
{
Id = Guid.NewGuid(),
Type = SendType.File,
UserId = Guid.NewGuid(),
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
AccessCount = 3,
MaxAccessCount = 10
};
_sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId).Returns(expectedUrl);
// Act
var (url, result) = await _nonAnonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId);
// Assert
Assert.Equal(expectedUrl, url);
Assert.Equal(SendAccessResult.Granted, result);
// Verify access count was incremented
Assert.Equal(4, send.AccessCount);
// Verify repository was updated
await _sendRepository.Received(1).ReplaceAsync(send);
await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send);
// Verify file storage service was called
await _sendFileStorageService.Received(1).GetSendFileDownloadUrlAsync(send, fileId);
}
[Fact]
public void SendCanBeAccessed_WithDisabledSend_ReturnsFalse()
{
// Arrange
var send = new Send
{
Disabled = true,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
AccessCount = 0,
MaxAccessCount = null
};
// Act
var result = INonAnonymousSendCommand.SendCanBeAccessed(send);
// Assert
Assert.False(result);
}
[Fact]
public void SendCanBeAccessed_WithMaxAccessCountReached_ReturnsFalse()
{
// Arrange
var send = new Send
{
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
AccessCount = 10,
MaxAccessCount = 10
};
// Act
var result = INonAnonymousSendCommand.SendCanBeAccessed(send);
// Assert
Assert.False(result);
}
[Fact]
public void SendCanBeAccessed_WithExpiredSend_ReturnsFalse()
{
// Arrange
var send = new Send
{
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = DateTime.UtcNow.AddDays(-1),
AccessCount = 0,
MaxAccessCount = null
};
// Act
var result = INonAnonymousSendCommand.SendCanBeAccessed(send);
// Assert
Assert.False(result);
}
[Fact]
public void SendCanBeAccessed_WithDeletionDatePassed_ReturnsFalse()
{
// Arrange
var send = new Send
{
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(-1),
ExpirationDate = null,
AccessCount = 0,
MaxAccessCount = null
};
// Act
var result = INonAnonymousSendCommand.SendCanBeAccessed(send);
// Assert
Assert.False(result);
}
[Fact]
public void SendCanBeAccessed_WithValidSend_ReturnsTrue()
{
// Arrange
var send = new Send
{
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = DateTime.UtcNow.AddDays(7),
AccessCount = 5,
MaxAccessCount = 10
};
// Act
var result = INonAnonymousSendCommand.SendCanBeAccessed(send);
// Assert
Assert.True(result);
}
[Fact]
public void SendCanBeAccessed_WithNullMaxAccessCount_ReturnsTrue()
{
// Arrange
var send = new Send
{
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
AccessCount = 100,
MaxAccessCount = null
};
// Act
var result = INonAnonymousSendCommand.SendCanBeAccessed(send);
// Assert
Assert.True(result);
}
[Fact]
public void SendCanBeAccessed_WithNullExpirationDate_ReturnsTrue()
{
// Arrange
var send = new Send
{
Disabled = false,
DeletionDate = DateTime.UtcNow.AddDays(7),
ExpirationDate = null,
AccessCount = 0,
MaxAccessCount = 10
};
// Act
var result = INonAnonymousSendCommand.SendCanBeAccessed(send);
// Assert
Assert.True(result);
}
}