Merge pull request 'Tickets eines entfernten Projekts aus dem Kanban entfernen' (#48) from fds2610-bot/monitor:main into main
Reviewed-on: https://codeberg.org/fds2610/monitor/pulls/48
This commit is contained in:
commit
05fdef1525
8 changed files with 80 additions and 11 deletions
2
API.md
2
API.md
|
|
@ -335,7 +335,7 @@ bisherigen Default-Projekte `fds2610/monitor` (gelb), `fds2610/Steuerkram` (grue
|
||||||
| `GET /api/v1/repo-configs` | - | Liste aller konfigurierten Projekte, sortiert nach `sort_order` |
|
| `GET /api/v1/repo-configs` | - | Liste aller konfigurierten Projekte, sortiert nach `sort_order` |
|
||||||
| `POST /api/v1/repo-configs` | `X-Admin-Key` | Legt ein Projekt an (`owner`, `repo`, `color` als 6-stelliger Hex-Wert, mit oder ohne `#`) |
|
| `POST /api/v1/repo-configs` | `X-Admin-Key` | Legt ein Projekt an (`owner`, `repo`, `color` als 6-stelliger Hex-Wert, mit oder ohne `#`) |
|
||||||
| `PATCH /api/v1/repo-configs/{id}` | `X-Admin-Key` | Aendert `owner`/`repo`/`color` eines Projekts |
|
| `PATCH /api/v1/repo-configs/{id}` | `X-Admin-Key` | Aendert `owner`/`repo`/`color` eines Projekts |
|
||||||
| `DELETE /api/v1/repo-configs/{id}` | `X-Admin-Key` | Entfernt ein Projekt (der Sync liest es danach nicht mehr) |
|
| `DELETE /api/v1/repo-configs/{id}` | `X-Admin-Key` | Entfernt ein Projekt (der Sync liest es danach nicht mehr) und loescht dessen Tickets aus dem Kanban (Issue #43) |
|
||||||
|
|
||||||
`owner`+`repo` muessen zusammen eindeutig sein (409 -> hier als `422` mit Fehlermeldung, da
|
`owner`+`repo` muessen zusammen eindeutig sein (409 -> hier als `422` mit Fehlermeldung, da
|
||||||
kein Konflikt-Statuscode im Fehlerformat vorgesehen ist). Der laufende `sync`-Dienst
|
kein Konflikt-Statuscode im Fehlerformat vorgesehen ist). Der laufende `sync`-Dienst
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ final class Application
|
||||||
$eventController = new EventController($events);
|
$eventController = new EventController($events);
|
||||||
$systemController = new SystemController($systems, new LegacyStatusFileReader(), $config->statusFilePath());
|
$systemController = new SystemController($systems, new LegacyStatusFileReader(), $config->statusFilePath());
|
||||||
$kpiController = new KpiController($kpis);
|
$kpiController = new KpiController($kpis);
|
||||||
$repoConfigController = new RepoConfigController($repoConfigs);
|
$repoConfigController = new RepoConfigController($repoConfigs, $tickets);
|
||||||
$webhookController = new WebhookController(new SyncTrigger($config->syncTriggerPath()), $hooks);
|
$webhookController = new WebhookController(new SyncTrigger($config->syncTriggerPath()), $hooks);
|
||||||
$streamController = new StreamController(
|
$streamController = new StreamController(
|
||||||
new ChangeFeed($tickets, $events, $systems, $kpis, $agents, $hooks),
|
new ChangeFeed($tickets, $events, $systems, $kpis, $agents, $hooks),
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,14 @@ use InvalidArgumentException;
|
||||||
use Monitor\Api\Http\Request;
|
use Monitor\Api\Http\Request;
|
||||||
use Monitor\Api\Http\Response;
|
use Monitor\Api\Http\Response;
|
||||||
use Monitor\Api\Repository\RepoConfigRepository;
|
use Monitor\Api\Repository\RepoConfigRepository;
|
||||||
|
use Monitor\Api\Repository\TicketRepository;
|
||||||
|
|
||||||
final class RepoConfigController
|
final class RepoConfigController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly RepoConfigRepository $repoConfigs)
|
public function __construct(
|
||||||
{
|
private readonly RepoConfigRepository $repoConfigs,
|
||||||
|
private readonly TicketRepository $tickets,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
|
|
@ -43,10 +46,16 @@ final class RepoConfigController
|
||||||
return Response::json(['repo' => $repo]);
|
return Response::json(['repo' => $repo]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, string> $params */
|
/**
|
||||||
|
* Issue #43: Nach dem Entfernen eines Projekts sollen auch dessen Tickets aus dem Kanban
|
||||||
|
* verschwinden statt (wie zuvor, Issue #34) nur ihre Projektfarbe zu verlieren.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $params
|
||||||
|
*/
|
||||||
public function delete(Request $request, array $params): Response
|
public function delete(Request $request, array $params): Response
|
||||||
{
|
{
|
||||||
$this->repoConfigs->delete((int) $params['id']);
|
$repo = $this->repoConfigs->delete((int) $params['id']);
|
||||||
|
$this->tickets->deleteByRepo($repo['owner'] . '/' . $repo['repo']);
|
||||||
|
|
||||||
return Response::json(['deleted' => true]);
|
return Response::json(['deleted' => true]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,14 +84,22 @@ final class RepoConfigRepository
|
||||||
return $this->find($id);
|
return $this->find($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(int $id): void
|
/**
|
||||||
|
* @return array<string, mixed> - der geloeschte Eintrag, damit der Aufrufer (Issue #43:
|
||||||
|
* RepoConfigController::delete() raeumt anschliessend die Tickets des Projekts auf) dessen
|
||||||
|
* owner/repo kennt, ohne ihn selbst erneut nachzuschlagen.
|
||||||
|
*/
|
||||||
|
public function delete(int $id): array
|
||||||
{
|
{
|
||||||
if ($this->find($id) === null) {
|
$repo = $this->find($id);
|
||||||
|
if ($repo === null) {
|
||||||
throw new NotFoundException('Projekt nicht gefunden: ' . $id);
|
throw new NotFoundException('Projekt nicht gefunden: ' . $id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->db->pdo()->prepare('DELETE FROM repo_configs WHERE id = :id');
|
$stmt = $this->db->pdo()->prepare('DELETE FROM repo_configs WHERE id = :id');
|
||||||
$stmt->execute(['id' => $id]);
|
$stmt->execute(['id' => $id]);
|
||||||
|
|
||||||
|
return $repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,19 @@ final class TicketRepository
|
||||||
$stmt->execute($slugs);
|
$stmt->execute($slugs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entfernt alle Tickets eines Projekts vollstaendig aus dem Kanban (Issue #43: bislang
|
||||||
|
* blieben Tickets eines ueber das Projekte-Modal geloeschten Projekts sichtbar, nur die
|
||||||
|
* Farbe wurde ueber clearColorForReposNotIn() geloescht). Wird von
|
||||||
|
* RepoConfigController::delete() direkt nach dem Entfernen des repo_configs-Eintrags
|
||||||
|
* aufgerufen, nicht erst beim naechsten periodischen Sync.
|
||||||
|
*/
|
||||||
|
public function deleteByRepo(string $repo): void
|
||||||
|
{
|
||||||
|
$stmt = $this->db->pdo()->prepare('DELETE FROM tickets WHERE repo = :repo');
|
||||||
|
$stmt->execute(['repo' => $repo]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Billiger "Fingerabdruck" (Anzahl + juengster Aenderungszeitpunkt) fuer den SSE-Endpoint
|
* Billiger "Fingerabdruck" (Anzahl + juengster Aenderungszeitpunkt) fuer den SSE-Endpoint
|
||||||
* (Realtime\ChangeFeed, F13) - aendert sich bei jedem Anlegen/Verschieben eines Tickets.
|
* (Realtime\ChangeFeed, F13) - aendert sich bei jedem Anlegen/Verschieben eines Tickets.
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,21 @@ use Monitor\Api\Controller\RepoConfigController;
|
||||||
use Monitor\Api\Db\Database;
|
use Monitor\Api\Db\Database;
|
||||||
use Monitor\Api\Http\Request;
|
use Monitor\Api\Http\Request;
|
||||||
use Monitor\Api\Repository\RepoConfigRepository;
|
use Monitor\Api\Repository\RepoConfigRepository;
|
||||||
|
use Monitor\Api\Repository\TicketRepository;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
final class RepoConfigControllerTest extends TestCase
|
final class RepoConfigControllerTest extends TestCase
|
||||||
{
|
{
|
||||||
private RepoConfigController $controller;
|
private RepoConfigController $controller;
|
||||||
|
private TicketRepository $tickets;
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$db = Database::inMemory();
|
$db = Database::inMemory();
|
||||||
$db->pdo()->exec('DELETE FROM repo_configs');
|
$db->pdo()->exec('DELETE FROM repo_configs');
|
||||||
$this->controller = new RepoConfigController(new RepoConfigRepository($db));
|
$db->pdo()->exec('DELETE FROM tickets');
|
||||||
|
$this->tickets = new TicketRepository($db);
|
||||||
|
$this->controller = new RepoConfigController(new RepoConfigRepository($db), $this->tickets);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testIndexReturnsEmptyListInitially(): void
|
public function testIndexReturnsEmptyListInitially(): void
|
||||||
|
|
@ -79,4 +83,24 @@ final class RepoConfigControllerTest extends TestCase
|
||||||
self::assertSame(200, $response->status);
|
self::assertSame(200, $response->status);
|
||||||
self::assertTrue($response->body()['deleted']);
|
self::assertTrue($response->body()['deleted']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDeleteRemovesTicketsOfTheProjectFromTheKanban(): void
|
||||||
|
{
|
||||||
|
$created = $this->controller->create(Request::create('POST', '/api/v1/repo-configs', body: [
|
||||||
|
'owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#fbca04',
|
||||||
|
]))->body()['repo'];
|
||||||
|
|
||||||
|
$orphaned = $this->tickets->create(['title' => 'A', 'repo' => 'fds2610/monitor']);
|
||||||
|
$unrelated = $this->tickets->create(['title' => 'B', 'repo' => 'fds2610/anderes-projekt']);
|
||||||
|
$manual = $this->tickets->create(['title' => 'C']);
|
||||||
|
|
||||||
|
$this->controller->delete(
|
||||||
|
Request::create('DELETE', '/api/v1/repo-configs/' . $created['id']),
|
||||||
|
['id' => (string) $created['id']]
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertNull($this->tickets->find((int) $orphaned['id']));
|
||||||
|
self::assertNotNull($this->tickets->find((int) $unrelated['id']));
|
||||||
|
self::assertNotNull($this->tickets->find((int) $manual['id']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,13 +81,15 @@ final class RepoConfigRepositoryTest extends TestCase
|
||||||
$this->repository->update(999, ['owner' => 'a', 'repo' => 'b', 'color' => '#000000']);
|
$this->repository->update(999, ['owner' => 'a', 'repo' => 'b', 'color' => '#000000']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDeleteRemovesEntry(): void
|
public function testDeleteRemovesEntryAndReturnsIt(): void
|
||||||
{
|
{
|
||||||
$repo = $this->repository->create(['owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#fbca04']);
|
$repo = $this->repository->create(['owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#fbca04']);
|
||||||
|
|
||||||
$this->repository->delete((int) $repo['id']);
|
$deleted = $this->repository->delete((int) $repo['id']);
|
||||||
|
|
||||||
self::assertSame([], $this->repository->all());
|
self::assertSame([], $this->repository->all());
|
||||||
|
self::assertSame('fds2610', $deleted['owner']);
|
||||||
|
self::assertSame('monitor', $deleted['repo']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDeleteUnknownIdThrowsNotFound(): void
|
public function testDeleteUnknownIdThrowsNotFound(): void
|
||||||
|
|
|
||||||
|
|
@ -193,4 +193,17 @@ final class TicketRepositoryTest extends TestCase
|
||||||
|
|
||||||
self::assertNull($this->repository->find((int) $ticket['id'])['color']);
|
self::assertNull($this->repository->find((int) $ticket['id'])['color']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDeleteByRepoRemovesOnlyTicketsOfThatRepo(): void
|
||||||
|
{
|
||||||
|
$removed = $this->repository->create(['title' => 'A', 'repo' => 'fds2610/altes-projekt']);
|
||||||
|
$kept = $this->repository->create(['title' => 'B', 'repo' => 'fds2610/monitor']);
|
||||||
|
$manual = $this->repository->create(['title' => 'C']);
|
||||||
|
|
||||||
|
$this->repository->deleteByRepo('fds2610/altes-projekt');
|
||||||
|
|
||||||
|
self::assertNull($this->repository->find((int) $removed['id']));
|
||||||
|
self::assertNotNull($this->repository->find((int) $kept['id']));
|
||||||
|
self::assertNotNull($this->repository->find((int) $manual['id']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue