Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Changed

- **Test credentials are generated at run time.** The provider secrets the suite configured as
literals are now `TestCredentials::secret('<purpose>')`, so no literal a secret scanner would
report reaches the repository, and `tests/Arch/NoLiteralCredentialsTest.php` fails on any literal
added later. The `laranail/package-tools` floor is now `^0.1.10`, and a second check keeps every
purpose passed to `TestCredentials` a single plain word, since a secret scanner reads a compound
purpose as the password itself.
- The components render through `laranail/captcha::components.*`. `laranail-captcha::` still
resolves the same files.
- Docs, the README and the install command lead with the scoped tags and rule.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"laranail/console": "^0.1",
"laranail/db-tools": "^0.1",
"laranail/enumerator": "^0.1",
"laranail/package-tools": "^0.1.3",
"laranail/package-tools": "^0.1.10",
"psr/clock": "^1.0",
"psr/log": "^3.0"
},
Expand Down
17 changes: 17 additions & 0 deletions tests/Arch/NoLiteralCredentialsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

use Simtabi\Laranail\Package\Tools\Testing\AssertsNoLiteralCredentials;

uses(AssertsNoLiteralCredentials::class);

it('keeps literal credentials out of the tests', function (): void {
$this->assertNoLiteralCredentialsIn([dirname(__DIR__)], minFiles: 24);
expect(true)->toBeTrue();
});

it('names every generated credential with a single word', function (): void {
$this->assertCredentialPurposesAreWordsIn([dirname(__DIR__)], minFiles: 24);
expect(true)->toBeTrue();
});
7 changes: 4 additions & 3 deletions tests/Feature/AdapterContractTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Simtabi\Laranail\Captcha\Enums\ErrorCode;
use Illuminate\Http\Client\ConnectionException;
use Simtabi\Laranail\Captcha\Contracts\CredentialStore;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;
use Simtabi\Laranail\Captcha\ValueObjects\VerificationContext;

