monitor/api/src/Sync/CurlCodebergClient.php
fds2610-bot 09a3b928ab Vier fehlschlagende Tests behoben
1. AgentRepository::updateActivity() schrieb current_ticket_id ungeprueft. Eine
   unbekannte Ticket-ID lief in die FOREIGN-KEY-Constraint und kam als
   ungefangene PDOException samt Stacktrace und Dateipfaden beim Client an.
   Jetzt wird die Existenz vorab geprueft (422). Zusaetzlich faengt
   Application::handle() unerwartete Throwables ab und antwortet mit einem
   generischen 500 - Details gehen nur noch ins Log.

2. Die Normalisierung der Codeberg-Label-Farbe ("#" davor) lag im
   CurlCodebergClient statt im CodebergSyncService. Damit haetten andere
   Client-Implementierungen unterschiedliche Farbformate ins Ticket geschrieben.
   Verschoben in die Domaenenschicht, leere Farben ergeben jetzt null.

3. Config::get() griff selbst auf getenv() zurueck, wodurch ein in der Umgebung
   gesetzter CODEBERG_TOKEN in eine explizit gebaute Config durchschlug - im
   Testlauf landete der Token im Klartext in der PHPUnit-Ausgabe. Die Umgebung
   wird jetzt ausschliesslich in fromEnvironment() eingesammelt.

Regressionstests fuer alle drei Punkte ergaenzt: 104 Tests, 181 Assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:17:31 +02:00

93 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Monitor\Api\Sync;
use RuntimeException;
final class CurlCodebergClient implements CodebergClient
{
private const PER_PAGE = 50;
private const MAX_PAGES = 20;
public function __construct(
private readonly string $apiBase,
private readonly ?string $token = null,
) {
}
public function listIssues(string $owner, string $repo, string $state): array
{
$issues = [];
for ($page = 1; $page <= self::MAX_PAGES; $page++) {
$url = sprintf(
'%s/repos/%s/%s/issues?state=%s&type=issues&page=%d&limit=%d',
$this->apiBase,
rawurlencode($owner),
rawurlencode($repo),
rawurlencode($state),
$page,
self::PER_PAGE
);
$pageIssues = $this->fetch($url);
if ($pageIssues === []) {
break;
}
foreach ($pageIssues as $issue) {
$issues[] = [
'number' => (int) $issue['number'],
'title' => (string) $issue['title'],
'body' => isset($issue['body']) ? (string) $issue['body'] : null,
'labels' => array_map(
static fn (array $label): array => [
'name' => (string) $label['name'],
'color' => (string) $label['color'],
],
$issue['labels'] ?? []
),
];
}
if (count($pageIssues) < self::PER_PAGE) {
break;
}
}
return $issues;
}
/** @return array<int, array<string, mixed>> */
private function fetch(string $url): array
{
$headers = ['Accept: application/json'];
if ($this->token !== null && $this->token !== '') {
$headers[] = 'Authorization: token ' . $this->token;
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 15,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($body === false) {
throw new RuntimeException('Codeberg-API nicht erreichbar: ' . $error);
}
if ($status >= 400) {
throw new RuntimeException('Codeberg-API-Fehler (HTTP ' . $status . '): ' . $body);
}
$decoded = json_decode((string) $body, true);
return is_array($decoded) ? $decoded : [];
}
}