PageRenderTime 41ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/PdoSessionHandlerTest.php

http://github.com/symfony/symfony
PHP | 401 lines | 302 code | 72 blank | 27 comment | 5 complexity | 2062b72154e0b27d1b3212f27004b239 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
  11. use PHPUnit\Framework\TestCase;
  12. use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
  13. /**
  14. * @requires extension pdo_sqlite
  15. * @group time-sensitive
  16. */
  17. class PdoSessionHandlerTest extends TestCase
  18. {
  19. private $dbFile;
  20. protected function tearDown(): void
  21. {
  22. // make sure the temporary database file is deleted when it has been created (even when a test fails)
  23. if ($this->dbFile) {
  24. @unlink($this->dbFile);
  25. }
  26. parent::tearDown();
  27. }
  28. protected function getPersistentSqliteDsn()
  29. {
  30. $this->dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_sessions');
  31. return 'sqlite:'.$this->dbFile;
  32. }
  33. protected function getMemorySqlitePdo()
  34. {
  35. $pdo = new \PDO('sqlite::memory:');
  36. $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
  37. $storage = new PdoSessionHandler($pdo);
  38. $storage->createTable();
  39. return $pdo;
  40. }
  41. public function testWrongPdoErrMode()
  42. {
  43. $this->expectException('InvalidArgumentException');
  44. $pdo = $this->getMemorySqlitePdo();
  45. $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT);
  46. new PdoSessionHandler($pdo);
  47. }
  48. public function testInexistentTable()
  49. {
  50. $this->expectException('RuntimeException');
  51. $storage = new PdoSessionHandler($this->getMemorySqlitePdo(), ['db_table' => 'inexistent_table']);
  52. $storage->open('', 'sid');
  53. $storage->read('id');
  54. $storage->write('id', 'data');
  55. $storage->close();
  56. }
  57. public function testCreateTableTwice()
  58. {
  59. $this->expectException('RuntimeException');
  60. $storage = new PdoSessionHandler($this->getMemorySqlitePdo());
  61. $storage->createTable();
  62. }
  63. public function testWithLazyDsnConnection()
  64. {
  65. $dsn = $this->getPersistentSqliteDsn();
  66. $storage = new PdoSessionHandler($dsn);
  67. $storage->createTable();
  68. $storage->open('', 'sid');
  69. $data = $storage->read('id');
  70. $storage->write('id', 'data');
  71. $storage->close();
  72. $this->assertSame('', $data, 'New session returns empty string data');
  73. $storage->open('', 'sid');
  74. $data = $storage->read('id');
  75. $storage->close();
  76. $this->assertSame('data', $data, 'Written value can be read back correctly');
  77. }
  78. public function testWithLazySavePathConnection()
  79. {
  80. $dsn = $this->getPersistentSqliteDsn();
  81. // Open is called with what ini_set('session.save_path', $dsn) would mean
  82. $storage = new PdoSessionHandler(null);
  83. $storage->open($dsn, 'sid');
  84. $storage->createTable();
  85. $data = $storage->read('id');
  86. $storage->write('id', 'data');
  87. $storage->close();
  88. $this->assertSame('', $data, 'New session returns empty string data');
  89. $storage->open($dsn, 'sid');
  90. $data = $storage->read('id');
  91. $storage->close();
  92. $this->assertSame('data', $data, 'Written value can be read back correctly');
  93. }
  94. public function testReadWriteReadWithNullByte()
  95. {
  96. $sessionData = 'da'."\0".'ta';
  97. $storage = new PdoSessionHandler($this->getMemorySqlitePdo());
  98. $storage->open('', 'sid');
  99. $readData = $storage->read('id');
  100. $storage->write('id', $sessionData);
  101. $storage->close();
  102. $this->assertSame('', $readData, 'New session returns empty string data');
  103. $storage->open('', 'sid');
  104. $readData = $storage->read('id');
  105. $storage->close();
  106. $this->assertSame($sessionData, $readData, 'Written value can be read back correctly');
  107. }
  108. public function testReadConvertsStreamToString()
  109. {
  110. $pdo = new MockPdo('pgsql');
  111. $pdo->prepareResult = $this->getMockBuilder('PDOStatement')->getMock();
  112. $content = 'foobar';
  113. $stream = $this->createStream($content);
  114. $pdo->prepareResult->expects($this->once())->method('fetchAll')
  115. ->willReturn([[$stream, 42, time()]]);
  116. $storage = new PdoSessionHandler($pdo);
  117. $result = $storage->read('foo');
  118. $this->assertSame($content, $result);
  119. }
  120. public function testReadLockedConvertsStreamToString()
  121. {
  122. if (filter_var(ini_get('session.use_strict_mode'), FILTER_VALIDATE_BOOLEAN)) {
  123. $this->markTestSkipped('Strict mode needs no locking for new sessions.');
  124. }
  125. $pdo = new MockPdo('pgsql');
  126. $selectStmt = $this->getMockBuilder('PDOStatement')->getMock();
  127. $insertStmt = $this->getMockBuilder('PDOStatement')->getMock();
  128. $pdo->prepareResult = function ($statement) use ($selectStmt, $insertStmt) {
  129. return 0 === strpos($statement, 'INSERT') ? $insertStmt : $selectStmt;
  130. };
  131. $content = 'foobar';
  132. $stream = $this->createStream($content);
  133. $exception = null;
  134. $selectStmt->expects($this->atLeast(2))->method('fetchAll')
  135. ->willReturnCallback(function () use (&$exception, $stream) {
  136. return $exception ? [[$stream, 42, time()]] : [];
  137. });
  138. $insertStmt->expects($this->once())->method('execute')
  139. ->willReturnCallback(function () use (&$exception) {
  140. throw $exception = new \PDOException('', '23');
  141. });
  142. $storage = new PdoSessionHandler($pdo);
  143. $result = $storage->read('foo');
  144. $this->assertSame($content, $result);
  145. }
  146. public function testReadingRequiresExactlySameId()
  147. {
  148. $storage = new PdoSessionHandler($this->getMemorySqlitePdo());
  149. $storage->open('', 'sid');
  150. $storage->write('id', 'data');
  151. $storage->write('test', 'data');
  152. $storage->write('space ', 'data');
  153. $storage->close();
  154. $storage->open('', 'sid');
  155. $readDataCaseSensitive = $storage->read('ID');
  156. $readDataNoCharFolding = $storage->read('tést');
  157. $readDataKeepSpace = $storage->read('space ');
  158. $readDataExtraSpace = $storage->read('space ');
  159. $storage->close();
  160. $this->assertSame('', $readDataCaseSensitive, 'Retrieval by ID should be case-sensitive (collation setting)');
  161. $this->assertSame('', $readDataNoCharFolding, 'Retrieval by ID should not do character folding (collation setting)');
  162. $this->assertSame('data', $readDataKeepSpace, 'Retrieval by ID requires spaces as-is');
  163. $this->assertSame('', $readDataExtraSpace, 'Retrieval by ID requires spaces as-is');
  164. }
  165. /**
  166. * Simulates session_regenerate_id(true) which will require an INSERT or UPDATE (replace).
  167. */
  168. public function testWriteDifferentSessionIdThanRead()
  169. {
  170. $storage = new PdoSessionHandler($this->getMemorySqlitePdo());
  171. $storage->open('', 'sid');
  172. $storage->read('id');
  173. $storage->destroy('id');
  174. $storage->write('new_id', 'data_of_new_session_id');
  175. $storage->close();
  176. $storage->open('', 'sid');
  177. $data = $storage->read('new_id');
  178. $storage->close();
  179. $this->assertSame('data_of_new_session_id', $data, 'Data of regenerated session id is available');
  180. }
  181. public function testWrongUsageStillWorks()
  182. {
  183. // wrong method sequence that should no happen, but still works
  184. $storage = new PdoSessionHandler($this->getMemorySqlitePdo());
  185. $storage->write('id', 'data');
  186. $storage->write('other_id', 'other_data');
  187. $storage->destroy('inexistent');
  188. $storage->open('', 'sid');
  189. $data = $storage->read('id');
  190. $otherData = $storage->read('other_id');
  191. $storage->close();
  192. $this->assertSame('data', $data);
  193. $this->assertSame('other_data', $otherData);
  194. }
  195. public function testSessionDestroy()
  196. {
  197. $pdo = $this->getMemorySqlitePdo();
  198. $storage = new PdoSessionHandler($pdo);
  199. $storage->open('', 'sid');
  200. $storage->read('id');
  201. $storage->write('id', 'data');
  202. $storage->close();
  203. $this->assertEquals(1, $pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
  204. $storage->open('', 'sid');
  205. $storage->read('id');
  206. $storage->destroy('id');
  207. $storage->close();
  208. $this->assertEquals(0, $pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn());
  209. $storage->open('', 'sid');
  210. $data = $storage->read('id');
  211. $storage->close();
  212. $this->assertSame('', $data, 'Destroyed session returns empty string');
  213. }
  214. /**
  215. * @runInSeparateProcess
  216. */
  217. public function testSessionGC()
  218. {
  219. $previousLifeTime = ini_set('session.gc_maxlifetime', 1000);
  220. $pdo = $this->getMemorySqlitePdo();
  221. $storage = new PdoSessionHandler($pdo);
  222. $storage->open('', 'sid');
  223. $storage->read('id');
  224. $storage->write('id', 'data');
  225. $storage->close();
  226. $storage->open('', 'sid');
  227. $storage->read('gc_id');
  228. ini_set('session.gc_maxlifetime', -1); // test that you can set lifetime of a session after it has been read
  229. $storage->write('gc_id', 'data');
  230. $storage->close();
  231. $this->assertEquals(2, $pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn(), 'No session pruned because gc not called');
  232. $storage->open('', 'sid');
  233. $data = $storage->read('gc_id');
  234. $storage->gc(-1);
  235. $storage->close();
  236. ini_set('session.gc_maxlifetime', $previousLifeTime);
  237. $this->assertSame('', $data, 'Session already considered garbage, so not returning data even if it is not pruned yet');
  238. $this->assertEquals(1, $pdo->query('SELECT COUNT(*) FROM sessions')->fetchColumn(), 'Expired session is pruned');
  239. }
  240. public function testGetConnection()
  241. {
  242. $storage = new PdoSessionHandler($this->getMemorySqlitePdo());
  243. $method = new \ReflectionMethod($storage, 'getConnection');
  244. $method->setAccessible(true);
  245. $this->assertInstanceOf('\PDO', $method->invoke($storage));
  246. }
  247. public function testGetConnectionConnectsIfNeeded()
  248. {
  249. $storage = new PdoSessionHandler('sqlite::memory:');
  250. $method = new \ReflectionMethod($storage, 'getConnection');
  251. $method->setAccessible(true);
  252. $this->assertInstanceOf('\PDO', $method->invoke($storage));
  253. }
  254. /**
  255. * @dataProvider provideUrlDsnPairs
  256. */
  257. public function testUrlDsn($url, $expectedDsn, $expectedUser = null, $expectedPassword = null)
  258. {
  259. $storage = new PdoSessionHandler($url);
  260. $reflection = new \ReflectionClass(PdoSessionHandler::class);
  261. foreach (['dsn' => $expectedDsn, 'username' => $expectedUser, 'password' => $expectedPassword] as $property => $expectedValue) {
  262. if (!isset($expectedValue)) {
  263. continue;
  264. }
  265. $property = $reflection->getProperty($property);
  266. $property->setAccessible(true);
  267. $this->assertSame($expectedValue, $property->getValue($storage));
  268. }
  269. }
  270. public function provideUrlDsnPairs()
  271. {
  272. yield ['mysql://localhost/test', 'mysql:host=localhost;dbname=test;'];
  273. yield ['mysql://localhost:56/test', 'mysql:host=localhost;port=56;dbname=test;'];
  274. yield ['mysql2://root:pwd@localhost/test', 'mysql:host=localhost;dbname=test;', 'root', 'pwd'];
  275. yield ['postgres://localhost/test', 'pgsql:host=localhost;dbname=test;'];
  276. yield ['postgresql://localhost:5634/test', 'pgsql:host=localhost;port=5634;dbname=test;'];
  277. yield ['postgres://root:pwd@localhost/test', 'pgsql:host=localhost;dbname=test;', 'root', 'pwd'];
  278. yield 'sqlite relative path' => ['sqlite://localhost/tmp/test', 'sqlite:tmp/test'];
  279. yield 'sqlite absolute path' => ['sqlite://localhost//tmp/test', 'sqlite:/tmp/test'];
  280. yield 'sqlite relative path without host' => ['sqlite:///tmp/test', 'sqlite:tmp/test'];
  281. yield 'sqlite absolute path without host' => ['sqlite3:////tmp/test', 'sqlite:/tmp/test'];
  282. yield ['sqlite://localhost/:memory:', 'sqlite::memory:'];
  283. yield ['mssql://localhost/test', 'sqlsrv:server=localhost;Database=test'];
  284. yield ['mssql://localhost:56/test', 'sqlsrv:server=localhost,56;Database=test'];
  285. }
  286. /**
  287. * @return resource
  288. */
  289. private function createStream($content)
  290. {
  291. $stream = tmpfile();
  292. fwrite($stream, $content);
  293. fseek($stream, 0);
  294. return $stream;
  295. }
  296. }
  297. class MockPdo extends \PDO
  298. {
  299. public $prepareResult;
  300. private $driverName;
  301. private $errorMode;
  302. public function __construct(string $driverName = null, int $errorMode = null)
  303. {
  304. $this->driverName = $driverName;
  305. $this->errorMode = null !== $errorMode ?: \PDO::ERRMODE_EXCEPTION;
  306. }
  307. public function getAttribute($attribute)
  308. {
  309. if (\PDO::ATTR_ERRMODE === $attribute) {
  310. return $this->errorMode;
  311. }
  312. if (\PDO::ATTR_DRIVER_NAME === $attribute) {
  313. return $this->driverName;
  314. }
  315. return parent::getAttribute($attribute);
  316. }
  317. public function prepare($statement, $driverOptions = [])
  318. {
  319. return \is_callable($this->prepareResult)
  320. ? ($this->prepareResult)($statement, $driverOptions)
  321. : $this->prepareResult;
  322. }
  323. public function beginTransaction()
  324. {
  325. }
  326. public function rollBack()
  327. {
  328. }
  329. }