using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Net.Http; using System.Text; using System.Threading.Tasks; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Models.Response; using Bit.Core.Models.View; using Bit.Core.Services; using Bit.Core.Test.AutoFixture; using Bit.Core.Utilities; using Bit.Test.Common; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Newtonsoft.Json; using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; namespace Bit.Core.Test.Services { public class SendServiceTests { [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task ReplaceAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); await sutProvider.Sut.ReplaceAsync(actualSendDataDict); await sutProvider.GetDependency().SetEncryptedSendsAsync(actualSendDataDict); } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 0)] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 1)] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 2)] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 3)] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 4)] public async Task DeleteAsync_Success(int numberToDelete, SutProvider sutProvider, string userId, IEnumerable sendDatas) { var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(actualSendDataDict); var idsToDelete = actualSendDataDict.Take(numberToDelete).Select(kvp => kvp.Key).ToArray(); var expectedSends = actualSendDataDict.Skip(numberToDelete).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); await sutProvider.Sut.DeleteAsync(idsToDelete); await sutProvider.GetDependency().SetEncryptedSendsAsync( Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); } [Theory, SutAutoData] public async Task ClearAsync_Success(SutProvider sutProvider, string userId) { await sutProvider.Sut.ClearAsync(userId); await sutProvider.GetDependency().SetEncryptedSendsAsync(null, userId); } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task DeleteWithServerAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var initialSendDatas = sendDatas.ToDictionary(d => d.Id, d => d); var idToDelete = initialSendDatas.First().Key; var expectedSends = initialSendDatas.Skip(1).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency() .GetEncryptedSendsAsync(Arg.Any()).Returns(initialSendDatas); await sutProvider.Sut.DeleteWithServerAsync(idToDelete); await sutProvider.GetDependency().Received(1).DeleteSendAsync(idToDelete); await sutProvider.GetDependency().SetEncryptedSendsAsync( Arg.Is>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s))); } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task GetAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); foreach (var dataKvp in sendDataDict) { var expected = new Send(dataKvp.Value); var actual = await sutProvider.Sut.GetAsync(dataKvp.Key); TestHelper.AssertPropertyEqual(expected, actual); } } [Theory, SutAutoData] public async Task GetAsync_NonExistingId_ReturnsNull(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var actual = await sutProvider.Sut.GetAsync(Guid.NewGuid().ToString()); Assert.Null(actual); } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task GetAllAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var allExpected = sendDataDict.Select(kvp => new Send(kvp.Value)); var allActual = await sutProvider.Sut.GetAllAsync(); foreach (var (actual, expected) in allActual.Zip(allExpected)) { TestHelper.AssertPropertyEqual(expected, actual); } } [Theory, SutAutoData] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task GetAllDecryptedAsync_Success(SutProvider sutProvider, string userId, IEnumerable sendDatas) { // TODO restore this once race condition is fixed or GHA can re-run jobs on individual platforms return; var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d); sutProvider.GetDependency().HasUserKeyAsync().Returns(true); ServiceContainer.Register("cryptoService", sutProvider.GetDependency()); sutProvider.GetDependency().StringComparer.Returns(StringComparer.CurrentCulture); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(sendDataDict); var actual = await sutProvider.Sut.GetAllDecryptedAsync(); Assert.Equal(sendDataDict.Count, actual.Count); foreach (var (actualView, expectedId) in actual.Zip(sendDataDict.Select(s => s.Key))) { // Note Send -> SendView is tested in SendTests Assert.Equal(expectedId, actualView.Id); } ServiceContainer.Reset(); } // SaveWithServer() [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] public async Task SaveWithServerAsync_NewTextSend_Success(SutProvider sutProvider, string userId, SendResponse response, Send send) { send.Id = null; sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PostSendAsync(Arg.Any()).Returns(response); var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content")); await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes); Predicate sendRequestPredicate = r => { // Note Send -> SendRequest tested in SendRequestTests TestHelper.AssertPropertyEqual(new SendRequest(send, fileContentBytes.Buffer?.LongLength), r); return true; }; switch (send.Type) { case SendType.Text: await sutProvider.GetDependency().Received(1) .PostSendAsync(Arg.Is(r => sendRequestPredicate(r))); break; case SendType.File: default: throw new Exception("Untested send type"); } } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task SaveWithServerAsync_NewFileSend_AzureUpload_Success(SutProvider sutProvider, string userId, SendFileUploadDataResponse response, Send send) { send.Id = null; response.FileUploadType = FileUploadType.Azure; sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PostFileTypeSendAsync(Arg.Any()).Returns(response); var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content")); await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes); switch (send.Type) { case SendType.File: await sutProvider.GetDependency().Received(1).UploadSendFileAsync(response, send.File.FileName, fileContentBytes); break; case SendType.Text: default: throw new Exception("Untested send type"); } } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task SaveWithServerAsync_NewFileSend_LegacyFallback_Success(SutProvider sutProvider, string userId, Send send, SendResponse response) { send.Id = null; sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); var error = new ErrorResponse(null, System.Net.HttpStatusCode.NotFound); sutProvider.GetDependency().PostFileTypeSendAsync(Arg.Any()).Throws(new ApiException(error)); sutProvider.GetDependency().PostSendFileAsync(Arg.Any()).Returns(response); var fileContentBytes = new EncByteArray(Encoding.UTF8.GetBytes("This is the file content")); await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes); await sutProvider.GetDependency().Received(1).PostSendFileAsync(Arg.Any()); } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task SaveWithServerAsync_PutSend_Success(SutProvider sutProvider, string userId, SendResponse response, Send send) { sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().PutSendAsync(send.Id, Arg.Any()).Returns(response); await sutProvider.Sut.SaveWithServerAsync(send, null); Predicate sendRequestPredicate = r => { // Note Send -> SendRequest tested in SendRequestTests TestHelper.AssertPropertyEqual(new SendRequest(send, null), r); return true; }; await sutProvider.GetDependency().Received(1) .PutSendAsync(send.Id, Arg.Is(r => sendRequestPredicate(r))); } [Theory, SutAutoData] public async Task RemovePasswordWithServerAsync_Success(SutProvider sutProvider, SendResponse response, string sendId) { sutProvider.GetDependency().PutSendRemovePasswordAsync(sendId).Returns(response); await sutProvider.Sut.RemovePasswordWithServerAsync(sendId); await sutProvider.GetDependency().Received(1).PutSendRemovePasswordAsync(sendId); await sutProvider.GetDependency().SetEncryptedSendsAsync(default, default); } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task UpsertAsync_Update_Success(SutProvider sutProvider, string userId, IEnumerable initialSends) { var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(initialSendDict); var updatedSends = CoreHelpers.Clone(initialSendDict); foreach (var kvp in updatedSends) { kvp.Value.Disabled = !kvp.Value.Disabled; } await sutProvider.Sut.UpsertAsync(updatedSends.Values.ToArray()); Predicate> matchSendsPredicate = actual => { Assert.Equal(updatedSends.Count, actual.Count); foreach (var (expectedKvp, actualKvp) in updatedSends.Zip(actual)) { Assert.Equal(expectedKvp.Key, actualKvp.Key); TestHelper.AssertPropertyEqual(expectedKvp.Value, actualKvp.Value); } return true; }; await sutProvider.GetDependency().SetEncryptedSendsAsync( Arg.Is>(d => matchSendsPredicate(d))); } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })] public async Task UpsertAsync_NewSends_Success(SutProvider sutProvider, string userId, IEnumerable initialSends, IEnumerable newSends) { var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s); sutProvider.GetDependency().GetActiveUserIdAsync().Returns(userId); sutProvider.GetDependency().GetEncryptedSendsAsync().Returns(initialSendDict); var expectedDict = CoreHelpers.Clone(initialSendDict).Concat(newSends.Select(s => new KeyValuePair(s.Id, s))); await sutProvider.Sut.UpsertAsync(newSends.ToArray()); Predicate> matchSendsPredicate = actual => { Assert.Equal(expectedDict.Count(), actual.Count); foreach (var (expectedKvp, actualKvp) in expectedDict.Zip(actual)) { Assert.Equal(expectedKvp.Key, actualKvp.Key); TestHelper.AssertPropertyEqual(expectedKvp.Value, actualKvp.Value); } return true; }; await sutProvider.GetDependency().SetEncryptedSendsAsync( Arg.Is>(d => matchSendsPredicate(d))); } [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(SymmetricCryptoKeyCustomization), typeof(TextSendCustomization) })] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(SymmetricCryptoKeyCustomization), typeof(FileSendCustomization) })] public async Task EncryptAsync_Success(SutProvider sutProvider, SendView view, byte[] fileData, SymmetricCryptoKey privateKey) { var prefix = "encrypted_"; var prefixBytes = Encoding.UTF8.GetBytes(prefix); byte[] getPbkdf(string password, byte[] key) => prefixBytes.Concat(Encoding.UTF8.GetBytes(password)).Concat(key).ToArray(); EncString encryptBytes(byte[] secret, SymmetricCryptoKey key) => new EncString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}"); EncString encrypt(string secret, SymmetricCryptoKey key) => new EncString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}"); EncByteArray encryptFileBytes(byte[] secret, SymmetricCryptoKey key) => new EncByteArray(secret.Concat(key.Key).ToArray()); sutProvider.GetDependency().Pbkdf2Async(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) .Returns(info => getPbkdf((string)info[0], (byte[])info[1])); sutProvider.GetDependency().EncryptAsync(Arg.Any(), Arg.Any()) .Returns(info => encryptBytes((byte[])info[0], (SymmetricCryptoKey)info[1])); sutProvider.GetDependency().EncryptAsync(Arg.Any(), Arg.Any()) .Returns(info => encrypt((string)info[0], (SymmetricCryptoKey)info[1])); sutProvider.GetDependency().EncryptToBytesAsync(Arg.Any(), Arg.Any()) .Returns(info => encryptFileBytes((byte[])info[0], (SymmetricCryptoKey)info[1])); var (send, encryptedFileData) = await sutProvider.Sut.EncryptAsync(view, fileData, view.Password, privateKey); TestHelper.AssertPropertyEqual(view, send, "Password", "Key", "Name", "Notes", "Text", "File", "AccessCount", "AccessId", "CryptoKey", "RevisionDate", "DeletionDate", "ExpirationDate", "UrlB64Key", "MaxAccessCountReached", "Expired", "PendingDelete", "HasPassword", "DisplayDate"); Assert.Equal(Convert.ToBase64String(getPbkdf(view.Password, view.Key)), send.Password); TestHelper.AssertPropertyEqual(encryptBytes(view.Key, privateKey), send.Key); TestHelper.AssertPropertyEqual(encrypt(view.Name, view.CryptoKey), send.Name); TestHelper.AssertPropertyEqual(encrypt(view.Notes, view.CryptoKey), send.Notes); switch (view.Type) { case SendType.Text: TestHelper.AssertPropertyEqual(view.Text, send.Text, "Text", "MaskedText"); TestHelper.AssertPropertyEqual(encrypt(view.Text.Text, view.CryptoKey), send.Text.Text); break; case SendType.File: // Only set filename TestHelper.AssertPropertyEqual(encrypt(view.File.FileName, view.CryptoKey), send.File.FileName); Assert.Equal(encryptFileBytes(fileData, view.CryptoKey).Buffer, encryptedFileData.Buffer); break; default: throw new Exception("Untested send type"); } } } }