create-prisma-php-app 5.1.0-alpha.22 → 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.
- package/README.md +23 -2
- package/dist/.github/copilot-instructions.md +21 -1
- package/dist/AGENTS.md +36 -20
- package/dist/bootstrap.php +205 -187
- package/dist/index.js +1 -1
- package/dist/settings/bs-config.ts +44 -1
- package/dist/settings/run-tests.ts +35 -0
- package/dist/src/Lib/Auth/Auth.php +12 -25
- package/dist/src/Lib/Websocket/ConnectionManager.php +500 -47
- package/dist/src/Lib/Websocket/Socket.php +170 -0
- package/dist/src/Lib/Websocket/SocketPool.php +50 -0
- package/dist/src/Lib/Websocket/SocketRegistry.php +88 -0
- package/dist/src/Lib/Websocket/sockets.php +50 -0
- package/dist/src/Lib/Websocket/websocket-server.php +10 -3
- package/dist/tests/AuthTest.php +59 -0
- package/dist/tests/ConnectionManagerTest.php +277 -0
- package/dist/tests/CsrfTest.php +119 -0
- package/dist/tests/FeaturesTest.php +41 -0
- package/dist/tests/README.md +118 -0
- package/dist/tests/RpcWireContractTest.php +101 -0
- package/dist/tests/SocketPoolTest.php +69 -0
- package/dist/tests/SocketRegistryTest.php +77 -0
- package/dist/tests/SocketTest.php +124 -0
- package/dist/tests/SocketsRegistrationTest.php +40 -0
- package/dist/tests/Support/FakeConnection.php +62 -0
- package/dist/tests/Support/Features.php +38 -0
- package/dist/tests/Support/RequiresFeature.php +26 -0
- package/dist/tests/bootstrap.php +44 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|