1
0
mirror of https://github.com/bitwarden/server synced 2026-01-14 06:23:46 +00:00

added AuthType prop to send related models with test coverage and debt cleanup

This commit is contained in:
John Harrington
2025-12-17 09:55:35 -07:00
parent c29ad65a17
commit bc22b1dff6
8 changed files with 657 additions and 12 deletions

View File

@@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator<IEnumerable<SendWithIdRe
throw new BadRequestException("All existing sends must be included in the rotation.");
}
result.Add(send.ToSend(existing, _sendAuthorizationService));
result.Add(send.UpdateSend(existing, _sendAuthorizationService));
}
return result;

View File

@@ -92,7 +92,7 @@ public class SendsController : Controller
throw new NotFoundException();
}
var sendResponse = new SendAccessResponseModel(send, _globalSettings);
var sendResponse = new SendAccessResponseModel(send);
if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault())
{
var creator = await _userService.GetUserByIdAsync(send.UserId.Value);
@@ -295,7 +295,7 @@ public class SendsController : Controller
throw new NotFoundException();
}
await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService));
await _nonAnonymousSendCommand.SaveSendAsync(model.UpdateSend(send, _sendAuthorizationService));
return new SendResponseModel(send);
}
@@ -309,7 +309,10 @@ public class SendsController : Controller
throw new NotFoundException();
}
// This endpoint exists because PUT preserves existing Password/Emails when not provided.
// This allows clients to update other fields without re-submitting sensitive auth data.
send.Password = null;
send.AuthType = AuthType.None;
await _nonAnonymousSendCommand.SaveSendAsync(send);
return new SendResponseModel(send);
}

View File

@@ -24,6 +24,11 @@ public class SendRequestModel
/// </summary>
public SendType Type { get; set; }
/// <summary>
/// Specifies the authentication method required to access this Send.
/// </summary>
public AuthType? AuthType { get; set; }
/// <summary>
/// Estimated length of the file accompanying the send. <see langword="null"/> when
/// <see cref="Type"/> is <see cref="SendType.Text"/>.
@@ -125,7 +130,7 @@ public class SendRequestModel
Type = Type,
UserId = (Guid?)userId
};
ToSend(send, sendAuthorizationService);
send = UpdateSend(send, sendAuthorizationService);
return send;
}
@@ -155,8 +160,7 @@ public class SendRequestModel
/// <param name="existingSend">The send to update</param>
/// <param name="sendAuthorizationService">Hashes the send password.</param>
/// <returns>The send object</returns>
// FIXME: rename to `UpdateSend`
public Send ToSend(Send existingSend, ISendAuthorizationService sendAuthorizationService)
public Send UpdateSend(Send existingSend, ISendAuthorizationService sendAuthorizationService)
{
existingSend = ToSendBase(existingSend, sendAuthorizationService);
switch (existingSend.Type)
@@ -249,11 +253,29 @@ public class SendRequestModel
var emails = Emails.Split(',', RemoveEmptyEntries | TrimEntries);
existingSend.Emails = string.Join(", ", emails);
existingSend.Password = null;
existingSend.AuthType = Core.Tools.Enums.AuthType.Email;
}
else if (!string.IsNullOrWhiteSpace(Password))
{
existingSend.Password = authorizationService.HashPassword(Password);
existingSend.Emails = null;
existingSend.AuthType = Core.Tools.Enums.AuthType.Password;
}
else
{
// Neither Password nor Emails provided - preserve existing values and infer AuthType
if (!string.IsNullOrWhiteSpace(existingSend.Password))
{
existingSend.AuthType = Core.Tools.Enums.AuthType.Password;
}
else if (!string.IsNullOrWhiteSpace(existingSend.Emails))
{
existingSend.AuthType = Core.Tools.Enums.AuthType.Email;
}
else
{
existingSend.AuthType = Core.Tools.Enums.AuthType.None;
}
}
existingSend.Disabled = Disabled.GetValueOrDefault();

