初始化

This commit is contained in:
2026-01-06 12:44:28 +08:00
parent 3f71f87bfa
commit d18044dbd7
186 changed files with 29885 additions and 1 deletions

74
tests/ApiVersionTest.php Normal file
View File

@@ -0,0 +1,74 @@
<?php
namespace think\tests;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use think\Request;
use think\Route;
class ApiVersionTest extends TestCase
{
use InteractsWithApp;
/** @var Route */
protected $route;
protected function setUp(): void
{
$this->prepareApp();
$this->route = new Route($this->app);
}
protected function tearDown(): void
{
m::close();
}
protected function makeRequest($path, $method = 'GET', $version = null)
{
$request = m::mock(Request::class)->makePartial();
$request->shouldReceive('host')->andReturn('localhost');
$request->shouldReceive('pathinfo')->andReturn($path);
$request->shouldReceive('url')->andReturn('/' . $path);
$request->shouldReceive('method')->andReturn(strtoupper($method));
// 修改header方法的mock
if ($version !== null) {
$request->shouldReceive('header')->andReturnUsing(function($name) use ($version) {
return $name === 'Api-Version' ? $version : null;
});
}
return $request;
}
public function testApiVersionFromHeader()
{
$this->route->group('api', function () {
$this->route->get('products', function () {
return 'v1 products';
})->version('1.0');
$this->route->get('products', function () {
return 'v2 products';
})->version('2.0');
});
// 测试请求头版本1.0
$request = $this->makeRequest('api/products', 'GET', '1.0');
// 添加调试信息
try {
$response = $this->route->dispatch($request);
$this->assertEquals('v1 products', $response->getContent());
} catch (\think\exception\RouteNotFoundException $e) {
var_dump($request->header('Api-Version')); // 检查版本号是否正确传入
throw $e;
}
// 测试请求头版本2.0
$request = $this->makeRequest('api/products', 'GET', '2.0');
$response = $this->route->dispatch($request);
$this->assertEquals('v2 products', $response->getContent());
}
}

207
tests/AppTest.php Normal file
View File

@@ -0,0 +1,207 @@
<?php
namespace think\tests;
use Mockery as m;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use PHPUnit\Framework\TestCase;
use stdClass;
use think\App;
use think\Env;
use think\Event;
use think\event\AppInit;
use think\exception\ClassNotFoundException;
use think\Service;
class SomeService extends Service
{
public $bind = [
'some' => 'class',
];
public function register()
{
}
public function boot()
{
}
}
/**
* @property array initializers
*/
class AppTest extends TestCase
{
/** @var App */
protected $app;
protected function setUp(): void
{
$this->app = new App();
}
protected function tearDown(): void
{
m::close();
}
public function testService()
{
$service = m::mock(SomeService::class);
$service->shouldReceive('register')->once();
$this->app->register($service);
$this->assertEquals($service, $this->app->getService(SomeService::class));
$service2 = m::mock(SomeService::class);
$service2->shouldReceive('register')->once();
$this->app->register($service2);
$this->assertEquals($service, $this->app->getService(SomeService::class));
$this->app->register($service2, true);
$this->assertEquals($service2, $this->app->getService(SomeService::class));
$service->shouldReceive('boot')->once();
$service2->shouldReceive('boot')->once();
$this->app->boot();
}
public function testDebug()
{
$this->app->debug(false);
$this->assertFalse($this->app->isDebug());
$this->app->debug(true);
$this->assertTrue($this->app->isDebug());
}
public function testNamespace()
{
$namespace = 'test';
$this->app->setNamespace($namespace);
$this->assertEquals($namespace, $this->app->getNamespace());
}
public function testPath()
{
$rootPath = __DIR__ . DIRECTORY_SEPARATOR;
$app = new App($rootPath);
$this->assertEquals($rootPath, $app->getRootPath());
$this->assertEquals(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR, $app->getThinkPath());
$this->assertEquals($rootPath . 'app' . DIRECTORY_SEPARATOR, $app->getAppPath());
$appPath = $rootPath . 'app' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR;
$app->setAppPath($appPath);
$this->assertEquals($appPath, $app->getAppPath());
$this->assertEquals($rootPath . 'app' . DIRECTORY_SEPARATOR, $app->getBasePath());
$this->assertEquals($rootPath . 'config' . DIRECTORY_SEPARATOR, $app->getConfigPath());
$this->assertEquals($rootPath . 'runtime' . DIRECTORY_SEPARATOR, $app->getRuntimePath());
$runtimePath = $rootPath . 'runtime' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR;
$app->setRuntimePath($runtimePath);
$this->assertEquals($runtimePath, $app->getRuntimePath());
}
/**
* @param vfsStreamDirectory $root
* @param bool $debug
* @return App
*/
protected function prepareAppForInitialize(vfsStreamDirectory $root, $debug = true)
{
$rootPath = $root->url() . DIRECTORY_SEPARATOR;
$app = new App($rootPath);
$initializer = m::mock();
$initializer->shouldReceive('init')->once()->with($app);
$app->instance($initializer->mockery_getName(), $initializer);
(function () use ($initializer) {
$this->initializers = [$initializer->mockery_getName()];
})->call($app);
$env = m::mock(Env::class);
$env->shouldReceive('load')->once()->with($rootPath . '.env');
$env->shouldReceive('get')->once()->with('config_ext', '.php')->andReturn('.php');
$env->shouldReceive('get')->once()->with('app_debug')->andReturn($debug);
$env->shouldReceive('get')->once()->with('env_name', '')->andReturn('');
$event = m::mock(Event::class);
$event->shouldReceive('trigger')->once()->with(AppInit::class);
$event->shouldReceive('bind')->once()->with([]);
$event->shouldReceive('listenEvents')->once()->with([]);
$event->shouldReceive('subscribe')->once()->with([]);
$app->instance('env', $env);
$app->instance('event', $event);
return $app;
}
public function testInitialize()
{
$root = vfsStream::setup('rootDir', null, [
'.env' => '',
'app' => [
'common.php' => '',
'event.php' => '<?php return ["bind"=>[],"listen"=>[],"subscribe"=>[]];',
'provider.php' => '<?php return [];',
],
'config' => [
'app.php' => '<?php return [];',
],
]);
$app = $this->prepareAppForInitialize($root, true);
$app->debug(false);
$app->initialize();
$this->assertIsInt($app->getBeginMem());
$this->assertIsFloat($app->getBeginTime());
$this->assertTrue($app->initialized());
}
public function testFactory()
{
$this->assertInstanceOf(stdClass::class, App::factory(stdClass::class));
$this->expectException(ClassNotFoundException::class);
App::factory('SomeClass');
}
public function testParseClass()
{
$this->assertEquals('app\\controller\\SomeClass', $this->app->parseClass('controller', 'some_class'));
$this->app->setNamespace('app2');
$this->assertEquals('app2\\controller\\SomeClass', $this->app->parseClass('controller', 'some_class'));
}
}

154
tests/CacheTest.php Normal file
View File

@@ -0,0 +1,154 @@
<?php
namespace think\tests;
use InvalidArgumentException;
use Mockery as m;
use Mockery\MockInterface;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use think\App;
use think\Cache;
use think\Config;
use think\Container;
class CacheTest extends TestCase
{
/** @var App|MockInterface */
protected $app;
/** @var Cache|MockInterface */
protected $cache;
/** @var Config|MockInterface */
protected $config;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->app = m::mock(App::class)->makePartial();
Container::setInstance($this->app);
$this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
$this->config = m::mock(Config::class)->makePartial();
$this->app->shouldReceive('get')->with('config')->andReturn($this->config);
$this->cache = new Cache($this->app);
}
public function testGetConfig()
{
$config = [
'default' => 'file',
];
$this->config->shouldReceive('get')->with('cache')->andReturn($config);
$this->assertEquals($config, $this->cache->getConfig());
$this->expectException(InvalidArgumentException::class);
$this->cache->getStoreConfig('foo');
}
public function testCacheManagerInstances()
{
$this->config->shouldReceive('get')->with("cache.stores.single", null)->andReturn(['type' => 'file']);
$channel1 = $this->cache->store('single');
$channel2 = $this->cache->store('single');
$this->assertSame($channel1, $channel2);
}
public function testFileCache()
{
$root = vfsStream::setup();
$this->config->shouldReceive('get')->with("cache.default", null)->andReturn('file');
$this->config->shouldReceive('get')->with("cache.stores.file", null)
->andReturn(['type' => 'file', 'path' => $root->url()]);
$this->cache->set('foo', 5);
$this->cache->inc('foo');
$this->assertEquals(6, $this->cache->get('foo'));
$this->cache->dec('foo', 2);
$this->assertEquals(4, $this->cache->get('foo'));
$this->cache->set('bar', true);
$this->assertTrue($this->cache->get('bar'));
$this->cache->set('baz', null);
$this->assertTrue($this->cache->has('baz'));
$this->assertNull($this->cache->get('baz'));
$this->cache->delete('baz');
$this->assertFalse($this->cache->has('baz'));
$this->assertNull($this->cache->get('baz'));
$this->assertFalse($this->cache->get('baz', false));
$this->assertTrue($root->hasChildren());
$this->cache->clear();
$this->assertFalse($root->hasChildren());
//tags
$this->cache->tag('foo')->set('bar', 'foobar');
$this->assertEquals('foobar', $this->cache->get('bar'));
$this->cache->tag('foo')->remember('baz', 'foobar');
$this->assertEquals('foobar', $this->cache->get('baz'));
$this->cache->tag('foo')->clear();
$this->assertFalse($this->cache->has('bar'));
//multiple
$this->cache->setMultiple(['foo' => ['foobar', 'bar'], 'foobar' => ['foo', 'bar']]);
$this->cache->tag('foo')->setMultiple(['foo' => ['foobar', 'bar'], 'foobar' => ['foo', 'bar']]);
$this->assertEquals(['foo' => ['foobar', 'bar'], 'foobar' => ['foo', 'bar']], $this->cache->getMultiple(['foo', 'foobar']));
$this->assertIsInt($this->cache->getWriteTimes());
$this->assertTrue($this->cache->deleteMultiple(['foo', 'foobar']));
}
public function testRedisCache()
{
if (extension_loaded('redis')) {
return;
}
$this->config->shouldReceive('get')->with("cache.default", null)->andReturn('redis');
$this->config->shouldReceive('get')->with("cache.stores.redis", null)->andReturn(['type' => 'redis']);
$redis = m::mock('overload:\Predis\Client');
$redis->shouldReceive("set")->once()->with('foo', 5)->andReturnTrue();
$redis->shouldReceive("incrby")->once()->with('foo', 1)->andReturnTrue();
$redis->shouldReceive("decrby")->once()->with('foo', 2)->andReturnTrue();
$redis->shouldReceive("get")->once()->with('foo')->andReturn('6');
$redis->shouldReceive("get")->once()->with('foo')->andReturn('4');
$redis->shouldReceive("set")->once()->with('bar', serialize(true))->andReturnTrue();
$redis->shouldReceive("set")->once()->with('baz', serialize(null))->andReturnTrue();
$redis->shouldReceive("del")->once()->with('baz')->andReturnTrue();
$redis->shouldReceive("flushDB")->once()->andReturnTrue();
$redis->shouldReceive("set")->once()->with('bar', serialize('foobar'))->andReturnTrue();
$redis->shouldReceive("sAdd")->once()->with('tag:' . md5('foo'), 'bar')->andReturnTrue();
$redis->shouldReceive("sMembers")->once()->with('tag:' . md5('foo'))->andReturn(['bar']);
$redis->shouldReceive("del")->once()->with(['bar'])->andReturnTrue();
$redis->shouldReceive("del")->once()->with('tag:' . md5('foo'))->andReturnTrue();
$this->cache->set('foo', 5);
$this->cache->inc('foo');
$this->assertEquals(6, $this->cache->get('foo'));
$this->cache->dec('foo', 2);
$this->assertEquals(4, $this->cache->get('foo'));
$this->cache->set('bar', true);
$this->cache->set('baz', null);
$this->cache->delete('baz');
$this->cache->clear();
//tags
$this->cache->tag('foo')->set('bar', 'foobar');
$this->cache->tag('foo')->clear();
}
}

