feat: add critical path test suite and fix auth/config issues
Test Infrastructure:
- Modernize phpunit.xml (bootstrap, source block, Laravel 12 env vars)
- Configure tests/Pest.php with pest()->extend(TestCase::class)->in('Feature')
- Add docker-compose.test.yml (Redis for test suite)
- Add composer test/test:quick scripts
- Rename CACHE_DRIVER to CACHE_STORE across config (backwards compatible)
- Update .env.testing for in-memory SQLite + Docker Redis
Test Coverage (190 tests):
- CriticalRoutes: public routes, auth routes, API endpoints, middleware, schedule
- Auth/LoginTest: login, logout, rate limiting, redirect behavior
- Auth/RegisterTest: registration flow, validation, disabled registration
- Auth/PasswordResetTest: reset request, token validation, password update
- Auth/TwoFactorTest: 2FA checkpoint, setup behind password confirmation
- Auth/PasswordConfirmationTest: sudo mode flow via Laravel password.confirm
- Api/ScopeTest: scope enforcement, public endpoints, admin access
Bugs Fixed:
- Fix unauthenticated API returning 500 instead of 401 (AuthenticationException
not handled in custom exception renderer in bootstrap/app.php)
- Replace custom DangerZone middleware with Laravel password.confirm
- Add HasFactory trait to Profile model for test factories
Bugs Documented (known-bugs group):
- Registration crashes with str_ends_with TypeError (RegisterController:82)
- OAuth routes use legacy array syntax causing ReflectionFunction TypeError
4 weeks ago
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
use App\Models\User;
|
feat: add critical path test suite and fix auth/config issues
Test Infrastructure:
- Modernize phpunit.xml (bootstrap, source block, Laravel 12 env vars)
- Configure tests/Pest.php with pest()->extend(TestCase::class)->in('Feature')
- Add docker-compose.test.yml (Redis for test suite)
- Add composer test/test:quick scripts
- Rename CACHE_DRIVER to CACHE_STORE across config (backwards compatible)
- Update .env.testing for in-memory SQLite + Docker Redis
Test Coverage (190 tests):
- CriticalRoutes: public routes, auth routes, API endpoints, middleware, schedule
- Auth/LoginTest: login, logout, rate limiting, redirect behavior
- Auth/RegisterTest: registration flow, validation, disabled registration
- Auth/PasswordResetTest: reset request, token validation, password update
- Auth/TwoFactorTest: 2FA checkpoint, setup behind password confirmation
- Auth/PasswordConfirmationTest: sudo mode flow via Laravel password.confirm
- Api/ScopeTest: scope enforcement, public endpoints, admin access
Bugs Fixed:
- Fix unauthenticated API returning 500 instead of 401 (AuthenticationException
not handled in custom exception renderer in bootstrap/app.php)
- Replace custom DangerZone middleware with Laravel password.confirm
- Add HasFactory trait to Profile model for test factories
Bugs Documented (known-bugs group):
- Registration crashes with str_ends_with TypeError (RegisterController:82)
- OAuth routes use legacy array syntax causing ReflectionFunction TypeError
4 weeks ago
|
|
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
|
|
|
|
use Laravel\Passport\Passport;
|
|
|
|
|
|
|
|
|
|
uses(LazilyRefreshDatabase::class);
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| API Scope & Authorization Tests
|
|
|
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
describe('unauthenticated requests', function () {
|
|
|
|
|
it('returns 401 for protected endpoints without a token', function (string $endpoint) {
|
|
|
|
|
$this->getJson($endpoint)
|
|
|
|
|
->assertUnauthorized();
|
|
|
|
|
})->with([
|
|
|
|
|
'verify credentials' => '/api/v1/accounts/verify_credentials',
|
|
|
|
|
'home timeline' => '/api/v1/timelines/home',
|
|
|
|
|
'notifications' => '/api/v1/notifications',
|
|
|
|
|
'bookmarks' => '/api/v1/bookmarks',
|
|
|
|
|
'favourites' => '/api/v1/favourites',
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('authenticated with read scope', function () {
|
|
|
|
|
it('allows read access to account endpoints', function () {
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
$user->refresh();
|
|
|
|
|
Passport::actingAs($user, ['read']);
|
|
|
|
|
|
|
|
|
|
$this->getJson('/api/v1/accounts/verify_credentials')
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonStructure(['id', 'username']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('allows read access to timelines', function () {
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
$user->refresh();
|
|
|
|
|
Passport::actingAs($user, ['read']);
|
|
|
|
|
|
|
|
|
|
$this->getJson('/api/v1/timelines/home')
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonIsArray();
|
|
|
|
|
|
|
|
|
|
$this->getJson('/api/v1/timelines/public')
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonIsArray();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('allows read access to notifications', function () {
|
|
|
|
|
$user = User::factory()->create();
|
|
|
|
|
$user->refresh();
|
|
|
|
|
Passport::actingAs($user, ['read']);
|
|
|
|
|
|
|
|
|
|
$this->getJson('/api/v1/notifications')
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonIsArray();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('public endpoints require no auth', function () {
|
|
|
|
|
it('returns instance info without authentication', function () {
|
|
|
|
|
$this->getJson('/api/v1/instance')
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonStructure(['uri', 'title', 'description']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns custom emojis without authentication', function () {
|
|
|
|
|
$this->getJson('/api/v1/custom_emojis')
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonIsArray();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns instance peers without authentication', function () {
|
|
|
|
|
$this->getJson('/api/v1/instance/peers')
|
|
|
|
|
->assertOk()
|
|
|
|
|
->assertJsonIsArray();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('accepts app registration without authentication', function () {
|
|
|
|
|
$this->postJson('/api/v1/apps', [
|
|
|
|
|
'client_name' => 'Test App',
|
|
|
|
|
'redirect_uris' => 'urn:ietf:wg:oauth:2.0:oob',
|
|
|
|
|
'scopes' => 'read',
|
|
|
|
|
])->assertOk()
|
|
|
|
|
->assertJsonStructure(['client_id', 'client_secret']);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('admin scope', function () {
|
|
|
|
|
it('returns 404 for admin endpoints with non-admin user', function () {
|
|
|
|
|
// The admin API middleware returns 404 (not 403) for non-admin users
|
|
|
|
|
// to avoid revealing the existence of admin endpoints.
|
|
|
|
|
$user = User::factory()->create(['is_admin' => false]);
|
|
|
|
|
$user->refresh();
|
|
|
|
|
Passport::actingAs($user, ['admin:read']);
|
|
|
|
|
|
|
|
|
|
$this->getJson('/api/admin/stats')
|
|
|
|
|
->assertNotFound();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('allows admin endpoints for admin user with admin scope', function () {
|
|
|
|
|
$user = User::factory()->admin()->create();
|
|
|
|
|
$user->refresh();
|
|
|
|
|
Passport::actingAs($user, ['admin:read']);
|
|
|
|
|
|
|
|
|
|
$this->getJson('/api/admin/stats')
|
|
|
|
|
->assertOk();
|
|
|
|
|
});
|
|
|
|
|
});
|