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(); // 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("jdeScopingInterop.rsaEncrypt", Arg.Any()) .Returns(new ValueTask(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(); var encryptedBase64 = Convert.ToBase64String(new byte[] { 10, 20, 30, 40 }); jsRuntime.InvokeAsync("jdeScopingInterop.rsaEncrypt", Arg.Any()) .Returns(new ValueTask(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(); var encryptedBase64 = Convert.ToBase64String(new byte[] { 1, 2, 3 }); jsRuntime.InvokeAsync("jdeScopingInterop.rsaEncrypt", Arg.Any()) .Returns(new ValueTask(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( "jdeScopingInterop.rsaEncrypt", Arg.Is(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(); jsRuntime.InvokeAsync("jdeScopingInterop.rsaEncrypt", Arg.Any()) .Returns(callInfo => { var args = callInfo.Arg(); capturedJson = args[1] as string; return new ValueTask(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(capturedJson); deserializedModel.ShouldNotBeNull(); deserializedModel.Username.ShouldBe("myuser"); deserializedModel.Password.ShouldBe("mypassword"); } }