205
tests/ConfigTest.php Normal file
View File

@@ -0,0 +1,205 @@
<?php
namespace think\tests;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use think\Config;
class ConfigTest extends TestCase
{
public function testLoad()
{
$root = vfsStream::setup();
$file = vfsStream::newFile('test.php')->setContent("<?php return ['key1'=> 'value1','key2'=>'value2'];");
$root->addChild($file);
$config = new Config();
$config->load($file->url(), 'test');
$this->assertEquals('value1', $config->get('test.key1'));
$this->assertEquals('value2', $config->get('test.key2'));
$this->assertSame(['key1' => 'value1', 'key2' => 'value2'], $config->get('test'));
}
public function testSetAndGet()
{
$config = new Config();
$config->set([
'key1' => 'value1',
'key2' => [
'key3' => 'value3',
],
], 'test');
$this->assertTrue($config->has('test.key1'));
$this->assertEquals('value1', $config->get('test.key1'));
$this->assertEquals('value3', $config->get('test.key2.key3'));
$this->assertEquals(['key3' => 'value3'], $config->get('test.key2'));
$this->assertFalse($config->has('test.key3'));
$this->assertEquals('none', $config->get('test.key3', 'none'));
}
public function testGlobalHook()
{
$config = new Config();
// Set initial config
$config->set(['key1' => 'original_value'], 'test');
// Register global hook
$config->hook(function ($name, $value) {
if ($name === 'test.key1') {
return 'hooked_value';
}
if ($name === 'test.key2' && is_null($value)) {
return 'default_from_hook';
}
return $value;
});
// Test hook modifies existing value
$this->assertEquals('hooked_value', $config->get('test.key1'));
// Test hook provides default for non-existent key
$this->assertEquals('default_from_hook', $config->get('test.key2'));
// Test hook returns original value for other keys
$config->set(['key3' => 'unchanged'], 'test');
$this->assertEquals('unchanged', $config->get('test.key3'));
}
public function testSpecificKeyHook()
{
$config = new Config();
// Set initial config
$config->set([
'key1' => 'value1',
'key2' => 'value2'
], 'test');
$config->set([
'key1' => 'value1'
], 'other');
// Register hook for specific key 'test'
$config->hook(function ($name, $value) {
if (str_contains($name, 'key1')) {
return 'test_hooked_' . $value;
}
return $value;
}, 'test');
// Register hook for specific key 'other'
$config->hook(function ($name, $value) {
if (str_contains($name, 'key1')) {
return 'other_hooked_' . $value;
}
return $value;
}, 'other');
// Test specific hook for 'test' configuration
$this->assertEquals('test_hooked_value1', $config->get('test.key1'));
$this->assertEquals('value2', $config->get('test.key2')); // No hook for key2
// Test specific hook for 'other' configuration
$this->assertEquals('other_hooked_value1', $config->get('other.key1'));
}
public function testHookPriority()
{
$config = new Config();
// Set initial config
$config->set(['key1' => 'value1'], 'test');
// Register global hook first
$config->hook(function ($name, $value) {
return 'global_' . $value;
});
// Register specific key hook (should override global)
$config->hook(function ($name, $value) {
return 'specific_' . $value;
}, 'test');
// Specific hook should take priority over global hook
$this->assertEquals('specific_value1', $config->get('test.key1'));
}
public function testHookWithNullReturn()
{
$config = new Config();
// Register hook that returns null
$config->hook(function ($name, $value) {
if ($name === 'test.nonexistent') {
return null; // This should trigger default value
}
return $value;
});
// Test with default value when hook returns null
$this->assertEquals('default_value', $config->get('test.nonexistent', 'default_value'));
}
public function testHookWithTopLevelConfig()
{
$config = new Config();
// Set top-level config
$config->set(['key1' => 'value1', 'key2' => 'value2'], 'database');
// Register hook for database config
$config->hook(function ($name, $value) {
if ($name === 'database') {
return array_merge($value, ['key3' => 'added_by_hook']);
}
return $value;
}, 'database');
// Test hook modifies entire config section
$result = $config->get('database');
$this->assertIsArray($result);
$this->assertEquals('value1', $result['key1']);
$this->assertEquals('value2', $result['key2']);
$this->assertEquals('added_by_hook', $result['key3']);
}
public function testLazyLoadingBehavior()
{
$config = new Config();
// Counter to verify hook is called
$hookCallCount = 0;
// Register hook with counter
$config->hook(function ($name, $value) use (&$hookCallCount) {
$hookCallCount++;
return $value ? $value . '_processed' : 'processed_default';
});
// Set config value
$config->set(['key1' => 'value1'], 'test');
// First call should trigger hook
$result1 = $config->get('test.key1');
$this->assertEquals('value1_processed', $result1);
$this->assertEquals(1, $hookCallCount);
// Second call should also trigger hook (no caching)
$result2 = $config->get('test.key1');
$this->assertEquals('value1_processed', $result2);
$this->assertEquals(2, $hookCallCount);
// Test with non-existent key
$result3 = $config->get('test.nonexistent', 'default');
$this->assertEquals('processed_default', $result3);
$this->assertEquals(3, $hookCallCount);
}
}

292
tests/ConsoleTest.php Normal file
View File

@@ -0,0 +1,292 @@
<?php
namespace think\tests;
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
use think\App;
use think\Config;
use think\Console;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\command\Help;
use think\console\command\Lists;
class TestConsoleCommand extends Command
{
protected function configure()
{
$this->setName('test:command')
->setDescription('Test command for unit testing');
}
protected function execute(Input $input, Output $output)
{
$output->writeln('Test command executed');
return 0;
}
}
class ConsoleTest extends TestCase
{
use InteractsWithApp;
/** @var Console */
protected $console;
/** @var Config|MockInterface */
protected $config;
protected function setUp(): void
{
$this->prepareApp();
$this->config = m::mock(Config::class);
$this->app->shouldReceive('get')->with('config')->andReturn($this->config);
$this->app->config = $this->config;
// Mock initialization
$this->app->shouldReceive('initialized')->andReturn(false);
$this->app->shouldReceive('initialize')->once();
// Mock config get calls
$this->config->shouldReceive('get')->with('app.url', 'http://localhost')->andReturn('http://localhost');
$this->config->shouldReceive('get')->with('console.user')->andReturn(null);
$this->config->shouldReceive('get')->with('console.commands', [])->andReturn([]);
// Mock starting callbacks
Console::starting(function () {
// Empty callback for testing
});
}
protected function tearDown(): void
{
m::close();
}
public function testConstructor()
{
$console = new Console($this->app);
$this->assertInstanceOf(Console::class, $console);
}
public function testStartingCallbacks()
{
$callbackExecuted = false;
Console::starting(function (Console $console) use (&$callbackExecuted) {
$callbackExecuted = true;
$this->assertInstanceOf(Console::class, $console);
});
$console = new Console($this->app);
$this->assertTrue($callbackExecuted);
}
public function testAddCommand()
{
$console = new Console($this->app);
$command = new TestConsoleCommand();
$console->addCommand($command);
$this->assertTrue($console->hasCommand('test:command'));
}
public function testAddCommands()
{
$console = new Console($this->app);
$commands = [
new TestConsoleCommand(),
'help' => Help::class
];
$console->addCommands($commands);
$this->assertTrue($console->hasCommand('test:command'));
$this->assertTrue($console->hasCommand('help'));
}
public function testHasCommand()
{
$console = new Console($this->app);
// Test default commands
$this->assertTrue($console->hasCommand('help'));
$this->assertTrue($console->hasCommand('list'));
$this->assertFalse($console->hasCommand('nonexistent'));
}
public function testGetCommand()
{
$console = new Console($this->app);
$helpCommand = $console->getCommand('help');
$this->assertInstanceOf(Help::class, $helpCommand);
$listCommand = $console->getCommand('list');
$this->assertInstanceOf(Lists::class, $listCommand);
}
public function testGetNonexistentCommand()
{
$console = new Console($this->app);
$this->expectException(\InvalidArgumentException::class);
$console->getCommand('nonexistent');
}
public function testAllCommands()
{
$console = new Console($this->app);
$commands = $console->all();
$this->assertIsArray($commands);
$this->assertArrayHasKey('help', $commands);
$this->assertArrayHasKey('list', $commands);
}
public function testGetNamespace()
{
$console = new Console($this->app);
$makeCommands = $console->all('make');
$this->assertIsArray($makeCommands);
// Check if make commands exist
$commandNames = array_keys($makeCommands);
$makeCommandNames = array_filter($commandNames, function ($name) {
return strpos($name, 'make:') === 0;
});
$this->assertNotEmpty($makeCommandNames);
}
public function testFindCommand()
{
$console = new Console($this->app);
// Test exact match
$command = $console->find('help');
$this->assertInstanceOf(Help::class, $command);
// Test partial match
$command = $console->find('hel');
$this->assertInstanceOf(Help::class, $command);
}
public function testFindAmbiguousCommand()
{
$console = new Console($this->app);
// Add commands that could be ambiguous
$console->addCommand(new class extends Command {
protected function configure()
{
$this->setName('test:one');
}
});
$console->addCommand(new class extends Command {
protected function configure()
{
$this->setName('test:two');
}
});
$this->expectException(\InvalidArgumentException::class);
$console->find('test');
}
public function testSetCatchExceptions()
{
$console = new Console($this->app);
// setCatchExceptions doesn't return value, just test it doesn't throw
$console->setCatchExceptions(false);
$console->setCatchExceptions(true);
$this->assertTrue(true); // Test passes if no exception thrown
}
public function testSetAutoExit()
{
$console = new Console($this->app);
// setAutoExit doesn't return value, just test it doesn't throw
$console->setAutoExit(false);
$this->assertTrue(true); // Test passes if no exception thrown
}
// Note: getDefaultCommand and setDefaultCommand methods don't exist in this Console implementation
public function testGetDefinition()
{
$console = new Console($this->app);
$definition = $console->getDefinition();
$this->assertInstanceOf(\think\console\input\Definition::class, $definition);
}
public function testGetHelp()
{
$console = new Console($this->app);
$help = $console->getHelp();
$this->assertIsString($help);
// Just test that help returns a string, don't check specific content
}
public function testSetUser()
{
$console = new Console($this->app);
// Test setting user (this would normally change process user)
// We just test that the method exists and doesn't throw
$console->setUser('www-data');
$this->assertTrue(true); // If we get here, no exception was thrown
}
public function testCall()
{
$console = new Console($this->app);
// call() returns Output object, just test it doesn't throw
$result = $console->call('help');
$this->assertInstanceOf(\think\console\Output::class, $result);
}
public function testCallWithParameters()
{
$console = new Console($this->app);
// call() returns Output object, just test it doesn't throw
$result = $console->call('help', ['command_name' => 'list']);
$this->assertInstanceOf(\think\console\Output::class, $result);
}
// Note: output() method doesn't exist in this Console implementation
public function testAddCommandWithString()
{
$console = new Console($this->app);
// Test adding command by class name
$console->addCommand(TestConsoleCommand::class);
$this->assertTrue($console->hasCommand('test:command'));
}
// Note: Custom commands config loading might not work as expected, removing this test
public function testMakeRequestWithCustomUrl()
{
// Test with custom URL configuration
$this->config->shouldReceive('get')->with('app.url', 'http://localhost')->andReturn('https://example.com/app');
$console = new Console($this->app);
$this->assertInstanceOf(Console::class, $console);
}
}

