SEBWIN-220: Finished draft of (re-)configuration mechanism.

This commit is contained in:
dbuechel 2018-06-27 14:02:16 +02:00
parent 639bde7860
commit eb47cb362b
30 changed files with 529 additions and 101 deletions

View file

@ -11,7 +11,7 @@ using System;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// The response to be used to reply to an authentication request (see <see cref="Messages.SimpleMessagePurport.Authenticate"/>).
/// The response to be used to reply to an authentication request (see <see cref="SimpleMessagePurport.Authenticate"/>).
/// </summary>
[Serializable]
public class AuthenticationResponse : Response

View file

@ -12,7 +12,7 @@ using SafeExamBrowser.Contracts.Configuration;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// The response to be used to reply to a configuration request (see <see cref="Messages.SimpleMessagePurport.ConfigurationNeeded"/>).
/// The response to be used to reply to a configuration request (see <see cref="SimpleMessagePurport.ConfigurationNeeded"/>).
/// </summary>
[Serializable]
public class ConfigurationResponse : Response

View file

@ -11,7 +11,7 @@ using System;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// The response transmitted to a <see cref="Messages.DisconnectionMessage"/>
/// The response transmitted to a <see cref="DisconnectionMessage"/>
/// </summary>
[Serializable]
public class DisconnectionResponse : Response

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2018 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;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// The reply to a <see cref="PasswordRequestMessage"/>.
/// </summary>
[Serializable]
public class PasswordReplyMessage : Message
{
/// <summary>
/// The password entered by the user, or <c>null</c> if the user interaction was unsuccessful.
/// </summary>
public string Password { get; private set; }
/// <summary>
/// The unique identifier for the password request.
/// </summary>
public Guid RequestId { get; private set; }
/// <summary>
/// Determines whether the user interaction was successful or not.
/// </summary>
public bool Success { get; private set; }
public PasswordReplyMessage(string password, Guid requestId, bool success)
{
Password = password;
RequestId = requestId;
Success = success;
}
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2018 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;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// This message is transmitted to the client to request a password input by the user.
/// </summary>
[Serializable]
public class PasswordRequestMessage : Message
{
/// <summary>
/// The purpose of the password request.
/// </summary>
public PasswordRequestPurpose Purpose { get; private set; }
/// <summary>
/// The unique identifier for the password request.
/// </summary>
public Guid RequestId { get; private set; }
public PasswordRequestMessage(PasswordRequestPurpose purpose, Guid requestId)
{
Purpose = purpose;
RequestId = requestId;
}
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2018 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/.
*/
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// Defines all possible reasons for a <see cref="PasswordRequestMessage"/>.
/// </summary>
public enum PasswordRequestPurpose
{
Undefined = 0,
/// <summary>
/// The password is to be used as administrator password for an application configuration.
/// </summary>
Administrator,
/// <summary>
/// The password is to be used as settings password for an application configuration.
/// </summary>
Settings
}
}

View file

@ -11,7 +11,7 @@ using System;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// The response to a <see cref="Messages.ReconfigurationMessage"/>.
/// The response to a <see cref="ReconfigurationMessage"/>.
/// </summary>
[Serializable]
public class ReconfigurationResponse : Response

View file

@ -11,7 +11,7 @@ using System;
namespace SafeExamBrowser.Contracts.Communication.Data
{
/// <summary>
/// The base class for respones, from which a response must inherit in order to be sent to an interlocutor as reply to <see cref="ICommunication.Send(Messages.Message)"/>.
/// The base class for respones, from which a response must inherit in order to be sent to an interlocutor as reply to <see cref="ICommunication.Send(Message)"/>.
/// </summary>
[Serializable]
public abstract class Response

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2018 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;
namespace SafeExamBrowser.Contracts.Communication.Events
{
/// <summary>
/// The event arguments used for the password input event fired by the <see cref="Hosts.IRuntimeHost"/>.
/// </summary>
public class PasswordEventArgs : CommunicationEventArgs
{
/// <summary>
/// The password entered by the user, or <c>null</c> if not available.
/// </summary>
public string Password { get; set; }
/// <summary>
/// Identifies the password request.
/// </summary>
public Guid RequestId { get; set; }
/// <summary>
/// Indicates whether the password has been successfully entered by the user.
/// </summary>
public bool Success { get; set; }
}
}

View file

@ -31,6 +31,11 @@ namespace SafeExamBrowser.Contracts.Communication.Hosts
/// </summary>
event CommunicationEventHandler ClientReady;
/// <summary>
/// Event fired when the client transmitted a password entered by the user.
/// </summary>
event CommunicationEventHandler<PasswordEventArgs> PasswordReceived;
/// <summary>
/// Event fired when the client requested a reconfiguration of the application.
/// </summary>

View file

@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using SafeExamBrowser.Contracts.Communication.Data;
namespace SafeExamBrowser.Contracts.Communication.Proxies
@ -26,5 +27,11 @@ namespace SafeExamBrowser.Contracts.Communication.Proxies
/// </summary>
/// <exception cref="System.ServiceModel.*">If the communication failed.</exception>
AuthenticationResponse RequestAuthentication();
/// <summary>
/// Requests the client to render a password dialog and subsequently return the interaction result as separate message.
/// </summary>
/// <exception cref="System.ServiceModel.*">If the communication failed.</exception>
void RequestPassword(PasswordRequestPurpose purpose, Guid requestId);
}
}

