SEBWIN-296: Implemented unit tests for configuration repository.

This commit is contained in:
dbuechel 2019-02-22 10:02:34 +01:00
parent f8cb521a1e
commit 5016c14ff3
7 changed files with 333 additions and 90 deletions

View file

@ -7,11 +7,16 @@
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Configuration.ConfigurationData;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Cryptography;
using SafeExamBrowser.Contracts.Configuration.DataFormats;
using SafeExamBrowser.Contracts.Configuration.DataResources;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.UnitTests
@ -20,19 +25,252 @@ namespace SafeExamBrowser.Configuration.UnitTests
public class ConfigurationRepositoryTests
{
private ConfigurationRepository sut;
private Mock<IDataParser> binaryParser;
private Mock<IDataSerializer> binarySerializer;
private Mock<ICertificateStore> certificateStore;
private Mock<IResourceLoader> fileLoader;
private Mock<IResourceSaver> fileSaver;
private Mock<IHashAlgorithm> hashAlgorithm;
private Mock<IModuleLogger> logger;
private Mock<IResourceLoader> networkLoader;
private Mock<IDataParser> xmlParser;
private Mock<IDataSerializer> xmlSerializer;
[TestInitialize]
public void Initialize()
{
var executablePath = Assembly.GetExecutingAssembly().Location;
var hashAlgorithm = new Mock<IHashAlgorithm>();
var logger = new Mock<IModuleLogger>();
sut = new ConfigurationRepository(hashAlgorithm.Object, logger.Object, executablePath, string.Empty, string.Empty, string.Empty);
binaryParser = new Mock<IDataParser>();
binarySerializer = new Mock<IDataSerializer>();
certificateStore = new Mock<ICertificateStore>();
fileLoader = new Mock<IResourceLoader>();
fileSaver = new Mock<IResourceSaver>();
hashAlgorithm = new Mock<IHashAlgorithm>();
logger = new Mock<IModuleLogger>();
networkLoader = new Mock<IResourceLoader>();
xmlParser = new Mock<IDataParser>();
xmlSerializer = new Mock<IDataSerializer>();
fileLoader.Setup(f => f.CanLoad(It.IsAny<Uri>())).Returns<Uri>(u => u.IsFile);
fileSaver.Setup(f => f.CanSave(It.IsAny<Uri>())).Returns<Uri>(u => u.IsFile);
networkLoader.Setup(n => n.CanLoad(It.IsAny<Uri>())).Returns<Uri>(u => u.Scheme.Equals("http") || u.Scheme.Equals("seb"));
sut = new ConfigurationRepository(certificateStore.Object, hashAlgorithm.Object, logger.Object, executablePath, string.Empty, string.Empty, string.Empty);
sut.InitializeAppConfig();
}
[TestMethod]
public void MustCorrectlyInitializeSessionConfiguration()
public void ConfigureClient_MustWorkAsExpected()
{
var stream = new MemoryStream() as Stream;
var password = new PasswordParameters { Password = "test123" };
var parseResult = new ParseResult
{
Format = FormatType.Binary,
RawData = new Dictionary<string, object>(),
Status = LoadStatus.Success
};
var serializeResult = new SerializeResult
{
Data = new MemoryStream(),
Status = SaveStatus.Success
};
RegisterModules();
fileLoader.Setup(n => n.TryLoad(It.IsAny<Uri>(), out stream)).Returns(LoadStatus.Success);
binaryParser.Setup(b => b.CanParse(It.IsAny<Stream>())).Returns(true);
binaryParser.Setup(b => b.TryParse(It.IsAny<Stream>(), It.IsAny<PasswordParameters>())).Returns(parseResult);
binarySerializer.Setup(b => b.CanSerialize(FormatType.Binary)).Returns(true);
binarySerializer.Setup(b => b.TrySerialize(It.IsAny<Dictionary<string, object>>(), It.IsAny<PasswordParameters>())).Returns(serializeResult);
fileSaver.Setup(f => f.TrySave(It.IsAny<Uri>(), It.IsAny<Stream>())).Returns(SaveStatus.Success);
var status = sut.ConfigureClientWith(new Uri("C:\\TEMP\\Some\\file.seb"), password);
fileLoader.Verify(n => n.TryLoad(It.IsAny<Uri>(), out stream), Times.Once);
binaryParser.Verify(b => b.TryParse(It.IsAny<Stream>(), It.IsAny<PasswordParameters>()), Times.Once);
certificateStore.Verify(c => c.ExtractAndImportIdentities(It.IsAny<Dictionary<string, object>>()), Times.Once);
binarySerializer.Verify(b => b.TrySerialize(
It.IsAny<Dictionary<string, object>>(),
It.Is<PasswordParameters>(p => p.IsHash == true && p.Password == string.Empty)), Times.Once);
fileSaver.Verify(f => f.TrySave(It.IsAny<Uri>(), It.IsAny<Stream>()), Times.Once);
Assert.AreEqual(SaveStatus.Success, status);
}
[TestMethod]
public void ConfigureClient_MustKeepSameEncryptionAccordingToConfiguration()
{
var stream = new MemoryStream() as Stream;
var password = new PasswordParameters { Password = "test123" };
var parseResult = new ParseResult
{
Encryption = new PublicKeyParameters
{
InnerEncryption = password,
SymmetricEncryption = true
},
Format = FormatType.Binary,
RawData = new Dictionary<string, object> { { Keys.ConfigurationFile.KeepClientConfigEncryption, true } },
Status = LoadStatus.Success
};
var serializeResult = new SerializeResult
{
Data = new MemoryStream(),
Status = SaveStatus.Success
};
RegisterModules();
fileLoader.Setup(n => n.TryLoad(It.IsAny<Uri>(), out stream)).Returns(LoadStatus.Success);
binaryParser.Setup(b => b.CanParse(It.IsAny<Stream>())).Returns(true);
binaryParser.Setup(b => b.TryParse(It.IsAny<Stream>(), It.IsAny<PasswordParameters>())).Returns(parseResult);
binarySerializer.Setup(b => b.CanSerialize(FormatType.Binary)).Returns(true);
binarySerializer.Setup(b => b.TrySerialize(It.IsAny<Dictionary<string, object>>(), It.IsAny<EncryptionParameters>())).Returns(serializeResult);
fileSaver.Setup(f => f.TrySave(It.IsAny<Uri>(), It.IsAny<Stream>())).Returns(SaveStatus.Success);
var status = sut.ConfigureClientWith(new Uri("C:\\TEMP\\Some\\file.seb"), password);
binarySerializer.Verify(b => b.TrySerialize(
It.IsAny<Dictionary<string, object>>(),
It.Is<PublicKeyParameters>(p => p.InnerEncryption == password && p.SymmetricEncryption)), Times.Once);
Assert.AreEqual(SaveStatus.Success, status);
}
[TestMethod]
public void ConfigureClient_MustAbortProcessOnError()
{
var stream = new MemoryStream() as Stream;
var password = new PasswordParameters { Password = "test123" };
var parseResult = new ParseResult
{
Format = FormatType.Binary,
RawData = new Dictionary<string, object>(),
Status = LoadStatus.Success
};
var serializeResult = new SerializeResult
{
Data = new MemoryStream(),
Status = SaveStatus.Success
};
RegisterModules();
fileLoader.Setup(n => n.TryLoad(It.IsAny<Uri>(), out stream)).Returns(LoadStatus.Success);
binaryParser.Setup(b => b.CanParse(It.IsAny<Stream>())).Throws<Exception>();
var status = sut.ConfigureClientWith(new Uri("C:\\TEMP\\Some\\file.seb"), password);
fileLoader.Verify(n => n.TryLoad(It.IsAny<Uri>(), out stream), Times.Once);
binaryParser.Verify(b => b.TryParse(It.IsAny<Stream>(), It.IsAny<PasswordParameters>()), Times.Never);
certificateStore.Verify(c => c.ExtractAndImportIdentities(It.IsAny<Dictionary<string, object>>()), Times.Never);
binarySerializer.Verify(b => b.TrySerialize(It.IsAny<Dictionary<string, object>>(), It.IsAny<EncryptionParameters>()), Times.Never);
fileSaver.Verify(f => f.TrySave(It.IsAny<Uri>(), It.IsAny<Stream>()), Times.Never);
Assert.AreEqual(SaveStatus.UnexpectedError, status);
}
[TestMethod]
public void TryLoad_MustWorkAsExpected()
{
var stream = new MemoryStream() as Stream;
var parseResult = new ParseResult { RawData = new Dictionary<string, object>(), Status = LoadStatus.Success };
RegisterModules();
networkLoader.Setup(n => n.TryLoad(It.IsAny<Uri>(), out stream)).Returns(LoadStatus.Success);
binaryParser.Setup(b => b.CanParse(It.IsAny<Stream>())).Returns(true);
binaryParser.Setup(b => b.TryParse(It.IsAny<Stream>(), It.IsAny<PasswordParameters>())).Returns(parseResult);
var result = sut.TryLoadSettings(new Uri("http://www.blubb.org"), out _);
fileLoader.Verify(f => f.CanLoad(It.IsAny<Uri>()), Times.Once);
fileLoader.Verify(f => f.TryLoad(It.IsAny<Uri>(), out stream), Times.Never);
networkLoader.Verify(n => n.CanLoad(It.IsAny<Uri>()), Times.Once);
networkLoader.Verify(n => n.TryLoad(It.IsAny<Uri>(), out stream), Times.Once);
binaryParser.Verify(b => b.CanParse(It.IsAny<Stream>()), Times.Once);
binaryParser.Verify(b => b.TryParse(It.IsAny<Stream>(), It.IsAny<PasswordParameters>()), Times.Once);
xmlParser.Verify(x => x.CanParse(It.IsAny<Stream>()), Times.AtMostOnce);
xmlParser.Verify(x => x.TryParse(It.IsAny<Stream>(), It.IsAny<PasswordParameters>()), Times.Never);
Assert.AreEqual(LoadStatus.Success, result);
}
[TestMethod]
public void TryLoad_MustReportPasswordNeed()
{
var stream = new MemoryStream() as Stream;
var parseResult = new ParseResult { Status = LoadStatus.PasswordNeeded };
RegisterModules();
networkLoader.Setup(n => n.TryLoad(It.IsAny<Uri>(), out stream)).Returns(LoadStatus.Success);
binaryParser.Setup(b => b.CanParse(It.IsAny<Stream>())).Returns(true);
binaryParser.Setup(b => b.TryParse(It.IsAny<Stream>(), It.IsAny<PasswordParameters>())).Returns(parseResult);
var result = sut.TryLoadSettings(new Uri("http://www.blubb.org"), out _);
Assert.AreEqual(LoadStatus.PasswordNeeded, result);
}
[TestMethod]
public void TryLoad_MustNotFailToIfNoLoaderRegistered()
{
var result = sut.TryLoadSettings(new Uri("http://www.blubb.org"), out _);
Assert.AreEqual(LoadStatus.NotSupported, result);
sut.Register(fileLoader.Object);
sut.Register(networkLoader.Object);
result = sut.TryLoadSettings(new Uri("ftp://www.blubb.org"), out _);
fileLoader.Verify(f => f.CanLoad(It.IsAny<Uri>()), Times.Once);
networkLoader.Verify(n => n.CanLoad(It.IsAny<Uri>()), Times.Once);
Assert.AreEqual(LoadStatus.NotSupported, result);
}
[TestMethod]
public void TryLoad_MustNotFailIfNoParserRegistered()
{
var data = default(Stream);
networkLoader.Setup(l => l.TryLoad(It.IsAny<Uri>(), out data)).Returns(LoadStatus.Success);
sut.Register(networkLoader.Object);
var result = sut.TryLoadSettings(new Uri("http://www.blubb.org"), out _);
networkLoader.Verify(n => n.TryLoad(It.IsAny<Uri>(), out data), Times.Once);
Assert.AreEqual(LoadStatus.NotSupported, result);
}
[TestMethod]
public void TryLoad_MustNotFailInCaseOfUnexpectedError()
{
var data = default(Stream);
networkLoader.Setup(l => l.TryLoad(It.IsAny<Uri>(), out data)).Throws<Exception>();
sut.Register(networkLoader.Object);
var result = sut.TryLoadSettings(new Uri("http://www.blubb.org"), out _);
Assert.AreEqual(LoadStatus.UnexpectedError, result);
binaryParser.Setup(b => b.CanParse(It.IsAny<Stream>())).Throws<Exception>();
networkLoader.Setup(l => l.TryLoad(It.IsAny<Uri>(), out data)).Returns(LoadStatus.Success);
sut.Register(binaryParser.Object);
result = sut.TryLoadSettings(new Uri("http://www.blubb.org"), out _);
Assert.AreEqual(LoadStatus.UnexpectedError, result);
}
[TestMethod]
public void MustInitializeSessionConfiguration()
{
var appConfig = sut.InitializeAppConfig();
var configuration = sut.InitializeSessionConfiguration();
@ -44,7 +282,7 @@ namespace SafeExamBrowser.Configuration.UnitTests
}
[TestMethod]
public void MustCorrectlyUpdateAppConfig()
public void MustUpdateAppConfig()
{
var appConfig = sut.InitializeAppConfig();
var clientAddress = appConfig.ClientAddress;
@ -64,7 +302,7 @@ namespace SafeExamBrowser.Configuration.UnitTests
}
[TestMethod]
public void MustCorrectlyUpdateSessionConfiguration()
public void MustUpdateSessionConfiguration()
{
var appConfig = sut.InitializeAppConfig();
var firstSession = sut.InitializeSessionConfiguration();
@ -76,5 +314,16 @@ namespace SafeExamBrowser.Configuration.UnitTests
Assert.AreNotEqual(secondSession.Id, thirdSession.Id);
Assert.AreNotEqual(secondSession.StartupToken, thirdSession.StartupToken);
}
private void RegisterModules()
{
sut.Register(binaryParser.Object);
sut.Register(binarySerializer.Object);
sut.Register(fileLoader.Object);
sut.Register(fileSaver.Object);
sut.Register(networkLoader.Object);
sut.Register(xmlParser.Object);
sut.Register(xmlSerializer.Object);
}
}
}

View file

@ -1,76 +0,0 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.ConfigurationData
{
internal class CertificateImporter
{
private ILogger logger;
internal CertificateImporter(ILogger logger)
{
this.logger = logger;
}
internal void ExtractAndImportIdentities(IDictionary<string, object> data)
{
const int IDENTITY_CERTIFICATE = 1;
var hasCertificates = data.TryGetValue(Keys.Network.Certificates.EmbeddedCertificates, out var value);
if (hasCertificates && value is IList<IDictionary<string, object>> certificates)
{
var toRemove = new List<IDictionary<string, object>>();
foreach (var certificate in certificates)
{
var hasData = certificate.TryGetValue(Keys.Network.Certificates.CertificateData, out var dataValue);
var hasType = certificate.TryGetValue(Keys.Network.Certificates.CertificateType, out var typeValue);
var isIdentity = typeValue is int type && type == IDENTITY_CERTIFICATE;
if (hasData && hasType && isIdentity && dataValue is byte[] certificateData)
{
ImportIdentityCertificate(certificateData, new X509Store(StoreLocation.CurrentUser));
ImportIdentityCertificate(certificateData, new X509Store(StoreName.TrustedPeople, StoreLocation.LocalMachine));
toRemove.Add(certificate);
}
}
toRemove.ForEach(c => certificates.Remove(c));
}
}
internal void ImportIdentityCertificate(byte[] certificateData, X509Store store)
{
try
{
var certificate = new X509Certificate2();
certificate.Import(certificateData, "Di𝈭l𝈖Ch𝈒ah𝉇t𝈁a𝉈Hai1972", X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet);
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
logger.Info($"Successfully imported identity certificate into {store.Location}.{store.Name}.");
}
catch (Exception e)
{
logger.Error($"Failed to import identity certificate into {store.Location}.{store.Name}!", e);
}
finally
{
store.Close();
}
}
}
}

