731 lines
25 KiB
PHP
731 lines
25 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace OCA\AToCal\Tests\Unit\Controller;
|
||
|
||
use OCA\AToCal\Controller\CalendarController;
|
||
use OCP\App\IAppManager;
|
||
use OCP\AppFramework\Http\RedirectResponse;
|
||
use OCP\AppFramework\Http\TemplateResponse;
|
||
use OCP\Calendar\ICalendar;
|
||
use OCP\Calendar\ICalendarEventBuilder;
|
||
use OCP\Calendar\ICreateFromString;
|
||
use OCP\Calendar\IManager;
|
||
use OCP\Constants;
|
||
use OCP\IDateTimeZone;
|
||
use OCP\IL10N;
|
||
use OCP\IRequest;
|
||
use OCP\IURLGenerator;
|
||
use OCP\IUser;
|
||
use OCP\IUserSession;
|
||
use PHPUnit\Framework\Attributes\DataProvider;
|
||
use PHPUnit\Framework\MockObject\MockObject;
|
||
use PHPUnit\Framework\TestCase;
|
||
use Psr\Log\LoggerInterface;
|
||
|
||
require_once __DIR__ . '/OCUtilStub.php';
|
||
|
||
class CalendarControllerTest extends TestCase
|
||
{
|
||
private IManager&MockObject $calendarManager;
|
||
private IAppManager&MockObject $appManager;
|
||
private IUserSession&MockObject $userSession;
|
||
private IURLGenerator&MockObject $urlGenerator;
|
||
private LoggerInterface&MockObject $logger;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
$this->calendarManager = $this->createMock(IManager::class);
|
||
$this->appManager = $this->createMock(IAppManager::class);
|
||
$this->userSession = $this->createMock(IUserSession::class);
|
||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||
$this->logger = $this->createMock(LoggerInterface::class);
|
||
}
|
||
|
||
private function newController(): CalendarController
|
||
{
|
||
$dateTimeZone = $this->createMock(IDateTimeZone::class);
|
||
$dateTimeZone->method('getTimeZone')->willReturn(new \DateTimeZone('UTC'));
|
||
|
||
$l10n = $this->createMock(IL10N::class);
|
||
$l10n->method('t')->willReturnCallback(
|
||
static fn (string $text) => $text
|
||
);
|
||
|
||
return new CalendarController(
|
||
'atocal',
|
||
$this->createMock(IRequest::class),
|
||
$this->calendarManager,
|
||
$this->appManager,
|
||
$this->userSession,
|
||
$this->urlGenerator,
|
||
$dateTimeZone,
|
||
$l10n,
|
||
$this->logger
|
||
);
|
||
}
|
||
|
||
private function invoke(CalendarController $c, string $method, array $args = []): mixed
|
||
{
|
||
$m = new \ReflectionMethod(CalendarController::class, $method);
|
||
|
||
return $m->invokeArgs($c, $args);
|
||
}
|
||
|
||
private function mockUser(string $uid): IUser&MockObject
|
||
{
|
||
$user = $this->createMock(IUser::class);
|
||
$user->method('getUID')->willReturn($uid);
|
||
|
||
return $user;
|
||
}
|
||
|
||
private function mockCalendar(
|
||
string $key,
|
||
string $uri,
|
||
string $displayName,
|
||
int $permissions
|
||
): ICreateFromString&MockObject {
|
||
$calendar = $this->createMock(ICreateFromString::class);
|
||
$calendar->method('getKey')->willReturn($key);
|
||
$calendar->method('getUri')->willReturn($uri);
|
||
$calendar->method('getDisplayName')->willReturn($displayName);
|
||
$calendar->method('getPermissions')->willReturn($permissions);
|
||
|
||
return $calendar;
|
||
}
|
||
|
||
/*
|
||
* assertClean
|
||
*/
|
||
|
||
public static function provideCleanCases(): iterable
|
||
{
|
||
return [
|
||
'plain text' => ['Weekly sync', false, true],
|
||
'description with newline' => ["line one\nline two", true, true],
|
||
'newline without allowance' => ["line one\nline two", false, false],
|
||
'NUL byte' => ["bad\x00title", false, false],
|
||
'backspace' => ["bad\x08title", false, false],
|
||
'delete char' => ["bad\x7Ftitle", false, false],
|
||
'bidi override LRE' => ["\u{202A}evil", false, false],
|
||
'bidi override RLO' => ["\u{202E}evil", false, false],
|
||
'bidi isolate LRI' => ["\u{2066}evil", false, false],
|
||
'bidi isolate PDI' => ["\u{2069}evil", false, false],
|
||
'line separator' => ["a\u{2028}b", false, false],
|
||
'paragraph separator' => ["a\u{2029}b", false, false],
|
||
'next line' => ["a\u{0085}b", false, false],
|
||
'zero-width joiner allowed' => ["\u{1F469}\u{200D}\u{1F4BB} dev", false, true],
|
||
'combining accent allowed' => ["caf\u{00E9}", false, true],
|
||
'invalid utf-8' => ["bad\xC3\x28utf", false, false],
|
||
];
|
||
}
|
||
|
||
#[DataProvider('provideCleanCases')]
|
||
public function testAssertClean(string $value, bool $allowNewlines, bool $expectedClean): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
if ($expectedClean) {
|
||
$this->invoke($c, 'assertClean', [$value, 'bad', $allowNewlines]);
|
||
$this->addToAssertionCount(1);
|
||
|
||
return;
|
||
}
|
||
|
||
$this->expectException(\InvalidArgumentException::class);
|
||
$this->invoke($c, 'assertClean', [$value, 'bad', $allowNewlines]);
|
||
}
|
||
|
||
/*
|
||
* assertHttpsUrl
|
||
*/
|
||
|
||
public static function provideUrlCases(): iterable
|
||
{
|
||
return [
|
||
'valid https' => ['https://example.com/meeting', true],
|
||
'valid with path query' => ['https://example.com/p?a=1#frag', true],
|
||
'space inside' => ['https://exa mple.com/', false],
|
||
'backslash user confusion' => ['https://example.com\\@evil.com/', false],
|
||
'backslash in path' => ['https://example.com/path\\', false],
|
||
'non-ascii host' => ['https://еxample.com/', false],
|
||
'userinfo user' => ['https://user@example.com/', false],
|
||
'userinfo pass only' => ['https://:pass@example.com/', false],
|
||
'userinfo user and pass' => ['https://u:p@example.com/', false],
|
||
'http scheme' => ['http://example.com/', false],
|
||
'ftp scheme' => ['ftp://example.com/', false],
|
||
'no host' => ['https://', false],
|
||
'garbage' => ['not a url', false],
|
||
];
|
||
}
|
||
|
||
#[DataProvider('provideUrlCases')]
|
||
public function testAssertHttpsUrl(string $value, bool $expectedValid): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
if ($expectedValid) {
|
||
$this->invoke($c, 'assertHttpsUrl', [$value, 'bad url']);
|
||
$this->addToAssertionCount(1);
|
||
|
||
return;
|
||
}
|
||
|
||
$this->expectException(\InvalidArgumentException::class);
|
||
$this->invoke($c, 'assertHttpsUrl', [$value, 'bad url']);
|
||
}
|
||
|
||
/*
|
||
* assertBoundedLength
|
||
*/
|
||
|
||
public function testAssertBoundedLength(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$this->invoke($c, 'assertBoundedLength', ['12345', 5, 'too long']);
|
||
$this->addToAssertionCount(1);
|
||
|
||
$this->expectException(\InvalidArgumentException::class);
|
||
$this->invoke($c, 'assertBoundedLength', ['123456', 5, 'too long']);
|
||
}
|
||
|
||
/*
|
||
* parseLocalDateTime
|
||
*/
|
||
|
||
public function testParseLocalDateTime(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$date = $this->invoke($c, 'parseLocalDateTime', ['2026-10-07T14:15']);
|
||
$this->assertInstanceOf(\DateTimeImmutable::class, $date);
|
||
$this->assertSame('2026-10-07 14:15:00 UTC', $date->format('Y-m-d H:i:s T'));
|
||
|
||
$date = $this->invoke($c, 'parseLocalDateTime', ['2026-10-07T14:15:30']);
|
||
$this->assertSame('2026-10-07 14:15:30 UTC', $date->format('Y-m-d H:i:s T'));
|
||
|
||
$this->expectException(\InvalidArgumentException::class);
|
||
$this->invoke($c, 'parseLocalDateTime', ['tomorrow']);
|
||
}
|
||
|
||
/*
|
||
* validateEventData
|
||
*/
|
||
|
||
private function validInput(): array
|
||
{
|
||
return [
|
||
'summary' => ' Weekly sync ',
|
||
'description' => ' Agenda attached ',
|
||
'start' => '2026-10-07T14:15',
|
||
'end' => '2026-10-07T15:15',
|
||
'organizer' => 'boss@example.com',
|
||
'location' => 'https://meet.example.com/room',
|
||
'url' => 'https://meet.example.com/room',
|
||
];
|
||
}
|
||
|
||
public function testValidateEventDataNormalizes(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$data = $this->invoke($c, 'validateEventData', array_values($this->validInput()));
|
||
|
||
$this->assertSame('Weekly sync', $data['summary']);
|
||
$this->assertSame('Agenda attached', $data['description']);
|
||
$this->assertSame('boss@example.com', $data['organizer']);
|
||
$this->assertInstanceOf(\DateTimeImmutable::class, $data['start']);
|
||
$this->assertInstanceOf(\DateTimeImmutable::class, $data['end']);
|
||
$this->assertSame('https://meet.example.com/room', $data['location']);
|
||
$this->assertSame('https://meet.example.com/room', $data['url']);
|
||
}
|
||
|
||
public static function provideValidationFailures(): iterable
|
||
{
|
||
return [
|
||
'missing title' => [
|
||
['summary' => ''],
|
||
'Meeting title is required.',
|
||
],
|
||
'title too long' => [
|
||
['summary' => str_repeat('a', 256)],
|
||
'Meeting title is too long.',
|
||
],
|
||
'title control char' => [
|
||
['summary' => "bad\x00title"],
|
||
'Meeting title contains invalid characters.',
|
||
],
|
||
'title bidi override' => [
|
||
['summary' => "\u{202E}evil title"],
|
||
'Meeting title contains invalid characters.',
|
||
],
|
||
'description too long' => [
|
||
['description' => str_repeat('a', 10001)],
|
||
'Meeting description is too long.',
|
||
],
|
||
'description control char' => [
|
||
['description' => "bad\x01desc"],
|
||
'Meeting description contains invalid characters.',
|
||
],
|
||
'end before start' => [
|
||
['start' => '2026-10-07T15:15', 'end' => '2026-10-07T14:15'],
|
||
'End time must be after start time.',
|
||
],
|
||
'bad date format' => [
|
||
['start' => 'tomorrow', 'end' => '2026-10-07T14:15'],
|
||
'Invalid date/time.',
|
||
],
|
||
'location not https' => [
|
||
['location' => 'http://meet.example.com/'],
|
||
'Location must be an HTTPS URL.',
|
||
],
|
||
'location with userinfo' => [
|
||
['location' => 'https://user@meet.example.com/'],
|
||
'Location must be an HTTPS URL.',
|
||
],
|
||
'location too long' => [
|
||
['location' => 'https://example.com/' . str_repeat('a', 2000)],
|
||
'Location is too long.',
|
||
],
|
||
'url not https' => [
|
||
['url' => 'http://meet.example.com/'],
|
||
'Meeting URL must be an HTTPS URL.',
|
||
],
|
||
'url control char' => [
|
||
['url' => "https://example.com/\x00/room"],
|
||
'Meeting URL contains invalid characters.',
|
||
],
|
||
'organizer no email' => [
|
||
['organizer' => 'not-an-email'],
|
||
'Invalid organizer email address.',
|
||
],
|
||
'organizer too long' => [
|
||
['organizer' => str_repeat('a', 250) . '@example.com'],
|
||
'Organizer email address is too long.',
|
||
],
|
||
'organizer control char' => [
|
||
['organizer' => "boss\x00@example.com"],
|
||
'Organizer contains invalid characters.',
|
||
],
|
||
];
|
||
}
|
||
|
||
#[DataProvider('provideValidationFailures')]
|
||
public function testValidateEventDataRejects(array $overrides, string $message): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$input = array_merge($this->validInput(), $overrides);
|
||
|
||
$this->expectException(\InvalidArgumentException::class);
|
||
$this->expectExceptionMessage($message);
|
||
|
||
$this->invoke($c, 'validateEventData', array_values($input));
|
||
}
|
||
|
||
/*
|
||
* mergeUrlIntoDescription
|
||
*/
|
||
|
||
public function testMergeUrlIntoDescription(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$urlEqualsLocation = [
|
||
'url' => 'https://example.com/room',
|
||
'location' => 'https://example.com/room',
|
||
'description' => 'Agenda',
|
||
];
|
||
$this->assertSame(
|
||
'Agenda',
|
||
$this->invoke($c, 'mergeUrlIntoDescription', [$urlEqualsLocation])
|
||
);
|
||
|
||
$emptyDescription = [
|
||
'url' => 'https://example.com/room',
|
||
'location' => '',
|
||
'description' => '',
|
||
];
|
||
$this->assertSame(
|
||
'https://example.com/room',
|
||
$this->invoke($c, 'mergeUrlIntoDescription', [$emptyDescription])
|
||
);
|
||
|
||
$both = [
|
||
'url' => 'https://example.com/room',
|
||
'location' => 'https://meet.example.com/other',
|
||
'description' => 'Agenda',
|
||
];
|
||
$this->assertSame(
|
||
"Agenda\n\nhttps://example.com/room",
|
||
$this->invoke($c, 'mergeUrlIntoDescription', [$both])
|
||
);
|
||
}
|
||
|
||
/*
|
||
* calendar form value, selection and default
|
||
*/
|
||
|
||
public function testCalendarValue(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$calendar = $this->mockCalendar('5', 'personal', 'Personal', Constants::PERMISSION_CREATE);
|
||
|
||
$this->assertSame('5::personal', $this->invoke($c, 'calendarValue', [$calendar]));
|
||
}
|
||
|
||
public function testSelectCalendarResolvesKeyCollision(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$personal = $this->mockCalendar('5', 'personal', 'Personal', Constants::PERMISSION_CREATE);
|
||
$shared = $this->mockCalendar('5', 'shared-by-bob', 'Shared', Constants::PERMISSION_CREATE);
|
||
|
||
$calendars = [$personal, $shared];
|
||
|
||
$this->assertSame($personal, $this->invoke($c, 'selectCalendar', [$calendars, '5::personal']));
|
||
$this->assertSame($shared, $this->invoke($c, 'selectCalendar', [$calendars, '5::shared-by-bob']));
|
||
}
|
||
|
||
public static function provideNoMatchValues(): iterable
|
||
{
|
||
return [
|
||
'no separator' => ['5'],
|
||
'wrong uri' => ['5::nope'],
|
||
'wrong key' => ['9::personal'],
|
||
'empty' => [''],
|
||
];
|
||
}
|
||
|
||
#[DataProvider('provideNoMatchValues')]
|
||
public function testSelectCalendarNoMatch(string $value): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$calendar = $this->mockCalendar('5', 'personal', 'Personal', Constants::PERMISSION_CREATE);
|
||
|
||
$this->assertNull($this->invoke($c, 'selectCalendar', [[$calendar], $value]));
|
||
}
|
||
|
||
public function testDefaultCalendarId(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$work = $this->mockCalendar('1', 'work', 'Work', Constants::PERMISSION_CREATE);
|
||
$personal = $this->mockCalendar('2', 'personal', 'Personal', Constants::PERMISSION_CREATE);
|
||
|
||
$this->assertSame('2::personal', $this->invoke($c, 'defaultCalendarId', [[$work, $personal]]));
|
||
$this->assertSame('1::work', $this->invoke($c, 'defaultCalendarId', [[$work]]));
|
||
$this->assertSame('', $this->invoke($c, 'defaultCalendarId', [[]]));
|
||
}
|
||
|
||
/*
|
||
* findWritableCalendars
|
||
*/
|
||
|
||
public function testFindWritableCalendars(): void
|
||
{
|
||
$this->calendarManager->method('getCalendarsForPrincipal')
|
||
->with('principals/users/alice')
|
||
->willReturn([
|
||
$this->mockCalendar('1', 'work', 'Work', Constants::PERMISSION_CREATE),
|
||
$this->mockCalendar('2', 'shared', 'Shared', Constants::PERMISSION_READ),
|
||
$this->createMock(ICalendar::class),
|
||
]);
|
||
|
||
$c = $this->newController();
|
||
|
||
$writable = $this->invoke($c, 'findWritableCalendars', ['alice']);
|
||
|
||
$this->assertCount(1, $writable);
|
||
$this->assertSame('1', $writable[0]->getKey());
|
||
}
|
||
|
||
/*
|
||
* createEvent: VALARM injection
|
||
*/
|
||
|
||
public function testCreateEventInjectsValarm(): void
|
||
{
|
||
$builder = $this->createMock(ICalendarEventBuilder::class);
|
||
$builder->method('toIcs')->willReturn(
|
||
"BEGIN:VCALENDAR\r\n" .
|
||
"VERSION:2.0\r\n" .
|
||
"PRODID:-//Nextcloud//AToCal//EN\r\n" .
|
||
"BEGIN:VEVENT\r\n" .
|
||
"UID:test-uid-123\r\n" .
|
||
"SUMMARY:Weekly sync\r\n" .
|
||
"END:VEVENT\r\n" .
|
||
"END:VCALENDAR\r\n"
|
||
);
|
||
$this->calendarManager->method('createEventBuilder')->willReturn($builder);
|
||
|
||
$calendar = $this->mockCalendar('5', 'personal', 'Personal', Constants::PERMISSION_CREATE);
|
||
$calendar->expects($this->once())->method('createFromString')
|
||
->willReturnCallback(function (string $name, string $data): bool {
|
||
$this->assertSame('test-uid-123.ics', $name);
|
||
$this->assertStringContainsString('BEGIN:VALARM', $data);
|
||
$this->assertStringContainsString('TRIGGER:-PT15M', $data);
|
||
|
||
return true;
|
||
});
|
||
|
||
$c = $this->newController();
|
||
|
||
$data = [
|
||
'summary' => 'Weekly sync',
|
||
'description' => '',
|
||
'start' => new \DateTimeImmutable('2026-10-07T14:15:00 UTC'),
|
||
'end' => new \DateTimeImmutable('2026-10-07T15:15:00 UTC'),
|
||
'organizer' => '',
|
||
'location' => '',
|
||
'url' => '',
|
||
];
|
||
|
||
$this->invoke($c, 'createEvent', [$calendar, $data]);
|
||
}
|
||
|
||
/*
|
||
* create(): happy path redirects to the Calendar app
|
||
*/
|
||
|
||
private function setUpWritableCalendars(): void
|
||
{
|
||
$this->calendarManager->method('getCalendarsForPrincipal')
|
||
->willReturn([
|
||
$this->mockCalendar('5', 'personal', 'Personal', Constants::PERMISSION_CREATE),
|
||
]);
|
||
}
|
||
|
||
/*
|
||
* The event builder ICS is parsed by VObject before it
|
||
* reaches createFromString(), so the create() tests need
|
||
* a valid skeleton ICS.
|
||
*/
|
||
private function setUpEventBuilder(): void
|
||
{
|
||
$builder = $this->createMock(ICalendarEventBuilder::class);
|
||
$builder->method('toIcs')->willReturn(
|
||
"BEGIN:VCALENDAR\r\n" .
|
||
"VERSION:2.0\r\n" .
|
||
"PRODID:-//Nextcloud//AToCal//EN\r\n" .
|
||
"BEGIN:VEVENT\r\n" .
|
||
"UID:test-uid-123\r\n" .
|
||
"SUMMARY:Weekly sync\r\n" .
|
||
"END:VEVENT\r\n" .
|
||
"END:VCALENDAR\r\n"
|
||
);
|
||
$this->calendarManager->method('createEventBuilder')->willReturn($builder);
|
||
}
|
||
|
||
private function validCreateArgs(): array
|
||
{
|
||
return [
|
||
'Weekly sync',
|
||
'Agenda',
|
||
'2026-10-07T14:15',
|
||
'2026-10-07T15:15',
|
||
'boss@example.com',
|
||
'',
|
||
'',
|
||
'5::personal',
|
||
];
|
||
}
|
||
|
||
public function testCreateRedirectsToCalendarApp(): void
|
||
{
|
||
$this->userSession->method('getUser')->willReturn($this->mockUser('alice'));
|
||
$this->setUpWritableCalendars();
|
||
$this->setUpEventBuilder();
|
||
$this->appManager->method('isEnabledForUser')->willReturn(true);
|
||
$this->urlGenerator->method('linkToRoute')->willReturn('/apps/calendar/');
|
||
|
||
$c = $this->newController();
|
||
|
||
$response = $c->create(...$this->validCreateArgs());
|
||
|
||
$this->assertInstanceOf(RedirectResponse::class, $response);
|
||
$this->assertSame('/apps/calendar/', $response->getRedirectURL());
|
||
}
|
||
|
||
public function testCreateFallsBackWithoutCalendarApp(): void
|
||
{
|
||
$this->userSession->method('getUser')->willReturn($this->mockUser('alice'));
|
||
$this->setUpWritableCalendars();
|
||
$this->setUpEventBuilder();
|
||
$this->appManager->method('isEnabledForUser')->willReturn(false);
|
||
$this->urlGenerator->method('getAbsoluteURL')->willReturn('https://cloud.example.com/');
|
||
|
||
$c = $this->newController();
|
||
|
||
$response = $c->create(...$this->validCreateArgs());
|
||
|
||
$this->assertInstanceOf(RedirectResponse::class, $response);
|
||
$this->assertSame('https://cloud.example.com/', $response->getRedirectURL());
|
||
}
|
||
|
||
public function testCreateUnwritableSelectionShowsError(): void
|
||
{
|
||
$this->userSession->method('getUser')->willReturn($this->mockUser('alice'));
|
||
$this->setUpWritableCalendars();
|
||
|
||
$args = $this->validCreateArgs();
|
||
$args[7] = '9::nope';
|
||
|
||
$c = $this->newController();
|
||
|
||
$response = $c->create(...$args);
|
||
|
||
$this->assertInstanceOf(TemplateResponse::class, $response);
|
||
$this->assertSame('confirm', $response->getTemplateName());
|
||
$this->assertSame(
|
||
'The selected calendar is no longer writable. Pick another one and try again.',
|
||
$response->getParams()['error']
|
||
);
|
||
}
|
||
|
||
public function testCreateNoWritableCalendars(): void
|
||
{
|
||
$this->userSession->method('getUser')->willReturn($this->mockUser('alice'));
|
||
$this->calendarManager->method('getCalendarsForPrincipal')->willReturn([]);
|
||
|
||
$c = $this->newController();
|
||
|
||
$response = $c->create(...$this->validCreateArgs());
|
||
|
||
$this->assertSame(
|
||
'No writable calendar was found in your account. Create one in the Nextcloud Calendar app, then try again.',
|
||
$response->getParams()['error']
|
||
);
|
||
}
|
||
|
||
public function testCreateLogsBackendFailure(): void
|
||
{
|
||
$this->userSession->method('getUser')->willReturn($this->mockUser('alice'));
|
||
$this->setUpEventBuilder();
|
||
|
||
$calendar = $this->mockCalendar('5', 'personal', 'Personal', Constants::PERMISSION_CREATE);
|
||
$calendar->method('createFromString')
|
||
->willThrowException(new \RuntimeException('backend down'));
|
||
$this->calendarManager->method('getCalendarsForPrincipal')
|
||
->willReturn([$calendar]);
|
||
|
||
$this->logger->expects($this->once())->method('error')
|
||
->with(
|
||
'Failed to create calendar event: backend down',
|
||
['exception' => new \RuntimeException('backend down')]
|
||
);
|
||
|
||
$c = $this->newController();
|
||
|
||
$response = $c->create(...$this->validCreateArgs());
|
||
|
||
$this->assertInstanceOf(TemplateResponse::class, $response);
|
||
$this->assertSame(
|
||
'The event could not be created. Please try again.',
|
||
$response->getParams()['error']
|
||
);
|
||
}
|
||
|
||
public function testCreateWithoutUserThrows(): void
|
||
{
|
||
$this->userSession->method('getUser')->willReturn(null);
|
||
|
||
$c = $this->newController();
|
||
|
||
$this->expectException(\RuntimeException::class);
|
||
$this->expectExceptionMessage('No authenticated Nextcloud user.');
|
||
|
||
$c->create(...$this->validCreateArgs());
|
||
}
|
||
|
||
public function testCreateInvalidInputRendersErrorPage(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$args = $this->validCreateArgs();
|
||
$args[0] = '';
|
||
|
||
$response = $c->create(...$args);
|
||
|
||
$this->assertInstanceOf(TemplateResponse::class, $response);
|
||
$this->assertSame('error', $response->getTemplateName());
|
||
$this->assertSame('core', $response->getApp());
|
||
$this->assertSame(
|
||
'Meeting title is required.',
|
||
$response->getParams()['errors'][0]['error']
|
||
);
|
||
}
|
||
|
||
/*
|
||
* confirm()
|
||
*/
|
||
|
||
public function testConfirmRendersTemplate(): void
|
||
{
|
||
$this->userSession->method('getUser')->willReturn($this->mockUser('alice'));
|
||
$this->setUpWritableCalendars();
|
||
$this->urlGenerator->method('getAbsoluteURL')->willReturn('https://cloud.example.com/');
|
||
|
||
$c = $this->newController();
|
||
|
||
$response = $c->confirm(
|
||
'Weekly sync',
|
||
'Agenda',
|
||
'2026-10-07T14:15',
|
||
'2026-10-07T15:15',
|
||
'boss@example.com',
|
||
'https://meet.example.com/room',
|
||
''
|
||
);
|
||
|
||
$this->assertInstanceOf(TemplateResponse::class, $response);
|
||
$this->assertSame('confirm', $response->getTemplateName());
|
||
|
||
$params = $response->getParams();
|
||
$this->assertSame('Weekly sync', $params['summary']);
|
||
$this->assertSame('2026-10-07T14:15', $params['start']);
|
||
$this->assertSame('', $params['error']);
|
||
$this->assertSame('5::personal', $params['calendars'][0]['value']);
|
||
$this->assertSame('5::personal', $params['calendarId']);
|
||
}
|
||
|
||
public function testConfirmInvalidInputRendersErrorPage(): void
|
||
{
|
||
$c = $this->newController();
|
||
|
||
$response = $c->confirm(
|
||
'Weekly sync',
|
||
'',
|
||
'2026-10-07T14:15',
|
||
'2026-10-07T13:15'
|
||
);
|
||
|
||
$this->assertSame('error', $response->getTemplateName());
|
||
$this->assertSame(
|
||
'End time must be after start time.',
|
||
$response->getParams()['errors'][0]['error']
|
||
);
|
||
}
|
||
|
||
public function testConfirmRegistersStylesheet(): void
|
||
{
|
||
$this->userSession->method('getUser')->willReturn($this->mockUser('alice'));
|
||
$this->setUpWritableCalendars();
|
||
$this->urlGenerator->method('getAbsoluteURL')->willReturn('https://cloud.example.com/');
|
||
|
||
$c = $this->newController();
|
||
|
||
$c->confirm('Weekly sync', '', '2026-10-07T14:15', '2026-10-07T15:15');
|
||
|
||
$this->assertContains(
|
||
['atocal', 'style', false],
|
||
\OC_Util::$styles
|
||
);
|
||
}
|
||
}
|