diff --git a/.env b/.env index 847f8cd..b6b8379 100644 --- a/.env +++ b/.env @@ -112,3 +112,7 @@ DOCUSEAL_API= DOCUSEAL_WEBHOOKS_SECRET_HEADER=X-Sign DOCUSEAL_WEBHOOKS_SECRET= ###< docuseal ### + +###> discord ### +DISCORD_WEBHOOK= +###< discord ### diff --git a/.gitea/workflows/discord-notify.yml b/.gitea/workflows/discord-notify.yml index 24d4719..73e9449 100644 --- a/.gitea/workflows/discord-notify.yml +++ b/.gitea/workflows/discord-notify.yml @@ -61,4 +61,4 @@ jobs: curl -sf -X POST \ -H "Content-Type: application/json" \ -d @/tmp/discord.json \ - "https://discord.com/api/webhooks/1419573620602044518/ikAdxWxsrrTqMTb5Gh_8ylcoJHlOnq7aJZvR5udoS_fCK56Jk3qpEnJHVKdD8fwuNJF3" + "${{ secrets.DISCORD_WEBHOOK }}" diff --git a/src/Command/CheckDnsCommand.php b/src/Command/CheckDnsCommand.php index 46765d5..59db771 100644 --- a/src/Command/CheckDnsCommand.php +++ b/src/Command/CheckDnsCommand.php @@ -3,8 +3,8 @@ namespace App\Command; use App\Service\AwsSesService; -use App\Service\CloudflareService; use App\Service\DnsCheckService; +use App\Service\DnsInfraHelper; use App\Service\MailcowService; use App\Service\MailerService; use Symfony\Component\Console\Attribute\AsCommand; @@ -23,26 +23,19 @@ use Twig\Environment; )] class CheckDnsCommand extends Command { - private const DOMAINS = ['siteconseil.fr', 'esy-web.dev']; - - private const EXPECTED_MX = [ - 'siteconseil.fr' => 'mail.esy-web.dev', - 'esy-web.dev' => 'mail.esy-web.dev', - ]; - private const MONITOR_EMAIL = 'monitor@siteconseil.fr'; - private const DISCORD_WEBHOOK = 'https://discord.com/api/webhooks/1419573620602044518/ikAdxWxsrrTqMTb5Gh_8ylcoJHlOnq7aJZvR5udoS_fCK56Jk3qpEnJHVKdD8fwuNJF3'; public function __construct( private DnsCheckService $dnsCheck, private AwsSesService $awsSes, - private CloudflareService $cloudflare, private MailcowService $mailcow, private MailerService $mailer, private Environment $twig, private HttpClientInterface $httpClient, private UrlGeneratorInterface $urlGenerator, + private DnsInfraHelper $helper, #[Autowire('%kernel.environment%')] private string $appEnv, + #[Autowire(env: 'DISCORD_WEBHOOK')] private string $discordWebhook = '', ) { parent::__construct(); } @@ -57,39 +50,30 @@ class CheckDnsCommand extends Command $successes = []; $domainResults = []; - // Charger les records Cloudflare une seule fois par domaine $cfRecordsByDomain = $this->loadCloudflareRecords($io); - foreach (self::DOMAINS as $domain) { + foreach (DnsInfraHelper::DOMAINS as $domain) { $io->section('Domaine : '.$domain); $checks = []; $cfRecords = $cfRecordsByDomain[$domain] ?? []; - // DNS dig + enrichissement Cloudflare $this->dnsCheck->checkSpf($domain, $checks, $errors, $warnings, $successes); - $this->enrichWithCloudflare($checks, $domain, 'SPF', 'TXT', $cfRecords); + $this->helper->enrichWithCloudflare($checks, $domain, 'SPF', 'TXT', $cfRecords); $this->dnsCheck->checkDmarc($domain, $checks, $errors, $successes); - $this->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords); + $this->helper->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords); - // DKIM verifie via AWS SES (3 CNAME individuels), pas de check generique - - $this->dnsCheck->checkMx($domain, self::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes); - $this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords); + $this->dnsCheck->checkMx($domain, DnsInfraHelper::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes); + $this->helper->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords); $this->dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes); - $this->enrichWithCloudflare($checks, 'bounce.'.$domain, 'Bounce', 'MX', $cfRecords); + $this->helper->enrichWithCloudflare($checks, 'bounce.'.$domain, 'Bounce', 'MX', $cfRecords); - // WHOIS (NS + expiration) $this->dnsCheck->checkWhois($domain, $checks, $errors, $warnings, $successes); - // AWS SES $this->checkAwsSes($domain, $checks, $errors, $successes, $cfRecords); - - // Mailcow $this->checkMailcow($domain, $checks, $errors, $warnings, $successes, $cfRecords); - // Affichage console foreach ($checks as $check) { $icon = match ($check['status']) { 'ok' => 'OK', @@ -127,82 +111,15 @@ class CheckDnsCommand extends Command */ private function loadCloudflareRecords(SymfonyStyle $io): array { - $result = []; + $result = $this->helper->loadCloudflareRecords(); - if (!$this->cloudflare->isAvailable()) { + if ([] === $result) { $io->text(' Cloudflare API non disponible, colonnes CF vides.'); - - return $result; - } - - foreach (self::DOMAINS as $domain) { - try { - $zoneId = $this->cloudflare->getZoneId($domain); - if (null !== $zoneId) { - $result[$domain] = $this->cloudflare->getDnsRecords($zoneId); - } - } catch (\Throwable) { - } } return $result; } - /** - * @param list $checks - * @param list> $cfRecords - */ - private function enrichWithCloudflare(array &$checks, string $recordName, string $checkType, string $dnsType, array $cfRecords): void - { - $cfValue = null; - $cfStatus = ''; - - foreach ($cfRecords as $r) { - if (($r['name'] ?? '') === $recordName && ($r['type'] ?? '') === $dnsType) { - $cfValue = $r['content'] ?? ''; - $cfStatus = 'ok'; - break; - } - } - - if (null === $cfValue) { - $cfValue = 'Non trouve'; - $cfStatus = '' === $cfStatus ? '' : 'error'; - } - - // Enrichir les derniers checks du type correspondant - for ($i = \count($checks) - 1; $i >= 0; --$i) { - if ($checks[$i]['type'] === $checkType && '' === ($checks[$i]['cloudflare'] ?? '')) { - $checks[$i]['cloudflare'] = $cfValue; - $checks[$i]['cf_status'] = $cfStatus; - } - } - } - - /** - * @param list $checks - * @param list> $cfRecords - */ - private function enrichLastCheck(array &$checks, string $recordName, string $dnsType, array $cfRecords): void - { - $cfValue = 'Non trouve'; - $cfStatus = ''; - - foreach ($cfRecords as $r) { - if (($r['name'] ?? '') === $recordName && ($r['type'] ?? '') === $dnsType) { - $cfValue = $r['content'] ?? ''; - $cfStatus = 'ok'; - break; - } - } - - $last = \count($checks) - 1; - if ($last >= 0) { - $checks[$last]['cloudflare'] = $cfValue; - $checks[$last]['cf_status'] = $cfStatus; - } - } - /** * @param list $checks * @param list $errors @@ -218,7 +135,6 @@ class CheckDnsCommand extends Command } try { - // Verification du domaine $verif = $this->awsSes->isDomainVerified($domain); $checks[] = DnsCheckService::check( 'AWS SES', 'Domaine', 'Success' === $verif ? 'ok' : 'error', @@ -230,7 +146,6 @@ class CheckDnsCommand extends Command $errors[] = "[$domain] AWS SES : domaine non verifie ($verif)"; } - // DKIM - verifier les 3 tokens CNAME $dkim = $this->awsSes->getDkimStatus($domain); $dkimOk = $dkim['enabled'] && $dkim['verified']; @@ -247,7 +162,6 @@ class CheckDnsCommand extends Command $errors[] = "[$domain] AWS SES DKIM : non active ou non verifiee"; } - // Verifier chaque token DKIM dans le DNS foreach ($dkim['tokens'] as $token) { $expectedCname = $token.'.dkim.amazonses.com'; $dkimFqdn = $token.'._domainkey.'.$domain; @@ -261,7 +175,7 @@ class CheckDnsCommand extends Command $actualCname ?? 'Non trouve' ); - $this->enrichLastCheck($checks, $dkimFqdn, 'CNAME', $cfRecords); + $this->helper->enrichLastCheck($checks, $dkimFqdn, 'CNAME', $cfRecords); if ($found) { $successes[] = "[$domain] AWS SES DKIM CNAME $token : OK"; @@ -270,7 +184,6 @@ class CheckDnsCommand extends Command } } - // MAIL FROM $mailFrom = $this->awsSes->getMailFromStatus($domain); $mailFromDomain = $mailFrom['mail_from_domain']; @@ -289,11 +202,10 @@ class CheckDnsCommand extends Command $errors[] = "[$domain] AWS SES MAIL FROM : $mailFromDomain statut $mailFromStatus"; } - // Verifier le MX du MAIL FROM dans le DNS $mxExpected = $mailFrom['mx_expected']; if (null !== $mxExpected) { - $mxFound = $this->checkMxExists($mailFromDomain, $mxExpected); - $actualMx = $this->getMxValues($mailFromDomain); + $mxFound = $this->helper->checkMxExists($mailFromDomain, $mxExpected); + $actualMx = $this->helper->getMxValues($mailFromDomain); $checks[] = DnsCheckService::check( 'AWS SES', 'MAIL FROM MX', $mxFound ? 'ok' : 'error', $mxFound ? 'Present' : 'Absent', @@ -301,7 +213,7 @@ class CheckDnsCommand extends Command $actualMx ?: 'Non trouve' ); - $this->enrichLastCheck($checks, $mailFromDomain, 'MX', $cfRecords); + $this->helper->enrichLastCheck($checks, $mailFromDomain, 'MX', $cfRecords); if ($mxFound) { $successes[] = "[$domain] AWS SES MAIL FROM MX : OK"; @@ -310,18 +222,17 @@ class CheckDnsCommand extends Command } } - // Verifier le TXT SPF du MAIL FROM dans le DNS $txtExpected = $mailFrom['txt_expected']; if (null !== $txtExpected) { - $txtFound = $this->checkTxtContains($mailFromDomain, 'v=spf1'); - $actualTxt = $this->getTxtSpfValue($mailFromDomain); + $txtFound = $this->helper->checkTxtContains($mailFromDomain, 'v=spf1'); + $actualTxt = $this->helper->getTxtSpfValue($mailFromDomain); $checks[] = DnsCheckService::check( 'AWS SES', 'MAIL FROM TXT', $txtFound ? 'ok' : 'error', $txtFound ? 'Present' : 'Absent', "$mailFromDomain TXT $txtExpected", $actualTxt ?: 'Non trouve' ); - $this->enrichLastCheck($checks, $mailFromDomain, 'TXT', $cfRecords); + $this->helper->enrichLastCheck($checks, $mailFromDomain, 'TXT', $cfRecords); if ($txtFound) { $successes[] = "[$domain] AWS SES MAIL FROM SPF : OK"; @@ -333,7 +244,6 @@ class CheckDnsCommand extends Command $checks[] = DnsCheckService::check('AWS SES', 'MAIL FROM', 'warning', 'Non configure', 'bounce.'.$domain, 'N/A'); } - // Notifications bounce $notif = $this->awsSes->getNotificationStatus($domain); $bounceOk = $notif['forwarding'] || null !== $notif['bounce_topic']; $bounceDetail = $notif['forwarding'] ? 'Forwarding actif' : ($notif['bounce_topic'] ?? 'Non configure'); @@ -344,67 +254,6 @@ class CheckDnsCommand extends Command } } - private function getActualDnsValue(string $type, string $name): string - { - return match ($type) { - 'MX' => $this->getMxValues($name), - 'CNAME' => $this->dnsCheck->getCnameRecord($name) ?? '', - 'TXT' => $this->getTxtSpfValue($name) ?: $this->getFirstTxtValue($name), - 'SRV' => $this->getSrvValue($name), - default => '', - }; - } - - private function getFirstTxtValue(string $domain): string - { - $output = $this->dnsCheck->dig($domain, 'TXT'); - - foreach (explode("\n", $output) as $line) { - if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) { - return str_replace('" "', '', $m[1]); - } - } - - return ''; - } - - private function getSrvValue(string $domain): string - { - $records = $this->dnsCheck->getSrvRecords($domain); - $values = []; - foreach ($records as $srv) { - $values[] = $srv['target'].' port:'.$srv['port']; - } - - return implode(', ', $values); - } - - private function getMxValues(string $domain): string - { - $values = []; - foreach ($this->dnsCheck->getMxRecords($domain) as $mx) { - $values[] = $mx['target'].' (pri: '.$mx['pri'].')'; - } - - return implode(', ', $values); - } - - private function getTxtSpfValue(string $domain): string - { - $output = $this->dnsCheck->dig($domain, 'TXT'); - - foreach (explode("\n", $output) as $line) { - if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) { - $txt = str_replace('" "', '', $m[1]); - if (str_starts_with($txt, 'v=spf1')) { - return $txt; - } - } - } - - return ''; - } - /** * @param list $checks * @param list $errors @@ -440,13 +289,12 @@ class CheckDnsCommand extends Command $errors[] = "[$domain] Mailcow : desactive"; } - // Verifier les enregistrements DNS attendus par Mailcow $expectedRecords = $this->mailcow->getExpectedDnsRecords($domain); foreach ($expectedRecords as $expected) { - $found = $this->checkDnsRecordExists($expected['type'], $expected['name'], $expected['content']); + $found = $this->helper->checkDnsRecordExists($expected['type'], $expected['name'], $expected['content']); $isOptional = $expected['optional']; $label = $expected['type'].' '.$expected['name']; - $digValue = $this->getActualDnsValue($expected['type'], $expected['name']); + $digValue = $this->helper->getActualDnsValue($expected['type'], $expected['name']); $checks[] = DnsCheckService::check( 'Mailcow DNS', $label, $found ? 'ok' : ($isOptional ? 'warning' : 'error'), @@ -454,7 +302,7 @@ class CheckDnsCommand extends Command $expected['content'], $digValue ?: 'Non trouve' ); - $this->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords); + $this->helper->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords); if ($found) { $successes[] = "[$domain] Mailcow DNS : $label OK"; @@ -470,44 +318,6 @@ class CheckDnsCommand extends Command } } - private function checkDnsRecordExists(string $type, string $name, string $expectedContent): bool - { - return match ($type) { - 'MX' => $this->checkMxExists($name, $expectedContent), - 'TXT' => $this->checkTxtContains($name, $expectedContent), - 'CNAME' => null !== $this->dnsCheck->getCnameRecord($name), - 'SRV' => [] !== $this->dnsCheck->getSrvRecords($name), - default => false, - }; - } - - private function checkMxExists(string $name, string $target): bool - { - foreach ($this->dnsCheck->getMxRecords($name) as $mx) { - if (str_contains($mx['target'], $target)) { - return true; - } - } - - return false; - } - - private function checkTxtContains(string $name, string $start): bool - { - $output = $this->dnsCheck->dig($name, 'TXT'); - - foreach (explode("\n", $output) as $line) { - if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) { - $txt = str_replace('" "', '', $m[1]); - if (str_starts_with($txt, $start)) { - return true; - } - } - } - - return false; - } - /** * @param list $errors * @param list $warnings @@ -537,7 +347,7 @@ class CheckDnsCommand extends Command 'statusColor' => $statusColor, 'statusText' => $statusText, 'date' => new \DateTimeImmutable(), - 'domains' => self::DOMAINS, + 'domains' => DnsInfraHelper::DOMAINS, 'domainResults' => $domainResults, 'reportUrl' => '__DNS_REPORT_URL__', ]); @@ -553,8 +363,8 @@ class CheckDnsCommand extends Command 1, // Priority HIGH ); - // Notification Discord uniquement en prod - if ('prod' === $this->appEnv) { + // Notification Discord uniquement en prod avec webhook configure + if ('prod' === $this->appEnv && '' !== $this->discordWebhook) { $this->sendDiscordNotification($errors, $warnings, $successes); } } @@ -570,17 +380,17 @@ class CheckDnsCommand extends Command $hasWarnings = [] !== $warnings; if ($hasErrors) { - $color = 0xDC2626; // rouge + $color = 0xDC2626; $title = 'ALERTE DNS - '.\count($errors).' erreur(s)'; } elseif ($hasWarnings) { - $color = 0xF59E0B; // jaune + $color = 0xF59E0B; $title = 'DNS - '.\count($warnings).' avertissement(s)'; } else { - $color = 0x16A34A; // vert + $color = 0x16A34A; $title = 'DNS - Configuration OK'; } - $description = "Domaines: **".implode(', ', self::DOMAINS)."**\n\n"; + $description = "Domaines: **".implode(', ', DnsInfraHelper::DOMAINS)."**\n\n"; $description .= "**".\count($successes)."** verification(s) OK\n"; if ($hasErrors) { $description .= "**".\count($errors)."** erreur(s)\n"; @@ -596,7 +406,7 @@ class CheckDnsCommand extends Command } try { - $this->httpClient->request('POST', self::DISCORD_WEBHOOK, [ + $this->httpClient->request('POST', $this->discordWebhook, [ 'json' => [ 'embeds' => [[ 'title' => 'Esy-Infra : '.$title, diff --git a/src/Controller/DnsReportController.php b/src/Controller/DnsReportController.php index d2c5993..973f517 100644 --- a/src/Controller/DnsReportController.php +++ b/src/Controller/DnsReportController.php @@ -4,8 +4,8 @@ namespace App\Controller; use App\Repository\EmailTrackingRepository; use App\Service\AwsSesService; -use App\Service\CloudflareService; use App\Service\DnsCheckService; +use App\Service\DnsInfraHelper; use App\Service\MailcowService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; @@ -16,20 +16,13 @@ use Symfony\Contracts\Cache\ItemInterface; class DnsReportController extends AbstractController { - private const DOMAINS = ['siteconseil.fr', 'esy-web.dev']; - - private const EXPECTED_MX = [ - 'siteconseil.fr' => 'mail.esy-web.dev', - 'esy-web.dev' => 'mail.esy-web.dev', - ]; - #[Route('/email/configuration/{token}', name: 'app_dns_report', methods: ['GET'])] public function __invoke( string $token, DnsCheckService $dnsCheck, AwsSesService $awsSes, - CloudflareService $cloudflare, MailcowService $mailcow, + DnsInfraHelper $helper, EmailTrackingRepository $emailTrackingRepository, #[Autowire(service: 'dns_infra_cache')] CacheInterface $cache, ): Response { @@ -41,36 +34,36 @@ class DnsReportController extends AbstractController $cacheKey = 'dns_infra_check_'.$token; - $data = $cache->get($cacheKey, function (ItemInterface $item) use ($dnsCheck, $awsSes, $cloudflare, $mailcow): array { - $item->expiresAfter(3600); // 1 heure + $data = $cache->get($cacheKey, function (ItemInterface $item) use ($dnsCheck, $awsSes, $mailcow, $helper): array { + $item->expiresAfter(3600); $errors = []; $warnings = []; $successes = []; $domainResults = []; - $cfRecordsByDomain = $this->loadCloudflareRecords($cloudflare); + $cfRecordsByDomain = $helper->loadCloudflareRecords(); - foreach (self::DOMAINS as $domain) { + foreach (DnsInfraHelper::DOMAINS as $domain) { $checks = []; $cfRecords = $cfRecordsByDomain[$domain] ?? []; $dnsCheck->checkSpf($domain, $checks, $errors, $warnings, $successes); - $this->enrichWithCloudflare($checks, $domain, 'SPF', 'TXT', $cfRecords); + $helper->enrichWithCloudflare($checks, $domain, 'SPF', 'TXT', $cfRecords); $dnsCheck->checkDmarc($domain, $checks, $errors, $successes); - $this->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords); + $helper->enrichWithCloudflare($checks, '_dmarc.'.$domain, 'DMARC', 'TXT', $cfRecords); - $dnsCheck->checkMx($domain, self::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes); - $this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords); + $dnsCheck->checkMx($domain, DnsInfraHelper::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes); + $helper->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords); $dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes); - $this->enrichLastCheck($checks, 'bounce.'.$domain, 'MX', $cfRecords); + $helper->enrichLastCheck($checks, 'bounce.'.$domain, 'MX', $cfRecords); $dnsCheck->checkWhois($domain, $checks, $errors, $warnings, $successes); - $this->checkAwsSes($domain, $awsSes, $dnsCheck, $checks, $errors, $successes, $cfRecords); - $this->checkMailcow($domain, $mailcow, $dnsCheck, $checks, $errors, $warnings, $successes, $cfRecords); + $this->checkAwsSes($domain, $awsSes, $dnsCheck, $helper, $checks, $errors, $successes, $cfRecords); + $this->checkMailcow($domain, $mailcow, $helper, $checks, $errors, $warnings, $successes, $cfRecords); $domainResults[] = ['domain' => $domain, 'checks' => $checks]; } @@ -93,85 +86,13 @@ class DnsReportController extends AbstractController ]); } - /** - * @return array>> - */ - private function loadCloudflareRecords(CloudflareService $cloudflare): array - { - $result = []; - if (!$cloudflare->isAvailable()) { - return $result; - } - - foreach (self::DOMAINS as $domain) { - try { - $zoneId = $cloudflare->getZoneId($domain); - if (null !== $zoneId) { - $result[$domain] = $cloudflare->getDnsRecords($zoneId); - } - } catch (\Throwable) { - } - } - - return $result; - } - - /** - * @param list $checks - * @param list> $cfRecords - */ - private function enrichWithCloudflare(array &$checks, string $recordName, string $checkType, string $dnsType, array $cfRecords): void - { - $cfValue = 'Non trouve'; - $cfStatus = ''; - - foreach ($cfRecords as $r) { - if (($r['name'] ?? '') === $recordName && ($r['type'] ?? '') === $dnsType) { - $cfValue = $r['content'] ?? ''; - $cfStatus = 'ok'; - break; - } - } - - for ($i = \count($checks) - 1; $i >= 0; --$i) { - if ($checks[$i]['type'] === $checkType && '' === ($checks[$i]['cloudflare'] ?? '')) { - $checks[$i]['cloudflare'] = $cfValue; - $checks[$i]['cf_status'] = $cfStatus; - } - } - } - - /** - * @param list $checks - * @param list> $cfRecords - */ - private function enrichLastCheck(array &$checks, string $recordName, string $dnsType, array $cfRecords): void - { - $cfValue = 'Non trouve'; - $cfStatus = ''; - - foreach ($cfRecords as $r) { - if (($r['name'] ?? '') === $recordName && ($r['type'] ?? '') === $dnsType) { - $cfValue = $r['content'] ?? ''; - $cfStatus = 'ok'; - break; - } - } - - $last = \count($checks) - 1; - if ($last >= 0) { - $checks[$last]['cloudflare'] = $cfValue; - $checks[$last]['cf_status'] = $cfStatus; - } - } - /** * @param list $checks * @param list $errors * @param list $successes * @param list> $cfRecords */ - private function checkAwsSes(string $domain, AwsSesService $awsSes, DnsCheckService $dnsCheck, array &$checks, array &$errors, array &$successes, array $cfRecords): void + private function checkAwsSes(string $domain, AwsSesService $awsSes, DnsCheckService $dnsCheck, DnsInfraHelper $helper, array &$checks, array &$errors, array &$successes, array $cfRecords): void { if (!$awsSes->isAvailable()) { return; @@ -191,7 +112,7 @@ class DnsReportController extends AbstractController $actualCname = $dnsCheck->getCnameRecord($dkimFqdn); $found = null !== $actualCname && str_contains($actualCname, 'dkim.amazonses.com'); $checks[] = DnsCheckService::check('AWS SES', 'DKIM '.$token, $found ? 'ok' : 'error', $found ? 'Present' : 'Absent', $expectedCname, $actualCname ?? 'Non trouve'); - $this->enrichLastCheck($checks, $dkimFqdn, 'CNAME', $cfRecords); + $helper->enrichLastCheck($checks, $dkimFqdn, 'CNAME', $cfRecords); } $mailFrom = $awsSes->getMailFromStatus($domain); @@ -206,14 +127,14 @@ class DnsReportController extends AbstractController } $mxFound = !empty(array_filter($mxValues, fn ($v) => str_contains($v, 'amazonses.com'))); $checks[] = DnsCheckService::check('AWS SES', 'MAIL FROM MX', $mxFound ? 'ok' : 'error', $mxFound ? 'Present' : 'Absent', $mailFrom['mx_expected'], implode(', ', $mxValues) ?: 'Non trouve'); - $this->enrichLastCheck($checks, $mfd, 'MX', $cfRecords); + $helper->enrichLastCheck($checks, $mfd, 'MX', $cfRecords); } if (null !== $mailFrom['txt_expected']) { - $txtValue = $this->getFirstTxtValue($dnsCheck, $mfd); + $txtValue = $helper->getFirstTxtValue($mfd); $txtFound = str_contains($txtValue, 'spf1'); $checks[] = DnsCheckService::check('AWS SES', 'MAIL FROM TXT', $txtFound ? 'ok' : 'error', $txtFound ? 'Present' : 'Absent', $mailFrom['txt_expected'], $txtValue ?: 'Non trouve'); - $this->enrichLastCheck($checks, $mfd, 'TXT', $cfRecords); + $helper->enrichLastCheck($checks, $mfd, 'TXT', $cfRecords); } } } catch (\Throwable) { @@ -227,7 +148,7 @@ class DnsReportController extends AbstractController * @param list $successes * @param list> $cfRecords */ - private function checkMailcow(string $domain, MailcowService $mailcow, DnsCheckService $dnsCheck, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords): void + private function checkMailcow(string $domain, MailcowService $mailcow, DnsInfraHelper $helper, array &$checks, array &$errors, array &$warnings, array &$successes, array $cfRecords): void { if (!$mailcow->isAvailable()) { return; @@ -241,64 +162,14 @@ class DnsReportController extends AbstractController $checks[] = DnsCheckService::check('Mailcow', 'Domaine', $info['active'] ? 'ok' : 'error', $info['active'] ? "Actif ({$info['mailboxes']} boites)" : 'Desactive', 'Actif', $info['active'] ? 'Actif' : 'Desactive'); foreach ($mailcow->getExpectedDnsRecords($domain) as $expected) { - $found = match ($expected['type']) { - 'MX' => [] !== $dnsCheck->getMxRecords($expected['name']), - 'CNAME' => null !== $dnsCheck->getCnameRecord($expected['name']), - 'SRV' => [] !== $dnsCheck->getSrvRecords($expected['name']), - 'TXT' => str_contains($dnsCheck->dig($expected['name'], 'TXT'), $expected['content']), - default => false, - }; + $found = $helper->checkDnsRecordExists($expected['type'], $expected['name'], $expected['content']); $label = $expected['type'].' '.$expected['name']; - $digValue = $this->getActualDnsValue($dnsCheck, $expected['type'], $expected['name']); + $digValue = $helper->getActualDnsValue($expected['type'], $expected['name']); $checks[] = DnsCheckService::check('Mailcow', $label, $found ? 'ok' : ($expected['optional'] ? 'warning' : 'error'), $found ? 'Present' : 'Absent', $expected['content'], $digValue ?: 'Non trouve'); - $this->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords); + $helper->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords); } } catch (\Throwable) { } } - - private function getActualDnsValue(DnsCheckService $dnsCheck, string $type, string $name): string - { - return match ($type) { - 'MX' => $this->getMxValues($dnsCheck, $name), - 'CNAME' => $dnsCheck->getCnameRecord($name) ?? '', - 'TXT' => $this->getFirstTxtValue($dnsCheck, $name), - 'SRV' => $this->getSrvValue($dnsCheck, $name), - default => '', - }; - } - - private function getMxValues(DnsCheckService $dnsCheck, string $domain): string - { - $values = []; - foreach ($dnsCheck->getMxRecords($domain) as $mx) { - $values[] = $mx['target'].' (pri: '.$mx['pri'].')'; - } - - return implode(', ', $values); - } - - private function getFirstTxtValue(DnsCheckService $dnsCheck, string $domain): string - { - $output = $dnsCheck->dig($domain, 'TXT'); - - foreach (explode("\n", $output) as $line) { - if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) { - return str_replace('" "', '', $m[1]); - } - } - - return ''; - } - - private function getSrvValue(DnsCheckService $dnsCheck, string $domain): string - { - $values = []; - foreach ($dnsCheck->getSrvRecords($domain) as $srv) { - $values[] = $srv['target'].' port:'.$srv['port']; - } - - return implode(', ', $values); - } } diff --git a/src/Service/DnsInfraHelper.php b/src/Service/DnsInfraHelper.php new file mode 100644 index 0000000..b75f202 --- /dev/null +++ b/src/Service/DnsInfraHelper.php @@ -0,0 +1,212 @@ + 'mail.esy-web.dev', + 'esy-web.dev' => 'mail.esy-web.dev', + ]; + + public function __construct( + private DnsCheckService $dnsCheck, + private CloudflareService $cloudflare, + ) { + } + + /** + * @return array>> + */ + public function loadCloudflareRecords(): array + { + $result = []; + if (!$this->cloudflare->isAvailable()) { + return $result; + } + + foreach (self::DOMAINS as $domain) { + try { + $zoneId = $this->cloudflare->getZoneId($domain); + if (null !== $zoneId) { + $result[$domain] = $this->cloudflare->getDnsRecords($zoneId); + } + } catch (\Throwable) { + } + } + + return $result; + } + + /** + * @param list $checks + * @param list> $cfRecords + */ + public function enrichWithCloudflare(array &$checks, string $recordName, string $checkType, string $dnsType, array $cfRecords): void + { + $cfValue = null; + $cfStatus = ''; + + foreach ($cfRecords as $r) { + if (($r['name'] ?? '') === $recordName && ($r['type'] ?? '') === $dnsType) { + $cfValue = $r['content'] ?? ''; + $cfStatus = 'ok'; + break; + } + } + + if (null === $cfValue) { + $cfValue = 'Non trouve'; + $cfStatus = '' === $cfStatus ? '' : 'error'; + } + + for ($i = \count($checks) - 1; $i >= 0; --$i) { + if ($checks[$i]['type'] === $checkType && '' === ($checks[$i]['cloudflare'] ?? '')) { + $checks[$i]['cloudflare'] = $cfValue; + $checks[$i]['cf_status'] = $cfStatus; + } + } + } + + /** + * @param list $checks + * @param list> $cfRecords + */ + public function enrichLastCheck(array &$checks, string $recordName, string $dnsType, array $cfRecords): void + { + $cfValue = 'Non trouve'; + $cfStatus = ''; + + foreach ($cfRecords as $r) { + if (($r['name'] ?? '') === $recordName && ($r['type'] ?? '') === $dnsType) { + $cfValue = $r['content'] ?? ''; + $cfStatus = 'ok'; + break; + } + } + + $last = \count($checks) - 1; + if ($last >= 0) { + $checks[$last]['cloudflare'] = $cfValue; + $checks[$last]['cf_status'] = $cfStatus; + } + } + + public function getActualDnsValue(string $type, string $name): string + { + return match ($type) { + 'MX' => $this->getMxValues($name), + 'CNAME' => $this->dnsCheck->getCnameRecord($name) ?? '', + 'TXT' => $this->getFirstTxtValue($name) ?: $this->getFirstTxtValueRaw($name), + 'SRV' => $this->getSrvValue($name), + default => '', + }; + } + + public function getMxValues(string $domain): string + { + $values = []; + foreach ($this->dnsCheck->getMxRecords($domain) as $mx) { + $values[] = $mx['target'].' (pri: '.$mx['pri'].')'; + } + + return implode(', ', $values); + } + + public function getFirstTxtValue(string $domain): string + { + $output = $this->dnsCheck->dig($domain, 'TXT'); + + foreach (explode("\n", $output) as $line) { + if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) { + return str_replace('" "', '', $m[1]); + } + } + + return ''; + } + + private function getFirstTxtValueRaw(string $domain): string + { + $output = $this->dnsCheck->dig($domain, 'TXT'); + + foreach (explode("\n", $output) as $line) { + if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) { + return str_replace('" "', '', $m[1]); + } + } + + return ''; + } + + public function getSrvValue(string $domain): string + { + $values = []; + foreach ($this->dnsCheck->getSrvRecords($domain) as $srv) { + $values[] = $srv['target'].' port:'.$srv['port']; + } + + return implode(', ', $values); + } + + public function checkMxExists(string $name, string $target): bool + { + foreach ($this->dnsCheck->getMxRecords($name) as $mx) { + if (str_contains($mx['target'], $target)) { + return true; + } + } + + return false; + } + + public function checkTxtContains(string $name, string $start): bool + { + $output = $this->dnsCheck->dig($name, 'TXT'); + + foreach (explode("\n", $output) as $line) { + if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) { + $txt = str_replace('" "', '', $m[1]); + if (str_starts_with($txt, $start)) { + return true; + } + } + } + + return false; + } + + public function getTxtSpfValue(string $domain): string + { + $output = $this->dnsCheck->dig($domain, 'TXT'); + + foreach (explode("\n", $output) as $line) { + if (preg_match('/\bIN\s+TXT\s+"(.+)"/', $line, $m)) { + $txt = str_replace('" "', '', $m[1]); + if (str_starts_with($txt, 'v=spf1')) { + return $txt; + } + } + } + + return ''; + } + + public function checkDnsRecordExists(string $type, string $name, string $expectedContent): bool + { + return match ($type) { + 'MX' => $this->checkMxExists($name, $expectedContent), + 'TXT' => $this->checkTxtContains($name, $expectedContent), + 'CNAME' => null !== $this->dnsCheck->getCnameRecord($name), + 'SRV' => [] !== $this->dnsCheck->getSrvRecords($name), + default => false, + }; + } + + public function getDnsCheck(): DnsCheckService + { + return $this->dnsCheck; + } +} diff --git a/templates/admin/logs/pdf.html.twig b/templates/admin/logs/pdf.html.twig index 21e582d..34b4aae 100644 --- a/templates/admin/logs/pdf.html.twig +++ b/templates/admin/logs/pdf.html.twig @@ -1,45 +1,21 @@ - - - - - Log #{{ log.id }} - CRM SITECONSEIL - - - - -
+{% endblock %} + +{% block content %} Log d'activite