View file

@ -51,7 +51,7 @@ namespace SafeExamBrowser.Contracts.Configuration
/// Attempts to load settings from the specified resource, using the optional passwords. Returns a <see cref="LoadStatus"/>
/// indicating the result of the operation.
/// </summary>
LoadStatus LoadSettings(Uri resource, string settingsPassword = null, string adminPassword = null);
LoadStatus LoadSettings(Uri resource, string adminPassword = null, string settingsPassword = null);
/// <summary>
/// Loads the default settings.

View file

@ -44,6 +44,10 @@ namespace SafeExamBrowser.Contracts.I18n
MessageBox_StartupErrorTitle,
Notification_AboutTooltip,
Notification_LogTooltip,
PasswordDialog_AdminPasswordRequired,
PasswordDialog_AdminPasswordRequiredTitle,
PasswordDialog_SettingsPasswordRequired,
PasswordDialog_SettingsPasswordRequiredTitle,
ProgressIndicator_CloseRuntimeConnection,
ProgressIndicator_EmptyClipboard,
ProgressIndicator_FinalizeServiceSession,

View file

@ -60,8 +60,12 @@
<Compile Include="Browser\DownloadFinishedCallback.cs" />
<Compile Include="Browser\DownloadRequestedEventHandler.cs" />
<Compile Include="Browser\IBrowserApplicationController.cs" />
<Compile Include="Communication\Data\PasswordRequestMessage.cs" />
<Compile Include="Communication\Data\PasswordRequestPurpose.cs" />
<Compile Include="Communication\Data\PasswordReplyMessage.cs" />
<Compile Include="Communication\Events\CommunicationEventArgs.cs" />
<Compile Include="Communication\Events\CommunicationEventHandler.cs" />
<Compile Include="Communication\Events\PasswordEventArgs.cs" />
<Compile Include="Communication\Events\ReconfigurationEventArgs.cs" />
<Compile Include="Communication\Hosts\IClientHost.cs" />
<Compile Include="Communication\Hosts\IHostObject.cs" />
@ -139,6 +143,8 @@
<Compile Include="UserInterface\MessageBox\IMessageBox.cs" />
<Compile Include="UserInterface\IProgressIndicator.cs" />
<Compile Include="UserInterface\Taskbar\QuitButtonClickedEventHandler.cs" />
<Compile Include="UserInterface\Windows\IPasswordDialog.cs" />
<Compile Include="UserInterface\Windows\IPasswordDialogResult.cs" />
<Compile Include="UserInterface\Windows\IRuntimeWindow.cs" />
<Compile Include="UserInterface\MessageBox\MessageBoxResult.cs" />
<Compile Include="UserInterface\Taskbar\INotificationButton.cs" />

View file

