diff --git a/bitwarden-server.sln b/bitwarden-server.sln
index 5316ccbc6f..1579c1ce5c 100644
--- a/bitwarden-server.sln
+++ b/bitwarden-server.sln
@@ -50,6 +50,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventsProcessor", "src\Even
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrator", "util\Migrator\Migrator.csproj", "{54DED792-A022-417E-9804-21FCC9C7C610}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Test", "test\Api.Test\Api.Test.csproj", "{860DE301-0B3E-4717-9C21-A9B4C3C2B121}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -114,6 +116,10 @@ Global
{54DED792-A022-417E-9804-21FCC9C7C610}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54DED792-A022-417E-9804-21FCC9C7C610}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54DED792-A022-417E-9804-21FCC9C7C610}.Release|Any CPU.Build.0 = Release|Any CPU
+ {860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {860DE301-0B3E-4717-9C21-A9B4C3C2B121}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -133,6 +139,7 @@ Global
{8EF31E6C-400A-4174-8BE3-502B08FB10B5} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{79BB453F-D0D8-4DDF-9809-A405C56692BD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84D}
{54DED792-A022-417E-9804-21FCC9C7C610} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
+ {860DE301-0B3E-4717-9C21-A9B4C3C2B121} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj
new file mode 100644
index 0000000000..515ad4c9f3
--- /dev/null
+++ b/test/Api.Test/Api.Test.csproj
@@ -0,0 +1,21 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Api.Test/Controllers/AccountsControllerTests.cs b/test/Api.Test/Controllers/AccountsControllerTests.cs
new file mode 100644
index 0000000000..14ca7987e5
--- /dev/null
+++ b/test/Api.Test/Controllers/AccountsControllerTests.cs
@@ -0,0 +1,352 @@
+using System;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Bit.Api.Controllers;
+using Bit.Core;
+using Bit.Core.Enums;
+using Bit.Core.Exceptions;
+using Bit.Core.Models.Api;
+using Bit.Core.Models.Data;
+using Bit.Core.Models.Table;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Microsoft.AspNetCore.Identity;
+using NSubstitute;
+using Xunit;
+
+namespace Bit.Api.Test.Controllers
+{
+ public class AccountsControllerTests : IDisposable
+ {
+ private readonly AccountsController _sut;
+
+ private readonly IUserService _userService;
+ private readonly IUserRepository _userRepository;
+ private readonly ICipherRepository _cipherRepository;
+ private readonly IFolderRepository _folderRepository;
+ private readonly IOrganizationUserRepository _organizationUserRepository;
+ private readonly IPaymentService _paymentService;
+ private readonly GlobalSettings _globalSettings;
+
+ public AccountsControllerTests()
+ {
+ _userService = Substitute.For();
+ _userRepository = Substitute.For();
+ _cipherRepository = Substitute.For();
+ _folderRepository = Substitute.For();
+ _organizationUserRepository = Substitute.For();
+ _paymentService = Substitute.For();
+ _globalSettings = new GlobalSettings();
+ _sut = new AccountsController(
+ _userService,
+ _userRepository,
+ _cipherRepository,
+ _folderRepository,
+ _organizationUserRepository,
+ _paymentService,
+ _globalSettings
+ );
+ }
+
+ public void Dispose()
+ {
+ _sut?.Dispose();
+ }
+
+ [Fact]
+ public async Task PostPrelogin_WhenUserExists_ShouldReturnUserKdfInfo()
+ {
+ var userKdfInfo = new UserKdfInformation
+ {
+ Kdf = KdfType.PBKDF2_SHA256,
+ KdfIterations = 5000
+ };
+ _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(Task.FromResult(userKdfInfo));
+
+ var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
+
+ Assert.Equal(userKdfInfo.Kdf, response.Kdf);
+ Assert.Equal(userKdfInfo.KdfIterations, response.KdfIterations);
+ }
+
+ [Fact]
+ public async Task PostPrelogin_WhenUserDoesNotExist_ShouldDefaultToSha256And100000Iterations()
+ {
+ _userRepository.GetKdfInformationByEmailAsync(Arg.Any()).Returns(Task.FromResult((UserKdfInformation)null));
+
+ var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
+
+ Assert.Equal(KdfType.PBKDF2_SHA256, response.Kdf);
+ Assert.Equal(100000, response.KdfIterations);
+ }
+
+ [Fact]
+ public async Task PostRegister_ShouldRegisterUser()
+ {
+ var passwordHash = "abcdef";
+ var token = "123456";
+ var userGuid = new Guid();
+ _userService.RegisterUserAsync(Arg.Any(), passwordHash, token, userGuid)
+ .Returns(Task.FromResult(IdentityResult.Success));
+ var request = new RegisterRequestModel
+ {
+ Name = "Example User",
+ Email = "user@example.com",
+ MasterPasswordHash = passwordHash,
+ MasterPasswordHint = "example",
+ Token = token,
+ OrganizationUserId = userGuid
+ };
+
+ await _sut.PostRegister(request);
+
+ await _userService.Received(1).RegisterUserAsync(Arg.Any(), passwordHash, token, userGuid);
+ }
+
+ [Fact]
+ public async Task PostRegister_WhenUserServiceFails_ShouldThrowBadRequestException()
+ {
+ var passwordHash = "abcdef";
+ var token = "123456";
+ var userGuid = new Guid();
+ _userService.RegisterUserAsync(Arg.Any(), passwordHash, token, userGuid)
+ .Returns(Task.FromResult(IdentityResult.Failed()));
+ var request = new RegisterRequestModel
+ {
+ Name = "Example User",
+ Email = "user@example.com",
+ MasterPasswordHash = passwordHash,
+ MasterPasswordHint = "example",
+ Token = token,
+ OrganizationUserId = userGuid
+ };
+
+ await Assert.ThrowsAsync(() => _sut.PostRegister(request));
+ }
+
+ [Fact]
+ public async Task PostPasswordHint_ShouldNotifyUserService()
+ {
+ var email = "user@example.com";
+
+ await _sut.PostPasswordHint(new PasswordHintRequestModel { Email = email });
+
+ await _userService.Received(1).SendMasterPasswordHintAsync(email);
+ }
+
+ [Fact]
+ public async Task PostEmailToken_ShouldInitiateEmailChange()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ ConfigureUserServiceToAcceptPasswordFor(user);
+ var newEmail = "example@user.com";
+
+ await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail });
+
+ await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
+ }
+
+ [Fact]
+ public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
+ {
+ ConfigureUserServiceToReturnNullPrincipal();
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostEmailToken(new EmailTokenRequestModel())
+ );
+ }
+
+ [Fact]
+ public async Task PostEmailToken_WhenInvalidPasssword_ShouldThrowBadRequestException()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ ConfigureUserServiceToRejectPasswordFor(user);
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostEmailToken(new EmailTokenRequestModel())
+ );
+ }
+
+ [Fact]
+ public async Task PostEmail_ShouldChangeUserEmail()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ _userService.ChangeEmailAsync(user, default, default, default, default, default)
+ .Returns(Task.FromResult(IdentityResult.Success));
+
+ await _sut.PostEmail(new EmailRequestModel());
+
+ await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
+ }
+
+ [Fact]
+ public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
+ {
+ ConfigureUserServiceToReturnNullPrincipal();
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostEmail(new EmailRequestModel())
+ );
+ }
+
+ [Fact]
+ public async Task PostEmail_WhenEmailCannotBeChanged_ShouldThrowBadRequestException()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ _userService.ChangeEmailAsync(user, default, default, default, default, default)
+ .Returns(Task.FromResult(IdentityResult.Failed()));
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostEmail(new EmailRequestModel())
+ );
+ }
+
+ [Fact]
+ public async Task PostVerifyEmail_ShouldSendEmailVerification()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+
+ await _sut.PostVerifyEmail();
+
+ await _userService.Received(1).SendEmailVerificationAsync(user);
+ }
+
+ [Fact]
+ public async Task PostVerifyEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
+ {
+ ConfigureUserServiceToReturnNullPrincipal();
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostVerifyEmail()
+ );
+ }
+
+ [Fact]
+ public async Task PostVerifyEmailToken_ShouldConfirmEmail()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidIdFor(user);
+ _userService.ConfirmEmailAsync(user, Arg.Any())
+ .Returns(Task.FromResult(IdentityResult.Success));
+
+ await _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" });
+
+ await _userService.Received(1).ConfirmEmailAsync(user, Arg.Any());
+ }
+
+ [Fact]
+ public async Task PostVerifyEmailToken_WhenUserDoesNotExist_ShouldThrowUnauthorizedAccessException()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnNullUserId();
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" })
+ );
+ }
+
+ [Fact]
+ public async Task PostVerifyEmailToken_WhenEmailConfirmationFails_ShouldThrowBadRequestException()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidIdFor(user);
+ _userService.ConfirmEmailAsync(user, Arg.Any())
+ .Returns(Task.FromResult(IdentityResult.Failed()));
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostVerifyEmailToken(new VerifyEmailRequestModel { UserId = "12345678-1234-1234-1234-123456789012" })
+ );
+ }
+
+ [Fact]
+ public async Task PostPassword_ShouldChangePassword()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ _userService.ChangePasswordAsync(user, default, default, default)
+ .Returns(Task.FromResult(IdentityResult.Success));
+
+ await _sut.PostPassword(new PasswordRequestModel());
+
+ await _userService.Received(1).ChangePasswordAsync(user, default, default, default);
+ }
+
+ [Fact]
+ public async Task PostPassword_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
+ {
+ ConfigureUserServiceToReturnNullPrincipal();
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostPassword(new PasswordRequestModel())
+ );
+ }
+
+ [Fact]
+ public async Task PostPassword_WhenPasswordChangeFails_ShouldBadRequestException()
+ {
+ var user = GenerateExampleUser();
+ ConfigureUserServiceToReturnValidPrincipalFor(user);
+ _userService.ChangePasswordAsync(user, default, default, default)
+ .Returns(Task.FromResult(IdentityResult.Failed()));
+
+ await Assert.ThrowsAsync(
+ () => _sut.PostPassword(new PasswordRequestModel())
+ );
+ }
+
+ // Below are helper functions that currently belong to this
+ // test class, but ultimately may need to be split out into
+ // something greater in order to share common test steps with
+ // other test suites. They are included here for the time being
+ // until that day comes.
+ private User GenerateExampleUser()
+ {
+ return new User
+ {
+ Email = "user@example.com"
+ };
+ }
+
+ private void ConfigureUserServiceToReturnNullPrincipal()
+ {
+ _userService.GetUserByPrincipalAsync(Arg.Any())
+ .Returns(Task.FromResult((User)null));
+ }
+
+ private void ConfigureUserServiceToReturnValidPrincipalFor(User user)
+ {
+ _userService.GetUserByPrincipalAsync(Arg.Any())
+ .Returns(Task.FromResult(user));
+ }
+
+ private void ConfigureUserServiceToRejectPasswordFor(User user)
+ {
+ _userService.CheckPasswordAsync(user, Arg.Any())
+ .Returns(Task.FromResult(false));
+ }
+
+ private void ConfigureUserServiceToAcceptPasswordFor(User user)
+ {
+ _userService.CheckPasswordAsync(user, Arg.Any())
+ .Returns(Task.FromResult(true));
+ }
+
+ private void ConfigureUserServiceToReturnValidIdFor(User user)
+ {
+ _userService.GetUserByIdAsync(Arg.Any())
+ .Returns(Task.FromResult(user));
+ }
+
+ private void ConfigureUserServiceToReturnNullUserId()
+ {
+ _userService.GetUserByIdAsync(Arg.Any())
+ .Returns(Task.FromResult((User)null));
+ }
+ }
+}
+