Initial Commit

This commit is contained in:
Elijah 2026-07-19 13:10:55 -07:00
commit 86fc06b877
37 changed files with 3424 additions and 0 deletions

View file

@ -0,0 +1,40 @@
using System.Text.Json;
using Decky.Core.Api.Models;
using Decky.Core.Api.Requests;
namespace Decky.Core.Tests;
public sealed class CardUpdateRequestTests
{
[Fact]
public void FromCard_preserves_fields_and_serializes_explicit_null_done()
{
var card = new CardDto
{
Id = 7,
StackId = 3,
Title = "Original",
Description = "Details",
Type = "plain",
Owner = "elijah",
Order = 42,
Archived = false,
Done = DateTimeOffset.Parse("2026-07-19T12:00:00Z"),
DueDate = DateTimeOffset.Parse("2026-07-20T12:00:00Z"),
StartDate = DateTimeOffset.Parse("2026-07-18T12:00:00Z"),
Color = "d99080",
};
var request = CardUpdateRequest.FromCard(card, title: "Changed", done: null, replaceDone: true);
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var document = JsonDocument.Parse(json);
var root = document.RootElement;
Assert.Equal("Changed", root.GetProperty("title").GetString());
Assert.Equal("elijah", root.GetProperty("owner").GetString());
Assert.Equal("D99080", root.GetProperty("color").GetString());
Assert.Equal("2026-07-20T12:00:00+00:00", root.GetProperty("duedate").GetString());
Assert.Equal(JsonValueKind.Null, root.GetProperty("done").ValueKind);
}
}

View file

@ -0,0 +1,90 @@
using System.Net;
using System.Text;
using Decky.Core.Api;
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<ArgumentException>(() =>
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<string>();
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);
}
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 sealed class StubHttpMessageHandler(
Func<HttpRequestMessage, HttpResponseMessage> responseFactory) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) => Task.FromResult(responseFactory(request));
}
}

View file

@ -0,0 +1,24 @@
using Decky.Core.Utilities;
namespace Decky.Core.Tests;
public sealed class DeckColorTests
{
[Theory]
[InlineData("#d99080", "D99080")]
[InlineData(" 7FBC9F ", "7FBC9F")]
public void Normalize_returns_api_hex(string input, string expected)
{
Assert.Equal(expected, DeckColor.Normalize(input));
}
[Theory]
[InlineData("FFFFFF", true)]
[InlineData("000000", false)]
[InlineData("D99080", true)]
public void UseDarkText_uses_relative_luminance(string color, bool expected)
{
Assert.Equal(expected, DeckColor.UseDarkText(color));
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Decky.Core\Decky.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,75 @@
using Decky.Core.Models;
using Decky.Core.Services;
namespace Decky.Core.Tests;
public sealed class JsonSettingsServiceTests : IDisposable
{
private readonly string _testDirectory = Path.Combine(
Path.GetTempPath(),
$"Decky.Tests.{Guid.NewGuid():N}");
[Fact]
public async Task LoadAsync_returns_defaults_when_file_does_not_exist()
{
using var service = CreateService();
var settings = await service.LoadAsync();
Assert.Equal(30, settings.RefreshIntervalSeconds);
Assert.Empty(settings.OpenBoardTabs);
}
[Fact]
public async Task SaveAsync_round_trips_connection_and_board_settings()
{
using var service = CreateService();
var expected = new AppSettings
{
ServerUrl = "https://cloud.example.test/",
Username = "elijah",
ApiPath = "index.php/apps/deck/api/v1.0/",
OpenBoardTabs = [17],
Boards = new Dictionary<int, BoardViewSettings>
{
[17] = new() { Orientation = BoardOrientation.Vertical },
},
};
await service.SaveAsync(expected);
var actual = await service.LoadAsync();
Assert.Equal(expected.ServerUrl, actual.ServerUrl);
Assert.Equal(expected.Username, actual.Username);
Assert.Equal(expected.ApiPath, actual.ApiPath);
Assert.Equal([17], actual.OpenBoardTabs);
Assert.Equal(BoardOrientation.Vertical, actual.Boards[17].Orientation);
Assert.DoesNotContain("password", await File.ReadAllTextAsync(GetSettingsPath()), StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task LoadAsync_preserves_corrupt_file_and_returns_defaults()
{
Directory.CreateDirectory(_testDirectory);
await File.WriteAllTextAsync(GetSettingsPath(), "{ definitely not json }");
using var service = CreateService();
var settings = await service.LoadAsync();
Assert.Equal(string.Empty, settings.ServerUrl);
Assert.False(File.Exists(GetSettingsPath()));
Assert.Single(Directory.GetFiles(_testDirectory, "settings.corrupt-*.json"));
}
private JsonSettingsService CreateService() => new(GetSettingsPath());
private string GetSettingsPath() => Path.Combine(_testDirectory, "settings.json");
public void Dispose()
{
if (Directory.Exists(_testDirectory))
{
Directory.Delete(_testDirectory, recursive: true);
}
}
}

View file

@ -0,0 +1 @@
global using Xunit;