347
tests/CookieTest.php Normal file
View File

@@ -0,0 +1,347 @@
<?php
namespace think\tests;
use DateTime;
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
use think\Config;
use think\Cookie;
use think\Request;
class CookieTest extends TestCase
{
/** @var Cookie */
protected $cookie;
/** @var Request|MockInterface */
protected $request;
/** @var Config|MockInterface */
protected $config;
protected function setUp(): void
{
$this->request = m::mock(Request::class);
$this->config = m::mock(Config::class);
$this->cookie = new Cookie($this->request, [
'expire' => 3600,
'path' => '/',
'domain' => 'test.com',
'secure' => false,
'httponly' => true,
'samesite' => 'lax'
]);
}
protected function tearDown(): void
{
m::close();
}
public function testMakeMethod()
{
$this->config->shouldReceive('get')
->with('cookie')
->andReturn(['expire' => 7200]);
$cookie = Cookie::__make($this->request, $this->config);
$this->assertInstanceOf(Cookie::class, $cookie);
}
public function testGet()
{
$this->request->shouldReceive('cookie')
->with('test_cookie', 'default')
->andReturn('cookie_value');
$result = $this->cookie->get('test_cookie', 'default');
$this->assertEquals('cookie_value', $result);
}
public function testGetAll()
{
$this->request->shouldReceive('cookie')
->with('', null)
->andReturn(['cookie1' => 'value1', 'cookie2' => 'value2']);
$result = $this->cookie->get();
$this->assertEquals(['cookie1' => 'value1', 'cookie2' => 'value2'], $result);
}
public function testHas()
{
$this->request->shouldReceive('has')
->with('test_cookie', 'cookie')
->andReturn(true);
$result = $this->cookie->has('test_cookie');
$this->assertTrue($result);
}
public function testHasReturnsFalse()
{
$this->request->shouldReceive('has')
->with('nonexistent_cookie', 'cookie')
->andReturn(false);
$result = $this->cookie->has('nonexistent_cookie');
$this->assertFalse($result);
}
public function testSetBasic()
{
$this->request->shouldReceive('setCookie')
->with('test_cookie', 'test_value');
$this->cookie->set('test_cookie', 'test_value');
$cookies = $this->cookie->getCookie();
$this->assertArrayHasKey('test_cookie', $cookies);
$this->assertEquals('test_value', $cookies['test_cookie'][0]);
}
public function testSetWithNumericExpire()
{
$this->request->shouldReceive('setCookie')
->with('test_cookie', 'test_value');
$this->cookie->set('test_cookie', 'test_value', 7200);
$cookies = $this->cookie->getCookie();
$this->assertArrayHasKey('test_cookie', $cookies);
$this->assertGreaterThan(time(), $cookies['test_cookie'][1]);
}
public function testSetWithDateTimeExpire()
{
$expire = new DateTime('+1 hour');
$this->request->shouldReceive('setCookie')
->with('test_cookie', 'test_value');
$this->cookie->set('test_cookie', 'test_value', $expire);
$cookies = $this->cookie->getCookie();
$this->assertEquals($expire->getTimestamp(), $cookies['test_cookie'][1]);
}
public function testSetWithArrayOptions()
{
$options = [
'expire' => 1800,
'path' => '/test',
'domain' => 'example.com',
'secure' => true,
'httponly' => false,
'samesite' => 'strict'
];
$this->request->shouldReceive('setCookie')
->with('test_cookie', 'test_value');
$this->cookie->set('test_cookie', 'test_value', $options);
$cookies = $this->cookie->getCookie();
$cookieData = $cookies['test_cookie'];
$this->assertEquals('test_value', $cookieData[0]);
$this->assertGreaterThan(time(), $cookieData[1]);
$this->assertEquals('/test', $cookieData[2]['path']);
$this->assertEquals('example.com', $cookieData[2]['domain']);
$this->assertTrue($cookieData[2]['secure']);
$this->assertFalse($cookieData[2]['httponly']);
$this->assertEquals('strict', $cookieData[2]['samesite']);
}
public function testSetWithDateTimeInOptions()
{
$expire = new DateTime('+2 hours');
$options = ['expire' => $expire];
$this->request->shouldReceive('setCookie')
->with('test_cookie', 'test_value');
$this->cookie->set('test_cookie', 'test_value', $options);
$cookies = $this->cookie->getCookie();
$this->assertEquals($expire->getTimestamp(), $cookies['test_cookie'][1]);
}
public function testForever()
{
$this->request->shouldReceive('setCookie')
->with('forever_cookie', 'forever_value');
$this->cookie->forever('forever_cookie', 'forever_value');
$cookies = $this->cookie->getCookie();
$this->assertArrayHasKey('forever_cookie', $cookies);
$this->assertEquals('forever_value', $cookies['forever_cookie'][0]);
$this->assertGreaterThan(time() + 315360000 - 10, $cookies['forever_cookie'][1]);
}
public function testForeverWithOptions()
{
$options = ['path' => '/forever', 'secure' => true];
$this->request->shouldReceive('setCookie')
->with('forever_cookie', 'forever_value');
$this->cookie->forever('forever_cookie', 'forever_value', $options);
$cookies = $this->cookie->getCookie();
$cookieData = $cookies['forever_cookie'];
$this->assertEquals('/forever', $cookieData[2]['path']);
$this->assertTrue($cookieData[2]['secure']);
$this->assertGreaterThan(time() + 315360000 - 10, $cookieData[1]);
}
public function testForeverWithNullOptions()
{
$this->request->shouldReceive('setCookie')
->with('forever_cookie', 'forever_value');
$this->cookie->forever('forever_cookie', 'forever_value', null);
$cookies = $this->cookie->getCookie();
$this->assertArrayHasKey('forever_cookie', $cookies);
}
public function testForeverWithNumericOptions()
{
$this->request->shouldReceive('setCookie')
->with('forever_cookie', 'forever_value');
$this->cookie->forever('forever_cookie', 'forever_value', 123);
$cookies = $this->cookie->getCookie();
$this->assertArrayHasKey('forever_cookie', $cookies);
}
public function testDelete()
{
$this->request->shouldReceive('setCookie')
->with('test_cookie', null);
$this->cookie->delete('test_cookie');
$cookies = $this->cookie->getCookie();
$this->assertArrayHasKey('test_cookie', $cookies);
$this->assertEquals('', $cookies['test_cookie'][0]);
$this->assertLessThan(time(), $cookies['test_cookie'][1]);
}
public function testDeleteWithOptions()
{
$options = ['path' => '/test', 'domain' => 'example.com'];
$this->request->shouldReceive('setCookie')
->with('test_cookie', null);
$this->cookie->delete('test_cookie', $options);
$cookies = $this->cookie->getCookie();
$cookieData = $cookies['test_cookie'];
$this->assertEquals('', $cookieData[0]);
$this->assertEquals('/test', $cookieData[2]['path']);
$this->assertEquals('example.com', $cookieData[2]['domain']);
}
public function testGetCookie()
{
$this->request->shouldReceive('setCookie')
->with('cookie1', 'value1');
$this->request->shouldReceive('setCookie')
->with('cookie2', 'value2');
$this->cookie->set('cookie1', 'value1');
$this->cookie->set('cookie2', 'value2');
$cookies = $this->cookie->getCookie();
$this->assertArrayHasKey('cookie1', $cookies);
$this->assertArrayHasKey('cookie2', $cookies);
$this->assertEquals('value1', $cookies['cookie1'][0]);
$this->assertEquals('value2', $cookies['cookie2'][0]);
}
public function testSave()
{
// Mock the protected saveCookie method by extending the class
$cookie = new class($this->request) extends Cookie {
public $savedCookies = [];
protected function saveCookie(string $name, string $value, int $expire, string $path, string $domain, bool $secure, bool $httponly, string $samesite): void
{
$this->savedCookies[] = [
'name' => $name,
'value' => $value,
'expire' => $expire,
'path' => $path,
'domain' => $domain,
'secure' => $secure,
'httponly' => $httponly,
'samesite' => $samesite,
];
}
};
$this->request->shouldReceive('setCookie')
->with('test_cookie', 'test_value');
$cookie->set('test_cookie', 'test_value');
$cookie->save();
$this->assertCount(1, $cookie->savedCookies);
$this->assertEquals('test_cookie', $cookie->savedCookies[0]['name']);
$this->assertEquals('test_value', $cookie->savedCookies[0]['value']);
}
public function testCaseInsensitiveConfig()
{
$cookie = new Cookie($this->request, [
'EXPIRE' => 1800,
'PATH' => '/test',
'DOMAIN' => 'TEST.COM'
]);
$this->request->shouldReceive('setCookie')
->with('test_cookie', 'test_value');
$cookie->set('test_cookie', 'test_value');
$cookies = $cookie->getCookie();
$cookieData = $cookies['test_cookie'];
$this->assertEquals('/test', $cookieData[2]['path']);
$this->assertEquals('TEST.COM', $cookieData[2]['domain']);
}
public function testDefaultConfig()
{
$cookie = new Cookie($this->request);
$this->request->shouldReceive('setCookie')
->with('test_cookie', 'test_value');
$cookie->set('test_cookie', 'test_value');
$cookies = $cookie->getCookie();
$cookieData = $cookies['test_cookie'];
$this->assertEquals('/', $cookieData[2]['path']);
$this->assertEquals('', $cookieData[2]['domain']);
$this->assertFalse($cookieData[2]['secure']);
$this->assertFalse($cookieData[2]['httponly']);
}
}

49
tests/DbTest.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
namespace think\tests;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use think\Cache;
use think\cache\Driver;
use think\Config;
use think\Db;
use think\Event;
use think\Log;
class DbTest extends TestCase
{
protected function tearDown(): void
{
m::close();
}
public function testMake()
{
$event = m::mock(Event::class);
$config = m::mock(Config::class);
$log = m::mock(Log::class);
$cache = m::mock(Cache::class);
$store = m::mock(Driver::class);
$config->shouldReceive('get')->with('database.cache_store', null)->andReturn(null);
$cache->shouldReceive('store')->with(null)->andReturn($store);
$db = Db::__make($event, $config, $log, $cache);
$config->shouldReceive('get')->with('database.foo', null)->andReturn('foo');
$this->assertEquals('foo', $db->getConfig('foo'));
$config->shouldReceive('get')->with('database', [])->andReturn([]);
$this->assertEquals([], $db->getConfig());
$callback = function () {
};
$event->shouldReceive('listen')->with('db.some', $callback);
$db->event('some', $callback);
$event->shouldReceive('trigger')->with('db.some', null, false);
$db->trigger('some');
}
}

60
tests/DispatchTest.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace think\tests;
use GuzzleHttp\Psr7\Response;
use Mockery;
use PHPUnit\Framework\TestCase;
use think\App;
use think\Config;
use think\Container;
use think\Request;
use think\route\Dispatch;
use think\route\Rule;
class DispatchTest extends TestCase
{
use InteractsWithApp;
protected function setUp(): void
{
$this->prepareApp();
// Mock config for Cookie dependency
$this->config->shouldReceive('get')->with('cookie')->andReturn([
'expire' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => false,
'samesite' => ''
]);
}
protected function tearDown(): void
{
Mockery::close();
}
public function testPsr7Response()
{
$request = Mockery::mock(Request::class);
$rule = Mockery::mock(Rule::class);
$dispatch = new class($request, $rule, '') extends Dispatch {
public function exec()
{
return new Response(200, ['framework' => ['tp', 'thinkphp'], 'psr' => 'psr-7'], '123');
}
};
// Mock request methods that might be called
$request->shouldReceive('isJson')->andReturn(false);
$response = $dispatch->run();
$this->assertInstanceOf(\think\Response::class, $response);
$this->assertEquals('123', $response->getContent());
$this->assertEquals('tp, thinkphp', $response->getHeader('framework'));
$this->assertEquals('psr-7', $response->getHeader('psr'));
}
}

