feat: add routes, lang, tests, stubs, docs, and docker configurations
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
use App\Models\PasswordHistory;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\PasswordPolicyService;
|
||||
use App\Services\SystemConfig\SystemConfigService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
beforeEach(function () {
|
||||
$ref = new ReflectionClass(SystemConfigService::class);
|
||||
$prop = $ref->getProperty('resolvedSettings');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue(null, null);
|
||||
Cache::flush();
|
||||
|
||||
Role::findOrCreate('User', 'web');
|
||||
});
|
||||
|
||||
// ── deleteAccount ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('delete account succeeds when correct password is supplied', function () {
|
||||
$user = User::factory()->create(['password' => Hash::make('secret')]);
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->deleteJson('/api/v1/profile/delete', ['password' => 'secret'])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', 'success');
|
||||
|
||||
expect(User::find($user->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('delete account is rejected with wrong password', function () {
|
||||
$user = User::factory()->create(['password' => Hash::make('secret')]);
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->deleteJson('/api/v1/profile/delete', ['password' => 'wrong'])
|
||||
->assertStatus(422);
|
||||
|
||||
expect(User::find($user->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('delete account requires password field', function () {
|
||||
$user = User::factory()->create();
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->deleteJson('/api/v1/profile/delete', [])
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
test('delete account is not accessible to guests', function () {
|
||||
$this->deleteJson('/api/v1/profile/delete', ['password' => 'x'])
|
||||
->assertUnauthorized();
|
||||
});
|
||||
|
||||
test('delete account also revokes all user tokens', function () {
|
||||
$user = User::factory()->create(['password' => Hash::make('secret')]);
|
||||
$user->createToken('device-a');
|
||||
$user->createToken('device-b');
|
||||
$token = $user->createToken('device-c')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->deleteJson('/api/v1/profile/delete', ['password' => 'secret'])
|
||||
->assertOk();
|
||||
|
||||
expect($user->tokens()->count())->toBe(0);
|
||||
});
|
||||
|
||||
// ── updatePassword (API) ──────────────────────────────────────────────────────
|
||||
|
||||
test('api password update succeeds with valid credentials', function () {
|
||||
$user = User::factory()->create(['password' => Hash::make('old-pass')]);
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->postJson('/api/v1/profile/password', [
|
||||
'current_password' => 'old-pass',
|
||||
'password' => 'New-Api-Pass1',
|
||||
'password_confirmation' => 'New-Api-Pass1',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', 'success');
|
||||
|
||||
expect(Hash::check('New-Api-Pass1', $user->fresh()->password))->toBeTrue();
|
||||
});
|
||||
|
||||
test('api password update is rejected when current password is wrong', function () {
|
||||
$user = User::factory()->create(['password' => Hash::make('real-pass')]);
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->postJson('/api/v1/profile/password', [
|
||||
'current_password' => 'not-real',
|
||||
'password' => 'New-Api-Pass2',
|
||||
'password_confirmation' => 'New-Api-Pass2',
|
||||
])
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
test('api password update is rejected when reusing a history password', function () {
|
||||
app(SystemConfigService::class)->update(['password_history_count' => 3]);
|
||||
|
||||
$user = User::factory()->create(['password' => Hash::make('current')]);
|
||||
PasswordHistory::create(['user_id' => $user->id, 'password' => Hash::make('Recycled-Api!1')]);
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->postJson('/api/v1/profile/password', [
|
||||
'current_password' => 'current',
|
||||
'password' => 'Recycled-Api!1',
|
||||
'password_confirmation' => 'Recycled-Api!1',
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('status', 'error');
|
||||
});
|
||||
|
||||
test('api password update records change in password history', function () {
|
||||
app(SystemConfigService::class)->update(['password_history_count' => 5]);
|
||||
|
||||
$user = User::factory()->create(['password' => Hash::make('old')]);
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->postJson('/api/v1/profile/password', [
|
||||
'current_password' => 'old',
|
||||
'password' => 'Api-NewPass-99',
|
||||
'password_confirmation' => 'Api-NewPass-99',
|
||||
]);
|
||||
|
||||
expect(PasswordHistory::where('user_id', $user->id)->count())->toBe(1);
|
||||
});
|
||||
|
||||
// ── register with PasswordPolicyService ──────────────────────────────────────
|
||||
|
||||
test('register enforces min length from password policy setting', function () {
|
||||
app(SystemConfigService::class)->update(['password_min_length' => 12]);
|
||||
|
||||
$this->postJson('/api/v1/register', [
|
||||
'name' => 'Test',
|
||||
'email' => 'policy@example.com',
|
||||
'password' => 'short',
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
test('register succeeds when password meets policy min length', function () {
|
||||
app(SystemConfigService::class)->update(['password_min_length' => 6]);
|
||||
|
||||
$this->postJson('/api/v1/register', [
|
||||
'name' => 'Policy User',
|
||||
'email' => 'policy2@example.com',
|
||||
'password' => 'longenough',
|
||||
])->assertStatus(201);
|
||||
});
|
||||
|
||||
test('register enforces numeric requirement when setting is enabled', function () {
|
||||
app(SystemConfigService::class)->update([
|
||||
'password_require_numeric' => true,
|
||||
'password_min_length' => 6,
|
||||
]);
|
||||
|
||||
$this->postJson('/api/v1/register', [
|
||||
'name' => 'No Digits',
|
||||
'email' => 'nodigits@example.com',
|
||||
'password' => 'NoDigitsHere',
|
||||
])->assertStatus(422);
|
||||
});
|
||||
|
||||
// ── updateProfile ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('authenticated user can update their name via api', function () {
|
||||
$user = User::factory()->create();
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->postJson('/api/v1/profile/update', [
|
||||
'name' => 'Updated Name',
|
||||
'email' => $user->email,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('status', 'success');
|
||||
|
||||
expect($user->fresh()->name)->toBe('Updated Name');
|
||||
});
|
||||
|
||||
test('profile update rejects name longer than 255 chars', function () {
|
||||
$user = User::factory()->create();
|
||||
$token = $user->createToken('app')->plainTextToken;
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->postJson('/api/v1/profile/update', [
|
||||
'name' => str_repeat('a', 256),
|
||||
'email' => $user->email,
|
||||
])
|
||||
->assertStatus(422);
|
||||
});
|
||||
Reference in New Issue
Block a user