feat: add routes, lang, tests, stubs, docs, and docker configurations
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
beforeEach(function () {
|
||||
Cache::flush();
|
||||
|
||||
Route::middleware(['web', 'auth', 'active-permission:probe'])
|
||||
->get('/__probe', fn () => response('ok'))
|
||||
->name('test.probe');
|
||||
});
|
||||
|
||||
test('inactive permission returns 403 even when user has it', function () {
|
||||
Permission::firstOrCreate(['name' => 'probe', 'guard_name' => 'web', 'is_active' => false]);
|
||||
$user = User::factory()->create();
|
||||
$user->givePermissionTo('probe');
|
||||
|
||||
$this->actingAs($user)->get('/__probe')->assertForbidden();
|
||||
});
|
||||
|
||||
test('active permission allows the request through', function () {
|
||||
Permission::firstOrCreate(['name' => 'probe', 'guard_name' => 'web', 'is_active' => true]);
|
||||
$user = User::factory()->create();
|
||||
$user->givePermissionTo('probe');
|
||||
|
||||
$this->actingAs($user)->get('/__probe')->assertOk()->assertSeeText('ok');
|
||||
});
|
||||
|
||||
test('missing permission returns 403', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get('/__probe')->assertForbidden();
|
||||
});
|
||||
|
||||
test('cache is consulted on subsequent hits', function () {
|
||||
Permission::firstOrCreate(['name' => 'probe', 'guard_name' => 'web', 'is_active' => true]);
|
||||
$user = User::factory()->create();
|
||||
$user->givePermissionTo('probe');
|
||||
|
||||
$this->actingAs($user)->get('/__probe')->assertOk();
|
||||
|
||||
expect(Cache::has('permission_status:probe'))->toBeTrue();
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\CheckLegalAgreement;
|
||||
use App\Models\User;
|
||||
use App\Models\UserConsent;
|
||||
use App\Services\SystemConfig\SystemConfigService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
beforeEach(function () {
|
||||
// Pest.php disables CheckLegalAgreement globally for Feature tests — re-enable it here.
|
||||
$this->withMiddleware(CheckLegalAgreement::class);
|
||||
|
||||
$ref = new ReflectionClass(SystemConfigService::class);
|
||||
$prop = $ref->getProperty('resolvedSettings');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue(null, null);
|
||||
Cache::flush();
|
||||
|
||||
Route::middleware(['web', 'auth', CheckLegalAgreement::class])
|
||||
->get('/__legal-probe', fn () => response('ok'));
|
||||
});
|
||||
|
||||
function setLegalVersion(string $prefix, int $version): void
|
||||
{
|
||||
app(SystemConfigService::class)->update(["{$prefix}_document_version" => $version]);
|
||||
}
|
||||
|
||||
test('guest is unaffected by middleware', function () {
|
||||
$this->get('/__legal-probe')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('user without consent is redirected to re-agree', function () {
|
||||
setLegalVersion('tos', 1);
|
||||
setLegalVersion('pdp', 1);
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get('/__legal-probe')
|
||||
->assertRedirect(route('legal.re-agree', absolute: false));
|
||||
});
|
||||
|
||||
test('user with current consent passes through', function () {
|
||||
setLegalVersion('tos', 1);
|
||||
setLegalVersion('pdp', 1);
|
||||
$user = User::factory()->create();
|
||||
UserConsent::create(['user_id' => $user->id, 'consent_type' => 'tos', 'version_id' => 1, 'ip_address' => '127.0.0.1']);
|
||||
UserConsent::create(['user_id' => $user->id, 'consent_type' => 'privacy', 'version_id' => 1, 'ip_address' => '127.0.0.1']);
|
||||
|
||||
$this->actingAs($user)->get('/__legal-probe')->assertOk();
|
||||
});
|
||||
|
||||
test('user with outdated consent is redirected', function () {
|
||||
setLegalVersion('tos', 2);
|
||||
setLegalVersion('pdp', 2);
|
||||
$user = User::factory()->create();
|
||||
UserConsent::create(['user_id' => $user->id, 'consent_type' => 'tos', 'version_id' => 1, 'ip_address' => '127.0.0.1']);
|
||||
UserConsent::create(['user_id' => $user->id, 'consent_type' => 'privacy', 'version_id' => 1, 'ip_address' => '127.0.0.1']);
|
||||
|
||||
$this->actingAs($user)->get('/__legal-probe')
|
||||
->assertRedirect(route('legal.re-agree', absolute: false));
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\IpAccessControl;
|
||||
use App\Models\User;
|
||||
use App\Services\SystemConfig\SystemConfigService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
beforeEach(function () {
|
||||
$ref = new ReflectionClass(SystemConfigService::class);
|
||||
$prop = $ref->getProperty('resolvedSettings');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue(null, null);
|
||||
Cache::flush();
|
||||
|
||||
Route::middleware([IpAccessControl::class])
|
||||
->get('/__ip-probe', fn () => response('ok'))
|
||||
->name('test.ip-probe');
|
||||
|
||||
Route::middleware([IpAccessControl::class])
|
||||
->get('/users/__ip-probe', fn () => response('ok-users'));
|
||||
});
|
||||
|
||||
function setIpSetting(string $key, mixed $value): void
|
||||
{
|
||||
app(SystemConfigService::class)->update([$key => $value]);
|
||||
}
|
||||
|
||||
test('request passes through with no IP rules configured', function () {
|
||||
$this->get('/__ip-probe')->assertOk()->assertSeeText('ok');
|
||||
});
|
||||
|
||||
test('blacklisted IP gets 403', function () {
|
||||
setIpSetting('ip_blacklist', '127.0.0.1, 10.0.0.5');
|
||||
|
||||
$this->get('/__ip-probe', ['REMOTE_ADDR' => '127.0.0.1'])->assertForbidden();
|
||||
});
|
||||
|
||||
test('non-blacklisted IP passes through', function () {
|
||||
setIpSetting('ip_blacklist', '10.0.0.5');
|
||||
|
||||
$this->get('/__ip-probe', ['REMOTE_ADDR' => '127.0.0.1'])->assertOk();
|
||||
});
|
||||
|
||||
test('admin whitelist denies non-whitelisted IPs on admin routes', function () {
|
||||
setIpSetting('ip_whitelist_admin', '203.0.113.1');
|
||||
|
||||
$this->call('GET', '/users/__ip-probe', server: ['REMOTE_ADDR' => '127.0.0.1'])->assertForbidden();
|
||||
});
|
||||
|
||||
test('admin whitelist permits whitelisted IPs on admin routes', function () {
|
||||
setIpSetting('ip_whitelist_admin', '127.0.0.1');
|
||||
|
||||
$this->call('GET', '/users/__ip-probe', server: ['REMOTE_ADDR' => '127.0.0.1'])->assertOk();
|
||||
});
|
||||
|
||||
test('admin whitelist does not affect non-admin routes', function () {
|
||||
setIpSetting('ip_whitelist_admin', '203.0.113.1');
|
||||
|
||||
$this->get('/__ip-probe', ['REMOTE_ADDR' => '127.0.0.1'])->assertOk();
|
||||
});
|
||||
|
||||
test('auto-blocked IP returns 429', function () {
|
||||
setIpSetting('auto_block_ip', true);
|
||||
Cache::put('ip_block:127.0.0.1', true, now()->addHour());
|
||||
|
||||
$this->get('/__ip-probe', ['REMOTE_ADDR' => '127.0.0.1'])->assertStatus(429);
|
||||
});
|
||||
|
||||
test('single session enforcement logs out stale session', function () {
|
||||
setIpSetting('session_single_session', true);
|
||||
$user = User::factory()->create(['last_session_id' => 'OTHER_SESSION_ID']);
|
||||
|
||||
$this->actingAs($user)->get('/__ip-probe')
|
||||
->assertRedirect(route('login', absolute: false));
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\PasswordExpiryMiddleware;
|
||||
use App\Models\User;
|
||||
use App\Services\SystemConfig\SystemConfigService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
beforeEach(function () {
|
||||
$ref = new ReflectionClass(SystemConfigService::class);
|
||||
$prop = $ref->getProperty('resolvedSettings');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue(null, null);
|
||||
Cache::flush();
|
||||
|
||||
Route::middleware(['web', 'auth', PasswordExpiryMiddleware::class])
|
||||
->get('/__pwd-probe', fn () => response('ok'));
|
||||
});
|
||||
|
||||
function setExpirySetting(int $days): void
|
||||
{
|
||||
app(SystemConfigService::class)->update(['password_expiry_days' => $days]);
|
||||
}
|
||||
|
||||
test('user with fresh password passes through', function () {
|
||||
setExpirySetting(30);
|
||||
$user = User::factory()->create();
|
||||
DB::table('users')->where('id', $user->id)
|
||||
->update(['password_changed_at' => now()->subDays(5)]);
|
||||
|
||||
$this->actingAs($user->fresh())->get('/__pwd-probe')->assertOk();
|
||||
});
|
||||
|
||||
test('user with expired password is redirected to profile', function () {
|
||||
setExpirySetting(30);
|
||||
$user = User::factory()->create();
|
||||
DB::table('users')->where('id', $user->id)
|
||||
->update(['password_changed_at' => now()->subDays(40)]);
|
||||
|
||||
$this->actingAs($user->fresh())->get('/__pwd-probe')
|
||||
->assertRedirect(route('profile.edit', absolute: false))
|
||||
->assertSessionHas('warning');
|
||||
});
|
||||
|
||||
test('expiry disabled (0 days) never redirects', function () {
|
||||
setExpirySetting(0);
|
||||
$user = User::factory()->create();
|
||||
DB::table('users')->where('id', $user->id)
|
||||
->update(['password_changed_at' => now()->subYears(2)]);
|
||||
|
||||
$this->actingAs($user->fresh())->get('/__pwd-probe')->assertOk();
|
||||
});
|
||||
|
||||
test('guest is unaffected', function () {
|
||||
$this->get('/__pwd-probe')->assertRedirect('/login');
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use App\Services\SystemConfig\SystemConfigService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
beforeEach(function () {
|
||||
$ref = new ReflectionClass(SystemConfigService::class);
|
||||
$prop = $ref->getProperty('resolvedSettings');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue(null, null);
|
||||
Cache::flush();
|
||||
|
||||
Route::middleware('web')
|
||||
->get('/__sec-probe', fn () => response('ok'));
|
||||
});
|
||||
|
||||
test('X-Content-Type-Options nosniff is present', function () {
|
||||
$r = $this->get('/__sec-probe');
|
||||
expect($r->headers->get('X-Content-Type-Options'))->toBe('nosniff');
|
||||
});
|
||||
|
||||
test('X-Frame-Options SAMEORIGIN is present', function () {
|
||||
$r = $this->get('/__sec-probe');
|
||||
expect($r->headers->get('X-Frame-Options'))->toBe('SAMEORIGIN');
|
||||
});
|
||||
|
||||
test('Referrer-Policy is strict-origin-when-cross-origin', function () {
|
||||
$r = $this->get('/__sec-probe');
|
||||
expect($r->headers->get('Referrer-Policy'))->toBe('strict-origin-when-cross-origin');
|
||||
});
|
||||
|
||||
test('Permissions-Policy locks down camera, microphone, geolocation', function () {
|
||||
$r = $this->get('/__sec-probe');
|
||||
$pp = $r->headers->get('Permissions-Policy');
|
||||
expect($pp)->toContain('camera=()')->toContain('microphone=()')->toContain('geolocation=()');
|
||||
});
|
||||
|
||||
test('X-XSS-Protection header is set', function () {
|
||||
$r = $this->get('/__sec-probe');
|
||||
expect($r->headers->get('X-XSS-Protection'))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('HSTS is omitted over plain HTTP regardless of setting', function () {
|
||||
$r = $this->get('/__sec-probe');
|
||||
expect($r->headers->get('Strict-Transport-Security'))->toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user