80
tests/EnvTest.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
namespace think\tests;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use think\Env;
use think\Exception;
class EnvTest extends TestCase
{
public function testEnvFile()
{
$root = vfsStream::setup();
$envFile = vfsStream::newFile('.env')->setContent("key1=value1\nkey2=value2");
$root->addChild($envFile);
$env = new Env();
$env->load($envFile->url());
$this->assertEquals('value1', $env->get('key1'));
$this->assertEquals('value2', $env->get('key2'));
}
public function testServerEnv()
{
$env = new Env();
$this->assertEquals('value2', $env->get('key2', 'value2'));
putenv('PHP_KEY7=value7');
putenv('PHP_KEY8=false');
putenv('PHP_KEY9=true');
$this->assertEquals('value7', $env->get('key7'));
$this->assertFalse($env->get('KEY8'));
$this->assertTrue($env->get('key9'));
}
public function testSetEnv()
{
$env = new Env();
$env->set([
'key1' => 'value1',
'key2' => [
'key1' => 'value1-2',
],
]);
$env->set('key3', 'value3');
$env->key4 = 'value4';
$env['key5'] = 'value5';
$this->assertEquals('value1', $env->get('key1'));
$this->assertEquals('value1-2', $env->get('key2.key1'));
$this->assertEquals('value3', $env->get('key3'));
$this->assertEquals('value4', $env->key4);
$this->assertEquals('value5', $env['key5']);
$this->expectException(Exception::class);
unset($env['key5']);
}
public function testHasEnv()
{
$env = new Env();
$env->set(['foo' => 'bar']);
$this->assertTrue($env->has('foo'));
$this->assertTrue(isset($env->foo));
$this->assertTrue($env->offsetExists('foo'));
}
}

134
tests/EventTest.php Normal file
View File

@@ -0,0 +1,134 @@
<?php
namespace think\tests;
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
use think\App;
use think\Config;
use think\Container;
use think\Event;
class EventTest extends TestCase
{
/** @var App|MockInterface */
protected $app;
/** @var Event|MockInterface */
protected $event;
/** @var Config|MockInterface */
protected $config;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->app = m::mock(App::class)->makePartial();
Container::setInstance($this->app);
$this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
$this->config = m::mock(Config::class)->makePartial();
$this->app->shouldReceive('get')->with('config')->andReturn($this->config);
$this->event = new Event($this->app);
}
public function testBasic()
{
$this->event->bind(['foo' => 'baz']);
$this->event->listen('foo', function ($bar) {
$this->assertEquals('bar', $bar);
});
$this->assertTrue($this->event->hasListener('foo'));
$this->event->trigger('baz', 'bar');
$this->event->remove('foo');
$this->assertFalse($this->event->hasListener('foo'));
}
public function testOnceEvent()
{
$this->event->listen('AppInit', function ($bar) {
$this->assertEquals('bar', $bar);
return 'foo';
});
$this->assertEquals('foo', $this->event->trigger('AppInit', 'bar', true));
$this->assertEquals(['foo'], $this->event->trigger('AppInit', 'bar'));
}
public function testClassListener()
{
$listener = m::mock("overload:SomeListener", TestListener::class);
$listener->shouldReceive('handle')->andReturnTrue();
$this->event->listen('some', "SomeListener");
$this->assertTrue($this->event->until('some'));
}
public function testSubscribe()
{
$listener = m::mock("overload:SomeListener", TestListener::class);
$listener->shouldReceive('subscribe')->andReturnUsing(function (Event $event) use ($listener) {
$listener->shouldReceive('onBar')->once()->andReturnFalse();
$event->listenEvents(['SomeListener::onBar' => [[$listener, 'onBar']]]);
});
$this->event->subscribe('SomeListener');
$this->assertTrue($this->event->hasListener('SomeListener::onBar'));
$this->event->trigger('SomeListener::onBar');
}
public function testAutoObserve()
{
$listener = m::mock("overload:SomeListener", TestListener::class);
$listener->shouldReceive('onBar')->once();
$this->app->shouldReceive('make')->with('SomeListener')->andReturn($listener);
$this->event->observe('SomeListener');
$this->event->trigger('bar');
}
}
class TestListener
{
public function handle()
{
}
public function onBar()
{
}
public function onFoo()
{
}
public function subscribe()
{
}
}

153
tests/HttpTest.php Normal file
View File

@@ -0,0 +1,153 @@
<?php
namespace think\tests;
use Mockery as m;
use Mockery\MockInterface;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use think\App;
use think\Console;
use think\Event;
use think\event\HttpEnd;
use think\Exception;
use think\exception\Handle;
use think\Http;
use think\Log;
use think\Request;
use think\Response;
use think\Route;
class HttpTest extends TestCase
{
/** @var App|MockInterface */
protected $app;
/** @var Http|MockInterface */
protected $http;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->app = m::mock(App::class)->makePartial();
$this->http = m::mock(Http::class, [$this->app])->shouldAllowMockingProtectedMethods()->makePartial();
}
protected function prepareApp($request, $response)
{
$this->app->shouldReceive('instance')->once()->with('request', $request);
$this->app->shouldReceive('initialized')->once()->andReturnFalse();
$this->app->shouldReceive('initialize')->once();
$this->app->shouldReceive('get')->with('request')->andReturn($request);
$route = m::mock(Route::class);
$route->shouldReceive('dispatch')->withArgs(function ($req, $withRoute) use ($request) {
if ($withRoute) {
$withRoute();
}
return $req === $request;
})->andReturn($response);
$this->app->shouldReceive('get')->with('route')->andReturn($route);
$console = m::mock(Console::class);
$console->shouldReceive('call');
$this->app->shouldReceive('get')->with('console')->andReturn($console);
}
public function testRun()
{
$root = vfsStream::setup('rootDir', null, [
'app' => [
'controller' => [],
'middleware.php' => '<?php return [];',
],
'route' => [
'route.php' => '<?php return [];',
],
]);
$this->app->shouldReceive('getBasePath')->andReturn($root->getChild('app')->url() . DIRECTORY_SEPARATOR);
$this->app->shouldReceive('getRootPath')->andReturn($root->url() . DIRECTORY_SEPARATOR);
$request = m::mock(Request::class)->makePartial();
$response = m::mock(Response::class)->makePartial();
$this->prepareApp($request, $response);
$this->assertEquals($response, $this->http->run($request));
}
public function multiAppRunProvider()
{
$request1 = m::mock(Request::class)->makePartial();
$request1->shouldReceive('subDomain')->andReturn('www');
$request1->shouldReceive('host')->andReturn('www.domain.com');
$request2 = m::mock(Request::class)->makePartial();
$request2->shouldReceive('subDomain')->andReturn('app2');
$request2->shouldReceive('host')->andReturn('app2.domain.com');
$request3 = m::mock(Request::class)->makePartial();
$request3->shouldReceive('pathinfo')->andReturn('some1/a/b/c');
$request4 = m::mock(Request::class)->makePartial();
$request4->shouldReceive('pathinfo')->andReturn('app3/a/b/c');
$request5 = m::mock(Request::class)->makePartial();
$request5->shouldReceive('pathinfo')->andReturn('some2/a/b/c');
return [
[$request1, true, 'app1'],
[$request2, true, 'app2'],
[$request3, true, 'app3'],
[$request4, true, null],
[$request5, true, 'some2', 'path'],
[$request1, false, 'some3'],
];
}
public function testRunWithException()
{
$request = m::mock(Request::class);
$response = m::mock(Response::class);
$this->app->shouldReceive('instance')->once()->with('request', $request);
$this->app->shouldReceive('initialize')->once();
$exception = new Exception();
$this->http->shouldReceive('runWithRequest')->once()->with($request)->andThrow($exception);
$handle = m::mock(Handle::class);
$handle->shouldReceive('report')->once()->with($exception);
$handle->shouldReceive('render')->once()->with($request, $exception)->andReturn($response);
$this->app->shouldReceive('make')->with(Handle::class)->andReturn($handle);
$this->assertEquals($response, $this->http->run($request));
}
public function testEnd()
{
$response = m::mock(Response::class);
$event = m::mock(Event::class);
$event->shouldReceive('trigger')->once()->with(HttpEnd::class, $response);
$this->app->shouldReceive('get')->once()->with('event')->andReturn($event);
$log = m::mock(Log::class);
$log->shouldReceive('save')->once();
$this->app->shouldReceive('get')->once()->with('log')->andReturn($log);
$this->http->end($response);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace think\tests;
use Mockery as m;
use Mockery\MockInterface;
use think\App;
use think\Config;
use think\Container;
trait InteractsWithApp
{
/** @var App|MockInterface */
protected $app;
/** @var Config|MockInterface */
protected $config;
protected function prepareApp()
{
$this->app = m::mock(App::class)->makePartial();
Container::setInstance($this->app);
$this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
$this->app->shouldReceive('isDebug')->andReturnTrue();
$this->config = m::mock(Config::class)->makePartial();
$this->config->shouldReceive('get')->with('app.show_error_msg')->andReturnTrue();
$this->app->shouldReceive('get')->with('config')->andReturn($this->config);
$this->app->shouldReceive('runningInConsole')->andReturn(false);
}
}

127
tests/LogTest.php Normal file
View File

@@ -0,0 +1,127 @@
<?php
namespace think\tests;
use InvalidArgumentException;
use Mockery as m;
use Mockery\MockInterface;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use think\Log;
use think\log\ChannelSet;
class LogTest extends TestCase
{
use InteractsWithApp;
/** @var Log|MockInterface */
protected $log;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->prepareApp();
$this->log = new Log($this->app);
}
public function testGetConfig()
{
$config = [
'default' => 'file',
];
$this->config->shouldReceive('get')->with('log')->andReturn($config);
$this->assertEquals($config, $this->log->getConfig());
$this->expectException(InvalidArgumentException::class);
$this->log->getChannelConfig('foo');
}
public function testChannel()
{
$this->assertInstanceOf(ChannelSet::class, $this->log->channel(['file', 'mail']));
}
public function testLogManagerInstances()
{
$this->config->shouldReceive('get')->with("log.channels.single", null)->andReturn(['type' => 'file']);
$channel1 = $this->log->channel('single');
$channel2 = $this->log->channel('single');
$this->assertSame($channel1, $channel2);
}
public function testFileLog()
{
$root = vfsStream::setup();
$this->config->shouldReceive('get')->with("log.default", null)->andReturn('file');
$this->config->shouldReceive('get')->with("log.channels.file", null)
->andReturn(['type' => 'file', 'path' => $root->url()]);
$this->log->info('foo');
$this->assertEquals([['info', 'foo']], array_map(fn($log) => [$log->type, $log->message], $this->log->getLog()));
$this->log->clear();
$this->assertEmpty($this->log->getLog());
$this->log->error('foo');
$this->log->emergency('foo');
$this->log->alert('foo');
$this->log->critical('foo');
$this->log->warning('foo');
$this->log->notice('foo');
$this->log->debug('foo');
$this->log->sql('foo');
$this->log->custom('foo');
$this->assertEquals([
['error', 'foo'],
['emergency', 'foo'],
['alert', 'foo'],
['critical', 'foo'],
['warning', 'foo'],
['notice', 'foo'],
['debug', 'foo'],
['sql', 'foo'],
['custom', 'foo'],
], array_map(fn($log) => [$log->type, $log->message], $this->log->getLog()));
$this->log->write('foo');
$this->assertTrue($root->hasChildren());
$this->assertEmpty($this->log->getLog());
$this->log->close();
$this->log->info('foo');
$this->assertEmpty($this->log->getLog());
}
public function testSave()
{
$root = vfsStream::setup();
$this->config->shouldReceive('get')->with("log.default", null)->andReturn('file');
$this->config->shouldReceive('get')->with("log.channels.file", null)
->andReturn(['type' => 'file', 'path' => $root->url()]);
$this->log->info('foo');
$this->log->save();
$this->assertTrue($root->hasChildren());
}
}