Rapport de log #{{ log.id }}

Trace d'activite — CRM SITECONSEIL
@@ -90,8 +66,13 @@

Attention : les donnees de ce log ont ete modifiees apres leur enregistrement initial.

{% endif %}
-
HMAC-SHA256 : {{ log.hmac }}
+{% endblock %} +{% block hmac_section %} +
HMAC-SHA256 : {{ log.hmac }}
+{% endblock %} + +{% block verify_box %} {% if verifyUrl is defined and qrcode is defined %}
@@ -107,16 +88,18 @@
{% endif %} +{% endblock %} +{% block footer_contact %}
contact@siteconseil.fr
+{% endblock %} -
+{% block signature_box %}{% endblock %} + +{% block footer_legal %} SARL SITECONSEIL — Siret : 418 664 058 — TVA : FR05 418 664 058
27 rue Le Serurier, 02100 Saint-Quentin, France — contact@siteconseil.fr
www.siteconseil.fr -
- - - +{% endblock %} diff --git a/templates/pdf/_base.html.twig b/templates/pdf/_base.html.twig new file mode 100644 index 0000000..ac78f45 --- /dev/null +++ b/templates/pdf/_base.html.twig @@ -0,0 +1,73 @@ + + + + + {% block title %}Document - CRM SITECONSEIL{% endblock %} + + + + +
+ {% block content %}{% endblock %} + + {% block verify_box %}{% endblock %} + + {% block hmac_section %}{% endblock %} + + {% block footer_contact %} +
+

