fix: SonarQube - SentryService, RgpdService, FacturePdf, DevisPdf

SentryService :
- PATH_PROJECTS, PATH_ISSUES constantes (9 occurrences)
- request() 5->3 returns via executeRequest()

RgpdService :
- verifyCode() 5->2 returns (logique booleenne unifiee)

FacturePdf :
- Suppression appendRib() et displayHmac() inutilisees
- Catch vide commente (CGV best-effort)

DevisPdf :
- Catch vide commente (CGV best-effort)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-08 14:39:08 +02:00
parent c119b88192
commit 4892b721c3
5 changed files with 46 additions and 165 deletions

View File

@@ -204,6 +204,7 @@ class DevisPdf extends Fpdi
@unlink($tmpCgv);
} catch (\Throwable) {
// CGV generation is best-effort
}
}

View File

@@ -214,50 +214,10 @@ class FacturePdf extends Fpdi
@unlink($tmpCgv);
} catch (\Throwable) { // @codeCoverageIgnore
// CGV generation is best-effort
}
}
/**
* Importe les pages de public/rib.pdf apres les CGV.
*
* @codeCoverageIgnore
*
* @phpstan-ignore-next-line
*/
private function appendRib(): void
{
$ribPath = $this->kernel->getProjectDir().'/public/rib.pdf';
if (!file_exists($ribPath)) {
return;
}
try {
$pageCount = $this->setSourceFile($ribPath);
for ($i = 1; $i <= $pageCount; ++$i) {
$tpl = $this->importPage($i);
$size = $this->getTemplateSize($tpl);
$this->AddPage($size['orientation'] ?? 'P', [$size['width'], $size['height']]);
$this->useTemplate($tpl);
}
} catch (\Throwable) {
}
}
/**
* @codeCoverageIgnore
*
* @phpstan-ignore-next-line
*/
private function displayHmac(): void
{
$this->Ln(6);
$this->SetFont('Arial', '', 6);
$this->SetTextColor(150, 150, 150);
$this->Cell(190, 4, $this->enc('Signature HMAC-SHA256 : '.$this->facture->getHmac()), 0, 1, 'L');
$this->SetTextColor(0, 0, 0);
}
private function displayQrCode(): void
{
if ('' === $this->qrBase64) {

View File

@@ -99,28 +99,16 @@ class RgpdService
}
$data = json_decode(file_get_contents($codePath), true);
if (null === $data) {
$isValid = null !== $data
&& time() <= ($data['expires'] ?? 0)
&& $code === ($data['code'] ?? '');
// Supprime le fichier si invalide (expire/corrompu) ou valide (usage unique)
if (null === $data || time() > ($data['expires'] ?? 0) || $isValid) {
@unlink($codePath);
return false;
}
// Expire ?
if (time() > ($data['expires'] ?? 0)) {
@unlink($codePath);
return false;
}
// Code correct ?
if ($code !== ($data['code'] ?? '')) {
return false;
}
// Supprime le fichier (usage unique)
@unlink($codePath);
return true;
return $isValid;
}
private function getCodeFilePath(string $email, string $ip, string $type): string

View File

@@ -13,6 +13,9 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
*/
class SentryService
{
private const PATH_PROJECTS = '/projects/';
private const PATH_ISSUES = '/issues/';
public function __construct(
private HttpClientInterface $httpClient,
private LoggerInterface $logger,
@@ -36,14 +39,14 @@ class SentryService
{
$org = $this->resolveOrg($org);
return $this->request('POST', '/teams/'.$org.'/'.$teamSlug.'/projects/', ['name' => $name]);
return $this->request('POST', '/teams/'.$org.'/'.$teamSlug.self::PATH_PROJECTS, ['name' => $name]);
}
public function deleteProject(string $projectSlug, string $org = ''): bool
{
$org = $this->resolveOrg($org);
return null !== $this->request('DELETE', '/projects/'.$org.'/'.$projectSlug.'/');
return null !== $this->request('DELETE', self::PATH_PROJECTS.$org.'/'.$projectSlug.'/');
}
/**
@@ -52,7 +55,7 @@ class SentryService
public function getProjectDsn(string $projectSlug, string $org = ''): ?string
{
$org = $this->resolveOrg($org);
$result = $this->request('GET', '/projects/'.$org.'/'.$projectSlug.'/keys/');
$result = $this->request('GET', self::PATH_PROJECTS.$org.'/'.$projectSlug.'/keys/');
if (null === $result || !isset($result[0]['dsn']['public'])) {
return null;
@@ -71,7 +74,7 @@ class SentryService
$org = $this->resolveOrg($org);
/* @var list<array<string, mixed>> */
return $this->request('GET', '/organizations/'.$org.'/projects/') ?? [];
return $this->request('GET', '/organizations/'.$org.self::PATH_PROJECTS) ?? [];
}
/**
@@ -82,7 +85,7 @@ class SentryService
public function listProjectErrors(string $projectSlug, ?string $query = null, string $org = ''): array
{
$org = $this->resolveOrg($org);
$path = '/projects/'.$org.'/'.$projectSlug.'/issues/';
$path = self::PATH_PROJECTS.$org.'/'.$projectSlug.self::PATH_ISSUES;
if (null !== $query && '' !== $query) {
$path .= '?query='.urlencode($query);
@@ -99,7 +102,7 @@ class SentryService
*/
public function getIssueDetails(string $issueId): ?array
{
return $this->request('GET', '/issues/'.$issueId.'/');
return $this->request('GET', self::PATH_ISSUES.$issueId.'/');
}
/**
@@ -107,7 +110,7 @@ class SentryService
*/
public function resolveIssue(string $issueId): bool
{
return null !== $this->request('PUT', '/issues/'.$issueId.'/', ['status' => 'resolved']);
return null !== $this->request('PUT', self::PATH_ISSUES.$issueId.'/', ['status' => 'resolved']);
}
/**
@@ -115,7 +118,7 @@ class SentryService
*/
public function deleteIssue(string $issueId): bool
{
return null !== $this->request('DELETE', '/issues/'.$issueId.'/');
return null !== $this->request('DELETE', self::PATH_ISSUES.$issueId.'/');
}
/**
@@ -132,44 +135,38 @@ class SentryService
}
try {
$options = [
'headers' => [
'Authorization' => 'Bearer '.$this->authToken,
'Content-Type' => 'application/json',
],
];
if (null !== $body) {
$options['json'] = $body;
}
$response = $this->httpClient->request($method, rtrim($this->apiUrl, '/').$path, $options);
$statusCode = $response->getStatusCode();
if ($statusCode >= 400) {
$this->logger->error('SentryService: HTTP '.$statusCode.' '.$method.' '.$path, [
'body' => $response->getContent(false),
]);
return null;
}
// DELETE retourne 204 sans body
if (204 === $statusCode || '' === $response->getContent(false)) {
return [];
}
return $response->toArray();
return $this->executeRequest($method, $path, $body);
} catch (\Throwable $e) {
$this->logger->error('SentryService: '.$e->getMessage(), [
'method' => $method,
'path' => $path,
]);
$this->logger->error('SentryService: '.$e->getMessage(), ['method' => $method, 'path' => $path]);
return null;
}
}
/**
* @param array<string, mixed>|null $body
*
* @return array<int|string, mixed>|null
*/
private function executeRequest(string $method, string $path, ?array $body): ?array
{
$options = ['headers' => ['Authorization' => 'Bearer '.$this->authToken, 'Content-Type' => 'application/json']];
if (null !== $body) {
$options['json'] = $body;
}
$response = $this->httpClient->request($method, rtrim($this->apiUrl, '/').$path, $options);
$statusCode = $response->getStatusCode();
if ($statusCode >= 400) {
$this->logger->error('SentryService: HTTP '.$statusCode.' '.$method.' '.$path, ['body' => $response->getContent(false)]);
return null;
}
return (204 === $statusCode || '' === $response->getContent(false)) ? [] : $response->toArray();
}
private function resolveOrg(string $org): string
{
return '' !== $org ? $org : $this->defaultOrg;

View File

@@ -341,69 +341,4 @@ class FacturePdfTest extends TestCase
$this->assertStringStartsWith('%PDF', $output);
}
public function testDisplayHmacViaReflection(): void
{
// displayHmac() is a private method that is never called in generate().
// Call it via reflection to ensure method-level coverage.
$facture = $this->makeFacture('04/2026-00015');
$facture->setTotalHt('10.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('10.00');
$pdf = new FacturePdf($this->kernel, $facture);
$pdf->generate(); // initialises the PDF with a page
$ref = new \ReflectionMethod(FacturePdf::class, 'displayHmac');
$ref->setAccessible(true);
$ref->invoke($pdf);
$output = $pdf->Output('S');
$this->assertStringStartsWith('%PDF', $output);
}
public function testAppendRibViaReflectionWhenNoRibFile(): void
{
// appendRib() is a private method that is never called in generate().
// With no rib.pdf file present, it should return early.
$facture = $this->makeFacture('04/2026-00016');
$pdf = new FacturePdf($this->kernel, $facture);
$pdf->generate();
$ref = new \ReflectionMethod(FacturePdf::class, 'appendRib');
$ref->setAccessible(true);
$ref->invoke($pdf); // no rib.pdf -> returns immediately
$output = $pdf->Output('S');
$this->assertStringStartsWith('%PDF', $output);
}
public function testAppendRibViaReflectionWithRibFile(): void
{
// appendRib() with a valid rib.pdf file exercises the file-import path.
$ribDir = $this->projectDir.'/public';
if (!is_dir($ribDir)) {
mkdir($ribDir, 0775, true);
}
$miniPdf = new \setasign\Fpdi\Fpdi();
$miniPdf->AddPage();
$ribPath = $ribDir.'/rib.pdf';
$miniPdf->Output('F', $ribPath);
$facture = $this->makeFacture('04/2026-00017');
$facture->setTotalHt('20.00');
$facture->setTotalTva('0.00');
$facture->setTotalTtc('20.00');
$pdf = new FacturePdf($this->kernel, $facture);
$pdf->generate();
$ref = new \ReflectionMethod(FacturePdf::class, 'appendRib');
$ref->setAccessible(true);
$ref->invoke($pdf);
$output = $pdf->Output('S');
$this->assertStringStartsWith('%PDF', $output);
}
}