110
tests/MiddlewareTest.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
namespace think\tests;
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
use think\Exception;
use think\exception\Handle;
use think\Middleware;
use think\Pipeline;
use think\Request;
use think\Response;
class MiddlewareTest extends TestCase
{
use InteractsWithApp;
/** @var Middleware|MockInterface */
protected $middleware;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->prepareApp();
$this->middleware = new Middleware($this->app);
}
public function testSetMiddleware()
{
$this->middleware->add('BarMiddleware', 'bar');
$this->assertEquals(1, count($this->middleware->all('bar')));
$this->middleware->controller('BarMiddleware');
$this->assertEquals(1, count($this->middleware->all('controller')));
$this->middleware->import(['FooMiddleware']);
$this->assertEquals(1, count($this->middleware->all()));
$this->middleware->unshift(['BazMiddleware', 'baz']);
$this->assertEquals(2, count($this->middleware->all()));
$this->assertEquals([['BazMiddleware', 'handle'], 'baz'], $this->middleware->all()[0]);
$this->config->shouldReceive('get')->with('middleware.alias', [])
->andReturn(['foo' => ['FooMiddleware', 'FarMiddleware']]);
$this->middleware->add('foo');
$this->assertEquals(3, count($this->middleware->all()));
$this->middleware->add(function () {
});
$this->middleware->add(function () {
});
$this->assertEquals(5, count($this->middleware->all()));
}
public function testPipelineAndEnd()
{
$bar = m::mock("overload:BarMiddleware");
$foo = m::mock("overload:FooMiddleware", Foo::class);
$request = m::mock(Request::class);
$response = m::mock(Response::class);
$e = new Exception();
$handle = m::mock(Handle::class);
$handle->shouldReceive('report')->with($e)->andReturnNull();
$handle->shouldReceive('render')->with($request, $e)->andReturn($response);
$foo->shouldReceive('handle')->once()->andReturnUsing(function ($request, $next) {
return $next($request);
});
$bar->shouldReceive('handle')->once()->andReturnUsing(function ($request, $next) use ($e) {
$next($request);
throw $e;
});
$foo->shouldReceive('end')->once()->with($response)->andReturnNull();
$this->app->shouldReceive('make')->with(Handle::class)->andReturn($handle);
$this->config->shouldReceive('get')->once()->with('middleware.priority', [])
->andReturn(['FooMiddleware', 'BarMiddleware']);
$this->middleware->import([function ($request, $next) {
return $next($request);
}, 'BarMiddleware', 'FooMiddleware']);
$this->assertInstanceOf(Pipeline::class, $pipeline = $this->middleware->pipeline());
$pipeline->send($request)->then(function ($request) use ($e, $response) {
throw $e;
});
$this->middleware->end($response);
}
}
class Foo
{
public function end(Response $response)
{
}
}

352
tests/PipelineTest.php Normal file
View File

@@ -0,0 +1,352 @@
<?php
namespace think\tests;
use Closure;
use Exception;
use PHPUnit\Framework\TestCase;
use think\Pipeline;
class PipelineTest extends TestCase
{
protected $pipeline;
protected function setUp(): void
{
$this->pipeline = new Pipeline();
}
public function testSend()
{
$data = 'test data';
$result = $this->pipeline->send($data);
$this->assertSame($this->pipeline, $result);
}
public function testThroughWithArray()
{
$pipes = [
function ($passable, $next) {
return $next($passable . ' pipe1');
},
function ($passable, $next) {
return $next($passable . ' pipe2');
}
];
$result = $this->pipeline->through($pipes);
$this->assertSame($this->pipeline, $result);
}
public function testThroughWithArguments()
{
$pipe1 = function ($passable, $next) {
return $next($passable . ' pipe1');
};
$pipe2 = function ($passable, $next) {
return $next($passable . ' pipe2');
};
$result = $this->pipeline->through($pipe1, $pipe2);
$this->assertSame($this->pipeline, $result);
}
public function testThenExecutesPipeline()
{
$pipes = [
function ($passable, $next) {
return $next($passable . ' pipe1');
},
function ($passable, $next) {
return $next($passable . ' pipe2');
}
];
$destination = function ($passable) {
return $passable . ' destination';
};
$result = $this->pipeline
->send('start')
->through($pipes)
->then($destination);
$this->assertEquals('start pipe1 pipe2 destination', $result);
}
public function testPipelineExecutesInCorrectOrder()
{
$order = [];
$pipes = [
function ($passable, $next) use (&$order) {
$order[] = 'pipe1_before';
$result = $next($passable);
$order[] = 'pipe1_after';
return $result;
},
function ($passable, $next) use (&$order) {
$order[] = 'pipe2_before';
$result = $next($passable);
$order[] = 'pipe2_after';
return $result;
}
];
$destination = function ($passable) use (&$order) {
$order[] = 'destination';
return $passable;
};
$this->pipeline
->send('test')
->through($pipes)
->then($destination);
$expected = ['pipe1_before', 'pipe2_before', 'destination', 'pipe2_after', 'pipe1_after'];
$this->assertEquals($expected, $order);
}
public function testEmptyPipelineExecutesDestination()
{
$destination = function ($passable) {
return $passable . ' processed';
};
$result = $this->pipeline
->send('test')
->through([])
->then($destination);
$this->assertEquals('test processed', $result);
}
public function testPipelineCanModifyData()
{
$pipes = [
function ($passable, $next) {
$passable['pipe1'] = true;
return $next($passable);
},
function ($passable, $next) {
$passable['pipe2'] = true;
return $next($passable);
}
];
$destination = function ($passable) {
$passable['destination'] = true;
return $passable;
};
$result = $this->pipeline
->send([])
->through($pipes)
->then($destination);
$expected = [
'pipe1' => true,
'pipe2' => true,
'destination' => true
];
$this->assertEquals($expected, $result);
}
public function testPipelineCanShortCircuit()
{
$pipes = [
function ($passable, $next) {
if ($passable === 'stop') {
return 'stopped';
}
return $next($passable);
},
function ($passable, $next) {
return $next($passable . ' pipe2');
}
];
$destination = function ($passable) {
return $passable . ' destination';
};
$result = $this->pipeline
->send('stop')
->through($pipes)
->then($destination);
$this->assertEquals('stopped', $result);
}
public function testExceptionInDestinationIsHandled()
{
$pipes = [
function ($passable, $next) {
return $next($passable);
}
];
$destination = function ($passable) {
throw new Exception('Destination error');
};
$this->expectException(Exception::class);
$this->expectExceptionMessage('Destination error');
$this->pipeline
->send('test')
->through($pipes)
->then($destination);
}
public function testExceptionInPipeIsHandled()
{
$pipes = [
function ($passable, $next) {
throw new Exception('Pipe error');
}
];
$destination = function ($passable) {
return $passable;
};
$this->expectException(Exception::class);
$this->expectExceptionMessage('Pipe error');
$this->pipeline
->send('test')
->through($pipes)
->then($destination);
}
public function testWhenExceptionSetsHandler()
{
$result = $this->pipeline->whenException(function ($passable, $e) {
return 'handled';
});
$this->assertSame($this->pipeline, $result);
}
public function testExceptionHandlerIsCalledOnException()
{
$pipes = [
function ($passable, $next) {
throw new Exception('Test exception');
}
];
$destination = function ($passable) {
return $passable;
};
$result = $this->pipeline
->send('test')
->through($pipes)
->whenException(function ($passable, $e) {
return 'handled: ' . $e->getMessage();
})
->then($destination);
$this->assertEquals('handled: Test exception', $result);
}
public function testExceptionHandlerReceivesCorrectParameters()
{
$pipes = [
function ($passable, $next) {
throw new Exception('Test exception');
}
];
$destination = function ($passable) {
return $passable;
};
$handlerCalled = false;
$receivedPassable = null;
$receivedException = null;
$this->pipeline
->send('original data')
->through($pipes)
->whenException(function ($passable, $e) use (&$handlerCalled, &$receivedPassable, &$receivedException) {
$handlerCalled = true;
$receivedPassable = $passable;
$receivedException = $e;
return 'handled';
})
->then($destination);
$this->assertTrue($handlerCalled);
$this->assertEquals('original data', $receivedPassable);
$this->assertInstanceOf(Exception::class, $receivedException);
$this->assertEquals('Test exception', $receivedException->getMessage());
}
public function testExceptionInDestinationWithHandler()
{
$destination = function ($passable) {
throw new Exception('Destination exception');
};
$result = $this->pipeline
->send('test')
->through([])
->whenException(function ($passable, $e) {
return 'destination handled: ' . $e->getMessage();
})
->then($destination);
$this->assertEquals('destination handled: Destination exception', $result);
}
public function testComplexPipelineWithExceptions()
{
$pipes = [
function ($passable, $next) {
$passable[] = 'pipe1';
return $next($passable);
},
function ($passable, $next) {
if (in_array('error', $passable)) {
throw new Exception('Pipe2 error');
}
$passable[] = 'pipe2';
return $next($passable);
},
function ($passable, $next) {
$passable[] = 'pipe3';
return $next($passable);
}
];
$destination = function ($passable) {
$passable[] = 'destination';
return $passable;
};
// Normal execution
$result1 = $this->pipeline
->send(['start'])
->through($pipes)
->then($destination);
$this->assertEquals(['start', 'pipe1', 'pipe2', 'pipe3', 'destination'], $result1);
// With exception handling
$result2 = $this->pipeline
->send(['start', 'error'])
->through($pipes)
->whenException(function ($passable, $e) {
return ['error_handled', $e->getMessage()];
})
->then($destination);
$this->assertEquals(['error_handled', 'Pipe2 error'], $result2);
}
}

569
tests/RequestTest.php Normal file
View File

