Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/icons/heroicons/sparkles.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions default_translations/de/messages.de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,10 @@ admin:
title: Inaktive Nutzer:innen entfernen
description: Löscht Nutzer:innen, die seit mehr als 2 Jahren inaktiv sind. Admin- und permanente Konten werden ausgenommen.
button: Inaktive entfernen
removeUnredeemedVouchers:
title: Nicht eingelöste Einladungscodes entfernen
description: Entfernt nicht eingelöste Einladungscodes von gelöschten Nutzer:innen.
button: Einladungscodes entfernen
unknown_task: Unbekannte Wartungsaufgabe.
dispatched: Wartungsaufgabe erfolgreich gestartet.
table:
Expand Down
4 changes: 4 additions & 0 deletions default_translations/en/messages.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,10 @@ admin:
title: Remove Inactive Users
description: Delete users who have been inactive for more than 2 years. Admin and permanent users are excluded.
button: Remove inactive users
removeUnredeemedVouchers:
title: Remove Unredeemed Invite Codes
description: Remove unredeemed invite codes from deleted users.
button: Remove unredeemed invite codes
unknown_task: Unknown maintenance task.
dispatched: Maintenance task dispatched successfully.
title: Settings
Expand Down
8 changes: 8 additions & 0 deletions features/admin_maintenance.feature
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Feature: Admin (Maintenance)
And I should see "Prune Webhook Deliveries"
And I should see "Remove Inactive Users"
And I should see "Unlink Redeemed Invite Codes"
And I should see "Remove Unredeemed Invite Codes"

@maintenance
Scenario: Admin can dispatch prune user notifications task
Expand Down Expand Up @@ -54,3 +55,10 @@ Feature: Admin (Maintenance)
When I am on "/admin/maintenance"
And I press "Unlink invite codes"
Then I should see "Maintenance task dispatched successfully"

@maintenance
Scenario: Admin can dispatch remove unredeemed vouchers task
Given I am authenticated as "louis@example.org"
When I am on "/admin/maintenance"
And I press "Remove unredeemed invite codes"
Then I should see "Maintenance task dispatched successfully"
2 changes: 2 additions & 0 deletions src/Controller/Admin/MaintenanceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Message\PruneUserNotifications;
use App\Message\PruneWebhookDeliveries;
use App\Message\RemoveInactiveUsers;
use App\Message\RemoveUnredeemedVouchers;
use App\Message\UnlinkRedeemedVouchers;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
Expand All @@ -24,6 +25,7 @@ final class MaintenanceController extends AbstractController
'prune_user_notifications' => PruneUserNotifications::class,
'prune_webhook_deliveries' => PruneWebhookDeliveries::class,
'remove_inactive_users' => RemoveInactiveUsers::class,
'remove_unredeemed_vouchers' => RemoveUnredeemedVouchers::class,
'unlink_redeemed_vouchers' => UnlinkRedeemedVouchers::class,
];

Expand Down
12 changes: 12 additions & 0 deletions src/Message/RemoveUnredeemedVouchers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace App\Message;

use Symfony\Component\Messenger\Attribute\AsMessage;

#[AsMessage('async')]
final class RemoveUnredeemedVouchers
{
}
41 changes: 41 additions & 0 deletions src/MessageHandler/RemoveUnredeemedVouchersHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace App\MessageHandler;

use App\Entity\Voucher;
use App\Message\RemoveUnredeemedVouchers;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler(sign: true)]
final readonly class RemoveUnredeemedVouchersHandler
{
public function __construct(
private EntityManagerInterface $entityManager,
private LoggerInterface $logger,
) {
}

public function __invoke(RemoveUnredeemedVouchers $message): void
{
/** @var Voucher[] $vouchers */
$vouchers = $this->entityManager->getRepository(Voucher::class)
->createQueryBuilder('v')
->join('v.user', 'u')
->where('v.redeemedTime IS NULL')
->andWhere('u.deleted = true')
->getQuery()
->getResult();

foreach ($vouchers as $voucher) {
$this->entityManager->remove($voucher);
}

$this->entityManager->flush();

$this->logger->info('Removed unredeemed vouchers of deleted users', ['count' => count($vouchers)]);
}
}
2 changes: 2 additions & 0 deletions src/Schedule/MaintenanceSchedule.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Message\PruneUserNotifications;
use App\Message\PruneWebhookDeliveries;
use App\Message\RemoveInactiveUsers;
use App\Message\RemoveUnredeemedVouchers;
use App\Message\SendWeeklyReport;
use App\Message\UnlinkRedeemedVouchers;
use DateTimeImmutable;
Expand All @@ -33,6 +34,7 @@ public function getSchedule(): Schedule
->add(RecurringMessage::every('1 day', new PruneWebhookDeliveries(), new DateTimeImmutable('03:00')))
->add(RecurringMessage::every('1 day', new PruneUserNotifications(), new DateTimeImmutable('03:30')))
->add(RecurringMessage::every('1 day', new UnlinkRedeemedVouchers(), new DateTimeImmutable('04:00')))
->add(RecurringMessage::every('1 week', new RemoveUnredeemedVouchers(), new DateTimeImmutable('05:00')))
->add(RecurringMessage::every('1 week', new RemoveInactiveUsers(), new DateTimeImmutable('06:00')))
->add(RecurringMessage::every('1 week', new SendWeeklyReport(), new DateTimeImmutable('07:00')));
}
Expand Down
9 changes: 9 additions & 0 deletions templates/Admin/Maintenance/show.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@
task: 'unlink_redeemed_vouchers',
csrf_token_id: 'maintenance_unlink_redeemed_vouchers',
} %}

