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; 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(); 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()); } 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(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(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(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 GetAsync_non_generic_with_unknown_profile_throws_not_found() { var (client, scope) = CreateScopedClient(); using var _ = scope; var ex = Assert.ThrowsAsync(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(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(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")); } }