Files
biiproject-kit-v1/tests/Feature/Api/ApiAuthExtendedTest.php
T

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);
});