diff --git a/src/boost/docs-ported.md b/src/boost/docs-ported.md
index 3cce4e454..c929e0bd5 100644
--- a/src/boost/docs-ported.md
+++ b/src/boost/docs-ported.md
@@ -8,6 +8,7 @@ cache.md
collections.md
concurrency.md
configuration.md
+console-tests.md
container.md
pagination.md
requests.md
diff --git a/src/boost/docs/console-tests.md b/src/boost/docs/console-tests.md
index 66199e45e..91132527f 100644
--- a/src/boost/docs/console-tests.md
+++ b/src/boost/docs/console-tests.md
@@ -8,7 +8,7 @@
## Introduction
-In addition to simplifying HTTP testing, Laravel provides a simple API for testing your application's [custom console commands](/docs/{{version}}/artisan).
+In addition to simplifying HTTP testing, Hypervel provides a simple API for testing your application's [custom console commands](/docs/{{version}}/artisan).
## Success / Failure Expectations
@@ -17,7 +17,7 @@ To get started, let's explore how to make assertions regarding an Artisan comman
```php tab=Pest
test('console command', function () {
- $this->artisan('inspire')->assertExitCode(0);
+ $this->artisan('about')->assertExitCode(0);
});
```
@@ -27,30 +27,34 @@ test('console command', function () {
*/
public function test_console_command(): void
{
- $this->artisan('inspire')->assertExitCode(0);
+ $this->artisan('about')->assertExitCode(0);
}
```
You may use the `assertNotExitCode` method to assert that the command did not exit with a given exit code:
```php
-$this->artisan('inspire')->assertNotExitCode(1);
+$this->artisan('about')->assertNotExitCode(1);
```
Of course, all terminal commands typically exit with a status code of `0` when they are successful and a non-zero exit code when they are not successful. Therefore, for convenience, you may utilize the `assertSuccessful` and `assertFailed` assertions to assert that a given command exited with a successful exit code or not:
```php
-$this->artisan('inspire')->assertSuccessful();
+$this->artisan('about')->assertSuccessful();
-$this->artisan('inspire')->assertFailed();
+$this->artisan('example:failing-command')->assertFailed();
```
+The `assertOk` method is also available as an alias of the `assertSuccessful` method.
+
## Input / Output Expectations
-Laravel allows you to easily "mock" user input for your console commands using the `expectsQuestion` method. In addition, you may specify the exit code and text that you expect to be output by the console command using the `assertExitCode` and `expectsOutput` methods. For example, consider the following console command:
+Hypervel allows you to easily "mock" user input for your console commands using the `expectsQuestion` method. In addition, you may specify the exit code and text that you expect to be output by the console command using the `assertExitCode` and `expectsOutput` methods. For example, consider the following console command:
```php
+use Hypervel\Support\Facades\Artisan;
+
Artisan::command('question', function () {
$name = $this->ask('What is your name?');
@@ -70,7 +74,11 @@ You may test this command with the following test:
test('console command', function () {
$this->artisan('question')
->expectsQuestion('What is your name?', 'Taylor Otwell')
- ->expectsQuestion('Which language do you prefer?', 'PHP')
+ ->expectsChoice('Which language do you prefer?', 'PHP', [
+ 'PHP',
+ 'Ruby',
+ 'Python',
+ ])
->expectsOutput('Your name is Taylor Otwell and you prefer PHP.')
->doesntExpectOutput('Your name is Taylor Otwell and you prefer Ruby.')
->assertExitCode(0);
@@ -85,14 +93,20 @@ public function test_console_command(): void
{
$this->artisan('question')
->expectsQuestion('What is your name?', 'Taylor Otwell')
- ->expectsQuestion('Which language do you prefer?', 'PHP')
+ ->expectsChoice('Which language do you prefer?', 'PHP', [
+ 'PHP',
+ 'Ruby',
+ 'Python',
+ ])
->expectsOutput('Your name is Taylor Otwell and you prefer PHP.')
->doesntExpectOutput('Your name is Taylor Otwell and you prefer Ruby.')
->assertExitCode(0);
}
```
-If you are utilizing the `search` or `multisearch` functions provided by [Laravel Prompts](/docs/{{version}}/prompts), you may use the `expectsSearch` assertion to mock the user's input, search results, and selection:
+If the order of the available choices is important, you may pass `true` as the fourth argument to the `expectsChoice` method to assert that the choices are offered in the exact order provided.
+
+If you are utilizing the `search` or `multisearch` functions provided by [Hypervel Prompts](/docs/{{version}}/prompts), you may use the `expectsSearch` assertion to mock the user's input, search results, and selection:
```php tab=Pest
test('console command', function () {
@@ -193,6 +207,29 @@ $this->artisan('users:all')
]);
```
+
+#### Prompts Output Expectations
+
+If your command displays messages or tables using [Hypervel Prompts](/docs/{{version}}/prompts), you may use the `expectsPromptsInfo`, `expectsPromptsWarning`, `expectsPromptsError`, `expectsPromptsAlert`, `expectsPromptsIntro`, `expectsPromptsOutro`, and `expectsPromptsTable` methods:
+
+```php
+$this->artisan('report:generate')
+ ->expectsPromptsInfo('Welcome to the application!')
+ ->expectsPromptsWarning('This action cannot be undone')
+ ->expectsPromptsError('Something went wrong')
+ ->expectsPromptsAlert('Important notice!')
+ ->expectsPromptsIntro('Starting process...')
+ ->expectsPromptsOutro('Process completed!')
+ ->expectsPromptsTable(
+ headers: ['Name', 'Email'],
+ rows: [
+ ['Taylor Otwell', 'taylor@example.com'],
+ ['Jason Beggs', 'jason@example.com'],
+ ]
+ )
+ ->assertExitCode(0);
+```
+
## Console Events
diff --git a/src/jwt/src/Blacklist.php b/src/jwt/src/Blacklist.php
index ae8a942bf..60a38d108 100644
--- a/src/jwt/src/Blacklist.php
+++ b/src/jwt/src/Blacklist.php
@@ -120,6 +120,10 @@ protected function getGraceTimestamp(): int
/**
* Set the grace period.
*
+ * Only call this once at boot (service provider). Blacklist is a
+ * worker-lifetime singleton; runtime mutation affects every subsequent
+ * request and races across coroutines.
+ *
* @return $this
*/
public function setGracePeriod(int $gracePeriod): static
@@ -152,6 +156,10 @@ public function getKey(array $payload): mixed
/**
* Set the unique key held within the blacklist.
*
+ * Only call this once at boot (service provider). Blacklist is a
+ * worker-lifetime singleton; runtime mutation affects every subsequent
+ * request and races across coroutines.
+ *
* @return $this
*/
public function setKey(string $key): static
@@ -164,6 +172,10 @@ public function setKey(string $key): static
/**
* Set the refresh time limit.
*
+ * Only call this once at boot (service provider). Blacklist is a
+ * worker-lifetime singleton; runtime mutation affects every subsequent
+ * request and races across coroutines.
+ *
* @return $this
*/
public function setRefreshTTL(int $ttl): static
diff --git a/src/jwt/src/Providers/Lcobucci.php b/src/jwt/src/Providers/Lcobucci.php
index abb4c01a3..2b296f3b9 100644
--- a/src/jwt/src/Providers/Lcobucci.php
+++ b/src/jwt/src/Providers/Lcobucci.php
@@ -166,6 +166,19 @@ protected function buildConfig(): Configuration
return $config;
}
+ /**
+ * Rebuild cached derived state after a configuration change.
+ *
+ * Signer is rebuilt before config because buildConfig() reads $this->signer.
+ *
+ * @throws \Hypervel\JWT\Exceptions\JWTException
+ */
+ protected function onConfigurationChanged(): void
+ {
+ $this->signer = $this->getSigner();
+ $this->config = $this->buildConfig();
+ }
+
/**
* Get the signer instance.
*
diff --git a/src/jwt/src/Providers/Provider.php b/src/jwt/src/Providers/Provider.php
index c3c269c9b..f4dc705d2 100644
--- a/src/jwt/src/Providers/Provider.php
+++ b/src/jwt/src/Providers/Provider.php
@@ -39,11 +39,16 @@ public function __construct(
/**
* Set the algorithm used to sign the token.
*
+ * Only call this once at boot (service provider). Providers are cached
+ * on the singleton JWTManager; runtime mutation affects every subsequent
+ * request and races across coroutines.
+ *
* @return $this
*/
public function setAlgo(string $algo): static
{
$this->algo = $algo;
+ $this->onConfigurationChanged();
return $this;
}
@@ -59,11 +64,16 @@ public function getAlgo(): string
/**
* Set the secret used to sign the token.
*
+ * Only call this once at boot (service provider). Providers are cached
+ * on the singleton JWTManager; runtime mutation affects every subsequent
+ * request and races across coroutines.
+ *
* @return $this
*/
public function setSecret(string $secret): static
{
$this->secret = $secret;
+ $this->onConfigurationChanged();
return $this;
}
@@ -79,11 +89,16 @@ public function getSecret(): string
/**
* Set the keys used to sign the token.
*
+ * Only call this once at boot (service provider). Providers are cached
+ * on the singleton JWTManager; runtime mutation affects every subsequent
+ * request and races across coroutines.
+ *
* @return $this
*/
public function setKeys(array $keys): static
{
$this->keys = $keys;
+ $this->onConfigurationChanged();
return $this;
}
@@ -141,4 +156,14 @@ protected function getVerificationKey(): mixed
* Determine if the algorithm is asymmetric, and thus requires a public/private key combo.
*/
abstract protected function isAsymmetric(): bool;
+
+ /**
+ * Hook fired after any of algo/secret/keys is mutated.
+ *
+ * Subclasses that cache derived state (signers, configuration objects)
+ * override this to rebuild it so the new values actually take effect.
+ */
+ protected function onConfigurationChanged(): void
+ {
+ }
}
diff --git a/tests/Integration/Routing/MethodOverrideTest.php b/tests/Integration/Routing/MethodOverrideTest.php
new file mode 100644
index 000000000..2988222a2
--- /dev/null
+++ b/tests/Integration/Routing/MethodOverrideTest.php
@@ -0,0 +1,28 @@
+ "updated-{$widget}");
+
+ $this->post('/widgets/42', ['_method' => 'PUT'])
+ ->assertOk()
+ ->assertContent('updated-42');
+ }
+
+ public function testQueryStringMethodOverrideDispatchesToPutRoute()
+ {
+ Route::put('/widgets/{widget}', fn (string $widget) => "updated-{$widget}");
+
+ $this->post('/widgets/42?_method=PUT')
+ ->assertOk()
+ ->assertContent('updated-42');
+ }
+}
diff --git a/tests/JWT/Providers/keys/id_rsa b/tests/JWT/Fixtures/keys/id_rsa
similarity index 100%
rename from tests/JWT/Providers/keys/id_rsa
rename to tests/JWT/Fixtures/keys/id_rsa
diff --git a/tests/JWT/Providers/keys/id_rsa.pub b/tests/JWT/Fixtures/keys/id_rsa.pub
similarity index 100%
rename from tests/JWT/Providers/keys/id_rsa.pub
rename to tests/JWT/Fixtures/keys/id_rsa.pub
diff --git a/tests/JWT/Fixtures/keys/id_rsa_alt b/tests/JWT/Fixtures/keys/id_rsa_alt
new file mode 100644
index 000000000..adc866589
--- /dev/null
+++ b/tests/JWT/Fixtures/keys/id_rsa_alt
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDLJsCnfubmsSOx
+JkG1m4kgMdKjtEUdpwukZr6ejWWP5dTnZJgOI+O0wWMgspNHPzhlQpBk4xg0fWnf
+oMsU4eFG178Lj1135AIPt3tBuVudZREmxTpC5H65A+J6L6iU9wNQVDW1JM+p8qQg
+v3nWi8kOOSk9xgp6nLfD6qZO83lro5wKTLhmAPpHivfwDg5B4v9DVDjXeW0cnrHN
+dcfYQi7e4DlQ5n3nyxOh7BXnc0Xk95EskWuzkuuoTqnhDE9ixYJB+6R2UhoAXfSs
+JxgQwmVLSSExqEfu/a1C4Jc0cFtIxNchVpnXsAIQb9rQUjNdkPJ9Dl9NnGDke/B8
+QN1rOpfrAgMBAAECggEAObUV596AB8sM6P8FjmmSv873V2JXec9gWsCG6HIhBgnS
++zITkXy3a7q9hmXDuWrngbgxXT83On3fq9M4rQNnI2EY11dPxUVDIqTLwgU91Y2G
+nD1NSgAvUS4d0Pilyj8KtXBeW0kMJmoTHmxV11CY+c3Z/b66bk8DUode27GE7cKn
+0plc+56arqYwEr2LqDLy2Sr7GiB57FDSHlB0gd2aDH7oPjg2KFsq1OOIGIWvVZf9
+g3g53usStoA0PK/iAnRnxbrYsF1fxFOO65EIW5A9Xu+X7UygJr4LtalBdYUsuW+H
+5+156BN9qr57LNy9upz+QVexFihM1mM8vjOmmUkZxQKBgQDvfBKGQ6f+NiP5/1Mb
+4S9VqgWLbtRR7j5n87L8sjJicWa6lqXU0pnoO2S2BAqo48uF+TEuPc3XsTKAo5e6
+ByBddNVHRRc4dk19MmomZESuSgZPVGl2lVDxlUEWiqUxm1JZD4NWl4BRBu1fUVVC
+eC080TJvu0fzvb03+65sYtSe1wKBgQDZKT4WGHrZjIMYsABkWDyI6tyGTHs4HduH
+LCTChbF3WOyI3gpyqUucT6mRNpmMj6atBNvrww12dSClyVvgahzRVFfzvrAroNT3
+86E1dY92NOd/6kmniNpAGY6ZxOxHgKDgqOQhgSRGUUE4WdOTdVdgaLSmRc5BbTub
+kgmekifRDQKBgQCpO5w7lQPZ+JhjybIJOZAhkQxqA9+2Jg2jl+sb9zDzr/9QS7TU
+OB7apV394c1Gm/LusbG3Y6VajrWJghFuPCr639z3iDNoivEnT8EDcEe4gkcDODtu
+uQWCMl7UdxVVgNUoanX9cwISQDrt6KO/XP1axNpHaqjl7WcRcKFJpm1p8QKBgQCf
+gUyKHDb33d5Y3sWa7rif9Ko/tqN2529cjM7/VgWw3M74BOd1quXPjS/Gam1EMitB
+wGTseZtE2+k7/HeQkUBTfPkRHon1sa9b5EYPpybVsywq52JsPPfZxyvXxC2so00H
+VuYhueJ0B7C4/DmMgM7KEH5H9tP1eI8kyJJqN34pbQKBgAi+QUUkTzjGsmGJcx/s
+7ulPWIZLpUlcwB8P4y7+crPJ4JprNuc+LVdAKi0H/JOGpUgkwcvsRMhNmtstZeQ9
+B0lP5Oxs4hr1CjQ7Gz5v4fFvl1T33Oa0ThyqrlZ9eT8i3TFToXU3xXA/qeqglZff
+986zyaNhxIG04t1SxmHNX7YM
+-----END PRIVATE KEY-----
diff --git a/tests/JWT/Fixtures/keys/id_rsa_alt.pub b/tests/JWT/Fixtures/keys/id_rsa_alt.pub
new file mode 100644
index 000000000..5eba34924
--- /dev/null
+++ b/tests/JWT/Fixtures/keys/id_rsa_alt.pub
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyybAp37m5rEjsSZBtZuJ
+IDHSo7RFHacLpGa+no1lj+XU52SYDiPjtMFjILKTRz84ZUKQZOMYNH1p36DLFOHh
+Rte/C49dd+QCD7d7QblbnWURJsU6QuR+uQPiei+olPcDUFQ1tSTPqfKkIL951ovJ
+DjkpPcYKepy3w+qmTvN5a6OcCky4ZgD6R4r38A4OQeL/Q1Q413ltHJ6xzXXH2EIu
+3uA5UOZ958sToewV53NF5PeRLJFrs5LrqE6p4QxPYsWCQfukdlIaAF30rCcYEMJl
+S0khMahH7v2tQuCXNHBbSMTXIVaZ17ACEG/a0FIzXZDyfQ5fTZxg5HvwfEDdazqX
+6wIDAQAB
+-----END PUBLIC KEY-----
diff --git a/tests/JWT/Providers/LcobucciTest.php b/tests/JWT/Providers/LcobucciTest.php
index fcf781558..61a75819e 100644
--- a/tests/JWT/Providers/LcobucciTest.php
+++ b/tests/JWT/Providers/LcobucciTest.php
@@ -17,6 +17,8 @@ class LcobucciTest extends TestCase
protected function setUp(): void
{
+ parent::setUp();
+
Carbon::setTestNow('2000-01-01T00:00:00.000000Z');
$this->testNowTimestamp = Carbon::now()->timestamp;
@@ -199,6 +201,63 @@ public function testShouldReturnTheKeys()
$this->assertSame($keys, $provider->getKeys());
}
+ public function testSetAlgoTakesEffectOnEncoding()
+ {
+ $payload = ['sub' => 1, 'iat' => $this->testNowTimestamp];
+
+ $provider = $this->getProvider($this->getRandomString(), Provider::ALGO_HS256);
+ $provider->setAlgo(Provider::ALGO_HS512);
+
+ $token = $provider->encode($payload);
+ $header = json_decode(base64_decode(explode('.', $token)[0]), true);
+
+ $this->assertEquals(Provider::ALGO_HS512, $header['alg']);
+ }
+
+ public function testSetSecretTakesEffectOnSigning()
+ {
+ $payload = ['sub' => 1, 'iat' => $this->testNowTimestamp];
+
+ $originalSecret = $this->getRandomString();
+ $rotatedSecret = $this->getRandomString();
+
+ $provider = $this->getProvider($originalSecret, Provider::ALGO_HS256);
+ $provider->setSecret($rotatedSecret);
+
+ $token = $provider->encode($payload);
+
+ // A fresh provider using the new secret decodes the token successfully.
+ $decoded = $this->getProvider($rotatedSecret, Provider::ALGO_HS256)->decode($token);
+ $this->assertEquals('1', $decoded['sub']);
+
+ // A fresh provider using the old secret cannot verify the signature.
+ $this->expectException(TokenInvalidException::class);
+ $this->expectExceptionMessage('Token Signature could not be verified.');
+ $this->getProvider($originalSecret, Provider::ALGO_HS256)->decode($token);
+ }
+
+ public function testSetKeysTakesEffectOnSigning()
+ {
+ $payload = ['sub' => 1, 'iat' => $this->testNowTimestamp];
+
+ $keyPair1 = ['private' => $this->getDummyPrivateKey(), 'public' => $this->getDummyPublicKey()];
+ $keyPair2 = ['private' => $this->getAltPrivateKey(), 'public' => $this->getAltPublicKey()];
+
+ $provider = $this->getProvider('does_not_matter', Provider::ALGO_RS256, $keyPair1);
+ $provider->setKeys($keyPair2);
+
+ $token = $provider->encode($payload);
+
+ // A fresh provider using key pair 2 decodes the token successfully.
+ $decoded = $this->getProvider('does_not_matter', Provider::ALGO_RS256, $keyPair2)->decode($token);
+ $this->assertEquals('1', $decoded['sub']);
+
+ // A fresh provider using key pair 1 cannot verify the signature.
+ $this->expectException(TokenInvalidException::class);
+ $this->expectExceptionMessage('Token Signature could not be verified.');
+ $this->getProvider('does_not_matter', Provider::ALGO_RS256, $keyPair1)->decode($token);
+ }
+
private function getProvider(string $secret, string $algo, array $keys = []): Lcobucci
{
return new Lcobucci($secret, $algo, $keys);
@@ -219,11 +278,21 @@ private function getRandomString(int $length = 64)
private function getDummyPrivateKey()
{
- return file_get_contents(__DIR__ . '/keys/id_rsa');
+ return file_get_contents(__DIR__ . '/../Fixtures/keys/id_rsa');
}
private function getDummyPublicKey()
{
- return file_get_contents(__DIR__ . '/keys/id_rsa.pub');
+ return file_get_contents(__DIR__ . '/../Fixtures/keys/id_rsa.pub');
+ }
+
+ private function getAltPrivateKey()
+ {
+ return file_get_contents(__DIR__ . '/../Fixtures/keys/id_rsa_alt');
+ }
+
+ private function getAltPublicKey()
+ {
+ return file_get_contents(__DIR__ . '/../Fixtures/keys/id_rsa_alt.pub');
}
}
diff --git a/tests/JWT/Providers/ProviderTest.php b/tests/JWT/Providers/ProviderTest.php
index 51bd172ed..020bdf79a 100644
--- a/tests/JWT/Providers/ProviderTest.php
+++ b/tests/JWT/Providers/ProviderTest.php
@@ -28,4 +28,13 @@ public function testSetTheSecret()
$this->assertSame('foo', $provider->getSecret());
}
+
+ public function testSetTheKeys()
+ {
+ $provider = new ProviderStub('secret', 'HS256', []);
+
+ $provider->setKeys($keys = ['private' => 'priv', 'public' => 'pub']);
+
+ $this->assertSame($keys, $provider->getKeys());
+ }
}