/**
Expand All @@ -30,7 +31,7 @@
foreach (httpProviders() as $provider) {
$credentials[$provider->value] = [
'site_key' => 'site-key',
'secret' => 'secret-key',
'secret' => TestCredentials::secret('captcha'),
'project_id' => 'demo-project',
'client' => 'demo',
];
Expand Down Expand Up @@ -115,13 +116,13 @@ function httpProviders(): array
})->with('http adapters');

it('never leaks the secret into the failure it returns', function (Provider $provider): void {
Http::fake(fn () => throw new ConnectionException('POST failed: secret=secret-key'));
Http::fake(fn () => throw new ConnectionException('POST failed: secret=' . TestCredentials::secret('captcha')));

$result = app(AdapterFactory::class)->make($provider)->verify('a-token', VerificationContext::none());

// A Guzzle RequestException stringifies the request it failed on, and that request body
// carries the secret. Reporting the exception verbatim is how a secret reaches the log.
expect(json_encode($result->toArray()))->not->toContain('secret-key');
expect(json_encode($result->toArray()))->not->toContain(TestCredentials::secret('captcha'));
})->with('http adapters');

it('passes a genuine success through', function (Provider $provider, array $body): void {
Expand Down
5 changes: 3 additions & 2 deletions tests/Feature/BrowserRuntimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Symfony\Component\Process\Process;
use Simtabi\Laranail\Captcha\Services\CaptchaService;
use Simtabi\Laranail\Captcha\Contracts\CredentialStore;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

/**
* Runs the widget in a real browser.
Expand Down Expand Up @@ -118,8 +119,8 @@ function writeManifest(array $pages): void
// Three shapes, because the runtime branches on all three: a server-rendered challenge with no
// vendor script, a hosted widget holding live state, and a provider with nothing to click.
writeFixture('math', 'math', []);
writeFixture('turnstile', 'turnstile', ['site_key' => 'site-key-abc', 'secret' => 'secret-key']);
writeFixture('recaptcha-v3', 'recaptcha-v3', ['site_key' => 'site-key-abc', 'secret' => 'secret-key']);
writeFixture('turnstile', 'turnstile', ['site_key' => 'site-key-abc', 'secret' => TestCredentials::secret('captcha')]);
writeFixture('recaptcha-v3', 'recaptcha-v3', ['site_key' => 'site-key-abc', 'secret' => TestCredentials::secret('captcha')]);

writeManifest(['math', 'turnstile', 'recaptcha-v3']);

Expand Down
9 changes: 5 additions & 4 deletions tests/Feature/CommandsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Simtabi\Laranail\Captcha\Enums\Provider;
use Simtabi\Laranail\Captcha\Models\CaptchaSetting;
use Simtabi\Laranail\Captcha\Contracts\CredentialStore;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

/**
* The commands, actually run.
Expand Down Expand Up @@ -44,7 +45,7 @@
config()->set('laranail.captcha.provider', 'turnstile');
config()->set('laranail.captcha.environments.default.turnstile', [
'site_key' => 'site-key-abcdef',
'secret' => 'a-secret',
'secret' => TestCredentials::secret('captcha'),
]);
config()->set('laranail.captcha.verification.enforce_hostname', true);
config()->set('laranail.captcha.verification.allowed_hostnames', []);
Expand All @@ -71,7 +72,7 @@
it('shows where every provider resolves its credentials from', function (): void {
config()->set('laranail.captcha.environments.default.turnstile', [
'site_key' => 'site-key-abcdef',
'secret' => 'a-secret',
'secret' => TestCredentials::secret('captcha'),
]);
app()->forgetInstance(CredentialStore::class);

Expand All @@ -83,13 +84,13 @@
it('never prints a secret', function (): void {
config()->set('laranail.captcha.environments.default.turnstile', [
'site_key' => 'site-key-abcdef',
'secret' => 'the-actual-secret',
'secret' => TestCredentials::secret('printed'),
]);
app()->forgetInstance(CredentialStore::class);

$this->artisan('laranail::captcha.keys')->assertSuccessful();

expect(app(Kernel::class)->output())->not->toContain('the-actual-secret');
expect(app(Kernel::class)->output())->not->toContain(TestCredentials::secret('printed'));
});

it('publishes the configuration', function (): void {
Expand Down
12 changes: 8 additions & 4 deletions tests/Feature/CredentialResolutionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Simtabi\Laranail\Captcha\Models\CaptchaSetting;
use Simtabi\Laranail\Captcha\Enums\CredentialSource;
use Simtabi\Laranail\Captcha\Contracts\CredentialStore;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

/**
* The resolution order the package promises: database, then config, then test keys.
Expand All @@ -19,7 +20,7 @@
config()->set('laranail.captcha.credentials.database.enabled', true);
config()->set('laranail.captcha.environments.default.turnstile', [
'site_key' => 'config-site-key',
'secret' => 'config-secret',
'secret' => TestCredentials::secret('config'),
]);

app()->forgetInstance(CredentialStore::class);
Expand All @@ -39,14 +40,17 @@ function setting(string $environment, string $key, string $value): void
// Written through the model so the encrypted cast round-trips exactly as it will in an
// application. A raw builder insert would bypass the cast and prove nothing about the read.
setting('testing', 'site_key', 'db-site-key');
setting('testing', 'secret', 'db-secret');
setting('testing', 'secret', TestCredentials::secret('database'));

$credentials = app(CredentialStore::class)->get(Provider::Turnstile, 'testing');

// The secret assertion below proves the row won only while the two secrets differ.
expect(TestCredentials::secret('database'))->not->toBe(TestCredentials::secret('config'));

// An operator changing a key in an admin UI has to beat the .env the application booted with,
// or the admin UI is decorative.
expect($credentials?->siteKey)->toBe('db-site-key')
->and($credentials?->secret)->toBe('db-secret')
->and($credentials?->secret)->toBe(TestCredentials::secret('database'))
->and($credentials?->source)->toBe(CredentialSource::Database);
});

Expand All @@ -59,7 +63,7 @@ function setting(string $environment, string $key, string $value): void

it('scopes rows to the environment they were written for', function (): void {
setting('production', 'site_key', 'prod-site-key');
setting('production', 'secret', 'prod-secret');
setting('production', 'secret', TestCredentials::secret('production'));

// A production row must not answer for a staging lookup. This is the whole reason the
// environment column is not nullable.
Expand Down
3 changes: 2 additions & 1 deletion tests/Feature/RuntimeScriptTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Symfony\Component\Process\Process;
use Simtabi\Laranail\Captcha\Services\CaptchaService;
use Simtabi\Laranail\Captcha\Contracts\CredentialStore;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

/**
* Runs the emitted JavaScript, rather than asserting it was emitted.
Expand All @@ -28,7 +29,7 @@ function extractRuntime(): string
config()->set('laranail.captcha.provider', 'turnstile');
config()->set('laranail.captcha.environments.default.turnstile', [
'site_key' => 'site-key-abc',
'secret' => 'secret-key',
'secret' => TestCredentials::secret('captcha'),
]);

app()->forgetInstance(CaptchaService::class);
Expand Down
9 changes: 5 additions & 4 deletions tests/Feature/Security/PostVerificationChecksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Simtabi\Laranail\Captcha\Enums\ErrorCode;
use Simtabi\Laranail\Captcha\Services\CaptchaService;
use Simtabi\Laranail\Captcha\Contracts\CredentialStore;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;
use Simtabi\Laranail\Captcha\ValueObjects\VerificationContext;

/**
Expand All @@ -22,7 +23,7 @@
config()->set('laranail.captcha.provider', 'turnstile');
config()->set('laranail.captcha.environments.default.turnstile', [
'site_key' => 'site-key',
'secret' => 'secret-key',
'secret' => TestCredentials::secret('captcha'),
]);

freezeClockAt('2026-08-12T12:00:00+00:00');
Expand Down Expand Up @@ -132,7 +133,7 @@ function turnstileAnswers(array $overrides = []): void
it('blocks a score below the review threshold', function (): void {
config()->set('laranail.captcha.provider', 'recaptcha-v3');
config()->set('laranail.captcha.environments.default.recaptcha-v3', [
'site_key' => 'site-key', 'secret' => 'secret-key',
'site_key' => 'site-key', 'secret' => TestCredentials::secret('captcha'),
]);
Http::fake(fn () => Http::response(['success' => true, 'score' => 0.1], 200));

Expand All @@ -144,7 +145,7 @@ function turnstileAnswers(array $overrides = []): void
it('marks a middling score for review rather than blocking it', function (): void {
config()->set('laranail.captcha.provider', 'recaptcha-v3');
config()->set('laranail.captcha.environments.default.recaptcha-v3', [
'site_key' => 'site-key', 'secret' => 'secret-key',
'site_key' => 'site-key', 'secret' => TestCredentials::secret('captcha'),
]);
Http::fake(fn () => Http::response(['success' => true, 'score' => 0.4], 200));

Expand All @@ -159,7 +160,7 @@ function turnstileAnswers(array $overrides = []): void
it('allows a high score outright', function (): void {
config()->set('laranail.captcha.provider', 'recaptcha-v3');
config()->set('laranail.captcha.environments.default.recaptcha-v3', [
'site_key' => 'site-key', 'secret' => 'secret-key',
'site_key' => 'site-key', 'secret' => TestCredentials::secret('captcha'),
]);
Http::fake(fn () => Http::response(['success' => true, 'score' => 0.9], 200));

Expand Down
3 changes: 2 additions & 1 deletion tests/Feature/WidgetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
use Simtabi\Laranail\Captcha\Enums\Provider;
use Simtabi\Laranail\Captcha\Services\CaptchaService;
use Simtabi\Laranail\Captcha\Contracts\CredentialStore;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

function useProvider(string $provider): void
{
config()->set('laranail.captcha.provider', $provider);
config()->set("laranail.captcha.environments.default.{$provider}", [
'site_key' => 'site-key-abc',
'secret' => 'secret-key',
'secret' => TestCredentials::secret('captcha'),
]);

app()->forgetInstance(CaptchaService::class);
Expand Down
Loading