SchemaTest.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. <?php
  2. namespace Test\Depo\RequestKit;
  3. use DateTime;
  4. use Depo\RequestKit\Exceptions\InvalidDataException;
  5. use Depo\RequestKit\Schema\Schema;
  6. use Depo\RequestKit\Type;
  7. use Depo\RequestKit\Utils\KeyValueObject;
  8. use Depo\UniTester\TestCase;
  9. use Psr\Http\Message\ServerRequestInterface;
  10. use Psr\Http\Message\StreamInterface;
  11. use Psr\Http\Message\UriInterface;
  12. class SchemaTest extends TestCase
  13. {
  14. private ?Schema $schema = null;
  15. protected function setUp(): void
  16. {
  17. $this->schema = Schema::create([
  18. 'name' => Type::string()->length(3, 100)->required(),
  19. 'age' => Type::int()->min(18)->max(99),
  20. 'email' => Type::email()->lowercase(),
  21. 'date_of_birth' => Type::date()->format('Y-m-d'),
  22. 'created_at' => Type::datetime()->format('Y-m-d H:i:s')->default(date('2025-01-01 12:00:00')),
  23. 'active' => Type::bool()->strict(),
  24. ]);
  25. }
  26. protected function tearDown(): void
  27. {
  28. // TODO: Implement tearDown() method.
  29. }
  30. protected function execute(): void
  31. {
  32. $this->testValidData();
  33. $this->testInvalidEmail();
  34. $this->testEdgeCaseAge();
  35. $this->testStrictBool();
  36. $this->testMissingOptionalField();
  37. $this->testMissingRequiredField();
  38. $this->testMultipleValidationErrors();
  39. $this->testNestedData();
  40. $this->testCollection();
  41. $this->testArray();
  42. $this->testExtend();
  43. $this->testExampleData();
  44. $this->testWithHeaders();
  45. $this->testProcessForm();
  46. }
  47. public function testWithHeaders()
  48. {
  49. $schema = Schema::create([
  50. 'name' => Type::string()->required(),
  51. ])->withHeaders([
  52. 'Content-Type' => Type::string()->equals('application/json'),
  53. 'X-Api-Key' => Type::string()->required()->length(10),
  54. ]);
  55. // Test case 1: Valid headers - should not throw an exception
  56. $request = $this->createServerRequest(
  57. ['Content-Type' => 'application/json', 'X-Api-Key' => '1234567890'],
  58. ['name' => 'test']
  59. );
  60. $result = $schema->processHttpRequest($request);
  61. $this->assertEquals('test', $result->get('name')); // Assert data is processed
  62. // Test case 2: Missing required header
  63. $request = $this->createServerRequest(
  64. ['Content-Type' => 'application/json'],
  65. ['name' => 'test']
  66. );
  67. $this->expectException(InvalidDataException::class, function () use ($schema, $request) {
  68. try {
  69. $schema->processHttpRequest($request);
  70. } catch (InvalidDataException $e) {
  71. $this->assertNotEmpty($e->getError('x-api-key'));
  72. throw $e;
  73. }
  74. });
  75. // Test case 3: Header constraint violation
  76. $request = $this->createServerRequest(
  77. ['Content-Type' => 'application/xml', 'X-Api-Key' => '1234567890'],
  78. ['name' => 'test']
  79. );
  80. $this->expectException(InvalidDataException::class, function () use ($schema, $request) {
  81. try {
  82. $schema->processHttpRequest($request);
  83. } catch (InvalidDataException $e) {
  84. $this->assertNotEmpty($e->getError('content-type'));
  85. throw $e;
  86. }
  87. });
  88. }
  89. public function testProcessForm()
  90. {
  91. $schema = Schema::create([
  92. 'username' => Type::string()->required(),
  93. ]);
  94. $csrfToken = 'a_very_secret_token_123';
  95. // Test case 1: Valid form with correct CSRF token - should not throw an exception
  96. $request = $this->createServerRequest(
  97. [
  98. 'Content-Type' => 'application/x-www-form-urlencoded',
  99. ],
  100. ['username' => 'john.doe', '_csrf' => $csrfToken]
  101. );
  102. $result = $schema->processFormHttpRequest($request, $csrfToken);
  103. $this->assertEquals('john.doe', $result->get('username'));
  104. // Test case 2: Valid form without CSRF check (optional) - should not throw an exception
  105. $request = $this->createServerRequest(
  106. [
  107. 'Content-Type' => 'application/x-www-form-urlencoded',
  108. ],
  109. ['username' => 'jane.doe']
  110. );
  111. $result = $schema->processFormHttpRequest($request, null); // Pass null to skip CSRF
  112. $this->assertEquals('jane.doe', $result->get('username'));
  113. // Test case 3: Form with incorrect CSRF token
  114. $request = $this->createServerRequest(
  115. [
  116. 'Content-Type' => 'application/x-www-form-urlencoded',
  117. ],
  118. ['username' => 'hacker', '_csrf' => 'wrong_token'],
  119. );
  120. $this->expectException(InvalidDataException::class, function () use ($schema, $request, $csrfToken) {
  121. try {
  122. $schema->processFormHttpRequest($request, $csrfToken);
  123. } catch (InvalidDataException $e) {
  124. $this->assertEquals('Invalid CSRF token.', $e->getMessage());
  125. throw $e;
  126. }
  127. });
  128. // Test case 4: Form with missing CSRF token
  129. $request = $this->createServerRequest(
  130. [
  131. 'Content-Type' => 'application/x-www-form-urlencoded',
  132. ],
  133. ['username' => 'hacker'],
  134. );
  135. $this->expectException(InvalidDataException::class, function () use ($schema, $request, $csrfToken) {
  136. try {
  137. $schema->processFormHttpRequest($request, $csrfToken);
  138. } catch (InvalidDataException $e) {
  139. $this->assertEquals('Invalid CSRF token.', $e->getMessage());
  140. throw $e;
  141. }
  142. });
  143. }
  144. public function testValidData(): void
  145. {
  146. $data = [
  147. 'name' => 'John Doe',
  148. 'age' => 25,
  149. 'email' => 'JOHN@EXAMPLE.COM',
  150. 'date_of_birth' => '1990-01-01',
  151. 'created_at' => '2023-01-01 12:00:00',
  152. 'active' => true,
  153. ];
  154. $result = $this->schema->process($data);
  155. $result = $result->toArray();
  156. $this->assertStrictEquals($result['name'], 'John Doe');
  157. $this->assertStrictEquals($result['age'], 25);
  158. $this->assertStrictEquals($result['email'], 'john@example.com');
  159. $this->assertStrictEquals($result['date_of_birth']->format('Y-m-d'), '1990-01-01');
  160. $this->assertStrictEquals($result['created_at']->format('Y-m-d H:i:s'), '2023-01-01 12:00:00');
  161. $this->assertStrictEquals($result['active'], true);
  162. }
  163. public function testInvalidEmail(): void
  164. {
  165. $data = [
  166. 'name' => 'John Doe',
  167. 'age' => 25,
  168. 'email' => 'invalid-email', // Email invalide
  169. 'date_of_birth' => '1990-01-01',
  170. 'created_at' => '2023-01-01 12:00:00',
  171. 'active' => true,
  172. ];
  173. $this->expectException(InvalidDataException::class, function () use ($data) {
  174. try {
  175. $result = $this->schema->process($data);
  176. } catch (InvalidDataException $e) {
  177. $this->assertNotEmpty($e->getErrors());
  178. $this->assertEquals(1, count($e->getErrors()));
  179. $this->assertNotEmpty($e->getError('email'));
  180. throw $e;
  181. }
  182. });
  183. }
  184. public function testEdgeCaseAge(): void
  185. {
  186. $data = [
  187. 'name' => 'John Doe',
  188. 'age' => '18', // Âge limite
  189. 'email' => 'john@example.com',
  190. 'date_of_birth' => '1990-01-01',
  191. 'created_at' => '2023-01-01 12:00:00',
  192. 'active' => true,
  193. ];
  194. $result = $this->schema->process($data);
  195. $result = $result->toArray();
  196. $this->assertEquals(18, $result['age']);
  197. }
  198. public function testStrictBool(): void
  199. {
  200. $data = [
  201. 'name' => 'John Doe',
  202. 'age' => 25,
  203. 'email' => 'john@example.com',
  204. 'date_of_birth' => '1990-01-01',
  205. 'created_at' => '2023-01-01 12:00:00',
  206. 'active' => 1, //
  207. ];
  208. $this->expectException(InvalidDataException::class, function () use ($data) {
  209. try {
  210. $result = $this->schema->process($data);
  211. } catch (InvalidDataException $e) {
  212. $this->assertNotEmpty($e->getErrors());
  213. $this->assertEquals(1, count($e->getErrors()));
  214. $this->assertNotEmpty($e->getError('active'));
  215. throw $e;
  216. }
  217. });
  218. }
  219. public function testMissingOptionalField(): void
  220. {
  221. $data = [
  222. 'name' => 'John Doe',
  223. 'age' => 25,
  224. 'email' => 'john@example.com',
  225. 'date_of_birth' => '1990-01-01',
  226. // 'created_at'
  227. 'active' => true,
  228. ];
  229. $result = $this->schema->process($data);
  230. $result = $result->toArray();
  231. $this->assertInstanceOf(DateTime::class, $result['created_at']);
  232. }
  233. public function testMissingRequiredField(): void
  234. {
  235. $data = [
  236. // 'name' manquant
  237. 'age' => 25,
  238. 'email' => 'john@example.com',
  239. 'date_of_birth' => '1990-01-01',
  240. 'created_at' => '2023-01-01 12:00:00',
  241. 'active' => true,
  242. ];
  243. $this->expectException(InvalidDataException::class, function () use ($data) {
  244. try {
  245. $result = $this->schema->process($data);
  246. } catch (InvalidDataException $e) {
  247. $this->assertNotEmpty($e->getErrors());
  248. $this->assertEquals(1, count($e->getErrors()));
  249. $this->assertNotEmpty($e->getError('name'));
  250. throw $e;
  251. }
  252. });
  253. }
  254. public function testMultipleValidationErrors(): void
  255. {
  256. $data = [
  257. 'name' => 'John Doe',
  258. 'age' => 17, // Âge invalide
  259. 'email' => 'invalid-email', // Email invalide
  260. 'date_of_birth' => '1990-01-01',
  261. 'created_at' => '2023-01-01 12:00:00',
  262. 'active' => true,
  263. ];
  264. $this->expectException(InvalidDataException::class, function () use ($data) {
  265. try {
  266. $result = $this->schema->process($data);
  267. } catch (InvalidDataException $e) {
  268. $this->assertNotEmpty($e->getErrors());
  269. $this->assertEquals(2, count($e->getErrors()));
  270. $this->assertNotEmpty($e->getError('age'));
  271. $this->assertNotEmpty($e->getError('email'));
  272. throw $e;
  273. }
  274. });
  275. }
  276. public function testNestedData(): void
  277. {
  278. $schema = Schema::create([
  279. 'user' => Type::item([
  280. 'name' => Type::string()->length(20, 50)->required(),
  281. 'age' => Type::int()->strict()->alias('my_age'),
  282. 'roles' => Type::arrayOf(Type::string()->strict())->required(),
  283. 'address' => Type::item([
  284. 'street' => Type::string()->length(15, 100),
  285. 'city' => Type::string()->allowed('Paris', 'London'),
  286. ]),
  287. ]),
  288. ]);
  289. $data = [
  290. 'user' => [
  291. // 'name' => 'John Doe',
  292. 'my_age' => '25',
  293. // 'roles' => [
  294. // 1,
  295. // 2,
  296. // ],
  297. 'address' => [
  298. 'street' => 'Main Street',
  299. 'city' => 'New York',
  300. ]
  301. ],
  302. ];
  303. $this->expectException(InvalidDataException::class, function () use ($schema, $data) {
  304. try {
  305. $schema->process($data);
  306. } catch (InvalidDataException $e) {
  307. $this->assertNotEmpty($e->getErrors());
  308. $this->assertEquals(5, count($e->getErrors()));
  309. $this->assertNotEmpty($e->getError('user.name'));
  310. $this->assertNotEmpty($e->getError('user.age'));
  311. $this->assertNotEmpty($e->getError('user.address.street'));
  312. $this->assertNotEmpty($e->getError('user.address.city'));
  313. $this->assertNotEmpty($e->getError('user.roles'));
  314. throw $e;
  315. }
  316. });
  317. }
  318. public function testCollection(): void
  319. {
  320. $schema = Schema::create([
  321. 'users' => Type::arrayOf(Type::item([
  322. 'name' => Type::string()->length(3, 50)->required(),
  323. 'age' => Type::int(),
  324. 'roles' => Type::arrayOf(Type::string())->required(),
  325. 'address' => Type::item([
  326. 'street' => Type::string()->length(5, 100),
  327. 'city' => Type::string()->allowed('Paris', 'London'),
  328. ]),
  329. ])),
  330. ]);
  331. $data = [
  332. 'users' => [
  333. [
  334. 'name' => 'John Doe',
  335. 'age' => '25',
  336. 'roles' => [
  337. 1,
  338. 2,
  339. ],
  340. 'address' => [
  341. 'street' => 'Main Street',
  342. 'city' => 'London',
  343. ]
  344. ],
  345. [
  346. 'name' => 'Jane Doe',
  347. 'age' => '30',
  348. 'roles' => [
  349. 3,
  350. 4,
  351. ],
  352. 'address' => [
  353. 'street' => 'Main Street',
  354. 'city' => 'Paris',
  355. ]
  356. ],
  357. ]
  358. ];
  359. $result = $schema->process($data);
  360. $this->assertStrictEquals(2, count($result->get('users')));
  361. $this->assertStrictEquals('John Doe', $result->get('users.0.name'));
  362. $this->assertStrictEquals(25, $result->get('users.0.age'));
  363. $this->assertStrictEquals(2, count($result->get('users.0.roles')));
  364. $this->assertStrictEquals('Main Street', $result->get('users.0.address.street'));
  365. $this->assertStrictEquals('London', $result->get('users.0.address.city'));
  366. $this->assertStrictEquals('Jane Doe', $result->get('users.1.name'));
  367. $this->assertStrictEquals(30, $result->get('users.1.age'));
  368. $this->assertStrictEquals(2, count($result->get('users.1.roles')));
  369. $this->assertStrictEquals('Main Street', $result->get('users.1.address.street'));
  370. $this->assertStrictEquals('Paris', $result->get('users.1.address.city'));
  371. }
  372. private function testExtend()
  373. {
  374. $schema1 = Schema::create([
  375. 'name' => Type::string()->length(20, 50)->required(),
  376. 'age' => Type::int()->strict()->alias('my_age'),
  377. 'roles' => Type::arrayOf(Type::string()->strict())->required(),
  378. 'address' => Type::item([
  379. 'street' => Type::string()->length(15, 100),
  380. 'city' => Type::string()->allowed('Paris', 'London'),
  381. ]),
  382. ]);
  383. $schema2 = $schema1->extend([
  384. 'password' => Type::string()->length(10, 100),
  385. 'address' => Type::item([
  386. 'zip' => Type::string()->length(5, 10),
  387. ]),
  388. ]);
  389. $this->assertStrictEquals(5, count($schema2->copyDefinitions()));
  390. /**
  391. * @var Type\ItemType $address
  392. */
  393. $address = $schema2->copyDefinitions()['address'];
  394. $this->assertStrictEquals(1, count($address->copyDefinitions()));
  395. }
  396. private function testExampleData()
  397. {
  398. $schema1 = Schema::create([
  399. 'name' => Type::string()->length(20, 50)->required()->example('John Doe'),
  400. 'age' => Type::int()->strict()->alias('my_age')->example(20),
  401. 'roles' => Type::arrayOf(Type::string()->strict())->required()->example(['admin']),
  402. 'address' => Type::item([
  403. 'street' => Type::string()->length(15, 100),
  404. 'city' => Type::string()->allowed('Paris', 'London'),
  405. ])->example([
  406. 'street' => 'Main Street',
  407. 'city' => 'London',
  408. ]
  409. ),
  410. ]);
  411. $this->assertEquals($schema1->generateExampleData(), [
  412. 'name' => 'John Doe',
  413. 'age' => 20,
  414. 'roles' => ['admin'],
  415. 'address' => [
  416. 'street' => 'Main Street',
  417. 'city' => 'London',
  418. ]
  419. ]);
  420. $schema2 = Schema::create([
  421. 'name' => Type::string()->length(20, 50)->required()->example('John Doe'),
  422. 'age' => Type::int()->strict()->alias('my_age')->example(20),
  423. 'roles' => Type::arrayOf(Type::string()->strict())->required()->example(['admin']),
  424. 'keysValues' => Type::arrayOf(Type::string()->strict())
  425. ->required()
  426. ->acceptStringKeys()
  427. ->example(['key' => 'value']),
  428. 'address' => Type::item([
  429. 'street' => Type::string()->length(15, 100)->example('Main Street'),
  430. 'city' => Type::string()->allowed('Paris', 'London')->example('London'),
  431. ]),
  432. ]);
  433. $this->assertEquals($schema2->generateExampleData(), [
  434. 'name' => 'John Doe',
  435. 'age' => 20,
  436. 'roles' => ['admin'],
  437. 'keysValues' => ['key' => 'value'],
  438. 'address' => [
  439. 'street' => 'Main Street',
  440. 'city' => 'London',
  441. ]
  442. ]);
  443. }
  444. private function testArray()
  445. {
  446. $schema = Schema::create([
  447. 'roles' => Type::arrayOf(Type::string()->strict())->required()->example(['admin']),
  448. 'dependencies' => Type::arrayOf(Type::string()->strict())->acceptStringKeys()
  449. ]);
  450. $data = [
  451. 'roles' => ['admin'],
  452. 'dependencies' => [
  453. 'key1' => 'value1',
  454. 'key2' => 'value2',
  455. ],
  456. ];
  457. $result = $schema->process($data);
  458. $this->assertStrictEquals('admin', $result->get('roles.0'));
  459. $this->assertStrictEquals('value1', $result->get('dependencies.key1'));
  460. $this->assertStrictEquals('value2', $result->get('dependencies.key2'));
  461. $schema = Schema::create([
  462. 'roles' => Type::arrayOf(Type::string()->strict())->required()->example('admin')->acceptCommaSeparatedValues(),
  463. ]);
  464. $data = [
  465. 'roles' => 'admin,user,manager',
  466. ];
  467. $result = $schema->process($data);
  468. $this->assertStrictEquals('admin', $result->get('roles.0'));
  469. $this->assertStrictEquals('user', $result->get('roles.1'));
  470. $this->assertStrictEquals('manager', $result->get('roles.2'));
  471. $schema = Schema::create([
  472. 'autoload.psr-4' => Type::map(Type::string()->strict()->trim())->required(),
  473. 'dependencies' => Type::map(Type::string()->strict()->trim())
  474. ]);
  475. $data = [
  476. 'autoload.psr-4' => [
  477. 'App\\' => 'app/',
  478. ],
  479. 'dependencies' => [
  480. 'key1' => 'value1',
  481. 'key2' => 'value2',
  482. ],
  483. ];
  484. $result = $schema->process($data);
  485. $this->assertInstanceOf( KeyValueObject::class, $result->get('autoload.psr-4'));
  486. $this->assertInstanceOf( KeyValueObject::class, $result->get('dependencies'));
  487. $this->assertEquals(1, count($result->get('autoload.psr-4')));
  488. $this->assertEquals(2, count($result->get('dependencies')));
  489. $schema = Schema::create([
  490. 'autoload.psr-4' => Type::map(Type::string()->strict()->trim()),
  491. 'dependencies' => Type::map(Type::string()->strict()->trim())
  492. ]);
  493. $data = [
  494. 'autoload.psr-4' => [
  495. ],
  496. ];
  497. $result = $schema->process($data);
  498. $this->assertInstanceOf( KeyValueObject::class, $result->get('autoload.psr-4'));
  499. $this->assertInstanceOf( KeyValueObject::class, $result->get('dependencies'));
  500. $this->assertEquals(0, count($result->get('autoload.psr-4')));
  501. $this->assertEquals(0, count($result->get('dependencies')));
  502. }
  503. /**
  504. * Helper to create a simple ServerRequestInterface object for tests.
  505. */
  506. private function createServerRequest(array $headers, array $body): ServerRequestInterface
  507. {
  508. return new class($headers, $body) implements ServerRequestInterface {
  509. private array $headers;
  510. private array $body;
  511. public function __construct(array $headers, array $body)
  512. {
  513. foreach ($headers as $name => $value) {
  514. $this->headers[$name][] = $value;
  515. }
  516. $this->body = $body;
  517. }
  518. public function getHeaders(): array { return $this->headers; }
  519. public function hasHeader($name): bool { return isset($this->headers[strtolower($name)]); }
  520. public function getHeader($name): array { return (array)($this->headers[strtolower($name)] ?? []); }
  521. public function getHeaderLine($name): string { return implode(', ', $this->getHeader(strtolower($name))); }
  522. public function getParsedBody() { return $this->body; }
  523. public function getBody(): StreamInterface {
  524. $stream = $this->createMock(StreamInterface::class);
  525. $stream->method('getContents')->willReturn(json_encode($this->body));
  526. return $stream;
  527. }
  528. // --- The rest of the methods are not needed for these tests ---
  529. public function getProtocolVersion(): string
  530. {}
  531. public function withProtocolVersion($version): \Psr\Http\Message\MessageInterface
  532. {}
  533. public function withHeader($name, $value): \Psr\Http\Message\MessageInterface
  534. {}
  535. public function withAddedHeader($name, $value): \Psr\Http\Message\MessageInterface
  536. {}
  537. public function withoutHeader($name): \Psr\Http\Message\MessageInterface
  538. {}
  539. public function withBody(StreamInterface $body): \Psr\Http\Message\MessageInterface
  540. {}
  541. public function getRequestTarget(): string
  542. {}
  543. public function withRequestTarget($requestTarget): \Psr\Http\Message\RequestInterface
  544. {}
  545. public function getMethod(): string
  546. {}
  547. public function withMethod($method): \Psr\Http\Message\RequestInterface
  548. {}
  549. public function getUri(): UriInterface
  550. {}
  551. public function withUri(UriInterface $uri, $preserveHost = false): \Psr\Http\Message\RequestInterface
  552. {}
  553. public function getServerParams(): array
  554. {}
  555. public function getCookieParams(): array
  556. {}
  557. public function withCookieParams(array $cookies): ServerRequestInterface
  558. {}
  559. public function getQueryParams(): array
  560. {}
  561. public function withQueryParams(array $query): ServerRequestInterface
  562. {}
  563. public function getUploadedFiles(): array
  564. {}
  565. public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
  566. {}
  567. public function getAttribute($name, $default = null){}
  568. public function withAttribute($name, $value): ServerRequestInterface
  569. {}
  570. public function withoutAttribute($name): ServerRequestInterface
  571. {}
  572. public function withParsedBody($data): ServerRequestInterface
  573. {
  574. // TODO: Implement withParsedBody() method.
  575. }
  576. public function getAttributes(): array
  577. {
  578. // TODO: Implement getAttributes() method.
  579. }
  580. };
  581. }
  582. }