Publication initiale de RelaisSMS : documentation et sources nettoyees
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Calendar;
|
||||
|
||||
use App\Contracts\Calendar\CalendarClientInterface;
|
||||
use App\Contracts\Sms\SmsProviderInterface;
|
||||
use App\Models\CalendarEvent;
|
||||
use App\Models\CalendarReminder;
|
||||
use App\Models\Contact;
|
||||
use App\Models\SmsCampaign;
|
||||
use App\Models\User;
|
||||
use App\Services\Calendar\CalendarReminderSender;
|
||||
use App\Services\Calendar\CalendarRemoteEvent;
|
||||
use App\Services\Sms\SmsMessageLength;
|
||||
use App\Services\Sms\SmsProviderException;
|
||||
use App\Services\Sms\SmsSendResult;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CalendarReminderProcessingTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_uncertain_reminder_is_not_retried_after_calendar_sync(): void
|
||||
{
|
||||
Carbon::setTestNow(Carbon::parse('2026-10-06 13:05:00', 'UTC'));
|
||||
User::factory()->create();
|
||||
$contact = Contact::query()->create(['source' => 'local', 'first_name' => 'Contact', 'last_name' => 'Test']);
|
||||
$contact->phoneNumbers()->create([
|
||||
'raw_number' => '06 11 11 11 11', 'normalized_number' => '+33611111111',
|
||||
'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true,
|
||||
]);
|
||||
config(['relais_sms.caldav.default_contact_ids' => [$contact->id]]);
|
||||
$this->mock(SmsProviderInterface::class)->shouldReceive('send')->once()
|
||||
->andThrow(new SmsProviderException('Timeout', outcomeUnknown: true));
|
||||
$ical = "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:uncertain-reminder\nSUMMARY:Reunion\nDTSTART:20261006T140000Z\nBEGIN:VALARM\nACTION:DISPLAY\nTRIGGER:-PT1H\nEND:VALARM\nEND:VEVENT\nEND:VCALENDAR";
|
||||
$this->mock(CalendarClientInterface::class)->shouldReceive('fetchEvents')->twice()
|
||||
->andReturn([new CalendarRemoteEvent(href: '/calendar/uncertain.ics', etag: 'etag', ical: $ical)]);
|
||||
$this->artisan('calendar:process-reminders --force-sync')->assertSuccessful();
|
||||
$this->assertDatabaseHas('calendar_reminders', ['status' => 'uncertain', 'sent_at' => null]);
|
||||
$this->artisan('calendar:process-reminders --force-sync')->assertSuccessful();
|
||||
$this->assertSame(1, SmsCampaign::query()->count());
|
||||
$this->assertDatabaseHas('calendar_reminders', ['status' => 'uncertain']);
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
|
||||
public function test_expired_pre_event_reminder_is_skipped_without_calling_the_sms_provider(): void
|
||||
{
|
||||
$this->mock(SmsProviderInterface::class)->shouldNotReceive('send');
|
||||
$event = CalendarEvent::query()->create([
|
||||
'source' => 'caldav',
|
||||
'external_id' => 'expired-reminder',
|
||||
'summary' => 'Reunion passee',
|
||||
'starts_at' => '2026-09-30 08:00:00',
|
||||
]);
|
||||
$reminder = $event->reminders()->create([
|
||||
'reminder_key' => 'expired',
|
||||
'action' => 'DISPLAY',
|
||||
'remind_at' => '2026-09-30 07:00:00',
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$sent = $this->app->make(CalendarReminderSender::class)->sendDue(Carbon::parse('2026-09-30 10:00:00', 'Europe/Paris'));
|
||||
|
||||
$this->assertSame(0, $sent);
|
||||
$this->assertSame('skipped', $reminder->fresh()->status);
|
||||
$this->assertSame(0, SmsCampaign::query()->count());
|
||||
}
|
||||
|
||||
public function test_reminder_message_prioritizes_title_date_location_then_truncated_description_in_one_sms(): void
|
||||
{
|
||||
config([
|
||||
'relais_sms.caldav.reminder_message_header' => '',
|
||||
'relais_sms.caldav.reminder_message_footer' => '',
|
||||
'relais_sms.caldav.reminder_description_max_length' => 500,
|
||||
'relais_sms.caldav.display_timezone' => 'Europe/Paris',
|
||||
]);
|
||||
$event = CalendarEvent::query()->create([
|
||||
'source' => 'caldav',
|
||||
'external_id' => 'long-message',
|
||||
'summary' => 'Reunion importante',
|
||||
'description' => str_repeat('description longue ', 30),
|
||||
'location' => 'Salle A',
|
||||
'starts_at' => '2026-06-12 14:00:00',
|
||||
]);
|
||||
$reminder = new CalendarReminder;
|
||||
$reminder->setRelation('event', $event);
|
||||
$method = new \ReflectionMethod(CalendarReminderSender::class, 'message');
|
||||
|
||||
/** @var string $message */
|
||||
$message = $method->invoke($this->app->make(CalendarReminderSender::class), $reminder);
|
||||
|
||||
$this->assertStringStartsWith("Rappel : Reunion importante\n12/06/2026 16:00\nSalle A\n", $message);
|
||||
$this->assertStringEndsWith('[...]', $message);
|
||||
$this->assertTrue($this->app->make(SmsMessageLength::class)->fitsInOneSms($message));
|
||||
}
|
||||
|
||||
public function test_due_display_alarm_sends_one_sms_to_attendee_and_organizer(): void
|
||||
{
|
||||
Carbon::setTestNow(Carbon::parse('2026-06-12 15:05:00', 'Europe/Paris'));
|
||||
config([
|
||||
'relais_sms.caldav.reminder_message_header' => 'AFPMA',
|
||||
'relais_sms.caldav.reminder_message_footer' => 'A bientot',
|
||||
'relais_sms.caldav.reminder_description_max_length' => 20,
|
||||
'relais_sms.caldav.sync_interval_minutes' => 60,
|
||||
]);
|
||||
|
||||
$calendarClient = new class implements CalendarClientInterface
|
||||
{
|
||||
public int $calls = 0;
|
||||
|
||||
public function fetchEvents(CarbonInterface $from, CarbonInterface $to): array
|
||||
{
|
||||
$this->calls++;
|
||||
|
||||
return [
|
||||
new CalendarRemoteEvent(
|
||||
href: '/remote.php/dav/calendars/david/personal/event-test.ics',
|
||||
etag: 'etag-1',
|
||||
ical: <<<'ICS'
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
UID:event-test
|
||||
SUMMARY:test
|
||||
DESCRIPTION:Description calendrier tres longue pour le SMS.
|
||||
LOCATION:Salle A
|
||||
DTSTART;TZID=Europe/Paris:20260612T160000
|
||||
DTEND;TZID=Europe/Paris:20260612T170000
|
||||
ORGANIZER;CN=David Mercereau:mailto:david@example.test
|
||||
ATTENDEE;CN=Marie Durand:mailto:marie@example.test
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER:-PT1H
|
||||
DESCRIPTION:Rappel
|
||||
END:VALARM
|
||||
BEGIN:VALARM
|
||||
ACTION:EMAIL
|
||||
TRIGGER:-PT2H
|
||||
DESCRIPTION:Rappel email
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
ICS,
|
||||
),
|
||||
];
|
||||
}
|
||||
};
|
||||
$this->app->bind(CalendarClientInterface::class, fn (): CalendarClientInterface => $calendarClient);
|
||||
|
||||
$this->app->bind(SmsProviderInterface::class, fn (): SmsProviderInterface => new class implements SmsProviderInterface
|
||||
{
|
||||
public function send(SmsCampaign $campaign, array $recipients): SmsSendResult
|
||||
{
|
||||
return new SmsSendResult(
|
||||
serviceName: 'sms-test',
|
||||
providerJobId: 'job-calendar',
|
||||
requestPayload: ['receivers' => array_column($recipients, 'number')],
|
||||
responsePayload: ['ok' => true],
|
||||
providerMessageIds: ['message-calendar-1', 'message-calendar-2'],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
User::factory()->create();
|
||||
$contact = Contact::query()->create([
|
||||
'source' => 'local',
|
||||
'first_name' => 'Marie',
|
||||
'last_name' => 'Durand',
|
||||
'email' => 'marie@example.test',
|
||||
]);
|
||||
$contact->phoneNumbers()->create([
|
||||
'raw_number' => '06 11 11 11 11',
|
||||
'normalized_number' => '+33611111111',
|
||||
'country' => 'FR',
|
||||
'is_valid' => true,
|
||||
'is_mobile' => true,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
$organizer = Contact::query()->create([
|
||||
'source' => 'local',
|
||||
'first_name' => 'David',
|
||||
'last_name' => 'Mercereau',
|
||||
'email' => 'david@example.test',
|
||||
]);
|
||||
$organizer->phoneNumbers()->create([
|
||||
'raw_number' => '06 22 22 22 22',
|
||||
'normalized_number' => '+33622222222',
|
||||
'country' => 'FR',
|
||||
'is_valid' => true,
|
||||
'is_mobile' => true,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
|
||||
$this->artisan('calendar:process-reminders')->assertExitCode(0);
|
||||
$this->artisan('calendar:process-reminders')->assertExitCode(0);
|
||||
|
||||
$this->assertSame(1, $calendarClient->calls, 'Le client CalDAV ne doit etre appele qu une fois dans l intervalle configure.');
|
||||
$this->assertDatabaseHas('calendar_events', [
|
||||
'source' => 'caldav',
|
||||
'external_id' => 'event-test',
|
||||
'summary' => 'test',
|
||||
]);
|
||||
$this->assertDatabaseHas('calendar_reminders', [
|
||||
'status' => 'sent',
|
||||
'action' => 'DISPLAY',
|
||||
'trigger_minutes' => -60,
|
||||
]);
|
||||
$this->assertSame(1, CalendarReminder::query()->count(), 'Un seul rappel DISPLAY doit etre programme.');
|
||||
$this->assertSame(1, SmsCampaign::query()->count(), 'Un rappel deja envoye ne doit pas recreer de campagne.');
|
||||
$this->assertDatabaseHas('sms_campaigns', [
|
||||
'message' => "AFPMA\nRappel : test\n12/06/2026 16:00\nSalle A\nDescription calendri[...]\nA bientot",
|
||||
'status' => 'sent',
|
||||
'recipients_count' => 2,
|
||||
]);
|
||||
$this->assertDatabaseHas('sms_deliveries', [
|
||||
'number' => '+33611111111',
|
||||
'status' => 'sent',
|
||||
'provider_message_id' => 'message-calendar-1',
|
||||
]);
|
||||
$this->assertDatabaseHas('sms_deliveries', [
|
||||
'number' => '+33622222222',
|
||||
'status' => 'sent',
|
||||
'provider_message_id' => 'message-calendar-2',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_skipped_reminder_is_not_reactivated_after_organizer_becomes_resolvable(): void
|
||||
{
|
||||
Carbon::setTestNow(Carbon::parse('2026-06-12 15:05:00', 'Europe/Paris'));
|
||||
config(['relais_sms.caldav.sync_interval_minutes' => 60]);
|
||||
|
||||
$calendarClient = new class implements CalendarClientInterface
|
||||
{
|
||||
public function fetchEvents(CarbonInterface $from, CarbonInterface $to): array
|
||||
{
|
||||
return [
|
||||
new CalendarRemoteEvent(
|
||||
href: '/calendar/organizer-only.ics',
|
||||
etag: 'etag-organizer',
|
||||
ical: <<<'ICS'
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
BEGIN:VEVENT
|
||||
UID:organizer-only
|
||||
SUMMARY:organizer only
|
||||
DTSTART;TZID=Europe/Paris:20260612T160000
|
||||
DTEND;TZID=Europe/Paris:20260612T170000
|
||||
ORGANIZER;CN=David Mercereau:mailto:david@example.test
|
||||
BEGIN:VALARM
|
||||
ACTION:DISPLAY
|
||||
TRIGGER:-PT1H
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
ICS,
|
||||
),
|
||||
];
|
||||
}
|
||||
};
|
||||
$this->app->bind(CalendarClientInterface::class, fn (): CalendarClientInterface => $calendarClient);
|
||||
User::factory()->create();
|
||||
|
||||
$this->artisan('calendar:process-reminders')->assertExitCode(0);
|
||||
|
||||
$this->assertDatabaseHas('calendar_reminders', ['status' => 'skipped']);
|
||||
|
||||
$organizer = Contact::query()->create([
|
||||
'source' => 'local',
|
||||
'first_name' => 'David',
|
||||
'last_name' => 'Mercereau',
|
||||
'email' => 'david@example.test',
|
||||
]);
|
||||
$organizer->phoneNumbers()->create([
|
||||
'raw_number' => '06 22 22 22 22',
|
||||
'normalized_number' => '+33622222222',
|
||||
'country' => 'FR',
|
||||
'is_valid' => true,
|
||||
'is_mobile' => true,
|
||||
'is_primary' => true,
|
||||
]);
|
||||
Carbon::setTestNow(Carbon::now()->addHour());
|
||||
|
||||
$this->artisan('calendar:process-reminders --force-sync')->assertExitCode(0);
|
||||
|
||||
$this->assertDatabaseHas('calendar_reminders', ['status' => 'skipped']);
|
||||
$this->assertSame(0, SmsCampaign::query()->count());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Calendar;
|
||||
|
||||
use App\Contracts\Calendar\CalendarClientInterface;
|
||||
use App\Contracts\Sms\SmsProviderInterface;
|
||||
use App\Models\CalendarEvent;
|
||||
use App\Models\CalendarReminder;
|
||||
use App\Models\Contact;
|
||||
use App\Models\SmsCampaign;
|
||||
use App\Models\User;
|
||||
use App\Services\Calendar\CalendarEventSyncService;
|
||||
use App\Services\Calendar\CalendarReminderSender;
|
||||
use App\Services\Calendar\CalendarRemoteEvent;
|
||||
use App\Services\Calendar\ICalendarParser;
|
||||
use App\Services\Sms\SmsSendResult;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RecurringCalendarTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Carbon::setTestNow();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function calendar(string $properties = '', string $exceptions = ''): string
|
||||
{
|
||||
return "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:staff\nSUMMARY:Point Salaries\nDTSTART;TZID=Europe/Paris:20261005T110000\nDTEND;TZID=Europe/Paris:20261005T120000\nRRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO;COUNT=4\n{$properties}\nBEGIN:VALARM\nACTION:DISPLAY\nTRIGGER:-PT2H\nEND:VALARM\nEND:VEVENT\n{$exceptions}\nEND:VCALENDAR";
|
||||
}
|
||||
|
||||
private function sync(string $ical): void
|
||||
{
|
||||
$client = $this->mock(CalendarClientInterface::class);
|
||||
$client->shouldReceive('fetchEvents')->once()->andReturn([
|
||||
new CalendarRemoteEvent('/calendar/staff.ics', 'etag', $ical),
|
||||
]);
|
||||
$this->app->make(CalendarEventSyncService::class)->sync();
|
||||
}
|
||||
|
||||
public function test_each_occurrence_sends_once_and_keeps_local_time_across_dst(): void
|
||||
{
|
||||
Carbon::setTestNow(Carbon::parse('2026-10-19 09:14:00', 'Europe/Paris'));
|
||||
User::factory()->create();
|
||||
$contact = Contact::query()->create(['source' => 'local', 'first_name' => 'Test', 'last_name' => 'Contact']);
|
||||
$contact->phoneNumbers()->create([
|
||||
'raw_number' => '0611111111', 'normalized_number' => '+33611111111',
|
||||
'country' => 'FR', 'is_valid' => true, 'is_mobile' => true, 'is_primary' => true,
|
||||
]);
|
||||
config(['relais_sms.caldav.default_contact_ids' => [$contact->id], 'relais_sms.caldav.display_timezone' => 'Europe/Paris']);
|
||||
$this->mock(SmsProviderInterface::class)->shouldReceive('send')->twice()->andReturn(
|
||||
new SmsSendResult('test', 'job', [], [], ['message']),
|
||||
);
|
||||
$ical = $this->calendar('EXDATE;TZID=Europe/Paris:20261005T110000');
|
||||
$this->sync($ical);
|
||||
$sender = $this->app->make(CalendarReminderSender::class);
|
||||
$this->assertSame(1, $sender->sendDue());
|
||||
$this->sync($ical);
|
||||
$this->assertSame(0, $sender->sendDue());
|
||||
$this->assertDatabaseHas('calendar_events', ['starts_at' => '2026-11-02 10:00:00']);
|
||||
$this->assertDatabaseHas('calendar_reminders', ['remind_at' => '2026-11-02 08:00:00', 'status' => 'pending']);
|
||||
|
||||
Carbon::setTestNow(Carbon::parse('2026-11-02 09:14:00', 'Europe/Paris'));
|
||||
$this->sync($ical);
|
||||
$this->assertSame(1, $sender->sendDue());
|
||||
$this->assertSame(2, SmsCampaign::query()->count());
|
||||
$this->assertStringContainsString('02/11/2026 11:00', SmsCampaign::query()->latest('id')->first()->message);
|
||||
}
|
||||
|
||||
public function test_new_exdate_and_cancelled_exception_cancel_existing_pending_reminders(): void
|
||||
{
|
||||
Carbon::setTestNow(Carbon::parse('2026-10-18 12:00:00', 'UTC'));
|
||||
$this->sync($this->calendar());
|
||||
$october = CalendarEvent::query()->where('starts_at', '2026-10-19 09:00:00')->firstOrFail();
|
||||
$november = CalendarEvent::query()->where('starts_at', '2026-11-02 10:00:00')->firstOrFail();
|
||||
$this->sync($this->calendar('EXDATE;TZID=Europe/Paris:20261019T110000', "BEGIN:VEVENT\nUID:staff\nRECURRENCE-ID;TZID=Europe/Paris:20261102T110000\nSTATUS:CANCELLED\nEND:VEVENT"));
|
||||
foreach ([$october, $november] as $event) {
|
||||
$this->assertNotNull($event->fresh()->deleted_at);
|
||||
$this->assertSame('cancelled', $event->reminders()->first()->status);
|
||||
}
|
||||
$this->mock(SmsProviderInterface::class)->shouldNotReceive('send');
|
||||
Carbon::setTestNow(Carbon::parse('2026-11-02 08:14:00', 'UTC'));
|
||||
$this->assertSame(0, $this->app->make(CalendarReminderSender::class)->sendDue());
|
||||
}
|
||||
|
||||
public function test_moved_occurrence_updates_one_reminder_and_keeps_identity_after_send(): void
|
||||
{
|
||||
Carbon::setTestNow(Carbon::parse('2026-10-18 12:00:00', 'UTC'));
|
||||
$this->sync($this->calendar());
|
||||
$event = CalendarEvent::query()->where('starts_at', '2026-10-19 09:00:00')->firstOrFail();
|
||||
$reminder = $event->reminders()->firstOrFail();
|
||||
$exception = "BEGIN:VEVENT\nUID:staff\nRECURRENCE-ID;TZID=Europe/Paris:20261019T110000\nDTSTART;TZID=Europe/Paris:20261020T140000\nDTEND;TZID=Europe/Paris:20261020T150000\nSUMMARY:Point deplace\nBEGIN:VALARM\nACTION:DISPLAY\nTRIGGER:-PT2H\nEND:VALARM\nEND:VEVENT";
|
||||
$this->sync($this->calendar('', $exception));
|
||||
$this->assertSame('2026-10-20 12:00:00', $event->fresh()->getRawOriginal('starts_at'));
|
||||
$this->assertSame('2026-10-20 10:00:00', $reminder->fresh()->getRawOriginal('remind_at'));
|
||||
$this->assertSame(1, $event->reminders()->count());
|
||||
$reminder->update(['status' => 'sent']);
|
||||
$this->sync($this->calendar('', str_replace('20261020T140000', '20261020T160000', $exception)));
|
||||
$this->assertSame('sent', $reminder->fresh()->status);
|
||||
$this->assertSame(1, $event->reminders()->count());
|
||||
}
|
||||
|
||||
public function test_existing_sent_first_occurrence_is_preserved_when_importing_a_series(): void
|
||||
{
|
||||
Carbon::setTestNow(Carbon::parse('2026-10-05 07:15:00', 'UTC'));
|
||||
$event = CalendarEvent::query()->create(['source' => 'caldav', 'external_id' => 'staff', 'starts_at' => '2026-10-05 09:00:00']);
|
||||
$event->reminders()->create(['reminder_key' => sha1('DISPLAY|-PT2H|0'), 'status' => 'sent', 'sent_at' => Carbon::now(), 'remind_at' => '2026-10-05 07:00:00']);
|
||||
$this->sync($this->calendar());
|
||||
$this->assertSame(4, CalendarEvent::query()->count());
|
||||
$this->assertSame('sent', $event->reminders()->first()->status);
|
||||
$this->assertStringStartsWith('recurrence:', $event->fresh()->external_id);
|
||||
$this->assertSame(1, CalendarReminder::query()->where('status', 'sent')->count());
|
||||
}
|
||||
|
||||
public function test_cancelled_series_can_be_restored_and_expired_reminders_are_not_sent(): void
|
||||
{
|
||||
Carbon::setTestNow(Carbon::parse('2026-10-19 09:36:00', 'UTC'));
|
||||
$this->mock(SmsProviderInterface::class)->shouldNotReceive('send');
|
||||
$this->sync($this->calendar());
|
||||
$event = CalendarEvent::query()->where('starts_at', '2026-10-19 09:00:00')->firstOrFail();
|
||||
$this->assertSame(0, $this->app->make(CalendarReminderSender::class)->sendDue());
|
||||
$this->assertSame('skipped', $event->reminders()->first()->status);
|
||||
$future = CalendarEvent::query()->where('starts_at', '2026-11-02 10:00:00')->firstOrFail();
|
||||
$this->sync($this->calendar('STATUS:CANCELLED'));
|
||||
$this->assertSame('cancelled', $future->reminders()->first()->status);
|
||||
$this->sync($this->calendar());
|
||||
$this->assertNull($future->fresh()->deleted_at);
|
||||
$this->assertSame('pending', $future->reminders()->first()->status);
|
||||
$this->assertSame('skipped', $event->reminders()->first()->status);
|
||||
}
|
||||
|
||||
public function test_monthly_count_until_and_additional_dates_are_bounded(): void
|
||||
{
|
||||
$parser = new ICalendarParser;
|
||||
$from = Carbon::parse('2026-10-01', 'UTC');
|
||||
$to = Carbon::parse('2027-02-01', 'UTC');
|
||||
$ical = str_replace('FREQ=WEEKLY;INTERVAL=2;BYDAY=MO;COUNT=4', 'FREQ=MONTHLY;BYDAY=1MO;COUNT=3', $this->calendar());
|
||||
$events = $parser->parse($ical, $from, $to);
|
||||
$this->assertSame(['2026-10-05 09:00:00', '2026-11-02 10:00:00', '2026-12-07 10:00:00'], array_map(fn ($e) => $e->startsAt->format('Y-m-d H:i:s'), $events));
|
||||
$ical = str_replace('COUNT=3', 'UNTIL=20261102T100000Z', $ical);
|
||||
$this->assertCount(2, $parser->parse($ical, $from, $to));
|
||||
$combined = str_replace('BEGIN:VALARM', "RDATE;TZID=Europe/Paris:20261019T110000,20261102T110000\nBEGIN:VALARM", $ical);
|
||||
$this->assertCount(3, $parser->parse($combined, $from, $to));
|
||||
$ical = str_replace('RRULE:FREQ=MONTHLY;BYDAY=1MO;UNTIL=20261102T100000Z', 'RDATE;TZID=Europe/Paris:20261019T110000,20261102T110000', $ical);
|
||||
$this->assertCount(3, $parser->parse($ical, $from, $to));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user