View file

@ -22,7 +22,7 @@ namespace SafeExamBrowser.Configuration
{
public class ConfigurationRepository : IConfigurationRepository
{
private CertificateImporter certificateImporter;
private ICertificateStore certificateStore;
private IList<IDataParser> dataParsers;
private IList<IDataSerializer> dataSerializers;
private DataMapper dataMapper;
@ -33,6 +33,7 @@ namespace SafeExamBrowser.Configuration
private IList<IResourceSaver> resourceSavers;
public ConfigurationRepository(
ICertificateStore certificateStore,
IHashAlgorithm hashAlgorithm,
IModuleLogger logger,
string executablePath,
@ -40,10 +41,10 @@ namespace SafeExamBrowser.Configuration
string programTitle,
string programVersion)
{
this.certificateStore = certificateStore;
this.hashAlgorithm = hashAlgorithm;
this.logger = logger;
certificateImporter = new CertificateImporter(logger.CloneFor(nameof(CertificateImporter)));
dataParsers = new List<IDataParser>();
dataSerializers = new List<IDataSerializer>();
dataMapper = new DataMapper();
@ -64,7 +65,7 @@ namespace SafeExamBrowser.Configuration
{
TryParseData(data, out var encryption, out var format, out var rawData, password);
certificateImporter.ExtractAndImportIdentities(rawData);
certificateStore.ExtractAndImportIdentities(rawData);
encryption = DetermineEncryptionForClientConfiguration(rawData, encryption);
var status = TrySerializeData(rawData, format, out var serialized, encryption);

View file

@ -6,15 +6,21 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using SafeExamBrowser.Configuration.ConfigurationData;
using SafeExamBrowser.Contracts.Configuration.Cryptography;
using SafeExamBrowser.Contracts.Logging;
namespace SafeExamBrowser.Configuration.Cryptography
{
public class CertificateStore : ICertificateStore
{
private ILogger logger;
private readonly X509Store[] stores = new[]
{
new X509Store(StoreLocation.CurrentUser),
@ -22,6 +28,11 @@ namespace SafeExamBrowser.Configuration.Cryptography
new X509Store(StoreName.TrustedPeople)
};
public CertificateStore(ILogger logger)
{
this.logger = logger;
}
public bool TryGetCertificateWith(byte[] keyHash, out X509Certificate2 certificate)
{
certificate = default(X509Certificate2);
@ -56,5 +67,56 @@ namespace SafeExamBrowser.Configuration.Cryptography
return false;
}
public void ExtractAndImportIdentities(IDictionary<string, object> data)
{
const int IDENTITY_CERTIFICATE = 1;
var hasCertificates = data.TryGetValue(Keys.Network.Certificates.EmbeddedCertificates, out var value);
if (hasCertificates && value is IList<IDictionary<string, object>> certificates)
{
var toRemove = new List<IDictionary<string, object>>();
foreach (var certificate in certificates)
{
var hasData = certificate.TryGetValue(Keys.Network.Certificates.CertificateData, out var dataValue);
var hasType = certificate.TryGetValue(Keys.Network.Certificates.CertificateType, out var typeValue);
var isIdentity = typeValue is int type && type == IDENTITY_CERTIFICATE;
if (hasData && hasType && isIdentity && dataValue is byte[] certificateData)
{
ImportIdentityCertificate(certificateData, new X509Store(StoreLocation.CurrentUser));
ImportIdentityCertificate(certificateData, new X509Store(StoreName.TrustedPeople, StoreLocation.LocalMachine));
toRemove.Add(certificate);
}
}
toRemove.ForEach(c => certificates.Remove(c));
}
}
private void ImportIdentityCertificate(byte[] certificateData, X509Store store)
{
try
{
var certificate = new X509Certificate2();
certificate.Import(certificateData, "Di𝈭l𝈖Ch𝈒ah𝉇t𝈁a𝉈Hai1972", X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.PersistKeySet);
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
logger.Info($"Successfully imported identity certificate into {store.Location}.{store.Name}.");
}
catch (Exception e)
{
logger.Error($"Failed to import identity certificate into {store.Location}.{store.Name}!", e);
}
finally
{
store.Close();
}
}
}
}

View file

@ -58,7 +58,6 @@
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="ConfigurationData\CertificateImporter.cs" />
<Compile Include="ConfigurationData\Keys.cs" />
<Compile Include="ConfigurationData\DataValues.cs" />
<Compile Include="Cryptography\CertificateStore.cs" />

View file

@ -6,12 +6,13 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
namespace SafeExamBrowser.Contracts.Configuration.Cryptography
{
/// <summary>
/// Provides functionality to load certificates installed on the computer.
/// Provides functionality related to certificates installed on the computer.
/// </summary>
public interface ICertificateStore
{
@ -20,5 +21,10 @@ namespace SafeExamBrowser.Contracts.Configuration.Cryptography
/// Returns <c>true</c> if the certificate was found, otherwise <c>false</c>.
/// </summary>
bool TryGetCertificateWith(byte[] keyHash, out X509Certificate2 certificate);
/// <summary>
/// Extracts all identity certificates from the given configuration data and installs them on the computer.
/// </summary>
void ExtractAndImportIdentities(IDictionary<string, object> data);
}
}

View file

@ -116,15 +116,17 @@ namespace SafeExamBrowser.Runtime
var programCopyright = executable.GetCustomAttribute<AssemblyCopyrightAttribute>().Copyright;
var programTitle = executable.GetCustomAttribute<AssemblyTitleAttribute>().Title;
var programVersion = executable.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
var certificateStore = new CertificateStore(ModuleLogger(nameof(CertificateStore)));
var compressor = new GZipCompressor(ModuleLogger(nameof(GZipCompressor)));
var passwordEncryption = new PasswordEncryption(ModuleLogger(nameof(PasswordEncryption)));
var publicKeyEncryption = new PublicKeyEncryption(new CertificateStore(), ModuleLogger(nameof(PublicKeyEncryption)));
var symmetricEncryption = new PublicKeySymmetricEncryption(new CertificateStore(), ModuleLogger(nameof(PublicKeySymmetricEncryption)), passwordEncryption);
var publicKeyEncryption = new PublicKeyEncryption(certificateStore, ModuleLogger(nameof(PublicKeyEncryption)));
var symmetricEncryption = new PublicKeySymmetricEncryption(certificateStore, ModuleLogger(nameof(PublicKeySymmetricEncryption)), passwordEncryption);
var repositoryLogger = ModuleLogger(nameof(ConfigurationRepository));
var xmlParser = new XmlParser(ModuleLogger(nameof(XmlParser)));
var xmlSerializer = new XmlSerializer(ModuleLogger(nameof(XmlSerializer)));
configuration = new ConfigurationRepository(new HashAlgorithm(), repositoryLogger, executable.Location, programCopyright, programTitle, programVersion);
configuration = new ConfigurationRepository(certificateStore, new HashAlgorithm(), repositoryLogger, executable.Location, programCopyright, programTitle, programVersion);
appConfig = configuration.InitializeAppConfig();
configuration.Register(new BinaryParser(compressor, new HashAlgorithm(), ModuleLogger(nameof(BinaryParser)), passwordEncryption, publicKeyEncryption, symmetricEncryption, xmlParser));