- Event-Medien tragen Typ (image/video/audio), Dauer und Notiz; Videos bekommen ein Poster, Audio wird per ffmpeg nach AAC (audio/mp4) gewandelt, Originale werden mit Range-Unterstützung gestreamt. - Upload nimmt eine Notiz (caption) direkt mit, PATCH ändert sie später. - Neue Event-Felder: Zeitraum (end_date), Kategorie, Personen, Meilenstein. Regeln zentral in EventFields, damit CRUD und Sync gleich prüfen. - Titel darf leer sein: ein Event entsteht mit dem ersten Eintrag. - Artisan-Befehl media:transcode-voice-notes wandelt alte WebM/Ogg-Notizen, die Safari nicht abspielen kann. - Login-Throttling, Settings-Validierung, Sync-Fehlercodes ohne Rohtext. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
83 lines
2.7 KiB
PHP
83 lines
2.7 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Laravel\Passport\ClientRepository;
|
|
|
|
beforeEach(function () {
|
|
RateLimiter::clear('api-login|throttle@example.com|127.0.0.1');
|
|
RateLimiter::clear('api-login|other@example.com|127.0.0.1');
|
|
|
|
// A successful login issues a personal access token.
|
|
app(ClientRepository::class)->createPersonalAccessGrantClient('Test Personal Access Client', 'users');
|
|
});
|
|
|
|
test('repeated failed logins are locked out with http 429', function () {
|
|
User::factory()->create([
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'correct-horse',
|
|
]);
|
|
|
|
// Five wrong passwords are answered with the normal validation error.
|
|
foreach (range(1, 5) as $attempt) {
|
|
$this->postJson('/api/login', [
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'wrong',
|
|
])->assertStatus(422);
|
|
}
|
|
|
|
// The sixth is refused before the password is ever checked.
|
|
$this->postJson('/api/login', [
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'wrong',
|
|
])->assertStatus(429);
|
|
|
|
// Even the correct password stays locked out during the cooldown.
|
|
$this->postJson('/api/login', [
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'correct-horse',
|
|
])->assertStatus(429);
|
|
});
|
|
|
|
test('a successful login clears the failure counter', function () {
|
|
User::factory()->create([
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'correct-horse',
|
|
]);
|
|
|
|
foreach (range(1, 4) as $attempt) {
|
|
$this->postJson('/api/login', [
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'wrong',
|
|
])->assertStatus(422);
|
|
}
|
|
|
|
$this->postJson('/api/login', [
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'correct-horse',
|
|
])->assertOk();
|
|
|
|
// The counter is back to zero, so a fresh run of wrong attempts is
|
|
// answered with 422 again rather than an immediate lockout.
|
|
$this->postJson('/api/login', [
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'wrong',
|
|
])->assertStatus(422);
|
|
});
|
|
|
|
test('the throttle is scoped per email so one account cannot lock out another', function () {
|
|
User::factory()->create(['email' => 'throttle@example.com', 'password' => 'correct-horse']);
|
|
User::factory()->create(['email' => 'other@example.com', 'password' => 'correct-horse']);
|
|
|
|
foreach (range(1, 6) as $attempt) {
|
|
$this->postJson('/api/login', [
|
|
'email' => 'throttle@example.com',
|
|
'password' => 'wrong',
|
|
]);
|
|
}
|
|
|
|
$this->postJson('/api/login', [
|
|
'email' => 'other@example.com',
|
|
'password' => 'correct-horse',
|
|
])->assertOk();
|
|
});
|