{% include 'Admin/_maintenance_card.html.twig' with {
icon: 'heroicons:trash',
title_key: 'admin.maintenance.removeUnredeemedVouchers.title',
description_key: 'admin.maintenance.removeUnredeemedVouchers.description',
button_key: 'admin.maintenance.removeUnredeemedVouchers.button',
task: 'remove_unredeemed_vouchers',
csrf_token_id: 'maintenance_remove_unredeemed_vouchers',
} %}
</div>
</div>
</div>
Expand Down
102 changes: 102 additions & 0 deletions tests/MessageHandler/RemoveUnredeemedVouchersHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace App\Tests\MessageHandler;

use App\Entity\User;
use App\Entity\Voucher;
use App\Message\RemoveUnredeemedVouchers;
use App\MessageHandler\RemoveUnredeemedVouchersHandler;
use App\Repository\VoucherRepository;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;

class RemoveUnredeemedVouchersHandlerTest extends TestCase
{
public function testRemovesUnredeemedVouchersOfDeletedUsers(): void
{
$deletedUser = new User('deleted@example.org');
$deletedUser->setDeleted(true);

$voucher1 = new Voucher('A');
$voucher1->setUser($deletedUser);

$voucher2 = new Voucher('B');
$voucher2->setUser($deletedUser);

$expectedResultSet = [$voucher1, $voucher2];

$qb = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class)
->disableOriginalConstructor()
->onlyMethods(['join', 'where', 'andWhere', 'getQuery'])
->getMock();

$qb->expects($this->any())->method('join')->willReturnSelf();
$qb->expects($this->any())->method('where')->willReturnSelf();
$qb->expects($this->any())->method('andWhere')->willReturnSelf();

$query = $this->getMockBuilder(\Doctrine\ORM\Query::class)
->disableOriginalConstructor()
->onlyMethods(['getResult'])
->getMock();
$query->expects($this->once())->method('getResult')->willReturn($expectedResultSet);

$qb->expects($this->once())->method('getQuery')->willReturn($query);

$repo = $this->getMockBuilder(VoucherRepository::class)
->disableOriginalConstructor()
->onlyMethods(['createQueryBuilder'])
->getMock();
$repo->expects($this->once())->method('createQueryBuilder')->willReturn($qb);

$em = $this->createMock(EntityManagerInterface::class);
$em->expects($this->any())->method('getRepository')->willReturn($repo);
$em->expects($this->exactly(2))->method('remove')->with($this->isInstanceOf(Voucher::class));
$em->expects($this->once())->method('flush');

$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())->method('info')->with('Removed unredeemed vouchers of deleted users', ['count' => 2]);

$handler = new RemoveUnredeemedVouchersHandler($em, $logger);
$handler(new RemoveUnredeemedVouchers());
}

public function testHandlesEmptyResultGracefully(): void
{
$qb = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class)
->disableOriginalConstructor()
->onlyMethods(['join', 'where', 'andWhere', 'getQuery'])
->getMock();

$qb->expects($this->any())->method('join')->willReturnSelf();
$qb->expects($this->any())->method('where')->willReturnSelf();
$qb->expects($this->any())->method('andWhere')->willReturnSelf();

$query = $this->getMockBuilder(\Doctrine\ORM\Query::class)
->disableOriginalConstructor()
->onlyMethods(['getResult'])
->getMock();
$query->expects($this->once())->method('getResult')->willReturn([]);

$qb->expects($this->once())->method('getQuery')->willReturn($query);

$repo = $this->getMockBuilder(VoucherRepository::class)
->disableOriginalConstructor()
->onlyMethods(['createQueryBuilder'])
->getMock();
$repo->expects($this->once())->method('createQueryBuilder')->willReturn($qb);

$em = $this->createMock(EntityManagerInterface::class);
$em->expects($this->any())->method('getRepository')->willReturn($repo);
$em->expects($this->never())->method('remove');
$em->expects($this->once())->method('flush');

$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())->method('info')->with('Removed unredeemed vouchers of deleted users', ['count' => 0]);

$handler = new RemoveUnredeemedVouchersHandler($em, $logger);
$handler(new RemoveUnredeemedVouchers());
}
}
4 changes: 3 additions & 1 deletion tests/Schedule/MaintenanceScheduleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Message\PruneUserNotifications;
use App\Message\PruneWebhookDeliveries;
use App\Message\RemoveInactiveUsers;
use App\Message\RemoveUnredeemedVouchers;
use App\Message\SendWeeklyReport;
use App\Message\UnlinkRedeemedVouchers;
use App\Schedule\MaintenanceSchedule;
Expand All @@ -27,10 +28,11 @@ public function testScheduleContainsAllExpectedMessages(): void
$recurringMessages,
);

self::assertCount(5, $recurringMessages);
self::assertCount(6, $recurringMessages);
self::assertContains(PruneWebhookDeliveries::class, $messageTypes);
self::assertContains(PruneUserNotifications::class, $messageTypes);
self::assertContains(UnlinkRedeemedVouchers::class, $messageTypes);
self::assertContains(RemoveUnredeemedVouchers::class, $messageTypes);
self::assertContains(RemoveInactiveUsers::class, $messageTypes);
self::assertContains(SendWeeklyReport::class, $messageTypes);
}
Expand Down
Loading