feat(worker): BandelTicketClient fuer die Ticketsystem-REST-API

This commit is contained in:
mika kuns
2026-08-27 12:42:22 +02:00
parent e9dfea905f
commit 1065278021
6 changed files with 302 additions and 0 deletions
@@ -0,0 +1,102 @@
using System.Net;
using ClaudeDo.Worker.Tickets;
using Microsoft.Extensions.Logging.Abstractions;
namespace ClaudeDo.Worker.Tests.Tickets;
public sealed class BandelTicketClientTests
{
private static BandelTicketClient Make(StubHandler handler)
=> new(new HttpClient(handler), "http://api.local", "tsp_x", NullLogger<BandelTicketClient>.Instance);
[Fact]
public async Task GetIdentity_returns_user_name()
{
var h = new StubHandler(_ => (HttpStatusCode.OK,
"""{"success":true,"data":{"userName":"mika.kuns","isPat":true,"scopes":["pat:board:read"]},"message":"ok"}"""));
var identity = await Make(h).GetIdentityAsync(CancellationToken.None);
Assert.Equal("mika.kuns", identity.UserName);
Assert.Contains("pat:board:read", identity.Scopes);
Assert.Equal("http://api.local/api/ticketsystem/pat/me", h.Requests[0].RequestUri!.ToString());
Assert.Equal("tsp_x", h.Requests[0].Headers.Authorization!.Parameter);
Assert.Equal("Bearer", h.Requests[0].Headers.Authorization!.Scheme);
}
[Fact]
public async Task GetProjectBoard_maps_items()
{
var h = new StubHandler(_ => (HttpStatusCode.OK,
"""{"success":true,"data":[{"id":12,"title":"Fix X","description":"d","statusId":1,"statusName":"Offen","assigneeID":7,"assigneeName":"mika.kuns"}],"message":"ok"}"""));
var items = await Make(h).GetProjectBoardAsync(393, CancellationToken.None);
var item = Assert.Single(items);
Assert.Equal(12, item.Id);
Assert.Equal("Fix X", item.Title);
Assert.Equal(1, item.StatusId);
Assert.Equal("mika.kuns", item.AssigneeName);
Assert.Equal("http://api.local/api/Board/project/393", h.Requests[0].RequestUri!.ToString());
}
[Fact]
public async Task SetStatus_patches_the_right_url_and_body()
{
var h = new StubHandler(_ => (HttpStatusCode.OK, """{"success":true,"data":{},"message":"ok"}"""));
await Make(h).SetStatusAsync(12, 2, CancellationToken.None);
Assert.Equal(HttpMethod.Patch, h.Requests[0].Method);
Assert.Equal("http://api.local/api/Ticket/12/status", h.Requests[0].RequestUri!.ToString());
Assert.Contains("\"statusId\":2", h.Bodies[0]);
}
[Fact]
public async Task Unauthorized_throws_with_a_readable_message()
{
var h = new StubHandler(_ => (HttpStatusCode.Unauthorized, ""));
var ex = await Assert.ThrowsAsync<TicketApiException>(
() => Make(h).GetIdentityAsync(CancellationToken.None));
Assert.Contains("401", ex.Message);
}
[Fact]
public async Task Forbidden_names_the_missing_scope_situation()
{
var h = new StubHandler(_ => (HttpStatusCode.Forbidden, ""));
var ex = await Assert.ThrowsAsync<TicketApiException>(
() => Make(h).GetProjectBoardAsync(1, CancellationToken.None));
Assert.Contains("403", ex.Message);
}
[Fact]
public async Task Success_false_in_the_envelope_throws()
{
var h = new StubHandler(_ => (HttpStatusCode.OK,
"""{"success":false,"data":null,"message":"Projekt nicht gefunden","errorCode":"NOT_FOUND"}"""));
var ex = await Assert.ThrowsAsync<TicketApiException>(
() => Make(h).GetProjectBoardAsync(999, CancellationToken.None));
Assert.Contains("Projekt nicht gefunden", ex.Message);
}
[Fact]
public async Task GetProjects_flattens_departments()
{
var h = new StubHandler(_ => (HttpStatusCode.OK,
"""{"success":true,"data":[{"departmentId":1,"departmentName":"Entwicklung","projects":[{"id":393,"title":"Bandel.LagerApp"},{"id":398,"title":"Bandel.Hub"}]}],"message":"ok"}"""));
var projects = await Make(h).GetProjectsAsync(CancellationToken.None);
Assert.Equal(2, projects.Count);
Assert.Equal(393, projects[0].Id);
Assert.Equal("Entwicklung", projects[0].DepartmentName);
Assert.Equal("Bandel.Hub", projects[1].Title);
}
}
@@ -0,0 +1,25 @@
using System.Net;
using System.Text;
namespace ClaudeDo.Worker.Tests.Tickets;
/// <summary>
/// Fakes the HTTP transport for BandelTicketClient tests. No test using this may touch the
/// network — reused by later tasks that also exercise the ticket client.
/// </summary>
internal sealed class StubHandler : HttpMessageHandler
{
public readonly List<HttpRequestMessage> Requests = new();
public readonly List<string> Bodies = new();
private readonly Func<HttpRequestMessage, (HttpStatusCode, string)> _respond;
public StubHandler(Func<HttpRequestMessage, (HttpStatusCode, string)> respond) => _respond = respond;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
Requests.Add(request);
Bodies.Add(request.Content is null ? "" : await request.Content.ReadAsStringAsync(ct));
var (code, body) = _respond(request);
return new HttpResponseMessage(code) { Content = new StringContent(body, Encoding.UTF8, "application/json") };
}
}