Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@
"TeleBot\\App\\": "App/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"cli-init": [
"php -r \"if (!file_exists('.env')) @copy('.env.sample', '.env');\"",
"php cli update init"
],
"post-install-cmd": [
"@cli-init"
]
],
"test": "phpunit"
},
"authors": [
{
Expand All @@ -29,5 +35,8 @@
"ext-mbstring": "*",
"predis/predis": "^2.2",
"ext-pdo": "*"
},
"require-dev": {
"phpunit/phpunit": "^11.5"
}
}
12 changes: 12 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
46 changes: 46 additions & 0 deletions tests/Unit/System/Core/CommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace Tests\Unit\System\Core;

use PHPUnit\Framework\TestCase;
use TeleBot\System\Core\Console\Command;

class CommandTest extends TestCase
{
public function testGetHelpTextIncludesArgumentValidationMetadata(): void
{
$command = new class extends Command {
public string $command = 'demo';
public string $description = 'Demo command';
public array $arguments = [
'user' => [
'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);
}
}
143 changes: 143 additions & 0 deletions tests/Unit/System/Core/CoreRequestedFilesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

declare(strict_types=1);

namespace Tests\Unit\System\Core;

use Exception;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use TeleBot\System\Core\Bootstrap;
use TeleBot\System\Core\Database;
use TeleBot\System\Core\Dotenv;
use TeleBot\System\Core\Filesystem;
use TeleBot\System\Core\Handler;
use TeleBot\System\Core\HelperLoader;
use TeleBot\System\Core\Logger;
use TeleBot\System\Core\Process;
use TeleBot\System\Core\Router;
use TeleBot\System\Core\Runtime;
use TeleBot\System\Core\ServiceContainer;
use TeleBot\System\Core\Enums\RuntimeType;

require_once __DIR__ . '/../../../../System/Utils/Helpers.php';
require_once __DIR__ . '/../../../../System/Utils/Instances.php';

class CoreRequestedFilesTest extends TestCase
{
public function testBootstrapInitLoadsConfig(): void
{
Bootstrap::init();

$this->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', '<?php');

$files = Filesystem::getFiles($root);
$this->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, '<?php return ["a" => 1];');
file_put_contents($file2, '<?php return ["b" => 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']));
}
}
110 changes: 110 additions & 0 deletions tests/Unit/System/Core/CoreSurfaceSmokeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace Tests\Unit\System\Core;

use PHPUnit\Framework\TestCase;

class CoreSurfaceSmokeTest extends TestCase
{
public function testCoreSymbolsAreAutoloadable(): void
{
$files = $this->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,
];
}
}
Loading