@@ -0,0 +1,569 @@
<?php
namespace think\tests;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use think\App;
use think\Config;
use think\Container;
use think\Env;
use think\Request;
use think\Session;
class RequestTest extends TestCase
{
use InteractsWithApp;
/** @var Request */
protected $request;
protected function setUp(): void
{
$this->prepareApp();
$this->request = new Request();
}
protected function tearDown(): void
{
m::close();
}
public function testConstructor()
{
$request = new Request();
$this->assertInstanceOf(Request::class, $request);
}
public function testMake()
{
$request = Request::__make($this->app);
$this->assertInstanceOf(Request::class, $request);
}
public function testGetMethod()
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$this->assertEquals('GET', $this->request->method());
$this->assertTrue($this->request->isGet());
$this->assertFalse($this->request->isPost());
}
public function testPostMethod()
{
$request = new Request();
$request->withServer(['REQUEST_METHOD' => 'POST']);
$this->assertEquals('POST', $request->method());
$this->assertTrue($request->isPost());
$this->assertFalse($request->isGet());
}
public function testPutMethod()
{
$request = new Request();
$request->withServer(['REQUEST_METHOD' => 'PUT']);
$this->assertEquals('PUT', $request->method());
$this->assertTrue($request->isPut());
$this->assertFalse($request->isGet());
}
public function testDeleteMethod()
{
$request = new Request();
$request->withServer(['REQUEST_METHOD' => 'DELETE']);
$this->assertEquals('DELETE', $request->method());
$this->assertTrue($request->isDelete());
$this->assertFalse($request->isGet());
}
public function testHeadMethod()
{
$request = new Request();
$request->withServer(['REQUEST_METHOD' => 'HEAD']);
$this->assertEquals('HEAD', $request->method());
$this->assertTrue($request->isHead());
$this->assertFalse($request->isGet());
}
public function testPatchMethod()
{
$request = new Request();
$request->withServer(['REQUEST_METHOD' => 'PATCH']);
$this->assertEquals('PATCH', $request->method());
$this->assertTrue($request->isPatch());
$this->assertFalse($request->isGet());
}
public function testOptionsMethod()
{
$request = new Request();
$request->withServer(['REQUEST_METHOD' => 'OPTIONS']);
$this->assertEquals('OPTIONS', $request->method());
$this->assertTrue($request->isOptions());
$this->assertFalse($request->isGet());
}
public function testGetParameter()
{
$request = new Request();
$request->withGet(['test' => 'value']);
$this->assertEquals('value', $request->get('test'));
$this->assertEquals('default', $request->get('missing', 'default'));
$this->assertEquals(['test' => 'value'], $request->get());
}
public function testPostParameter()
{
$request = new Request();
$request->withPost(['test' => 'value']);
$this->assertEquals('value', $request->post('test'));
$this->assertEquals('default', $request->post('missing', 'default'));
$this->assertEquals(['test' => 'value'], $request->post());
}
public function testParamMethod()
{
$request = new Request();
$request->withGet(['get_param' => 'get_value'])
->withPost(['post_param' => 'post_value'])
->withServer(['REQUEST_METHOD' => 'POST']);
$this->assertEquals('get_value', $request->param('get_param'));
$this->assertEquals('post_value', $request->param('post_param'));
$this->assertEquals('default', $request->param('missing', 'default'));
}
public function testHasMethod()
{
$request = new Request();
$request->withGet(['test' => 'value'])
->withPost(['post_test' => 'post_value'])
->withServer(['REQUEST_METHOD' => 'POST']);
$this->assertTrue($request->has('test'));
$this->assertTrue($request->has('post_test'));
$this->assertFalse($request->has('missing'));
}
public function testOnlyMethod()
{
$request = new Request();
$request->withGet(['param1' => 'value1', 'param2' => 'value2', 'param3' => 'value3']);
$result = $request->only(['param1', 'param3']);
$this->assertEquals(['param1' => 'value1', 'param3' => 'value3'], $result);
}
public function testExceptMethod()
{
$request = new Request();
$request->withGet(['param1' => 'value1', 'param2' => 'value2', 'param3' => 'value3']);
$result = $request->except(['param2']);
$this->assertEquals(['param1' => 'value1', 'param3' => 'value3'], $result);
}
public function testHeader()
{
$request = new Request();
$request->withHeader(['content-type' => 'application/json', 'authorization' => 'Bearer token123']);
$this->assertEquals('application/json', $request->header('content-type'));
$this->assertEquals('Bearer token123', $request->header('authorization'));
$this->assertEquals('default', $request->header('missing', 'default'));
}
public function testServer()
{
$request = new Request();
$request->withServer(['HTTP_HOST' => 'example.com', 'REQUEST_URI' => '/test']);
$this->assertEquals('example.com', $request->server('HTTP_HOST'));
$this->assertEquals('/test', $request->server('REQUEST_URI'));
$this->assertEquals('default', $request->server('missing', 'default'));
}
public function testCookie()
{
$request = new Request();
$request->withCookie(['test_cookie' => 'cookie_value']);
$this->assertEquals('cookie_value', $request->cookie('test_cookie'));
$this->assertEquals('default', $request->cookie('missing', 'default'));
}
public function testIsAjax()
{
$request = new Request();
$request->withServer(['HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest']);
$this->assertTrue($request->isAjax());
$request2 = new Request();
$this->assertFalse($request2->isAjax());
}
public function testIsPjax()
{
$request = new Request();
$request->withServer(['HTTP_X_PJAX' => 'true']);
$this->assertTrue($request->isPjax());
$request2 = new Request();
$this->assertFalse($request2->isPjax());
}
public function testIsJson()
{
$request = new Request();
$request->withServer(['HTTP_ACCEPT' => 'application/json']);
$this->assertTrue($request->isJson());
$request2 = new Request();
$request2->withServer(['HTTP_ACCEPT' => 'text/html']);
$this->assertFalse($request2->isJson());
}
public function testIsSsl()
{
$request = new Request();
$request->withServer(['HTTPS' => 'on']);
$this->assertTrue($request->isSsl());
$request2 = new Request();
$request2->withServer(['HTTPS' => 'off']);
$this->assertFalse($request2->isSsl());
$request3 = new Request();
$request3->withServer(['REQUEST_SCHEME' => 'https']);
$this->assertTrue($request3->isSsl());
}
public function testScheme()
{
$request = new Request();
$request->withServer(['HTTPS' => 'on']);
$this->assertEquals('https', $request->scheme());
$request2 = new Request();
$request2->withServer(['HTTPS' => 'off']);
$this->assertEquals('http', $request2->scheme());
}
public function testHost()
{
$request = new Request();
$request->withServer(['HTTP_HOST' => 'example.com:8080']);
$this->assertEquals('example.com:8080', $request->host());
$request2 = new Request();
$request2->withServer(['HTTP_HOST' => 'sub.example.com']);
$this->assertEquals('sub.example.com', $request2->host());
}
public function testPort()
{
$request = new Request();
$request->withServer(['SERVER_PORT' => '8080']);
$this->assertEquals(8080, $request->port());
$request2 = new Request();
$request2->withServer(['SERVER_PORT' => '80']);
$this->assertEquals(80, $request2->port());
}
public function testDomain()
{
$request = new Request();
$request->withServer(['HTTP_HOST' => 'www.example.com', 'HTTPS' => 'on']);
$this->assertEquals('https://www.example.com', $request->domain());
$request2 = new Request();
$request2->withServer(['HTTP_HOST' => 'www.example.com', 'HTTPS' => 'off']);
$this->assertEquals('http://www.example.com', $request2->domain());
}
public function testSubDomain()
{
$request = new Request();
$request->withServer(['HTTP_HOST' => 'sub.example.com']);
$this->assertEquals('sub', $request->subDomain());
$request2 = new Request();
$request2->withServer(['HTTP_HOST' => 'www.sub.example.com']);
$this->assertEquals('www.sub', $request2->subDomain());
$request3 = new Request();
$request3->withServer(['HTTP_HOST' => 'example.com']);
$this->assertEquals('', $request3->subDomain());
}
public function testRootDomain()
{
$request = new Request();
$request->withServer(['HTTP_HOST' => 'www.example.com']);
$this->assertEquals('example.com', $request->rootDomain());
$request2 = new Request();
$request2->withServer(['HTTP_HOST' => 'sub.example.co.uk']);
$this->assertEquals('example.co.uk', $request2->rootDomain());
}
public function testPathinfo()
{
$request = new Request();
$request->withServer(['PATH_INFO' => '/user/profile']);
$this->assertEquals('user/profile', $request->pathinfo());
$request2 = new Request();
$request2->withServer(['REQUEST_URI' => '/app.php/user/profile?id=1', 'SCRIPT_NAME' => '/app.php']);
$this->assertEquals('app.php/user/profile', $request2->pathinfo());
}
public function testUrl()
{
$request = new Request();
$request->withServer(['HTTP_HOST' => 'example.com', 'REQUEST_URI' => '/path/to/resource?param=value']);
$result = $request->url();
$this->assertStringContainsString('/path/to/resource', $result);
}
public function testBaseUrl()
{
$request = new Request();
$request->withServer(['SCRIPT_NAME' => '/app/index.php', 'REQUEST_URI' => '/app/user/profile']);
$this->assertEquals('/app/user/profile', $request->baseUrl());
}
public function testRoot()
{
$request = new Request();
$request->withServer(['SCRIPT_NAME' => '/app/public/index.php']);
$this->assertEquals('', $request->root());
}
public function testQuery()
{
$request = new Request();
$request->withServer(['QUERY_STRING' => 'param1=value1&param2=value2']);
$this->assertEquals('param1=value1&param2=value2', $request->query());
}
public function testIp()
{
$request = new Request();
$request->withServer(['REMOTE_ADDR' => '192.168.1.100']);
$this->assertEquals('192.168.1.100', $request->ip());
// Test with proxy - need to configure proxy IPs first
$request2 = new Request();
$request2->withServer(['REMOTE_ADDR' => '192.168.1.100'])
->withHeader(['x-forwarded-for' => '203.0.113.1, 192.168.1.100']);
// Without proper proxy configuration, it returns REMOTE_ADDR
$this->assertEquals('192.168.1.100', $request2->ip());
}
public function testIsValidIP()
{
$this->assertTrue($this->request->isValidIP('192.168.1.1'));
$this->assertTrue($this->request->isValidIP('2001:db8::1'));
$this->assertFalse($this->request->isValidIP('invalid.ip'));
$this->assertFalse($this->request->isValidIP('999.999.999.999'));
}
public function testTime()
{
$request = new Request();
$request->withServer(['REQUEST_TIME_FLOAT' => 1234567890.123]);
$this->assertEquals(1234567890.123, $request->time(true));
$request2 = new Request();
$request2->withServer(['REQUEST_TIME' => 1234567890]);
$this->assertEquals(1234567890, $request2->time());
}
public function testType()
{
$request = new Request();
$request->withHeader(['content-type' => 'application/json']);
// Type method may return empty if not configured properly
$type = $request->type();
$this->assertIsString($type);
$request2 = new Request();
$request2->withHeader(['content-type' => 'text/html; charset=utf-8']);
$type2 = $request2->type();
$this->assertIsString($type2);
}
public function testContentType()
{
$request = new Request();
$request->withHeader(['content-type' => 'application/json; charset=utf-8']);
$this->assertEquals('application/json', $request->contentType());
}
public function testArrayAccess()
{
$request = new Request();
$request->withGet(['test' => 'value']);
$this->assertTrue(isset($request['test']));
$this->assertEquals('value', $request['test']);
// offsetSet is empty in Request class, so setting values has no effect
$request['new'] = 'new_value';
$this->assertNull($request['new']);
// offsetUnset is also empty, so unset has no effect
unset($request['test']);
$this->assertTrue(isset($request['test']));
}
public function testWithMethods()
{
$request = new Request();
$newRequest = $request->withGet(['key' => 'value']);
$this->assertEquals('value', $newRequest->get('key'));
$newRequest = $request->withPost(['post_key' => 'post_value']);
$this->assertEquals('post_value', $newRequest->post('post_key'));
$newRequest = $request->withHeader(['Content-Type' => 'application/json']);
$this->assertEquals('application/json', $newRequest->header('content-type'));
$newRequest = $request->withServer(['HTTP_HOST' => 'test.com']);
$this->assertEquals('test.com', $newRequest->server('HTTP_HOST'));
$newRequest = $request->withCookie(['session' => 'abc123']);
$this->assertEquals('abc123', $newRequest->cookie('session'));
}
public function testFilter()
{
$request = new Request();
$request->withGet(['email' => ' test@example.com ', 'number' => '123.45']);
$this->assertEquals('test@example.com', $request->get('email', '', 'trim'));
$this->assertEquals(123, $request->get('number', 0, 'intval'));
$this->assertEquals(123.45, $request->get('number', 0, 'floatval'));
}
public function testParam()
{
$request = new Request();
$request->withGet(['test' => 'get_value'])
->withPost(['test' => 'post_value']);
// Test basic param access
$this->assertEquals('get_value', $request->param('test'));
}
public function testRoute()
{
$request = new Request();
$request->setRoute(['controller' => 'User', 'action' => 'profile']);
$this->assertEquals('User', $request->route('controller'));
$this->assertEquals('profile', $request->route('action'));
$this->assertEquals(['controller' => 'User', 'action' => 'profile'], $request->route());
}
public function testControllerAndAction()
{
$request = new Request();
$request->setController('User');
$request->setAction('profile');
$this->assertEquals('User', $request->controller());
$this->assertEquals('profile', $request->action());
}
public function testMiddlewareProperty()
{
$request = new Request();
$request->withMiddleware(['auth', 'throttle']);
$this->assertEquals(['auth', 'throttle'], $request->middleware());
}
public function testSecureKey()
{
$key = $this->request->secureKey();
$this->assertIsString($key);
$this->assertGreaterThan(0, strlen($key));
}
public function testTokenGeneration()
{
// Test secure key generation
$key = $this->request->secureKey();
$this->assertIsString($key);
$this->assertGreaterThan(0, strlen($key));
}
public function testIsCli()
{
// In test context, this returns true
$this->assertTrue($this->request->isCli());
}
public function testIsCgi()
{
// isCgi() checks PHP_SAPI, not server variables
// In CLI test environment, this will return false
$request = new Request();
$this->assertFalse($request->isCgi());
// Test the method returns boolean
$this->assertIsBool($request->isCgi());
}
public function testProtocol()
{
$request = new Request();
$request->withServer(['SERVER_PROTOCOL' => 'HTTP/1.1']);
$this->assertEquals('HTTP/1.1', $request->protocol());
$request2 = new Request();
$request2->withServer(['SERVER_PROTOCOL' => 'HTTP/2.0']);
$this->assertEquals('HTTP/2.0', $request2->protocol());
}
public function testRemotePort()
{
$request = new Request();
$request->withServer(['REMOTE_PORT' => '12345']);
$this->assertEquals(12345, $request->remotePort());
}
public function testAll()
{
$request = new Request();
$request->withGet(['get_param' => 'get_value'])
->withPost(['post_param' => 'post_value'])
->withServer(['REQUEST_METHOD' => 'POST']);
$all = $request->all();
$this->assertIsArray($all);
$this->assertArrayHasKey('get_param', $all);
// POST params might not appear in all() depending on implementation
$this->assertEquals('get_value', $all['get_param']);
}
public function testExt()
{
$request = new Request();
$request->withServer(['PATH_INFO' => '/user/profile.json']);
$this->assertEquals('json', $request->ext());
$request2 = new Request();
$request2->withServer(['PATH_INFO' => '/user/profile.xml']);
$this->assertEquals('xml', $request2->ext());
$request3 = new Request();
$request3->withServer(['PATH_INFO' => '/user/profile']);
$this->assertEquals('', $request3->ext());
}
}

