Skip to content
Merged
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
95 changes: 95 additions & 0 deletions tests/Functional/Commands/MailTestCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace App\Tests\Functional\Commands;

use App\Command\MailTestCommand;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\RawMessage;

/**
* This command is the only way to exercise the mail settings without creating an event, so it has
* to tell the two failures apart: an address that is not one, and a server that is not answering.
*/
class MailTestCommandTest extends KernelTestCase
{
private function mailer(?\Throwable $failure = null): MailerInterface
{
return new class($failure) implements MailerInterface {
public array $sent = [];

public function __construct(private ?\Throwable $failure)
{
}

public function send(RawMessage $message, ?Envelope $envelope = null): void
{
if ($this->failure) {
throw $this->failure;
}

$this->sent[] = $message;
}
};
}

private function tester(MailerInterface $mailer, ?string $inviteAddress = 'no-reply@example.org'): CommandTester
{
$application = new Application();
$application->add(new MailTestCommand($mailer, $inviteAddress));

return new CommandTester($application->find('davis:mail:test'));
}

public function testItSendsOneMessageFromTheConfiguredAddress(): void
{
$tester = $this->tester($mailer = $this->mailer());

$this->assertSame(Command::SUCCESS, $tester->execute(['to' => 'someone@example.org']));
$this->assertCount(1, $mailer->sent);

$message = $mailer->sent[0];
$this->assertSame('no-reply@example.org', $message->getFrom()[0]->getAddress());
$this->assertSame('someone@example.org', $message->getTo()[0]->getAddress());
}

public function testAnAddressThatIsNotOneIsReportedAsSuch(): void
{
$tester = $this->tester($mailer = $this->mailer());

$this->assertSame(Command::FAILURE, $tester->execute(['to' => 'not-an-address']));
$this->assertStringContainsString('not-an-address', $tester->getDisplay());
$this->assertSame([], $mailer->sent, 'Nothing should be handed to the transport');
}

public function testATransportThatIsNotAnsweringIsReportedAsSuch(): void
{
$tester = $this->tester($this->mailer(new TransportException('Connection refused')));

$this->assertSame(Command::FAILURE, $tester->execute(['to' => 'someone@example.org']));

$display = $tester->getDisplay();
$this->assertStringContainsString('Connection refused', $display);
$this->assertStringContainsString('MAILER_DSN', $display, 'It should point at the setting to check');
}

/**
* Without a sender address Davis sends no invitation at all, so testing the transport would
* prove nothing.
*/
public function testItRefusesToRunWithoutASenderAddress(): void
{
$tester = $this->tester($mailer = $this->mailer(), null);

$this->assertSame(Command::FAILURE, $tester->execute(['to' => 'someone@example.org']));
$this->assertStringContainsString('INVITE_FROM_ADDRESS', $tester->getDisplay());
$this->assertSame([], $mailer->sent);
}
}
3 changes: 2 additions & 1 deletion tests/Functional/Controllers/CalendarControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ public function testCalendarNew(): void
$this->assertResponseRedirects('/calendars/'.$userId);
$client->followRedirect();

$this->assertSelectorTextContains('h5', 'default.calendar.title');
// The list has no ORDER BY, so assert both are present rather than which comes first
$this->assertAnySelectorTextContains('h5', 'default.calendar.title');
$this->assertAnySelectorTextContains('h5', 'New test calendar');
}

Expand Down
131 changes: 131 additions & 0 deletions tests/Functional/Plugins/BirthdayCalendarPluginTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace App\Tests\Functional\Plugins;

use App\Entity\AddressBook;
use App\Entity\CalendarObject;
use App\Plugins\BirthdayCalendarPlugin;
use App\Services\BirthdayService;
use Doctrine\ORM\EntityManagerInterface;
use Sabre\CalDAV\Backend\PDO as CalendarBackend;
use Sabre\CardDAV;
use Sabre\DAV;
use Sabre\DAVACL\PrincipalBackend\PDO as PrincipalBackend;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

/**
* `BirthdayServiceTest` covers the service and `SyncBirthdayCalendarTest` the command. This covers
* the piece between them: the plugin that turns a card written over CardDAV into a birthday event.
*/
class BirthdayCalendarPluginTest extends KernelTestCase
{
private const PRINCIPAL = 'principals/test_user';
private const CARD = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Jane Doe\r\nUID:jane-1\r\nBDAY:19900615\r\nEND:VCARD\r\n";

private EntityManagerInterface $em;
private DAV\Server $server;
private CardDAV\Backend\PDO $cardBackend;

protected function setUp(): void
{
self::bootKernel();
$container = static::getContainer();

$this->em = $container->get(EntityManagerInterface::class);
$pdo = $this->em->getConnection()->getNativeConnection();

$principalBackend = new PrincipalBackend($pdo);
$this->cardBackend = new CardDAV\Backend\PDO($pdo);

$this->server = new DAV\Server([
new CardDAV\AddressBookRoot($principalBackend, $this->cardBackend),
]);
$this->server->setBaseUri('/dav/');
$this->server->addPlugin(new CardDAV\Plugin());
$this->server->addPlugin(new BirthdayCalendarPlugin(
$container->get(BirthdayService::class),
new CalendarBackend($pdo)
));
}

private function addressBook(): AddressBook
{
return $this->em->getRepository(AddressBook::class)->findOneBy([
'principalUri' => self::PRINCIPAL,
'uri' => 'default',
]);
}

private function includeInBirthdayCalendar(bool $included): void
{
$this->addressBook()->setIncludedInBirthdayCalendar($included);
$this->em->flush();
$this->em->clear();
}

private function birthdayEvent(): ?CalendarObject
{
$this->em->clear();

return $this->em->getRepository(CalendarObject::class)->findOneBy(['uri' => 'default-jane.vcf.ics']);
}

/**
* Emits the event sabre emits after a `PUT` creates a card, which is what the plugin listens to.
*/
private function writeCardThroughDav(): void
{
$path = 'addressbooks/test_user/default/jane.vcf';
$this->cardBackend->createCard($this->addressBook()->getId(), 'jane.vcf', self::CARD);

$this->server->emit('afterCreateFile', [$path, $this->server->tree->getNodeForPath(\dirname($path))]);
}

public function testWritingACardCreatesTheBirthdayEvent(): void
{
$this->includeInBirthdayCalendar(true);

$this->writeCardThroughDav();

$event = $this->birthdayEvent();
$this->assertNotNull($event, 'The plugin should have produced a birthday event');
$this->assertStringContainsString('Jane Doe', $event->getCalendarData());
}

public function testNothingHappensWhenTheAddressBookOptedOut(): void
{
$this->includeInBirthdayCalendar(false);

$this->writeCardThroughDav();

$this->assertNull($this->birthdayEvent());
}

public function testDeletingACardRemovesItsBirthdayEvent(): void
{
$this->includeInBirthdayCalendar(true);
$this->writeCardThroughDav();
$this->assertNotNull($this->birthdayEvent());

$this->server->emit('beforeUnbind', ['addressbooks/test_user/default/jane.vcf']);

$this->assertNull($this->birthdayEvent(), 'The birthday event should go with the card');
}

/**
* The hooks fire for every write in the tree, not just cards.
*/
public function testANonCardWriteIsIgnored(): void
{
$this->includeInBirthdayCalendar(true);

$this->server->emit('afterCreateFile', [
'addressbooks/test_user/default',
$this->server->tree->getNodeForPath('addressbooks/test_user'),
]);

$this->assertNull($this->birthdayEvent());
}
}
Loading