mirror of
https://github.com/bitwarden/mobile
synced 2026-01-07 02:53:56 +00:00
Port send jslib to mobile (#1219)
* Expand Hkdf crypto functions * Add tests for hkdf crypto functions Took the testing infrastructure from bitwarden/server * Move Hkdf to cryptoFunctionService * Port changes from bitwarden/jslib#192 * Port changes from bitwarden/jslib#205 * Make Send Expiration Optional implement changes from bitwarden/jslib#242 * Bug fixes found by testing * Test helpers * Test conversion between model types * Test SendService These are mostly happy-path tests to ensure a reasonably correct implementation * Add run tests step to GitHub Actions * Test send decryption * Test Request generation from Send * Constructor dependencies on separate lines * Remove unused testing infrastructure * Rename to match class name * Move fat arrows to previous lines * Handle exceptions in App layer * PR review cleanups * Throw when attempting to save an unkown Send Type I think it's best to only throw on unknown send types here. I don't think we want to throw whenever we encounter one since that would do bad things like lock up Sync if clients get out of date relative to servers. Instead, keep the client from ruining saved data by complaining last minute that it doesn't know what it's doing.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using AutoFixture;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture
|
||||
{
|
||||
public class SymmetricCryptoKeyCustomization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var keyMaterial = (new PclCryptoFunctionService(null)).RandomBytes(32);
|
||||
fixture.Register(() => new SymmetricCryptoKey(keyMaterial));
|
||||
}
|
||||
}
|
||||
}
|
||||
65
test/Core.Test/AutoFixture/Send/SendCustomizations.cs
Normal file
65
test/Core.Test/AutoFixture/Send/SendCustomizations.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Models.Api;
|
||||
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.Enums;
|
||||
|
||||
namespace Bit.Core.Test.AutoFixture
|
||||
{
|
||||
internal class TextSendCustomization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<SendData>(composer => composer
|
||||
.With(c => c.Type, SendType.Text)
|
||||
.With(c => c.Text, fixture.Create<SendTextData>())
|
||||
.Without(c => c.File));
|
||||
fixture.Customize<Send>(composer => composer
|
||||
.With(c => c.Type, SendType.Text)
|
||||
.With(c => c.Text, fixture.Create<SendText>())
|
||||
.Without(c => c.File));
|
||||
fixture.Customize<SendView>(composer => composer
|
||||
.With(c => c.Type, SendType.Text)
|
||||
.With(c => c.Text, fixture.Create<SendTextView>())
|
||||
.Without(c => c.File));
|
||||
fixture.Customize<SendRequest>(composer => composer
|
||||
.With(c => c.Type, SendType.Text)
|
||||
.With(c => c.Text, fixture.Create<SendTextApi>())
|
||||
.Without(c => c.File));
|
||||
fixture.Customize<SendResponse>(composer => composer
|
||||
.With(c => c.Type, SendType.Text)
|
||||
.With(c => c.Text, fixture.Create<SendTextApi>())
|
||||
.Without(c => c.File));
|
||||
}
|
||||
}
|
||||
|
||||
internal class FileSendCustomization : ICustomization
|
||||
{
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<SendData>(composer => composer
|
||||
.With(c => c.Type, SendType.File)
|
||||
.With(c => c.File, fixture.Create<SendFileData>())
|
||||
.Without(c => c.Text));
|
||||
fixture.Customize<Send>(composer => composer
|
||||
.With(c => c.Type, SendType.File)
|
||||
.With(c => c.File, fixture.Create<SendFile>())
|
||||
.Without(c => c.Text));
|
||||
fixture.Customize<SendView>(composer => composer
|
||||
.With(c => c.Type, SendType.File)
|
||||
.With(c => c.File, fixture.Create<SendFileView>())
|
||||
.Without(c => c.Text));
|
||||
fixture.Customize<SendRequest>(composer => composer
|
||||
.With(c => c.Type, SendType.File)
|
||||
.With(c => c.File, fixture.Create<SendFileApi>())
|
||||
.Without(c => c.Text));
|
||||
fixture.Customize<SendResponse>(composer => composer
|
||||
.With(c => c.Type, SendType.File)
|
||||
.With(c => c.File, fixture.Create<SendFileApi>())
|
||||
.Without(c => c.Text));
|
||||
}
|
||||
}
|
||||
}
|
||||
26
test/Core.Test/Core.Test.csproj
Normal file
26
test/Core.Test/Core.Test.csproj
Normal file
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>Bit.Core.Test</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
|
||||
<PackageReference Include="NSubstitute" Version="4.2.2" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
|
||||
<PackageReference Include="AutoFixture.AutoNSubstitute" Version="4.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
23
test/Core.Test/Models/Data/SendDataTests.cs
Normal file
23
test/Core.Test/Models/Data/SendDataTests.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Test.Common;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Data
|
||||
{
|
||||
public class SendDataTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(FileSendCustomization) })]
|
||||
public void SendData_FromSendResponse_Success(string userId, SendResponse response)
|
||||
{
|
||||
var data = new SendData(response, userId);
|
||||
|
||||
TestHelper.AssertPropertyEqual(response, data, "UserId");
|
||||
Assert.Equal(data.UserId, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
78
test/Core.Test/Models/Domain/SendTests.cs
Normal file
78
test/Core.Test/Models/Domain/SendTests.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Test.Common;
|
||||
using System.Text;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using AutoFixture.AutoNSubstitute;
|
||||
|
||||
namespace Bit.Core.Test.Models.Domain
|
||||
{
|
||||
public class SendTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(FileSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(TextSendCustomization) })]
|
||||
public void Send_FromSendData_Success(SendData data)
|
||||
{
|
||||
var send = new Send(data);
|
||||
|
||||
TestHelper.AssertPropertyEqual(data, send, "Name", "Notes", "Key", "SendFileData.FileName", "SendFileData.Key", "SendTextData.Text");
|
||||
Assert.Equal(data.Name, send.Name?.EncryptedString);
|
||||
Assert.Equal(data.Notes, send.Notes?.EncryptedString);
|
||||
Assert.Equal(data.Key, send.Key?.EncryptedString);
|
||||
Assert.Equal(data.Text?.Text, send.Text?.Text?.EncryptedString);
|
||||
Assert.Equal(data.File?.FileName, send.File?.FileName?.EncryptedString);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(AutoNSubstituteCustomization), typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(AutoNSubstituteCustomization), typeof(FileSendCustomization) })]
|
||||
public async void DecryptAsync_Success(ICryptoService cryptoService, Send send)
|
||||
{
|
||||
var prefix = "decrypted_";
|
||||
var prefixBytes = Encoding.UTF8.GetBytes(prefix);
|
||||
|
||||
cryptoService.DecryptToBytesAsync(Arg.Any<CipherString>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => prefixBytes.Concat(Encoding.UTF8.GetBytes(((CipherString)info[0]).EncryptedString)).ToArray());
|
||||
cryptoService.DecryptFromBytesAsync(Arg.Any<byte[]>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => prefixBytes.Concat((byte[])info[0]).ToArray());
|
||||
cryptoService.DecryptToUtf8Async(Arg.Any<CipherString>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => $"{prefix}{((CipherString)info[0]).EncryptedString}");
|
||||
ServiceContainer.Register("cryptoService", cryptoService);
|
||||
|
||||
var view = await send.DecryptAsync();
|
||||
|
||||
string expectedDecryptionString(CipherString encryptedString) =>
|
||||
encryptedString?.EncryptedString == null ? null : $"{prefix}{encryptedString.EncryptedString}";
|
||||
|
||||
TestHelper.AssertPropertyEqual(send, view, "Name", "Notes", "File", "Text", "Key", "UserId");
|
||||
Assert.Equal(expectedDecryptionString(send.Name), view.Name);
|
||||
Assert.Equal(expectedDecryptionString(send.Notes), view.Notes);
|
||||
Assert.Equal(Encoding.UTF8.GetBytes(expectedDecryptionString(send.Key)), view.Key);
|
||||
|
||||
switch (send.Type)
|
||||
{
|
||||
case SendType.File:
|
||||
TestHelper.AssertPropertyEqual(send.File, view.File, "FileName");
|
||||
Assert.Equal(expectedDecryptionString(send.File.FileName), view.File.FileName);
|
||||
break;
|
||||
case SendType.Text:
|
||||
TestHelper.AssertPropertyEqual(send.Text, view?.Text, "Text");
|
||||
Assert.Equal(expectedDecryptionString(send.Text.Text), view.Text.Text);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Untested Send type");
|
||||
}
|
||||
|
||||
ServiceContainer.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
test/Core.Test/Models/Request/SendRequestTests.cs
Normal file
43
test/Core.Test/Models/Request/SendRequestTests.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Models.Request
|
||||
{
|
||||
public class SendRequestTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(FileSendCustomization) })]
|
||||
public void SendRequest_FromSend_Success(Send send)
|
||||
{
|
||||
var request = new SendRequest(send);
|
||||
|
||||
TestHelper.AssertPropertyEqual(send, request, "Id", "AccessId", "UserId", "Name", "Notes", "File", "Text", "Key", "AccessCount", "RevisionDate");
|
||||
Assert.Equal(send.Name?.EncryptedString, request.Name);
|
||||
Assert.Equal(send.Notes?.EncryptedString, request.Notes);
|
||||
|
||||
switch (send.Type)
|
||||
{
|
||||
case SendType.File:
|
||||
// Only sets filename
|
||||
Assert.Equal(send.File.FileName?.EncryptedString, request.File.FileName);
|
||||
break;
|
||||
case SendType.Text:
|
||||
TestHelper.AssertPropertyEqual(send.Text, request?.Text, "Text");
|
||||
Assert.Equal(send.Text.Text?.EncryptedString, request.Text.Text);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Untested Send type");
|
||||
}
|
||||
|
||||
ServiceContainer.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
87
test/Core.Test/Services/CryptoFunctionServiceTests.cs
Normal file
87
test/Core.Test/Services/CryptoFunctionServiceTests.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
{
|
||||
public class CryptoFunctionServiceTests
|
||||
{
|
||||
const string regular256Key = "qBUmEYtwTwwGPuw/z6bs/qYXXYNUlocFlyAuuANI8Pw=";
|
||||
const string utf8256Key = "6DfJwW1R3txgiZKkIFTvVAb7qVlG7lKcmJGJoxR2GBU=";
|
||||
const string unicode256Key = "gejGI82xthA+nKtKmIh82kjw+ttHr+ODsUoGdu5sf0A=";
|
||||
const string regular512Key = "xe5cIG6ZfwGmb1FvsOedM0XKOm21myZkjL/eDeKIqqM=";
|
||||
const string utf8512Key = "XQMVBnxVEhlvjSFDQc77j5GDE9aorvbS0vKnjhRg0LY=";
|
||||
const string unicode512Key = "148GImrTbrjaGAe/iWEpclINM8Ehhko+9lB14+52lqc=";
|
||||
const string regularSalt = "salt";
|
||||
const string utf8Salt = "üser_salt";
|
||||
const string unicodeSalt = "😀salt🙏";
|
||||
const string regularInfo = "info";
|
||||
const string utf8Info = "üser_info";
|
||||
const string unicodeInfo = "😀info🙏";
|
||||
|
||||
const string prk16Byte = "criAmKtfzxanbgea5/kelQ==";
|
||||
const string prk32Byte = "F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y=";
|
||||
const string prk64Byte = "ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+" +
|
||||
"gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==";
|
||||
|
||||
|
||||
[Theory, AutoSubstitutionData]
|
||||
async public Task HkdfExpand_PrkTooSmall_Throws(PclCryptoFunctionService sut)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => sut.HkdfExpandAsync(Convert.FromBase64String(prk16Byte), "info", 32, HkdfAlgorithm.Sha256));
|
||||
Assert.Contains("too small", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, AutoSubstitutionData]
|
||||
async public Task HkdfoExpand_OutputTooBig_Throws(PclCryptoFunctionService sut)
|
||||
{
|
||||
var exception = await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => sut.HkdfExpandAsync(Convert.FromBase64String(prk32Byte), "info", 8161, HkdfAlgorithm.Sha256));
|
||||
Assert.Contains("too large", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineAutoSubstitutionData(regular256Key, HkdfAlgorithm.Sha256, prk16Byte, regularSalt, regularInfo)]
|
||||
[InlineAutoSubstitutionData(utf8256Key, HkdfAlgorithm.Sha256, prk16Byte, utf8Salt, utf8Info)]
|
||||
[InlineAutoSubstitutionData(unicode256Key, HkdfAlgorithm.Sha256, prk16Byte, unicodeSalt, unicodeInfo)]
|
||||
[InlineAutoSubstitutionData(regular512Key, HkdfAlgorithm.Sha512, prk16Byte, regularSalt, regularInfo)]
|
||||
[InlineAutoSubstitutionData(utf8512Key, HkdfAlgorithm.Sha512, prk16Byte, utf8Salt, utf8Info)]
|
||||
[InlineAutoSubstitutionData(unicode512Key, HkdfAlgorithm.Sha512, prk16Byte, unicodeSalt, unicodeInfo)]
|
||||
async public Task Hkdf_Success(string expectedKey, HkdfAlgorithm algorithm, string ikmString, string salt, string info, PclCryptoFunctionService sut)
|
||||
{
|
||||
byte[] ikm = Convert.FromBase64String(ikmString);
|
||||
|
||||
var key = await sut.HkdfAsync(ikm, salt, info, 32, algorithm);
|
||||
Assert.Equal(expectedKey, Convert.ToBase64String(key));
|
||||
|
||||
var keyFromByteArray = await sut.HkdfAsync(ikm, Encoding.UTF8.GetBytes(salt), Encoding.UTF8.GetBytes(info), 32, algorithm);
|
||||
Assert.Equal(key, keyFromByteArray);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineAutoSubstitutionData("BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8=",
|
||||
HkdfAlgorithm.Sha256, prk32Byte, 32, regularInfo)]
|
||||
[InlineAutoSubstitutionData("BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+/queOZenPNkDhmlVyL2WZ3OSU5+7ISNF5NhNfvZA==",
|
||||
HkdfAlgorithm.Sha256, prk32Byte, 64, regularInfo)]
|
||||
[InlineAutoSubstitutionData("uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlk=",
|
||||
HkdfAlgorithm.Sha512, prk64Byte, 32, regularInfo)]
|
||||
[InlineAutoSubstitutionData("uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlkY5Pv0sB+MqvaopmkC6sD/j89zDwTV9Ib2fpucUydO8w==",
|
||||
HkdfAlgorithm.Sha512, prk64Byte, 64, regularInfo)]
|
||||
async public Task HkdfExpand_Success(string expectedKey, HkdfAlgorithm algorithm, string prkString, int outputByteSize, string info, PclCryptoFunctionService sut)
|
||||
{
|
||||
var prk = Convert.FromBase64String(prkString);
|
||||
|
||||
var key = await sut.HkdfExpandAsync(prk, info, outputByteSize, algorithm);
|
||||
Assert.Equal(expectedKey, Convert.ToBase64String(key));
|
||||
|
||||
var keyFromByteArray = await sut.HkdfExpandAsync(prk, Encoding.UTF8.GetBytes(info), outputByteSize, algorithm);
|
||||
Assert.Equal(key, keyFromByteArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
376
test/Core.Test/Services/SendServiceTests.cs
Normal file
376
test/Core.Test/Services/SendServiceTests.cs
Normal file
@@ -0,0 +1,376 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Test.Common;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using System.Text;
|
||||
using System.Net.Http;
|
||||
using Bit.Core.Models.Request;
|
||||
using Bit.Core.Test.AutoFixture;
|
||||
using System.Linq.Expressions;
|
||||
using Bit.Core.Models.View;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
{
|
||||
public class SendServiceTests
|
||||
{
|
||||
private string GetSendKey(string userId) => SendService.GetSendKey(userId);
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
public async Task ReplaceAsync_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
|
||||
{
|
||||
var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
|
||||
await sutProvider.Sut.ReplaceAsync(actualSendDataDict);
|
||||
|
||||
await sutProvider.GetDependency<IStorageService>()
|
||||
.Received(1).SaveAsync(GetSendKey(userId), 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<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
|
||||
{
|
||||
var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IStorageService>()
|
||||
.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).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<IStorageService>().Received(1)
|
||||
.SaveAsync(GetSendKey(userId),
|
||||
Arg.Is<Dictionary<string, SendData>>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s)));
|
||||
}
|
||||
|
||||
[Theory, SutAutoData]
|
||||
public async Task ClearAsync_Success(SutProvider<SendService> sutProvider, string userId)
|
||||
{
|
||||
await sutProvider.Sut.ClearAsync(userId);
|
||||
|
||||
await sutProvider.GetDependency<IStorageService>().Received(1).RemoveAsync(GetSendKey(userId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
public async Task DeleteWithServerAsync_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> 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<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IStorageService>()
|
||||
.GetAsync<Dictionary<string, SendData>>(Arg.Any<string>()).Returns(initialSendDatas);
|
||||
|
||||
await sutProvider.Sut.DeleteWithServerAsync(idToDelete);
|
||||
|
||||
await sutProvider.GetDependency<IApiService>().Received(1).DeleteSendAsync(idToDelete);
|
||||
await sutProvider.GetDependency<IStorageService>().Received(1)
|
||||
.SaveAsync(GetSendKey(userId),
|
||||
Arg.Is<Dictionary<string, SendData>>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
public async Task GetAsync_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
|
||||
{
|
||||
var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).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_NonExistringId_ReturnsNull(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
|
||||
{
|
||||
var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).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<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
|
||||
{
|
||||
var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).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<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
|
||||
{
|
||||
var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
|
||||
sutProvider.GetDependency<ICryptoService>().HasKeyAsync().Returns(true);
|
||||
ServiceContainer.Register("cryptoService", sutProvider.GetDependency<ICryptoService>());
|
||||
sutProvider.GetDependency<II18nService>().StringComparer.Returns(StringComparer.CurrentCulture);
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).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<SendService> sutProvider, string userId, SendResponse response, Send send)
|
||||
{
|
||||
send.Id = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IApiService>().PostSendAsync(Arg.Any<SendRequest>()).Returns(response);
|
||||
sutProvider.GetDependency<IApiService>().PostSendFileAsync(Arg.Any<MultipartFormDataContent>()).Returns(response);
|
||||
|
||||
var fileContentBytes = Encoding.UTF8.GetBytes("This is the file content");
|
||||
|
||||
await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes);
|
||||
|
||||
Predicate<SendRequest> sendRequestPredicate = r =>
|
||||
{
|
||||
// Note Send -> SendRequest tested in SendRequestTests
|
||||
TestHelper.AssertPropertyEqual(new SendRequest(send), r);
|
||||
return true;
|
||||
};
|
||||
|
||||
switch (send.Type)
|
||||
{
|
||||
case SendType.Text:
|
||||
await sutProvider.GetDependency<IApiService>().Received(1)
|
||||
.PostSendAsync(Arg.Is<SendRequest>(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_Success(SutProvider<SendService> sutProvider, string userId, SendResponse response, Send send)
|
||||
{
|
||||
send.Id = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IApiService>().PostSendAsync(Arg.Any<SendRequest>()).Returns(response);
|
||||
sutProvider.GetDependency<IApiService>().PostSendFileAsync(Arg.Any<MultipartFormDataContent>()).Returns(response);
|
||||
|
||||
var fileContentBytes = Encoding.UTF8.GetBytes("This is the file content");
|
||||
|
||||
await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes);
|
||||
|
||||
Predicate<MultipartFormDataContent> formDataPredicate = fd =>
|
||||
{
|
||||
Assert.Equal(2, fd.Count()); // expect a request and file content
|
||||
|
||||
var expectedRequest = JsonConvert.SerializeObject(new SendRequest(send));
|
||||
var actualRequest = fd.First().ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
Assert.Equal(expectedRequest, actualRequest);
|
||||
|
||||
var actualFileContent = fd.Skip(1).First().ReadAsByteArrayAsync().GetAwaiter().GetResult();
|
||||
Assert.Equal(fileContentBytes, actualFileContent);
|
||||
return true;
|
||||
};
|
||||
|
||||
switch (send.Type)
|
||||
{
|
||||
case SendType.File:
|
||||
await sutProvider.GetDependency<IApiService>().Received(1)
|
||||
.PostSendFileAsync(Arg.Is<MultipartFormDataContent>(f => formDataPredicate(f)));
|
||||
break;
|
||||
case SendType.Text:
|
||||
default:
|
||||
throw new Exception("Untested send type");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
public async Task SaveWithServerAsync_PutSend_Success(SutProvider<SendService> sutProvider, string userId, SendResponse response, Send send)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IApiService>().PutSendAsync(send.Id, Arg.Any<SendRequest>()).Returns(response);
|
||||
|
||||
await sutProvider.Sut.SaveWithServerAsync(send, null);
|
||||
|
||||
Predicate<SendRequest> sendRequestPredicate = r =>
|
||||
{
|
||||
// Note Send -> SendRequest tested in SendRequestTests
|
||||
TestHelper.AssertPropertyEqual(new SendRequest(send), r);
|
||||
return true;
|
||||
};
|
||||
|
||||
await sutProvider.GetDependency<IApiService>().Received(1)
|
||||
.PutSendAsync(send.Id, Arg.Is<SendRequest>(r => sendRequestPredicate(r)));
|
||||
}
|
||||
|
||||
[Theory, SutAutoData]
|
||||
public async Task RemovePasswordWithServerAsync_Success(SutProvider<SendService> sutProvider, SendResponse response, string sendId)
|
||||
{
|
||||
sutProvider.GetDependency<IApiService>().PutSendRemovePasswordAsync(sendId).Returns(response);
|
||||
|
||||
await sutProvider.Sut.RemovePasswordWithServerAsync(sendId);
|
||||
|
||||
await sutProvider.GetDependency<IApiService>().Received(1).PutSendRemovePasswordAsync(sendId);
|
||||
await sutProvider.GetDependency<IStorageService>().ReceivedWithAnyArgs(1).SaveAsync<Dictionary<string, SendData>>(default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
public async Task UpsertAsync_Update_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> initialSends)
|
||||
{
|
||||
var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s);
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).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<Dictionary<string, SendData>> 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<IStorageService>().Received(1).SaveAsync(GetSendKey(userId), Arg.Is<Dictionary<string, SendData>>(d => matchSendsPredicate(d)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
|
||||
public async Task UpsertAsync_NewSends_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> initialSends, IEnumerable<SendData> newSends)
|
||||
{
|
||||
var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s);
|
||||
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
|
||||
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).Returns(initialSendDict);
|
||||
|
||||
var expectedDict = CoreHelpers.Clone(initialSendDict).Concat(newSends.Select(s => new KeyValuePair<string, SendData>(s.Id, s)));
|
||||
|
||||
await sutProvider.Sut.UpsertAsync(newSends.ToArray());
|
||||
|
||||
Predicate<Dictionary<string, SendData>> 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<IStorageService>().Received(1).SaveAsync(GetSendKey(userId), Arg.Is<Dictionary<string, SendData>>(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<SendService> 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();
|
||||
CipherString encryptBytes(byte[] secret, SymmetricCryptoKey key) =>
|
||||
new CipherString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}");
|
||||
CipherString encrypt(string secret, SymmetricCryptoKey key) =>
|
||||
new CipherString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}");
|
||||
|
||||
sutProvider.GetDependency<ICryptoFunctionService>().Pbkdf2Async(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CryptoHashAlgorithm>(), Arg.Any<int>())
|
||||
.Returns(info => getPbkdf((string)info[0], (byte[])info[1]));
|
||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(Arg.Any<byte[]>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => encryptBytes((byte[])info[0], (SymmetricCryptoKey)info[1]));
|
||||
sutProvider.GetDependency<ICryptoService>().EncryptAsync(Arg.Any<string>(), Arg.Any<SymmetricCryptoKey>())
|
||||
.Returns(info => encrypt((string)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");
|
||||
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);
|
||||
TestHelper.AssertPropertyEqual(encryptBytes(fileData, view.CryptoKey), encryptedFileData);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Untested send type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user