OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
147
tests/Unit/app/Core/Support/AvatarcreatorTest.php
Normal file
147
tests/Unit/app/Core/Support/AvatarcreatorTest.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\app\Core\Support;
|
||||
|
||||
use LasseRafn\InitialAvatarGenerator\InitialAvatar;
|
||||
use LasseRafn\Initials\Initials;
|
||||
use Leantime\Core\Support\Avatarcreator;
|
||||
use Leantime\Core\UI\Theme;
|
||||
use SVG\SVG;
|
||||
use Unit\TestCase;
|
||||
|
||||
class AvatarcreatorTest extends TestCase
|
||||
{
|
||||
private $avatarGenerator;
|
||||
|
||||
private $initials;
|
||||
|
||||
private $theme;
|
||||
|
||||
private $avatarCreator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->avatarGenerator = $this->createMock(InitialAvatar::class);
|
||||
$this->avatarGenerator->method('background')->willReturn($this->avatarGenerator);
|
||||
$this->avatarGenerator->method('font')->willReturn($this->avatarGenerator);
|
||||
$this->avatarGenerator->method('color')->willReturn($this->avatarGenerator);
|
||||
$this->avatarGenerator->method('generateSvg')->willReturn(SVG::fromString('<svg></svg>'));
|
||||
|
||||
$this->initials = $this->createMock(Initials::class);
|
||||
$this->theme = $this->createMock(Theme::class);
|
||||
|
||||
$this->avatarCreator = new Avatarcreator(
|
||||
$this->avatarGenerator,
|
||||
$this->initials,
|
||||
$this->theme
|
||||
);
|
||||
}
|
||||
|
||||
public function test_set_background_color()
|
||||
{
|
||||
$this->avatarGenerator->expects($this->once())
|
||||
->method('background')
|
||||
->with('#ffffff');
|
||||
|
||||
$this->avatarCreator->setBackground('#ffffff');
|
||||
}
|
||||
|
||||
public function test_set_file_prefix()
|
||||
{
|
||||
$this->avatarCreator->setFilePrefix('test-prefix');
|
||||
$this->assertEquals('test-prefix', $this->avatarCreator->getFilePrefix());
|
||||
}
|
||||
|
||||
public function test_set_initials_with_valid_name()
|
||||
{
|
||||
$this->initials->expects($this->once())
|
||||
->method('name')
|
||||
->with('john-doe');
|
||||
|
||||
$this->avatarGenerator->expects($this->once())
|
||||
->method('name')
|
||||
->with('john-doe');
|
||||
|
||||
$this->avatarCreator->setInitials('John Doe');
|
||||
}
|
||||
|
||||
public function test_set_initials_with_empty_name()
|
||||
{
|
||||
$this->initials->expects($this->once())
|
||||
->method('name')
|
||||
->with('👻');
|
||||
|
||||
$this->avatarCreator->setInitials('');
|
||||
}
|
||||
|
||||
public function test_get_initials()
|
||||
{
|
||||
$this->initials->expects($this->once())
|
||||
->method('getInitials')
|
||||
->willReturn('JD');
|
||||
|
||||
$this->assertEquals('JD', $this->avatarCreator->getInitials());
|
||||
}
|
||||
|
||||
public function test_get_avatar_with_cache_hit()
|
||||
{
|
||||
$this->initials->method('getInitials')->willReturn('JD');
|
||||
|
||||
// Create test file
|
||||
$cacheDir = storage_path('framework/cache/avatars');
|
||||
if (! is_dir($cacheDir)) {
|
||||
mkdir($cacheDir, 0777, true);
|
||||
}
|
||||
$testFile = $cacheDir.'/user-jd.svg';
|
||||
file_put_contents($testFile, '<svg>test</svg>');
|
||||
|
||||
$result = $this->avatarCreator->getAvatar('John Doe');
|
||||
|
||||
$this->assertEquals(SVG::fromString('<svg>test</svg>'), $result);
|
||||
unlink($testFile);
|
||||
}
|
||||
|
||||
public function test_get_avatar_with_cache_miss()
|
||||
{
|
||||
$this->initials->method('getInitials')->willReturn('JD');
|
||||
$this->avatarGenerator->method('generateSvg')
|
||||
->willReturn(SVG::fromString('<svg></svg>'));
|
||||
|
||||
$result = $this->avatarCreator->getAvatar('John Doe');
|
||||
|
||||
$cacheDir = storage_path('framework/cache/avatars');
|
||||
$testFile = $cacheDir.'/user-jd.svg';
|
||||
|
||||
$this->assertFileExists($testFile);
|
||||
}
|
||||
|
||||
public function test_get_avatar_with_special_characters()
|
||||
{
|
||||
$this->initials->method('getInitials')->willReturn('JD');
|
||||
$this->avatarGenerator->method('generateSvg')
|
||||
->willReturn(SVG::fromString('<svg></svg>'));
|
||||
|
||||
$result = $this->avatarCreator->getAvatar('John@Doe#$%');
|
||||
|
||||
$cacheDir = storage_path('framework/cache/avatars');
|
||||
$testFile = $cacheDir.'/user-jd.svg';
|
||||
|
||||
$this->assertFileExists($testFile);
|
||||
}
|
||||
|
||||
public function test_get_avatar_with_non_latin_characters()
|
||||
{
|
||||
$this->initials->method('getInitials')->willReturn('李王');
|
||||
$this->avatarGenerator->method('generateSvg')
|
||||
->willReturn(SVG::fromString('<svg></svg>'));
|
||||
|
||||
$result = $this->avatarCreator->getAvatar('李王');
|
||||
|
||||
$cacheDir = storage_path('framework/cache/avatars');
|
||||
$testFile = $cacheDir.'/user-李王.svg';
|
||||
|
||||
$this->assertFileExists($testFile);
|
||||
}
|
||||
}
|
||||
122
tests/Unit/app/Core/Support/CarbonMacrosTest.php
Normal file
122
tests/Unit/app/Core/Support/CarbonMacrosTest.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\App\Core\Support;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Leantime\Core\Language;
|
||||
use Leantime\Core\Support\CarbonMacros;
|
||||
use Unit\TestCase;
|
||||
|
||||
class CarbonMacrosTest extends TestCase
|
||||
{
|
||||
private CarbonMacros $carbonMacros;
|
||||
|
||||
private Language $languageMock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->languageMock = $this->createMock(Language::class);
|
||||
$this->languageMock->method('__')
|
||||
->willReturnCallback(function ($key) {
|
||||
return match ($key) {
|
||||
'language.dateformat' => 'm/d/Y',
|
||||
'language.timeformat' => 'h:i A',
|
||||
'language.dayNamesShort' => 'zo,ma,di,wo,do,vr,za',
|
||||
'language.dayNamesMin' => 'zo,ma,di,wo,do,vr,za',
|
||||
'language.monthNamesShort' => 'jan,feb,mrt,apr,mei,jun,jul,aug,sep,okt,nov,dec',
|
||||
default => $key
|
||||
};
|
||||
});
|
||||
|
||||
app()->instance(Language::class, $this->languageMock);
|
||||
|
||||
// Initialize with test values
|
||||
$this->carbonMacros = new CarbonMacros(
|
||||
userTimezone: 'America/Los_Angeles',
|
||||
userLanguage: 'en_US',
|
||||
userDateFormat: 'm/d/Y',
|
||||
userTimeFormat: 'h:i A',
|
||||
dbFormat: 'Y-m-d H:i:s',
|
||||
dbTimezone: 'UTC'
|
||||
);
|
||||
|
||||
// Mix in the macros to CarbonImmutable
|
||||
CarbonImmutable::mixin($this->carbonMacros);
|
||||
}
|
||||
|
||||
public function test_format_date_for_user(): void
|
||||
{
|
||||
$date = CarbonImmutable::create(2023, 12, 25, 14, 30, 0, 'UTC');
|
||||
$formatted = $date->formatDateForUser();
|
||||
|
||||
// Should be formatted according to user's timezone (PST) and format (m/d/Y)
|
||||
$this->assertEquals('12/25/2023', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_time_for_user(): void
|
||||
{
|
||||
$date = CarbonImmutable::create(2023, 12, 25, 14, 30, 0, 'UTC');
|
||||
$formatted = $date->formatTimeForUser();
|
||||
|
||||
// UTC 14:30 is 06:30 AM in PST
|
||||
$this->assertEquals('06:30 AM', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_24h_time_for_user(): void
|
||||
{
|
||||
$date = CarbonImmutable::create(2023, 12, 25, 14, 30, 0, 'UTC');
|
||||
$formatted = $date->format24HTimeForUser();
|
||||
|
||||
// UTC 14:30 is 06:30 in PST
|
||||
$this->assertEquals('06:30', $formatted);
|
||||
}
|
||||
|
||||
public function test_format_date_time_for_db(): void
|
||||
{
|
||||
// Create a date in user's timezone
|
||||
$date = CarbonImmutable::create(2023, 12, 25, 6, 30, 0, 'America/Los_Angeles');
|
||||
$formatted = $date->formatDateTimeForDb();
|
||||
|
||||
// Should be converted to UTC and formatted as Y-m-d H:i:s
|
||||
$this->assertEquals('2023-12-25 14:30:00', $formatted);
|
||||
}
|
||||
|
||||
public function test_set_to_user_timezone(): void
|
||||
{
|
||||
$date = CarbonImmutable::create(2023, 12, 25, 14, 30, 0, 'UTC');
|
||||
$converted = $date->setToUserTimezone();
|
||||
|
||||
$this->assertEquals('America/Los_Angeles', $converted->timezone->getName());
|
||||
$this->assertEquals('06:30', $converted->format('H:i'));
|
||||
}
|
||||
|
||||
public function test_set_to_db_timezone(): void
|
||||
{
|
||||
$date = CarbonImmutable::create(2023, 12, 25, 6, 30, 0, 'America/Los_Angeles');
|
||||
$converted = $date->setToDbTimezone();
|
||||
|
||||
$this->assertEquals('UTC', $converted->timezone->getName());
|
||||
$this->assertEquals('14:30', $converted->format('H:i'));
|
||||
}
|
||||
|
||||
public function test_dutch_language_support(): void
|
||||
{
|
||||
$macros = new CarbonMacros(
|
||||
userTimezone: 'Europe/Amsterdam',
|
||||
userLanguage: 'nl_NL',
|
||||
userDateFormat: 'd-m-Y',
|
||||
userTimeFormat: 'H:i',
|
||||
dbFormat: 'Y-m-d H:i:s',
|
||||
dbTimezone: 'UTC'
|
||||
);
|
||||
|
||||
CarbonImmutable::mixin($macros);
|
||||
|
||||
$date = CarbonImmutable::create(2023, 12, 25, 14, 30, 0, 'UTC');
|
||||
$formatted = $date->formatDateForUser();
|
||||
|
||||
$this->assertEquals('25-12-2023', $formatted);
|
||||
}
|
||||
}
|
||||
290
tests/Unit/app/Core/Support/DateTimeHelperTest.php
Normal file
290
tests/Unit/app/Core/Support/DateTimeHelperTest.php
Normal file
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\App\Core\Support;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Carbon\Exceptions\InvalidDateException;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
use Leantime\Core\Configuration\Environment;
|
||||
use Leantime\Core\Language;
|
||||
use Leantime\Core\Support\CarbonMacros;
|
||||
use Leantime\Core\Support\DateTimeHelper;
|
||||
use Unit\TestCase;
|
||||
|
||||
class DateTimeHelperTest extends TestCase
|
||||
{
|
||||
use \Codeception\Test\Feature\Stub;
|
||||
|
||||
private DateTimeHelper $dateTimeHelper;
|
||||
|
||||
private Environment $environmentMock;
|
||||
|
||||
private Language $languageMock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Mock the Environment class
|
||||
$this->environmentMock = $this->make(Environment::class, [
|
||||
'defaultTimezone' => 'UTC',
|
||||
'language' => 'en-US',
|
||||
]);
|
||||
app()->instance(Environment::class, $this->environmentMock);
|
||||
|
||||
$this->languageMock = $this->createMock(Language::class);
|
||||
$this->languageMock->method('__')->willReturnCallback(function ($index) {
|
||||
$map = [
|
||||
'language.dateformat' => 'm/d/Y',
|
||||
'language.timeformat' => 'h:i A',
|
||||
];
|
||||
|
||||
return $map[$index] ?? null;
|
||||
});
|
||||
app()->instance(\Leantime\Core\Language::class, $this->languageMock);
|
||||
|
||||
// Register mocks with the application container
|
||||
//
|
||||
// app()->instance(Language::class, $this->languageMock);
|
||||
|
||||
// America Los_Angeles is UTC - 8 so all db times need to come back from UTC - 8 hours
|
||||
CarbonImmutable::mixin(new CarbonMacros(
|
||||
'America/Los_Angeles',
|
||||
'en-US',
|
||||
'm/d/Y',
|
||||
'h:i A'
|
||||
));
|
||||
|
||||
// Create the DateTimeHelper instance
|
||||
$this->dateTimeHelper = new DateTimeHelper;
|
||||
}
|
||||
|
||||
public function test_parse_iso8601_with_timezone_offset_midnight(): void
|
||||
{
|
||||
// Test ISO 8601 with timezone offset (2025-04-16T00:00:00-04:00)
|
||||
$dateString = '2025-04-16T00:00:00-04:00';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('00', $parsedDate->format('H'));
|
||||
$this->assertEquals('00', $parsedDate->format('i'));
|
||||
$this->assertEquals('00', $parsedDate->format('s'));
|
||||
$this->assertEquals('-04:00', $parsedDate->format('P'));
|
||||
}
|
||||
|
||||
public function test_parse_iso8601_with_timezone_offset(): void
|
||||
{
|
||||
// Test ISO 8601 with timezone offset (2025-04-16T23:59:59-04:00)
|
||||
$dateString = '2025-04-16T23:59:59-04:00';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('23', $parsedDate->format('H'));
|
||||
$this->assertEquals('59', $parsedDate->format('i'));
|
||||
$this->assertEquals('59', $parsedDate->format('s'));
|
||||
$this->assertEquals('-04:00', $parsedDate->format('P'));
|
||||
}
|
||||
|
||||
public function test_parse_iso8601_with_timezone_offset_hhmm(): void
|
||||
{
|
||||
// Test ISO 8601 with timezone offset (2025-04-16T23:59:59-0400)
|
||||
$dateString = '2025-04-16T23:59:59-0400';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('23', $parsedDate->format('H'));
|
||||
$this->assertEquals('59', $parsedDate->format('i'));
|
||||
$this->assertEquals('59', $parsedDate->format('s'));
|
||||
$this->assertEquals('-04:00', $parsedDate->format('P'));
|
||||
}
|
||||
|
||||
public function test_parse_iso8601_with_timezone_offset_hh(): void
|
||||
{
|
||||
// Test ISO 8601 with timezone offset (2025-04-16T23:59:59-04)
|
||||
$dateString = '2025-04-16T23:59:59-04';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('23', $parsedDate->format('H'));
|
||||
$this->assertEquals('59', $parsedDate->format('i'));
|
||||
$this->assertEquals('59', $parsedDate->format('s'));
|
||||
$this->assertEquals('-04:00', $parsedDate->format('P'));
|
||||
}
|
||||
|
||||
public function test_parse_iso8601_with_zulu_time(): void
|
||||
{
|
||||
// Test ISO 8601 with Z/Zulu time (2025-04-16T23:59:59Z)
|
||||
$dateString = '2025-04-16T23:59:59Z';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('23', $parsedDate->format('H'));
|
||||
$this->assertEquals('59', $parsedDate->format('i'));
|
||||
$this->assertEquals('59', $parsedDate->format('s'));
|
||||
// Z time should be parsed as UTC
|
||||
$this->assertEquals('+00:00', $parsedDate->format('P'));
|
||||
}
|
||||
|
||||
public function test_parse_iso8601_without_timezone(): void
|
||||
{
|
||||
// Test ISO 8601 without timezone (2025-04-16T23:59:59)
|
||||
$dateString = '2025-04-16T23:59:59';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('23', $parsedDate->format('H'));
|
||||
$this->assertEquals('59', $parsedDate->format('i'));
|
||||
$this->assertEquals('59', $parsedDate->format('s'));
|
||||
}
|
||||
|
||||
public function test_parse_user_date_format(): void
|
||||
{
|
||||
// Test parsing date in user format (m/d/Y)
|
||||
$dateString = '04/16/2025';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
}
|
||||
|
||||
public function test_parse_user_date_and_time_format(): void
|
||||
{
|
||||
// Test parsing date and time in user format (m/d/Y h:i A)
|
||||
$dateString = '04/16/2025';
|
||||
$timeString = '11:59 PM';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString, $timeString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('23', $parsedDate->format('H'));
|
||||
$this->assertEquals('59', $parsedDate->format('i'));
|
||||
}
|
||||
|
||||
public function test_parse_user_date_with_start_of_day(): void
|
||||
{
|
||||
// Test parsing date with start of day
|
||||
$dateString = '04/16/2025';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString, 'start');
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('00', $parsedDate->format('H'));
|
||||
$this->assertEquals('00', $parsedDate->format('i'));
|
||||
$this->assertEquals('00', $parsedDate->format('s'));
|
||||
}
|
||||
|
||||
public function test_parse_user_date_with_end_of_day(): void
|
||||
{
|
||||
// Test parsing date with end of day
|
||||
$dateString = '04/16/2025';
|
||||
$parsedDate = $this->dateTimeHelper->parseUserDateTime($dateString, 'end');
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('23', $parsedDate->format('H'));
|
||||
$this->assertEquals('59', $parsedDate->format('i'));
|
||||
$this->assertEquals('59', $parsedDate->format('s'));
|
||||
}
|
||||
|
||||
public function test_invalid_date_string(): void
|
||||
{
|
||||
// Test with invalid date string
|
||||
$this->expectException(InvalidFormatException::class);
|
||||
$this->dateTimeHelper->parseUserDateTime('not-a-date');
|
||||
}
|
||||
|
||||
public function test_empty_date_string(): void
|
||||
{
|
||||
// Test with empty date string
|
||||
$this->expectException(InvalidDateException::class);
|
||||
$this->dateTimeHelper->parseUserDateTime('');
|
||||
}
|
||||
|
||||
public function test_parse_db_date_time(): void
|
||||
{
|
||||
// Test parsing DB date time
|
||||
$dbDate = '2025-04-16 23:59:59';
|
||||
$parsedDate = $this->dateTimeHelper->parseDbDateTime($dbDate);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedDate);
|
||||
$this->assertEquals('2025', $parsedDate->format('Y'));
|
||||
$this->assertEquals('04', $parsedDate->format('m'));
|
||||
$this->assertEquals('16', $parsedDate->format('d'));
|
||||
$this->assertEquals('23', $parsedDate->format('H'));
|
||||
$this->assertEquals('59', $parsedDate->format('i'));
|
||||
$this->assertEquals('59', $parsedDate->format('s'));
|
||||
}
|
||||
|
||||
public function test_parse_user_24h_time(): void
|
||||
{
|
||||
// Test parsing 24h time
|
||||
$timeString = '23:59';
|
||||
$parsedTime = $this->dateTimeHelper->parseUser24hTime($timeString);
|
||||
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $parsedTime);
|
||||
$this->assertEquals('23', $parsedTime->format('H'));
|
||||
$this->assertEquals('59', $parsedTime->format('i'));
|
||||
}
|
||||
|
||||
public function test_user_now(): void
|
||||
{
|
||||
// Test user now returns current time
|
||||
$now = $this->dateTimeHelper->userNow();
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $now);
|
||||
|
||||
// Should be within a few seconds of now
|
||||
$this->assertLessThan(5, abs(time() - $now->timestamp));
|
||||
}
|
||||
|
||||
public function test_db_now(): void
|
||||
{
|
||||
// Test db now returns current time in UTC
|
||||
$now = $this->dateTimeHelper->dbNow();
|
||||
$this->assertInstanceOf(CarbonImmutable::class, $now);
|
||||
|
||||
// Should be within a few seconds of now
|
||||
$this->assertLessThan(5, abs(time() - $now->timestamp));
|
||||
|
||||
// Should be in UTC timezone
|
||||
$this->assertEquals('UTC', $now->timezone->getName());
|
||||
}
|
||||
|
||||
public function test_is_valid_date_string(): void
|
||||
{
|
||||
// Test valid date strings
|
||||
$this->assertTrue($this->dateTimeHelper->isValidDateString('2025-04-16 23:59:59'));
|
||||
$this->assertTrue($this->dateTimeHelper->isValidDateString('2025-04-16T23:59:59-04:00'));
|
||||
|
||||
// Test invalid date strings
|
||||
$this->assertFalse($this->dateTimeHelper->isValidDateString(''));
|
||||
$this->assertFalse($this->dateTimeHelper->isValidDateString(null));
|
||||
$this->assertFalse($this->dateTimeHelper->isValidDateString('1969-12-31 00:00:00'));
|
||||
$this->assertFalse($this->dateTimeHelper->isValidDateString('0000-00-00 00:00:00'));
|
||||
}
|
||||
}
|
||||
73
tests/Unit/app/Core/Support/FormatTest.php
Normal file
73
tests/Unit/app/Core/Support/FormatTest.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Unit\app\Core\Support;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Leantime\Core\Support\CarbonMacros;
|
||||
use Leantime\Core\Support\Format;
|
||||
use Tests\DateTimeHelper;
|
||||
use Tests\Language;
|
||||
use Tests\MockObject;
|
||||
use Unit\TestCase;
|
||||
|
||||
class FormatTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DateTimeHelper|MockObject
|
||||
*/
|
||||
private $carbonMacrosMock;
|
||||
|
||||
/**
|
||||
* @var Language|MockObject
|
||||
*/
|
||||
private $languageMock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$this->languageMock = $this->createMock(\Leantime\Core\Language::class);
|
||||
app()->instance(\Leantime\Core\Support\CarbonMacros::class, $this->carbonMacrosMock);
|
||||
app()->instance(\Leantime\Core\Language::class, $this->languageMock);
|
||||
|
||||
// America Los_Angeles is UTC - 8 so all db times need to come back from UTC - 8 hours
|
||||
CarbonImmutable::mixin(new CarbonMacros(
|
||||
'America/Los_Angeles',
|
||||
'en-US',
|
||||
'm/d/Y',
|
||||
'h:i A'
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
public function test_date(): void
|
||||
{
|
||||
$formattedDateString = '12/31/2021';
|
||||
$dbDate = '2022-01-01 00:00:00';
|
||||
$format = new Format($dbDate, '');
|
||||
|
||||
$this->assertSame($formattedDateString, $format->date());
|
||||
}
|
||||
|
||||
public function test_time(): void
|
||||
{
|
||||
$formattedTimeString = '04:00 PM';
|
||||
$dbDate = '2022-01-01 00:00:00';
|
||||
$format = new Format($dbDate, '');
|
||||
|
||||
$this->assertSame($formattedTimeString, $format->time());
|
||||
}
|
||||
|
||||
public function test_time24(): void
|
||||
{
|
||||
$formattedTimeString = '16:00';
|
||||
$dbDate = '2022-01-01 00:00:00';
|
||||
$format = new Format($dbDate, '');
|
||||
|
||||
$this->assertSame($formattedTimeString, $format->time24());
|
||||
|
||||
}
|
||||
|
||||
// Similarly you can add tests for other 'Format' class methods.
|
||||
}
|
||||
61
tests/Unit/app/Core/Support/NameSanitizerTest.php
Normal file
61
tests/Unit/app/Core/Support/NameSanitizerTest.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Unit\app\Core\Support;
|
||||
|
||||
use Leantime\Core\Support\NameSanitizer;
|
||||
use Unit\TestCase;
|
||||
|
||||
/**
|
||||
* Regression tests for the invite-spam abuse fix: person names were stored and
|
||||
* emailed raw, letting spammers use the firstname field as an email payload.
|
||||
* The sanitizer must strip abuse vectors (contact numbers, URLs, emails, bidi
|
||||
* tricks) while letting legitimate names in any script through unchanged.
|
||||
*/
|
||||
class NameSanitizerTest extends TestCase
|
||||
{
|
||||
public function test_legitimate_names_pass_unchanged(): void
|
||||
{
|
||||
$this->assertSame('Marcel', NameSanitizer::clean('Marcel'));
|
||||
$this->assertSame('María José', NameSanitizer::clean('María José'));
|
||||
$this->assertSame('汪小明', NameSanitizer::clean('汪小明'));
|
||||
$this->assertSame('محمد علي', NameSanitizer::clean('محمد علي'));
|
||||
$this->assertSame("O'Connor-Smith", NameSanitizer::clean("O'Connor-Smith"));
|
||||
}
|
||||
|
||||
public function test_strips_contact_number_from_spam_payload(): void
|
||||
{
|
||||
// The actual payload from the 2026-07 abuse reports
|
||||
$this->assertStringNotContainsString('992600898', NameSanitizer::clean('+汪汪992600898-ن颂58嗏،Virtual'));
|
||||
}
|
||||
|
||||
public function test_strips_html(): void
|
||||
{
|
||||
$this->assertSame('alert(1)', NameSanitizer::clean('<script>alert(1)</script>'));
|
||||
}
|
||||
|
||||
public function test_strips_urls_and_emails(): void
|
||||
{
|
||||
$this->assertSame('Buy cheap', NameSanitizer::clean('Buy http://spam.example.com cheap'));
|
||||
$this->assertSame('Visit now', NameSanitizer::clean('Visit www.spam.example now'));
|
||||
$this->assertSame('mail me', NameSanitizer::clean('mail spam@evil.example me'));
|
||||
}
|
||||
|
||||
public function test_strips_control_and_bidi_characters(): void
|
||||
{
|
||||
$this->assertSame('JohnSmith', NameSanitizer::clean("John\u{202E}Smith"));
|
||||
$this->assertSame('AB', NameSanitizer::clean("A\u{200B}\u{0000}B"));
|
||||
}
|
||||
|
||||
public function test_caps_length_and_handles_non_strings(): void
|
||||
{
|
||||
$this->assertSame(50, mb_strlen(NameSanitizer::clean(str_repeat('A', 200))));
|
||||
$this->assertSame('', NameSanitizer::clean(null));
|
||||
$this->assertSame('', NameSanitizer::clean(12345));
|
||||
$this->assertSame('', NameSanitizer::clean(['array']));
|
||||
}
|
||||
|
||||
public function test_collapses_whitespace(): void
|
||||
{
|
||||
$this->assertSame('John Smith', NameSanitizer::clean(" John Smith \n"));
|
||||
}
|
||||
}
|
||||
84
tests/Unit/app/Core/Support/OutboundUrlGuardTest.php
Normal file
84
tests/Unit/app/Core/Support/OutboundUrlGuardTest.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Unit\app\Core\Support;
|
||||
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Leantime\Core\Support\OutboundUrlGuard;
|
||||
use Unit\TestCase;
|
||||
|
||||
/**
|
||||
* Covers the SSRF guard's address classification and redirect re-validation using IP literals and
|
||||
* direct calls, so nothing here depends on live DNS or the network.
|
||||
*/
|
||||
class OutboundUrlGuardTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider ipProvider
|
||||
*/
|
||||
public function test_is_ip_allowed(string $ip, bool $expected): void
|
||||
{
|
||||
$this->assertSame($expected, OutboundUrlGuard::isIpAllowed($ip));
|
||||
}
|
||||
|
||||
public static function ipProvider(): array
|
||||
{
|
||||
return [
|
||||
'loopback v4' => ['127.0.0.1', false],
|
||||
'private 10/8' => ['10.1.2.3', false],
|
||||
'private 172.16/12' => ['172.16.5.5', false],
|
||||
'private 192.168/16' => ['192.168.1.1', false],
|
||||
'cgnat 100.64/10' => ['100.64.0.1', false],
|
||||
'link-local metadata' => ['169.254.169.254', false],
|
||||
'reserved 0.0.0.0/8' => ['0.0.0.0', false],
|
||||
'public v4 (google dns)' => ['8.8.8.8', true],
|
||||
'public v4 (cloudflare)' => ['1.1.1.1', true],
|
||||
'loopback v6' => ['::1', false],
|
||||
'public v6 (cloudflare)' => ['2606:4700:4700::1111', true],
|
||||
'ipv4-mapped loopback' => ['::ffff:127.0.0.1', false],
|
||||
'ipv4-mapped cgnat' => ['::ffff:100.64.0.1', false],
|
||||
'ipv4-mapped public' => ['::ffff:8.8.8.8', true],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider urlProvider
|
||||
*/
|
||||
public function test_is_allowed_url(string $url, bool $expected): void
|
||||
{
|
||||
$this->assertSame($expected, OutboundUrlGuard::isAllowedUrl($url));
|
||||
}
|
||||
|
||||
public static function urlProvider(): array
|
||||
{
|
||||
return [
|
||||
'loopback literal' => ['http://127.0.0.1/feed.ics', false],
|
||||
'cgnat literal' => ['http://100.64.0.1/', false],
|
||||
'metadata literal' => ['http://169.254.169.254/latest/meta-data/', false],
|
||||
'public literal' => ['https://8.8.8.8/', true],
|
||||
'non-http scheme' => ['ftp://8.8.8.8/', false],
|
||||
'file scheme' => ['file:///etc/passwd', false],
|
||||
'garbage' => ['not-a-url', false],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_redirect_options_block_disallowed_hop(): void
|
||||
{
|
||||
$onRedirect = OutboundUrlGuard::redirectOptions()['on_redirect'];
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
$onRedirect(new Request('GET', 'https://8.8.8.8/'), new Response(302), new Uri('http://169.254.169.254/'));
|
||||
}
|
||||
|
||||
public function test_redirect_options_allow_public_hop(): void
|
||||
{
|
||||
$onRedirect = OutboundUrlGuard::redirectOptions()['on_redirect'];
|
||||
|
||||
// A public → public redirect must not throw.
|
||||
$onRedirect(new Request('GET', 'https://8.8.8.8/'), new Response(302), new Uri('https://1.1.1.1/'));
|
||||
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user