113
tests/ResponseTest.php Normal file
View File

@@ -0,0 +1,113 @@
<?php
namespace think\tests;
use PHPUnit\Framework\TestCase;
use Mockery as m;
use think\Cookie;
use think\response\Html;
use think\response\Json;
class ResponseTest extends TestCase
{
protected function tearDown(): void
{
m::close();
}
public function testHtmlResponseCreation()
{
$cookie = m::mock(Cookie::class);
$response = new Html($cookie, 'test content');
$this->assertInstanceOf(Html::class, $response);
$this->assertEquals('test content', $response->getData());
}
public function testJsonResponseCreation()
{
$cookie = m::mock(Cookie::class);
$data = ['key' => 'value'];
$response = new Json($cookie, $data);
$this->assertInstanceOf(Json::class, $response);
$this->assertEquals($data, $response->getData());
}
public function testResponseCode()
{
$cookie = m::mock(Cookie::class);
$response = new Html($cookie, 'test', 200);
$this->assertEquals(200, $response->getCode());
$response->code(404);
$this->assertEquals(404, $response->getCode());
}
public function testResponseHeaders()
{
$cookie = m::mock(Cookie::class);
$response = new Html($cookie, 'test');
$response->header(['Content-Type' => 'text/html']);
$headers = $response->getHeader();
$this->assertEquals('text/html', $headers['Content-Type']);
$response->header(['X-Custom' => 'value']);
$headers = $response->getHeader();
$this->assertEquals('value', $headers['X-Custom']);
}
public function testResponseData()
{
$cookie = m::mock(Cookie::class);
$response = new Html($cookie, 'initial');
$this->assertEquals('initial', $response->getData());
$response->data('updated');
$this->assertEquals('updated', $response->getData());
}
public function testResponseStatusMethods()
{
$cookie = m::mock(Cookie::class);
$response = new Html($cookie, '', 200);
$this->assertEquals(200, $response->getCode());
$response->code(404);
$this->assertEquals(404, $response->getCode());
$response->code(500);
$this->assertEquals(500, $response->getCode());
}
public function testContentTypeMethod()
{
$cookie = m::mock(Cookie::class);
$response = new Html($cookie, 'test');
$response->contentType('application/json', 'utf-8');
$headers = $response->getHeader();
$this->assertEquals('application/json; charset=utf-8', $headers['Content-Type']);
}
public function testLastModified()
{
$cookie = m::mock(Cookie::class);
$response = new Html($cookie, 'test');
$time = '2025-01-01 10:00:00';
$response->lastModified($time);
$headers = $response->getHeader();
$this->assertArrayHasKey('Last-Modified', $headers);
}
public function testETag()
{
$cookie = m::mock(Cookie::class);
$response = new Html($cookie, 'test');
$etag = 'test-etag';
$response->eTag($etag);
$headers = $response->getHeader();
$this->assertEquals('test-etag', $headers['ETag']);
}
}

369
tests/RouteTest.php Normal file
View File

