sharee.bike-App/LockIt.BLE.Tests/Services/BluetoothLock/BLE/TestLockItEventBased.cs

316 lines
17 KiB
C#
Raw Normal View History

2022-08-30 15:42:25 +02:00
using System;
using System.Threading;
using System.Threading.Tasks;
2021-05-13 17:25:46 +02:00
using NSubstitute;
using NUnit.Framework;
using Plugin.BLE.Abstractions.Contracts;
2022-08-30 15:42:25 +02:00
using Plugin.BLE.Abstractions.EventArgs;
2024-04-09 12:53:23 +02:00
using ShareeBike.Services.BluetoothLock.BLE;
using ShareeBike.Services.BluetoothLock.Exception;
using ShareeBike.Services.BluetoothLock.Tdo;
2021-05-13 17:25:46 +02:00
using DeviceState = Plugin.BLE.Abstractions.DeviceState;
2024-04-09 12:53:23 +02:00
namespace LockIt.BLE.Tests
2021-05-13 17:25:46 +02:00
{
2023-08-31 12:20:06 +02:00
public class TestLockItEventBased
2022-09-06 16:08:19 +02:00
{
[Test]
public void TestOpen()
{
var device = Substitute.For<IDevice>();
var adapter = Substitute.For<IAdapter>();
2024-04-09 12:53:23 +02:00
var cipher = Substitute.For<ShareeBike.Model.Device.ICipher>();
2022-09-06 16:08:19 +02:00
var auth = Substitute.For<ICharacteristic>();
var controlService = Substitute.For<IService>();
var controlCharacteristic = Substitute.For<ICharacteristic>();
var stateCharacteristic = Substitute.For<ICharacteristic>();
var activateCharacteristic = Substitute.For<ICharacteristic>();
var authTdo = new LockInfoAuthTdo.Builder
{
Id = 12,
K_seed = new byte[] { (byte)'p', (byte)'a', (byte)'w', (byte)'m', (byte)'X', (byte)'8', (byte)'T', (byte)'X', (byte)'Q', (byte)'Z', (byte)'d', (byte)'l', (byte)'k', (byte)'3', (byte)'e', (byte)'V', },
K_u = new byte[16]
}.Build();
// Calls related to Authenticate functionality.
device.State.Returns(DeviceState.Connected);
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[8], LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(LockItBase.ERRORCODE_SUCCESS);
2022-09-06 16:08:19 +02:00
device.Name.Returns("ISHAREIT+123");
// Calls related to open functionality.
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
2024-04-09 12:53:23 +02:00
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[] { 1 /* State read before open action: closed */ }, LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateCharacteristic));
2024-04-09 12:53:23 +02:00
activateCharacteristic.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
2022-09-06 16:08:19 +02:00
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Open /* State passed as event argument after opening. */});
// Use factory to create LockIt-object.
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
var lockState = lockIt.OpenAsync();
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
Assert.That(lockState.Result, Is.EqualTo(LockitLockingState.Open));
}
[Test]
public void TestOpen_ThrowsCouldntOpenInconsistentStateExecption()
{
var device = Substitute.For<IDevice>();
var adapter = Substitute.For<IAdapter>();
2024-04-09 12:53:23 +02:00
var cipher = Substitute.For<ShareeBike.Model.Device.ICipher>();
2022-09-06 16:08:19 +02:00
var auth = Substitute.For<ICharacteristic>();
var controlService = Substitute.For<IService>();
var controlCharacteristic = Substitute.For<ICharacteristic>();
var stateCharacteristic = Substitute.For<ICharacteristic>();
var activateLock = Substitute.For<ICharacteristic>();
var authTdo = new LockInfoAuthTdo.Builder
{
Id = 12,
K_seed = new byte[] { (byte)'m', (byte)'l', (byte)'Q', (byte)'I', (byte)'S', (byte)'z', (byte)'p', (byte)'H', (byte)'m', (byte)'n', (byte)'V', (byte)'n', (byte)'7', (byte)'f', (byte)'i', (byte)'3' },
K_u = new byte[16]
}.Build();
// Calls related to Authenticate functionality.
device.State.Returns(DeviceState.Connected);
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(controlService));
controlService.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[8], LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(LockItBase.ERRORCODE_SUCCESS);
2022-09-06 16:08:19 +02:00
device.Name.Returns("ISHAREIT+123");
// Calls related to open functionality.
controlService.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
2024-04-09 12:53:23 +02:00
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[] { 1 /* State read before open action: closed */ }, LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
controlService.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
2024-04-09 12:53:23 +02:00
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
2022-09-06 16:08:19 +02:00
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Closed /* State passed as event argument after opening. */});
// Use factory to create LockIt-object.
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
Assert.That(async () =>
{
var lockState = lockIt.OpenAsync();
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
await lockState;
},
Throws.InstanceOf<CouldntOpenInconsistentStateExecption>());
}
[Test]
2023-08-31 12:20:06 +02:00
public void TestOpen_ThrowsCouldntOpenBoltBlockedException()
2022-09-06 16:08:19 +02:00
{
var device = Substitute.For<IDevice>();
var adapter = Substitute.For<IAdapter>();
2024-04-09 12:53:23 +02:00
var cipher = Substitute.For<ShareeBike.Model.Device.ICipher>();
2022-09-06 16:08:19 +02:00
var auth = Substitute.For<ICharacteristic>();
var lockControl = Substitute.For<IService>();
var controlCharacteristic = Substitute.For<ICharacteristic>();
var activateLock = Substitute.For<ICharacteristic>();
var stateCharacteristic = Substitute.For<ICharacteristic>();
var authTdo = new LockInfoAuthTdo.Builder
{
Id = 12,
K_seed = new byte[] { (byte)'i', (byte)'r', (byte)'F', (byte)'h', (byte)'G', (byte)'T', (byte)'z', (byte)'P', (byte)'F', (byte)'Z', (byte)'n', (byte)'z', (byte)'Y', (byte)'B', (byte)'s', (byte)'9' },
K_u = new byte[16]
}.Build();
// Calls related to Authenticate functionality.
device.State.Returns(DeviceState.Connected);
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[8], LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(LockItBase.ERRORCODE_SUCCESS);
2022-09-06 16:08:19 +02:00
device.Name.Returns("ISHAREIT+123");
// Calls related to open functionality.
lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
2024-04-09 12:53:23 +02:00
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[] { 1 /* closed */}, LockItBase.ERRORCODE_SUCCESS)), Task.FromResult((new byte[] { 4 /* bold blocked */ }, LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
2024-04-09 12:53:23 +02:00
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
2023-08-31 12:20:06 +02:00
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.CouldntOpenBoltBlocked /* State passed as event argument after opening. */});
2022-09-06 16:08:19 +02:00
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
// Use factory to create LockIt-object.
Assert.That(async () =>
{
var lockState = lockIt.OpenAsync();
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
await lockState;
},
Throws.InstanceOf<CouldntOpenBoldIsBlockedException>());
}
[Test]
public void TestClose()
{
var device = Substitute.For<IDevice>();
var adapter = Substitute.For<IAdapter>();
2024-04-09 12:53:23 +02:00
var cipher = Substitute.For<ShareeBike.Model.Device.ICipher>();
2022-09-06 16:08:19 +02:00
var auth = Substitute.For<ICharacteristic>();
var lockControl = Substitute.For<IService>();
var controlCharacteristic = Substitute.For<ICharacteristic>();
var activateLock = Substitute.For<ICharacteristic>();
var stateCharacteristic = Substitute.For<ICharacteristic>();
var authTdo = new LockInfoAuthTdo.Builder
{
Id = 12,
K_seed = new byte[] { (byte)'I', (byte)'7', (byte)'y', (byte)'B', (byte)'f', (byte)'s', (byte)'v', (byte)'L', (byte)'G', (byte)'L', (byte)'7', (byte)'b', (byte)'7', (byte)'X', (byte)'z', (byte)'t' },
K_u = new byte[16]
}.Build();
// Calls related to Authenticate functionality.
device.State.Returns(DeviceState.Connected);
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[8], LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(LockItBase.ERRORCODE_SUCCESS);
2022-09-06 16:08:19 +02:00
device.Name.Returns("ISHAREIT+123");
// Calls related to close functionality.
lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
2024-04-09 12:53:23 +02:00
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[] { 0 /* open */ }, LockItBase.ERRORCODE_SUCCESS)), Task.FromResult((new byte[] { 1 /* closed */}, LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
2024-04-09 12:53:23 +02:00
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
2022-09-06 16:08:19 +02:00
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Closed /* State passed as event argument after closing. */});
// Use factory to create LockIt-object.
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
var lockState = lockIt.CloseAsync();
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
Assert.That(lockState.Result, Is.EqualTo(LockitLockingState.Closed));
}
[Test]
public void TestClose_ThrowsCouldntCloseInconsistentStateExecption()
{
var device = Substitute.For<IDevice>();
var adapter = Substitute.For<IAdapter>();
2024-04-09 12:53:23 +02:00
var cipher = Substitute.For<ShareeBike.Model.Device.ICipher>();
2022-09-06 16:08:19 +02:00
var auth = Substitute.For<ICharacteristic>();
var lockControl = Substitute.For<IService>();
var controlCharacteristic = Substitute.For<ICharacteristic>();
var activateLock = Substitute.For<ICharacteristic>();
var stateCharacteristic = Substitute.For<ICharacteristic>();
var authTdo = new LockInfoAuthTdo.Builder
{
Id = 12,
K_seed = new byte[] { (byte)'8', (byte)'q', (byte)'3', (byte)'9', (byte)'i', (byte)'6', (byte)'c', (byte)'g', (byte)'9', (byte)'L', (byte)'V', (byte)'7', (byte)'T', (byte)'G', (byte)'l', (byte)'f' },
K_u = new byte[16]
}.Build();
// Calls related to Authenticate functionality.
device.State.Returns(DeviceState.Connected);
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[8], LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(LockItBase.ERRORCODE_SUCCESS);
2022-09-06 16:08:19 +02:00
device.Name.Returns("ISHAREIT+123");
// Calls related to close functionality.
lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
2024-04-09 12:53:23 +02:00
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[] { 0 /* opened */}, LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
2024-04-09 12:53:23 +02:00
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
2023-02-22 14:03:35 +01:00
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.Unknown /* State passed as event argument after closing. */}); /* changed from .Open to .Unknown (20.02.23, AMM) after bug fix #657. */
2022-09-06 16:08:19 +02:00
// Use factory to create LockIt-object.
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
// Use factory to create LockIt-object.
Assert.That(async () =>
{
var lockState = lockIt.CloseAsync();
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
await lockState;
},
Throws.InstanceOf<CouldntCloseInconsistentStateExecption>());
}
[Test]
2023-08-31 12:20:06 +02:00
public void TestClose_ThrowsCouldntCloseBoltBlockedException()
2022-09-06 16:08:19 +02:00
{
var device = Substitute.For<IDevice>();
var adapter = Substitute.For<IAdapter>();
2024-04-09 12:53:23 +02:00
var cipher = Substitute.For<ShareeBike.Model.Device.ICipher>();
2022-09-06 16:08:19 +02:00
var auth = Substitute.For<ICharacteristic>();
var lockControl = Substitute.For<IService>();
var controlCharacteristic = Substitute.For<ICharacteristic>();
var activateLock = Substitute.For<ICharacteristic>();
var stateCharacteristic = Substitute.For<ICharacteristic>();
var authTdo = new LockInfoAuthTdo.Builder
{
Id = 12,
K_seed = new byte[] { (byte)'v', (byte)'f', (byte)'u', (byte)'v', (byte)'j', (byte)'E', (byte)'b', (byte)'x', (byte)'p', (byte)'z', (byte)'a', (byte)'h', (byte)'V', (byte)'5', (byte)'9', (byte)'i' },
K_u = new byte[16]
}.Build();
// Calls related to Authenticate functionality.
device.State.Returns(DeviceState.Connected);
device.Id.Returns(new Guid("0000f00d-1212-efde-1523-785fef13d123"));
device.GetServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(lockControl));
lockControl.GetCharacteristicAsync(new Guid("0000baab-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(auth));
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
auth.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[8], LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
cipher.Decrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[3]);
cipher.Encrypt(Arg.Any<byte[]>(), Arg.Any<byte[]>()).Returns(new byte[16]);
2024-04-09 12:53:23 +02:00
auth.WriteAsync(Arg.Any<byte[]>()).Returns(LockItBase.ERRORCODE_SUCCESS);
2022-09-06 16:08:19 +02:00
device.Name.Returns("ISHAREIT+123");
// Calls related to Close functionality.
lockControl.GetCharacteristicAsync(new Guid("0000baaa-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(controlCharacteristic));
2024-04-09 12:53:23 +02:00
controlCharacteristic.ReadAsync(Arg.Any<CancellationToken>()).Returns(Task.FromResult((new byte[] { 0 /* open */ }, LockItBase.ERRORCODE_SUCCESS)));
2022-09-06 16:08:19 +02:00
lockControl.GetCharacteristicAsync(new Guid("0000beee-1212-efde-1523-785fef13d123")).Returns(Task.FromResult(activateLock));
2024-04-09 12:53:23 +02:00
activateLock.WriteAsync(Arg.Any<byte[]>()).Returns(Task.FromResult(LockItBase.ERRORCODE_SUCCESS));
2023-08-31 12:20:06 +02:00
stateCharacteristic.Value.Returns(new byte[] { (byte)LockitLockingState.CouldntCloseBoltBlocked /* State passed as event argument after opening. */});
2022-09-06 16:08:19 +02:00
// Use factory to create LockIt-object.
var lockIt = LockItEventBased.Authenticate(device, authTdo, adapter, cipher).Result;
// Use factory to create LockIt-object.
Assert.That(async () =>
{
var lockState = lockIt.CloseAsync();
controlCharacteristic.ValueUpdated += Raise.EventWith(new object(), new CharacteristicUpdatedEventArgs(stateCharacteristic));
await lockState;
},
2023-08-31 12:20:06 +02:00
Throws.InstanceOf<CouldntCloseBoltBlockedException>());
2022-09-06 16:08:19 +02:00
}
}
2023-02-22 14:03:35 +01:00
}