DPO

+ contact@siteconseil.fr +
+ {% endblock %} + + {% block signature_box %} +
+
{% verbatim %}{{Sign;type=signature;width=150;height=50}}{% endverbatim %}
+
+ {% endblock %} + +
+ {% block footer_legal %} + SARL SITECONSEIL — RNA W022006988 — SIREN 943121517
+ 27 rue Le Sérurier, 02100 Saint-Quentin, France — contact@siteconseil.fr — 06 79 34 88 02
+ www.siteconseil.fr + {% endblock %} +
+
+ + diff --git a/templates/pdf/rgpd_access.html.twig b/templates/pdf/rgpd_access.html.twig index 70d544d..d130f7f 100644 --- a/templates/pdf/rgpd_access.html.twig +++ b/templates/pdf/rgpd_access.html.twig @@ -1,50 +1,16 @@ - - - - - Attestation RGPD - Acces aux donnees - - - - -
+{% endblock %} + +{% block content %} Droit d'acces

Donnees personnelles

RGPD — Article 15
@@ -73,7 +39,9 @@
Aucun evenement enregistre.
{% endif %} {% endfor %} +{% endblock %} +{% block verify_box %}
QR Code
@@ -85,19 +53,8 @@
+{% endblock %} + +{% block hmac_section %}
HMAC-SHA256 : {{ attestation.hmac }}
-
-