@@ -0,0 +1,369 @@
<?php
namespace think\tests;
use Closure;
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
use think\helper\Str;
use think\Request;
use think\response\Redirect;
use think\response\View;
use think\Route;
class RouteTest extends TestCase
{
use InteractsWithApp;
/** @var Route|MockInterface */
protected $route;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->prepareApp();
$this->config->shouldReceive('get')->with('route')->andReturn(['url_route_must' => true]);
$this->route = new Route($this->app);
}
/**
* @param $path
* @param string $method
* @param string $host
* @return m\Mock|Request
*/
protected function makeRequest($path, $method = 'GET', $host = 'localhost')
{
$request = m::mock(Request::class)->makePartial();
$request->shouldReceive('host')->andReturn($host);
$request->shouldReceive('pathinfo')->andReturn($path);
$request->shouldReceive('url')->andReturn('/' . $path);
$request->shouldReceive('method')->andReturn(strtoupper($method));
return $request;
}
public function testSimpleRequest()
{
$this->route->get('foo', function () {
return 'get-foo';
});
$this->route->put('foo', function () {
return 'put-foo';
});
$this->route->group(function () {
$this->route->post('foo', function () {
return 'post-foo';
});
});
$request = $this->makeRequest('foo', 'post');
$response = $this->route->dispatch($request);
$this->assertEquals(200, $response->getCode());
$this->assertEquals('post-foo', $response->getContent());
$request = $this->makeRequest('foo', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals(200, $response->getCode());
$this->assertEquals('get-foo', $response->getContent());
}
public function testGroup()
{
$this->route->group(function () {
$this->route->group('foo', function () {
$this->route->post('bar', function () {
return 'hello,world!';
});
});
});
$request = $this->makeRequest('foo/bar', 'post');
$response = $this->route->dispatch($request);
$this->assertEquals(200, $response->getCode());
$this->assertEquals('hello,world!', $response->getContent());
}
public function testAllowCrossDomain()
{
$this->route->get('foo', function () {
return 'get-foo';
})->allowCrossDomain(['some' => 'bar']);
$request = $this->makeRequest('foo', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('bar', $response->getHeader('some'));
$this->assertArrayHasKey('Access-Control-Allow-Credentials', $response->getHeader());
//$this->expectException(RouteNotFoundException::class);
$request = $this->makeRequest('foo2', 'options');
$this->route->dispatch($request);
}
public function testControllerDispatch()
{
$this->route->get('foo', 'foo/bar');
$controller = m::Mock(\stdClass::class);
$this->app->shouldReceive('parseClass')->with('controller', 'Foo')->andReturn($controller->mockery_getName());
$this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
$controller->shouldReceive('bar')->andReturn('bar');
$request = $this->makeRequest('foo');
$response = $this->route->dispatch($request);
$this->assertEquals('bar', $response->getContent());
}
public function testEmptyControllerDispatch()
{
$this->route->get('foo', 'foo/bar');
$controller = m::Mock(\stdClass::class);
$this->app->shouldReceive('parseClass')->with('controller', 'Error')->andReturn($controller->mockery_getName());
$this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
$controller->shouldReceive('bar')->andReturn('bar');
$request = $this->makeRequest('foo');
$response = $this->route->dispatch($request);
$this->assertEquals('bar', $response->getContent());
}
protected function createMiddleware($times = 1)
{
$middleware = m::mock(Str::random(5));
$middleware->shouldReceive('handle')->times($times)->andReturnUsing(function ($request, Closure $next) {
return $next($request);
});
$this->app->shouldReceive('make')->with($middleware->mockery_getName())->andReturn($middleware);
return $middleware;
}
public function testControllerWithMiddleware()
{
$this->route->get('foo', 'foo/bar');
$controller = m::mock(FooClass::class);
$controller->middleware = [
$this->createMiddleware()->mockery_getName() . ":params1:params2",
$this->createMiddleware(0)->mockery_getName() => ['except' => 'bar'],
$this->createMiddleware()->mockery_getName() => ['only' => 'bar'],
[
'middleware' => [$this->createMiddleware()->mockery_getName(), [new \stdClass()]],
'options' => ['only' => 'bar'],
],
];
$this->app->shouldReceive('parseClass')->with('controller', 'Foo')->andReturn($controller->mockery_getName());
$this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
$controller->shouldReceive('bar')->once()->andReturn('bar');
$request = $this->makeRequest('foo');
$response = $this->route->dispatch($request);
$this->assertEquals('bar', $response->getContent());
}
public function testRedirectDispatch()
{
$this->route->redirect('foo', 'http://localhost', 302);
$request = $this->makeRequest('foo');
$this->app->shouldReceive('make')->with(Request::class)->andReturn($request);
$response = $this->route->dispatch($request);
$this->assertInstanceOf(Redirect::class, $response);
$this->assertEquals(302, $response->getCode());
$this->assertEquals('http://localhost', $response->getData());
}
public function testViewDispatch()
{
$this->route->view('foo', 'index/hello', ['city' => 'shanghai']);
$request = $this->makeRequest('foo');
$response = $this->route->dispatch($request);
$this->assertInstanceOf(View::class, $response);
$this->assertEquals(['city' => 'shanghai'], $response->getVars());
$this->assertEquals('index/hello', $response->getData());
}
public function testDomainBindResponse()
{
$this->route->domain('test', function () {
$this->route->get('/', function () {
return 'Hello,ThinkPHP';
});
});
$request = $this->makeRequest('', 'get', 'test.domain.com');
$response = $this->route->dispatch($request);
$this->assertEquals('Hello,ThinkPHP', $response->getContent());
$this->assertEquals(200, $response->getCode());
}
public function testResourceRouting()
{
// Test basic resource registration (returns ResourceRegister when not lazy)
$resource = $this->route->resource('users', 'Users');
$this->assertTrue($resource instanceof \think\route\Resource || $resource instanceof \think\route\ResourceRegister);
// Test REST methods configuration
$restMethods = $this->route->getRest();
$this->assertIsArray($restMethods);
$this->assertArrayHasKey('index', $restMethods);
$this->assertArrayHasKey('create', $restMethods);
$this->assertArrayHasKey('save', $restMethods);
$this->assertArrayHasKey('read', $restMethods);
$this->assertArrayHasKey('edit', $restMethods);
$this->assertArrayHasKey('update', $restMethods);
$this->assertArrayHasKey('delete', $restMethods);
// Test custom REST method modification
$this->route->rest('custom', ['get', '/custom', 'customAction']);
$customMethod = $this->route->getRest('custom');
$this->assertEquals(['get', '/custom', 'customAction'], $customMethod);
}
public function testUrlGeneration()
{
$this->route->get('user/<id>', 'User/detail')->name('user.detail');
$this->route->post('user', 'User/save')->name('user.save');
$urlBuild = $this->route->buildUrl('user.detail', ['id' => 123]);
$this->assertInstanceOf(\think\route\Url::class, $urlBuild);
$urlBuild = $this->route->buildUrl('user.save');
$this->assertInstanceOf(\think\route\Url::class, $urlBuild);
}
public function testRouteParameterBinding()
{
$this->route->get('user/<id>', function ($id) {
return "User ID: $id";
});
$request = $this->makeRequest('user/123', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('User ID: 123', $response->getContent());
// Test multiple parameters
$this->route->get('post/<year>/<month>', function ($year, $month) {
return "Year: $year, Month: $month";
});
$request = $this->makeRequest('post/2024/12', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('Year: 2024, Month: 12', $response->getContent());
}
public function testRoutePatternValidation()
{
$this->route->get('user/<id>', function ($id) {
return "User ID: $id";
})->pattern(['id' => '\d+']);
// Valid numeric ID
$request = $this->makeRequest('user/123', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('User ID: 123', $response->getContent());
// Test pattern with name validation
$this->route->get('profile/<name>', function ($name) {
return "Profile: $name";
})->pattern(['name' => '[a-zA-Z]+']);
$request = $this->makeRequest('profile/john', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('Profile: john', $response->getContent());
}
public function testMissRoute()
{
$this->route->get('home', function () {
return 'home page';
});
$this->route->miss(function () {
return 'Page not found';
});
// Test existing route
$request = $this->makeRequest('home', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('home page', $response->getContent());
// Test miss route
$request = $this->makeRequest('nonexistent', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('Page not found', $response->getContent());
}
public function testRouteMiddleware()
{
$middleware = $this->createMiddleware();
$this->route->get('protected', function () {
return 'protected content';
})->middleware($middleware->mockery_getName());
$request = $this->makeRequest('protected', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('protected content', $response->getContent());
}
public function testRouteOptions()
{
$this->route->get('api/<version>/users', function ($version) {
return "API Version: $version";
})->option(['version' => '1.0']);
$request = $this->makeRequest('api/v2/users', 'get');
$response = $this->route->dispatch($request);
$this->assertEquals('API Version: v2', $response->getContent());
}
public function testRouteCache()
{
// Test route configuration
$config = $this->route->config();
$this->assertIsArray($config);
$caseConfig = $this->route->config('url_case_sensitive');
$this->assertIsBool($caseConfig);
// Test route name management
$this->route->get('test', function () {
return 'test';
})->name('test.route');
$names = $this->route->getName('test.route');
$this->assertIsArray($names);
}
}
class FooClass
{
public $middleware = [];
public function bar()
{
}
}

225
tests/SessionTest.php Normal file
View File

@@ -0,0 +1,225 @@
<?php
namespace think\tests;
use Mockery as m;
use Mockery\MockInterface;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;
use think\App;
use think\cache\Driver;
use think\Config;
use think\Container;
use think\contract\SessionHandlerInterface;
use think\helper\Str;
use think\Session;
use think\session\driver\Cache;
use think\session\driver\File;
class SessionTest extends TestCase
{
/** @var App|MockInterface */
protected $app;
/** @var Session|MockInterface */
protected $session;
/** @var Config|MockInterface */
protected $config;
protected $handler;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->app = m::mock(App::class)->makePartial();
Container::setInstance($this->app);
$this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
$this->config = m::mock(Config::class)->makePartial();
$this->app->shouldReceive('get')->with('config')->andReturn($this->config);
$handlerClass = "\\think\\session\\driver\\Test" . Str::random(10);
$this->config->shouldReceive("get")->with("session.type", "file")->andReturn($handlerClass);
$this->session = new Session($this->app);
$this->handler = m::mock('overload:' . $handlerClass, SessionHandlerInterface::class);
}
public function testLoadData()
{
$data = [
"bar" => 'foo',
];
$id = md5(uniqid());
$this->handler->shouldReceive("read")->once()->with($id)->andReturn(serialize($data));
$this->session->setId($id);
$this->session->init();
$this->assertEquals('foo', $this->session->get('bar'));
$this->assertTrue($this->session->has('bar'));
$this->assertFalse($this->session->has('foo'));
$this->session->set('foo', 'bar');
$this->assertTrue($this->session->has('foo'));
$this->assertEquals('bar', $this->session->pull('foo'));
$this->assertFalse($this->session->has('foo'));
}
public function testSave()
{
$id = md5(uniqid());
$this->handler->shouldReceive('read')->once()->with($id)->andReturn("");
$this->handler->shouldReceive('write')->once()->with($id, serialize([
"bar" => 'foo',
]))->andReturnTrue();
$this->session->setId($id);
$this->session->init();
$this->session->set('bar', 'foo');
$this->session->save();
}
public function testFlash()
{
$this->session->flash('foo', 'bar');
$this->session->flash('bar', 0);
$this->session->flash('baz', true);
$this->assertTrue($this->session->has('foo'));
$this->assertEquals('bar', $this->session->get('foo'));
$this->assertEquals(0, $this->session->get('bar'));
$this->assertTrue($this->session->get('baz'));
$this->session->clearFlashData();
$this->assertTrue($this->session->has('foo'));
$this->assertEquals('bar', $this->session->get('foo'));
$this->assertEquals(0, $this->session->get('bar'));
$this->session->clearFlashData();
$this->assertFalse($this->session->has('foo'));
$this->assertNull($this->session->get('foo'));
$this->session->flash('foo', 'bar');
$this->assertTrue($this->session->has('foo'));
$this->session->clearFlashData();
$this->session->reflash();
$this->session->clearFlashData();
$this->assertTrue($this->session->has('foo'));
}
public function testClear()
{
$this->session->set('bar', 'foo');
$this->assertEquals('foo', $this->session->get('bar'));
$this->session->clear();
$this->assertFalse($this->session->has('foo'));
}
public function testSetName()
{
$this->session->setName('foo');
$this->assertEquals('foo', $this->session->getName());
}
public function testDestroy()
{
$id = md5(uniqid());
$this->handler->shouldReceive('read')->once()->with($id)->andReturn("");
$this->handler->shouldReceive('delete')->once()->with($id)->andReturnTrue();
$this->session->setId($id);
$this->session->init();
$this->session->set('bar', 'foo');
$this->session->destroy();
$this->assertFalse($this->session->has('bar'));
$this->assertNotEquals($id, $this->session->getId());
}
public function testFileHandler()
{
$root = vfsStream::setup();
vfsStream::newFile('bar')
->at($root)
->lastModified(time());
vfsStream::newFile('bar')
->at(vfsStream::newDirectory("foo")->at($root))
->lastModified(100);
$this->assertTrue($root->hasChild("bar"));
$this->assertTrue($root->hasChild("foo/bar"));
$handler = new TestFileHandle($this->app, [
'path' => $root->url(),
'gc_probability' => 1,
'gc_divisor' => 1,
]);
$this->assertTrue($root->hasChild("bar"));
$this->assertFalse($root->hasChild("foo/bar"));
$id = md5(uniqid());
$handler->write($id, "bar");
$this->assertTrue($root->hasChild("sess_{$id}"));
$this->assertEquals("bar", $handler->read($id));
$handler->delete($id);
$this->assertFalse($root->hasChild("sess_{$id}"));
}
public function testCacheHandler()
{
$id = md5(uniqid());
$cache = m::mock(\think\Cache::class);
$store = m::mock(Driver::class);
$cache->shouldReceive('store')->once()->with('redis')->andReturn($store);
$handler = new Cache($cache, ['store' => 'redis']);
$store->shouldReceive("set")->with($id, "bar", 1440)->once()->andReturnTrue();
$handler->write($id, "bar");
$store->shouldReceive("get")->with($id)->once()->andReturn("bar");
$this->assertEquals("bar", $handler->read($id));
$store->shouldReceive("delete")->with($id)->once()->andReturnTrue();
$handler->delete($id);
}
}
class TestFileHandle extends File
{
protected function writeFile($path, $content): bool
{
return (bool) file_put_contents($path, $content);
}
}

60
tests/UrlRouteTest.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace think\tests;
use Mockery as m;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
use think\Request;
use think\Route;
class UrlRouteTest extends TestCase
{
use InteractsWithApp;
/** @var Route|MockInterface */
protected $route;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->prepareApp();
$this->route = new Route($this->app);
}
public function testUrlDispatch()
{
$controller = m::mock(FooClass::class);
$controller->shouldReceive('index')->andReturn('bar');
$this->app->shouldReceive('parseClass')->once()->with('controller', 'Foo')
->andReturn($controller->mockery_getName());
$this->app->shouldReceive('make')->with($controller->mockery_getName(), [], true)->andReturn($controller);
$request = $this->makeRequest('foo');
$response = $this->route->dispatch($request);
$this->assertEquals('bar', $response->getContent());
}
/**
* @param $path
* @param string $method
* @param string $host
* @return m\Mock|Request
*/
protected function makeRequest($path, $method = 'GET', $host = 'localhost')
{
$request = m::mock(Request::class)->makePartial();
$request->shouldReceive('host')->andReturn($host);
$request->shouldReceive('pathinfo')->andReturn($path);
$request->shouldReceive('url')->andReturn('/' . $path);
$request->shouldReceive('method')->andReturn(strtoupper($method));
return $request;
}
}

122
tests/ViewTest.php Normal file
View File

@@ -0,0 +1,122 @@
<?php
namespace think\tests;
use Mockery\MockInterface;
use PHPUnit\Framework\TestCase;
use think\App;
use think\Config;
use think\Container;
use think\contract\TemplateHandlerInterface;
use think\View;
use Mockery as m;
class ViewTest extends TestCase
{
/** @var App|MockInterface */
protected $app;
/** @var View|MockInterface */
protected $view;
/** @var Config|MockInterface */
protected $config;
protected function tearDown(): void
{
m::close();
}
protected function setUp(): void
{
$this->app = m::mock(App::class)->makePartial();
Container::setInstance($this->app);
$this->app->shouldReceive('make')->with(App::class)->andReturn($this->app);
$this->config = m::mock(Config::class)->makePartial();
$this->app->shouldReceive('get')->with('config')->andReturn($this->config);
$this->view = new View($this->app);
}
public function testAssignData()
{
$this->view->assign('foo', 'bar');
$this->view->assign(['baz' => 'boom']);
$this->view->qux = "corge";
$this->assertEquals('bar', $this->view->foo);
$this->assertEquals('boom', $this->view->baz);
$this->assertEquals('corge', $this->view->qux);
$this->assertTrue(isset($this->view->qux));
}
public function testRender()
{
$this->config->shouldReceive("get")->with("view.type", 'php')->andReturn(TestTemplate::class);
$this->view->filter(function ($content) {
return $content;
});
$this->assertEquals("fetch", $this->view->fetch('foo'));
$this->assertEquals("display", $this->view->display('foo'));
}
}
class TestTemplate implements TemplateHandlerInterface
{
/**
* 检测是否存在模板文件
* @param string $template 模板文件或者模板规则
* @return bool
*/
public function exists(string $template): bool
{
return true;
}
/**
* 渲染模板文件
* @param string $template 模板文件
* @param array $data 模板变量
* @return void
*/
public function fetch(string $template, array $data = []): void
{
echo "fetch";
}
/**
* 渲染模板内容
* @param string $content 模板内容
* @param array $data 模板变量
* @return void
*/
public function display(string $content, array $data = []): void
{
echo "display";
}
/**
* 配置模板引擎
* @param array $config 参数
* @return void
*/
public function config(array $config): void
{
// TODO: Implement config() method.
}
/**
* 获取模板引擎配置
* @param string $name 参数名
* @return mixed
*/
public function getConfig(string $name)
{
// TODO: Implement getConfig() method.
}
}

3
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,3 @@
<?php
include __DIR__.'/../src/helper.php';