Add UnitTests and made code testable with DI

This commit is contained in:
2025-11-04 20:27:51 +10:30
parent e5787c9321
commit 17be096ae2
22 changed files with 865 additions and 140 deletions
@@ -0,0 +1,111 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using TinfoilVibeServer.Authentication;
using TinfoilVibeServer.Services;
// <-- adjust namespace
namespace TinfoilVibeServerTest.Tests
{
[TestFixture]
public class AuthStoreTests
{
private Mock<ILogger<AuthStore>> _loggerMock;
private AuthStore _authStore;
[SetUp]
public void SetUp()
{
_loggerMock = new Mock<ILogger<AuthStore>>();
// Assume Settings is static and can be patched for tests
MockConfigManager = new Mock<ConfigManager>();
_authStore = new AuthStore(_loggerMock.Object, MockConfigManager.Object);
}
public Mock<ConfigManager> MockConfigManager { get; set; }
[TearDown]
public void TearDown()
{
_authStore.Dispose();
}
[Test]
public void LoadAll_ShouldPopulateCollections()
{
// Act
var users = _authStore.Credentials.Count;
var fprs = _authStore.Fingerprints.Count;
// Assert
Assert.That(users, Is.GreaterThan(0), "At least one user must be loaded");
Assert.That(fprs, Is.GreaterThanOrEqualTo(0));
_loggerMock.Verify(
x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Loaded")),
null,
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
Times.AtLeastOnce);
}
[Test]
public void TryValidate_NewUser_ShouldCreateAndVerify()
{
// Arrange
var newUser = "newuser";
var ip = "127.0.0.1";
var password = "";
var uid = null as int?;
// Act
var result = _authStore.TryValidate(newUser, password, uid, ip, out var cred);
// Assert
Assert.That(result, Is.False, "New user should be not be verified automatically");
Assert.That(_authStore.Credentials[newUser], Is.Not.Null);
Assert.That(_authStore.Credentials[newUser].Verified, Is.False);
// New user should now exist
Assert.That(_authStore.Credentials.Any(u => u.Value.Username == newUser), Is.True);
}
[Test]
public void IncrementFailed_BeforeBlacklist_ShouldNotBlacklist()
{
// Arrange
var ip = "203.0.113.5";
var cred = new Credential("dummy", "hash", 1, false);
_authStore.UnbanIp(ip); // ensure clean
// Act
var counter = _authStore.IncrementFailed(cred.Username, ip);
// Assert
Assert.That(counter, Is.EqualTo(1));
Assert.That(_authStore.IsIPBlacklisted(ip), Is.False);
}
[Test]
public void IncrementFailed_ExceedingThreshold_ShouldBlacklist()
{
// Arrange
var ip = "203.0.113.5";
var cred = new Credential("dummy", "hash", MockConfigManager.Object.Settings.MaxFailedAttempts, false);
int threshold = MockConfigManager.Object.Settings.MaxFailedAttempts;
// Simulate threshold failures
for (int i = 0; i < threshold; i++)
_authStore.IncrementFailed(cred.Username, ip);
// Act
int final = _authStore.IncrementFailed(cred.Username, ip);
// Assert
Assert.That(final, Is.EqualTo(threshold + 1));
Assert.That(_authStore.IsIPBlacklisted(ip), Is.True);
}
}
}
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using TinfoilVibeServer.Authentication;
using TinfoilVibeServer.Middleware;
namespace TinfoilVibeServerTest.Tests
{
[TestFixture]
public class BasicAuthMiddlewareTests
{
private Mock<ILogger<BasicAuthMiddleware>> _loggerMock;
private Mock<IAuthStore> _authMock;
private BasicAuthMiddleware _middleware;
private RequestDelegate _next;
[SetUp]
public void SetUp()
{
_loggerMock = new Mock<ILogger<BasicAuthMiddleware>>();
_authMock = new Mock<IAuthStore>();
_next = (HttpContext ctx) => Task.CompletedTask;
_middleware = new BasicAuthMiddleware(_next);
}
private HttpContext CreateContext(string authHeader = null, string ip = "127.0.0.1", int? uid = null)
{
var ctx = new DefaultHttpContext();
ctx.Connection.RemoteIpAddress = IPAddress.Parse(ip);
if (!string.IsNullOrEmpty(authHeader))
{
ctx.Request.Headers["Authorization"] = authHeader;
}
if (uid!= null)
{
ctx.Request.Headers["UID"] = uid.ToString();
}
return ctx;
}
[Test]
public async Task InvokeAsync_NoAuthHeader_ShouldReturn401()
{
// Arrange
var ctx = CreateContext();
// Act
await _middleware.InvokeAsync(ctx, _authMock.Object, _loggerMock.Object);
// Assert
Assert.That(ctx.Response.StatusCode, Is.EqualTo(StatusCodes.Status401Unauthorized));
_loggerMock.Verify(l => l.Log(
LogLevel.Warning,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Missing Authorization header")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once);
}
[Test]
public async Task InvokeAsync_BlacklistedIP_ShouldReturn403()
{
// Arrange
var ctx = CreateContext("Basic dXNlcjpwYXNz");
_authMock.Setup(a => a.IsIPBlacklisted("127.0.0.1")).Returns(true);
// Act
await _middleware.InvokeAsync(ctx, _authMock.Object, _loggerMock.Object);
// Assert
Assert.That(ctx.Response.StatusCode, Is.EqualTo(StatusCodes.Status403Forbidden));
}
[Test]
public async Task InvokeAsync_ValidCredentials_ShouldCallNext()
{
// Arrange
var user = "alice";
var pw = "secret";
var uid = 1234;
var header = $"Basic {Convert.ToBase64String(Encoding.ASCII.GetBytes($"{user}:{pw}"))}";
var ip = "127.0.0.1";
var ctx = CreateContext(header,ip, uid);
string? error;
_authMock.Setup(a =>
a.TryValidate(user, pw, uid, ip, out error))
.Returns(true);
bool nextCalled = false;
_next = (HttpContext _) => { nextCalled = true; return Task.CompletedTask; };
_middleware = new BasicAuthMiddleware(_next);
// Act
await _middleware.InvokeAsync(ctx, _authMock.Object, _loggerMock.Object);
// Assert
Assert.That(nextCalled, Is.True);
Assert.That(ctx.Response.StatusCode, Is.EqualTo(StatusCodes.Status200OK));
}
}
}
@@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using LibHac.Ncm;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using NUnit.Framework;
using TinfoilVibeServer.Models;
using TinfoilVibeServer.Services;
using TinfoilVibeServer.Utilities;
namespace TinfoilVibeServerTest.Tests
{
[TestFixture]
public class SnapshotServiceTests
{
private Mock<ILogger<SnapshotService>> _loggerMock;
private SnapshotService _service;
private Mock<INSPExtractor> _nspExtractorMock;
private Mock<IArchiveHandler> _archiveHander;
private Mock<IOptionsMonitor<SnapshotOptions>> _mockOptions;
private SnapshotOptions _options;
[SetUp]
public void SetUp()
{
_mockOptions = new Mock<IOptionsMonitor<SnapshotOptions>>();
_options = new SnapshotOptions()
{
RomExtensions = [".nsp"],
RootDirectories = ["TestData/ROMS"],
SnapshotFile = "TestData/snapshot.json",
SnapshotBackupFile = "TestData/snapshot.bak"
};
/*// ensure ROM test directory has test files removed
foreach (var file in Directory.GetFiles("TestData/ROMS"))
{
File.Delete(file);
}*/
_mockOptions.Setup(m => m.CurrentValue).Returns(_options);
_loggerMock = new Mock<ILogger<SnapshotService>>();
_archiveHander = new Mock<IArchiveHandler>();
_nspExtractorMock = new Mock<INSPExtractor>();
_nspExtractorMock.Setup(extractor => extractor.ExtractHashFromStream(It.IsAny<Stream>())).Returns("HASH");
_nspExtractorMock.Setup(extractor => extractor.ExtractFromStream(It.IsAny<Stream>())).Returns(
new NcaMetadataWithHash(titleId: "0000000000000000","0000000000000000", version: 1, ContentMetaType.Application, "HASH"));
//Settings.RootDirs = new List<string> { "TestData/Root1", "TestData/Root2" };
_service = new SnapshotService(_mockOptions.Object, _nspExtractorMock.Object, _archiveHander.Object, _loggerMock.Object);
}
[TearDown]
public void TearDown()
{
_service.Dispose();
}
[Test]
public async Task BuildSnapshot_WhenFilesChanged_ShouldPersist()
{
// Arrange
await File.WriteAllTextAsync(_options.SnapshotFile, "[]");
var initialHash = _service.GetSnapshot()?.Hash;
// Add a file to Root1
var newFile = Path.Combine(_options.RootDirectories.First(), "new.nsp");
FileSystemExtensions.EnsureDirectoryExists(Path.GetDirectoryName(newFile));
// Create a new valid NSP file
// copy to temp to touch modified date
foreach (var file in Directory.GetFiles("../../../Data/"))
{
var filename = Path.GetFileName(file);
var destFilename = Path.Combine(Path.GetTempPath(), filename);
File.Copy(file, destFilename, true);
var info = new FileInfo(destFilename)
{
LastWriteTimeUtc = DateTime.UtcNow
};
info.CopyTo(Path.Combine(_options.RootDirectories.First(),filename), true);
}
// Act
_service.SnapshotRebuilt+= (sender, args) =>
{
// Assert
var newHash = _service.GetSnapshot()?.Hash;
Assert.That(newHash, Is.Not.EqualTo(initialHash));
};
Task.Delay(300).Wait();
_loggerMock.Verify(
l => l.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Snapshot rebuilt")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once);
}
[Test]
public async Task BuildSnapshot_NoChange_ShouldNotPersist()
{
// Act
_service.BuildSnapshot();
// Act again snapshot should be identical
_service.BuildSnapshot();
// Assert
_loggerMock.Verify(
l => l.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("persisting new snapshot")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Never);
}
}
}
@@ -6,18 +6,41 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat>
<CoverletOutput>../coverage/</CoverletOutput>
<ExcludeByFile>**/Program.cs</ExcludeByFile> <!-- if you dont want the host code -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="NUnit" Version="4.2.2"/>
<PackageReference Include="NUnit.Analyzers" Version="4.4.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"/>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.11.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="5.2.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="22.0.16" />
</ItemGroup>
<ItemGroup>
<Using Include="NUnit.Framework"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TinfoilVibeServer\TinfoilVibeServer.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\" />
</ItemGroup>
<ItemGroup>
<Reference Include="LibHac">
<HintPath>..\libhac\src\LibHac\bin\Release\net8.0\LibHac.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
-15
View File
@@ -1,15 +0,0 @@
namespace TinfoilVibeServerTest;
public class Tests
{
[SetUp]
public void Setup()
{
}
[Test]
public void Test1()
{
Assert.Pass();
}
}