create-prisma-php-app 5.1.0-alpha.23 → 5.1.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Tests;
6
+
7
+ use Lib\Websocket\Socket;
8
+ use Lib\Websocket\SocketPool;
9
+ use PHPUnit\Framework\TestCase;
10
+ use Tests\Support\FakeConnection;
11
+ use Tests\Support\RequiresFeature;
12
+
13
+ /**
14
+ * A broadcast pool: one value to everyone, with gone connections pruned on
15
+ * the way.
16
+ */
17
+ final class SocketPoolTest extends TestCase
18
+ {
19
+ use RequiresFeature;
20
+
21
+ protected function setUp(): void
22
+ {
23
+ $this->requireFeature('websocket');
24
+ }
25
+
26
+ public function testBroadcastReachesEveryOpenSocket(): void
27
+ {
28
+ $pool = new SocketPool();
29
+ $connA = new FakeConnection();
30
+ $connB = new FakeConnection();
31
+
32
+ $pool->add(new Socket($connA, 'chat'));
33
+ $pool->add(new Socket($connB, 'chat'));
34
+
35
+ $pool->broadcast(['text' => 'hello room']);
36
+
37
+ self::assertSame([['text' => 'hello room']], $connA->sentFrames);
38
+ self::assertSame([['text' => 'hello room']], $connB->sentFrames);
39
+ }
40
+
41
+ public function testClosedSocketsArePrunedByBroadcast(): void
42
+ {
43
+ $pool = new SocketPool();
44
+ $connA = new FakeConnection();
45
+ $gone = new Socket(new FakeConnection(), 'chat');
46
+
47
+ $pool->add(new Socket($connA, 'chat'));
48
+ $pool->add($gone);
49
+ $gone->handleClose();
50
+
51
+ $pool->broadcast('first');
52
+
53
+ self::assertSame(1, $pool->count(), 'the closed connection was forgotten');
54
+ self::assertSame(['first'], $connA->sentFrames);
55
+ }
56
+
57
+ public function testDiscardRemovesOnlyThatSocket(): void
58
+ {
59
+ $pool = new SocketPool();
60
+ $keep = new Socket(new FakeConnection(), 'chat');
61
+ $drop = new Socket(new FakeConnection(), 'chat');
62
+
63
+ $pool->add($keep);
64
+ $pool->add($drop);
65
+ $pool->discard($drop);
66
+
67
+ self::assertSame(1, $pool->count());
68
+ }
69
+ }
@@ -0,0 +1,77 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Tests;
6
+
7
+ use InvalidArgumentException;
8
+ use Lib\Websocket\SocketRegistry;
9
+ use PHPUnit\Framework\TestCase;
10
+ use ReflectionClass;
11
+ use Tests\Support\RequiresFeature;
12
+
13
+ /**
14
+ * The named-socket registry: names are the whole address, so they are unique
15
+ * application-wide and a duplicate is refused at registration time.
16
+ */
17
+ final class SocketRegistryTest extends TestCase
18
+ {
19
+ use RequiresFeature;
20
+
21
+ protected function setUp(): void
22
+ {
23
+ $this->requireFeature('websocket');
24
+ self::resetRegistry();
25
+ }
26
+
27
+ protected function tearDown(): void
28
+ {
29
+ self::resetRegistry();
30
+ }
31
+
32
+ public static function resetRegistry(): void
33
+ {
34
+ // tearDown() still runs after a feature skip, and in an app without
35
+ // the websocket scaffold the class does not exist — nothing to reset.
36
+ if (!class_exists(SocketRegistry::class)) {
37
+ return;
38
+ }
39
+
40
+ $reflection = new ReflectionClass(SocketRegistry::class);
41
+ $reflection->getProperty('sockets')->setValue(null, []);
42
+ }
43
+
44
+ public function testRegisterAndResolveByName(): void
45
+ {
46
+ $handler = static function (): void {};
47
+
48
+ SocketRegistry::register('chat', $handler, requireAuth: true, allowedRoles: ['admin']);
49
+
50
+ $entry = SocketRegistry::get('chat');
51
+ self::assertNotNull($entry);
52
+ self::assertSame('chat', $entry['name']);
53
+ self::assertSame($handler, $entry['handler']);
54
+ self::assertTrue($entry['requireAuth']);
55
+ self::assertSame(['admin'], $entry['allowedRoles']);
56
+ self::assertSame(['chat'], SocketRegistry::names());
57
+ }
58
+
59
+ public function testUnknownNameResolvesToNull(): void
60
+ {
61
+ self::assertNull(SocketRegistry::get('nope'));
62
+ }
63
+
64
+ public function testDuplicateNameIsRefused(): void
65
+ {
66
+ SocketRegistry::register('chat', static function (): void {});
67
+
68
+ $this->expectException(InvalidArgumentException::class);
69
+ SocketRegistry::register('chat', static function (): void {});
70
+ }
71
+
72
+ public function testEmptyNameIsRefused(): void
73
+ {
74
+ $this->expectException(InvalidArgumentException::class);
75
+ SocketRegistry::register('', static function (): void {});
76
+ }
77
+ }
@@ -0,0 +1,124 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Tests;
6
+
7
+ use InvalidArgumentException;
8
+ use Lib\Websocket\Socket;
9
+ use PHPUnit\Framework\TestCase;
10
+ use Tests\Support\FakeConnection;
11
+ use Tests\Support\RequiresFeature;
12
+
13
+ /**
14
+ * One open connection, as a socket handler holds it: JSON frames out, the
15
+ * reserved `{"error": ...}` shape refused, and close semantics.
16
+ */
17
+ final class SocketTest extends TestCase
18
+ {
19
+ use RequiresFeature;
20
+
21
+ protected function setUp(): void
22
+ {
23
+ $this->requireFeature('websocket');
24
+ }
25
+
26
+ public function testSendEncodesOneJsonValuePerFrame(): void
27
+ {
28
+ $conn = new FakeConnection();
29
+ $socket = new Socket($conn, 'echo');
30
+
31
+ self::assertTrue($socket->send(['text' => 'hello']));
32
+ self::assertTrue($socket->send('plain'));
33
+ self::assertTrue($socket->send(42));
34
+
35
+ self::assertSame([['text' => 'hello'], 'plain', 42], $conn->sentFrames);
36
+ }
37
+
38
+ public function testSendRefusesTheReservedErrorShape(): void
39
+ {
40
+ $socket = new Socket(new FakeConnection(), 'echo');
41
+
42
+ $this->expectException(InvalidArgumentException::class);
43
+ $socket->send(['error' => 'looks like a failure frame']);
44
+ }
45
+
46
+ public function testAPayloadThatMerelyContainsAnErrorKeyIsSendable(): void
47
+ {
48
+ $conn = new FakeConnection();
49
+ $socket = new Socket($conn, 'echo');
50
+
51
+ self::assertTrue($socket->send(['error' => 'mine', 'other' => 1]));
52
+ self::assertSame(['error' => 'mine', 'other' => 1], $conn->lastFrame());
53
+ }
54
+
55
+ public function testSendAfterCloseReturnsFalse(): void
56
+ {
57
+ $conn = new FakeConnection();
58
+ $socket = new Socket($conn, 'echo');
59
+
60
+ $socket->handleClose();
61
+
62
+ self::assertFalse($socket->send('late'));
63
+ self::assertSame([], $conn->sentFrames);
64
+ }
65
+
66
+ public function testMessageListenerReceivesDecodedValues(): void
67
+ {
68
+ $socket = new Socket(new FakeConnection(), 'echo');
69
+
70
+ $seen = [];
71
+ $socket->onMessage(function (mixed $value) use (&$seen): void {
72
+ $seen[] = $value;
73
+ });
74
+
75
+ $socket->handleMessage(['text' => 'hi']);
76
+ $socket->handleMessage('plain');
77
+
78
+ self::assertSame([['text' => 'hi'], 'plain'], $seen);
79
+ }
80
+
81
+ public function testCloseListenerFiresExactlyOnce(): void
82
+ {
83
+ $socket = new Socket(new FakeConnection(), 'echo');
84
+
85
+ $closes = 0;
86
+ $socket->onClose(function () use (&$closes): void {
87
+ $closes++;
88
+ });
89
+
90
+ $socket->handleClose();
91
+ $socket->handleClose();
92
+
93
+ self::assertSame(1, $closes);
94
+ self::assertFalse($socket->isOpen());
95
+ }
96
+
97
+ public function testErrorSendsTheReservedFrameThenCloses(): void
98
+ {
99
+ $conn = new FakeConnection();
100
+ $socket = new Socket($conn, 'echo');
101
+
102
+ $socket->error('no such room');
103
+
104
+ self::assertSame(['error' => 'no such room'], $conn->lastFrame());
105
+ self::assertSame([1008], $conn->closeCodes);
106
+ }
107
+
108
+ public function testCloseUsesNormalClosure(): void
109
+ {
110
+ $conn = new FakeConnection();
111
+ $socket = new Socket($conn, 'echo');
112
+
113
+ $socket->close();
114
+
115
+ self::assertSame([1000], $conn->closeCodes);
116
+ }
117
+
118
+ public function testPayloadIsHandedToTheHandler(): void
119
+ {
120
+ $socket = new Socket(new FakeConnection(), 'echo', ['role' => 'admin']);
121
+
122
+ self::assertSame(['role' => 'admin'], $socket->payload());
123
+ }
124
+ }
@@ -0,0 +1,40 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Tests;
6
+
7
+ use Lib\Websocket\SocketRegistry;
8
+ use PHPUnit\Framework\TestCase;
9
+ use Tests\Support\RequiresFeature;
10
+
11
+ /**
12
+ * The app's real registration file loads cleanly and registers every socket
13
+ * the frontend connects to. A socket the browser names but the server never
14
+ * registered is a refused connection in production — catch it here instead.
15
+ */
16
+ final class SocketsRegistrationTest extends TestCase
17
+ {
18
+ use RequiresFeature;
19
+
20
+ protected function setUp(): void
21
+ {
22
+ $this->requireFeature('websocket');
23
+ SocketRegistryTest::resetRegistry();
24
+ }
25
+
26
+ protected function tearDown(): void
27
+ {
28
+ SocketRegistryTest::resetRegistry();
29
+ }
30
+
31
+ public function testTheRegistrationFileLoadsAndNamesItsSockets(): void
32
+ {
33
+ require DOCUMENT_PATH . '/src/Lib/Websocket/sockets.php';
34
+
35
+ $names = SocketRegistry::names();
36
+
37
+ self::assertContains('echo', $names);
38
+ self::assertContains('chat', $names);
39
+ }
40
+ }
@@ -0,0 +1,62 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Tests\Support;
6
+
7
+ use Ratchet\ConnectionInterface;
8
+ use GuzzleHttp\Psr7\Request as Psr7Request;
9
+
10
+ /**
11
+ * A Ratchet connection stand-in that records what the server sends.
12
+ *
13
+ * The named-socket wire is frames and closes, so a test only needs to see
14
+ * both: `$sentFrames` collects every `send(...)` payload (decoded from JSON
15
+ * when possible) and `$closeCodes` collects every `close(...)` code. The
16
+ * `httpRequest` property mirrors how Ratchet exposes the handshake request.
17
+ */
18
+ final class FakeConnection implements ConnectionInterface
19
+ {
20
+ /** @var list<mixed> Decoded JSON frames (raw string when not JSON). */
21
+ public array $sentFrames = [];
22
+
23
+ /** @var list<string> Raw frames exactly as sent. */
24
+ public array $rawFrames = [];
25
+
26
+ /** @var list<int> */
27
+ public array $closeCodes = [];
28
+
29
+ public bool $isClosed = false;
30
+
31
+ public ?Psr7Request $httpRequest = null;
32
+
33
+ /**
34
+ * @param array<string, string> $headers Handshake headers (e.g. Origin, Cookie).
35
+ */
36
+ public function __construct(string $uri = '/__pulsepoint/ws', array $headers = [])
37
+ {
38
+ $this->httpRequest = new Psr7Request('GET', $uri, $headers);
39
+ }
40
+
41
+ public function send($data): ConnectionInterface
42
+ {
43
+ $this->rawFrames[] = (string) $data;
44
+
45
+ $decoded = json_decode((string) $data, true);
46
+ $this->sentFrames[] = json_last_error() === JSON_ERROR_NONE ? $decoded : (string) $data;
47
+
48
+ return $this;
49
+ }
50
+
51
+ public function close($code = 1000): void
52
+ {
53
+ $this->closeCodes[] = (int) $code;
54
+ $this->isClosed = true;
55
+ }
56
+
57
+ /** The last frame the server sent, decoded. */
58
+ public function lastFrame(): mixed
59
+ {
60
+ return $this->sentFrames === [] ? null : end($this->sentFrames);
61
+ }
62
+ }
@@ -0,0 +1,38 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Tests\Support;
6
+
7
+ /**
8
+ * Feature flags for the test suite, read from `prisma-php.json`.
9
+ *
10
+ * `prisma-php.json` is the single source of truth for which optional Prisma
11
+ * PHP features are enabled in the current app (`websocket`, `mcp`,
12
+ * `swaggerDocs`, `prisma`, `tailwindcss`, `typescript`, `backendOnly`).
13
+ * Tests that exercise an optional feature's scaffold must guard on the flag —
14
+ * when the feature is off, its files do not exist and the test must skip,
15
+ * not fatal.
16
+ */
17
+ final class Features
18
+ {
19
+ /** @var array<string, mixed>|null */
20
+ private static ?array $config = null;
21
+
22
+ /** @return array<string, mixed> The parsed prisma-php.json. */
23
+ public static function config(): array
24
+ {
25
+ if (self::$config === null) {
26
+ $raw = @file_get_contents(DOCUMENT_PATH . '/prisma-php.json');
27
+ $decoded = is_string($raw) ? json_decode($raw, true) : null;
28
+ self::$config = is_array($decoded) ? $decoded : [];
29
+ }
30
+
31
+ return self::$config;
32
+ }
33
+
34
+ public static function enabled(string $feature): bool
35
+ {
36
+ return (self::config()[$feature] ?? false) === true;
37
+ }
38
+ }
@@ -0,0 +1,26 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Tests\Support;
6
+
7
+ /**
8
+ * Skip guard for tests of optional Prisma PHP features.
9
+ *
10
+ * Call `$this->requireFeature('websocket')` as the FIRST line of `setUp()`,
11
+ * before touching any class from the feature's scaffold — a disabled
12
+ * feature's files are not generated, so reaching them would fatal instead
13
+ * of failing cleanly. A skipped test reports why and how to enable it.
14
+ */
15
+ trait RequiresFeature
16
+ {
17
+ protected function requireFeature(string $feature): void
18
+ {
19
+ if (!Features::enabled($feature)) {
20
+ self::markTestSkipped(
21
+ "The `$feature` feature is disabled in prisma-php.json; "
22
+ . "enable it and run `npx pp update project -y` to use this suite."
23
+ );
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,44 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * Shared PHPUnit setup for the app-level test suite.
7
+ *
8
+ * Mirrors what the runtime gives app code — the composer autoloader and the
9
+ * path constants from `settings/paths.php` — without booting the framework:
10
+ * `bootstrap.php` (route resolution, CSRF cookie, rendering) is NOT loaded
11
+ * here, so tests exercise app classes directly.
12
+ *
13
+ * The real `.env` is deliberately NOT loaded. Tests run against deterministic
14
+ * environment defaults so the suite behaves the same on any machine and can
15
+ * never depend on (or leak) real secrets. Each default is only applied when
16
+ * the variable is not already set, so CI can still override.
17
+ */
18
+
19
+ require_once dirname(__DIR__) . '/vendor/autoload.php';
20
+ require_once dirname(__DIR__) . '/settings/paths.php';
21
+
22
+ // Keep the app in its non-production code paths during tests (dev origin
23
+ // relaxations, readable error messages).
24
+ if (getenv('APP_ENV') === false) {
25
+ putenv('APP_ENV=development');
26
+ $_ENV['APP_ENV'] = 'development';
27
+ }
28
+
29
+ // Deterministic secrets so Auth and Csrf construct without a real .env.
30
+ // HS256 requires at least 32 bytes of key material.
31
+ if (getenv('AUTH_SECRET') === false) {
32
+ putenv('AUTH_SECRET=test-secret-not-for-production-32bytes');
33
+ $_ENV['AUTH_SECRET'] = 'test-secret-not-for-production-32bytes';
34
+ }
35
+
36
+ if (getenv('FUNCTION_CALL_SECRET') === false) {
37
+ putenv('FUNCTION_CALL_SECRET=test-function-call-secret');
38
+ $_ENV['FUNCTION_CALL_SECRET'] = 'test-function-call-secret';
39
+ }
40
+
41
+ // Auth::signIn writes the session payload; give CLI tests a session store.
42
+ if (!isset($_SESSION)) {
43
+ $_SESSION = [];
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-prisma-php-app",
3
- "version": "5.1.0-alpha.23",
3
+ "version": "5.1.0-alpha.24",
4
4
  "description": "Prisma-PHP: A Revolutionary Library Bridging PHP with Prisma ORM",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",