using System; using System.Threading; using System.Threading.Tasks; using NSubstitute; using NUnit.Framework; using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock; using ShareeBike.Model.Connector; using ShareeBike.Model.State; using ShareeBike.Repository.Request; using ShareeBike.Services.BluetoothLock; using ShareeBike.Services.BluetoothLock.Tdo; using ShareeBike.Services.Geolocation; using Geolocation = ShareeBike.Services.Geolocation.Geolocation; using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand; using NSubstitute.ExceptionExtensions; using ShareeBike.Services.BluetoothLock.Exception; using ShareeBike.Repository.Exception; using Newtonsoft.Json; using ShareeBike.Repository.Response; using ShareeBike.ViewModel; namespace SharedBusinessLogic.Tests.Model.Bikes.BikeInfoNS.BluetoothLock.Command { [TestFixture] public class TestCloseCommand { /// /// Use case: Close lock /// [Test] public async Task TestClose() { var bike = Substitute.For(); var connector = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); geolocation.GetAsync(Arg.Any(), Arg.Any()).Returns(new Geolocation.Builder { Latitude = 47.99, Longitude = 7.78 }.Build()); locks[0].CloseAsync() .Returns(Task.FromResult((LockitLockingState?)LockitLockingState.Closed)); // Return lock state indicating success bike.State.Value.Returns(InUseStateEnum.Booked); bike.LockInfo.State.Returns(LockingState.Open); await InvokeAsync( bike, geolocation, locks, () => true, // isConnectedDelegate (isConnected) => connector, listener, null); // Verify that location is kept because bike might be returned later. Assert.That( bike.LockInfo.Location.Latitude, Is.EqualTo(47.99).Within(0.001)); Assert.That( bike.LockInfo.Location.Longitude, Is.EqualTo(7.78).Within(0.001)); Assert.That( bike.LockInfo.State, Is.EqualTo(LockingState.Closed)); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed listener.Received().ReportStep(Step.WaitStopPollingQueryLocation); listener.Received().ReportStep(Step.QueryLocationTerminated); await connector.Command.UpdateLockingStateAsync(bike, Arg.Is(x => x.Latitude == 47.99 && x.Longitude == 7.78)); }); } [Test] public async Task TestCloseGeolocationServiceGetAsyncFails() { var bike = Substitute.For(); var connector = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); var startOfTest = DateTime.Now; geolocation.GetAsync(Arg.Any(), Arg.Any()).Throws(new Exception("Ups no location...")); locks[0].CloseAsync() .Returns(Task.FromResult((LockitLockingState?)LockitLockingState.Closed)); // Return lock state indicating success bike.State.Value.Returns(InUseStateEnum.Booked); await InvokeAsync( bike, geolocation, locks, () => true, // isConnectedDelegate (isConnected) => connector, listener, null); // Verify that location is kept because bike might be returned later. Assert.That( bike.LockInfo.Location, Is.Null); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); await listener.Received().ReportStateAsync(State.StartGeolocationException, "Ups no location..."); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await connector.Command.UpdateLockingStateAsync(bike, Arg.Any()); }); } [Test] public void TestCloseCloseFailsOutOfReachException() { var bike = Substitute.For(); var connector = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); geolocation.GetAsync(Arg.Any(), Arg.Any()).Returns(new Geolocation.Builder { Latitude = 47.99, Longitude = 7.78 }.Build()); locks[0].CloseAsync() .Returns>(x => throw new OutOfReachException()); bike.State.Value.Returns(InUseStateEnum.Booked); bike.LockInfo.State.Returns(LockingState.Open); Assert.That( async () => await InvokeAsync( bike, geolocation, locks, () => true, // isConnectedDelegate (isConnected) => connector, listener, null), Throws.InstanceOf()); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await listener.Received().ReportStateAsync(State.OutOfReachError, "Exception of type 'ShareeBike.Services.BluetoothLock.Exception.OutOfReachException' was thrown."); }); Assert.That( bike.LockInfo.State, Is.EqualTo(LockingState.UnknownDisconnected)); } [Test] public void TestCloseCloseFailsCouldntCloseMovingException() { var bike = Substitute.For(); var connector = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); geolocation.GetAsync(Arg.Any(), Arg.Any()).Returns(new Geolocation.Builder { Latitude = 47.99, Longitude = 7.78 }.Build()); locks[0].CloseAsync() .Returns>(x => throw new CouldntCloseMovingException()); bike.State.Value.Returns(InUseStateEnum.Booked); bike.LockInfo.State.Returns(LockingState.Open); Assert.That( async () => await InvokeAsync( bike, geolocation, locks, () => true, (isConnected) => connector, listener, null), Throws.InstanceOf()); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await listener.Received().ReportStateAsync(State.CouldntCloseMovingError, "The process is motion sensitive. Step close to the lock, do not move, and try again."); await connector.Command.UpdateLockingStateAsync(bike, Arg.Is(x => x.Latitude == 47.99 && x.Longitude == 7.78)); }); Assert.That( bike.LockInfo.State, Is.EqualTo(LockingState.Open)); } [Test] public void TestCloseCloseFailsCouldntCloseBoltBlockedException() { var bike = Substitute.For(); var connector = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); geolocation.GetAsync(Arg.Any(), Arg.Any()).Returns(new Geolocation.Builder { Latitude = 47.99, Longitude = 7.78 }.Build()); locks[0].CloseAsync() .Returns>(x => throw new CouldntCloseBoltBlockedException()); bike.State.Value.Returns(InUseStateEnum.Booked); bike.LockInfo.State.Returns(LockingState.Open); Assert.That( async () => await InvokeAsync(bike, geolocation, locks, () => true, (isConnected) => connector, listener, null), Throws.InstanceOf()); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await listener.Received().ReportStateAsync(State.CouldntCloseBoltBlockedError, "Lock bolt is blocked. Make sure that no spoke or any other obstacle prevents the lock from closing and try again."); await connector.Command.UpdateLockingStateAsync(bike, Arg.Is(x => x.Latitude == 47.99 && x.Longitude == 7.78)); }); Assert.That( bike.LockInfo.State, Is.EqualTo(LockingState.UnknownFromHardwareError)); } [Test] public void TestCloseCloseFailsCouldntCloseBoltBlockedExceptionOpen() { var bike = Substitute.For(); var connector = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); geolocation.GetAsync(Arg.Any(), Arg.Any()).Returns(new Geolocation.Builder { Latitude = 47.99, Longitude = 7.78 }.Build()); locks[0].CloseAsync() .Returns>(x => throw new CouldntCloseBoltBlockedException(LockingState.Open)); bike.State.Value.Returns(InUseStateEnum.Booked); bike.LockInfo.State.Returns(LockingState.Open); Assert.That( async () => await InvokeAsync(bike, geolocation, locks, () => true, (isConnected) => connector, listener, null), Throws.InstanceOf()); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await listener.Received().ReportStateAsync(State.CouldntCloseBoltBlockedError, "Lock bolt is blocked. Make sure that no spoke or any other obstacle prevents the lock from closing and try again."); await connector.Command.UpdateLockingStateAsync(bike, Arg.Is(x => x.Latitude == 47.99 && x.Longitude == 7.78)); }); Assert.That( bike.LockInfo.State, Is.EqualTo(LockingState.Open)); } [Test] public void TestCloseCloseFailsCouldntCloseInconsistentState() { var bike = Substitute.For(); var connector = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); geolocation.GetAsync(Arg.Any(), Arg.Any()).Returns(new Geolocation.Builder { Latitude = 47.99, Longitude = 7.78 }.Build()); locks[0].CloseAsync() .Returns>(x => throw new CouldntCloseInconsistentStateExecption(LockitLockingState.Unknown.GetLockingState())); bike.State.Value.Returns(InUseStateEnum.Booked); bike.LockInfo.State.Returns(LockingState.Open); Assert.That( async () => await InvokeAsync(bike, geolocation, locks, () => true, (isConnected) => connector, listener, null), Throws.InstanceOf()); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await listener.Received().ReportStateAsync(State.GeneralCloseError, "Lock reports unknown bold position. Lock could be closed or open. Please try again or contact customer support."); await connector.Command.UpdateLockingStateAsync(bike, Arg.Is(x => x.Latitude == 47.99 && x.Longitude == 7.78)); }); Assert.That( bike.LockInfo.State, Is.EqualTo(LockingState.UnknownFromHardwareError)); } [Test] public void TestCloseCloseFailsException() { var bike = Substitute.For(); var connector = Substitute.For(); var command = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); geolocation.GetAsync(Arg.Any(), Arg.Any()).Returns(new Geolocation.Builder { Latitude = 47.99, Longitude = 7.78 }.Build()); locks[0].CloseAsync() .Returns>(x => throw new Exception("Exception message.")); bike.State.Value.Returns(InUseStateEnum.Booked); Assert.That( async () => await InvokeAsync(bike, geolocation, locks, () => true, (isConnected) => connector, listener, null), Throws.InstanceOf()); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await listener.ReportStateAsync(State.GeneralCloseError, "Exception message."); await connector.Command.UpdateLockingStateAsync(bike, Arg.Is(x => x.Latitude == 47.99 && x.Longitude == 7.78)); }); Assert.That( bike.LockInfo.State, Is.EqualTo(LockingState.UnknownDisconnected)); } [Test] public async Task TestCloseWaitGeolocationServiceGetAsyncFails() { var bike = Substitute.For(); var connector = Substitute.For(); var command = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); locks[0].CloseAsync() .Returns(LockitLockingState.Closed); // Return lock state indicating success geolocation.GetAsync(Arg.Any(), Arg.Any()).Returns(Task.FromException(new Exception("Ups, some error..."))); bike.State.Value.Returns(InUseStateEnum.Booked); await InvokeAsync(bike, geolocation, locks, () => true, (isConnected) => connector, listener, null); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed listener.Received().ReportStep(Step.WaitStopPollingQueryLocation); await listener.Received().ReportStateAsync(State.WaitGeolocationException, "Ups, some error..."); listener.Received().ReportStep(Step.QueryLocationTerminated); await connector.Command.UpdateLockingStateAsync(bike, Arg.Any()); }); } [Test] public async Task TestCloseUpdateLockingStateFailsWebConnectFailureException() { var bike = Substitute.For(); var connector = Substitute.For(); var command = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); locks[0].CloseAsync() .Returns(LockitLockingState.Closed); // Return lock state indicating success connector.Command.UpdateLockingStateAsync(bike, Arg.Any()).Returns(x => throw new WebConnectFailureException("Context info.", new System.Exception("hoppla"))); bike.State.Value.Returns(InUseStateEnum.Booked); await InvokeAsync(bike, geolocation, locks, () => true, (isConnected) => connector, listener, null); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await connector.Command.UpdateLockingStateAsync(bike, Arg.Any()); await listener.ReportStateAsync(State.WebConnectFailed, "Context info."); }); } [Test] public async Task TestCloseUpdateLockingStateFailsResponseException() { var bike = Substitute.For(); var connector = Substitute.For(); var command = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); locks[0].CloseAsync() .Returns(LockitLockingState.Closed); // Return lock state indicating success connector.Command.UpdateLockingStateAsync(bike, Arg.Any()).Returns(x => throw new ReturnBikeException(JsonConvert.DeserializeObject(@"{ ""response_text"" : ""Some invalid data received!""}"), "Outer message.")); bike.State.Value.Returns(InUseStateEnum.Booked); await InvokeAsync(bike, geolocation, locks, () => true, (isConnected) => connector, listener, null); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await connector.Command.UpdateLockingStateAsync(bike, Arg.Any()); await listener.ReportStateAsync(State.ResponseIsInvalid, "Outer message."); }); } [Test] public async Task TestCloseUpdateLockingStateFailsException() { var bike = Substitute.For(); var connector = Substitute.For(); var command = Substitute.For(); var geolocation = Substitute.For(); var locks = Substitute.For(); var listener = Substitute.For(); var viewUpdateManager = Substitute.For(); locks[0].CloseAsync() .Returns(LockitLockingState.Closed); // Return lock state indicating success connector.Command.UpdateLockingStateAsync(bike, Arg.Any()).Returns(x => throw new Exception("Exception message.")); bike.State.Value.Returns(InUseStateEnum.Booked); await InvokeAsync(bike, geolocation, locks, () => true, (isConnected) => connector, listener, null); // Verify behavior Received.InOrder(async () => { listener.Received().ReportStep(Step.StartingQueryingLocation); await geolocation.GetAsync(Arg.Any(), Arg.Any()); listener.Received().ReportStep(Step.ClosingLock); await locks.Received()[0].CloseAsync(); // Lock must be closed await connector.Command.UpdateLockingStateAsync(bike, Arg.Any()); await listener.ReportStateAsync(State.BackendUpdateFailed, "Exception message."); }); } } }