diff --git a/F1Server.Tests/WebApi/Controllers/HealthControllerTests.cs b/F1Server.Tests/WebApi/Controllers/HealthControllerTests.cs new file mode 100644 index 0000000..d604a47 --- /dev/null +++ b/F1Server.Tests/WebApi/Controllers/HealthControllerTests.cs @@ -0,0 +1,46 @@ +using F1Server.WebApi.Controllers; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; + +namespace F1Server.Tests.WebApi.Controllers; + +/// +/// Contains unit tests verifying the health check of the +/// +[TestClass] +public class HealthControllerTests +{ + #region Methods + + /// + /// Verifies that an initialized database together with an available SignalR hub reports a healthy server + /// + [TestMethod] + public void HealthControllerGetWithHubReturnsOk() + { + using (var hub = new RecordingHubContext()) + { + var controller = new HealthController(NullLogger.Instance, hub); + + var result = controller.Get(); + + Assert.IsInstanceOfType(result, "An initialized database and an available hub should report a healthy server!"); + } + } + + /// + /// Verifies that a missing SignalR hub reports an unhealthy server + /// + [TestMethod] + public void HealthControllerGetWithoutHubReturnsBadRequest() + { + var controller = new HealthController(NullLogger.Instance, null!); + + var result = controller.Get(); + + Assert.IsInstanceOfType(result, "A missing hub should report an unhealthy server!"); + } + + #endregion // Methods +} \ No newline at end of file diff --git a/F1Server.Tests/WebApi/Controllers/LiveSessionControllerTests.cs b/F1Server.Tests/WebApi/Controllers/LiveSessionControllerTests.cs new file mode 100644 index 0000000..f8629ee --- /dev/null +++ b/F1Server.Tests/WebApi/Controllers/LiveSessionControllerTests.cs @@ -0,0 +1,90 @@ +using F1Server.Data; +using F1Server.WebApi.Controllers; +using F1Server.WebApi.Core; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; + +namespace F1Server.Tests.WebApi.Controllers; + +/// +/// Contains unit tests verifying that the starts the live session broadcast +/// +[TestClass] +public class LiveSessionControllerTests +{ + #region Constants + + /// + /// Maximum time in milliseconds a test waits for a broadcast of the timer + /// + private const int BroadcastTimeout = 10000; + + #endregion // Constants + + #region Methods + + /// + /// Verifies that the first request starts the timer, which broadcasts the live session state and data + /// + [TestMethod] + public void LiveSessionControllerGetBroadcastsLiveSession() + { + var appData = new F1ServerApplicationData + { + IsLiveSession = true, + LiveSessionId = 460200, + LiveSessionData = new LiveSessionData + { + DbId = 460200 + } + }; + + using (var hub = new RecordingHubContext()) + { + using (var timerManager = new TimerManager()) + { + var controller = new LiveSessionController(appData, timerManager, hub, NullLogger.Instance); + + var result = controller.Get(); + + Assert.IsInstanceOfType(result, "The live session request should be answered with Ok!"); + Assert.IsTrue(timerManager.IsTimerStarted, "The first request should start the broadcast timer!"); + + var liveSessionState = hub.WaitForMessage("IsLiveSession", BroadcastTimeout); + + Assert.IsNotNull(liveSessionState, "The live session state should be broadcast!"); + Assert.IsTrue((bool?)liveSessionState[0], "The broadcast should report an active live session!"); + Assert.AreEqual(460200L, liveSessionState[1], "The broadcast should contain the live session id!"); + Assert.IsNotNull(hub.WaitForMessage("LiveSessionDataUpdated", BroadcastTimeout), "The live session data should be broadcast!"); + } + } + } + + /// + /// Verifies that a request with an already started timer keeps the running broadcast + /// + [TestMethod] + public void LiveSessionControllerGetWithStartedTimerKeepsTimer() + { + using (var hub = new RecordingHubContext()) + { + using (var timerManager = new TimerManager()) + { + timerManager.PrepareTimer(() => + { + }); + + var timerStarted = timerManager.TimerStarted; + var controller = new LiveSessionController(new F1ServerApplicationData(), timerManager, hub, NullLogger.Instance); + + var result = controller.Get(); + + Assert.IsInstanceOfType(result, "The live session request should be answered with Ok!"); + Assert.AreEqual(timerStarted, timerManager.TimerStarted, "An already started timer must not be restarted!"); + } + } + } + + #endregion // Methods +} \ No newline at end of file diff --git a/F1Server.Tests/WebApi/Controllers/LiveSessionDataControllerTests.cs b/F1Server.Tests/WebApi/Controllers/LiveSessionDataControllerTests.cs new file mode 100644 index 0000000..8719b89 --- /dev/null +++ b/F1Server.Tests/WebApi/Controllers/LiveSessionDataControllerTests.cs @@ -0,0 +1,134 @@ +using F1Server.Core.Enumerations; +using F1Server.Data; +using F1Server.WebApi.Controllers; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace F1Server.Tests.WebApi.Controllers; + +/// +/// Contains unit tests verifying that the maps the live session data into its view data +/// +[TestClass] +public class LiveSessionDataControllerTests +{ + #region Static methods + + /// + /// Creates live session data with one driver + /// + /// Live session data + private static LiveSessionData CreateLiveSessionData() + { + var liveSessionData = new LiveSessionData + { + DbId = 460100, + SessionGameId = 460101, + CurrentCarsOnTrack = 1, + SessionType = SessionType.Qualifying1, + AirTemperature = 24, + TrackTemperature = 38, + FastestSector1 = 30250, + FastestSector1Driver = 3, + FastestLap = 91000, + FastestLapDriver = 3, + TimeTable = [3] + }; + + liveSessionData.Drivers.Add(new LiveDriverData + { + ArrayIndex = 3, + DriverName = "Live Test Driver", + CarNumber = 44, + TeamName = "Live Test Team", + CarPosition = 1, + CurrentDriverStatus = DriverStatus.FlyingLap, + FastestLapTime = 91000, + LapsDriven = 5, + CurrentUsedTyre = VisualTyreCompound.Soft + }); + + return liveSessionData; + } + + #endregion // Static methods + + #region Methods + + /// + /// Verifies that the session values of the live session data are mapped into the view data + /// + [TestMethod] + public void LiveSessionDataControllerGetMapsSessionData() + { + var appData = new F1ServerApplicationData + { + LiveSessionData = CreateLiveSessionData() + }; + + var controller = new LiveSessionDataController(NullLogger.Instance, appData); + + var viewData = controller.Get(); + + Assert.AreEqual(460100, viewData.DbId, "The database id of the session should be mapped!"); + Assert.AreEqual(460101UL, viewData.SessionGameId, "The game id of the session should be mapped!"); + Assert.AreEqual(SessionType.Qualifying1, viewData.SessionType, "The session type should be mapped!"); + Assert.AreEqual(1, viewData.CurrentCarsOnTrack, "The cars on track should be mapped!"); + Assert.AreEqual(24, viewData.AirTemperature, "The air temperature should be mapped!"); + Assert.AreEqual(38, viewData.TrackTemperature, "The track temperature should be mapped!"); + Assert.AreEqual(30250U, viewData.FastestSector1, "The fastest sector 1 should be mapped!"); + Assert.AreEqual(91000U, viewData.FastestLap, "The fastest lap should be mapped!"); + Assert.AreEqual(3, viewData.FastestLapDriver, "The driver of the fastest lap should be mapped!"); + Assert.AreSequenceEqual(new List + { + 3 + }, + viewData.TimeTable, + "The time table should be mapped!"); + } + + /// + /// Verifies that the drivers of the live session data are mapped into the view data + /// + [TestMethod] + public void LiveSessionDataControllerGetMapsDrivers() + { + var appData = new F1ServerApplicationData + { + LiveSessionData = CreateLiveSessionData() + }; + + var controller = new LiveSessionDataController(NullLogger.Instance, appData); + + var viewData = controller.Get(); + + Assert.HasCount(1, viewData.Drivers, "Every driver of the live session should be mapped!"); + + var driver = viewData.Drivers[0]; + + Assert.AreEqual(3, driver.ArrayIndex, "The array index of the driver should be mapped!"); + Assert.AreEqual("Live Test Driver", driver.DriverName, "The name of the driver should be mapped!"); + Assert.AreEqual(44, driver.CarNumber, "The car number should be mapped!"); + Assert.AreEqual("Live Test Team", driver.TeamName, "The team name should be mapped!"); + Assert.AreEqual(DriverStatus.FlyingLap, driver.CurrentDriverStatus, "The driver status should be mapped!"); + Assert.AreEqual(91000U, driver.FastestLapTime, "The fastest lap time of the driver should be mapped!"); + Assert.AreEqual(5, driver.LapsDriven, "The driven laps should be mapped!"); + Assert.AreEqual(VisualTyreCompound.Soft, driver.CurrentUsedTyre, "The used tyre should be mapped!"); + } + + /// + /// Verifies that missing live session data returns empty view data + /// + [TestMethod] + public void LiveSessionDataControllerGetWithoutLiveSessionReturnsEmptyViewData() + { + var controller = new LiveSessionDataController(NullLogger.Instance, new F1ServerApplicationData()); + + var viewData = controller.Get(); + + Assert.AreEqual(0, viewData.DbId, "Without a live session no session id should be returned!"); + Assert.IsEmpty(viewData.Drivers, "Without a live session no drivers should be returned!"); + } + + #endregion // Methods +} \ No newline at end of file diff --git a/F1Server.Tests/WebApi/Controllers/RecordingHubContext.cs b/F1Server.Tests/WebApi/Controllers/RecordingHubContext.cs new file mode 100644 index 0000000..fe8d159 --- /dev/null +++ b/F1Server.Tests/WebApi/Controllers/RecordingHubContext.cs @@ -0,0 +1,160 @@ +using System.Collections.Concurrent; + +using F1Server.WebApi.Hubs; + +using Microsoft.AspNetCore.SignalR; + +namespace F1Server.Tests.WebApi.Controllers; + +/// +/// SignalR hub context recording every message sent to its clients to test the broadcasts of the controllers +/// +internal sealed class RecordingHubContext : IHubContext, IHubClients, IClientProxy, IGroupManager, IDisposable +{ + #region Fields + + /// + /// Names of the sent hub methods with their arguments + /// + private readonly ConcurrentQueue<(string Method, object?[] Arguments)> _messages = new(); + + /// + /// Signaled whenever a message was sent + /// + private readonly AutoResetEvent _messageSent = new(false); + + #endregion // Fields + + #region Methods + + /// + /// Waits until a message of the given hub method was sent + /// + /// Name of the hub method + /// Maximum waiting time in milliseconds + /// Arguments of the first sent message of the hub method, or null if none was sent within the timeout + public object?[]? WaitForMessage(string method, int timeout) + { + var deadline = DateTime.UtcNow.AddMilliseconds(timeout); + + object?[]? arguments = null; + + while (arguments is null && DateTime.UtcNow < deadline) + { + arguments = _messages.FirstOrDefault(m => m.Method == method).Arguments; + + if (arguments is null) + { + _messageSent.WaitOne(TimeSpan.FromMilliseconds(100)); + } + } + + return arguments; + } + + #endregion // Methods + + #region IHubContext + + /// + IHubClients IHubContext.Clients => this; + + /// + IGroupManager IHubContext.Groups => this; + + #endregion // IHubContext + + #region IHubClients + + /// + public IClientProxy All => this; + + /// + public IClientProxy AllExcept(IReadOnlyList excludedConnectionIds) + { + return this; + } + + /// + public IClientProxy Client(string connectionId) + { + return this; + } + + /// + public IClientProxy Clients(IReadOnlyList connectionIds) + { + return this; + } + + /// + public IClientProxy Group(string groupName) + { + return this; + } + + /// + public IClientProxy GroupExcept(string groupName, IReadOnlyList excludedConnectionIds) + { + return this; + } + + /// + public IClientProxy Groups(IReadOnlyList groupNames) + { + return this; + } + + /// + public IClientProxy User(string userId) + { + return this; + } + + /// + public IClientProxy Users(IReadOnlyList userIds) + { + return this; + } + + #endregion // IHubClients + + #region IClientProxy + + /// + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + _messages.Enqueue((method, args)); + _messageSent.Set(); + + return Task.CompletedTask; + } + + #endregion // IClientProxy + + #region IGroupManager + + /// + public Task AddToGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + /// + public Task RemoveFromGroupAsync(string connectionId, string groupName, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + #endregion // IGroupManager + + #region IDisposable + + /// + public void Dispose() + { + _messageSent.Dispose(); + } + + #endregion // IDisposable +} \ No newline at end of file diff --git a/F1Server.Tests/WebApi/Controllers/ServerHealthControllerTests.cs b/F1Server.Tests/WebApi/Controllers/ServerHealthControllerTests.cs new file mode 100644 index 0000000..a79bee9 --- /dev/null +++ b/F1Server.Tests/WebApi/Controllers/ServerHealthControllerTests.cs @@ -0,0 +1,148 @@ +using System.Diagnostics; + +using F1Server.Core.Observability; +using F1Server.Data; +using F1Server.Tests.Processors; +using F1Server.WebApi.Controllers; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; + +namespace F1Server.Tests.WebApi.Controllers; + +/// +/// Contains unit tests verifying the server feature checks of the +/// +[TestClass] +public class ServerHealthControllerTests +{ + #region Static methods + + /// + /// Creates application data of a receiving server with a ready telemetry writer + /// + /// Application data + private static F1ServerApplicationData CreateReceivingAppData() + { + return new F1ServerApplicationData + { + IsReceiving = true, + TelemetryWriter = new RecordingTelemetryWriter() + }; + } + + /// + /// Creates a server health controller + /// + /// Application data + /// Server health controller + private static ServerHealthController CreateController(F1ServerApplicationData appData) + { + return new ServerHealthController(appData, NullLogger.Instance); + } + + #endregion // Static methods + + #region Methods + + /// + /// Verifies that a receiving server without configured observability features reports a healthy server + /// + [TestMethod] + public void ServerHealthControllerGetReceivingServerReturnsOk() + { + var result = CreateController(CreateReceivingAppData()).Get(); + + Assert.IsInstanceOfType(result, "A receiving server with a ready telemetry writer should report a healthy server!"); + } + + /// + /// Verifies that a server that is not receiving data reports an unhealthy server + /// + [TestMethod] + public void ServerHealthControllerGetNotReceivingServerReturnsBadRequest() + { + var appData = CreateReceivingAppData(); + + appData.IsReceiving = false; + + var result = CreateController(appData).Get(); + + Assert.IsInstanceOfType(result, "A server that is not receiving data should report an unhealthy server!"); + } + + /// + /// Verifies that a missing telemetry writer reports an unhealthy server + /// + [TestMethod] + public void ServerHealthControllerGetWithoutTelemetryWriterReturnsBadRequest() + { + var appData = CreateReceivingAppData(); + + appData.TelemetryWriter = null; + + var result = CreateController(appData).Get(); + + Assert.IsInstanceOfType(result, "A missing telemetry writer should report an unhealthy server!"); + } + + /// + /// Verifies that configured but uninitialized metrics report an unhealthy server + /// + [TestMethod] + public void ServerHealthControllerGetConfiguredMetricsWithoutInstanceReturnsBadRequest() + { + var appData = CreateReceivingAppData(); + + appData.IsMetricsConfigured = true; + + var result = CreateController(appData).Get(); + + Assert.IsInstanceOfType(result, "Configured but uninitialized metrics should report an unhealthy server!"); + } + + /// + /// Verifies that configured logging reports a healthy server only with a logger factory + /// + /// Is a logger factory available? + /// Expected health state + [TestMethod] + [DataRow(true, true)] + [DataRow(false, false)] + public void ServerHealthControllerGetConfiguredLoggingRequiresLoggerFactory(bool hasLoggerFactory, bool expectedHealthy) + { + var appData = CreateReceivingAppData(); + + appData.IsLoggingConfigured = true; + appData.LoggerFactory = hasLoggerFactory ? NullLoggerFactory.Instance : null; + + var result = CreateController(appData).Get(); + + Assert.AreEqual(expectedHealthy, result is OkResult, "The health state with configured logging is not as expected!"); + } + + /// + /// Verifies that configured tracing with an attached activity listener reports a healthy server + /// + [TestMethod] + public void ServerHealthControllerGetConfiguredTracingWithListenerReturnsOk() + { + var appData = CreateReceivingAppData(); + + appData.IsTracingConfigured = true; + + using var listener = new ActivityListener + { + ShouldListenTo = source => source.Name == AppActivity.SrvSource.Name, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData + }; + + ActivitySource.AddActivityListener(listener); + + var result = CreateController(appData).Get(); + + Assert.IsInstanceOfType(result, "Configured tracing with an attached listener should report a healthy server!"); + } + + #endregion // Methods +} \ No newline at end of file diff --git a/F1Server.Tests/WebApi/Controllers/StatisticsControllerTests.cs b/F1Server.Tests/WebApi/Controllers/StatisticsControllerTests.cs new file mode 100644 index 0000000..4c0edc4 --- /dev/null +++ b/F1Server.Tests/WebApi/Controllers/StatisticsControllerTests.cs @@ -0,0 +1,45 @@ +using F1Server.Data; +using F1Server.WebApi.Controllers; + +using Microsoft.Extensions.Logging.Abstractions; + +namespace F1Server.Tests.WebApi.Controllers; + +/// +/// Contains unit tests verifying that the returns the telemetry statistics of the application +/// +[TestClass] +public class StatisticsControllerTests +{ + #region Methods + + /// + /// Verifies that the statistics of the application data are returned + /// + [TestMethod] + public void StatisticsControllerGetReturnsApplicationStatistics() + { + var appData = new F1ServerApplicationData(); + var controller = new StatisticsController(appData, NullLogger.Instance); + + var statistics = controller.Get(); + + Assert.AreSame(appData.Statistics, statistics, "The statistics of the application data should be returned!"); + } + + /// + /// Verifies that missing application data returns empty statistics instead of failing + /// + [TestMethod] + public void StatisticsControllerGetWithoutApplicationDataReturnsEmptyStatistics() + { + var controller = new StatisticsController(null!, NullLogger.Instance); + + var statistics = controller.Get(); + + Assert.IsNotNull(statistics, "Missing application data should still return statistics!"); + Assert.AreEqual(0, statistics.PacketsReceivedTotal, "Statistics without application data should not contain received packets!"); + } + + #endregion // Methods +} \ No newline at end of file