@ -37,6 +37,11 @@ namespace SafeExamBrowser.Contracts.UserInterface
/// </summary>
IBrowserWindow CreateBrowserWindow(IBrowserControl control, BrowserSettings settings);
/// <summary>
/// Creates a system control which allows to change the keyboard layout of the computer.
/// </summary>
ISystemKeyboardLayoutControl CreateKeyboardLayoutControl();
/// <summary>
/// Creates a new log window which runs on its own thread.
/// </summary>
@ -48,9 +53,9 @@ namespace SafeExamBrowser.Contracts.UserInterface
INotificationButton CreateNotification(INotificationInfo info);
/// <summary>
/// Creates a system control which allows to change the keyboard layout of the computer.
/// Creates a password dialog with the given message and title.
/// </summary>
ISystemKeyboardLayoutControl CreateKeyboardLayoutControl();
IPasswordDialog CreatePasswordDialog(string message, string title);
/// <summary>
/// Creates a system control displaying the power supply status of the computer.

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2018 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/.
*/
namespace SafeExamBrowser.Contracts.UserInterface.Windows
{
/// <summary>
/// Defines the functionality of a password dialog.
/// </summary>
public interface IPasswordDialog : IWindow
{
/// <summary>
/// Shows the dialog as topmost window. If a parent window is specified, the dialog is rendered modally for the given parent.
/// </summary>
IPasswordDialogResult Show(IWindow parent = null);
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2018 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/.
*/
namespace SafeExamBrowser.Contracts.UserInterface.Windows
{
/// <summary>
/// Defines the user interaction result of an <see cref="IPasswordDialog"/>.
/// </summary>
public interface IPasswordDialogResult
{
/// <summary>
/// The password entered by the user, or <c>null</c> if the interaction was unsuccessful.
/// </summary>
string Password { get; }
/// <summary>
/// Indicates whether the user confirmed the dialog or not.
/// </summary>
bool Success { get; }
}
}

View file

@ -14,14 +14,14 @@ using SafeExamBrowser.Core.Communication.Hosts;
namespace SafeExamBrowser.Core.UnitTests.Communication.Hosts
{
internal class BaseHostImpl : BaseHost
internal class BaseHostStub : BaseHost
{
public Func<Guid?, bool> OnConnectStub { get; set; }
public Action OnDisconnectStub { get; set; }
public Func<Message, Response> OnReceiveStub { get; set; }
public Func<SimpleMessagePurport, Response> OnReceiveSimpleMessageStub { get; set; }
public BaseHostImpl(string address, IHostObjectFactory factory, ILogger logger) : base(address, factory, logger)
public BaseHostStub(string address, IHostObjectFactory factory, ILogger logger) : base(address, factory, logger)
{
}

View file

@ -24,7 +24,7 @@ namespace SafeExamBrowser.Core.UnitTests.Communication.Hosts
private Mock<IHostObject> hostObject;
private Mock<IHostObjectFactory> hostObjectFactory;
private Mock<ILogger> logger;
private BaseHostImpl sut;
private BaseHostStub sut;
[TestInitialize]
public void Initialize()
@ -35,7 +35,7 @@ namespace SafeExamBrowser.Core.UnitTests.Communication.Hosts
hostObjectFactory.Setup(f => f.CreateObject(It.IsAny<string>(), It.IsAny<ICommunication>())).Returns(hostObject.Object);
sut = new BaseHostImpl("net.pipe://some/address/here", hostObjectFactory.Object, logger.Object);
sut = new BaseHostStub("net.pipe://some/address/here", hostObjectFactory.Object, logger.Object);
}
[TestMethod]

View file

@ -83,7 +83,7 @@
<Compile Include="Behaviour\OperationModel\I18nOperationTests.cs" />
<Compile Include="Behaviour\OperationModel\DelegateOperationTests.cs" />
<Compile Include="Behaviour\OperationModel\OperationSequenceTests.cs" />
<Compile Include="Communication\Hosts\BaseHostImpl.cs" />
<Compile Include="Communication\Hosts\BaseHostStub.cs" />
<Compile Include="Communication\Hosts\BaseHostTests.cs" />
<Compile Include="Communication\Proxies\BaseProxyImpl.cs" />
<Compile Include="Communication\Proxies\BaseProxyTests.cs" />

View file

@ -6,6 +6,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using System;
using System.ServiceModel;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Proxies;
@ -43,5 +44,11 @@ namespace SafeExamBrowser.Core.Communication.Proxies
throw new CommunicationException($"Did not receive authentication response! Received: {ToString(response)}.");
}
public void RequestPassword(PasswordRequestPurpose purpose, Guid requestId)
{
// TODO
throw new NotImplementedException();
}
}
}

