200 lines
7.5 KiB
PHP
200 lines
7.5 KiB
PHP
<?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);
|
|
});
|