- 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>
282 lines
8.8 KiB
PHP
282 lines
8.8 KiB
PHP
<?php
|
|
|
|
use App\Models\Event;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Str;
|
|
use Laravel\Passport\Passport;
|
|
|
|
beforeEach(function () {
|
|
$this->user = User::factory()->create();
|
|
Passport::actingAs($this->user);
|
|
});
|
|
|
|
test('can list events', function () {
|
|
Event::factory()->count(3)->create(['user_id' => $this->user->id]);
|
|
|
|
$response = $this->getJson('/api/events');
|
|
|
|
$response->assertOk()
|
|
->assertJsonCount(3, 'data');
|
|
});
|
|
|
|
test('list only returns own events', function () {
|
|
Event::factory()->count(2)->create(['user_id' => $this->user->id]);
|
|
Event::factory()->count(3)->create(); // other user
|
|
|
|
$response = $this->getJson('/api/events');
|
|
|
|
$response->assertOk()
|
|
->assertJsonCount(2, 'data');
|
|
});
|
|
|
|
test('can filter events by since parameter', function () {
|
|
Event::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'updated_at' => now()->subDays(5),
|
|
]);
|
|
Event::factory()->create([
|
|
'user_id' => $this->user->id,
|
|
'updated_at' => now()->subHour(),
|
|
]);
|
|
|
|
$response = $this->getJson('/api/events?since='.now()->subDay()->toISOString());
|
|
|
|
$response->assertOk()
|
|
->assertJsonCount(1, 'data');
|
|
});
|
|
|
|
test('can create an event', function () {
|
|
$clientId = Str::uuid()->toString();
|
|
|
|
$response = $this->postJson('/api/events', [
|
|
'id' => $clientId,
|
|
'title' => 'Mein Event',
|
|
'date' => '2024-06-15',
|
|
'emotion' => 0.75,
|
|
'customColor' => null,
|
|
'gradientPreset' => 2,
|
|
'image' => null,
|
|
'note' => 'Eine Notiz',
|
|
]);
|
|
|
|
$response->assertCreated()
|
|
->assertJsonPath('data.id', $clientId)
|
|
->assertJsonPath('data.title', 'Mein Event')
|
|
->assertJsonPath('data.syncStatus', 'synced');
|
|
|
|
$this->assertDatabaseHas('events', [
|
|
'client_id' => $clientId,
|
|
'user_id' => $this->user->id,
|
|
]);
|
|
});
|
|
|
|
test('create validates required fields', function () {
|
|
$response = $this->postJson('/api/events', []);
|
|
|
|
$response->assertUnprocessable()
|
|
->assertJsonValidationErrors(['id', 'title', 'date', 'emotion']);
|
|
});
|
|
|
|
test('can show a single event', function () {
|
|
$event = Event::factory()->create(['user_id' => $this->user->id]);
|
|
|
|
$response = $this->getJson("/api/events/{$event->client_id}");
|
|
|
|
$response->assertOk()
|
|
->assertJsonPath('data.id', $event->client_id)
|
|
->assertJsonPath('data.title', $event->title);
|
|
});
|
|
|
|
test('cannot show another users event', function () {
|
|
$event = Event::factory()->create();
|
|
|
|
$response = $this->getJson("/api/events/{$event->client_id}");
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
test('can update an event', function () {
|
|
$event = Event::factory()->create(['user_id' => $this->user->id]);
|
|
|
|
$response = $this->putJson("/api/events/{$event->client_id}", [
|
|
'title' => 'Updated Title',
|
|
'emotion' => -0.5,
|
|
]);
|
|
|
|
$response->assertOk()
|
|
->assertJsonPath('data.title', 'Updated Title');
|
|
});
|
|
|
|
test('can delete an event', function () {
|
|
$event = Event::factory()->create(['user_id' => $this->user->id]);
|
|
|
|
$response = $this->deleteJson("/api/events/{$event->client_id}");
|
|
|
|
$response->assertNoContent();
|
|
$this->assertDatabaseMissing('events', ['id' => $event->id]);
|
|
});
|
|
|
|
test('cannot delete another users event', function () {
|
|
$event = Event::factory()->create();
|
|
|
|
$response = $this->deleteJson("/api/events/{$event->client_id}");
|
|
|
|
$response->assertNotFound();
|
|
});
|
|
|
|
test('batch sync creates updates and deletes', function () {
|
|
$existingEvent = Event::factory()->create(['user_id' => $this->user->id]);
|
|
$newId = Str::uuid()->toString();
|
|
$deleteEvent = Event::factory()->create(['user_id' => $this->user->id]);
|
|
|
|
$response = $this->postJson('/api/events/sync', [
|
|
'mutations' => [
|
|
[
|
|
'action' => 'create',
|
|
'eventId' => $newId,
|
|
'payload' => [
|
|
'title' => 'New via sync',
|
|
'date' => '2025-01-01',
|
|
'emotion' => 0.3,
|
|
],
|
|
],
|
|
[
|
|
'action' => 'update',
|
|
'eventId' => $existingEvent->client_id,
|
|
'payload' => [
|
|
'title' => 'Updated via sync',
|
|
],
|
|
],
|
|
[
|
|
'action' => 'delete',
|
|
'eventId' => $deleteEvent->client_id,
|
|
'payload' => null,
|
|
],
|
|
],
|
|
]);
|
|
|
|
$response->assertOk()
|
|
->assertJsonCount(3, 'results');
|
|
|
|
$this->assertDatabaseHas('events', ['client_id' => $newId, 'title' => 'New via sync']);
|
|
$this->assertDatabaseHas('events', ['client_id' => $existingEvent->client_id, 'title' => 'Updated via sync']);
|
|
$this->assertDatabaseMissing('events', ['client_id' => $deleteEvent->client_id]);
|
|
});
|
|
|
|
test('sync is idempotent for creates', function () {
|
|
$clientId = Str::uuid()->toString();
|
|
|
|
$this->postJson('/api/events/sync', [
|
|
'mutations' => [[
|
|
'action' => 'create',
|
|
'eventId' => $clientId,
|
|
'payload' => ['title' => 'First', 'date' => '2025-01-01', 'emotion' => 0],
|
|
]],
|
|
])->assertOk();
|
|
|
|
$this->postJson('/api/events/sync', [
|
|
'mutations' => [[
|
|
'action' => 'create',
|
|
'eventId' => $clientId,
|
|
'payload' => ['title' => 'Duplicate', 'date' => '2025-01-01', 'emotion' => 0],
|
|
]],
|
|
])->assertOk();
|
|
|
|
expect(Event::where('client_id', $clientId)->count())->toBe(1);
|
|
});
|
|
|
|
test('can create an event with an empty title', function () {
|
|
// An event is created with its first entry, which may be a note or a
|
|
// photo. The title may therefore be empty, but it has to be sent.
|
|
$clientId = Str::uuid()->toString();
|
|
|
|
$this->postJson('/api/events', [
|
|
'id' => $clientId,
|
|
'title' => '',
|
|
'date' => '2024-06-15',
|
|
'emotion' => 0,
|
|
'note' => 'Nur eine Notiz',
|
|
])->assertCreated()
|
|
->assertJsonPath('data.title', '');
|
|
|
|
expect(Event::query()->where('client_id', $clientId)->value('title'))->toBe('');
|
|
});
|
|
|
|
test('title must still be sent when creating an event', function () {
|
|
$this->postJson('/api/events', [
|
|
'id' => Str::uuid()->toString(),
|
|
'date' => '2024-06-15',
|
|
'emotion' => 0,
|
|
])->assertUnprocessable()
|
|
->assertJsonValidationErrors(['title']);
|
|
});
|
|
|
|
test('stores category, people, a span and the milestone flag', function () {
|
|
$clientId = Str::uuid()->toString();
|
|
|
|
$this->postJson('/api/events', [
|
|
'id' => $clientId,
|
|
'title' => 'Umzug nach Hamburg',
|
|
'date' => '2019-04-01',
|
|
'endDate' => '2019-04-14',
|
|
'emotion' => 0.4,
|
|
'milestone' => true,
|
|
'category' => 'home',
|
|
'people' => ['Lena', ' Papa ', '', 'Lena'],
|
|
])
|
|
->assertCreated()
|
|
->assertJsonPath('data.endDate', '2019-04-14')
|
|
->assertJsonPath('data.milestone', true)
|
|
->assertJsonPath('data.category', 'home')
|
|
// Trimmed, blanks dropped, duplicates removed.
|
|
->assertJsonPath('data.people', ['Lena', 'Papa']);
|
|
|
|
// Clearing the span and the people list on update.
|
|
$this->putJson("/api/events/{$clientId}", [
|
|
'endDate' => null,
|
|
'people' => [],
|
|
'milestone' => false,
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('data.endDate', null)
|
|
->assertJsonPath('data.people', [])
|
|
->assertJsonPath('data.milestone', false);
|
|
});
|
|
|
|
test('rejects an unknown category and a span that ends before it starts', function () {
|
|
$base = ['id' => Str::uuid()->toString(), 'title' => 'x', 'date' => '2020-05-10', 'emotion' => 0];
|
|
|
|
$this->postJson('/api/events', [...$base, 'category' => 'aliens'])
|
|
->assertUnprocessable()
|
|
->assertJsonValidationErrors(['category']);
|
|
|
|
$this->postJson('/api/events', [...$base, 'endDate' => '2020-05-09'])
|
|
->assertUnprocessable()
|
|
->assertJsonValidationErrors(['endDate']);
|
|
|
|
$this->postJson('/api/events', [...$base, 'people' => [str_repeat('x', 81)]])
|
|
->assertUnprocessable()
|
|
->assertJsonValidationErrors(['people.0']);
|
|
});
|
|
|
|
test('sync writes the new fields like crud does', function () {
|
|
$clientId = Str::uuid()->toString();
|
|
|
|
$this->postJson('/api/events/sync', ['mutations' => [[
|
|
'action' => 'create',
|
|
'eventId' => $clientId,
|
|
'payload' => [
|
|
'title' => 'Abschluss',
|
|
'date' => '2015-07-01',
|
|
'emotion' => 0.9,
|
|
'milestone' => true,
|
|
'category' => 'education',
|
|
'people' => ['Prof. Weber'],
|
|
],
|
|
]]])->assertOk()->assertJsonPath('results.0.status', 'ok');
|
|
|
|
$event = Event::query()->where('client_id', $clientId)->firstOrFail();
|
|
expect($event->milestone)->toBeTrue()
|
|
->and($event->category)->toBe('education')
|
|
->and($event->people)->toBe(['Prof. Weber']);
|
|
});
|