diff --git a/tests/Modules/LocalRecordsModule.Tests/GlobalUsings.cs b/tests/Modules/LocalRecordsModule.Tests/GlobalUsings.cs
new file mode 100644
index 000000000..c802f4480
--- /dev/null
+++ b/tests/Modules/LocalRecordsModule.Tests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
diff --git a/tests/Modules/LocalRecordsModule.Tests/LocalRecordsModule.Tests.csproj b/tests/Modules/LocalRecordsModule.Tests/LocalRecordsModule.Tests.csproj
new file mode 100644
index 000000000..ae41820e7
--- /dev/null
+++ b/tests/Modules/LocalRecordsModule.Tests/LocalRecordsModule.Tests.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\..\..\.nuget\packages\moq\4.20.70\lib\net6.0\Moq.dll
+
+
+
+
diff --git a/tests/Modules/LocalRecordsModule.Tests/Services/LocalRecordsServiceTests.cs b/tests/Modules/LocalRecordsModule.Tests/Services/LocalRecordsServiceTests.cs
new file mode 100644
index 000000000..36085949f
--- /dev/null
+++ b/tests/Modules/LocalRecordsModule.Tests/Services/LocalRecordsServiceTests.cs
@@ -0,0 +1,318 @@
+using EvoSC.Common.Database.Models.Maps;
+using EvoSC.Common.Database.Models.Player;
+using EvoSC.Common.Interfaces;
+using EvoSC.Common.Interfaces.Models;
+using EvoSC.Common.Interfaces.Models.Enums;
+using EvoSC.Common.Interfaces.Services;
+using EvoSC.Common.Interfaces.Themes;
+using EvoSC.Common.Models.Maps;
+using EvoSC.Common.Models.Players;
+using EvoSC.Common.Tests;
+using EvoSC.Common.Themes;
+using EvoSC.Manialinks;
+using EvoSC.Manialinks.Interfaces;
+using EvoSC.Modules.Official.LocalRecordsModule.Config;
+using EvoSC.Modules.Official.LocalRecordsModule.Database.Models;
+using EvoSC.Modules.Official.LocalRecordsModule.Interfaces;
+using EvoSC.Modules.Official.LocalRecordsModule.Interfaces.Database;
+using EvoSC.Modules.Official.LocalRecordsModule.Interfaces.Services;
+using EvoSC.Modules.Official.LocalRecordsModule.Services;
+using EvoSC.Modules.Official.PlayerRecords.Database.Models;
+using EvoSC.Modules.Official.PlayerRecords.Interfaces;
+using EvoSC.Modules.Official.PlayerRecords.Interfaces.Models;
+using EvoSC.Testing;
+using GbxRemoteNet.Interfaces;
+using Microsoft.Extensions.Logging;
+using Microsoft.VisualBasic;
+using Moq;
+
+namespace LocalRecordsModule.Tests.Services;
+
+using MockData = (
+ ILocalRecordsService Service,
+ Mock MapService,
+ Mock LocalRecordRepository,
+ Mock PlayerManagerService,
+ Mock ManialinkManager,
+ ILogger Logger,
+ Mock Settings,
+ (Mock Client, Mock Remote) Server,
+ Mock ThemeManager,
+ Mock PlayerRecordsRepository);
+
+public class LocalRecordsServiceTests
+{
+ private MockData NewLocalRecordsServiceMock()
+ {
+ var mapService = new Mock();
+ var localRecordRepository = new Mock();
+ var playerManager = new Mock();
+ var manialinkManager = new Mock();
+ var logger = LoggerSetup.CreateLogger();
+ var settings = new Mock();
+ var server = Mocking.NewServerClientMock();
+ var themeManager = new Mock();
+ var playerRecordsRepository = new Mock();
+
+ themeManager.SetupGet(m => m.Theme)
+ .Returns(new DynamicThemeOptions(new Dictionary { { "Info", "FFF" } }));
+
+ var localRecordsService = new LocalRecordsService(
+ mapService.Object,
+ localRecordRepository.Object,
+ playerManager.Object,
+ manialinkManager.Object,
+ logger,
+ settings.Object,
+ server.Client.Object,
+ themeManager.Object,
+ playerRecordsRepository.Object
+ );
+
+ return (
+ localRecordsService,
+ mapService,
+ localRecordRepository,
+ playerManager,
+ manialinkManager,
+ logger,
+ settings,
+ server,
+ themeManager,
+ playerRecordsRepository
+ );
+ }
+
+ private (IMap Map, IPlayer Player) SetupMockRecords(MockData mock)
+ {
+ var currentMap = new Map { Id = 1234, Uid = "my map" };
+ var player = new Player { Id = 3, AccountId = "myplayer", NickName = "my player" };
+
+ DbPlayerRecord[] records =
+ [
+ new DbPlayerRecord
+ {
+ Id = 1,
+ MapId = currentMap.Id,
+ Score = 1337,
+ RecordType = PlayerRecordType.Time,
+ DbMap = new DbMap(currentMap),
+ DbPlayer = new DbPlayer{ Id = 2, AccountId = "myplayer2", NickName = "my player2"}
+ },
+ new DbPlayerRecord
+ {
+ Id = 2,
+ MapId = currentMap.Id,
+ Score = 1337,
+ RecordType = PlayerRecordType.Time,
+ DbMap = new DbMap(currentMap),
+ DbPlayer = new DbPlayer{ Id = 3, AccountId = "myplayer3", NickName = "my player3"}
+ },
+ new DbPlayerRecord
+ {
+ Id = 3,
+ MapId = currentMap.Id,
+ Score = 1337,
+ RecordType = PlayerRecordType.Time,
+ DbMap = new DbMap(currentMap),
+ DbPlayer = new DbPlayer(player)
+ },
+ new DbPlayerRecord
+ {
+ Id = 4,
+ MapId = currentMap.Id,
+ Score = 1337,
+ RecordType = PlayerRecordType.Time,
+ DbMap = new DbMap(currentMap),
+ DbPlayer = new DbPlayer{ Id = 4, AccountId = "myplayer4", NickName = "my player4"}
+ },
+ new DbPlayerRecord
+ {
+ Id = 5,
+ MapId = currentMap.Id,
+ Score = 1337,
+ RecordType = PlayerRecordType.Time,
+ DbMap = new DbMap(currentMap),
+ DbPlayer = new DbPlayer{ Id = 5, AccountId = "myplayer5", NickName = "my player5"}
+ },
+ ];
+
+ mock.MapService.Setup(m => m.GetCurrentMapAsync()).ReturnsAsync(currentMap);
+ mock.LocalRecordRepository.Setup(m => m.GetLocalRecordsOfMapByIdAsync(currentMap.Id))
+ .ReturnsAsync(records.Select(r => new DbLocalRecord
+ {
+ Id = 0,
+ MapId = (long)r.MapId!,
+ RecordId = r.Id,
+ Position = (int)r.Id,
+ DbMap = r.DbMap,
+ DbRecord = r
+ }));
+
+ return (currentMap, player);
+ }
+
+ [Fact]
+ public async Task GetLocalsOfCurrentMap_Returns_Local_Records_Of_CurrentMap()
+ {
+ var mock = NewLocalRecordsServiceMock();
+ SetupMockRecords(mock);
+ var records = await mock.Service.GetLocalsOfCurrentMapAsync();
+
+ Assert.Equal(5, records.Length);
+ Assert.All(records, r => Assert.Equal("my map", r.Map.Uid));
+ }
+
+ [Fact]
+ public async Task Current_Map_Not_Found_Throws_Error()
+ {
+ var mock = NewLocalRecordsServiceMock();
+
+ mock.MapService.Setup(m => m.GetCurrentMapAsync()).ReturnsAsync((IMap?)null);
+
+ await Assert.ThrowsAsync(() => mock.Service.GetLocalsOfCurrentMapAsync());
+ }
+
+ [Fact]
+ public async Task Widget_Is_Shown_To_Player()
+ {
+ var mock = NewLocalRecordsServiceMock();
+ var mockSetup = SetupMockRecords(mock);
+
+ await mock.Service.ShowWidgetAsync(mockSetup.Player);
+
+ mock.ManialinkManager.Verify(m =>
+ m.SendManialinkAsync(mockSetup.Player, "LocalRecordsModule.LocalRecordsWidget", It.IsAny