toBeTrue(); }); it('returns false when the current date is outside the range', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); $from = CarbonImmutable::parse('2026-07-01T00:00:00Z'); $to = CarbonImmutable::parse('2026-07-31T23:59:59Z'); // WHEN $result = DatesHelper::isIn($from, $to); // THEN expect($result)->toBeFalse(); }); it('returns true when only "from" is provided and current date is after', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); $from = CarbonImmutable::parse('2026-06-01T00:00:00Z'); // WHEN $result = DatesHelper::isIn($from, null); // THEN expect($result)->toBeTrue(); }); it('returns false when only "from" is provided and current date is before', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); $from = CarbonImmutable::parse('2026-07-01T00:00:00Z'); // WHEN $result = DatesHelper::isIn($from, null); // THEN expect($result)->toBeFalse(); }); it('returns true when only "to" is provided and current date is before', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); $to = CarbonImmutable::parse('2026-06-30T23:59:59Z'); // WHEN $result = DatesHelper::isIn(null, $to); // THEN expect($result)->toBeTrue(); }); it('returns false when only "to" is provided and current date is after', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); $to = CarbonImmutable::parse('2026-05-31T23:59:59Z'); // WHEN $result = DatesHelper::isIn(null, $to); // THEN expect($result)->toBeFalse(); }); it('returns false when both boundaries are null', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); // WHEN $result = DatesHelper::isIn(null, null); // THEN expect($result)->toBeFalse(); }); }); describe('toRfc3339 & toNullableRfc3339', function (): void { it('formats an immutable datetime to UTC RFC3339 format', function (): void { // GIVEN $datetime = new \DateTimeImmutable('2026-06-15 14:00:00', new \DateTimeZone('+02:00')); // WHEN $result = DatesHelper::toRfc3339($datetime); // THEN expect($result)->toBe('2026-06-15T12:00:00+00:00'); }); it('formats a mutable datetime to UTC RFC3339 without mutating the original object', function (): void { // GIVEN $datetime = new \DateTime('2026-06-15 14:00:00', new \DateTimeZone('+02:00')); // WHEN $result = DatesHelper::toRfc3339($datetime); // THEN expect($result)->toBe('2026-06-15T12:00:00+00:00') ->and($datetime->getTimezone()->getName())->toBe('+02:00'); }); it('formats a string date directly to RFC3339', function (): void { // GIVEN $dateString = '2026-06-15'; // WHEN $result = DatesHelper::toRfc3339($dateString); // THEN expect($result)->toBe('2026-06-15T00:00:00+00:00'); }); it('returns null when passing null to toNullableRfc3339', function (): void { // GIVEN $datetime = null; // WHEN $result = DatesHelper::toNullableRfc3339($datetime); // THEN expect($result)->toBeNull(); }); it('formats a valid date through toNullableRfc3339', function (): void { // GIVEN $dateString = '2026-06-15'; // WHEN $result = DatesHelper::toNullableRfc3339($dateString); // THEN expect($result)->toBe('2026-06-15T00:00:00+00:00'); }); }); describe('parseDateTime', function (): void { it('parses french date format (Y-m-d) at midnight UTC', function (): void { // GIVEN $input = '2026-04-10'; // WHEN $result = DatesHelper::parseDateTime($input); // THEN expect($result->getTimezone()->getName())->toBe('UTC') ->and($result->format('Y-m-d H:i:s'))->toBe('2026-04-10 00:00:00'); }); it('parses inverted french date format (d-m-Y) at midnight UTC', function (): void { // GIVEN $input = '10-04-2026'; // WHEN $result = DatesHelper::parseDateTime($input); // THEN expect($result->getTimezone()->getName())->toBe('UTC') ->and($result->format('Y-m-d H:i:s'))->toBe('2026-04-10 00:00:00'); }); it('parses ISO8601 string ending with Z', function (): void { // GIVEN $input = '2026-04-10T15:30:00Z'; // WHEN $result = DatesHelper::parseDateTime($input); // THEN expect($result->getTimezone()->getName())->toBe('UTC') ->and($result->format('Y-m-d H:i:s'))->toBe('2026-04-10 15:30:00'); }); it('parses RFC3339 string with positive timezone offset converted to UTC', function (): void { // GIVEN $input = '2026-04-10T16:30:00+02:00'; // WHEN $result = DatesHelper::parseDateTime($input); // THEN expect($result->getTimezone()->getName())->toBe('UTC') ->and($result->format('Y-m-d H:i:s'))->toBe('2026-04-10 14:30:00'); }); it('parses RFC3339 string with negative timezone offset converted to UTC', function (): void { // GIVEN $input = '2026-04-10T10:30:00-04:00'; // WHEN $result = DatesHelper::parseDateTime($input); // THEN expect($result->getTimezone()->getName())->toBe('UTC') ->and($result->format('Y-m-d H:i:s'))->toBe('2026-04-10 14:30:00'); }); it('parses RFC3339 string with microseconds', function (): void { // GIVEN $input = '2026-04-10T14:30:00.123456Z'; // WHEN $result = DatesHelper::parseDateTime($input); // THEN expect($result->getTimezone()->getName())->toBe('UTC') ->and($result->format('Y-m-d H:i:s'))->toBe('2026-04-10 14:30:00'); }); it('falls back to native date parser when given standard english format', function (): void { // GIVEN $input = '2026/04/10 14:00:00'; // WHEN $result = DatesHelper::parseDateTime($input); // THEN expect($result->getTimezone()->getName())->toBe('UTC') ->and($result->format('Y-m-d H:i:s'))->toBe('2026-04-10 14:00:00'); }); it('throws an exception when parsing an invalid date string', function (): void { // GIVEN $invalidInput = 'not-a-valid-date'; // WHEN / THEN expect(fn () => DatesHelper::parseDateTime($invalidInput)) ->toThrow(InvalidDateFormatException::class); }); it('throws an exception when parsing an empty date string', function (): void { // GIVEN $emptyInput = ' '; // WHEN / THEN expect(fn () => DatesHelper::parseDateTime($emptyInput)) ->toThrow(InvalidDateFormatException::class); }); }); describe('now', function (): void { it('returns the current UTC time as CarbonImmutable', function (): void { // GIVEN CarbonImmutable::setTestNow('2026-06-15T12:00:00Z'); // WHEN $now = DatesHelper::now(); // THEN expect($now)->toBeInstanceOf(CarbonImmutable::class) ->and($now->getTimezone()->getName())->toBe('UTC') ->and($now->format(\DateTimeInterface::ATOM))->toBe('2026-06-15T12:00:00+00:00'); }); }); });