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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to `laranail/toolkit` are documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- Test credentials are generated at run time with `TestCredentials` instead of being written into
the tests, and `tests/Architecture/NoLiteralCredentialsTest.php` fails the suite on any literal
credential left behind, so secret scanning has nothing to report. A second test there keeps
every generated credential's purpose a single plain word, because secret scanning reads a
compound purpose as the password itself. The `laranail/package-tools` floor is now `^0.1.10`,
the first release shipping that check.

## [0.3.1] - 2026-10-12

### Changed
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"phpunit/phpunit": "^11.0",
"rector/rector": "^2.5.8",
"squizlabs/php_codesniffer": "^3.10 || ^4.0",
"laranail/package-tools": "^0.1.3"
"laranail/package-tools": "^0.1.10"
},
"minimum-stability": "dev",
"prefer-stable": true,
Expand Down
3 changes: 3 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
<testsuite name="Regression">
<directory>tests/Regression</directory>
</testsuite>
<testsuite name="Architecture">
<directory>tests/Architecture</directory>
</testsuite>
</testsuites>
<source>
<include>
Expand Down
23 changes: 23 additions & 0 deletions tests/Architecture/NoLiteralCredentialsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Simtabi\Laranail\Toolkit\Tests\Architecture;

use PHPUnit\Framework\TestCase;
use Simtabi\Laranail\Package\Tools\Testing\AssertsNoLiteralCredentials;