View file

@ -84,6 +84,18 @@
<Entry key="Notification_LogTooltip">
Application Log
</Entry>
<Entry key="PasswordDialog_AdminPasswordRequired">
Please enter the administrator password for the application configuration:
</Entry>
<Entry key="PasswordDialog_AdminPasswordRequiredTitle">
Administrator Password Required
</Entry>
<Entry key="PasswordDialog_SettingsPasswordRequired">
Please enter the settings password for the application configuration:
</Entry>
<Entry key="PasswordDialog_SettingsPasswordRequiredTitle">
Settings Password Required
</Entry>
<Entry key="ProgressIndicator_CloseRuntimeConnection">
Closing runtime connection
</Entry>

View file

@ -11,11 +11,14 @@ using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SafeExamBrowser.Contracts.Behaviour.OperationModel;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.I18n;
using SafeExamBrowser.Contracts.Logging;
using SafeExamBrowser.Contracts.UserInterface;
using SafeExamBrowser.Contracts.UserInterface.MessageBox;
using SafeExamBrowser.Contracts.UserInterface.Windows;
using SafeExamBrowser.Runtime.Behaviour.Operations;
namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
@ -23,12 +26,15 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
[TestClass]
public class ConfigurationOperationTests
{
private RuntimeInfo info;
private Mock<ILogger> logger;
private Mock<IMessageBox> messageBox;
private RuntimeInfo info;
private Mock<IPasswordDialog> passwordDialog;
private Mock<IConfigurationRepository> repository;
private Mock<IRuntimeHost> runtimeHost;
private Settings settings;
private Mock<IText> text;
private Mock<IUserInterfaceFactory> uiFactory;
private ConfigurationOperation sut;
[TestInitialize]
@ -37,42 +43,24 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
info = new RuntimeInfo();
logger = new Mock<ILogger>();
messageBox = new Mock<IMessageBox>();
passwordDialog = new Mock<IPasswordDialog>();
repository = new Mock<IConfigurationRepository>();
runtimeHost = new Mock<IRuntimeHost>();
settings = new Settings();
text = new Mock<IText>();
uiFactory = new Mock<IUserInterfaceFactory>();
info.AppDataFolder = @"C:\Not\Really\AppData";
info.DefaultSettingsFileName = "SettingsDummy.txt";
info.ProgramDataFolder = @"C:\Not\Really\ProgramData";
}
[TestMethod]
public void MustNotFailWithoutCommandLineArgs()
{
repository.Setup(r => r.LoadDefaultSettings());
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, null);
sut.Perform();
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, new string[] { });
sut.Perform();
repository.Verify(r => r.LoadDefaultSettings(), Times.Exactly(2));
}
[TestMethod]
public void MustNotFailWithInvalidUri()
{
var path = @"an/invalid\path.'*%yolo/()";
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, new[] { "blubb.exe", path });
sut.Perform();
uiFactory.Setup(f => f.CreatePasswordDialog(It.IsAny<string>(), It.IsAny<string>())).Returns(passwordDialog.Object);
}
[TestMethod]
public void MustUseCommandLineArgumentAs1stPrio()
{
var path = @"http://www.safeexambrowser.org/whatever.seb";
var url = @"http://www.safeexambrowser.org/whatever.seb";
var location = Path.GetDirectoryName(GetType().Assembly.Location);
info.ProgramDataFolder = location;
@ -81,10 +69,10 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, new[] { "blubb.exe", path });
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, new[] { "blubb.exe", url });
sut.Perform();
var resource = new Uri(path);
var resource = new Uri(url);
repository.Verify(r => r.LoadSettings(It.Is<Uri>(u => u.Equals(resource)), null, null), Times.Once);
}
@ -100,7 +88,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, null);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, null);
sut.Perform();
var resource = new Uri(Path.Combine(location, "SettingsDummy.txt"));
@ -118,7 +106,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, null);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, null);
sut.Perform();
var resource = new Uri(Path.Combine(location, "SettingsDummy.txt"));
@ -129,7 +117,7 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
[TestMethod]
public void MustFallbackToDefaultsAsLastPrio()
{
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, null);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, null);
sut.Perform();
repository.Verify(r => r.LoadDefaultSettings(), Times.Once);
@ -139,11 +127,11 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
public void MustAbortIfWishedByUser()
{
info.ProgramDataFolder = Path.GetDirectoryName(GetType().Assembly.Location);
messageBox.Setup(u => u.Show(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MessageBoxAction>(), It.IsAny<MessageBoxIcon>())).Returns(MessageBoxResult.Yes);
messageBox.Setup(m => m.Show(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MessageBoxAction>(), It.IsAny<MessageBoxIcon>())).Returns(MessageBoxResult.Yes);
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, null);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, null);
var result = sut.Perform();
@ -153,15 +141,121 @@ namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
[TestMethod]
public void MustNotAbortIfNotWishedByUser()
{
messageBox.Setup(u => u.Show(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MessageBoxAction>(), It.IsAny<MessageBoxIcon>())).Returns(MessageBoxResult.No);
messageBox.Setup(m => m.Show(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MessageBoxAction>(), It.IsAny<MessageBoxIcon>())).Returns(MessageBoxResult.No);
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, info, text.Object, null);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, null);
var result = sut.Perform();
Assert.AreEqual(OperationResult.Success, result);
}
[TestMethod]
public void MustNotAllowToAbortIfNotInConfigureClientMode()
{
settings.ConfigurationMode = ConfigurationMode.Exam;
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, null);
sut.Perform();
messageBox.Verify(m => m.Show(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<MessageBoxAction>(), It.IsAny<MessageBoxIcon>()), Times.Never);
}
[TestMethod]
public void MustNotFailWithoutCommandLineArgs()
{
repository.Setup(r => r.LoadDefaultSettings());
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, null);
sut.Perform();
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, new string[] { });
sut.Perform();
repository.Verify(r => r.LoadDefaultSettings(), Times.Exactly(2));
}
[TestMethod]
public void MustNotFailWithInvalidUri()
{
var uri = @"an/invalid\uri.'*%yolo/()你好";
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, new[] { "blubb.exe", uri });
sut.Perform();
}
[TestMethod]
public void MustOnlyAllowToEnterAdminPasswordFiveTimes()
{
var result = new PasswordDialogResultStub { Success = true };
var url = @"http://www.safeexambrowser.org/whatever.seb";
passwordDialog.Setup(d => d.Show(null)).Returns(result);
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.AdminPasswordNeeded);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, new[] { "blubb.exe", url });
sut.Perform();
repository.Verify(r => r.LoadSettings(It.IsAny<Uri>(), null, null), Times.Exactly(5));
}
[TestMethod]
public void MustOnlyAllowToEnterSettingsPasswordFiveTimes()
{
var result = new PasswordDialogResultStub { Success = true };
var url = @"http://www.safeexambrowser.org/whatever.seb";
passwordDialog.Setup(d => d.Show(null)).Returns(result);
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.SettingsPasswordNeeded);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, new[] { "blubb.exe", url });
sut.Perform();
repository.Verify(r => r.LoadSettings(It.IsAny<Uri>(), null, null), Times.Exactly(5));
}
[TestMethod]
public void MustSucceedIfAdminPasswordCorrect()
{
var password = "test";
var result = new PasswordDialogResultStub { Password = password, Success = true };
var url = @"http://www.safeexambrowser.org/whatever.seb";
passwordDialog.Setup(d => d.Show(null)).Returns(result);
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.AdminPasswordNeeded);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), password, null)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, new[] { "blubb.exe", url });
sut.Perform();
repository.Verify(r => r.LoadSettings(It.IsAny<Uri>(), null, null), Times.Exactly(1));
repository.Verify(r => r.LoadSettings(It.IsAny<Uri>(), password, null), Times.Exactly(1));
}
[TestMethod]
public void MustSucceedIfSettingsPasswordCorrect()
{
var password = "test";
var result = new PasswordDialogResultStub { Password = password, Success = true };
var url = @"http://www.safeexambrowser.org/whatever.seb";
passwordDialog.Setup(d => d.Show(null)).Returns(result);
repository.SetupGet(r => r.CurrentSettings).Returns(settings);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, null)).Returns(LoadStatus.SettingsPasswordNeeded);
repository.Setup(r => r.LoadSettings(It.IsAny<Uri>(), null, password)).Returns(LoadStatus.Success);
sut = new ConfigurationOperation(repository.Object, logger.Object, messageBox.Object, runtimeHost.Object, info, text.Object, uiFactory.Object, new[] { "blubb.exe", url });
sut.Perform();
repository.Verify(r => r.LoadSettings(It.IsAny<Uri>(), null, null), Times.Exactly(1));
repository.Verify(r => r.LoadSettings(It.IsAny<Uri>(), null, password), Times.Exactly(1));
}
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright (c) 2018 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 SafeExamBrowser.Contracts.UserInterface.Windows;
namespace SafeExamBrowser.Runtime.UnitTests.Behaviour.Operations
{
internal class PasswordDialogResultStub : IPasswordDialogResult
{
public string Password { get; set; }
public bool Success { get; set; }
}
}