DPO

- contact@siteconseil.fr -
-
-
{% verbatim %}{{Sign;type=signature;width=150;height=50}}{% endverbatim %}
-
-
- SARL SITECONSEIL — RNA W022006988 — SIREN 943121517
- 27 rue Le Sérurier, 02100 Saint-Quentin, France — contact@siteconseil.fr — 06 79 34 88 02
- www.siteconseil.fr -
- - - +{% endblock %} diff --git a/templates/pdf/rgpd_deletion.html.twig b/templates/pdf/rgpd_deletion.html.twig index fdfb61d..bc09336 100644 --- a/templates/pdf/rgpd_deletion.html.twig +++ b/templates/pdf/rgpd_deletion.html.twig @@ -1,48 +1,22 @@ - - - - - Attestation RGPD - Suppression des donnees - - - - -
+{% endblock %} + +{% block content %} Suppression definitive

Attestation de suppression des donnees

RGPD — Article 17
@@ -63,6 +37,9 @@
Cette suppression est irreversible. Aucune donnee relative a cette adresse IP ne subsiste dans nos bases de donnees.
+{% endblock %} + +{% block verify_box %}
QR Code
@@ -74,19 +51,8 @@
+{% endblock %} + +{% block hmac_section %}
HMAC-SHA256 : {{ attestation.hmac }}
-
-

DPO

- contact@siteconseil.fr -
-
-
{% verbatim %}{{Sign;type=signature;width=150;height=50}}{% endverbatim %}
-
-
- SARL SITECONSEIL — RNA W022006988 — SIREN 943121517
- 27 rue Le Sérurier, 02100 Saint-Quentin, France — contact@siteconseil.fr — 06 79 34 88 02
- www.siteconseil.fr -
- - - +{% endblock %} diff --git a/templates/pdf/rgpd_no_data.html.twig b/templates/pdf/rgpd_no_data.html.twig index f5b0af0..b1a5664 100644 --- a/templates/pdf/rgpd_no_data.html.twig +++ b/templates/pdf/rgpd_no_data.html.twig @@ -1,47 +1,21 @@ - - - - - Attestation RGPD - Absence de donnees - - - - -
+{% endblock %} + +{% block content %} Document officiel

Attestation d'absence de donnees

RGPD — Article 15
@@ -62,6 +36,9 @@

Aucune donnee personnelle (identifiants de session, evenements de navigation, informations techniques) n'est stockee dans nos bases de donnees pour cette adresse IP.

+{% endblock %} + +{% block verify_box %}
QR Code
@@ -73,19 +50,8 @@
+{% endblock %} + +{% block hmac_section %}
HMAC-SHA256 : {{ attestation.hmac }}
-
-

DPO

