test(client): add CryptoService tests

This commit is contained in:
Joseph Doherty
2026-01-03 08:48:24 -05:00
parent d59f096cc9
commit 5c19cf0dbd
3 changed files with 144 additions and 8 deletions
@@ -11,6 +11,7 @@
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
@@ -22,6 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\JdeScoping.Client\JdeScoping.Client.csproj" />
<ProjectReference Include="..\..\src\JdeScoping.Core\JdeScoping.Core.csproj" />
</ItemGroup>
</Project>
@@ -1,8 +0,0 @@
// This file exists to ensure the test project compiles.
// Add tests here as needed.
namespace JdeScoping.Client.Tests;
public class Placeholder
{
// Tests will be added here
}
@@ -0,0 +1,142 @@
using System.Text.Json;
using JdeScoping.Client.Services;
using JdeScoping.Core.Models.Auth;
using Microsoft.JSInterop;
using NSubstitute;
using RichardSzalay.MockHttp;
using Shouldly;
namespace JdeScoping.Client.Tests.Services;
public class CryptoServiceTests
{
[Fact]
public async Task EncryptLoginAsync_ReturnsBase64String()
{
// Arrange
var publicKeyPem = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgk...\n-----END PUBLIC KEY-----";
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("/api/auth/public-key")
.Respond("application/json", JsonSerializer.Serialize(new PublicKeyResponse(publicKeyPem)));
var httpClient = new HttpClient(mockHttp) { BaseAddress = new Uri("http://localhost/") };
var jsRuntime = Substitute.For<IJSRuntime>();
// Mock the JavaScript interop to return a base64 encoded string
var expectedBase64 = Convert.ToBase64String(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 });
jsRuntime.InvokeAsync<string>("jdeScopingInterop.rsaEncrypt", Arg.Any<object[]>())
.Returns(new ValueTask<string>(expectedBase64));
var service = new CryptoService(httpClient, jsRuntime);
var loginModel = new LoginModel { Username = "testuser", Password = "testpass" };
// Act
var result = await service.EncryptLoginAsync(loginModel);
// Assert - result should be valid base64
result.ShouldNotBeNullOrEmpty();
var decoded = Convert.FromBase64String(result);
decoded.ShouldNotBeEmpty();
}
[Fact]
public async Task EncryptLoginAsync_CachesPublicKey_OnlyOneHttpCall()
{
// Arrange
var publicKeyPem = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgk...\n-----END PUBLIC KEY-----";
var mockHttp = new MockHttpMessageHandler();
// Track how many times the public key endpoint is called
var keyRequest = mockHttp.Expect("/api/auth/public-key")
.Respond("application/json", JsonSerializer.Serialize(new PublicKeyResponse(publicKeyPem)));
var httpClient = new HttpClient(mockHttp) { BaseAddress = new Uri("http://localhost/") };
var jsRuntime = Substitute.For<IJSRuntime>();
var encryptedBase64 = Convert.ToBase64String(new byte[] { 10, 20, 30, 40 });
jsRuntime.InvokeAsync<string>("jdeScopingInterop.rsaEncrypt", Arg.Any<object[]>())
.Returns(new ValueTask<string>(encryptedBase64));
var service = new CryptoService(httpClient, jsRuntime);
var loginModel1 = new LoginModel { Username = "user1", Password = "pass1" };
var loginModel2 = new LoginModel { Username = "user2", Password = "pass2" };
// Act - call EncryptLoginAsync twice
await service.EncryptLoginAsync(loginModel1);
await service.EncryptLoginAsync(loginModel2);
// Assert - public key should only be fetched once (cached)
mockHttp.GetMatchCount(keyRequest).ShouldBe(1);
}
[Fact]
public async Task EncryptLoginAsync_CallsJavaScriptInteropWithCorrectParameters()
{
// Arrange
var publicKeyPem = "-----BEGIN PUBLIC KEY-----\nTestKey\n-----END PUBLIC KEY-----";
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("/api/auth/public-key")
.Respond("application/json", JsonSerializer.Serialize(new PublicKeyResponse(publicKeyPem)));
var httpClient = new HttpClient(mockHttp) { BaseAddress = new Uri("http://localhost/") };
var jsRuntime = Substitute.For<IJSRuntime>();
var encryptedBase64 = Convert.ToBase64String(new byte[] { 1, 2, 3 });
jsRuntime.InvokeAsync<string>("jdeScopingInterop.rsaEncrypt", Arg.Any<object[]>())
.Returns(new ValueTask<string>(encryptedBase64));
var service = new CryptoService(httpClient, jsRuntime);
var loginModel = new LoginModel { Username = "testuser", Password = "secret123" };
// Act
await service.EncryptLoginAsync(loginModel);
// Assert - verify JS interop was called with correct function name
await jsRuntime.Received(1).InvokeAsync<string>(
"jdeScopingInterop.rsaEncrypt",
Arg.Is<object[]>(args =>
args.Length == 2 &&
args[0] is string &&
((string)args[0]) == publicKeyPem &&
args[1] is string));
}
[Fact]
public async Task EncryptLoginAsync_SerializesLoginModelToJson()
{
// Arrange
var publicKeyPem = "-----BEGIN PUBLIC KEY-----\nTestKey\n-----END PUBLIC KEY-----";
var mockHttp = new MockHttpMessageHandler();
mockHttp.When("/api/auth/public-key")
.Respond("application/json", JsonSerializer.Serialize(new PublicKeyResponse(publicKeyPem)));
var httpClient = new HttpClient(mockHttp) { BaseAddress = new Uri("http://localhost/") };
string? capturedJson = null;
var jsRuntime = Substitute.For<IJSRuntime>();
jsRuntime.InvokeAsync<string>("jdeScopingInterop.rsaEncrypt", Arg.Any<object[]>())
.Returns(callInfo =>
{
var args = callInfo.Arg<object[]>();
capturedJson = args[1] as string;
return new ValueTask<string>(Convert.ToBase64String(new byte[] { 1 }));
});
var service = new CryptoService(httpClient, jsRuntime);
var loginModel = new LoginModel { Username = "myuser", Password = "mypassword" };
// Act
await service.EncryptLoginAsync(loginModel);
// Assert - verify the JSON contains the login credentials
capturedJson.ShouldNotBeNull();
capturedJson.ShouldContain("myuser");
capturedJson.ShouldContain("mypassword");
// Verify it's valid JSON that can deserialize back to LoginModel
var deserializedModel = JsonSerializer.Deserialize<LoginModel>(capturedJson);
deserializedModel.ShouldNotBeNull();
deserializedModel.Username.ShouldBe("myuser");
deserializedModel.Password.ShouldBe("mypassword");
}
}