fix(worker): fix Bandel ticket sync and reactivate the integration
The status PATCH needs statusName alongside statusId or the API 400s, so every write-back had been silently failing. The board's flat assigneeName is null on every live ticket; the real assignee list is ticketAssignees[]. Import now also picks up In Bearbeitung tickets, not just Offen ones. Both issues are live-verified against the real API, so the feature flag flips back on.
This commit is contained in:
@@ -28,15 +28,20 @@ public sealed class TicketImportServiceTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Imports_only_open_tickets_assigned_to_the_token_owner()
|
||||
public async Task Imports_open_and_in_progress_tickets_assigned_to_the_token_owner()
|
||||
{
|
||||
var (import, ctx) = await HarnessAsync();
|
||||
await SeedExistingTaskAsync(ctx, listId: ListId, ticketRef: "bandel:4");
|
||||
|
||||
var result = await import.ImportAsync(ListId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(4, result.Examined);
|
||||
Assert.Equal(1, result.Created);
|
||||
Assert.Equal(7, result.Examined);
|
||||
Assert.Equal(2, result.Created);
|
||||
|
||||
var refs = await ctx.Tasks
|
||||
.Where(t => t.CreatedBy == "ticketsystem")
|
||||
.Select(t => t.TicketRef)
|
||||
.ToListAsync();
|
||||
Assert.Equal(["bandel:1", "bandel:3"], refs.Order());
|
||||
|
||||
var created = await ctx.Tasks.Where(t => t.TicketRef == "bandel:1").SingleAsync();
|
||||
Assert.Equal("Fix X", created.Title);
|
||||
@@ -45,6 +50,30 @@ public sealed class TicketImportServiceTests : IDisposable
|
||||
Assert.Contains("Ticket #1", created.Description);
|
||||
}
|
||||
|
||||
// Der eigentliche Fehler der ersten Fassung: gefiltert wurde auf das flache assigneeName,
|
||||
// das live auf jedem Ticket null ist. Ticket #5 hat es gesetzt, aber leere ticketAssignees —
|
||||
// wer wieder aufs flache Feld zurückfällt, importiert es und lässt diesen Test scheitern.
|
||||
[Fact]
|
||||
public async Task Flat_assigneeName_is_not_an_assignment()
|
||||
{
|
||||
var (import, ctx) = await HarnessAsync();
|
||||
|
||||
await import.ImportAsync(ListId, CancellationToken.None);
|
||||
|
||||
Assert.False(await ctx.Tasks.AnyAsync(t => t.TicketRef == "bandel:5"));
|
||||
Assert.False(await ctx.Tasks.AnyAsync(t => t.TicketRef == "bandel:7"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Finished_tickets_are_never_imported()
|
||||
{
|
||||
var (import, ctx) = await HarnessAsync();
|
||||
|
||||
await import.ImportAsync(ListId, CancellationToken.None);
|
||||
|
||||
Assert.False(await ctx.Tasks.AnyAsync(t => t.TicketRef == "bandel:6"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Assignee_match_is_case_insensitive()
|
||||
{
|
||||
@@ -53,7 +82,7 @@ public sealed class TicketImportServiceTests : IDisposable
|
||||
|
||||
var result = await import.ImportAsync(ListId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, result.Created);
|
||||
Assert.Equal(2, result.Created);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -64,7 +93,7 @@ public sealed class TicketImportServiceTests : IDisposable
|
||||
var first = await import.ImportAsync(ListId, CancellationToken.None);
|
||||
var second = await import.ImportAsync(ListId, CancellationToken.None);
|
||||
|
||||
Assert.Equal(1, first.Created);
|
||||
Assert.Equal(2, first.Created);
|
||||
Assert.Equal(0, second.Created);
|
||||
Assert.Equal(1, await ctx.Tasks.CountAsync(t => t.TicketRef == "bandel:1"));
|
||||
}
|
||||
@@ -106,11 +135,11 @@ public sealed class TicketImportServiceTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Board liefert immer vier Tickets: #1 offen/mein (importierbar), #2 offen/fremd (Assignee-
|
||||
/// Filter), #3 in Bearbeitung/mein (Status-Filter), #4 offen/mein aber bereits importiert
|
||||
/// (Dedup-Filter — hier vorab als bestehender Task angelegt, damit alle Default-Aufrufe
|
||||
/// dieselbe, in sich konsistente Ausgangslage haben; Test 1 legt ihn zusätzlich explizit an,
|
||||
/// was denselben Zustand nur redundant reproduziert).
|
||||
/// Board liefert immer sieben Tickets, je einer pro Filterzweig:
|
||||
/// #1 offen/mein → importierbar · #2 offen/fremd → Assignee-Filter ·
|
||||
/// #3 in Bearbeitung/mein → importierbar · #4 offen/mein, bereits importiert → Dedup
|
||||
/// (vorab als Task angelegt) · #5 offen, nur flaches assigneeName gesetzt → muss draußen
|
||||
/// bleiben · #6 fertig/mein → Status-Filter · #7 offen, ticketAssignees null → draußen.
|
||||
/// </summary>
|
||||
private async Task<(TicketImportService Import, ClaudeDoDbContext Ctx)> HarnessAsync(
|
||||
bool configured = true,
|
||||
@@ -149,12 +178,24 @@ public sealed class TicketImportServiceTests : IDisposable
|
||||
""");
|
||||
}
|
||||
|
||||
// Die flachen assigneeID/assigneeName sind hier absichtlich null — genau so liefert
|
||||
// die echte API sie auf JEDEM Ticket. Die Zuweisung steckt nur in ticketAssignees.
|
||||
return (HttpStatusCode.OK, $$"""
|
||||
{"success":true,"data":[
|
||||
{"id":1,"title":"Fix X","description":"d","statusId":1,"statusName":"Offen","assigneeID":7,"assigneeName":"{{boardAssignee}}"},
|
||||
{"id":2,"title":"Other","description":null,"statusId":1,"statusName":"Offen","assigneeID":8,"assigneeName":"jemand.anders"},
|
||||
{"id":3,"title":"InProgress","description":null,"statusId":2,"statusName":"InBearbeitung","assigneeID":7,"assigneeName":"{{boardAssignee}}"},
|
||||
{"id":4,"title":"AlreadyImported","description":null,"statusId":1,"statusName":"Offen","assigneeID":7,"assigneeName":"{{boardAssignee}}"}
|
||||
{"id":1,"title":"Fix X","description":"d","statusId":1,"statusName":"Offen","assigneeID":null,"assigneeName":null,
|
||||
"ticketAssignees":[{"assigneeId":26,"assigneeName":"{{boardAssignee}}"}]},
|
||||
{"id":2,"title":"Other","description":null,"statusId":1,"statusName":"Offen","assigneeID":null,"assigneeName":null,
|
||||
"ticketAssignees":[{"assigneeId":8,"assigneeName":"jemand.anders"}]},
|
||||
{"id":3,"title":"InProgress","description":null,"statusId":2,"statusName":"In Bearbeitung","assigneeID":null,"assigneeName":null,
|
||||
"ticketAssignees":[{"assigneeId":8,"assigneeName":"jemand.anders"},{"assigneeId":26,"assigneeName":"{{boardAssignee}}"}]},
|
||||
{"id":4,"title":"AlreadyImported","description":null,"statusId":1,"statusName":"Offen","assigneeID":null,"assigneeName":null,
|
||||
"ticketAssignees":[{"assigneeId":26,"assigneeName":"{{boardAssignee}}"}]},
|
||||
{"id":5,"title":"FlatAssigneeOnly","description":null,"statusId":1,"statusName":"Offen","assigneeID":26,"assigneeName":"{{boardAssignee}}",
|
||||
"ticketAssignees":[]},
|
||||
{"id":6,"title":"Done","description":null,"statusId":3,"statusName":"Fertig","assigneeID":null,"assigneeName":null,
|
||||
"ticketAssignees":[{"assigneeId":26,"assigneeName":"{{boardAssignee}}"}]},
|
||||
{"id":7,"title":"Unassigned","description":null,"statusId":1,"statusName":"Offen","assigneeID":null,"assigneeName":null,
|
||||
"ticketAssignees":null}
|
||||
],"message":"ok"}
|
||||
""");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user