refactor: sécurité Discord webhook, tests 100% coverage, factorisation templates PDF et DNS
Sécurité - Discord Webhook :
- Suppression de l'URL Discord webhook en dur dans CheckDnsCommand (ligne 34)
- Ajout de la variable d'environnement DISCORD_WEBHOOK dans .env (vide par défaut)
- Injection via #[Autowire(env: 'DISCORD_WEBHOOK')] dans le constructeur
- Vérification que le webhook est configuré avant envoi ('' !== $this->discordWebhook)
- Remplacement de l'URL en dur dans discord-notify.yml par ${{ secrets.DISCORD_WEBHOOK }}
Factorisation DNS (suppression duplication SonarQube) :
- Création de src/Service/DnsInfraHelper.php avec les méthodes partagées :
enrichWithCloudflare, enrichLastCheck, loadCloudflareRecords, getActualDnsValue,
getMxValues, getFirstTxtValue, getSrvValue, checkMxExists, checkTxtContains,
checkDnsRecordExists, getTxtSpfValue
- Constantes DOMAINS et EXPECTED_MX centralisées dans DnsInfraHelper
- Refactorisation de CheckDnsCommand pour utiliser DnsInfraHelper au lieu des
méthodes privées dupliquées (enrichWithCloudflare, enrichLastCheck, etc.)
- Refactorisation de DnsReportController pour utiliser DnsInfraHelper au lieu
des méthodes privées dupliquées (enrichWithCloudflare, enrichLastCheck, etc.)
Factorisation templates PDF (suppression duplication lignes 6-22) :
- Création de templates/pdf/_base.html.twig comme layout commun avec :
CSS partagé (banner, container, info-grid, verify-box, hmac, contact-box, data tables),
blocs Twig configurables (title, font_size, extra_styles, content, verify_box,
hmac_section, footer_contact, signature_box, footer_legal)
- Refactorisation de rgpd_access.html.twig : extends _base, accent #4338ca,
bloc content avec sessions/events, styles session-meta et no-data
- Refactorisation de rgpd_deletion.html.twig : extends _base, accent #dc2626,
font 11px, bloc content avec attestation-box et warning
- Refactorisation de rgpd_no_data.html.twig : extends _base, accent #fabf04/#111827,
font 11px, bloc content avec attestation absence
- Refactorisation de admin/logs/pdf.html.twig : extends _base, accent #4338ca,
bloc content avec tables utilisateur/requête et HMAC verification box,
suppression du bloc signature, footer légal avec Siret/TVA
Tests - Couverture 100% (469 tests, 857 assertions, 0 failures) :
AnalyticsControllerTest (8 tests) :
- testTrackInvalidToken : token incorrect retourne 404
- testTrackEmptyPayload : payload sans clé 'd' retourne 400
- testTrackInvalidEncryptedData : données chiffrées invalides retourne 403
- testTrackNewVisitorCreation : création visiteur avec screen/language/UA, retourne uid+hash
- testTrackPageViewWithValidHash : page view avec uid/hash valides retourne 204
- testTrackSetUserWithValidHash : setUser avec uid/hash valides retourne 204
- testTrackWithInvalidHash : hash incorrect retourne 403
- testTrackWithMissingHash : hash absent retourne 403
AttestationControllerTest (8 tests) :
- testVerifyNotFound : référence inconnue retourne 200 (template not_found)
- testVerifyFound : attestation trouvée retourne 200 (template verify)
- testDownloadNotFound : référence inconnue lance NotFoundHttpException
- testDownloadNoPdf : attestation sans PDF lance NotFoundHttpException
- testDownloadWithPdf : attestation avec PDF signé retourne BinaryFileResponse 200
- testAuditNotFound : référence inconnue lance NotFoundHttpException
- testAuditNoCertificate : attestation sans certificat lance NotFoundHttpException
- testAuditWithCertificate : attestation avec certificat retourne BinaryFileResponse 200
CspReportControllerTest (13 tests) :
- testGetReturns204 : GET /my-csp-report retourne 204
- testReportEmptyPayload : payload vide retourne 400
- testReportInvalidJson : JSON invalide retourne 400
- testReportIgnoredExtension : chrome-extension ignoré, retourne 204
- testReportIgnoredMozExtension : moz-extension ignoré, retourne 204
- testReportIgnoredLocalhost : localhost ignoré, retourne 204
- testReportIgnoredLocalDomain : .local ignoré, retourne 204
- testReportIgnoredWasmEval : wasm-eval ignoré, retourne 204
- testReportIgnoredAboutBlank : about:blank ignoré, retourne 204
- testReportIgnoredNodeModulesInline : node_modules inline ignoré, retourne 204
- testReportRealViolationSendsEmail : violation réelle envoie email, retourne 204
- testReportRealViolationEmailFailure : échec email ne bloque pas, retourne 204
- testReportWithoutCspReportWrapper : payload sans wrapper csp-report fonctionne
EmailTrackingControllerTest (10 tests) :
- testTrackWithExistingTracking : tracking trouvé, markAsOpened appelé, état 'opened'
- testTrackWithNonExistingTracking : tracking absent, retourne image sans erreur
- testViewNotFound : messageId inconnu lance NotFoundHttpException
- testViewNoHtmlBody : tracking sans htmlBody lance NotFoundHttpException
- testViewWithHtmlBody : retourne HTML du tracking
- testViewWithAttachments : retourne HTML avec section pièces jointes
- testAttachmentNotFoundEmail : email inconnu lance NotFoundHttpException
- testAttachmentIndexNotFound : index absent lance NotFoundHttpException
- testAttachmentFileNotExists : fichier supprimé lance NotFoundHttpException
- testAttachmentSuccess : téléchargement pièce jointe retourne BinaryFileResponse
StatsControllerTest (4 tests) :
- testIndexCurrentPeriod : période 'current', dates du mois en cours
- testIndexCustomPeriod : période 'custom' avec from/to explicites
- testIndexMonthsPeriod : période '3', dateFrom = -3 mois
- testIndexDefaultPeriod : pas de paramètre, défaut 'current'
StatusControllerTest (20 tests) :
- testIndexEmpty : catégories vides retourne 200
- testIndexWithServices : catégorie avec service, appel getHistoryForDays/getDailyStatus
- testManage : page gestion retourne 200
- testCategoryCreateEmptyName : nom vide redirige avec flash error
- testCategoryCreateSuccess : création catégorie avec position redirige avec flash success
- testCategoryDelete : suppression catégorie redirige avec flash success
- testServiceCreateEmptyName : nom vide redirige avec flash error
- testServiceCreateCategoryNotFound : catégorie inexistante redirige avec flash error
- testServiceCreateSuccess : création service avec URL redirige avec flash success
- testServiceCreateWithExternalType : création service externe avec type http_check
- testServiceDelete : suppression service redirige avec flash success
- testUpdateValidStatus : statut 'down' avec message, setStatus appelé
- testUpdateInvalidStatus : statut invalide redirige avec flash error
- testUpdateStatusWithEmptyMessage : statut 'up' sans message (null passé)
- testMessageCreateEmptyFields : champs vides redirige avec flash error
- testMessageCreateServiceNotFound : service inexistant redirige avec flash error
- testMessageCreateSuccessNoUser : message créé sans utilisateur connecté
- testMessageCreateSuccessWithUser : message créé avec User injecté via tokenStorage
- testMessageResolve : message résolu, isActive=false, resolvedAt non null
- testApiDaily : retourne JsonResponse avec données getDailyStatus
SyncControllerTest (14 tests) :
- testIndexWithMixedPrices : prix avec/sans stripeId, compteurs stripeSynced/stripeNotSynced
- testSyncCustomersSuccess : indexation 1 client dans Meilisearch
- testSyncCustomersError : exception findAll, flash error
- testSyncRevendeursSuccess : indexation 1 revendeur dans Meilisearch
- testSyncRevendeursError : exception findAll, flash error
- testSyncPricesSuccess : indexation 1 tarif dans Meilisearch
- testSyncPricesError : exception findAll, flash error
- testSyncStripeWebhooksEmptyUrl : WEBHOOK_BASE_URL vide, flash error
- testSyncStripeWebhooksCreatedNew : webhook créé + webhook existant, persist nouveau secret
- testSyncStripeWebhooksUpdateExisting : mise à jour secret existant + erreurs Stripe
- testSyncStripePricesNoErrors : sync sans erreurs, flash success
- testSyncStripePricesWithErrors : sync avec erreurs, flash success + flash errors
- testSyncAllSuccess : sync all avec données, flash success
- testSyncAllError : exception setupIndexes, flash error
ServiceMessageTest (3 tests) :
- testConstructorDefaults : valeurs par défaut (info, active, null author/resolvedAt)
- testConstructorWithSeverityAndAuthor : severity custom + User author
- testResolve : isActive=false, resolvedAt DateTimeImmutable, fluent return
StripeWebhookSecretTest (4 tests) :
- testConstructorDefaults : type/secret, endpointId null, createdAt DateTimeImmutable
- testConstructorWithEndpointId : constructeur avec 3 arguments
- testSetSecret : modification du secret
- testSetEndpointId : set/unset endpointId (nullable)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
4
.env
4
.env
@@ -112,3 +112,7 @@ DOCUSEAL_API=
|
|||||||
DOCUSEAL_WEBHOOKS_SECRET_HEADER=X-Sign
|
DOCUSEAL_WEBHOOKS_SECRET_HEADER=X-Sign
|
||||||
DOCUSEAL_WEBHOOKS_SECRET=
|
DOCUSEAL_WEBHOOKS_SECRET=
|
||||||
###< docuseal ###
|
###< docuseal ###
|
||||||
|
|
||||||
|
###> discord ###
|
||||||
|
DISCORD_WEBHOOK=
|
||||||
|
###< discord ###
|
||||||
|
|||||||
@@ -61,4 +61,4 @@ jobs:
|
|||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d @/tmp/discord.json \
|
-d @/tmp/discord.json \
|
||||||
"https://discord.com/api/webhooks/1419573620602044518/ikAdxWxsrrTqMTb5Gh_8ylcoJHlOnq7aJZvR5udoS_fCK56Jk3qpEnJHVKdD8fwuNJF3"
|
"${{ secrets.DISCORD_WEBHOOK }}"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
namespace App\Command;
|
namespace App\Command;
|
||||||
|
|
||||||
use App\Service\AwsSesService;
|
use App\Service\AwsSesService;
|
||||||
use App\Service\CloudflareService;
|
|
||||||
use App\Service\DnsCheckService;
|
use App\Service\DnsCheckService;
|
||||||
|
use App\Service\DnsInfraHelper;
|
||||||
use App\Service\MailcowService;
|
use App\Service\MailcowService;
|
||||||
use App\Service\MailerService;
|
use App\Service\MailerService;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
@@ -23,26 +23,19 @@ use Twig\Environment;
|
|||||||
)]
|
)]
|
||||||
class CheckDnsCommand extends Command
|
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 MONITOR_EMAIL = 'monitor@siteconseil.fr';
|
||||||
private const DISCORD_WEBHOOK = 'https://discord.com/api/webhooks/1419573620602044518/ikAdxWxsrrTqMTb5Gh_8ylcoJHlOnq7aJZvR5udoS_fCK56Jk3qpEnJHVKdD8fwuNJF3';
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private DnsCheckService $dnsCheck,
|
private DnsCheckService $dnsCheck,
|
||||||
private AwsSesService $awsSes,
|
private AwsSesService $awsSes,
|
||||||
private CloudflareService $cloudflare,
|
|
||||||
private MailcowService $mailcow,
|
private MailcowService $mailcow,
|
||||||
private MailerService $mailer,
|
private MailerService $mailer,
|
||||||
private Environment $twig,
|
private Environment $twig,
|
||||||
private HttpClientInterface $httpClient,
|
private HttpClientInterface $httpClient,
|
||||||
private UrlGeneratorInterface $urlGenerator,
|
private UrlGeneratorInterface $urlGenerator,
|
||||||
|
private DnsInfraHelper $helper,
|
||||||
#[Autowire('%kernel.environment%')] private string $appEnv,
|
#[Autowire('%kernel.environment%')] private string $appEnv,
|
||||||
|
#[Autowire(env: 'DISCORD_WEBHOOK')] private string $discordWebhook = '',
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -57,39 +50,30 @@ class CheckDnsCommand extends Command
|
|||||||
$successes = [];
|
$successes = [];
|
||||||
$domainResults = [];
|
$domainResults = [];
|
||||||
|
|
||||||
// Charger les records Cloudflare une seule fois par domaine
|
|
||||||
$cfRecordsByDomain = $this->loadCloudflareRecords($io);
|
$cfRecordsByDomain = $this->loadCloudflareRecords($io);
|
||||||
|
|
||||||
foreach (self::DOMAINS as $domain) {
|
foreach (DnsInfraHelper::DOMAINS as $domain) {
|
||||||
$io->section('Domaine : '.$domain);
|
$io->section('Domaine : '.$domain);
|
||||||
$checks = [];
|
$checks = [];
|
||||||
$cfRecords = $cfRecordsByDomain[$domain] ?? [];
|
$cfRecords = $cfRecordsByDomain[$domain] ?? [];
|
||||||
|
|
||||||
// DNS dig + enrichissement Cloudflare
|
|
||||||
$this->dnsCheck->checkSpf($domain, $checks, $errors, $warnings, $successes);
|
$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->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, DnsInfraHelper::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes);
|
||||||
|
$this->helper->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
|
||||||
$this->dnsCheck->checkMx($domain, self::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes);
|
|
||||||
$this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
|
|
||||||
|
|
||||||
$this->dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes);
|
$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);
|
$this->dnsCheck->checkWhois($domain, $checks, $errors, $warnings, $successes);
|
||||||
|
|
||||||
// AWS SES
|
|
||||||
$this->checkAwsSes($domain, $checks, $errors, $successes, $cfRecords);
|
$this->checkAwsSes($domain, $checks, $errors, $successes, $cfRecords);
|
||||||
|
|
||||||
// Mailcow
|
|
||||||
$this->checkMailcow($domain, $checks, $errors, $warnings, $successes, $cfRecords);
|
$this->checkMailcow($domain, $checks, $errors, $warnings, $successes, $cfRecords);
|
||||||
|
|
||||||
// Affichage console
|
|
||||||
foreach ($checks as $check) {
|
foreach ($checks as $check) {
|
||||||
$icon = match ($check['status']) {
|
$icon = match ($check['status']) {
|
||||||
'ok' => '<fg=green>OK</>',
|
'ok' => '<fg=green>OK</>',
|
||||||
@@ -127,82 +111,15 @@ class CheckDnsCommand extends Command
|
|||||||
*/
|
*/
|
||||||
private function loadCloudflareRecords(SymfonyStyle $io): array
|
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.');
|
$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;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param list<array> $checks
|
|
||||||
* @param list<array<string, mixed>> $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<array> $checks
|
|
||||||
* @param list<array<string, mixed>> $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<array> $checks
|
* @param list<array> $checks
|
||||||
* @param list<string> $errors
|
* @param list<string> $errors
|
||||||
@@ -218,7 +135,6 @@ class CheckDnsCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Verification du domaine
|
|
||||||
$verif = $this->awsSes->isDomainVerified($domain);
|
$verif = $this->awsSes->isDomainVerified($domain);
|
||||||
$checks[] = DnsCheckService::check(
|
$checks[] = DnsCheckService::check(
|
||||||
'AWS SES', 'Domaine', 'Success' === $verif ? 'ok' : 'error',
|
'AWS SES', 'Domaine', 'Success' === $verif ? 'ok' : 'error',
|
||||||
@@ -230,7 +146,6 @@ class CheckDnsCommand extends Command
|
|||||||
$errors[] = "[$domain] AWS SES : domaine non verifie ($verif)";
|
$errors[] = "[$domain] AWS SES : domaine non verifie ($verif)";
|
||||||
}
|
}
|
||||||
|
|
||||||
// DKIM - verifier les 3 tokens CNAME
|
|
||||||
$dkim = $this->awsSes->getDkimStatus($domain);
|
$dkim = $this->awsSes->getDkimStatus($domain);
|
||||||
$dkimOk = $dkim['enabled'] && $dkim['verified'];
|
$dkimOk = $dkim['enabled'] && $dkim['verified'];
|
||||||
|
|
||||||
@@ -247,7 +162,6 @@ class CheckDnsCommand extends Command
|
|||||||
$errors[] = "[$domain] AWS SES DKIM : non active ou non verifiee";
|
$errors[] = "[$domain] AWS SES DKIM : non active ou non verifiee";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifier chaque token DKIM dans le DNS
|
|
||||||
foreach ($dkim['tokens'] as $token) {
|
foreach ($dkim['tokens'] as $token) {
|
||||||
$expectedCname = $token.'.dkim.amazonses.com';
|
$expectedCname = $token.'.dkim.amazonses.com';
|
||||||
$dkimFqdn = $token.'._domainkey.'.$domain;
|
$dkimFqdn = $token.'._domainkey.'.$domain;
|
||||||
@@ -261,7 +175,7 @@ class CheckDnsCommand extends Command
|
|||||||
$actualCname ?? 'Non trouve'
|
$actualCname ?? 'Non trouve'
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->enrichLastCheck($checks, $dkimFqdn, 'CNAME', $cfRecords);
|
$this->helper->enrichLastCheck($checks, $dkimFqdn, 'CNAME', $cfRecords);
|
||||||
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$successes[] = "[$domain] AWS SES DKIM CNAME $token : OK";
|
$successes[] = "[$domain] AWS SES DKIM CNAME $token : OK";
|
||||||
@@ -270,7 +184,6 @@ class CheckDnsCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MAIL FROM
|
|
||||||
$mailFrom = $this->awsSes->getMailFromStatus($domain);
|
$mailFrom = $this->awsSes->getMailFromStatus($domain);
|
||||||
$mailFromDomain = $mailFrom['mail_from_domain'];
|
$mailFromDomain = $mailFrom['mail_from_domain'];
|
||||||
|
|
||||||
@@ -289,11 +202,10 @@ class CheckDnsCommand extends Command
|
|||||||
$errors[] = "[$domain] AWS SES MAIL FROM : $mailFromDomain statut $mailFromStatus";
|
$errors[] = "[$domain] AWS SES MAIL FROM : $mailFromDomain statut $mailFromStatus";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifier le MX du MAIL FROM dans le DNS
|
|
||||||
$mxExpected = $mailFrom['mx_expected'];
|
$mxExpected = $mailFrom['mx_expected'];
|
||||||
if (null !== $mxExpected) {
|
if (null !== $mxExpected) {
|
||||||
$mxFound = $this->checkMxExists($mailFromDomain, $mxExpected);
|
$mxFound = $this->helper->checkMxExists($mailFromDomain, $mxExpected);
|
||||||
$actualMx = $this->getMxValues($mailFromDomain);
|
$actualMx = $this->helper->getMxValues($mailFromDomain);
|
||||||
$checks[] = DnsCheckService::check(
|
$checks[] = DnsCheckService::check(
|
||||||
'AWS SES', 'MAIL FROM MX', $mxFound ? 'ok' : 'error',
|
'AWS SES', 'MAIL FROM MX', $mxFound ? 'ok' : 'error',
|
||||||
$mxFound ? 'Present' : 'Absent',
|
$mxFound ? 'Present' : 'Absent',
|
||||||
@@ -301,7 +213,7 @@ class CheckDnsCommand extends Command
|
|||||||
$actualMx ?: 'Non trouve'
|
$actualMx ?: 'Non trouve'
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->enrichLastCheck($checks, $mailFromDomain, 'MX', $cfRecords);
|
$this->helper->enrichLastCheck($checks, $mailFromDomain, 'MX', $cfRecords);
|
||||||
|
|
||||||
if ($mxFound) {
|
if ($mxFound) {
|
||||||
$successes[] = "[$domain] AWS SES MAIL FROM MX : OK";
|
$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'];
|
$txtExpected = $mailFrom['txt_expected'];
|
||||||
if (null !== $txtExpected) {
|
if (null !== $txtExpected) {
|
||||||
$txtFound = $this->checkTxtContains($mailFromDomain, 'v=spf1');
|
$txtFound = $this->helper->checkTxtContains($mailFromDomain, 'v=spf1');
|
||||||
$actualTxt = $this->getTxtSpfValue($mailFromDomain);
|
$actualTxt = $this->helper->getTxtSpfValue($mailFromDomain);
|
||||||
$checks[] = DnsCheckService::check(
|
$checks[] = DnsCheckService::check(
|
||||||
'AWS SES', 'MAIL FROM TXT', $txtFound ? 'ok' : 'error',
|
'AWS SES', 'MAIL FROM TXT', $txtFound ? 'ok' : 'error',
|
||||||
$txtFound ? 'Present' : 'Absent',
|
$txtFound ? 'Present' : 'Absent',
|
||||||
"$mailFromDomain TXT $txtExpected",
|
"$mailFromDomain TXT $txtExpected",
|
||||||
$actualTxt ?: 'Non trouve'
|
$actualTxt ?: 'Non trouve'
|
||||||
);
|
);
|
||||||
$this->enrichLastCheck($checks, $mailFromDomain, 'TXT', $cfRecords);
|
$this->helper->enrichLastCheck($checks, $mailFromDomain, 'TXT', $cfRecords);
|
||||||
|
|
||||||
if ($txtFound) {
|
if ($txtFound) {
|
||||||
$successes[] = "[$domain] AWS SES MAIL FROM SPF : OK";
|
$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');
|
$checks[] = DnsCheckService::check('AWS SES', 'MAIL FROM', 'warning', 'Non configure', 'bounce.'.$domain, 'N/A');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifications bounce
|
|
||||||
$notif = $this->awsSes->getNotificationStatus($domain);
|
$notif = $this->awsSes->getNotificationStatus($domain);
|
||||||
$bounceOk = $notif['forwarding'] || null !== $notif['bounce_topic'];
|
$bounceOk = $notif['forwarding'] || null !== $notif['bounce_topic'];
|
||||||
$bounceDetail = $notif['forwarding'] ? 'Forwarding actif' : ($notif['bounce_topic'] ?? 'Non configure');
|
$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<array> $checks
|
* @param list<array> $checks
|
||||||
* @param list<string> $errors
|
* @param list<string> $errors
|
||||||
@@ -440,13 +289,12 @@ class CheckDnsCommand extends Command
|
|||||||
$errors[] = "[$domain] Mailcow : desactive";
|
$errors[] = "[$domain] Mailcow : desactive";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifier les enregistrements DNS attendus par Mailcow
|
|
||||||
$expectedRecords = $this->mailcow->getExpectedDnsRecords($domain);
|
$expectedRecords = $this->mailcow->getExpectedDnsRecords($domain);
|
||||||
foreach ($expectedRecords as $expected) {
|
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'];
|
$isOptional = $expected['optional'];
|
||||||
$label = $expected['type'].' '.$expected['name'];
|
$label = $expected['type'].' '.$expected['name'];
|
||||||
$digValue = $this->getActualDnsValue($expected['type'], $expected['name']);
|
$digValue = $this->helper->getActualDnsValue($expected['type'], $expected['name']);
|
||||||
|
|
||||||
$checks[] = DnsCheckService::check(
|
$checks[] = DnsCheckService::check(
|
||||||
'Mailcow DNS', $label, $found ? 'ok' : ($isOptional ? 'warning' : 'error'),
|
'Mailcow DNS', $label, $found ? 'ok' : ($isOptional ? 'warning' : 'error'),
|
||||||
@@ -454,7 +302,7 @@ class CheckDnsCommand extends Command
|
|||||||
$expected['content'], $digValue ?: 'Non trouve'
|
$expected['content'], $digValue ?: 'Non trouve'
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords);
|
$this->helper->enrichLastCheck($checks, $expected['name'], $expected['type'], $cfRecords);
|
||||||
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$successes[] = "[$domain] Mailcow DNS : $label OK";
|
$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<string> $errors
|
* @param list<string> $errors
|
||||||
* @param list<string> $warnings
|
* @param list<string> $warnings
|
||||||
@@ -537,7 +347,7 @@ class CheckDnsCommand extends Command
|
|||||||
'statusColor' => $statusColor,
|
'statusColor' => $statusColor,
|
||||||
'statusText' => $statusText,
|
'statusText' => $statusText,
|
||||||
'date' => new \DateTimeImmutable(),
|
'date' => new \DateTimeImmutable(),
|
||||||
'domains' => self::DOMAINS,
|
'domains' => DnsInfraHelper::DOMAINS,
|
||||||
'domainResults' => $domainResults,
|
'domainResults' => $domainResults,
|
||||||
'reportUrl' => '__DNS_REPORT_URL__',
|
'reportUrl' => '__DNS_REPORT_URL__',
|
||||||
]);
|
]);
|
||||||
@@ -553,8 +363,8 @@ class CheckDnsCommand extends Command
|
|||||||
1, // Priority HIGH
|
1, // Priority HIGH
|
||||||
);
|
);
|
||||||
|
|
||||||
// Notification Discord uniquement en prod
|
// Notification Discord uniquement en prod avec webhook configure
|
||||||
if ('prod' === $this->appEnv) {
|
if ('prod' === $this->appEnv && '' !== $this->discordWebhook) {
|
||||||
$this->sendDiscordNotification($errors, $warnings, $successes);
|
$this->sendDiscordNotification($errors, $warnings, $successes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,17 +380,17 @@ class CheckDnsCommand extends Command
|
|||||||
$hasWarnings = [] !== $warnings;
|
$hasWarnings = [] !== $warnings;
|
||||||
|
|
||||||
if ($hasErrors) {
|
if ($hasErrors) {
|
||||||
$color = 0xDC2626; // rouge
|
$color = 0xDC2626;
|
||||||
$title = 'ALERTE DNS - '.\count($errors).' erreur(s)';
|
$title = 'ALERTE DNS - '.\count($errors).' erreur(s)';
|
||||||
} elseif ($hasWarnings) {
|
} elseif ($hasWarnings) {
|
||||||
$color = 0xF59E0B; // jaune
|
$color = 0xF59E0B;
|
||||||
$title = 'DNS - '.\count($warnings).' avertissement(s)';
|
$title = 'DNS - '.\count($warnings).' avertissement(s)';
|
||||||
} else {
|
} else {
|
||||||
$color = 0x16A34A; // vert
|
$color = 0x16A34A;
|
||||||
$title = 'DNS - Configuration OK';
|
$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";
|
$description .= "**".\count($successes)."** verification(s) OK\n";
|
||||||
if ($hasErrors) {
|
if ($hasErrors) {
|
||||||
$description .= "**".\count($errors)."** erreur(s)\n";
|
$description .= "**".\count($errors)."** erreur(s)\n";
|
||||||
@@ -596,7 +406,7 @@ class CheckDnsCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->httpClient->request('POST', self::DISCORD_WEBHOOK, [
|
$this->httpClient->request('POST', $this->discordWebhook, [
|
||||||
'json' => [
|
'json' => [
|
||||||
'embeds' => [[
|
'embeds' => [[
|
||||||
'title' => 'Esy-Infra : '.$title,
|
'title' => 'Esy-Infra : '.$title,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ namespace App\Controller;
|
|||||||
|
|
||||||
use App\Repository\EmailTrackingRepository;
|
use App\Repository\EmailTrackingRepository;
|
||||||
use App\Service\AwsSesService;
|
use App\Service\AwsSesService;
|
||||||
use App\Service\CloudflareService;
|
|
||||||
use App\Service\DnsCheckService;
|
use App\Service\DnsCheckService;
|
||||||
|
use App\Service\DnsInfraHelper;
|
||||||
use App\Service\MailcowService;
|
use App\Service\MailcowService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -16,20 +16,13 @@ use Symfony\Contracts\Cache\ItemInterface;
|
|||||||
|
|
||||||
class DnsReportController extends AbstractController
|
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'])]
|
#[Route('/email/configuration/{token}', name: 'app_dns_report', methods: ['GET'])]
|
||||||
public function __invoke(
|
public function __invoke(
|
||||||
string $token,
|
string $token,
|
||||||
DnsCheckService $dnsCheck,
|
DnsCheckService $dnsCheck,
|
||||||
AwsSesService $awsSes,
|
AwsSesService $awsSes,
|
||||||
CloudflareService $cloudflare,
|
|
||||||
MailcowService $mailcow,
|
MailcowService $mailcow,
|
||||||
|
DnsInfraHelper $helper,
|
||||||
EmailTrackingRepository $emailTrackingRepository,
|
EmailTrackingRepository $emailTrackingRepository,
|
||||||
#[Autowire(service: 'dns_infra_cache')] CacheInterface $cache,
|
#[Autowire(service: 'dns_infra_cache')] CacheInterface $cache,
|
||||||
): Response {
|
): Response {
|
||||||
@@ -41,36 +34,36 @@ class DnsReportController extends AbstractController
|
|||||||
|
|
||||||
$cacheKey = 'dns_infra_check_'.$token;
|
$cacheKey = 'dns_infra_check_'.$token;
|
||||||
|
|
||||||
$data = $cache->get($cacheKey, function (ItemInterface $item) use ($dnsCheck, $awsSes, $cloudflare, $mailcow): array {
|
$data = $cache->get($cacheKey, function (ItemInterface $item) use ($dnsCheck, $awsSes, $mailcow, $helper): array {
|
||||||
$item->expiresAfter(3600); // 1 heure
|
$item->expiresAfter(3600);
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$warnings = [];
|
$warnings = [];
|
||||||
$successes = [];
|
$successes = [];
|
||||||
$domainResults = [];
|
$domainResults = [];
|
||||||
|
|
||||||
$cfRecordsByDomain = $this->loadCloudflareRecords($cloudflare);
|
$cfRecordsByDomain = $helper->loadCloudflareRecords();
|
||||||
|
|
||||||
foreach (self::DOMAINS as $domain) {
|
foreach (DnsInfraHelper::DOMAINS as $domain) {
|
||||||
$checks = [];
|
$checks = [];
|
||||||
$cfRecords = $cfRecordsByDomain[$domain] ?? [];
|
$cfRecords = $cfRecordsByDomain[$domain] ?? [];
|
||||||
|
|
||||||
$dnsCheck->checkSpf($domain, $checks, $errors, $warnings, $successes);
|
$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);
|
$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);
|
$dnsCheck->checkMx($domain, DnsInfraHelper::EXPECTED_MX[$domain] ?? '', $checks, $errors, $successes);
|
||||||
$this->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
|
$helper->enrichWithCloudflare($checks, $domain, 'MX', 'MX', $cfRecords);
|
||||||
|
|
||||||
$dnsCheck->checkBounce($domain, $checks, $errors, $warnings, $successes);
|
$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);
|
$dnsCheck->checkWhois($domain, $checks, $errors, $warnings, $successes);
|
||||||
|
|
||||||
$this->checkAwsSes($domain, $awsSes, $dnsCheck, $checks, $errors, $successes, $cfRecords);
|
$this->checkAwsSes($domain, $awsSes, $dnsCheck, $helper, $checks, $errors, $successes, $cfRecords);
|
||||||
$this->checkMailcow($domain, $mailcow, $dnsCheck, $checks, $errors, $warnings, $successes, $cfRecords);
|
$this->checkMailcow($domain, $mailcow, $helper, $checks, $errors, $warnings, $successes, $cfRecords);
|
||||||
|
|
||||||
$domainResults[] = ['domain' => $domain, 'checks' => $checks];
|
$domainResults[] = ['domain' => $domain, 'checks' => $checks];
|
||||||
}
|
}
|
||||||
@@ -93,85 +86,13 @@ class DnsReportController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, list<array<string, mixed>>>
|
|
||||||
*/
|
|
||||||
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<array> $checks
|
|
||||||
* @param list<array<string, mixed>> $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<array> $checks
|
|
||||||
* @param list<array<string, mixed>> $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<array> $checks
|
* @param list<array> $checks
|
||||||
* @param list<string> $errors
|
* @param list<string> $errors
|
||||||
* @param list<string> $successes
|
* @param list<string> $successes
|
||||||
* @param list<array<string, mixed>> $cfRecords
|
* @param list<array<string, mixed>> $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()) {
|
if (!$awsSes->isAvailable()) {
|
||||||
return;
|
return;
|
||||||
@@ -191,7 +112,7 @@ class DnsReportController extends AbstractController
|
|||||||
$actualCname = $dnsCheck->getCnameRecord($dkimFqdn);
|
$actualCname = $dnsCheck->getCnameRecord($dkimFqdn);
|
||||||
$found = null !== $actualCname && str_contains($actualCname, 'dkim.amazonses.com');
|
$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');
|
$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);
|
$mailFrom = $awsSes->getMailFromStatus($domain);
|
||||||
@@ -206,14 +127,14 @@ class DnsReportController extends AbstractController
|
|||||||
}
|
}
|
||||||
$mxFound = !empty(array_filter($mxValues, fn ($v) => str_contains($v, 'amazonses.com')));
|
$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');
|
$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']) {
|
if (null !== $mailFrom['txt_expected']) {
|
||||||
$txtValue = $this->getFirstTxtValue($dnsCheck, $mfd);
|
$txtValue = $helper->getFirstTxtValue($mfd);
|
||||||
$txtFound = str_contains($txtValue, 'spf1');
|
$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');
|
$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) {
|
} catch (\Throwable) {
|
||||||
@@ -227,7 +148,7 @@ class DnsReportController extends AbstractController
|
|||||||
* @param list<string> $successes
|
* @param list<string> $successes
|
||||||
* @param list<array<string, mixed>> $cfRecords
|
* @param list<array<string, mixed>> $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()) {
|
if (!$mailcow->isAvailable()) {
|
||||||
return;
|
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');
|
$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) {
|
foreach ($mailcow->getExpectedDnsRecords($domain) as $expected) {
|
||||||
$found = match ($expected['type']) {
|
$found = $helper->checkDnsRecordExists($expected['type'], $expected['name'], $expected['content']);
|
||||||
'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,
|
|
||||||
};
|
|
||||||
|
|
||||||
$label = $expected['type'].' '.$expected['name'];
|
$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');
|
$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) {
|
} 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
212
src/Service/DnsInfraHelper.php
Normal file
212
src/Service/DnsInfraHelper.php
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
class DnsInfraHelper
|
||||||
|
{
|
||||||
|
public const DOMAINS = ['siteconseil.fr', 'esy-web.dev'];
|
||||||
|
|
||||||
|
public const EXPECTED_MX = [
|
||||||
|
'siteconseil.fr' => 'mail.esy-web.dev',
|
||||||
|
'esy-web.dev' => 'mail.esy-web.dev',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private DnsCheckService $dnsCheck,
|
||||||
|
private CloudflareService $cloudflare,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, list<array<string, mixed>>>
|
||||||
|
*/
|
||||||
|
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<array> $checks
|
||||||
|
* @param list<array<string, mixed>> $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<array> $checks
|
||||||
|
* @param list<array<string, mixed>> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
{% extends 'pdf/_base.html.twig' %}
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
{% block title %}Log #{{ log.id }} - CRM SITECONSEIL{% endblock %}
|
||||||
<meta charset="UTF-8">
|
{% block data_table_mt %}8px{% endblock %}
|
||||||
<title>Log #{{ log.id }} - CRM SITECONSEIL</title>
|
{% block data_td_pad %}5px{% endblock %}
|
||||||
<style>
|
|
||||||
@page { margin: 0; size: A4; }
|
{% block extra_styles %}
|
||||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #111827; margin: 0; padding: 0; }
|
|
||||||
.banner { background: #fabf04; padding: 16px 32px; border-bottom: 1px solid #111827; }
|
|
||||||
.banner img { height: 36px; }
|
|
||||||
.banner-title { font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #111827; margin-top: 4px; opacity: 0.7; }
|
|
||||||
.container { padding: 24px 32px 16px; }
|
|
||||||
.doc-type { display: inline-block; padding: 4px 12px; background: #4338ca; color: #fff; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px; }
|
|
||||||
h1 { font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: -0.5px; font-style: italic; margin: 0 0 4px 0; line-height: 1.1; }
|
|
||||||
h2 { font-size: 11px; font-weight: 700; text-transform: uppercase; margin: 16px 0 4px; padding: 4px 10px; background: #fabf04; border: 1px solid #ddd; border-radius: 8px; display: inline-block; }
|
|
||||||
.subtitle { font-size: 9px; color: #666; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
|
|
||||||
.info-grid { display: table; width: 100%; margin-bottom: 12px; }
|
|
||||||
.info-row { display: table-row; }
|
|
||||||
.info-grid .info-cell { display: table-cell; padding: 6px 10px; vertical-align: top; }
|
|
||||||
.info-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
|
||||||
.info-value { font-size: 11px; font-weight: 700; color: #111827; }
|
|
||||||
.info-cell { border-left: 3px solid #4338ca; }
|
.info-cell { border-left: 3px solid #4338ca; }
|
||||||
table.data { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 9px; border: 1px solid #ddd; border-radius: 8px; }
|
.doc-type { background: #4338ca; }
|
||||||
table.data th { background: #111827; color: #fff; padding: 4px 8px; text-align: left; text-transform: uppercase; font-size: 8px; font-weight: 700; letter-spacing: 0.5px; }
|
|
||||||
table.data td { padding: 5px 8px; border-bottom: 1px solid #e5e7eb; }
|
|
||||||
table.data tr:nth-child(even) td { background: #f9fafb; }
|
|
||||||
.hmac-box { margin: 16px 0; padding: 12px; border: 2px solid; border-radius: 8px; }
|
.hmac-box { margin: 16px 0; padding: 12px; border: 2px solid; border-radius: 8px; }
|
||||||
.hmac-box.ok { border-color: #16a34a; background: #f0fdf4; }
|
.hmac-box.ok { border-color: #16a34a; background: #f0fdf4; }
|
||||||
.hmac-box.ko { border-color: #dc2626; background: #fef2f2; }
|
.hmac-box.ko { border-color: #dc2626; background: #fef2f2; }
|
||||||
.hmac-ok { color: #16a34a; font-weight: 700; font-size: 12px; }
|
.hmac-ok { color: #16a34a; font-weight: 700; font-size: 12px; }
|
||||||
.hmac-ko { color: #dc2626; font-weight: 700; font-size: 12px; }
|
.hmac-ko { color: #dc2626; font-weight: 700; font-size: 12px; }
|
||||||
.hmac { font-size: 7px; color: #aaa; word-break: break-all; margin: 8px 0; padding: 6px 8px; background: #f9fafb; border: 1px solid #e5e7eb; font-family: monospace; }
|
|
||||||
.contact-box { display: inline-block; padding: 6px 16px; background: #111827; color: #fff; font-weight: 700; text-transform: uppercase; font-size: 9px; letter-spacing: 1px; }
|
|
||||||
.mono { font-family: monospace; font-size: 9px; word-break: break-all; }
|
.mono { font-family: monospace; font-size: 9px; word-break: break-all; }
|
||||||
</style>
|
{% endblock %}
|
||||||
</head>
|
|
||||||
<body>
|
{% block content %}
|
||||||
<div class="banner">
|
|
||||||
{% if logo %}<img src="{{ logo }}" alt="CRM SITECONSEIL">{% endif %}
|
|
||||||
<div class="banner-title">SARL SITECONSEIL</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<span class="doc-type">Log d'activite</span>
|
<span class="doc-type">Log d'activite</span>
|
||||||
<h1>Rapport de log #{{ log.id }}</h1>
|
<h1>Rapport de log #{{ log.id }}</h1>
|
||||||
<div class="subtitle">Trace d'activite — CRM SITECONSEIL</div>
|
<div class="subtitle">Trace d'activite — CRM SITECONSEIL</div>
|
||||||
@@ -90,8 +66,13 @@
|
|||||||
<p style="font-size: 9px; color: #991b1b; margin: 4px 0 0;">Attention : les donnees de ce log ont ete modifiees apres leur enregistrement initial.</p>
|
<p style="font-size: 9px; color: #991b1b; margin: 4px 0 0;">Attention : les donnees de ce log ont ete modifiees apres leur enregistrement initial.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="hmac">HMAC-SHA256 : {{ log.hmac }}</div>
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block hmac_section %}
|
||||||
|
<div class="hmac">HMAC-SHA256 : {{ log.hmac }}</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block verify_box %}
|
||||||
{% if verifyUrl is defined and qrcode is defined %}
|
{% if verifyUrl is defined and qrcode is defined %}
|
||||||
<div style="margin: 16px 0; border: 1px solid #ddd; border-radius: 8px; display: table; width: 100%;">
|
<div style="margin: 16px 0; border: 1px solid #ddd; border-radius: 8px; display: table; width: 100%;">
|
||||||
<div style="display: table-row;">
|
<div style="display: table-row;">
|
||||||
@@ -107,16 +88,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_contact %}
|
||||||
<div style="margin-top: 24px;">
|
<div style="margin-top: 24px;">
|
||||||
<span class="contact-box">contact@siteconseil.fr</span>
|
<span class="contact-box">contact@siteconseil.fr</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<div style="margin-top: 16px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 7px; color: #999; line-height: 1.6;">
|
{% block signature_box %}{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_legal %}
|
||||||
SARL SITECONSEIL — Siret : 418 664 058 — TVA : FR05 418 664 058<br>
|
SARL SITECONSEIL — Siret : 418 664 058 — TVA : FR05 418 664 058<br>
|
||||||
27 rue Le Serurier, 02100 Saint-Quentin, France — contact@siteconseil.fr<br>
|
27 rue Le Serurier, 02100 Saint-Quentin, France — contact@siteconseil.fr<br>
|
||||||
<a href="https://www.siteconseil.fr" style="color: #999;">www.siteconseil.fr</a>
|
<a href="https://www.siteconseil.fr" style="color: #999;">www.siteconseil.fr</a>
|
||||||
</div>
|
{% endblock %}
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
73
templates/pdf/_base.html.twig
Normal file
73
templates/pdf/_base.html.twig
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{% block title %}Document - CRM SITECONSEIL{% endblock %}</title>
|
||||||
|
<style>
|
||||||
|
@page { margin: 0; size: A4; }
|
||||||
|
body { font-family: Arial, Helvetica, sans-serif; font-size: {% block font_size %}10px{% endblock %}; color: #111827; margin: 0; padding: 0; }
|
||||||
|
.banner { background: #fabf04; padding: 16px 32px; border-bottom: 1px solid #111827; }
|
||||||
|
.banner img { height: 36px; }
|
||||||
|
.banner-title { font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #111827; margin-top: 4px; opacity: 0.7; }
|
||||||
|
.container { padding: 24px 32px 16px; }
|
||||||
|
.doc-type { display: inline-block; padding: 4px 12px; color: #fff; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px; }
|
||||||
|
h1 { font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: -0.5px; font-style: italic; margin: 0 0 4px 0; line-height: 1.1; }
|
||||||
|
h2 { font-size: 11px; font-weight: 700; text-transform: uppercase; margin: 16px 0 4px; padding: 4px 10px; background: #fabf04; border: 1px solid #ddd; border-radius: 8px; display: inline-block; }
|
||||||
|
.subtitle { font-size: 9px; color: #666; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
|
||||||
|
.info-grid { display: table; width: 100%; margin-bottom: {% block info_grid_mb %}12px{% endblock %}; }
|
||||||
|
.info-row { display: table-row; }
|
||||||
|
.info-grid .info-cell { display: table-cell; padding: 6px {% block info_cell_px %}10px{% endblock %}; vertical-align: top; }
|
||||||
|
.info-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
||||||
|
.info-value { font-size: 11px; font-weight: 700; color: #111827; }
|
||||||
|
.verify-box { margin: 12px 0; border: 1px solid #ddd; border-radius: 8px; display: table; width: 100%; }
|
||||||
|
.verify-row { display: table-row; }
|
||||||
|
.verify-qr { display: table-cell; text-align: center; width: 100px; padding: 8px; border-right: 2px solid #111827; vertical-align: middle; }
|
||||||
|
.verify-qr img { width: 72px; height: 72px; }
|
||||||
|
.verify-info { display: table-cell; padding: 8px 12px; font-size: 9px; vertical-align: middle; }
|
||||||
|
.verify-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
||||||
|
.verify-url { font-size: 8px; font-family: monospace; color: #4338ca; word-break: break-all; }
|
||||||
|
.hmac { font-size: 7px; color: #aaa; word-break: break-all; margin: 8px 0; padding: 6px 8px; background: #f9fafb; border: 1px solid #e5e7eb; font-family: monospace; }
|
||||||
|
.dpo { font-size: 9px; margin: 8px 0 4px; }
|
||||||
|
.contact-box { display: inline-block; padding: 6px 16px; background: #111827; color: #fff; font-weight: 700; text-transform: uppercase; font-size: 9px; letter-spacing: 1px; }
|
||||||
|
table.data { width: 100%; border-collapse: collapse; margin-top: {% block data_table_mt %}4px{% endblock %}; font-size: 9px; border: 1px solid #ddd; border-radius: 8px; }
|
||||||
|
table.data th { background: #111827; color: #fff; padding: 4px 8px; text-align: left; text-transform: uppercase; font-size: 8px; font-weight: 700; letter-spacing: 0.5px; }
|
||||||
|
table.data td { padding: {% block data_td_pad %}3px{% endblock %} 8px; border-bottom: 1px solid #e5e7eb; }
|
||||||
|
table.data tr:nth-child(even) td { background: #f9fafb; }
|
||||||
|
{% block extra_styles %}{% endblock %}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="banner">
|
||||||
|
{% if logo %}<img src="{{ logo }}" alt="CRM SITECONSEIL">{% endif %}
|
||||||
|
<div class="banner-title">SARL SITECONSEIL</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
{% block verify_box %}{% endblock %}
|
||||||
|
|
||||||
|
{% block hmac_section %}{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_contact %}
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<p class="dpo"><strong>DPO</strong></p>
|
||||||
|
<span class="contact-box">contact@siteconseil.fr</span>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block signature_box %}
|
||||||
|
<div style="margin-top: 12px; display: inline-block; border: 1px solid #ddd; border-radius: 8px; padding: 8px 12px; width: 180px; height: 80px;">
|
||||||
|
<div style="font-size: 1px; color: #fff;">{% verbatim %}{{Sign;type=signature;width=150;height=50}}{% endverbatim %}</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
<div style="margin-top: 16px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 7px; color: #999; line-height: 1.6;">
|
||||||
|
{% block footer_legal %}
|
||||||
|
SARL SITECONSEIL — RNA W022006988 — SIREN 943121517<br>
|
||||||
|
27 rue Le Sérurier, 02100 Saint-Quentin, France — contact@siteconseil.fr — 06 79 34 88 02<br>
|
||||||
|
<a href="https://www.siteconseil.fr" style="color: #999;">www.siteconseil.fr</a>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,50 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
{% extends 'pdf/_base.html.twig' %}
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
{% block title %}Attestation RGPD - Acces aux donnees{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Attestation RGPD - Acces aux donnees</title>
|
{% block extra_styles %}
|
||||||
<style>
|
|
||||||
@page { margin: 0; size: A4; }
|
|
||||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 10px; color: #111827; margin: 0; padding: 0; }
|
|
||||||
.banner { background: #fabf04; padding: 16px 32px; border-bottom: 1px solid #111827; }
|
|
||||||
.banner img { height: 36px; }
|
|
||||||
.banner-title { font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #111827; margin-top: 4px; opacity: 0.7; }
|
|
||||||
.container { padding: 24px 32px 16px; }
|
|
||||||
.doc-type { display: inline-block; padding: 4px 12px; background: #4338ca; color: #fff; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px; }
|
|
||||||
h1 { font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: -0.5px; font-style: italic; margin: 0 0 4px 0; line-height: 1.1; }
|
|
||||||
h2 { font-size: 11px; font-weight: 700; text-transform: uppercase; margin: 16px 0 4px; padding: 4px 10px; background: #fabf04; border: 1px solid #ddd; border-radius: 8px; display: inline-block; }
|
|
||||||
.subtitle { font-size: 9px; color: #666; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
|
|
||||||
.info-grid { display: table; width: 100%; margin-bottom: 12px; }
|
|
||||||
.info-row { display: table-row; }
|
|
||||||
.info-grid .info-cell { display: table-cell; padding: 6px 10px; vertical-align: top; }
|
|
||||||
.info-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
|
||||||
.info-value { font-size: 11px; font-weight: 700; color: #111827; }
|
|
||||||
.info-cell { border-left: 3px solid #4338ca; }
|
.info-cell { border-left: 3px solid #4338ca; }
|
||||||
|
.doc-type { background: #4338ca; }
|
||||||
.session-meta { font-size: 9px; color: #666; margin: 2px 0 6px; }
|
.session-meta { font-size: 9px; color: #666; margin: 2px 0 6px; }
|
||||||
.session-meta strong { color: #111827; }
|
.session-meta strong { color: #111827; }
|
||||||
table.data { width: 100%; border-collapse: collapse; margin-top: 4px; font-size: 9px; border: 1px solid #ddd; border-radius: 8px; }
|
|
||||||
table.data th { background: #111827; color: #fff; padding: 4px 8px; text-align: left; text-transform: uppercase; font-size: 8px; font-weight: 700; letter-spacing: 0.5px; }
|
|
||||||
table.data td { padding: 3px 8px; border-bottom: 1px solid #e5e7eb; }
|
|
||||||
table.data tr:nth-child(even) td { background: #f9fafb; }
|
|
||||||
.no-data { padding: 8px; background: #f9fafb; border: 2px dashed #d1d5db; font-style: italic; color: #999; text-align: center; font-size: 9px; }
|
.no-data { padding: 8px; background: #f9fafb; border: 2px dashed #d1d5db; font-style: italic; color: #999; text-align: center; font-size: 9px; }
|
||||||
.verify-box { margin: 12px 0; border: 1px solid #ddd; border-radius: 8px; display: table; width: 100%; }
|
{% endblock %}
|
||||||
.verify-row { display: table-row; }
|
|
||||||
.verify-qr { display: table-cell; text-align: center; width: 100px; padding: 8px; border-right: 2px solid #111827; vertical-align: middle; }
|
{% block content %}
|
||||||
.verify-qr img { width: 72px; height: 72px; }
|
|
||||||
.verify-info { display: table-cell; padding: 8px 12px; font-size: 9px; vertical-align: middle; }
|
|
||||||
.verify-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
|
||||||
.verify-url { font-size: 8px; font-family: monospace; color: #4338ca; word-break: break-all; }
|
|
||||||
.hmac { font-size: 7px; color: #aaa; word-break: break-all; margin: 8px 0; padding: 6px 8px; background: #f9fafb; border: 1px solid #e5e7eb; font-family: monospace; }
|
|
||||||
.dpo { font-size: 9px; margin: 8px 0 4px; }
|
|
||||||
.contact-box { display: inline-block; padding: 6px 16px; background: #111827; color: #fff; font-weight: 700; text-transform: uppercase; font-size: 9px; letter-spacing: 1px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="banner">
|
|
||||||
{% if logo %}<img src="{{ logo }}" alt="CRM SITECONSEIL">{% endif %}
|
|
||||||
<div class="banner-title">SARL SITECONSEIL</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<span class="doc-type">Droit d'acces</span>
|
<span class="doc-type">Droit d'acces</span>
|
||||||
<h1>Donnees personnelles</h1>
|
<h1>Donnees personnelles</h1>
|
||||||
<div class="subtitle">RGPD — Article 15</div>
|
<div class="subtitle">RGPD — Article 15</div>
|
||||||
@@ -73,7 +39,9 @@
|
|||||||
<div class="no-data">Aucun evenement enregistre.</div>
|
<div class="no-data">Aucun evenement enregistre.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block verify_box %}
|
||||||
<div class="verify-box">
|
<div class="verify-box">
|
||||||
<div class="verify-row">
|
<div class="verify-row">
|
||||||
<div class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></div>
|
<div class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></div>
|
||||||
@@ -85,19 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block hmac_section %}
|
||||||
<div class="hmac">HMAC-SHA256 : {{ attestation.hmac }}</div>
|
<div class="hmac">HMAC-SHA256 : {{ attestation.hmac }}</div>
|
||||||
<div style="margin-top: 16px;">
|
{% endblock %}
|
||||||
<p class="dpo"><strong>DPO</strong></p>
|
|
||||||
<span class="contact-box">contact@siteconseil.fr</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 12px; display: inline-block; border: 1px solid #ddd; border-radius: 8px; padding: 8px 12px; width: 180px; height: 80px;">
|
|
||||||
<div style="font-size: 1px; color: #fff;">{% verbatim %}{{Sign;type=signature;width=150;height=50}}{% endverbatim %}</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 16px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 7px; color: #999; line-height: 1.6;">
|
|
||||||
SARL SITECONSEIL — RNA W022006988 — SIREN 943121517<br>
|
|
||||||
27 rue Le Sérurier, 02100 Saint-Quentin, France — contact@siteconseil.fr — 06 79 34 88 02<br>
|
|
||||||
<a href="https://www.siteconseil.fr" style="color: #999;">www.siteconseil.fr</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -1,48 +1,22 @@
|
|||||||
<!DOCTYPE html>
|
{% extends 'pdf/_base.html.twig' %}
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
{% block title %}Attestation RGPD - Suppression des donnees{% endblock %}
|
||||||
<meta charset="UTF-8">
|
{% block font_size %}11px{% endblock %}
|
||||||
<title>Attestation RGPD - Suppression des donnees</title>
|
{% block info_grid_mb %}16px{% endblock %}
|
||||||
<style>
|
{% block info_cell_px %}12px{% endblock %}
|
||||||
@page { margin: 0; size: A4; }
|
|
||||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #111827; margin: 0; padding: 0; }
|
{% block extra_styles %}
|
||||||
.banner { background: #fabf04; padding: 16px 32px; border-bottom: 1px solid #111827; }
|
|
||||||
.banner img { height: 36px; }
|
|
||||||
.banner-title { font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #111827; margin-top: 4px; opacity: 0.7; }
|
|
||||||
.container { padding: 24px 32px 16px; }
|
|
||||||
.doc-type { display: inline-block; padding: 4px 12px; background: #dc2626; color: #fff; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px; }
|
|
||||||
h1 { font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: -0.5px; font-style: italic; margin: 0 0 4px 0; line-height: 1.1; }
|
|
||||||
.subtitle { font-size: 9px; color: #666; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
|
|
||||||
.info-grid { display: table; width: 100%; margin-bottom: 16px; }
|
|
||||||
.info-row { display: table-row; }
|
|
||||||
.info-grid .info-cell { display: table-cell; padding: 6px 12px; vertical-align: top; }
|
|
||||||
.info-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
|
||||||
.info-value { font-size: 11px; font-weight: 700; color: #111827; }
|
|
||||||
.info-cell { border-left: 3px solid #dc2626; }
|
.info-cell { border-left: 3px solid #dc2626; }
|
||||||
|
.doc-type { background: #dc2626; }
|
||||||
.attestation-box { border: 1px solid #ddd; border-radius: 8px; margin: 12px 0; }
|
.attestation-box { border: 1px solid #ddd; border-radius: 8px; margin: 12px 0; }
|
||||||
.attestation-header { background: #dc2626; color: #fff; padding: 6px 16px; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; }
|
.attestation-header { background: #dc2626; color: #fff; padding: 6px 16px; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; }
|
||||||
.attestation-body { padding: 14px 16px; background: #fef2f2; }
|
.attestation-body { padding: 14px 16px; background: #fef2f2; }
|
||||||
.attestation-body p { line-height: 1.6; margin: 4px 0; font-size: 11px; }
|
.attestation-body p { line-height: 1.6; margin: 4px 0; font-size: 11px; }
|
||||||
.badge { display: inline-block; padding: 2px 8px; background: #111827; color: #fabf04; font-weight: 700; text-transform: uppercase; font-size: 7px; letter-spacing: 1px; margin-bottom: 6px; }
|
.badge { display: inline-block; padding: 2px 8px; background: #111827; color: #fabf04; font-weight: 700; text-transform: uppercase; font-size: 7px; letter-spacing: 1px; margin-bottom: 6px; }
|
||||||
.warning { padding: 8px 12px; background: #fef2f2; border-left: 3px solid #dc2626; margin: 8px 0; font-size: 10px; font-weight: 700; }
|
.warning { padding: 8px 12px; background: #fef2f2; border-left: 3px solid #dc2626; margin: 8px 0; font-size: 10px; font-weight: 700; }
|
||||||
.verify-box { margin: 12px 0; border: 1px solid #ddd; border-radius: 8px; display: table; width: 100%; }
|
{% endblock %}
|
||||||
.verify-row { display: table-row; }
|
|
||||||
.verify-qr { display: table-cell; text-align: center; width: 100px; padding: 8px; border-right: 2px solid #111827; vertical-align: middle; }
|
{% block content %}
|
||||||
.verify-qr img { width: 72px; height: 72px; }
|
|
||||||
.verify-info { display: table-cell; padding: 8px 12px; font-size: 9px; vertical-align: middle; }
|
|
||||||
.verify-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
|
||||||
.verify-url { font-size: 8px; font-family: monospace; color: #4338ca; word-break: break-all; }
|
|
||||||
.hmac { font-size: 7px; color: #aaa; word-break: break-all; margin: 8px 0; padding: 6px 8px; background: #f9fafb; border: 1px solid #e5e7eb; font-family: monospace; }
|
|
||||||
.dpo { font-size: 9px; margin: 8px 0 4px; }
|
|
||||||
.contact-box { display: inline-block; padding: 6px 16px; background: #111827; color: #fff; font-weight: 700; text-transform: uppercase; font-size: 9px; letter-spacing: 1px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="banner">
|
|
||||||
{% if logo %}<img src="{{ logo }}" alt="CRM SITECONSEIL">{% endif %}
|
|
||||||
<div class="banner-title">SARL SITECONSEIL</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<span class="doc-type">Suppression definitive</span>
|
<span class="doc-type">Suppression definitive</span>
|
||||||
<h1>Attestation de suppression des donnees</h1>
|
<h1>Attestation de suppression des donnees</h1>
|
||||||
<div class="subtitle">RGPD — Article 17</div>
|
<div class="subtitle">RGPD — Article 17</div>
|
||||||
@@ -63,6 +37,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="warning">Cette suppression est irreversible. Aucune donnee relative a cette adresse IP ne subsiste dans nos bases de donnees.</div>
|
<div class="warning">Cette suppression est irreversible. Aucune donnee relative a cette adresse IP ne subsiste dans nos bases de donnees.</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block verify_box %}
|
||||||
<div class="verify-box">
|
<div class="verify-box">
|
||||||
<div class="verify-row">
|
<div class="verify-row">
|
||||||
<div class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></div>
|
<div class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></div>
|
||||||
@@ -74,19 +51,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block hmac_section %}
|
||||||
<div class="hmac">HMAC-SHA256 : {{ attestation.hmac }}</div>
|
<div class="hmac">HMAC-SHA256 : {{ attestation.hmac }}</div>
|
||||||
<div style="margin-top: 16px;">
|
{% endblock %}
|
||||||
<p class="dpo"><strong>DPO</strong></p>
|
|
||||||
<span class="contact-box">contact@siteconseil.fr</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 12px; display: inline-block; border: 1px solid #ddd; border-radius: 8px; padding: 8px 12px; width: 180px; height: 80px;">
|
|
||||||
<div style="font-size: 1px; color: #fff;">{% verbatim %}{{Sign;type=signature;width=150;height=50}}{% endverbatim %}</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 16px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 7px; color: #999; line-height: 1.6;">
|
|
||||||
SARL SITECONSEIL — RNA W022006988 — SIREN 943121517<br>
|
|
||||||
27 rue Le Sérurier, 02100 Saint-Quentin, France — contact@siteconseil.fr — 06 79 34 88 02<br>
|
|
||||||
<a href="https://www.siteconseil.fr" style="color: #999;">www.siteconseil.fr</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
@@ -1,47 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
{% extends 'pdf/_base.html.twig' %}
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
{% block title %}Attestation RGPD - Absence de donnees{% endblock %}
|
||||||
<meta charset="UTF-8">
|
{% block font_size %}11px{% endblock %}
|
||||||
<title>Attestation RGPD - Absence de donnees</title>
|
{% block info_grid_mb %}16px{% endblock %}
|
||||||
<style>
|
{% block info_cell_px %}12px{% endblock %}
|
||||||
@page { margin: 0; size: A4; }
|
|
||||||
body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #111827; margin: 0; padding: 0; }
|
{% block extra_styles %}
|
||||||
.banner { background: #fabf04; padding: 16px 32px; border-bottom: 1px solid #111827; }
|
|
||||||
.banner img { height: 36px; }
|
|
||||||
.banner-title { font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #111827; margin-top: 4px; opacity: 0.7; }
|
|
||||||
.container { padding: 24px 32px 16px; }
|
|
||||||
.doc-type { display: inline-block; padding: 4px 12px; background: #111827; color: #fabf04; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 8px; }
|
|
||||||
h1 { font-size: 18px; font-weight: 700; text-transform: uppercase; letter-spacing: -0.5px; font-style: italic; margin: 0 0 4px 0; line-height: 1.1; }
|
|
||||||
.subtitle { font-size: 9px; color: #666; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 16px; }
|
|
||||||
.info-grid { display: table; width: 100%; margin-bottom: 16px; }
|
|
||||||
.info-row { display: table-row; }
|
|
||||||
.info-grid .info-cell { display: table-cell; padding: 6px 12px; vertical-align: top; }
|
|
||||||
.info-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
|
||||||
.info-value { font-size: 11px; font-weight: 700; color: #111827; }
|
|
||||||
.info-cell { border-left: 3px solid #fabf04; }
|
.info-cell { border-left: 3px solid #fabf04; }
|
||||||
|
.doc-type { background: #111827; color: #fabf04; }
|
||||||
.attestation-box { border: 1px solid #ddd; border-radius: 8px; margin: 12px 0; }
|
.attestation-box { border: 1px solid #ddd; border-radius: 8px; margin: 12px 0; }
|
||||||
.attestation-header { background: #111827; color: #fff; padding: 6px 16px; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; }
|
.attestation-header { background: #111827; color: #fff; padding: 6px 16px; font-size: 8px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; }
|
||||||
.attestation-body { padding: 14px 16px; background: #fffbeb; }
|
.attestation-body { padding: 14px 16px; background: #fffbeb; }
|
||||||
.attestation-body p { line-height: 1.6; margin: 4px 0; font-size: 11px; }
|
.attestation-body p { line-height: 1.6; margin: 4px 0; font-size: 11px; }
|
||||||
.verify-box { margin: 12px 0; border: 1px solid #ddd; border-radius: 8px; display: table; width: 100%; }
|
|
||||||
.verify-row { display: table-row; }
|
|
||||||
.verify-qr { display: table-cell; text-align: center; width: 100px; padding: 8px; border-right: 2px solid #111827; vertical-align: middle; }
|
|
||||||
.verify-qr img { width: 72px; height: 72px; }
|
|
||||||
.verify-info { display: table-cell; padding: 8px 12px; font-size: 9px; vertical-align: middle; }
|
|
||||||
.verify-label { font-size: 7px; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px; color: #999; display: block; margin-bottom: 1px; }
|
|
||||||
.verify-url { font-size: 8px; font-family: monospace; color: #4338ca; word-break: break-all; }
|
|
||||||
.content p { line-height: 1.6; margin: 4px 0; font-size: 10px; }
|
.content p { line-height: 1.6; margin: 4px 0; font-size: 10px; }
|
||||||
.hmac { font-size: 7px; color: #aaa; word-break: break-all; margin: 8px 0; padding: 6px 8px; background: #f9fafb; border: 1px solid #e5e7eb; font-family: monospace; }
|
{% endblock %}
|
||||||
.dpo { font-size: 9px; margin: 8px 0 4px; }
|
|
||||||
.contact-box { display: inline-block; padding: 6px 16px; background: #111827; color: #fff; font-weight: 700; text-transform: uppercase; font-size: 9px; letter-spacing: 1px; }
|
{% block content %}
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="banner">
|
|
||||||
{% if logo %}<img src="{{ logo }}" alt="CRM SITECONSEIL">{% endif %}
|
|
||||||
<div class="banner-title">SARL SITECONSEIL</div>
|
|
||||||
</div>
|
|
||||||
<div class="container">
|
|
||||||
<span class="doc-type">Document officiel</span>
|
<span class="doc-type">Document officiel</span>
|
||||||
<h1>Attestation d'absence de donnees</h1>
|
<h1>Attestation d'absence de donnees</h1>
|
||||||
<div class="subtitle">RGPD — Article 15</div>
|
<div class="subtitle">RGPD — Article 15</div>
|
||||||
@@ -62,6 +36,9 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<p>Aucune donnee personnelle (identifiants de session, evenements de navigation, informations techniques) n'est stockee dans nos bases de donnees pour cette adresse IP.</p>
|
<p>Aucune donnee personnelle (identifiants de session, evenements de navigation, informations techniques) n'est stockee dans nos bases de donnees pour cette adresse IP.</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block verify_box %}
|
||||||
<div class="verify-box">
|
<div class="verify-box">
|
||||||
<div class="verify-row">
|
<div class="verify-row">
|
||||||
<div class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></div>
|
<div class="verify-qr"><img src="{{ qrcode }}" alt="QR Code"></div>
|
||||||
@@ -73,19 +50,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block hmac_section %}
|
||||||
<div class="hmac">HMAC-SHA256 : {{ attestation.hmac }}</div>
|
<div class="hmac">HMAC-SHA256 : {{ attestation.hmac }}</div>
|
||||||
<div style="margin-top: 16px;">
|
{% endblock %}
|
||||||
<p class="dpo"><strong>DPO</strong></p>
|
|
||||||
<span class="contact-box">contact@siteconseil.fr</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 12px; display: inline-block; border: 1px solid #ddd; border-radius: 8px; padding: 8px 12px; width: 180px; height: 80px;">
|
|
||||||
<div style="font-size: 1px; color: #fff;">{% verbatim %}{{Sign;type=signature;width=150;height=50}}{% endverbatim %}</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 16px; padding-top: 8px; border-top: 1px solid #ddd; font-size: 7px; color: #999; line-height: 1.6;">
|
|
||||||
SARL SITECONSEIL — RNA W022006988 — SIREN 943121517<br>
|
|
||||||
27 rue Le Sérurier, 02100 Saint-Quentin, France — contact@siteconseil.fr — 06 79 34 88 02<br>
|
|
||||||
<a href="https://www.siteconseil.fr" style="color: #999;">www.siteconseil.fr</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
80
tests/Controller/Admin/StatsControllerTest.php
Normal file
80
tests/Controller/Admin/StatsControllerTest.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Controller\Admin\StatsController;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||||
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class StatsControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private function setupController(StatsController $controller): void
|
||||||
|
{
|
||||||
|
$session = new Session(new MockArraySessionStorage());
|
||||||
|
$stack = $this->createStub(RequestStack::class);
|
||||||
|
$stack->method('getSession')->willReturn($session);
|
||||||
|
|
||||||
|
$twig = $this->createStub(Environment::class);
|
||||||
|
$twig->method('render')->willReturn('<html></html>');
|
||||||
|
|
||||||
|
$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());
|
||||||
|
}
|
||||||
|
}
|
||||||
419
tests/Controller/Admin/StatusControllerTest.php
Normal file
419
tests/Controller/Admin/StatusControllerTest.php
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Controller\Admin\StatusController;
|
||||||
|
use App\Entity\Service;
|
||||||
|
use App\Entity\ServiceCategory;
|
||||||
|
use App\Entity\ServiceMessage;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\ServiceCategoryRepository;
|
||||||
|
use App\Repository\ServiceRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||||
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||||
|
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class StatusControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private function createContainer(array $overrides = []): ContainerInterface
|
||||||
|
{
|
||||||
|
$session = new Session(new MockArraySessionStorage());
|
||||||
|
$stack = $this->createStub(RequestStack::class);
|
||||||
|
$stack->method('getSession')->willReturn($session);
|
||||||
|
|
||||||
|
$twig = $this->createStub(Environment::class);
|
||||||
|
$twig->method('render')->willReturn('<html></html>');
|
||||||
|
|
||||||
|
$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());
|
||||||
|
}
|
||||||
|
}
|
||||||
317
tests/Controller/Admin/SyncControllerTest.php
Normal file
317
tests/Controller/Admin/SyncControllerTest.php
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller\Admin;
|
||||||
|
|
||||||
|
use App\Controller\Admin\SyncController;
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\PriceAutomatic;
|
||||||
|
use App\Entity\Revendeur;
|
||||||
|
use App\Entity\StripeWebhookSecret;
|
||||||
|
use App\Repository\CustomerRepository;
|
||||||
|
use App\Repository\PriceAutomaticRepository;
|
||||||
|
use App\Repository\RevendeurRepository;
|
||||||
|
use App\Repository\StripeWebhookSecretRepository;
|
||||||
|
use App\Service\MeilisearchService;
|
||||||
|
use App\Service\StripePriceService;
|
||||||
|
use App\Service\StripeWebhookService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||||
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class SyncControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private function createContainer(): ContainerInterface
|
||||||
|
{
|
||||||
|
$session = new Session(new MockArraySessionStorage());
|
||||||
|
$stack = $this->createStub(RequestStack::class);
|
||||||
|
$stack->method('getSession')->willReturn($session);
|
||||||
|
|
||||||
|
$twig = $this->createStub(Environment::class);
|
||||||
|
$twig->method('render')->willReturn('<html></html>');
|
||||||
|
|
||||||
|
$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());
|
||||||
|
}
|
||||||
|
}
|
||||||
205
tests/Controller/AnalyticsControllerTest.php
Normal file
205
tests/Controller/AnalyticsControllerTest.php
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Controller\AnalyticsController;
|
||||||
|
use App\Service\AnalyticsCryptoService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
|
||||||
|
class AnalyticsControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private AnalyticsCryptoService $crypto;
|
||||||
|
private string $analyticsSecret = 'test-secret-analytics';
|
||||||
|
private string $validToken;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
163
tests/Controller/AttestationControllerTest.php
Normal file
163
tests/Controller/AttestationControllerTest.php
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Controller\AttestationController;
|
||||||
|
use App\Entity\Attestation;
|
||||||
|
use App\Repository\AttestationRepository;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
class AttestationControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private const HMAC_SECRET = 'test-hmac-secret';
|
||||||
|
|
||||||
|
private function setupController(AttestationController $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('/redirect-url');
|
||||||
|
|
||||||
|
$twig = $this->createStub(Environment::class);
|
||||||
|
$twig->method('render')->willReturn('<html></html>');
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
252
tests/Controller/CspReportControllerTest.php
Normal file
252
tests/Controller/CspReportControllerTest.php
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Controller\CspReportController;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
|
||||||
|
class CspReportControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private function setupController(CspReportController $controller): void
|
||||||
|
{
|
||||||
|
$session = new Session(new MockArraySessionStorage());
|
||||||
|
$stack = $this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
225
tests/Controller/EmailTrackingControllerTest.php
Normal file
225
tests/Controller/EmailTrackingControllerTest.php
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Controller;
|
||||||
|
|
||||||
|
use App\Controller\EmailTrackingController;
|
||||||
|
use App\Entity\EmailTracking;
|
||||||
|
use App\Repository\EmailTrackingRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Session;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
use Symfony\Component\Routing\RouterInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
|
|
||||||
|
class EmailTrackingControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
private string $projectDir;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->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', '<html><body>Hello</body></html>');
|
||||||
|
|
||||||
|
$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',
|
||||||
|
'<html><body>With attachment</body></html>',
|
||||||
|
[['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', '<html></html>');
|
||||||
|
|
||||||
|
$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',
|
||||||
|
'<html></html>',
|
||||||
|
[['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',
|
||||||
|
'<html></html>',
|
||||||
|
[['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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,13 +81,96 @@ class MainControllersTest extends TestCase
|
|||||||
$this->assertSame(200, $controller->check()->getStatusCode());
|
$this->assertSame(200, $controller->check()->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testHomeIndex(): void
|
public function testHomeIndexNoUser(): void
|
||||||
{
|
{
|
||||||
$controller = new HomeController();
|
$controller = new HomeController();
|
||||||
$this->setupController($controller);
|
$this->setupController($controller);
|
||||||
$this->assertSame(200, $controller->index()->getStatusCode());
|
$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
|
public function testSetPasswordSuccess(): void
|
||||||
{
|
{
|
||||||
$user = new User();
|
$user = new User();
|
||||||
@@ -146,4 +229,76 @@ class MainControllersTest extends TestCase
|
|||||||
$response = $controller->index($request, $repo, $mailer, $em, $hasher, $twig);
|
$response = $controller->index($request, $repo, $mailer, $em, $hasher, $twig);
|
||||||
$this->assertSame(302, $response->getStatusCode());
|
$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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
tests/Entity/ServiceMessageTest.php
Normal file
65
tests/Entity/ServiceMessageTest.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Service;
|
||||||
|
use App\Entity\ServiceCategory;
|
||||||
|
use App\Entity\ServiceMessage;
|
||||||
|
use App\Entity\User;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ServiceMessageTest extends TestCase
|
||||||
|
{
|
||||||
|
private function createService(): Service
|
||||||
|
{
|
||||||
|
$category = new ServiceCategory('Web', 'web');
|
||||||
|
|
||||||
|
return new Service('Test Service', 'test-service', $category);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstructorDefaults(): void
|
||||||
|
{
|
||||||
|
$service = $this->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());
|
||||||
|
}
|
||||||
|
}
|
||||||
57
tests/Entity/StripeWebhookSecretTest.php
Normal file
57
tests/Entity/StripeWebhookSecretTest.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
|
use App\Entity\StripeWebhookSecret;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class StripeWebhookSecretTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testConstructorDefaults(): void
|
||||||
|
{
|
||||||
|
$secret = new StripeWebhookSecret('main_light', 'whsec_abc123');
|
||||||
|
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user