Updated `using` directives in `RecActionApiTests.cs` to alias `BatchRecActionViewResponse` as `ClientBatchResponse` for consistent usage. Replaced variable type `BatchRecActionViewResponse?` with `ClientBatchResponse?` in two instances. No functional changes were made to the `Assert.Pass` statements or the `InvokeAsync` method call.
272 lines
9.1 KiB
C#
272 lines
9.1 KiB
C#
using System.Net;
|
||
using System.Text.Json;
|
||
using Microsoft.Extensions.Configuration;
|
||
using ReC.Application.Common.Dto;
|
||
using ReC.Application.Common.Procedures.UpdateProcedure.Dto;
|
||
using ReC.Application.RecActions.Commands;
|
||
using ReC.Client;
|
||
using ReC.Client.Api;
|
||
using ClientInvokeReferences = ReC.Client.Api.InvokeReferences;
|
||
using ClientBatchResponse = ReC.Client.Api.BatchRecActionViewResponse;
|
||
|
||
namespace ReC.Tests.Client;
|
||
|
||
[TestFixture]
|
||
public class RecActionApiTests : RecClientTestBase
|
||
{
|
||
[Test]
|
||
public void ReCClient_is_resolvable_through_dependency_injection()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
Assert.That(client, Is.Not.Null);
|
||
Assert.That(client.RecActions, Is.Not.Null);
|
||
}
|
||
|
||
[Test]
|
||
public async Task GetAsync_without_filters_returns_deserialized_result_or_throws_not_found()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
try
|
||
{
|
||
var actions = await client.RecActions.GetAsync<RecActionViewDto[]>();
|
||
Assert.That(actions, Is.Not.Null);
|
||
}
|
||
catch (ReCApiException ex)
|
||
{
|
||
Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
||
Assert.That(ex.Method, Is.EqualTo("GET"));
|
||
Assert.That(ex.RequestUri!.AbsolutePath, Does.EndWith("api/RecAction"));
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public async Task GetAsync_non_generic_returns_dynamic_payload_or_throws_not_found()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
try
|
||
{
|
||
dynamic? actions = await client.RecActions.GetAsync();
|
||
// Server may return either a JsonElement (array or object) or nothing for empty payloads;
|
||
// either shape is acceptable, the call must just not have thrown.
|
||
if (actions is not null)
|
||
Assert.That(actions, Is.TypeOf<JsonElement>());
|
||
}
|
||
catch (ReCApiException ex)
|
||
{
|
||
// Any HTTP error here is acceptable - the SP behaviour for an unfiltered GET
|
||
// (no profile id) is not contractually defined when no data is present.
|
||
Assert.That(ex.Method, Is.EqualTo("GET"));
|
||
Assert.That(ex.RequestUri!.AbsolutePath, Does.EndWith("api/RecAction"));
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public async Task GetAsync_with_profile_filter_returns_only_matching_actions()
|
||
{
|
||
var profileId = await TryResolveProfileIdAsync();
|
||
if (profileId is null or <= 0)
|
||
Assert.Ignore("No profile available in the database for this test (set FakeProfileId or insert a profile).");
|
||
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
RecActionViewDto[]? actions;
|
||
try
|
||
{
|
||
actions = await client.RecActions.GetAsync<RecActionViewDto[]>(profileId: profileId);
|
||
}
|
||
catch (ReCApiException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
|
||
{
|
||
Assert.Pass("NotFound is acceptable when test data is unavailable.");
|
||
return;
|
||
}
|
||
|
||
Assert.That(actions, Is.Not.Null.And.Not.Empty);
|
||
Assert.That(actions, Has.All.Matches<RecActionViewDto>(a => a.ProfileId == profileId));
|
||
}
|
||
|
||
[Test]
|
||
public void GetAsync_with_unknown_profile_throws_not_found()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
var ex = Assert.ThrowsAsync<ReCApiException>(async () =>
|
||
await client.RecActions.GetAsync<RecActionViewDto[]>(profileId: long.MaxValue));
|
||
|
||
Assert.That(ex, Is.Not.Null);
|
||
Assert.That(ex!.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
||
Assert.That(ex.Method, Is.EqualTo("GET"));
|
||
Assert.That(ex.RequestUri!.Query, Does.Contain("ProfileId="));
|
||
}
|
||
|
||
[Test]
|
||
public void GetAsync_non_generic_with_unknown_profile_throws_not_found()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
var ex = Assert.ThrowsAsync<ReCApiException>(async () =>
|
||
await client.RecActions.GetAsync(profileId: long.MaxValue));
|
||
|
||
Assert.That(ex, Is.Not.Null);
|
||
Assert.That(ex!.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
|
||
Assert.That(ex.Method, Is.EqualTo("GET"));
|
||
Assert.That(ex.RequestUri!.Query, Does.Contain("ProfileId="));
|
||
}
|
||
|
||
[Test]
|
||
public void CreateAsync_with_invalid_foreign_key_throws_ReCApiException()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
var payload = new InsertActionCommand
|
||
{
|
||
ProfileId = long.MaxValue,
|
||
EndpointId = long.MaxValue,
|
||
Active = true,
|
||
Sequence = 1
|
||
};
|
||
|
||
var ex = Assert.ThrowsAsync<ReCApiException>(async () => await client.RecActions.CreateAsync(payload));
|
||
Assert.That(ex, Is.Not.Null);
|
||
Assert.That((int)ex!.StatusCode, Is.GreaterThanOrEqualTo(400));
|
||
Assert.That(ex.Method, Is.EqualTo("POST"));
|
||
}
|
||
|
||
[Test]
|
||
public async Task UpdateAsync_with_unknown_id_throws_or_completes()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
var unknownId = long.MaxValue;
|
||
var payload = new UpdateActionDto
|
||
{
|
||
ProfileId = 1,
|
||
Active = false,
|
||
Sequence = 1
|
||
};
|
||
|
||
try
|
||
{
|
||
await client.RecActions.UpdateAsync(unknownId, payload);
|
||
Assert.Pass("Update completed (SP is idempotent for unknown id).");
|
||
}
|
||
catch (ReCApiException ex)
|
||
{
|
||
Assert.That(ex.Method, Is.EqualTo("PUT"));
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public async Task DeleteAsync_sends_payload_as_query_string_not_body()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
var payload = new DeleteActionCommand
|
||
{
|
||
Start = long.MaxValue - 1,
|
||
End = long.MaxValue,
|
||
Force = false
|
||
};
|
||
|
||
try
|
||
{
|
||
await client.RecActions.DeleteAsync(payload);
|
||
}
|
||
catch (ReCApiException ex)
|
||
{
|
||
Assert.That(ex.Method, Is.EqualTo("DELETE"));
|
||
Assert.That(ex.RequestUri!.Query, Does.Contain("Start=").And.Contains("End=").And.Contains("Force="));
|
||
}
|
||
}
|
||
|
||
[Test]
|
||
public void InvokeAsync_with_unknown_profile_throws_ReCApiException()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
var ex = Assert.ThrowsAsync<ReCApiException>(async () =>
|
||
await client.RecActions.InvokeAsync(long.MaxValue, new ClientInvokeReferences { BatchId = "test-batch" }));
|
||
|
||
Assert.That(ex, Is.Not.Null);
|
||
Assert.That(ex!.Method, Is.EqualTo("POST"));
|
||
}
|
||
|
||
[Test]
|
||
public async Task InvokeAsync_returns_BatchRecActionViewResponse_on_success()
|
||
{
|
||
var profileId = await TryResolveProfileIdAsync();
|
||
if (profileId is null or <= 0)
|
||
Assert.Ignore("No profile available in the database for this test (set FakeProfileId or insert a profile).");
|
||
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
ClientBatchResponse? result;
|
||
try
|
||
{
|
||
result = await client.RecActions.InvokeAsync(profileId!.Value, new ClientInvokeReferences { BatchId = "test-batch" });
|
||
}
|
||
catch (ReCApiException ex)
|
||
{
|
||
Assert.Pass($"API returned {ex.StatusCode} <20> acceptable when test data is unavailable.");
|
||
return;
|
||
}
|
||
|
||
Assert.That(result, Is.Not.Null);
|
||
Assert.That(result!.TotalActionCount, Is.GreaterThanOrEqualTo(0));
|
||
Assert.That(result.ActionExceptionCount, Is.GreaterThanOrEqualTo(0));
|
||
Assert.That(result.ActionExceptionCount, Is.LessThanOrEqualTo(result.TotalActionCount));
|
||
}
|
||
|
||
[Test]
|
||
public async Task InvokeAsync_with_batchId_string_returns_BatchRecActionViewResponse()
|
||
{
|
||
var profileId = await TryResolveProfileIdAsync();
|
||
if (profileId is null or <= 0)
|
||
Assert.Ignore("No profile available in the database for this test (set FakeProfileId or insert a profile).");
|
||
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
ClientBatchResponse? result;
|
||
try
|
||
{
|
||
result = await client.RecActions.InvokeAsync(profileId!.Value, "test-batch");
|
||
}
|
||
catch (ReCApiException ex)
|
||
{
|
||
Assert.Pass($"API returned {ex.StatusCode} <20> acceptable when test data is unavailable.");
|
||
return;
|
||
}
|
||
|
||
Assert.That(result, Is.Not.Null);
|
||
Assert.That(result!.TotalActionCount, Is.GreaterThanOrEqualTo(0));
|
||
}
|
||
|
||
[Test]
|
||
public void InvokeAsync_with_batchId_string_and_unknown_profile_throws_ReCApiException()
|
||
{
|
||
var (client, scope) = CreateScopedClient();
|
||
using var _ = scope;
|
||
|
||
var ex = Assert.ThrowsAsync<ReCApiException>(async () =>
|
||
await client.RecActions.InvokeAsync(long.MaxValue, "test-batch"));
|
||
|
||
Assert.That(ex, Is.Not.Null);
|
||
Assert.That(ex!.Method, Is.EqualTo("POST"));
|
||
Assert.That(ex.RequestUri!.AbsolutePath, Does.Contain("invoke"));
|
||
}
|
||
}
|