- contact@siteconseil.fr -
-
-
{% verbatim %}{{Sign;type=signature;width=150;height=50}}{% endverbatim %}
-
-
- SARL SITECONSEIL — RNA W022006988 — SIREN 943121517
- 27 rue Le Sérurier, 02100 Saint-Quentin, France — contact@siteconseil.fr — 06 79 34 88 02
- www.siteconseil.fr -
- - - +{% endblock %} diff --git a/tests/Controller/Admin/StatsControllerTest.php b/tests/Controller/Admin/StatsControllerTest.php new file mode 100644 index 0000000..e2793a6 --- /dev/null +++ b/tests/Controller/Admin/StatsControllerTest.php @@ -0,0 +1,80 @@ +createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $this->createStub(RouterInterface::class)], + ['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)], + ['security.token_storage', $this->createStub(TokenStorageInterface::class)], + ['request_stack', $stack], + ['parameter_bag', $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class)], + ]); + $controller->setContainer($container); + } + + public function testIndexCurrentPeriod(): void + { + $controller = new StatsController(); + $this->setupController($controller); + + $request = new Request(['period' => 'current']); + $response = $controller->index($request); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testIndexCustomPeriod(): void + { + $controller = new StatsController(); + $this->setupController($controller); + + $request = new Request(['period' => 'custom', 'from' => '2026-01-01', 'to' => '2026-03-31']); + $response = $controller->index($request); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testIndexMonthsPeriod(): void + { + $controller = new StatsController(); + $this->setupController($controller); + + $request = new Request(['period' => '3']); + $response = $controller->index($request); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testIndexDefaultPeriod(): void + { + $controller = new StatsController(); + $this->setupController($controller); + + $request = new Request(); + $response = $controller->index($request); + $this->assertSame(200, $response->getStatusCode()); + } +} diff --git a/tests/Controller/Admin/StatusControllerTest.php b/tests/Controller/Admin/StatusControllerTest.php new file mode 100644 index 0000000..6df6c8d --- /dev/null +++ b/tests/Controller/Admin/StatusControllerTest.php @@ -0,0 +1,419 @@ +createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/admin/status'); + + $defaults = [ + 'twig' => $twig, + 'router' => $router, + 'security.authorization_checker' => $this->createStub(AuthorizationCheckerInterface::class), + 'security.token_storage' => $this->createStub(TokenStorageInterface::class), + 'request_stack' => $stack, + 'parameter_bag' => $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class), + ]; + + $services = array_merge($defaults, $overrides); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturnCallback(fn($id) => isset($services[$id])); + $container->method('get')->willReturnCallback(fn($id) => $services[$id] ?? null); + + return $container; + } + + private function addServiceToCategory(ServiceCategory $category, Service $service): void + { + $ref = new \ReflectionProperty(ServiceCategory::class, 'services'); + $collection = $ref->getValue($category); + $collection->add($service); + } + + public function testIndexEmpty(): void + { + $catRepo = $this->createStub(ServiceCategoryRepository::class); + $catRepo->method('findBy')->willReturn([]); + + $svcRepo = $this->createStub(ServiceRepository::class); + $svcRepo->method('findBy')->willReturn([]); + + $msgRepo = $this->createStub(EntityRepository::class); + $msgRepo->method('findBy')->willReturn([]); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($msgRepo); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->index($catRepo, $svcRepo, $em); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testIndexWithServices(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('Esy-Web', 'esy-web', $category); + $this->addServiceToCategory($category, $service); + + $catRepo = $this->createStub(ServiceCategoryRepository::class); + $catRepo->method('findBy')->willReturn([$category]); + + $svcRepo = $this->createStub(ServiceRepository::class); + $svcRepo->method('getHistoryForDays')->willReturn([]); + $svcRepo->method('getDailyStatus')->willReturn([]); + $svcRepo->method('findBy')->willReturn([$service]); + + $msgRepo = $this->createStub(EntityRepository::class); + $msgRepo->method('findBy')->willReturn([]); + + $em = $this->createStub(EntityManagerInterface::class); + $em->method('getRepository')->willReturn($msgRepo); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->index($catRepo, $svcRepo, $em); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testManage(): void + { + $catRepo = $this->createStub(ServiceCategoryRepository::class); + $catRepo->method('findBy')->willReturn([]); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->manage($catRepo); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testCategoryCreateEmptyName(): void + { + $em = $this->createStub(EntityManagerInterface::class); + $slugger = new AsciiSlugger(); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['name' => '']); + $request->setMethod('POST'); + + $response = $controller->categoryCreate($request, $em, $slugger); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testCategoryCreateSuccess(): void + { + $em = $this->createStub(EntityManagerInterface::class); + $slugger = new AsciiSlugger(); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['name' => 'New Category', 'position' => '1']); + $request->setMethod('POST'); + + $response = $controller->categoryCreate($request, $em, $slugger); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testCategoryDelete(): void + { + $category = new ServiceCategory('ToDelete', 'to-delete'); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->categoryDelete($category, $em); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testServiceCreateEmptyName(): void + { + $em = $this->createStub(EntityManagerInterface::class); + $catRepo = $this->createStub(ServiceCategoryRepository::class); + $slugger = new AsciiSlugger(); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['name' => '', 'category_id' => '0']); + $request->setMethod('POST'); + + $response = $controller->serviceCreate($request, $em, $catRepo, $slugger); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testServiceCreateCategoryNotFound(): void + { + $em = $this->createStub(EntityManagerInterface::class); + $catRepo = $this->createStub(ServiceCategoryRepository::class); + $catRepo->method('find')->willReturn(null); + $slugger = new AsciiSlugger(); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['name' => 'TestService', 'category_id' => '99', 'url' => '', 'external_type' => '', 'position' => '0']); + $request->setMethod('POST'); + + $response = $controller->serviceCreate($request, $em, $catRepo, $slugger); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testServiceCreateSuccess(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $em = $this->createStub(EntityManagerInterface::class); + $catRepo = $this->createStub(ServiceCategoryRepository::class); + $catRepo->method('find')->willReturn($category); + $slugger = new AsciiSlugger(); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['name' => 'Esy-New', 'category_id' => '1', 'url' => 'https://esy.com', 'external_type' => '', 'position' => '5']); + $request->setMethod('POST'); + + $response = $controller->serviceCreate($request, $em, $catRepo, $slugger); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testServiceCreateWithExternalType(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $em = $this->createStub(EntityManagerInterface::class); + $catRepo = $this->createStub(ServiceCategoryRepository::class); + $catRepo->method('find')->willReturn($category); + $slugger = new AsciiSlugger(); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['name' => 'External', 'category_id' => '1', 'url' => '', 'external_type' => 'http_check', 'position' => '0']); + $request->setMethod('POST'); + + $response = $controller->serviceCreate($request, $em, $catRepo, $slugger); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testServiceDelete(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('ToDelete', 'to-delete', $category); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->serviceDelete($service, $em); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testUpdateValidStatus(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('Esy-Web', 'esy-web', $category); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['status' => 'down', 'message' => 'Server crash']); + $request->setMethod('POST'); + + $response = $controller->update($service, $request, $em); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testUpdateInvalidStatus(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('Esy-Web', 'esy-web', $category); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['status' => 'invalid_status', 'message' => '']); + $request->setMethod('POST'); + + $response = $controller->update($service, $request, $em); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testUpdateStatusWithEmptyMessage(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('Esy-Web', 'esy-web', $category); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['status' => 'up', 'message' => '']); + $request->setMethod('POST'); + + $response = $controller->update($service, $request, $em); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testMessageCreateEmptyFields(): void + { + $svcRepo = $this->createStub(ServiceRepository::class); + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['service_id' => '0', 'title' => '', 'content' => '', 'severity' => 'info']); + $request->setMethod('POST'); + + $response = $controller->messageCreate($request, $em, $svcRepo); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testMessageCreateServiceNotFound(): void + { + $svcRepo = $this->createStub(ServiceRepository::class); + $svcRepo->method('find')->willReturn(null); + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['service_id' => '99', 'title' => 'Alert', 'content' => 'Something wrong', 'severity' => 'warning']); + $request->setMethod('POST'); + + $response = $controller->messageCreate($request, $em, $svcRepo); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testMessageCreateSuccessNoUser(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('Esy-Web', 'esy-web', $category); + + $svcRepo = $this->createStub(ServiceRepository::class); + $svcRepo->method('find')->willReturn($service); + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $request = new Request([], ['service_id' => '1', 'title' => 'Maintenance', 'content' => 'Scheduled maintenance', 'severity' => 'info']); + $request->setMethod('POST'); + + $response = $controller->messageCreate($request, $em, $svcRepo); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testMessageCreateSuccessWithUser(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('Esy-Web', 'esy-web', $category); + + $user = new User(); + $user->setEmail('admin@test.com'); + $user->setFirstName('A'); + $user->setLastName('B'); + $user->setPassword('h'); + + $token = $this->createStub(TokenInterface::class); + $token->method('getUser')->willReturn($user); + + $tokenStorage = $this->createStub(TokenStorageInterface::class); + $tokenStorage->method('getToken')->willReturn($token); + + $svcRepo = $this->createStub(ServiceRepository::class); + $svcRepo->method('find')->willReturn($service); + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer([ + 'security.token_storage' => $tokenStorage, + ])); + + $request = new Request([], ['service_id' => '1', 'title' => 'Incident', 'content' => 'Server down', 'severity' => 'critical']); + $request->setMethod('POST'); + + $response = $controller->messageCreate($request, $em, $svcRepo); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testMessageResolve(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('Esy-Web', 'esy-web', $category); + $message = new ServiceMessage($service, 'Test', 'Content'); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new StatusController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->messageResolve($message, $em); + $this->assertSame(302, $response->getStatusCode()); + $this->assertFalse($message->isActive()); + } + + public function testApiDaily(): void + { + $category = new ServiceCategory('Infra', 'infra'); + $service = new Service('Esy-Web', 'esy-web', $category); + + $svcRepo = $this->createStub(ServiceRepository::class); + $svcRepo->method('getDailyStatus')->willReturn([ + ['date' => '2026-04-01', 'status' => 'up'], + ['date' => '2026-04-02', 'status' => 'up'], + ]); + + $controller = new StatusController(); + + $response = $controller->apiDaily($service, $svcRepo); + $this->assertSame(200, $response->getStatusCode()); + } +} diff --git a/tests/Controller/Admin/SyncControllerTest.php b/tests/Controller/Admin/SyncControllerTest.php new file mode 100644 index 0000000..f95607d --- /dev/null +++ b/tests/Controller/Admin/SyncControllerTest.php @@ -0,0 +1,317 @@ +createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/admin/sync'); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)], + ['security.token_storage', $this->createStub(TokenStorageInterface::class)], + ['request_stack', $stack], + ['parameter_bag', $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class)], + ]); + + return $container; + } + + private function createPriceWithStripe(): PriceAutomatic + { + $price = new PriceAutomatic(); + $price->setType('esy-web'); + $price->setTitle('Esy-Web'); + $price->setPriceHt('100.00'); + $price->setStripeId('price_abc123'); + + return $price; + } + + private function createPriceWithoutStripe(): PriceAutomatic + { + $price = new PriceAutomatic(); + $price->setType('esy-mail'); + $price->setTitle('Esy-Mail'); + $price->setPriceHt('50.00'); + + return $price; + } + + public function testIndexWithMixedPrices(): void + { + $customerRepo = $this->createStub(CustomerRepository::class); + $customerRepo->method('count')->willReturn(10); + + $revendeurRepo = $this->createStub(RevendeurRepository::class); + $revendeurRepo->method('count')->willReturn(3); + + $priceRepo = $this->createStub(PriceAutomaticRepository::class); + $priceRepo->method('findAll')->willReturn([ + $this->createPriceWithStripe(), + $this->createPriceWithoutStripe(), + ]); + + $secretRepo = $this->createStub(StripeWebhookSecretRepository::class); + $secretRepo->method('findAll')->willReturn([]); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->index($customerRepo, $revendeurRepo, $priceRepo, $secretRepo); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testSyncCustomersSuccess(): void + { + $customer = $this->createStub(Customer::class); + + $customerRepo = $this->createStub(CustomerRepository::class); + $customerRepo->method('findAll')->willReturn([$customer]); + + $meilisearch = $this->createStub(MeilisearchService::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncCustomers($customerRepo, $meilisearch); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncCustomersError(): void + { + $customerRepo = $this->createStub(CustomerRepository::class); + $customerRepo->method('findAll')->willThrowException(new \RuntimeException('Connection refused')); + + $meilisearch = $this->createStub(MeilisearchService::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncCustomers($customerRepo, $meilisearch); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncRevendeursSuccess(): void + { + $revendeur = $this->createStub(Revendeur::class); + + $revendeurRepo = $this->createStub(RevendeurRepository::class); + $revendeurRepo->method('findAll')->willReturn([$revendeur]); + + $meilisearch = $this->createStub(MeilisearchService::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncRevendeurs($revendeurRepo, $meilisearch); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncRevendeursError(): void + { + $revendeurRepo = $this->createStub(RevendeurRepository::class); + $revendeurRepo->method('findAll')->willThrowException(new \RuntimeException('Timeout')); + + $meilisearch = $this->createStub(MeilisearchService::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncRevendeurs($revendeurRepo, $meilisearch); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncPricesSuccess(): void + { + $price = $this->createPriceWithStripe(); + + $priceRepo = $this->createStub(PriceAutomaticRepository::class); + $priceRepo->method('findAll')->willReturn([$price]); + + $meilisearch = $this->createStub(MeilisearchService::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncPrices($priceRepo, $meilisearch); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncPricesError(): void + { + $priceRepo = $this->createStub(PriceAutomaticRepository::class); + $priceRepo->method('findAll')->willThrowException(new \RuntimeException('Error')); + + $meilisearch = $this->createStub(MeilisearchService::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncPrices($priceRepo, $meilisearch); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncStripeWebhooksEmptyUrl(): void + { + $webhookService = $this->createStub(StripeWebhookService::class); + $secretRepo = $this->createStub(StripeWebhookSecretRepository::class); + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncStripeWebhooks($webhookService, $secretRepo, $em, ''); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncStripeWebhooksCreatedNew(): void + { + $webhookService = $this->createStub(StripeWebhookService::class); + $webhookService->method('createAllWebhooks')->willReturn([ + 'created' => [ + ['type' => 'Main Light', 'id' => 'we_123', 'status' => 'created', 'secret' => 'whsec_abc'], + ['type' => 'Main Instant', 'id' => 'we_456', 'status' => 'exists'], + ], + 'errors' => [], + ]); + + $secretRepo = $this->createStub(StripeWebhookSecretRepository::class); + $secretRepo->method('findByType')->willReturn(null); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncStripeWebhooks($webhookService, $secretRepo, $em, 'https://crm.siteconseil.fr'); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncStripeWebhooksUpdateExisting(): void + { + $existing = new StripeWebhookSecret(StripeWebhookSecret::TYPE_MAIN_LIGHT, 'old_secret', 'we_old'); + + $webhookService = $this->createStub(StripeWebhookService::class); + $webhookService->method('createAllWebhooks')->willReturn([ + 'created' => [ + ['type' => 'Main Light', 'id' => 'we_new', 'status' => 'created', 'secret' => 'whsec_new'], + ], + 'errors' => ['Connect webhook failed'], + ]); + + $secretRepo = $this->createStub(StripeWebhookSecretRepository::class); + $secretRepo->method('findByType')->willReturn($existing); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncStripeWebhooks($webhookService, $secretRepo, $em, 'https://crm.siteconseil.fr/'); + $this->assertSame(302, $response->getStatusCode()); + $this->assertSame('whsec_new', $existing->getSecret()); + $this->assertSame('we_new', $existing->getEndpointId()); + } + + public function testSyncStripePricesNoErrors(): void + { + $stripePriceService = $this->createStub(StripePriceService::class); + $stripePriceService->method('syncAll')->willReturn(['synced' => 5, 'errors' => []]); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncStripePrices($stripePriceService); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncStripePricesWithErrors(): void + { + $stripePriceService = $this->createStub(StripePriceService::class); + $stripePriceService->method('syncAll')->willReturn(['synced' => 3, 'errors' => ['Price X failed', 'Price Y failed']]); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncStripePrices($stripePriceService); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncAllSuccess(): void + { + $customerRepo = $this->createStub(CustomerRepository::class); + $customerRepo->method('findAll')->willReturn([$this->createStub(Customer::class)]); + + $revendeurRepo = $this->createStub(RevendeurRepository::class); + $revendeurRepo->method('findAll')->willReturn([$this->createStub(Revendeur::class)]); + + $priceRepo = $this->createStub(PriceAutomaticRepository::class); + $priceRepo->method('findAll')->willReturn([$this->createPriceWithStripe()]); + + $meilisearch = $this->createStub(MeilisearchService::class); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncAll($customerRepo, $revendeurRepo, $priceRepo, $meilisearch); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testSyncAllError(): void + { + $customerRepo = $this->createStub(CustomerRepository::class); + $customerRepo->method('findAll')->willReturn([]); + + $revendeurRepo = $this->createStub(RevendeurRepository::class); + $revendeurRepo->method('findAll')->willReturn([]); + + $priceRepo = $this->createStub(PriceAutomaticRepository::class); + $priceRepo->method('findAll')->willReturn([]); + + $meilisearch = $this->createStub(MeilisearchService::class); + $meilisearch->method('setupIndexes')->willThrowException(new \RuntimeException('Meilisearch down')); + + $controller = new SyncController(); + $controller->setContainer($this->createContainer()); + + $response = $controller->syncAll($customerRepo, $revendeurRepo, $priceRepo, $meilisearch); + $this->assertSame(302, $response->getStatusCode()); + } +} diff --git a/tests/Controller/AnalyticsControllerTest.php b/tests/Controller/AnalyticsControllerTest.php new file mode 100644 index 0000000..a0a0b46 --- /dev/null +++ b/tests/Controller/AnalyticsControllerTest.php @@ -0,0 +1,205 @@ +crypto = new AnalyticsCryptoService($this->analyticsSecret); + $this->validToken = substr(hash('sha256', $this->analyticsSecret.'_endpoint'), 0, 8); + } + + private function createBus(): MessageBusInterface + { + $bus = $this->createStub(MessageBusInterface::class); + $bus->method('dispatch')->willReturnCallback(fn($msg) => new Envelope($msg)); + return $bus; + } + + public function testTrackInvalidToken(): void + { + $controller = new AnalyticsController(); + $request = new Request([], [], [], [], [], [], '{}'); + + $response = $controller->track( + 'badtoken', + $this->analyticsSecret, + $request, + $this->crypto, + $this->createStub(EntityManagerInterface::class), + $this->createBus(), + ); + + $this->assertSame(404, $response->getStatusCode()); + } + + public function testTrackEmptyPayload(): void + { + $controller = new AnalyticsController(); + $request = new Request([], [], [], [], [], [], '{}'); + + $response = $controller->track( + $this->validToken, + $this->analyticsSecret, + $request, + $this->crypto, + $this->createStub(EntityManagerInterface::class), + $this->createBus(), + ); + + $this->assertSame(400, $response->getStatusCode()); + } + + public function testTrackInvalidEncryptedData(): void + { + $controller = new AnalyticsController(); + $request = new Request([], [], [], [], [], [], json_encode(['d' => 'invalid-base64-data'])); + + $response = $controller->track( + $this->validToken, + $this->analyticsSecret, + $request, + $this->crypto, + $this->createStub(EntityManagerInterface::class), + $this->createBus(), + ); + + $this->assertSame(403, $response->getStatusCode()); + } + + public function testTrackNewVisitorCreation(): void + { + $controller = new AnalyticsController(); + + $encrypted = $this->crypto->encrypt(['sw' => 1920, 'sh' => 1080, 'l' => 'fr']); + $request = new Request([], [], [], [], [], ['HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'], json_encode(['d' => $encrypted])); + + $em = $this->createStub(EntityManagerInterface::class); + + $response = $controller->track( + $this->validToken, + $this->analyticsSecret, + $request, + $this->crypto, + $em, + $this->createBus(), + ); + + $this->assertSame(200, $response->getStatusCode()); + $body = json_decode($response->getContent(), true); + $this->assertArrayHasKey('d', $body); + + $decrypted = $this->crypto->decrypt($body['d']); + $this->assertNotNull($decrypted); + $this->assertArrayHasKey('uid', $decrypted); + $this->assertArrayHasKey('h', $decrypted); + } + + public function testTrackPageViewWithValidHash(): void + { + $controller = new AnalyticsController(); + $uid = 'test-uid-1234'; + $hash = $this->crypto->generateVisitorHash($uid); + + $encrypted = $this->crypto->encrypt([ + 'uid' => $uid, + 'h' => $hash, + 'u' => '/test-page', + 't' => 'Test Page', + 'r' => 'https://google.com', + ]); + $request = new Request([], [], [], [], [], [], json_encode(['d' => $encrypted])); + + $response = $controller->track( + $this->validToken, + $this->analyticsSecret, + $request, + $this->crypto, + $this->createStub(EntityManagerInterface::class), + $this->createBus(), + ); + + $this->assertSame(204, $response->getStatusCode()); + } + + public function testTrackSetUserWithValidHash(): void + { + $controller = new AnalyticsController(); + $uid = 'test-uid-5678'; + $hash = $this->crypto->generateVisitorHash($uid); + + $encrypted = $this->crypto->encrypt([ + 'uid' => $uid, + 'h' => $hash, + 'setUser' => 42, + ]); + $request = new Request([], [], [], [], [], [], json_encode(['d' => $encrypted])); + + $response = $controller->track( + $this->validToken, + $this->analyticsSecret, + $request, + $this->crypto, + $this->createStub(EntityManagerInterface::class), + $this->createBus(), + ); + + $this->assertSame(204, $response->getStatusCode()); + } + + public function testTrackWithInvalidHash(): void + { + $controller = new AnalyticsController(); + + $encrypted = $this->crypto->encrypt([ + 'uid' => 'test-uid', + 'h' => 'wrong-hash', + ]); + $request = new Request([], [], [], [], [], [], json_encode(['d' => $encrypted])); + + $response = $controller->track( + $this->validToken, + $this->analyticsSecret, + $request, + $this->crypto, + $this->createStub(EntityManagerInterface::class), + $this->createBus(), + ); + + $this->assertSame(403, $response->getStatusCode()); + } + + public function testTrackWithMissingHash(): void + { + $controller = new AnalyticsController(); + + $encrypted = $this->crypto->encrypt([ + 'uid' => 'test-uid', + ]); + $request = new Request([], [], [], [], [], [], json_encode(['d' => $encrypted])); + + $response = $controller->track( + $this->validToken, + $this->analyticsSecret, + $request, + $this->crypto, + $this->createStub(EntityManagerInterface::class), + $this->createBus(), + ); + + $this->assertSame(403, $response->getStatusCode()); + } +} diff --git a/tests/Controller/AttestationControllerTest.php b/tests/Controller/AttestationControllerTest.php new file mode 100644 index 0000000..9a88db6 --- /dev/null +++ b/tests/Controller/AttestationControllerTest.php @@ -0,0 +1,163 @@ +createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/redirect-url'); + + $twig = $this->createStub(Environment::class); + $twig->method('render')->willReturn(''); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['twig', $twig], + ['router', $router], + ['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)], + ['security.token_storage', $this->createStub(TokenStorageInterface::class)], + ['request_stack', $stack], + ['parameter_bag', $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class)], + ]); + $controller->setContainer($container); + } + + public function testVerifyNotFound(): void + { + $repo = $this->createStub(AttestationRepository::class); + $repo->method('findOneBy')->willReturn(null); + + $controller = new AttestationController(); + $this->setupController($controller); + + $response = $controller->verify('REF-UNKNOWN', $repo, self::HMAC_SECRET); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testVerifyFound(): void + { + $attestation = new Attestation('access', '127.0.0.1', 'test@test.com', self::HMAC_SECRET); + + $repo = $this->createStub(AttestationRepository::class); + $repo->method('findOneBy')->willReturn($attestation); + + $controller = new AttestationController(); + $this->setupController($controller); + + $response = $controller->verify($attestation->getReference(), $repo, self::HMAC_SECRET); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testDownloadNotFound(): void + { + $repo = $this->createStub(AttestationRepository::class); + $repo->method('findOneBy')->willReturn(null); + + $controller = new AttestationController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->download('REF-UNKNOWN', $repo); + } + + public function testDownloadNoPdf(): void + { + $attestation = new Attestation('access', '127.0.0.1', 'test@test.com', self::HMAC_SECRET); + + $repo = $this->createStub(AttestationRepository::class); + $repo->method('findOneBy')->willReturn($attestation); + + $controller = new AttestationController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->download($attestation->getReference(), $repo); + } + + public function testDownloadWithPdf(): void + { + $attestation = new Attestation('access', '127.0.0.1', 'test@test.com', self::HMAC_SECRET); + $tmpFile = tempnam(sys_get_temp_dir(), 'att_test_'); + file_put_contents($tmpFile, '%PDF-test'); + $attestation->setPdfFileSigned($tmpFile); + + $repo = $this->createStub(AttestationRepository::class); + $repo->method('findOneBy')->willReturn($attestation); + + $controller = new AttestationController(); + $this->setupController($controller); + + $response = $controller->download($attestation->getReference(), $repo); + $this->assertSame(200, $response->getStatusCode()); + + @unlink($tmpFile); + } + + public function testAuditNotFound(): void + { + $repo = $this->createStub(AttestationRepository::class); + $repo->method('findOneBy')->willReturn(null); + + $controller = new AttestationController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->audit('REF-UNKNOWN', $repo); + } + + public function testAuditNoCertificate(): void + { + $attestation = new Attestation('deletion', '127.0.0.1', 'test@test.com', self::HMAC_SECRET); + + $repo = $this->createStub(AttestationRepository::class); + $repo->method('findOneBy')->willReturn($attestation); + + $controller = new AttestationController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->audit($attestation->getReference(), $repo); + } + + public function testAuditWithCertificate(): void + { + $attestation = new Attestation('deletion', '127.0.0.1', 'test@test.com', self::HMAC_SECRET); + $tmpFile = tempnam(sys_get_temp_dir(), 'cert_test_'); + file_put_contents($tmpFile, '%PDF-cert'); + $attestation->setPdfFileCertificate($tmpFile); + + $repo = $this->createStub(AttestationRepository::class); + $repo->method('findOneBy')->willReturn($attestation); + + $controller = new AttestationController(); + $this->setupController($controller); + + $response = $controller->audit($attestation->getReference(), $repo); + $this->assertSame(200, $response->getStatusCode()); + + @unlink($tmpFile); + } +} diff --git a/tests/Controller/CspReportControllerTest.php b/tests/Controller/CspReportControllerTest.php new file mode 100644 index 0000000..921676c --- /dev/null +++ b/tests/Controller/CspReportControllerTest.php @@ -0,0 +1,252 @@ +createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $paramBag = $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class); + $paramBag->method('get')->willReturn('admin@siteconseil.fr'); + $paramBag->method('has')->willReturn(true); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)], + ['security.token_storage', $this->createStub(TokenStorageInterface::class)], + ['request_stack', $stack], + ['parameter_bag', $paramBag], + ]); + $controller->setContainer($container); + } + + public function testGetReturns204(): void + { + $controller = new CspReportController(); + $response = $controller->get(); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportEmptyPayload(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $request = new Request([], [], [], [], [], [], ''); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(400, $response->getStatusCode()); + } + + public function testReportInvalidJson(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $request = new Request([], [], [], [], [], [], '{invalid'); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(400, $response->getStatusCode()); + } + + public function testReportIgnoredExtension(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => 'chrome-extension://abc123', + 'blocked-uri' => 'inline', + 'document-uri' => 'https://crm.siteconseil.fr', + 'violated-directive' => 'script-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportIgnoredMozExtension(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => 'moz-extension://abc', + 'blocked-uri' => '', + 'document-uri' => 'https://crm.siteconseil.fr', + 'violated-directive' => 'script-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportIgnoredLocalhost(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => 'http://localhost:3000/app.js', + 'blocked-uri' => 'eval', + 'document-uri' => 'https://crm.siteconseil.fr', + 'violated-directive' => 'script-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportIgnoredLocalDomain(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => '/app.js', + 'blocked-uri' => 'inline', + 'document-uri' => 'http://crm.local/dashboard', + 'violated-directive' => 'style-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportIgnoredWasmEval(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => 'https://crm.siteconseil.fr/app.js', + 'blocked-uri' => 'wasm-eval', + 'document-uri' => 'https://crm.siteconseil.fr', + 'violated-directive' => 'script-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportIgnoredAboutBlank(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => '', + 'blocked-uri' => 'about:blank', + 'document-uri' => 'https://crm.siteconseil.fr', + 'violated-directive' => 'frame-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportRealViolationSendsEmail(): void + { + $controller = new CspReportController(); + $this->setupController($controller); + $logger = $this->createStub(LoggerInterface::class); + $mailer = $this->createStub(MailerInterface::class); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => 'https://evil.com/inject.js', + 'blocked-uri' => 'https://evil.com', + 'document-uri' => 'https://crm.siteconseil.fr/dashboard', + 'violated-directive' => 'script-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $mailer, $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportRealViolationEmailFailure(): void + { + $controller = new CspReportController(); + $this->setupController($controller); + $logger = $this->createStub(LoggerInterface::class); + + $mailer = $this->createStub(MailerInterface::class); + $mailer->method('send')->willThrowException(new \Exception('SMTP error')); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => 'https://evil.com/inject.js', + 'blocked-uri' => 'https://evil.com', + 'document-uri' => 'https://crm.siteconseil.fr/dashboard', + 'violated-directive' => 'script-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $mailer, $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportWithoutCspReportWrapper(): void + { + $controller = new CspReportController(); + $this->setupController($controller); + $logger = $this->createStub(LoggerInterface::class); + $mailer = $this->createStub(MailerInterface::class); + + $payload = json_encode([ + 'source-file' => 'https://evil.com/inject.js', + 'blocked-uri' => 'https://evil.com', + 'document-uri' => 'https://crm.siteconseil.fr/dashboard', + 'violated-directive' => 'script-src', + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $mailer, $logger); + $this->assertSame(204, $response->getStatusCode()); + } + + public function testReportIgnoredNodeModulesInline(): void + { + $controller = new CspReportController(); + $logger = $this->createStub(LoggerInterface::class); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => '/home/user/node_modules/some-lib/index.js', + 'blocked-uri' => 'inline', + 'document-uri' => 'https://crm.siteconseil.fr', + 'violated-directive' => 'script-src', + ], + ]); + $request = new Request([], [], [], [], [], [], $payload); + $response = $controller->report($request, $this->createStub(MailerInterface::class), $logger); + $this->assertSame(204, $response->getStatusCode()); + } +} diff --git a/tests/Controller/EmailTrackingControllerTest.php b/tests/Controller/EmailTrackingControllerTest.php new file mode 100644 index 0000000..f34500a --- /dev/null +++ b/tests/Controller/EmailTrackingControllerTest.php @@ -0,0 +1,225 @@ +projectDir = sys_get_temp_dir().'/email_tracking_test_'.uniqid(); + mkdir($this->projectDir.'/public', 0775, true); + file_put_contents($this->projectDir.'/public/logo_facture.png', 'fake-png'); + } + + protected function tearDown(): void + { + @unlink($this->projectDir.'/public/logo_facture.png'); + @rmdir($this->projectDir.'/public'); + @rmdir($this->projectDir); + } + + private function setupController(EmailTrackingController $controller): void + { + $session = new Session(new MockArraySessionStorage()); + $stack = $this->createStub(RequestStack::class); + $stack->method('getSession')->willReturn($session); + + $router = $this->createStub(RouterInterface::class); + $router->method('generate')->willReturn('/email/msg123/attachment/0'); + + $container = $this->createStub(ContainerInterface::class); + $container->method('has')->willReturn(true); + $container->method('get')->willReturnMap([ + ['router', $router], + ['security.authorization_checker', $this->createStub(AuthorizationCheckerInterface::class)], + ['security.token_storage', $this->createStub(TokenStorageInterface::class)], + ['request_stack', $stack], + ['parameter_bag', $this->createStub(\Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface::class)], + ]); + $controller->setContainer($container); + } + + public function testTrackWithExistingTracking(): void + { + $tracking = new EmailTracking('msg-123', 'test@test.com', 'Subject'); + + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn($tracking); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new EmailTrackingController(); + $response = $controller->track('msg-123', $repo, $em, $this->projectDir); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('opened', $tracking->getState()); + } + + public function testTrackWithNonExistingTracking(): void + { + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn(null); + + $em = $this->createStub(EntityManagerInterface::class); + + $controller = new EmailTrackingController(); + $response = $controller->track('msg-unknown', $repo, $em, $this->projectDir); + + $this->assertSame(200, $response->getStatusCode()); + } + + public function testViewNotFound(): void + { + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn(null); + + $controller = new EmailTrackingController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->view('msg-unknown', $repo); + } + + public function testViewNoHtmlBody(): void + { + $tracking = new EmailTracking('msg-123', 'test@test.com', 'Subject'); + + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn($tracking); + + $controller = new EmailTrackingController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->view('msg-123', $repo); + } + + public function testViewWithHtmlBody(): void + { + $tracking = new EmailTracking('msg-123', 'test@test.com', 'Subject', 'Hello'); + + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn($tracking); + + $controller = new EmailTrackingController(); + $this->setupController($controller); + + $response = $controller->view('msg-123', $repo); + $this->assertSame(200, $response->getStatusCode()); + $this->assertStringContainsString('Hello', $response->getContent()); + } + + public function testViewWithAttachments(): void + { + $tmpFile = tempnam(sys_get_temp_dir(), 'att_'); + file_put_contents($tmpFile, 'test-content'); + + $tracking = new EmailTracking( + 'msg-456', + 'test@test.com', + 'Subject', + 'With attachment', + [['path' => $tmpFile, 'name' => 'document.pdf']], + ); + + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn($tracking); + + $controller = new EmailTrackingController(); + $this->setupController($controller); + + $response = $controller->view('msg-456', $repo); + $this->assertSame(200, $response->getStatusCode()); + $this->assertStringContainsString('Pieces jointes', $response->getContent()); + $this->assertStringContainsString('document.pdf', $response->getContent()); + + @unlink($tmpFile); + } + + public function testAttachmentNotFoundEmail(): void + { + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn(null); + + $controller = new EmailTrackingController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->attachment('msg-unknown', 0, $repo); + } + + public function testAttachmentIndexNotFound(): void + { + $tracking = new EmailTracking('msg-123', 'test@test.com', 'Subject', ''); + + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn($tracking); + + $controller = new EmailTrackingController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->attachment('msg-123', 0, $repo); + } + + public function testAttachmentFileNotExists(): void + { + $tracking = new EmailTracking( + 'msg-789', + 'test@test.com', + 'Subject', + '', + [['path' => '/nonexistent/file.pdf', 'name' => 'file.pdf']], + ); + + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn($tracking); + + $controller = new EmailTrackingController(); + $this->setupController($controller); + + $this->expectException(NotFoundHttpException::class); + $controller->attachment('msg-789', 0, $repo); + } + + public function testAttachmentSuccess(): void + { + $tmpFile = tempnam(sys_get_temp_dir(), 'att_dl_'); + file_put_contents($tmpFile, 'pdf-content'); + + $tracking = new EmailTracking( + 'msg-dl', + 'test@test.com', + 'Subject', + '', + [['path' => $tmpFile, 'name' => 'rapport.pdf']], + ); + + $repo = $this->createStub(EmailTrackingRepository::class); + $repo->method('findOneBy')->willReturn($tracking); + + $controller = new EmailTrackingController(); + $this->setupController($controller); + + $response = $controller->attachment('msg-dl', 0, $repo); + $this->assertSame(200, $response->getStatusCode()); + + @unlink($tmpFile); + } +} diff --git a/tests/Controller/MainControllersTest.php b/tests/Controller/MainControllersTest.php index 23c09c1..f6c0333 100644 --- a/tests/Controller/MainControllersTest.php +++ b/tests/Controller/MainControllersTest.php @@ -81,13 +81,96 @@ class MainControllersTest extends TestCase $this->assertSame(200, $controller->check()->getStatusCode()); } - public function testHomeIndex(): void + public function testHomeIndexNoUser(): void { $controller = new HomeController(); $this->setupController($controller); $this->assertSame(200, $controller->index()->getStatusCode()); } + public function testHomeIndexWithEmploye(): void + { + $user = new User(); + $user->setEmail('admin@test.com'); + $user->setFirstName('A'); + $user->setLastName('B'); + $user->setPassword('h'); + $user->setRoles(['ROLE_EMPLOYE']); + + $token = $this->createStub(\Symfony\Component\Security\Core\Authentication\Token\TokenInterface::class); + $token->method('getUser')->willReturn($user); + + $tokenStorage = $this->createStub(TokenStorageInterface::class); + $tokenStorage->method('getToken')->willReturn($token); + + $authChecker = $this->createStub(AuthorizationCheckerInterface::class); + $authChecker->method('isGranted')->willReturnCallback(fn ($role) => \in_array($role, ['ROLE_EMPLOYE', 'ROLE_USER'], true)); + + $controller = new HomeController(); + $this->setupController($controller, [ + 'security.token_storage' => $tokenStorage, + 'security.authorization_checker' => $authChecker, + ]); + + $response = $controller->index(); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testHomeIndexWithUserNoSpecificRole(): void + { + $user = new User(); + $user->setEmail('u@t.com'); + $user->setFirstName('U'); + $user->setLastName('T'); + $user->setPassword('h'); + + $token = $this->createStub(\Symfony\Component\Security\Core\Authentication\Token\TokenInterface::class); + $token->method('getUser')->willReturn($user); + + $tokenStorage = $this->createStub(TokenStorageInterface::class); + $tokenStorage->method('getToken')->willReturn($token); + + $authChecker = $this->createStub(AuthorizationCheckerInterface::class); + $authChecker->method('isGranted')->willReturn(false); + + $controller = new HomeController(); + $this->setupController($controller, [ + 'security.token_storage' => $tokenStorage, + 'security.authorization_checker' => $authChecker, + ]); + + $response = $controller->index(); + $this->assertSame(302, $response->getStatusCode()); + } + + public function testHomeIndexWithCustomer(): void + { + $user = new User(); + $user->setEmail('c@test.com'); + $user->setFirstName('C'); + $user->setLastName('D'); + $user->setPassword('h'); + $user->setRoles(['ROLE_CUSTOMER']); + + $token = $this->createStub(\Symfony\Component\Security\Core\Authentication\Token\TokenInterface::class); + $token->method('getUser')->willReturn($user); + + $tokenStorage = $this->createStub(TokenStorageInterface::class); + $tokenStorage->method('getToken')->willReturn($token); + + $authChecker = $this->createStub(AuthorizationCheckerInterface::class); + $authChecker->method('isGranted')->willReturnCallback(fn ($role) => \in_array($role, ['ROLE_CUSTOMER', 'ROLE_USER'], true)); + + $controller = new HomeController(); + $this->setupController($controller, [ + 'security.token_storage' => $tokenStorage, + 'security.authorization_checker' => $authChecker, + ]); + + $response = $controller->index(); + $this->assertSame(302, $response->getStatusCode()); + } + public function testSetPasswordSuccess(): void { $user = new User(); @@ -146,4 +229,76 @@ class MainControllersTest extends TestCase $response = $controller->index($request, $repo, $mailer, $em, $hasher, $twig); $this->assertSame(302, $response->getStatusCode()); } + + public function testForgotPasswordExpiredCode(): void + { + $repo = $this->createStub(UserRepository::class); + $mailer = $this->createStub(MailerService::class); + $em = $this->createStub(EntityManagerInterface::class); + $hasher = $this->createStub(UserPasswordHasherInterface::class); + $twig = $this->createStub(Environment::class); + + $controller = new ForgotPasswordController(); + $this->setupController($controller, ['twig' => $twig]); + + $session = new Session(new MockArraySessionStorage()); + $session->set('reset_code', 'abc123'); + $session->set('reset_email', 't@t.com'); + $session->set('reset_expires', time() - 100); + + $request = new Request([], ['action' => 'reset', 'code' => 'abc123', 'password' => 'newpassword8']); + $request->setMethod('POST'); + $request->setSession($session); + + $response = $controller->index($request, $repo, $mailer, $em, $hasher, $twig); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testForgotPasswordWrongCode(): void + { + $repo = $this->createStub(UserRepository::class); + $mailer = $this->createStub(MailerService::class); + $em = $this->createStub(EntityManagerInterface::class); + $hasher = $this->createStub(UserPasswordHasherInterface::class); + $twig = $this->createStub(Environment::class); + + $controller = new ForgotPasswordController(); + $this->setupController($controller, ['twig' => $twig]); + + $session = new Session(new MockArraySessionStorage()); + $session->set('reset_code', 'correct'); + $session->set('reset_email', 't@t.com'); + $session->set('reset_expires', time() + 600); + + $request = new Request([], ['action' => 'reset', 'code' => 'wrong', 'password' => 'newpassword8']); + $request->setMethod('POST'); + $request->setSession($session); + + $response = $controller->index($request, $repo, $mailer, $em, $hasher, $twig); + $this->assertSame(200, $response->getStatusCode()); + } + + public function testForgotPasswordShortPassword(): void + { + $repo = $this->createStub(UserRepository::class); + $mailer = $this->createStub(MailerService::class); + $em = $this->createStub(EntityManagerInterface::class); + $hasher = $this->createStub(UserPasswordHasherInterface::class); + $twig = $this->createStub(Environment::class); + + $controller = new ForgotPasswordController(); + $this->setupController($controller, ['twig' => $twig]); + + $session = new Session(new MockArraySessionStorage()); + $session->set('reset_code', 'abc123'); + $session->set('reset_email', 't@t.com'); + $session->set('reset_expires', time() + 600); + + $request = new Request([], ['action' => 'reset', 'code' => 'abc123', 'password' => 'short']); + $request->setMethod('POST'); + $request->setSession($session); + + $response = $controller->index($request, $repo, $mailer, $em, $hasher, $twig); + $this->assertSame(200, $response->getStatusCode()); + } } diff --git a/tests/Entity/ServiceMessageTest.php b/tests/Entity/ServiceMessageTest.php new file mode 100644 index 0000000..eacca8f --- /dev/null +++ b/tests/Entity/ServiceMessageTest.php @@ -0,0 +1,65 @@ +createService(); + $message = new ServiceMessage($service, 'Alert Title', 'Alert Content'); + + $this->assertNull($message->getId()); + $this->assertSame($service, $message->getService()); + $this->assertSame('Alert Title', $message->getTitle()); + $this->assertSame('Alert Content', $message->getContent()); + $this->assertSame('info', $message->getSeverity()); + $this->assertTrue($message->isActive()); + $this->assertNull($message->getAuthor()); + $this->assertInstanceOf(\DateTimeImmutable::class, $message->getCreatedAt()); + $this->assertNull($message->getResolvedAt()); + } + + public function testConstructorWithSeverityAndAuthor(): void + { + $service = $this->createService(); + $user = new User(); + $user->setEmail('admin@test.com'); + $user->setFirstName('Admin'); + $user->setLastName('User'); + $user->setPassword('h'); + + $message = new ServiceMessage($service, 'Critical', 'Server down', 'critical', $user); + + $this->assertSame('critical', $message->getSeverity()); + $this->assertSame($user, $message->getAuthor()); + } + + public function testResolve(): void + { + $service = $this->createService(); + $message = new ServiceMessage($service, 'Test', 'Content'); + + $this->assertTrue($message->isActive()); + $this->assertNull($message->getResolvedAt()); + + $result = $message->resolve(); + + $this->assertSame($message, $result); + $this->assertFalse($message->isActive()); + $this->assertInstanceOf(\DateTimeImmutable::class, $message->getResolvedAt()); + } +} diff --git a/tests/Entity/StripeWebhookSecretTest.php b/tests/Entity/StripeWebhookSecretTest.php new file mode 100644 index 0000000..0b1fb01 --- /dev/null +++ b/tests/Entity/StripeWebhookSecretTest.php @@ -0,0 +1,57 @@ +assertNull($secret->getId()); + $this->assertSame('main_light', $secret->getType()); + $this->assertSame('whsec_abc123', $secret->getSecret()); + $this->assertNull($secret->getEndpointId()); + $this->assertInstanceOf(\DateTimeImmutable::class, $secret->getCreatedAt()); + } + + public function testConstructorWithEndpointId(): void + { + $secret = new StripeWebhookSecret('connect_instant', 'whsec_xyz', 'we_456'); + + $this->assertSame('connect_instant', $secret->getType()); + $this->assertSame('whsec_xyz', $secret->getSecret()); + $this->assertSame('we_456', $secret->getEndpointId()); + } + + public function testSetSecret(): void + { + $secret = new StripeWebhookSecret('main_light', 'old_secret'); + $secret->setSecret('new_secret'); + + $this->assertSame('new_secret', $secret->getSecret()); + } + + public function testSetEndpointId(): void + { + $secret = new StripeWebhookSecret('main_light', 'whsec_abc'); + $this->assertNull($secret->getEndpointId()); + + $secret->setEndpointId('we_789'); + $this->assertSame('we_789', $secret->getEndpointId()); + + $secret->setEndpointId(null); + $this->assertNull($secret->getEndpointId()); + } + + public function testTypeConstants(): void + { + $this->assertSame('main_light', StripeWebhookSecret::TYPE_MAIN_LIGHT); + $this->assertSame('main_instant', StripeWebhookSecret::TYPE_MAIN_INSTANT); + $this->assertSame('connect_light', StripeWebhookSecret::TYPE_CONNECT_LIGHT); + $this->assertSame('connect_instant', StripeWebhookSecret::TYPE_CONNECT_INSTANT); + } +}