monitor/api/tests/Unit/Controller/TicketControllerTest.php
fds2610-bot bd27923321 Reload-Knopf am Kanban-Board: Tickets komplett neu einlesen (Issue #51)
Manchmal werden bei Codeberg geloeschte oder anderweitig verschwundene Issues
vom periodischen Sync nicht bereinigt (er upsertet nur, ruehrt fehlende Issues
nie an). Neuer Knopf neben der "Kanban-Board"-Ueberschrift stoesst dafuer einen
synchronen Voll-Resync an (POST /api/v1/tickets/resync, X-Admin-Key): jedes
konfigurierte Projekt wird neu gelesen, Tickets zu inzwischen verschwundenen
Issues werden je Projekt entfernt (auch wenn sie manuell auf eine nicht vom
Sync verwaltete Spalte wie "fertig" bewegt wurden), Tickets zu weiterhin
existierenden Issues bleiben unangetastet.

Der PO geht dabei sichtbar ans Board und steht dort, bis die Anfrage
abgeschlossen ist - anders als beim automatischen Board-Besuch (feste
Animationsdauer je Stage) macht das die tatsaechliche Sync-Dauer sichtbar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 19:52:09 +02:00

134 lines
4.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Monitor\Api\Tests\Controller;
use Monitor\Api\Controller\TicketController;
use Monitor\Api\Db\Database;
use Monitor\Api\Http\NotFoundException;
use Monitor\Api\Http\Request;
use Monitor\Api\Repository\RepoConfigRepository;
use Monitor\Api\Repository\TicketRepository;
use Monitor\Api\Sync\CodebergClient;
use Monitor\Api\Sync\CodebergSyncService;
use PHPUnit\Framework\TestCase;
use RuntimeException;
/** Testdouble fuer die Resync-Tests unten - liefert nie echte Issues, kann aber auf Wunsch fehlschlagen. */
final class StubCodebergClient implements CodebergClient
{
public function __construct(private readonly bool $shouldFail = false)
{
}
public function listIssues(string $owner, string $repo, string $state): array
{
if ($this->shouldFail) {
throw new RuntimeException('Codeberg-API nicht erreichbar.');
}
return [];
}
}
final class TicketControllerTest extends TestCase
{
private TicketController $controller;
private TicketRepository $repository;
protected function setUp(): void
{
$this->repository = new TicketRepository(Database::inMemory());
$this->controller = new TicketController(
$this->repository,
new CodebergSyncService(new StubCodebergClient(), $this->repository, new RepoConfigRepository(Database::inMemory()))
);
}
public function testIndexReturnsAllTickets(): void
{
$this->repository->create(['title' => 'A']);
$response = $this->controller->index(Request::create('GET', '/api/v1/tickets'));
self::assertSame(200, $response->status);
self::assertCount(1, $response->body()['tickets']);
}
public function testCreateRejectsMissingTitle(): void
{
$response = $this->controller->create(Request::create('POST', '/api/v1/tickets', body: []));
self::assertSame(422, $response->status);
}
public function testCreateReturns201WithTicket(): void
{
$response = $this->controller->create(Request::create('POST', '/api/v1/tickets', body: ['title' => 'Neu']));
self::assertSame(201, $response->status);
self::assertSame('Neu', $response->body()['ticket']['title']);
}
public function testCreateRejectsInvalidColumnWith422(): void
{
$response = $this->controller->create(Request::create('POST', '/api/v1/tickets', body: ['title' => 'X', 'column' => 'invalid']));
self::assertSame(422, $response->status);
}
public function testUpdateMovesTicket(): void
{
$ticket = $this->repository->create(['title' => 'X']);
$response = $this->controller->update(
Request::create('PATCH', '/api/v1/tickets/' . $ticket['id'], body: ['column' => 'bereit']),
['id' => (string) $ticket['id']]
);
self::assertSame(200, $response->status);
self::assertSame('bereit', $response->body()['ticket']['column_name']);
}
public function testUpdatePropagatesNotFoundForUnknownTicket(): void
{
$this->expectException(NotFoundException::class);
$this->controller->update(
Request::create('PATCH', '/api/v1/tickets/999', body: ['column' => 'bereit']),
['id' => '999']
);
}
public function testResyncRemovesOrphanedTicketOfConfiguredRepoAndReturnsNewBoardState(): void
{
// 'fds2610/monitor' ist eines der per Migration seedenden Default-Projekte (siehe
// 003_repo_configs.sql) - der StubCodebergClient liefert dafuer keine Issues, das
// Ticket ist also eine Karteileiche im Sinne von Issue #51.
$this->repository->create([
'title' => 'Karteileiche',
'repo' => 'fds2610/monitor',
'external_ref' => '1',
'source' => 'codeberg',
]);
$response = $this->controller->resync(Request::create('POST', '/api/v1/tickets/resync'));
self::assertSame(200, $response->status);
self::assertSame([], $response->body()['tickets']);
}
public function testResyncReturns502WhenCodebergUnreachable(): void
{
$repository = new TicketRepository(Database::inMemory());
$controller = new TicketController(
$repository,
new CodebergSyncService(new StubCodebergClient(shouldFail: true), $repository, new RepoConfigRepository(Database::inMemory()))
);
$response = $controller->resync(Request::create('POST', '/api/v1/tickets/resync'));
self::assertSame(502, $response->status);
}
}