using System.Net; using System.Text; using Decky.Core.Api; using Decky.Core.Api.Requests; using Decky.Core.Configuration; namespace Decky.Core.Tests; public sealed class DeckApiClientTests { [Fact] public void Connection_options_reject_insecure_remote_servers() { var exception = Assert.Throws(() => DeckConnectionOptions.Create("http://cloud.example.test", "elijah", "app-password")); Assert.Contains("HTTPS", exception.Message, StringComparison.Ordinal); } [Fact] public async Task GetBoardsAsync_sends_required_headers_and_deserializes_response() { HttpRequestMessage? capturedRequest = null; var handler = new StubHttpMessageHandler(request => { capturedRequest = CloneRequest(request); return JsonResponse("[{\"id\":10,\"title\":\"Homework\",\"color\":\"ff0000\"}]"); }); using var httpClient = new HttpClient(handler); var options = DeckConnectionOptions.Create("https://cloud.example.test", "elijah", "app-password"); var client = new DeckApiClient( httpClient, options, new Uri("https://cloud.example.test/index.php/apps/deck/api/v1.0/")); var boards = await client.GetBoardsAsync(); Assert.Single(boards); Assert.Equal("Homework", boards[0].Title); Assert.NotNull(capturedRequest); Assert.Equal( "https://cloud.example.test/index.php/apps/deck/api/v1.0/boards", capturedRequest.RequestUri!.ToString()); Assert.Equal("true", capturedRequest.Headers.GetValues("OCS-APIRequest").Single()); Assert.Equal("Basic", capturedRequest.Headers.Authorization!.Scheme); } [Fact] public async Task ConnectAsync_falls_back_to_rewritten_path_after_not_found() { var requestedPaths = new List(); var handler = new StubHttpMessageHandler(request => { requestedPaths.Add(request.RequestUri!.AbsolutePath); return request.RequestUri.AbsolutePath.Contains("/index.php/", StringComparison.Ordinal) ? new HttpResponseMessage(HttpStatusCode.NotFound) : JsonResponse("[]"); }); using var httpClient = new HttpClient(handler); var options = DeckConnectionOptions.Create("https://cloud.example.test", "elijah", "app-password"); var client = await DeckApiClient.ConnectAsync(httpClient, options); Assert.Equal("https://cloud.example.test/apps/deck/api/v1.0/", client.ApiBaseUri.ToString()); Assert.Equal(2, requestedPaths.Count); } [Fact] public async Task ReorderCardAsync_fetches_destination_card_after_boolean_acknowledgement() { var handler = new StubHttpMessageHandler(request => { if (request.Method == HttpMethod.Put) { return JsonResponse("true"); } Assert.Equal( "/index.php/apps/deck/api/v1.0/boards/2/stacks/8/cards/11", request.RequestUri!.AbsolutePath); return JsonResponse("{\"id\":11,\"stackId\":8,\"title\":\"Moved\",\"owner\":\"admin\"}"); }); using var httpClient = new HttpClient(handler); var client = CreateClient(httpClient); var card = await client.ReorderCardAsync( 2, 7, 11, new ReorderCardRequest { Order = 0, StackId = 8 }); Assert.Equal(8, card.StackId); } [Fact] public async Task ReorderCardAsync_retries_with_destination_route_when_body_stack_was_ignored() { var putCount = 0; var handler = new StubHttpMessageHandler(request => { if (request.Method == HttpMethod.Put) { putCount++; if (putCount == 2) { Assert.Contains("/stacks/8/", request.RequestUri!.AbsolutePath, StringComparison.Ordinal); } return JsonResponse("true"); } if (request.RequestUri!.AbsolutePath.Contains("/stacks/8/", StringComparison.Ordinal)) { if (putCount == 2) { return JsonResponse("{\"id\":11,\"stackId\":8,\"title\":\"Moved\",\"owner\":\"admin\"}"); } return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("{\"status\":404}", Encoding.UTF8, "application/json"), }; } return JsonResponse("{\"id\":11,\"stackId\":7,\"title\":\"Not moved\",\"owner\":\"admin\"}"); }); using var httpClient = new HttpClient(handler); var client = CreateClient(httpClient); var card = await client.ReorderCardAsync( 2, 7, 11, new ReorderCardRequest { Order = 0, StackId = 8 }); Assert.Equal(8, card.StackId); Assert.Equal(2, putCount); } private static HttpResponseMessage JsonResponse(string json) => new(HttpStatusCode.OK) { Content = new StringContent(json, Encoding.UTF8, "application/json"), }; private static HttpRequestMessage CloneRequest(HttpRequestMessage request) { var clone = new HttpRequestMessage(request.Method, request.RequestUri); foreach (var header in request.Headers) { clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } return clone; } private static DeckApiClient CreateClient(HttpClient httpClient) { var options = DeckConnectionOptions.Create("https://cloud.example.test", "elijah", "app-password"); return new DeckApiClient( httpClient, options, new Uri("https://cloud.example.test/index.php/apps/deck/api/v1.0/")); } private sealed class StubHttpMessageHandler( Func responseFactory) : HttpMessageHandler { protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(responseFactory(request)); } }