View File

@@ -3,7 +3,6 @@
using System.Text.Json;
using Bit.Core.Models.Api;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
using Bit.Core.Tools.Models.Data;
@@ -20,17 +19,13 @@ public class SendAccessResponseModel : ResponseModel
/// Instantiates a send access response model
/// </summary>
/// <param name="send">Content to transmit to the client.</param>
/// <param name="globalSettings">
/// Settings that control response generation.
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="send"/> is <see langword="null" />
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="send" /> has an invalid <see cref="Send.Type"/>.
/// </exception>
// FIXME: remove `globalSettings` variable
public SendAccessResponseModel(Send send, GlobalSettings globalSettings)
public SendAccessResponseModel(Send send)
: base("send-access")
{
if (send == null)
@@ -40,6 +35,7 @@ public class SendAccessResponseModel : ResponseModel
Id = CoreHelpers.Base64UrlEncode(send.Id.ToByteArray());
Type = send.Type;
AuthType = send.AuthType;
SendData sendData;
switch (send.Type)
@@ -72,6 +68,11 @@ public class SendAccessResponseModel : ResponseModel
/// </summary>
public SendType Type { get; set; }
/// <summary>
/// Specifies the authentication method required to access this Send.
/// </summary>
public AuthType? AuthType { get; set; }
/// <summary>
/// Label for the send. This is only visible to the owner of the send.
/// </summary>

View File

@@ -37,6 +37,9 @@ public class SendResponseModel : ResponseModel
Id = send.Id;
AccessId = CoreHelpers.Base64UrlEncode(send.Id.ToByteArray());
Type = send.Type;
AuthType = send.AuthType ?? (!string.IsNullOrWhiteSpace(send.Password)
? AuthType = Core.Tools.Enums.AuthType.Password
: (!string.IsNullOrWhiteSpace(send.Emails)? Core.Tools.Enums.AuthType.Email : Core.Tools.Enums.AuthType.None));
Key = send.Key;
MaxAccessCount = send.MaxAccessCount;
AccessCount = send.AccessCount;
@@ -84,6 +87,11 @@ public class SendResponseModel : ResponseModel
/// </summary>
public SendType Type { get; set; }
/// <summary>
/// Specifies the authentication method required to access this Send.
/// </summary>
public AuthType? AuthType { get; set; }
/// <summary>
/// Label for the send.
/// </summary>

View File

@@ -37,6 +37,12 @@ public class Send : ITableObject<Guid>
/// </summary>
public SendType Type { get; set; }
/// <summary>
/// Specifies the authentication method required to access this Send.
/// </summary>
/// <seealso cref="Tools.Enums.AuthType"/>
public AuthType? AuthType { get; set; }
/// <summary>
/// Stores data containing or pointing to the transmitted secret. JSON.
/// </summary>

View File

@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
namespace Bit.Core.Tools.Enums;
/// <summary>
/// Specifies the authentication method required to access a Send.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AuthType : byte
{
/// <summary>
/// Email-based OTP authentication
/// </summary>
Email = 0,
/// <summary>
/// Password-based authentication
/// </summary>
Password = 1,
/// <summary>
/// No authentication required
/// </summary>
None = 2
}

View File

@@ -3,6 +3,7 @@ using System.Text.Json;
using AutoFixture.Xunit2;
using Bit.Api.Models.Response;
using Bit.Api.Tools.Controllers;
using Bit.Api.Tools.Models;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Models.Response;
using Bit.Core.Entities;
@@ -173,4 +174,583 @@ public class SendsControllerTests : IDisposable
Assert.Empty(result.Data);
await _sendOwnerQuery.Received(1).GetOwned(Arg.Any<ClaimsPrincipal>());
}
[Theory, AutoData]
public async Task Post_WithPassword_InfersAuthTypePassword(Guid userId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "key",
Text = new SendTextModel { Text = "text" },
Password = "password",
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Post(request);
Assert.NotNull(result);
Assert.Equal(AuthType.Password, result.AuthType);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.AuthType == AuthType.Password &&
s.Password != null &&
s.Emails == null &&
s.UserId == userId &&
s.Type == SendType.Text));
_userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());
}
[Theory, AutoData]
public async Task Post_WithEmails_InfersAuthTypeEmail(Guid userId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "key",
Text = new SendTextModel { Text = "text" },
Emails = "test@example.com",
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Post(request);
Assert.NotNull(result);
Assert.Equal(AuthType.Email, result.AuthType);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.AuthType == AuthType.Email &&
s.Emails != null &&
s.Password == null &&
s.UserId == userId &&
s.Type == SendType.Text));
_userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());
}
[Theory, AutoData]
public async Task Post_WithoutPasswordOrEmails_InfersAuthTypeNone(Guid userId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "key",
Text = new SendTextModel { Text = "text" },
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Post(request);
Assert.NotNull(result);
Assert.Equal(AuthType.None, result.AuthType);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.AuthType == AuthType.None &&
s.Password == null &&
s.Emails == null &&
s.UserId == userId &&
s.Type == SendType.Text));
_userService.Received(1).GetProperUserId(Arg.Any<ClaimsPrincipal>());
}
[Theory]
[InlineData(AuthType.Password)]
[InlineData(AuthType.Email)]
[InlineData(AuthType.None)]
public async Task Access_ReturnsCorrectAuthType(AuthType authType)
{
var sendId = Guid.NewGuid();
var accessId = CoreHelpers.Base64UrlEncode(sendId.ToByteArray());
var send = new Send
{
Id = sendId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new Dictionary<string, string>()),
AuthType = authType
};
_sendRepository.GetByIdAsync(sendId).Returns(send);
_sendAuthorizationService.AccessAsync(send, "pwd123").Returns(SendAccessResult.Granted);
var request = new SendAccessRequestModel();
var actionResult = await _sut.Access(accessId, request);
var response = (actionResult as ObjectResult)?.Value as SendAccessResponseModel;
Assert.NotNull(response);
Assert.Equal(authType, response.AuthType);
}
[Theory]
[InlineData(AuthType.Password)]
[InlineData(AuthType.Email)]
[InlineData(AuthType.None)]
public async Task Get_ReturnsCorrectAuthType(AuthType authType)
{
var sendId = Guid.NewGuid();
var send = new Send
{
Id = sendId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("a", "b", "c", false)),
AuthType = authType
};
_sendOwnerQuery.Get(sendId, Arg.Any<ClaimsPrincipal>()).Returns(send);
var result = await _sut.Get(sendId.ToString());
Assert.NotNull(result);
Assert.Equal(authType, result.AuthType);
}
[Theory, AutoData]
public async Task Put_WithValidSend_UpdatesSuccessfully(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = userId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
AuthType = AuthType.None
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "updated-key",
Text = new SendTextModel { Text = "updated text" },
Password = "new-password",
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Put(sendId.ToString(), request);
Assert.NotNull(result);
Assert.Equal(sendId, result.Id);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s => s.Id == sendId));
}
[Theory, AutoData]
public async Task Put_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
_sendRepository.GetByIdAsync(sendId).Returns((Send)null);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "key",
Text = new SendTextModel { Text = "text" },
DeletionDate = DateTime.UtcNow.AddDays(7)
};
await Assert.ThrowsAsync<NotFoundException>(() => _sut.Put(sendId.ToString(), request));
}
[Theory, AutoData]
public async Task Put_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = otherUserId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false))
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "key",
Text = new SendTextModel { Text = "text" },
DeletionDate = DateTime.UtcNow.AddDays(7)
};
await Assert.ThrowsAsync<NotFoundException>(() => _sut.Put(sendId.ToString(), request));
}
[Theory, AutoData]
public async Task PutRemovePassword_WithValidSend_RemovesPasswordAndSetsAuthTypeNone(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = userId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
Password = "hashed-password",
AuthType = AuthType.Password
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
var result = await _sut.PutRemovePassword(sendId.ToString());
Assert.NotNull(result);
Assert.Equal(sendId, result.Id);
Assert.Equal(AuthType.None, result.AuthType);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.Id == sendId &&
s.Password == null &&
s.AuthType == AuthType.None));
}
[Theory, AutoData]
public async Task PutRemovePassword_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
_sendRepository.GetByIdAsync(sendId).Returns((Send)null);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PutRemovePassword(sendId.ToString()));
}
[Theory, AutoData]
public async Task PutRemovePassword_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = otherUserId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
Password = "hashed-password"
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.PutRemovePassword(sendId.ToString()));
}
[Theory, AutoData]
public async Task Delete_WithValidSend_DeletesSuccessfully(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = userId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false))
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
await _sut.Delete(sendId.ToString());
await _nonAnonymousSendCommand.Received(1).DeleteSendAsync(Arg.Is<Send>(s => s.Id == sendId));
}
[Theory, AutoData]
public async Task Delete_WithNonExistentSend_ThrowsNotFoundException(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
_sendRepository.GetByIdAsync(sendId).Returns((Send)null);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.Delete(sendId.ToString()));
}
[Theory, AutoData]
public async Task Delete_WithWrongUser_ThrowsNotFoundException(Guid userId, Guid otherUserId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = otherUserId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false))
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
await Assert.ThrowsAsync<NotFoundException>(() => _sut.Delete(sendId.ToString()));
}
[Theory, AutoData]
public async Task PostFile_WithPassword_InfersAuthTypePassword(Guid userId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
_nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())
.Returns("https://example.com/upload")
.AndDoes(callInfo =>
{
var send = callInfo.ArgAt<Send>(0);
var data = callInfo.ArgAt<SendFileData>(1);
send.Data = JsonSerializer.Serialize(data);
});
var request = new SendRequestModel
{
Type = SendType.File,
Key = "key",
File = new SendFileModel { FileName = "test.txt" },
FileLength = 1024L,
Password = "password",
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.PostFile(request);
Assert.NotNull(result);
Assert.NotNull(result.SendResponse);
Assert.Equal(AuthType.Password, result.SendResponse.AuthType);
await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(
Arg.Is<Send>(s =>
s.AuthType == AuthType.Password &&
s.Password != null &&
s.Emails == null &&
s.UserId == userId),
Arg.Any<SendFileData>(),
1024L);
}
[Theory, AutoData]
public async Task PostFile_WithEmails_InfersAuthTypeEmail(Guid userId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
_nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())
.Returns("https://example.com/upload")
.AndDoes(callInfo =>
{
var send = callInfo.ArgAt<Send>(0);
var data = callInfo.ArgAt<SendFileData>(1);
send.Data = JsonSerializer.Serialize(data);
});
var request = new SendRequestModel
{
Type = SendType.File,
Key = "key",
File = new SendFileModel { FileName = "test.txt" },
FileLength = 1024L,
Emails = "test@example.com",
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.PostFile(request);
Assert.NotNull(result);
Assert.NotNull(result.SendResponse);
Assert.Equal(AuthType.Email, result.SendResponse.AuthType);
await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(
Arg.Is<Send>(s =>
s.AuthType == AuthType.Email &&
s.Emails != null &&
s.Password == null &&
s.UserId == userId),
Arg.Any<SendFileData>(),
1024L);
}
[Theory, AutoData]
public async Task PostFile_WithoutPasswordOrEmails_InfersAuthTypeNone(Guid userId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
_nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())
.Returns("https://example.com/upload")
.AndDoes(callInfo =>
{
var send = callInfo.ArgAt<Send>(0);
var data = callInfo.ArgAt<SendFileData>(1);
send.Data = JsonSerializer.Serialize(data);
});
var request = new SendRequestModel
{
Type = SendType.File,
Key = "key",
File = new SendFileModel { FileName = "test.txt" },
FileLength = 1024L,
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.PostFile(request);
Assert.NotNull(result);
Assert.NotNull(result.SendResponse);
Assert.Equal(AuthType.None, result.SendResponse.AuthType);
await _nonAnonymousSendCommand.Received(1).SaveFileSendAsync(
Arg.Is<Send>(s =>
s.AuthType == AuthType.None &&
s.Password == null &&
s.Emails == null &&
s.UserId == userId),
Arg.Any<SendFileData>(),
1024L);
}
[Theory, AutoData]
public async Task Put_ChangingFromPasswordToEmails_UpdatesAuthTypeToEmail(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = userId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
Password = "hashed-password",
AuthType = AuthType.Password
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "updated-key",
Text = new SendTextModel { Text = "updated text" },
Emails = "new@example.com",
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Put(sendId.ToString(), request);
Assert.NotNull(result);
Assert.Equal(sendId, result.Id);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.Id == sendId &&
s.AuthType == AuthType.Email &&
s.Emails != null &&
s.Password == null));
}
[Theory, AutoData]
public async Task Put_ChangingFromEmailToPassword_UpdatesAuthTypeToPassword(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = userId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
Emails = "old@example.com",
AuthType = AuthType.Email
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "updated-key",
Text = new SendTextModel { Text = "updated text" },
Password = "new-password",
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Put(sendId.ToString(), request);
Assert.NotNull(result);
Assert.Equal(sendId, result.Id);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.Id == sendId &&
s.AuthType == AuthType.Password &&
s.Password != null &&
s.Emails == null));
}
[Theory, AutoData]
public async Task Put_WithoutPasswordOrEmails_PreservesExistingPassword(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = userId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
Password = "hashed-password",
AuthType = AuthType.Password
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "updated-key",
Text = new SendTextModel { Text = "updated text" },
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Put(sendId.ToString(), request);
Assert.NotNull(result);
Assert.Equal(sendId, result.Id);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.Id == sendId &&
s.AuthType == AuthType.Password &&
s.Password == "hashed-password" &&
s.Emails == null));
}
[Theory, AutoData]
public async Task Put_WithoutPasswordOrEmails_PreservesExistingEmails(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = userId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
Emails = "test@example.com",
AuthType = AuthType.Email
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "updated-key",
Text = new SendTextModel { Text = "updated text" },
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Put(sendId.ToString(), request);
Assert.NotNull(result);
Assert.Equal(sendId, result.Id);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.Id == sendId &&
s.AuthType == AuthType.Email &&
s.Emails == "test@example.com" &&
s.Password == null));
}
[Theory, AutoData]
public async Task Put_WithoutPasswordOrEmails_PreservesNoneAuthType(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
var existingSend = new Send
{
Id = sendId,
UserId = userId,
Type = SendType.Text,
Data = JsonSerializer.Serialize(new SendTextData("Old", "Old notes", "Old text", false)),
Password = null,
Emails = null,
AuthType = AuthType.None
};
_sendRepository.GetByIdAsync(sendId).Returns(existingSend);
var request = new SendRequestModel
{
Type = SendType.Text,
Key = "updated-key",
Text = new SendTextModel { Text = "updated text" },
DeletionDate = DateTime.UtcNow.AddDays(7)
};
var result = await _sut.Put(sendId.ToString(), request);
Assert.NotNull(result);
Assert.Equal(sendId, result.Id);
await _nonAnonymousSendCommand.Received(1).SaveSendAsync(Arg.Is<Send>(s =>
s.Id == sendId &&
s.AuthType == AuthType.None &&
s.Password == null &&
s.Emails == null));
}
}