diff --git a/composer.json b/composer.json index e405fa4..ae43d0b 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,11 @@ "TeleBot\\App\\": "App/" } }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, "scripts": { "cli-init": [ "php -r \"if (!file_exists('.env')) @copy('.env.sample', '.env');\"", @@ -16,7 +21,8 @@ ], "post-install-cmd": [ "@cli-init" - ] + ], + "test": "phpunit" }, "authors": [ { @@ -29,5 +35,8 @@ "ext-mbstring": "*", "predis/predis": "^2.2", "ext-pdo": "*" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9154006 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,12 @@ + + + + + tests/Unit + + + diff --git a/tests/Unit/System/Core/CommandTest.php b/tests/Unit/System/Core/CommandTest.php new file mode 100644 index 0000000..b46107e --- /dev/null +++ b/tests/Unit/System/Core/CommandTest.php @@ -0,0 +1,46 @@ + [ + 'required' => true, + 'validation' => [ + 'type' => 'regex', + 'pattern' => '/^\\d+$/', + ], + ], + 'limit' => [ + 'required' => false, + 'type' => 'number', + ], + ]; + + public function handle(...$args): void + { + } + }; + + $help = $command->getHelpText(); + + $this->assertStringContainsString('Command: demo', $help); + $this->assertStringContainsString('Description: Demo command', $help); + $this->assertStringContainsString('user', $help); + $this->assertStringContainsString('required', $help); + $this->assertStringContainsString('pattern: /^\\d+$/', $help); + $this->assertStringContainsString('limit', $help); + $this->assertStringContainsString('optional', $help); + } +} diff --git a/tests/Unit/System/Core/CoreRequestedFilesTest.php b/tests/Unit/System/Core/CoreRequestedFilesTest.php new file mode 100644 index 0000000..f6af37e --- /dev/null +++ b/tests/Unit/System/Core/CoreRequestedFilesTest.php @@ -0,0 +1,143 @@ +assertIsArray(Bootstrap::$config); + $this->assertArrayHasKey('routes', Bootstrap::$config); + } + + public function testDatabaseThrowsWhenRequiredEnvMissing(): void + { + putenv('DATABASE_NAME='); + putenv('DATABASE_USER='); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Database name not defined'); + new Database(); + } + + public function testDotenvLoadsKeyValuePairsFromCustomFile(): void + { + $tmp = sys_get_temp_dir() . '/dotenv-test-' . uniqid() . '.env'; + file_put_contents($tmp, "UNIT_DOTENV_KEY=loaded\n"); + + $prop = new ReflectionProperty(Dotenv::class, 'envFilename'); + $prop->setAccessible(true); + $original = $prop->getValue(); + $prop->setValue($tmp); + + try { + Dotenv::load(); + $this->assertSame('loaded', getenv('UNIT_DOTENV_KEY')); + } finally { + $prop->setValue($original); + @unlink($tmp); + putenv('UNIT_DOTENV_KEY='); + } + } + + public function testFilesystemHelpersResolveFilesAndNamespaces(): void + { + $root = sys_get_temp_dir() . '/fs-test-' . uniqid(); + @mkdir($root . '/A', 0777, true); + file_put_contents($root . '/A/Foo.php', 'assertContains($root . '/A/Foo', $files); + + $namespaced = Filesystem::getNamespacedFiles($root, 'Tmp'); + $this->assertContains('\\Tmp\\' . $root . '\\A\\Foo', $namespaced); + + $handlerPath = Filesystem::getNamespacedFile('Foo::handle', $root); + $this->assertNotNull($handlerPath); + } + + public function testHelperLoaderLoadSupportsSingleAndList(): void + { + $file1 = sys_get_temp_dir() . '/helper-a-' . uniqid() . '.php'; + $file2 = sys_get_temp_dir() . '/helper-b-' . uniqid() . '.php'; + file_put_contents($file1, ' 1];'); + file_put_contents($file2, ' 2];'); + + try { + $single = HelperLoader::load($file1, false); + $multi = HelperLoader::load([$file1, $file2], false); + + $this->assertSame(['a' => 1], $single); + $this->assertCount(2, $multi); + } finally { + @unlink($file1); + @unlink($file2); + } + } + + public function testLoggerGetInstanceReturnsLogger(): void + { + $this->assertInstanceOf(Logger::class, Logger::getInstance()); + } + + public function testProcessRunExecutesCommand(): void + { + $this->assertSame('core-test', Process::run('printf', 'core-test')); + } + + public function testQueueAndHandlerAndRouterAndRuntimeAndContainerBasics(): void + { + $this->assertTrue(class_exists(\TeleBot\System\Core\Queue::class)); + + $handler = new class { + public array $calls = []; + public function hello(string $name): void { $this->calls[] = $name; } + }; + Handler::assign($handler, 'hello', ['world']); + Handler::run(); + $this->assertSame(['world'], $handler->calls); + + $routerRef = new ReflectionClass(Router::class); + $method = $routerRef->getMethod('getFullRoute'); + $method->setAccessible(true); + $router = new Router(); + $this->assertSame('GET /v1/users', $method->invoke($router, '/users', '/v1', 'GET')); + + Runtime::setType(RuntimeType::REQUEST); + $this->assertSame(RuntimeType::REQUEST, Runtime::getType()); + $this->assertTrue(Runtime::is(RuntimeType::REQUEST)); + $this->assertInstanceOf(Runtime::class, Runtime::getInstance()); + + $container = new ServiceContainer(); + $container['svc'] = \ArrayObject::class; + $this->assertTrue(isset($container['svc'])); + $this->assertInstanceOf(\ArrayObject::class, $container['svc']); + unset($container['svc']); + $this->assertFalse(isset($container['svc'])); + } +} diff --git a/tests/Unit/System/Core/CoreSurfaceSmokeTest.php b/tests/Unit/System/Core/CoreSurfaceSmokeTest.php new file mode 100644 index 0000000..3763216 --- /dev/null +++ b/tests/Unit/System/Core/CoreSurfaceSmokeTest.php @@ -0,0 +1,110 @@ +collectCoreFiles(); + $this->assertNotEmpty($files); + + foreach ($files as $file) { + ['name' => $symbol, 'kind' => $kind] = $this->symbolFromFile($file); + + if ($kind === 'trait') { + $this->assertTrue(trait_exists($symbol), sprintf('Trait [%s] should be autoloadable.', $symbol)); + continue; + } + + if ($kind === 'enum') { + $this->assertTrue(enum_exists($symbol), sprintf('Enum [%s] should be autoloadable.', $symbol)); + continue; + } + + $this->assertTrue(class_exists($symbol), sprintf('Class [%s] should be autoloadable.', $symbol)); + } + } + + public function testConsoleCommandsExtendBaseCommand(): void + { + $commandFiles = glob(__DIR__ . '/../../../../System/Core/Console/Commands/*.php') ?: []; + $this->assertNotEmpty($commandFiles); + + foreach ($commandFiles as $file) { + ['name' => $class] = $this->symbolFromFile($file); + + $this->assertTrue( + is_subclass_of($class, \TeleBot\System\Core\Console\Command::class), + sprintf('Console command [%s] should extend base Command.', $class) + ); + } + } + + private function collectCoreFiles(): array + { + return array_merge( + glob(__DIR__ . '/../../../../System/Core/*.php') ?: [], + glob(__DIR__ . '/../../../../System/Core/Attributes/*.php') ?: [], + glob(__DIR__ . '/../../../../System/Core/Enums/*.php') ?: [], + glob(__DIR__ . '/../../../../System/Core/Traits/*.php') ?: [], + glob(__DIR__ . '/../../../../System/Core/Console/*.php') ?: [], + glob(__DIR__ . '/../../../../System/Core/Console/Commands/*.php') ?: [], + ); + } + + private function symbolFromFile(string $absolutePath): array + { + $content = file_get_contents($absolutePath); + $this->assertNotFalse($content, sprintf('Failed to read [%s].', $absolutePath)); + + $tokens = token_get_all($content); + $namespace = ''; + $symbol = null; + $kind = null; + + for ($i = 0, $count = count($tokens); $i < $count; $i++) { + $token = $tokens[$i]; + if (!is_array($token)) { + continue; + } + + if ($token[0] === T_NAMESPACE) { + $namespace = ''; + for ($j = $i + 1; $j < $count; $j++) { + $next = $tokens[$j]; + if ($next === ';') { + break; + } + + if (is_array($next) && in_array($next[0], [T_STRING, T_NAME_QUALIFIED, T_NS_SEPARATOR], true)) { + $namespace .= $next[1]; + } + } + } + + if (in_array($token[0], [T_CLASS, T_TRAIT, T_ENUM], true)) { + for ($j = $i + 1; $j < $count; $j++) { + $next = $tokens[$j]; + if (is_array($next) && $next[0] === T_STRING) { + $symbol = $next[1]; + $kind = strtolower(str_replace('T_', '', token_name($token[0]))); + break 2; + } + } + } + } + + $this->assertNotSame('', $namespace, sprintf('Namespace was not found in [%s].', $absolutePath)); + $this->assertNotNull($symbol, sprintf('Class/trait/enum name was not found in [%s].', $absolutePath)); + + return [ + 'name' => $namespace . '\\' . $symbol, + 'kind' => $kind, + ]; + } +} diff --git a/tests/Unit/System/Core/CoreTraitsTest.php b/tests/Unit/System/Core/CoreTraitsTest.php new file mode 100644 index 0000000..bb99b1a --- /dev/null +++ b/tests/Unit/System/Core/CoreTraitsTest.php @@ -0,0 +1,150 @@ +insertCalls[] = compact('table', 'data'); + } + } + + class VerifiableRequestStub { + public array $payload = []; + public string $uri = '/telegram'; + public string $ip = '127.0.0.1'; + public array $headers = []; + + public function json(): array { return $this->payload; } + public function uri(): string { return $this->uri; } + public function ip(): string { return $this->ip; } + public function header(string $key): ?string { return $this->headers[$key] ?? null; } + } + + class VerifiableResponseStub { + public ?int $status = null; + public function setStatusCode(int $code): self { $this->status = $code; return $this; } + public function end(): void {} + } + + $queueableDbStub = new QueueableDatabaseStub(); + $verifiableRequestStub = new VerifiableRequestStub(); + $verifiableResponseStub = new VerifiableResponseStub(); + + function database(): QueueableDatabaseStub + { + global $queueableDbStub; + return $queueableDbStub; + } + + function request(): VerifiableRequestStub + { + global $verifiableRequestStub; + return $verifiableRequestStub; + } + + function response(): VerifiableResponseStub + { + global $verifiableResponseStub; + return $verifiableResponseStub; + } +} + +namespace Tests\Unit\System\Core { + +use PHPUnit\Framework\TestCase; +use TeleBot\System\Core\Enums\LogType; +use TeleBot\System\Core\Traits\Expirable; +use TeleBot\System\Core\Traits\Loggable; +use TeleBot\System\Core\Traits\Queueable; +use TeleBot\System\Core\Traits\Verifiable; + +class CoreTraitsTest extends TestCase +{ + public function testExpirableTraitHasExpiredAndRestore(): void + { + $instance = new class { + use Expirable; + public function hasExpiredPublic(?array $data): bool { return $this->hasExpired($data); } + public function restorePublic(mixed $data): mixed { return $this->restore($data); } + }; + + $this->assertTrue($instance->hasExpiredPublic(['ttl' => time() - 1])); + $this->assertFalse($instance->hasExpiredPublic(['ttl' => time() + 600])); + $this->assertSame('value', $instance->restorePublic(['ttl' => time(), 'content' => 'value'])); + $this->assertSame('raw', $instance->restorePublic('raw')); + } + + public function testLoggableTraitWritesLogFile(): void + { + $dir = sys_get_temp_dir() . '/loggable-test-' . uniqid(); + @mkdir($dir, 0777, true); + + $logger = new class($dir) { + use Loggable; + public static string $dir; + public const LOG_DIR = ''; + public function __construct(string $dir) + { + self::$dir = $dir; + } + + protected static function writeToFile(LogType $logType, string $content): void + { + $filename = self::$dir . '/' . $logType->value . date('_Y_m_d') . '.log'; + file_put_contents($filename, $content . PHP_EOL, FILE_APPEND); + } + }; + + $logger::info('hello'); + $expected = $dir . '/info' . date('_Y_m_d') . '.log'; + + $this->assertFileExists($expected); + $this->assertStringContainsString('hello', (string)file_get_contents($expected)); + } + + public function testQueueableTraitDispatchUsesDatabaseHelper(): void + { + global $queueableDbStub; + $queueableDbStub->insertCalls = []; + + $job = new class { + use Queueable; + }; + + $job::dispatch(['id' => 1]); + + $this->assertCount(1, $queueableDbStub->insertCalls); + $this->assertSame('queue_jobs', $queueableDbStub->insertCalls[0]['table']); + } + + public function testVerifiableTraitVerifyUserIdHonorsWhiteAndBlackLists(): void + { + global $verifiableRequestStub; + + $verifiableRequestStub->payload = [ + 'update_id' => 1, + 'message' => ['from' => ['id' => '42']], + ]; + + $subject = new class { + use Verifiable; + public static array $config = [ + 'users' => ['whitelist' => ['42'], 'blacklist' => []], + 'routes' => [], + ]; + public function verifyUserIdPublic(): bool + { + return $this->verifyUserId(); + } + }; + + $this->assertTrue($subject->verifyUserIdPublic()); + + $subject::$config['users'] = ['whitelist' => [], 'blacklist' => ['42']]; + $this->assertFalse($subject->verifyUserIdPublic()); + } +} +} diff --git a/tests/Unit/System/Telegram/Coverage/TelegramSurfaceSmokeTest.php b/tests/Unit/System/Telegram/Coverage/TelegramSurfaceSmokeTest.php new file mode 100644 index 0000000..5ff4986 --- /dev/null +++ b/tests/Unit/System/Telegram/Coverage/TelegramSurfaceSmokeTest.php @@ -0,0 +1,140 @@ +assertNotEmpty($eventFiles); + + foreach ($eventFiles as $file) { + ['name' => $class] = $this->symbolFromFile($file); + + $this->assertTrue(class_exists($class), sprintf('Class [%s] should be autoloadable.', $class)); + $this->assertTrue( + is_subclass_of($class, IEvent::class), + sprintf('Class [%s] should implement [%s].', $class, IEvent::class) + ); + } + } + + public function testValidatorClassesImplementIValidator(): void + { + $validatorFiles = glob(__DIR__ . '/../../../../../System/Telegram/Validators/*.php') ?: []; + + $this->assertNotEmpty($validatorFiles); + + foreach ($validatorFiles as $file) { + ['name' => $class] = $this->symbolFromFile($file); + + $this->assertTrue(class_exists($class), sprintf('Class [%s] should be autoloadable.', $class)); + $this->assertTrue( + is_subclass_of($class, IValidator::class), + sprintf('Class [%s] should implement [%s].', $class, IValidator::class) + ); + } + } + + public function testTelegramTraitsAreLoadable(): void + { + $traitFiles = glob(__DIR__ . '/../../../../../System/Telegram/Traits/*.php') ?: []; + $methodTraitFiles = glob(__DIR__ . '/../../../../../System/Telegram/Traits/Methods/*.php') ?: []; + $allTraitFiles = array_merge($traitFiles, $methodTraitFiles); + + $this->assertNotEmpty($allTraitFiles); + + foreach ($allTraitFiles as $file) { + ['name' => $symbol, 'kind' => $kind] = $this->symbolFromFile($file); + + if ($kind === 'trait') { + $this->assertTrue(trait_exists($symbol), sprintf('Trait [%s] should be autoloadable.', $symbol)); + continue; + } + + $this->assertTrue( + class_exists($symbol) || enum_exists($symbol), + sprintf('Non-trait symbol [%s] should be autoloadable.', $symbol) + ); + } + } + + public function testTelegramTypeClassesAreLoadable(): void + { + $typeFiles = glob(__DIR__ . '/../../../../../System/Telegram/Types/*.php') ?: []; + + $this->assertNotEmpty($typeFiles); + + foreach ($typeFiles as $file) { + ['name' => $class] = $this->symbolFromFile($file); + + $this->assertTrue( + class_exists($class) || enum_exists($class), + sprintf('Class or enum [%s] should be autoloadable.', $class) + ); + } + } + + private function symbolFromFile(string $absolutePath): array + { + $content = file_get_contents($absolutePath); + $this->assertNotFalse($content, sprintf('Failed to read [%s].', $absolutePath)); + + $tokens = token_get_all($content); + $namespace = ''; + $symbol = null; + $kind = null; + + for ($i = 0, $count = count($tokens); $i < $count; $i++) { + $token = $tokens[$i]; + if (!is_array($token)) { + continue; + } + + if ($token[0] === T_NAMESPACE) { + $namespace = ''; + for ($j = $i + 1; $j < $count; $j++) { + $next = $tokens[$j]; + if ($next === ';') { + break; + } + + if (is_array($next) && in_array($next[0], [T_STRING, T_NAME_QUALIFIED, T_NS_SEPARATOR], true)) { + $namespace .= $next[1]; + } + } + } + + if (in_array($token[0], [T_CLASS, T_TRAIT, T_ENUM], true)) { + for ($j = $i + 1; $j < $count; $j++) { + $next = $tokens[$j]; + if (is_array($next) && $next[0] === T_STRING) { + $symbol = $next[1]; + $kind = token_name($token[0]); + break 2; + } + } + } + } + + $this->assertNotSame('', $namespace, sprintf('Namespace was not found in [%s].', $absolutePath)); + $this->assertNotNull($symbol, sprintf('Class/trait/enum name was not found in [%s].', $absolutePath)); + + return [ + 'name' => $namespace . '\\' . $symbol, + 'kind' => strtolower(str_replace('T_', '', (string)$kind)), + ]; + } +} diff --git a/tests/Unit/System/Telegram/Support/CanReplyWithTest.php b/tests/Unit/System/Telegram/Support/CanReplyWithTest.php new file mode 100644 index 0000000..5972177 --- /dev/null +++ b/tests/Unit/System/Telegram/Support/CanReplyWithTest.php @@ -0,0 +1,112 @@ +parseMode = $mode; + } + + public function replyTo(int $id, ?int $chatId = null): self + { + $this->replyTo = [$id, $chatId]; + + return $this; + } + + public function sendMessage(string $message): bool + { + $this->lastMessage = [$message]; + + return true; + } + + public function sendPhoto(string $photo, ?string $caption = null): bool + { + $this->lastPhoto = [$photo, $caption]; + + return true; + } + + public function sendVideo(string $video, ?string $caption = null): bool + { + $this->lastVideo = [$video, $caption]; + + return true; + } + + public function sendAudio(string $audio, ?string $caption = null): bool + { + $this->lastAudio = [$audio, $caption]; + + return true; + } +} + +function bot(): CanReplyWithBotStub +{ + static $instance = null; + + if ($instance === null) { + $instance = new CanReplyWithBotStub(); + } + + return $instance; +} + +namespace Tests\Unit\System\Telegram\Support; + +use PHPUnit\Framework\TestCase; +use TeleBot\System\Telegram\Enums\ParseMode; +use TeleBot\System\Telegram\Support\CanReplyWith; + +class CanReplyWithTest extends TestCase +{ + public function testWithModeDelegatesParseModeToBot(): void + { + $subject = new CanReplyWith(42, 101); + + $result = $subject->withMode(ParseMode::HTML); + + $this->assertSame($subject, $result); + $this->assertSame(ParseMode::HTML, \TeleBot\System\Telegram\Support\bot()->parseMode); + } + + public function testWithTextRepliesUsingMessageAndChatIds(): void + { + $subject = new CanReplyWith(15, 77); + + $this->assertTrue($subject->withText('hello world')); + + $this->assertSame([15, 77], \TeleBot\System\Telegram\Support\bot()->replyTo); + $this->assertSame(['hello world'], \TeleBot\System\Telegram\Support\bot()->lastMessage); + } + + public function testWithMediaMethodsForwardArguments(): void + { + $subject = new CanReplyWith(7, 14); + + $this->assertTrue($subject->withPhoto('photo.png', 'caption')); + $this->assertSame([7, 14], \TeleBot\System\Telegram\Support\bot()->replyTo); + $this->assertSame(['photo.png', 'caption'], \TeleBot\System\Telegram\Support\bot()->lastPhoto); + + $this->assertTrue($subject->withVideo('video.mp4')); + $this->assertSame(['video.mp4', null], \TeleBot\System\Telegram\Support\bot()->lastVideo); + + $this->assertTrue($subject->withAudio('audio.mp3', 'track')); + $this->assertSame(['audio.mp3', 'track'], \TeleBot\System\Telegram\Support\bot()->lastAudio); + } +} diff --git a/tests/Unit/System/Telegram/Support/EntityBuilderTest.php b/tests/Unit/System/Telegram/Support/EntityBuilderTest.php new file mode 100644 index 0000000..e60bb47 --- /dev/null +++ b/tests/Unit/System/Telegram/Support/EntityBuilderTest.php @@ -0,0 +1,66 @@ +mention(0, 4) + ->pre(5, 8, 'php') + ->command(0, 5) + ->toArray(); + + $this->assertSame([ + ['type' => 'mention', 'offset' => 0, 'length' => 4], + ['type' => 'pre', 'offset' => 5, 'length' => 8, 'language' => 'php'], + ['type' => 'bot_command', 'offset' => 0, 'length' => 5], + ], $result); + } + + public function testOptionalFieldsAreSkippedWhenNotProvided(): void + { + $builder = new EntityBuilder(); + + $result = $builder + ->textLink(0, 3) + ->customEmoji(4, 2) + ->toArray(); + + $this->assertSame([ + ['type' => 'text_link', 'offset' => 0, 'length' => 3], + ['type' => 'custom_emoji', 'offset' => 4, 'length' => 2], + ], $result); + } + + public function testTextMentionSerializesTheProvidedUser(): void + { + $userPayload = [ + 'id' => '1001', + 'first_name' => 'Unit', + 'is_bot' => false, + ]; + + $result = (new EntityBuilder()) + ->textMention(1, 6, new User($userPayload)) + ->toArray(); + + $this->assertSame([ + [ + 'type' => 'text_mention', + 'offset' => 1, + 'length' => 6, + 'user' => $userPayload, + ], + ], $result); + } +} diff --git a/tests/Unit/System/Telegram/Support/HydratorTest.php b/tests/Unit/System/Telegram/Support/HydratorTest.php new file mode 100644 index 0000000..67811ae --- /dev/null +++ b/tests/Unit/System/Telegram/Support/HydratorTest.php @@ -0,0 +1,99 @@ +value = $data['value'] ?? null; + } +} + +class FixtureSelfPayload +{ + public function __construct(public array $data) + { + } +} + +class FixtureHydratedTarget +{ + public ?string $seed = null; + + #[MapProp('name')] + public ?string $name = null; + + #[MapProp('child', FixtureChild::class)] + public ?FixtureChild $child = null; + + /** @var FixtureChild[] */ + #[MapProp('items', FixtureChild::class, true)] + public array $items = []; + + #[MapProp('status', FixtureStatus::class, false, false, true)] + public ?FixtureStatus $status = null; + + #[MapProp('timestamp', null, false, true)] + public ?DateTime $timestamp = null; + + #[MapProp('name:', FixtureSelfPayload::class)] + public ?FixtureSelfPayload $selfPayload = null; + + public function __construct(array $data) + { + $this->seed = $data['seed'] ?? null; + } +} + +class HydratorTest extends TestCase +{ + public function testHydrateMapsAttributesAndTypes(): void + { + $data = [ + 'seed' => 'constructor-value', + 'name' => 'Alice', + 'child' => ['value' => 'first-child'], + 'items' => [ + ['value' => 'one'], + ['value' => 'two'], + ], + 'status' => 'active', + 'timestamp' => 1_700_000_000, + ]; + + $result = Hydrator::hydrate(FixtureHydratedTarget::class, $data); + + $this->assertSame('constructor-value', $result->seed); + $this->assertSame('Alice', $result->name); + $this->assertInstanceOf(FixtureChild::class, $result->child); + $this->assertSame('first-child', $result->child?->value); + + $this->assertCount(2, $result->items); + $this->assertContainsOnlyInstancesOf(FixtureChild::class, $result->items); + $this->assertSame('one', $result->items[0]->value); + $this->assertSame('two', $result->items[1]->value); + + $this->assertSame(FixtureStatus::ACTIVE, $result->status); + $this->assertInstanceOf(DateTime::class, $result->timestamp); + $this->assertSame(1_700_000_000, $result->timestamp?->getTimestamp()); + + $this->assertInstanceOf(FixtureSelfPayload::class, $result->selfPayload); + $this->assertSame('Alice', $result->selfPayload?->data['name']); + } +} diff --git a/tests/Unit/System/Telegram/Support/MediaIteratorTest.php b/tests/Unit/System/Telegram/Support/MediaIteratorTest.php new file mode 100644 index 0000000..3d6c4b3 --- /dev/null +++ b/tests/Unit/System/Telegram/Support/MediaIteratorTest.php @@ -0,0 +1,66 @@ +variable = $variable; + if ($variable !== null) { + $this->{$variable} = $media; + } + } +} + +class MediaIteratorTest extends TestCase +{ + public function testEachReturnsWithoutVariableOrData(): void + { + $fixture = new MediaIteratorFixture(); + $called = false; + + $fixture->each(function () use (&$called): void { + $called = true; + }); + + $this->assertFalse($called); + } + + public function testEachPassesClonesByDefault(): void + { + $item = new stdClass(); + $item->name = 'origin'; + + $fixture = new MediaIteratorFixture('items', [$item]); + + $fixture->each(function (stdClass $media): void { + $media->name = 'changed'; + }); + + $this->assertSame('origin', $item->name); + } + + public function testEachCanPassByReference(): void + { + $item = new stdClass(); + $item->name = 'origin'; + + $fixture = new MediaIteratorFixture('items', [$item]); + + $fixture->each(function (stdClass $media): void { + $media->name = 'changed'; + }, PassBy::Reference); + + $this->assertSame('changed', $item->name); + } +} diff --git a/tests/Unit/System/Telegram/Support/ReplyMarkupBuilderTest.php b/tests/Unit/System/Telegram/Support/ReplyMarkupBuilderTest.php new file mode 100644 index 0000000..40579dd --- /dev/null +++ b/tests/Unit/System/Telegram/Support/ReplyMarkupBuilderTest.php @@ -0,0 +1,16 @@ +assertSame([], (new ReplyMarkupBuilder())->toArray()); + } +} diff --git a/tests/Unit/System/Telegram/Support/ReplyParametersBuilderTest.php b/tests/Unit/System/Telegram/Support/ReplyParametersBuilderTest.php new file mode 100644 index 0000000..2f3a3d6 --- /dev/null +++ b/tests/Unit/System/Telegram/Support/ReplyParametersBuilderTest.php @@ -0,0 +1,16 @@ +assertSame([], (new ReplyParametersBuilder())->toArray()); + } +} diff --git a/tests/Unit/System/Utils/HelpersTest.php b/tests/Unit/System/Utils/HelpersTest.php new file mode 100644 index 0000000..9310cec --- /dev/null +++ b/tests/Unit/System/Utils/HelpersTest.php @@ -0,0 +1,84 @@ + [ + 'name' => 'Alice', + 'profile' => ['age' => 30], + ], + ]; + + $this->assertSame('Alice', dot('user.name', $data)); + $this->assertSame(30, dot('user.profile.age', $data)); + $this->assertNull(dot('user.profile.country', $data)); + + $object = new class { + public object $user; + + public function __construct() + { + $this->user = new class { + public function getName(): string + { + return 'Bob'; + } + }; + } + }; + + $this->assertSame('Bob', dot('user.name', $object)); + } + + public function testEnvReadsDefaultAndBooleanValues(): void + { + putenv('UNIT_TEST_ENV_BOOL=true'); + putenv('UNIT_TEST_ENV_EMPTY='); + + $this->assertTrue(env('UNIT_TEST_ENV_BOOL')); + $this->assertSame('fallback', env('UNIT_TEST_ENV_EMPTY', 'fallback')); + $this->assertSame('fallback', env('UNIT_TEST_ENV_MISSING', 'fallback')); + } + + public function testFileIdAndUrlHelpers(): void + { + $this->assertTrue(is_file_id('abcDEF-123')); + $this->assertFalse(is_file_id('invalid/id')); + + $this->assertTrue(is_url('https://example.com/path')); + $this->assertFalse(is_url('not-a-url')); + } + + public function testGetBufferReturnsIdentifierForFileIdOrUrl(): void + { + $fileId = 'AABBCC-123'; + $url = 'https://example.com/image.jpg'; + + $this->assertSame($fileId, get_buffer($fileId)); + $this->assertSame($url, get_buffer($url)); + } + + public function testIso8601HelpersReturnTimestampsOrNull(): void + { + $seconds = iso8601_to_seconds('PT1M'); + $timestamp = iso8601_to_timestamp('PT1M'); + + $this->assertIsInt($seconds); + $this->assertIsInt($timestamp); + $this->assertGreaterThan(0, $seconds); + $this->assertGreaterThan(time(), $timestamp); + + $this->assertNull(iso8601_to_seconds('invalid')); + $this->assertNull(iso8601_to_timestamp('invalid')); + } +} diff --git a/tests/Unit/System/Utils/InstancesTest.php b/tests/Unit/System/Utils/InstancesTest.php new file mode 100644 index 0000000..5abae0e --- /dev/null +++ b/tests/Unit/System/Utils/InstancesTest.php @@ -0,0 +1,41 @@ +assertTrue(function_exists('config')); + $this->assertTrue(function_exists('router')); + $this->assertTrue(function_exists('request')); + $this->assertTrue(function_exists('response')); + $this->assertTrue(function_exists('bot')); + $this->assertTrue(function_exists('session')); + $this->assertTrue(function_exists('queue')); + $this->assertTrue(function_exists('database')); + $this->assertTrue(function_exists('cache')); + $this->assertTrue(function_exists('http')); + $this->assertTrue(function_exists('event')); + $this->assertTrue(function_exists('runtime')); + $this->assertTrue(function_exists('logger')); + $this->assertTrue(function_exists('services')); + $this->assertTrue(function_exists('throttle')); + } + + public function testHttpHelperCreatesConfiguredClient(): void + { + $client = http(['timeout' => 1.5]); + + $this->assertInstanceOf(Client::class, $client); + $this->assertSame(1.5, $client->getConfig('timeout')); + } +}