final class NoLiteralCredentialsTest extends TestCase
{
use AssertsNoLiteralCredentials;

public function test_it_keeps_literal_credentials_out_of_the_tests(): void
{
$this->assertNoLiteralCredentialsIn([dirname(__DIR__)], minFiles: 114);
}

public function test_it_names_every_generated_credential_with_a_single_word(): void
{
$this->assertCredentialPurposesAreWordsIn([dirname(__DIR__)], minFiles: 114);
}
}
13 changes: 7 additions & 6 deletions tests/Feature/Http/AccessLogMiddlewareTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Http\Response;
use PHPUnit\Framework\Attributes\Group;
use Simtabi\Laranail\Toolkit\Tests\TestCase;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;
use Simtabi\Laranail\Toolkit\Modules\Security\SecurityData;
use Simtabi\Laranail\Toolkit\Modules\Security\AccessLog\AccessLog;
use Simtabi\Laranail\Toolkit\Modules\Security\AccessLog\AccessLogMiddleware;
Expand All @@ -19,9 +20,9 @@ public function test_sensitive_fields_are_redacted(): void
{
$request = Request::create('/login', 'POST', [
'email' => 'user@example.com',
'password' => 'super-secret',
'token' => 'abc123',
'profile' => ['api_key' => 'nested-secret', 'name' => 'Jane'],
'password' => TestCredentials::password('login'),
'token' => TestCredentials::secret('token'),
'profile' => ['api_key' => TestCredentials::secret('nested'), 'name' => 'Jane'],
]);

$this->logRequest($request);
Expand All @@ -40,7 +41,7 @@ public function test_deeply_nested_secrets_are_redacted_recursively(): void
$request = Request::create('/deep', 'POST', [
'wrapper' => [
'inner' => [
'password' => 'deep-secret',
'password' => TestCredentials::password('deep'),
'safe' => 'keep',
],
],
Expand Down Expand Up @@ -84,7 +85,7 @@ public function test_config_override_replaces_the_default_deny_list(): void

$request = Request::create('/override', 'POST', [
'custom_field' => 'hide-me',
'password' => 'no-longer-in-deny-list',
'password' => TestCredentials::password('verbatim'),
]);

$this->logRequest($request);
Expand All @@ -93,7 +94,7 @@ public function test_config_override_replaces_the_default_deny_list(): void

$this->assertSame('[REDACTED]', $log->request_data['custom_field']);
// password is not in the overriding list, so it is stored verbatim.
$this->assertSame('no-longer-in-deny-list', $log->request_data['password']);
$this->assertSame(TestCredentials::password('verbatim'), $log->request_data['password']);
}

public function test_query_string_secrets_are_not_stored_in_url(): void
Expand Down
3 changes: 2 additions & 1 deletion tests/Feature/Http/CrudControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Database\Schema\Blueprint;
use Simtabi\Laranail\Toolkit\Tests\TestCase;
use Illuminate\Validation\ValidationException;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;
use Simtabi\Laranail\Toolkit\Http\Controllers\CrudController;

class CrudWidget extends Model
Expand Down Expand Up @@ -71,7 +72,7 @@ public function test_store_does_not_mass_assign_fields_outside_fillable(): void
$controller = new CrudWidgetController;

$response = $controller->storeRecord(
Request::create('/', 'POST', ['name' => 'Acme', 'secret' => 'pwned']),
Request::create('/', 'POST', ['name' => 'Acme', 'secret' => TestCredentials::secret('guarded')]),
);

$id = $response->getData(true)['data']['id'];
Expand Down
5 changes: 3 additions & 2 deletions tests/Feature/Traits/AuditableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Illuminate\Database\Schema\Blueprint;
use Simtabi\Laranail\Toolkit\Tests\TestCase;
use Simtabi\Laranail\Toolkit\Traits\Auditable;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

class AuditWidget extends Model
{
Expand Down Expand Up @@ -47,14 +48,14 @@ protected function tearDown(): void

public function test_create_audit_redacts_hidden_attributes(): void
{
AuditWidget::create(['name' => 'Acme', 'secret' => 'top-secret']);
AuditWidget::create(['name' => 'Acme', 'secret' => TestCredentials::secret('hidden')]);

$audit = DB::table('model_audits')->where('event', 'created')->first();
$newValues = json_decode((string) $audit->new_values, true);

$this->assertSame('Acme', $newValues['name']);
$this->assertSame('[REDACTED]', $newValues['secret']);
$this->assertStringNotContainsString('top-secret', $audit->new_values);
$this->assertStringNotContainsString(TestCredentials::secret('hidden'), $audit->new_values);
}

public function test_audit_failure_does_not_break_the_model_write(): void
Expand Down
7 changes: 4 additions & 3 deletions tests/Unit/Modules/FacadesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Simtabi\Laranail\Toolkit\Facades\Laranail;
use Simtabi\Laranail\Toolkit\Services\ModelService;
use Simtabi\Laranail\Toolkit\Support\RuntimeConfigurator;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;
use Simtabi\Laranail\Toolkit\Modules\LLM\LLMProviderInterface;
use Simtabi\Laranail\Toolkit\Modules\LLM\Claude\ClaudeProvider;
use Simtabi\Laranail\Toolkit\Modules\LLM\Gemini\GeminiProvider;
Expand Down Expand Up @@ -112,7 +113,7 @@ public function test_llm_binding_selects_openai_by_default(): void
{
$this->app->forgetInstance(LLMProviderInterface::class);
$this->app['config']->set('laranail.toolkit.llm.default_provider', 'openai');
$this->app['config']->set('laranail.toolkit.llm.openai.api_key', 'sk-test');
$this->app['config']->set('laranail.toolkit.llm.openai.api_key', sprintf('sk-%s', TestCredentials::secret('openai')));

$this->assertInstanceOf(OpenAIProvider::class, $this->app->make(LLMProviderInterface::class));
}
Expand All @@ -121,7 +122,7 @@ public function test_llm_binding_selects_gemini_when_configured(): void
{
$this->app->forgetInstance(LLMProviderInterface::class);
$this->app['config']->set('laranail.toolkit.llm.default_provider', 'gemini');
$this->app['config']->set('laranail.toolkit.llm.gemini.api_key', 'gemini-test');
$this->app['config']->set('laranail.toolkit.llm.gemini.api_key', TestCredentials::secret('gemini'));

$this->assertInstanceOf(GeminiProvider::class, $this->app->make(LLMProviderInterface::class));
}
Expand All @@ -130,7 +131,7 @@ public function test_llm_binding_selects_claude_when_configured(): void
{
$this->app->forgetInstance(LLMProviderInterface::class);
$this->app['config']->set('laranail.toolkit.llm.default_provider', 'claude');
$this->app['config']->set('laranail.toolkit.llm.claude.api_key', 'claude-test');
$this->app['config']->set('laranail.toolkit.llm.claude.api_key', TestCredentials::secret('claude'));

$this->assertInstanceOf(ClaudeProvider::class, $this->app->make(LLMProviderInterface::class));
}
Expand Down
17 changes: 9 additions & 8 deletions tests/Unit/Modules/Security/TokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPUnit\Framework\Attributes\Group;
use Simtabi\Laranail\Toolkit\Tests\TestCase;
use Simtabi\Laranail\Toolkit\Modules\Security\Token;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;

class TokenTest extends TestCase
{
Expand Down Expand Up @@ -62,7 +63,7 @@ public function test_prefix_is_prepended(): void

public function test_signed_round_trip_verifies_true(): void
{
$builder = Token::signed('s3cr3t')->encoding('hex')->length(32);
$builder = Token::signed(TestCredentials::secret('signing'))->encoding('hex')->length(32);

$token = $builder->generate();

Expand All @@ -71,7 +72,7 @@ public function test_signed_round_trip_verifies_true(): void

public function test_signed_token_with_prefix_and_type_round_trips(): void
{
$builder = Token::signed('s3cr3t')->prefix('rt_')->type('reset')->encoding('base64url')->length(32);
$builder = Token::signed(TestCredentials::secret('signing'))->prefix('rt_')->type('reset')->encoding('base64url')->length(32);

$token = $builder->generate();

Expand All @@ -81,7 +82,7 @@ public function test_signed_token_with_prefix_and_type_round_trips(): void

public function test_tampered_token_verifies_false(): void
{
$builder = Token::signed('s3cr3t')->encoding('hex')->length(32);
$builder = Token::signed(TestCredentials::secret('signing'))->encoding('hex')->length(32);
$token = $builder->generate();

$this->assertFalse($builder->verify($token . 'x'));
Expand Down Expand Up @@ -162,7 +163,7 @@ public function test_builder_is_immutable(): void
#[Group('security')]
public function test_non_expired_signed_token_verifies_true(): void
{
$builder = Token::signed('s3cr3t')->expiresIn(3600)->encoding('hex')->length(16);
$builder = Token::signed(TestCredentials::secret('signing'))->expiresIn(3600)->encoding('hex')->length(16);
$token = $builder->generate();

$this->assertTrue($builder->verify($token));
Expand All @@ -171,7 +172,7 @@ public function test_non_expired_signed_token_verifies_true(): void
#[Group('security')]
public function test_expired_token_verifies_false(): void
{
$secret = 's3cr3t';
$secret = TestCredentials::secret('signing');
$builder = Token::signed($secret)->expiresIn(60)->encoding('hex')->length(16);

// Hand-build a structurally valid but already-expired token using the SAME
Expand All @@ -189,7 +190,7 @@ public function test_expired_token_verifies_false(): void
#[Group('security')]
public function test_tampered_type_is_rejected_by_the_signature(): void
{
$builder = Token::signed('s3cr3t')->type('reset')->encoding('hex')->length(16);
$builder = Token::signed(TestCredentials::secret('signing'))->type('reset')->encoding('hex')->length(16);
$token = $builder->generate();

// Swap the type segment to 'verify' while keeping the original MAC.
Expand All @@ -202,7 +203,7 @@ public function test_tampered_type_is_rejected_by_the_signature(): void
#[Group('security')]
public function test_a_validly_signed_token_of_another_type_is_rejected(): void
{
$secret = 's3cr3t';
$secret = TestCredentials::secret('signing');

// A genuinely-signed token minted for the 'verify' purpose.
$verifyToken = Token::signed($secret)->type('verify')->encoding('hex')->length(16)->generate();
Expand All @@ -218,7 +219,7 @@ public function test_a_validly_signed_token_of_another_type_is_rejected(): void
#[Group('security')]
public function test_embedded_expiry_is_enforced_even_when_the_verifier_omits_expires_in(): void
{
$secret = 's3cr3t';
$secret = TestCredentials::secret('signing');

// Hand-build an already-expired but validly-signed token.
$body = bin2hex(random_bytes(16));
Expand Down
6 changes: 4 additions & 2 deletions tests/Unit/Rules/RejectCommonPasswordsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Contracts\Validation\ValidationRule;
use Simtabi\Laranail\Toolkit\Rules\RejectCommonPasswords;
use Simtabi\Laranail\Package\Tools\Testing\TestCredentials;
use Illuminate\Support\Facades\Validator as ValidatorFacade;

#[Group('security')]
Expand Down Expand Up @@ -284,7 +285,7 @@ public function test_hibp_fails_open_on_transport_error(): void

public function test_hibp_rejects_a_breached_password_when_suffix_matches(): void
{
$password = 'P@ssw0rd-breached-fixture';
$password = TestCredentials::password('breached');
$hash = strtoupper(sha1($password));
$prefix = substr($hash, 0, 5);
$suffix = substr($hash, 5);
Expand Down Expand Up @@ -336,6 +337,7 @@ public function test_min_zxcvbn_message_carries_feedback(): void

$rule = RejectCommonPasswords::config()->minZxcvbnScore(4)->rule();

// @credential-literal-exempt: a weak input the zxcvbn rule must reject
$validator = ValidatorFacade::make(['password' => 'Summer2024'], ['password' => [$rule]]);

$this->assertTrue($validator->fails());
Expand All @@ -357,7 +359,7 @@ public function test_hibp_never_transmits_the_full_password_only_the_sha1_prefix
'api.pwnedpasswords.com/*' => Http::response('', 200),
]);

$password = 'NeverLeakThisPassword!2026';
$password = TestCredentials::password('plaintext');
$expectedPrefix = strtoupper(substr(sha1($password), 0, 5));
$suffix = strtoupper(substr(sha1($password), 5));

Expand Down
Loading