View file

@ -81,6 +81,7 @@
<ItemGroup>
<Compile Include="Behaviour\Operations\ConfigurationOperationTests.cs" />
<Compile Include="Behaviour\Operations\KioskModeOperationTests.cs" />
<Compile Include="Behaviour\Operations\PasswordDialogResultStub.cs" />
<Compile Include="Behaviour\Operations\ServiceOperationTests.cs" />
<Compile Include="Behaviour\Operations\ClientOperationTests.cs" />
<Compile Include="Behaviour\Operations\ClientTerminationOperationTests.cs" />

View file

@ -8,7 +8,11 @@
using System;
using System.IO;
using System.Threading;
using SafeExamBrowser.Contracts.Behaviour.OperationModel;
using SafeExamBrowser.Contracts.Communication.Data;
using SafeExamBrowser.Contracts.Communication.Events;
using SafeExamBrowser.Contracts.Communication.Hosts;
using SafeExamBrowser.Contracts.Configuration;
using SafeExamBrowser.Contracts.Configuration.Settings;
using SafeExamBrowser.Contracts.I18n;
@ -23,8 +27,10 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
private IConfigurationRepository repository;
private ILogger logger;
private IMessageBox messageBox;
private IText text;
private IRuntimeHost runtimeHost;
private RuntimeInfo runtimeInfo;
private IText text;
private IUserInterfaceFactory uiFactory;
private string[] commandLineArgs;
public IProgressIndicator ProgressIndicator { private get; set; }
@ -33,16 +39,20 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
IConfigurationRepository repository,
ILogger logger,
IMessageBox messageBox,
IRuntimeHost runtimeHost,
RuntimeInfo runtimeInfo,
IText text,
IUserInterfaceFactory uiFactory,
string[] commandLineArgs)
{
this.repository = repository;
this.logger = logger;
this.messageBox = messageBox;
this.commandLineArgs = commandLineArgs;
this.runtimeHost = runtimeHost;
this.runtimeInfo = runtimeInfo;
this.text = text;
this.uiFactory = uiFactory;
}
public OperationResult Perform()
@ -54,22 +64,11 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
if (isValidUri)
{
logger.Info($"Loading settings from '{uri.AbsolutePath}'...");
logger.Info($"Attempting to load settings from '{uri.AbsolutePath}'...");
var result = LoadSettings(uri);
if (result == OperationResult.Success && repository.CurrentSettings.ConfigurationMode == ConfigurationMode.ConfigureClient)
{
var abort = IsConfigurationSufficient();
logger.Info($"The user chose to {(abort ? "abort" : "continue")} after successful client configuration.");
if (abort)
{
return OperationResult.Aborted;
}
}
HandleClientConfiguration(ref result);
LogOperationResult(result);
return result;
@ -90,7 +89,7 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
if (isValidUri)
{
logger.Info($"Loading settings from '{uri.AbsolutePath}'...");
logger.Info($"Attempting to load settings from '{uri.AbsolutePath}'...");
var result = LoadSettings(uri);
@ -117,19 +116,19 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
for (int adminAttempts = 0, settingsAttempts = 0; adminAttempts < 5 && settingsAttempts < 5;)
{
status = repository.LoadSettings(uri, settingsPassword, adminPassword);
status = repository.LoadSettings(uri, adminPassword, settingsPassword);
if (status == LoadStatus.AdminPasswordNeeded || status == LoadStatus.SettingsPasswordNeeded)
{
var isAdmin = status == LoadStatus.AdminPasswordNeeded;
var success = isAdmin ? TryGetAdminPassword(out adminPassword) : TryGetSettingsPassword(out settingsPassword);
var purpose = status == LoadStatus.AdminPasswordNeeded ? PasswordRequestPurpose.Administrator : PasswordRequestPurpose.Settings;
var aborted = !TryGetPassword(purpose, out string password);
if (success)
{
adminAttempts += isAdmin ? 1 : 0;
settingsAttempts += isAdmin ? 0 : 1;
}
else
adminAttempts += purpose == PasswordRequestPurpose.Administrator ? 1 : 0;
adminPassword = purpose == PasswordRequestPurpose.Administrator ? password : adminPassword;
settingsAttempts += purpose == PasswordRequestPurpose.Settings ? 1 : 0;
settingsPassword = purpose == PasswordRequestPurpose.Settings ? password : settingsPassword;
if (aborted)
{
return OperationResult.Aborted;
}
@ -142,45 +141,101 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
if (status == LoadStatus.InvalidData)
{
if (IsHtmlPage(uri))
{
repository.LoadDefaultSettings();
repository.CurrentSettings.Browser.StartUrl = uri.AbsoluteUri;
logger.Info($"The specified URI '{uri.AbsoluteUri}' appears to point to a HTML page, setting it as startup URL.");
return OperationResult.Success;
}
logger.Error($"The specified settings resource '{uri.AbsoluteUri}' is invalid!");
HandleInvalidData(ref status, uri);
}
return status == LoadStatus.Success ? OperationResult.Success : OperationResult.Failed;
}
private bool TryGetPassword(PasswordRequestPurpose purpose, out string password)
{
var isStartup = repository.CurrentSession == null;
var isRunningOnDefaultDesktop = repository.CurrentSettings?.KioskMode == KioskMode.DisableExplorerShell;
if (isStartup || isRunningOnDefaultDesktop)
{
return TryGetPasswordViaDialog(purpose, out password);
}
else
{
return TryGetPasswordViaClient(purpose, out password);
}
}
private bool TryGetPasswordViaDialog(PasswordRequestPurpose purpose, out string password)
{
var isAdmin = purpose == PasswordRequestPurpose.Administrator;
var message = isAdmin ? TextKey.PasswordDialog_AdminPasswordRequired : TextKey.PasswordDialog_SettingsPasswordRequired;
var title = isAdmin ? TextKey.PasswordDialog_AdminPasswordRequiredTitle : TextKey.PasswordDialog_SettingsPasswordRequiredTitle;
var dialog = uiFactory.CreatePasswordDialog(text.Get(message), text.Get(title));
var result = dialog.Show();
if (result.Success)
{
password = result.Password;
}
else
{
password = default(string);
}
return result.Success;
}
private bool TryGetPasswordViaClient(PasswordRequestPurpose purpose, out string password)
{
var requestId = Guid.NewGuid();
var response = default(PasswordEventArgs);
var responseEvent = new AutoResetEvent(false);
var responseEventHandler = new CommunicationEventHandler<PasswordEventArgs>((args) =>
{
if (args.RequestId == requestId)
{
response = args;
responseEvent.Set();
}
});
runtimeHost.PasswordReceived += responseEventHandler;
repository.CurrentSession.ClientProxy.RequestPassword(purpose, requestId);
responseEvent.WaitOne();
runtimeHost.PasswordReceived -= responseEventHandler;
if (response.Success)
{
password = response.Password;
}
else
{
password = default(string);
}
return response.Success;
}
private void HandleInvalidData(ref LoadStatus status, Uri uri)
{
if (IsHtmlPage(uri))
{
repository.LoadDefaultSettings();
repository.CurrentSettings.Browser.StartUrl = uri.AbsoluteUri;
logger.Info($"The specified URI '{uri.AbsoluteUri}' appears to point to a HTML page, setting it as startup URL.");
status = LoadStatus.Success;
}
else
{
logger.Error($"The specified settings resource '{uri.AbsoluteUri}' is invalid!");
}
}
private bool IsHtmlPage(Uri uri)
{
// TODO
return false;
}
private bool TryGetAdminPassword(out string password)
{
password = default(string);
// TODO
return true;
}
private bool TryGetSettingsPassword(out string password)
{
password = default(string);
// TODO
return true;
}
private bool TryInitializeSettingsUri(out Uri uri)
{
var path = string.Empty;
@ -224,6 +279,21 @@ namespace SafeExamBrowser.Runtime.Behaviour.Operations
return isValidUri;
}
private void HandleClientConfiguration(ref OperationResult result)
{
if (result == OperationResult.Success && repository.CurrentSettings.ConfigurationMode == ConfigurationMode.ConfigureClient)
{
var abort = IsConfigurationSufficient();
logger.Info($"The user chose to {(abort ? "abort" : "continue")} after successful client configuration.");
if (abort)
{
result = OperationResult.Aborted;
}
}
}
private bool IsConfigurationSufficient()
{
var message = text.Get(TextKey.MessageBox_ClientConfigurationQuestion);

View file

@ -25,6 +25,7 @@ namespace SafeExamBrowser.Runtime.Communication
public event CommunicationEventHandler ClientDisconnected;
public event CommunicationEventHandler ClientReady;
public event CommunicationEventHandler<PasswordEventArgs> PasswordReceived;
public event CommunicationEventHandler<ReconfigurationEventArgs> ReconfigurationRequested;
public event CommunicationEventHandler ShutdownRequested;
@ -62,6 +63,9 @@ namespace SafeExamBrowser.Runtime.Communication
{
switch (message)
{
case PasswordReplyMessage r:
PasswordReceived?.InvokeAsync(new PasswordEventArgs { Password = r.Password, RequestId = r.RequestId, Success = r.Success });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);
case ReconfigurationMessage r:
ReconfigurationRequested?.InvokeAsync(new ReconfigurationEventArgs { ConfigurationPath = r.ConfigurationPath });
return new SimpleResponse(SimpleResponsePurport.Acknowledged);

View file

@ -63,7 +63,7 @@ namespace SafeExamBrowser.Runtime
bootstrapOperations.Enqueue(new I18nOperation(logger, text));
bootstrapOperations.Enqueue(new CommunicationOperation(runtimeHost, logger));
sessionOperations.Enqueue(new ConfigurationOperation(configuration, logger, messageBox, runtimeInfo, text, args));
sessionOperations.Enqueue(new ConfigurationOperation(configuration, logger, messageBox, runtimeHost, runtimeInfo, text, uiFactory, args));
sessionOperations.Enqueue(new SessionInitializationOperation(configuration, logger, runtimeHost));
sessionOperations.Enqueue(new ServiceOperation(configuration, logger, serviceProxy, text));
sessionOperations.Enqueue(new ClientTerminationOperation(configuration, logger, processFactory, proxyFactory, runtimeHost, TEN_SECONDS));

View file

@ -44,6 +44,11 @@ namespace SafeExamBrowser.UserInterface.Classic
return new BrowserWindow(control, settings);
}
public ISystemKeyboardLayoutControl CreateKeyboardLayoutControl()
{
return new KeyboardLayoutControl();
}
public IWindow CreateLogWindow(ILogger logger)
{
LogWindow logWindow = null;
@ -74,9 +79,9 @@ namespace SafeExamBrowser.UserInterface.Classic
return new NotificationButton(info);
}
public ISystemKeyboardLayoutControl CreateKeyboardLayoutControl()
public IPasswordDialog CreatePasswordDialog(string message, string title)
{
return new KeyboardLayoutControl();
throw new System.NotImplementedException();
}
public ISystemPowerSupplyControl CreatePowerSupplyControl()

View file

@ -81,6 +81,12 @@ namespace SafeExamBrowser.UserInterface.Windows10
throw new System.NotImplementedException();
}
public IPasswordDialog CreatePasswordDialog(string message, string title)
{
// TODO
throw new System.NotImplementedException();
}
public ISystemPowerSupplyControl CreatePowerSupplyControl()
{
return new PowerSupplyControl();