Updated UnitTests, bump to first release.
This commit is contained in:
32
CHANGELOG.md
Normal file
32
CHANGELOG.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
_nothing changed yet_
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.1.0] - 2025-08-28
|
||||||
|
|
||||||
|
_Inital release_
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `CallMonitorClient` as client to connect to the call monitor endpoint
|
||||||
|
- `CallMonitorEventArgs` are the custom arugments, when `OnEvent` is raised.
|
||||||
|
- Notifying about
|
||||||
|
- `Ring`: An incoming call
|
||||||
|
- `Call`: An outgoing call
|
||||||
|
- `Connect`: The call is answered
|
||||||
|
- `Disconnect`: One party has hung up
|
||||||
|
- An unknown caller means, the `CallerNumber` is empty
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/AM-WD/FritzCallMonitor/compare/v0.1.0...HEAD
|
||||||
|
|
||||||
|
[v0.1.0]: https://github.com/AM-WD/FritzCallMonitor/commits/v0.1.0
|
||||||
@@ -35,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "config", "config", "{6FA27A
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{B5851E79-416B-40CA-959C-ADCAFCC8BADB}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{B5851E79-416B-40CA-959C-ADCAFCC8BADB}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
CHANGELOG.md = CHANGELOG.md
|
||||||
LICENSE.txt = LICENSE.txt
|
LICENSE.txt = LICENSE.txt
|
||||||
README.md = README.md
|
README.md = README.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ namespace AMWD.Net.Api.Fritz.CallMonitor
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CallMonitorClient"/> class.
|
/// Initializes a new instance of the <see cref="CallMonitorClient"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="host">The hostname or IP address of the FRITZ!Box to monitor.</param>
|
/// <param name="host">The hostname or IP address of the FRITZ!Box to monitor (Default: fritz.box).</param>
|
||||||
/// <param name="port">The port to connect to (Default: 1012).</param>
|
/// <param name="port">The port to connect to (Default: 1012).</param>
|
||||||
/// <exception cref="ArgumentNullException">The hostname is not set.</exception>
|
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="host"/> is not set.</exception>
|
||||||
/// <exception cref="ArgumentOutOfRangeException">The port is not in valid range of 1 to 65535.</exception>
|
/// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="port"/>is not in a valid range of 1 to 65535.</exception>
|
||||||
public CallMonitorClient(string host, int port = 1012)
|
public CallMonitorClient(string host = "fritz.box", int port = 1012)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(host))
|
if (string.IsNullOrWhiteSpace(host))
|
||||||
throw new ArgumentNullException(nameof(host));
|
throw new ArgumentNullException(nameof(host));
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace AMWD.Net.Api.Fritz.CallMonitor
|
|||||||
public int? ConnectionId { get; private set; }
|
public int? ConnectionId { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the line / port of signaled.
|
/// Gets the signaled line / port.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? LinePort { get; private set; }
|
public int? LinePort { get; private set; }
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ namespace AMWD.Net.Api.Fritz.CallMonitor
|
|||||||
public string? CalleeNumber { get; private set; }
|
public string? CalleeNumber { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the duarion of the call (only on <see cref="EventType.Disconnect"/> event).
|
/// Gets the duration of the call (only on <see cref="EventType.Disconnect"/> event).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? Duration { get; private set; }
|
public TimeSpan? Duration { get; private set; }
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
|||||||
|
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
|
|
||||||
|
// Ensure no connection attempts are running
|
||||||
|
_connectLock.WaitAsync().Wait();
|
||||||
|
|
||||||
|
// Stop the client
|
||||||
StopAsyncInternally(CancellationToken.None).Wait();
|
StopAsyncInternally(CancellationToken.None).Wait();
|
||||||
|
|
||||||
_connectLock.Dispose();
|
_connectLock.Dispose();
|
||||||
@@ -84,6 +88,7 @@ namespace AMWD.Net.Api.Fritz.CallMonitor.Utils
|
|||||||
var stopTask = Task.Run(async () =>
|
var stopTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
_stopCts?.Cancel();
|
_stopCts?.Cancel();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _monitorTask.ConfigureAwait(false);
|
await _monitorTask.ConfigureAwait(false);
|
||||||
|
|||||||
@@ -15,17 +15,32 @@ namespace FritzCallMonitor.Tests
|
|||||||
[TestClass]
|
[TestClass]
|
||||||
public class CallMonitorClientTest
|
public class CallMonitorClientTest
|
||||||
{
|
{
|
||||||
|
private const int ASYNC_DELAY = 100;
|
||||||
|
|
||||||
|
public TestContext TestContext { get; set; }
|
||||||
|
|
||||||
private const string HOST = "localhost";
|
private const string HOST = "localhost";
|
||||||
private const int PORT = 1012;
|
private const int PORT = 1012;
|
||||||
|
|
||||||
|
private string _dateOffset;
|
||||||
|
|
||||||
|
|
||||||
private Mock<ReconnectTcpClient> _tcpClientMock;
|
private Mock<ReconnectTcpClient> _tcpClientMock;
|
||||||
private Mock<NetworkStreamWrapper> _networkStreamMock;
|
private Mock<NetworkStreamWrapper> _networkStreamMock;
|
||||||
|
|
||||||
|
private bool _tcpClientConnected;
|
||||||
private Queue<(int DelaySeconds, byte[] BufferResponse)> _readAsyncResponses;
|
private Queue<(int DelaySeconds, byte[] BufferResponse)> _readAsyncResponses;
|
||||||
|
|
||||||
[TestInitialize]
|
[TestInitialize]
|
||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
|
var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.Now);
|
||||||
|
_dateOffset = offset < TimeSpan.Zero
|
||||||
|
? "-" + offset.ToString("hh\\:mm")
|
||||||
|
: "+" + offset.ToString("hh\\:mm");
|
||||||
|
|
||||||
|
_tcpClientConnected = true;
|
||||||
|
|
||||||
_readAsyncResponses = new Queue<(int, byte[])>();
|
_readAsyncResponses = new Queue<(int, byte[])>();
|
||||||
|
|
||||||
_readAsyncResponses.Enqueue((0, Encoding.UTF8.GetBytes("25.08.25 20:15:30;RING;2;012345678901;9876543;SIP0;\r\n")));
|
_readAsyncResponses.Enqueue((0, Encoding.UTF8.GetBytes("25.08.25 20:15:30;RING;2;012345678901;9876543;SIP0;\r\n")));
|
||||||
@@ -62,38 +77,211 @@ namespace FritzCallMonitor.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ShouldSetAndGetLogger()
|
public async Task ShouldSetAndGetLogger()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var loggerMock = new Mock<ILogger>();
|
var loggerMock = new Mock<ILogger>();
|
||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
|
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
client.Logger = loggerMock.Object;
|
client.Logger = loggerMock.Object;
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.AreEqual(loggerMock.Object, client.Logger);
|
Assert.AreEqual(loggerMock.Object, client.Logger);
|
||||||
|
|
||||||
_tcpClientMock.VerifySet(m => m.Logger = loggerMock.Object, Times.Once);
|
_tcpClientMock.VerifySet(m => m.Logger = loggerMock.Object, Times.Once);
|
||||||
_tcpClientMock.VerifyGet(m => m.IsConnected, Times.Once);
|
_tcpClientMock.VerifyGet(m => m.IsConnected, Times.Exactly(2));
|
||||||
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
|
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
public void ShouldDisposeOnlyOnce()
|
public async Task ShouldDisposeOnlyOnce()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var client = GetClient();
|
var client = GetClient();
|
||||||
|
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
_tcpClientMock.VerifyGet(m => m.IsConnected, Times.AtMost(2));
|
||||||
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldSkipTaskWhenStreamIsNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var client = GetClient();
|
||||||
|
_tcpClientMock.Setup(m => m.GetStream()).Returns((NetworkStreamWrapper)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_tcpClientMock.VerifyGet(m => m.IsConnected, Times.AtMost(2));
|
||||||
|
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldReadAndParseLine()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
bool eventRaised = false;
|
||||||
|
CallMonitorEventArgs eventArgs = null;
|
||||||
|
var client = GetClient();
|
||||||
|
client.OnEvent += (s, e) =>
|
||||||
|
{
|
||||||
|
eventRaised = true;
|
||||||
|
eventArgs = e;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(eventRaised);
|
||||||
|
Assert.IsNotNull(eventArgs);
|
||||||
|
|
||||||
|
Assert.AreEqual($"2025-08-25 20:15:30 {_dateOffset}", eventArgs.Timestamp?.ToString("yyyy-MM-dd HH:mm:ss K"));
|
||||||
|
Assert.AreEqual(EventType.Ring, eventArgs.Event);
|
||||||
|
Assert.AreEqual(2, eventArgs.ConnectionId);
|
||||||
|
Assert.IsNull(eventArgs.LinePort);
|
||||||
|
Assert.AreEqual("012345678901", eventArgs.CallerNumber);
|
||||||
|
Assert.AreEqual("9876543", eventArgs.CalleeNumber);
|
||||||
|
Assert.IsNull(eventArgs.Duration);
|
||||||
|
|
||||||
|
_tcpClientMock.VerifyGet(m => m.IsConnected, Times.Exactly(2));
|
||||||
|
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldReadAndParseInMultipleReads()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_readAsyncResponses.Clear();
|
||||||
|
_readAsyncResponses.Enqueue((0, Encoding.UTF8.GetBytes("25.08.25 20:15:30;RING;")));
|
||||||
|
_readAsyncResponses.Enqueue((0, Encoding.UTF8.GetBytes("2;012345678901;9876543;SIP0;\n")));
|
||||||
|
_readAsyncResponses.Enqueue((Timeout.Infinite, Array.Empty<byte>()));
|
||||||
|
|
||||||
|
bool eventRaised = false;
|
||||||
|
CallMonitorEventArgs eventArgs = null;
|
||||||
|
var client = GetClient();
|
||||||
|
client.OnEvent += (s, e) =>
|
||||||
|
{
|
||||||
|
eventRaised = true;
|
||||||
|
eventArgs = e;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(eventRaised);
|
||||||
|
Assert.IsNotNull(eventArgs);
|
||||||
|
|
||||||
|
Assert.AreEqual($"2025-08-25 20:15:30 {_dateOffset}", eventArgs.Timestamp?.ToString("yyyy-MM-dd HH:mm:ss K"));
|
||||||
|
Assert.AreEqual(EventType.Ring, eventArgs.Event);
|
||||||
|
Assert.AreEqual(2, eventArgs.ConnectionId);
|
||||||
|
Assert.IsNull(eventArgs.LinePort);
|
||||||
|
Assert.AreEqual("012345678901", eventArgs.CallerNumber);
|
||||||
|
Assert.AreEqual("9876543", eventArgs.CalleeNumber);
|
||||||
|
Assert.IsNull(eventArgs.Duration);
|
||||||
|
|
||||||
|
_tcpClientMock.VerifyGet(m => m.IsConnected, Times.Exactly(3));
|
||||||
|
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldReadAndParseMultipleEvents()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_readAsyncResponses.Clear();
|
||||||
|
_readAsyncResponses.Enqueue((0, Encoding.UTF8.GetBytes("25.08.25 20:15:30;RING;2;012345678901;9876543;SIP0;\n25.08.25 20:15:30")));
|
||||||
|
_readAsyncResponses.Enqueue((0, Encoding.UTF8.GetBytes(";RING;2;012345678901;9876543;SIP0;\r\n")));
|
||||||
|
_readAsyncResponses.Enqueue((Timeout.Infinite, Array.Empty<byte>()));
|
||||||
|
|
||||||
|
int eventsRaised = 0;
|
||||||
|
var client = GetClient();
|
||||||
|
client.OnEvent += (s, e) =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref eventsRaised);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(2, eventsRaised);
|
||||||
|
|
||||||
|
_tcpClientMock.VerifyGet(m => m.IsConnected, Times.Exactly(3));
|
||||||
|
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
|
||||||
|
|
||||||
|
VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task ShouldReadAndParseMultipleEventsWithOneError()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_readAsyncResponses.Clear();
|
||||||
|
_readAsyncResponses.Enqueue((0, Encoding.UTF8.GetBytes("25.08.25 20:15:30;TEST;2;012345678901;9876543;SIP0;\n25.08.25 20:15:30")));
|
||||||
|
_readAsyncResponses.Enqueue((0, Encoding.UTF8.GetBytes(";RING;2;012345678901;9876543;SIP0;\r\n")));
|
||||||
|
_readAsyncResponses.Enqueue((Timeout.Infinite, Array.Empty<byte>()));
|
||||||
|
|
||||||
|
int eventsRaised = 0;
|
||||||
|
var client = GetClient();
|
||||||
|
client.OnEvent += (s, e) =>
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref eventsRaised);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await Task.Delay(ASYNC_DELAY, TestContext.CancellationTokenSource.Token);
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual(1, eventsRaised);
|
||||||
|
|
||||||
|
_tcpClientMock.VerifyGet(m => m.IsConnected, Times.Exactly(3));
|
||||||
|
_tcpClientMock.Verify(m => m.GetStream(), Times.Once);
|
||||||
|
_tcpClientMock.Verify(c => c.Dispose(), Times.Once);
|
||||||
|
|
||||||
|
_networkStreamMock.Verify(m => m.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
|
||||||
|
|
||||||
VerifyNoOtherCalls();
|
VerifyNoOtherCalls();
|
||||||
}
|
}
|
||||||
@@ -109,6 +297,10 @@ namespace FritzCallMonitor.Tests
|
|||||||
_tcpClientMock = new Mock<ReconnectTcpClient>(HOST, PORT);
|
_tcpClientMock = new Mock<ReconnectTcpClient>(HOST, PORT);
|
||||||
_networkStreamMock = new Mock<NetworkStreamWrapper>(null);
|
_networkStreamMock = new Mock<NetworkStreamWrapper>(null);
|
||||||
|
|
||||||
|
_tcpClientMock
|
||||||
|
.Setup(m => m.IsConnected)
|
||||||
|
.Returns(() => _tcpClientConnected);
|
||||||
|
|
||||||
_tcpClientMock
|
_tcpClientMock
|
||||||
.Setup(m => m.GetStream())
|
.Setup(m => m.GetStream())
|
||||||
.Returns(_networkStreamMock.Object);
|
.Returns(_networkStreamMock.Object);
|
||||||
@@ -121,8 +313,10 @@ namespace FritzCallMonitor.Tests
|
|||||||
|
|
||||||
return Task.Delay(TimeSpan.FromSeconds(delaySeconds), token).ContinueWith(t =>
|
return Task.Delay(TimeSpan.FromSeconds(delaySeconds), token).ContinueWith(t =>
|
||||||
{
|
{
|
||||||
Array.Copy(bufferResponse, 0, buffer, offset, bufferResponse.Length);
|
int bytesToCopy = Math.Min(count, bufferResponse.Length - offset);
|
||||||
return bufferResponse.Length;
|
|
||||||
|
Array.Copy(bufferResponse, 0, buffer, offset, bytesToCopy);
|
||||||
|
return bytesToCopy;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user