test: add SearchApiClientTests with route and success/error tests

Add comprehensive unit tests for SearchApiClient covering:
- Route verification tests for all 6 methods (GetUserSearches, GetQueuedSearches,
  GetSearch, CopySearch, CreateSearch, GetResults)
- Success tests verifying deserialization for all methods
- Representative error tests (404 for GetSearch, 401 for GetUserSearches)

Uses MockHttpMessageHandler to verify correct API routes are called
and ApiResult<T> pattern maps responses correctly.
This commit is contained in:
Joseph Doherty
2026-01-06 11:27:33 -05:00
parent 299099f716
commit 76ce35458c
@@ -0,0 +1,253 @@
using System.Net;
using System.Text.Json;
using JdeScoping.Client.Services;
using JdeScoping.Core.ApiContracts;
using JdeScoping.Core.ViewModels;
using RichardSzalay.MockHttp;
using Shouldly;
namespace JdeScoping.Client.Tests.Services;
public class SearchApiClientTests
{
private readonly MockHttpMessageHandler _mockHttp;
private readonly SearchApiClient _client;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public SearchApiClientTests()
{
_mockHttp = new MockHttpMessageHandler();
var httpClient = new HttpClient(_mockHttp) { BaseAddress = new Uri("http://localhost/") };
_client = new SearchApiClient(httpClient);
}
// Route verification tests
[Fact]
public async Task GetUserSearchesAsync_CallsCorrectRoute()
{
// Arrange
var request = _mockHttp.Expect(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.Base}")
.Respond("application/json", "[]");
// Act
await _client.GetUserSearchesAsync();
// Assert
_mockHttp.GetMatchCount(request).ShouldBe(1);
}
[Fact]
public async Task GetQueuedSearchesAsync_CallsCorrectRoute()
{
// Arrange
var request = _mockHttp.Expect(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.Queue}")
.Respond("application/json", "[]");
// Act
await _client.GetQueuedSearchesAsync();
// Assert
_mockHttp.GetMatchCount(request).ShouldBe(1);
}
[Fact]
public async Task GetSearchAsync_CallsCorrectRoute()
{
// Arrange
var searchId = 42;
var request = _mockHttp.Expect(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.GetById(searchId)}")
.Respond("application/json", JsonSerializer.Serialize(CreateTestSearch(searchId), JsonOptions));
// Act
await _client.GetSearchAsync(searchId);
// Assert
_mockHttp.GetMatchCount(request).ShouldBe(1);
}
[Fact]
public async Task CreateSearchAsync_CallsCorrectRoute_WithPostMethod()
{
// Arrange
var request = _mockHttp.Expect(HttpMethod.Post, $"http://localhost/{ApiRoutes.Search.Base}")
.Respond("application/json", "123");
// Act
await _client.CreateSearchAsync(CreateTestSearch(0));
// Assert
_mockHttp.GetMatchCount(request).ShouldBe(1);
}
[Fact]
public async Task GetResultsAsync_CallsCorrectRoute()
{
// Arrange
var searchId = 42;
var request = _mockHttp.Expect(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.GetResults(searchId)}")
.Respond("application/octet-stream", new MemoryStream(new byte[] { 1, 2, 3 }));
// Act
await _client.GetResultsAsync(searchId);
// Assert
_mockHttp.GetMatchCount(request).ShouldBe(1);
}
[Fact]
public async Task CopySearchAsync_CallsCorrectRoute()
{
// Arrange
var searchId = 42;
var request = _mockHttp.Expect(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.GetCopy(searchId)}")
.Respond("application/json", JsonSerializer.Serialize(CreateTestSearch(100), JsonOptions));
// Act
await _client.CopySearchAsync(searchId);
// Assert
_mockHttp.GetMatchCount(request).ShouldBe(1);
}
// Success tests
[Fact]
public async Task GetUserSearchesAsync_Success_ReturnsSearchList()
{
// Arrange
var searches = new List<SearchViewModel> { CreateTestSearch(1), CreateTestSearch(2) };
_mockHttp.When(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.Base}")
.Respond("application/json", JsonSerializer.Serialize(searches, JsonOptions));
// Act
var result = await _client.GetUserSearchesAsync();
// Assert
result.IsSuccess.ShouldBeTrue();
result.Value.Count.ShouldBe(2);
result.Value[0].Id.ShouldBe(1);
}
[Fact]
public async Task GetSearchAsync_Success_ReturnsSearch()
{
// Arrange
var search = CreateTestSearch(42);
_mockHttp.When(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.GetById(42)}")
.Respond("application/json", JsonSerializer.Serialize(search, JsonOptions));
// Act
var result = await _client.GetSearchAsync(42);
// Assert
result.IsSuccess.ShouldBeTrue();
result.Value.Id.ShouldBe(42);
}
[Fact]
public async Task CreateSearchAsync_Success_ReturnsId()
{
// Arrange
_mockHttp.When(HttpMethod.Post, $"http://localhost/{ApiRoutes.Search.Base}")
.Respond("application/json", "123");
// Act
var result = await _client.CreateSearchAsync(CreateTestSearch(0));
// Assert
result.IsSuccess.ShouldBeTrue();
result.Value.ShouldBe(123);
}
[Fact]
public async Task GetQueuedSearchesAsync_Success_ReturnsSearchList()
{
// Arrange
var searches = new List<SearchViewModel> { CreateTestSearch(1) };
_mockHttp.When(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.Queue}")
.Respond("application/json", JsonSerializer.Serialize(searches, JsonOptions));
// Act
var result = await _client.GetQueuedSearchesAsync();
// Assert
result.IsSuccess.ShouldBeTrue();
result.Value.Count.ShouldBe(1);
}
[Fact]
public async Task GetResultsAsync_Success_ReturnsBytes()
{
// Arrange - GetResultsAsync returns byte[] for Excel file
var expectedBytes = new byte[] { 0x50, 0x4B, 0x03, 0x04 }; // ZIP/XLSX header
_mockHttp.When(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.GetResults(42)}")
.Respond("application/octet-stream", new MemoryStream(expectedBytes));
// Act
var result = await _client.GetResultsAsync(42);
// Assert
result.IsSuccess.ShouldBeTrue();
result.Value.ShouldBe(expectedBytes);
}
[Fact]
public async Task CopySearchAsync_Success_ReturnsCopiedSearch()
{
// Arrange
var copiedSearch = CreateTestSearch(100);
copiedSearch.Name = "Copy of Search 42";
_mockHttp.When(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.GetCopy(42)}")
.Respond("application/json", JsonSerializer.Serialize(copiedSearch, JsonOptions));
// Act
var result = await _client.CopySearchAsync(42);
// Assert
result.IsSuccess.ShouldBeTrue();
result.Value.Id.ShouldBe(100);
result.Value.Name.ShouldBe("Copy of Search 42");
}
// Representative error tests
[Fact]
public async Task GetSearchAsync_404_ReturnsNotFound()
{
// Arrange
_mockHttp.When(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.GetById(999)}")
.Respond(HttpStatusCode.NotFound);
// Act
var result = await _client.GetSearchAsync(999);
// Assert
result.IsNotFound.ShouldBeTrue();
}
[Fact]
public async Task GetUserSearchesAsync_401_ReturnsUnauthorized()
{
// Arrange
_mockHttp.When(HttpMethod.Get, $"http://localhost/{ApiRoutes.Search.Base}")
.Respond(HttpStatusCode.Unauthorized);
// Act
var result = await _client.GetUserSearchesAsync();
// Assert
result.IsUnauthorized.ShouldBeTrue();
}
private static SearchViewModel CreateTestSearch(int id) => new()
{
Id = id,
Name = $"Search {id}",
UserName = "testuser",
Status = Core.Models.Enums.SearchStatus.New
};
}