Files
hado/core/tests/Feature/MeApiTest.php

136 lines
5.1 KiB
PHP

<?php
namespace Tests\Feature;
use App\Enums\AfterDue;
use App\Enums\ChannelType;
use App\Enums\DueMode;
use App\Enums\EventState;
use App\Models\Event;
use App\Models\Source;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class MeApiTest extends TestCase
{
use RefreshDatabase;
private Source $source;
protected function setUp(): void
{
parent::setUp();
config(['hado.default_tz' => 'UTC']);
CarbonImmutable::setTestNow('2026-09-30T11:17:00Z');
$this->source = Source::create(['name' => 'docs', 'token_hash' => hash('sha256', 'x')]);
}
protected function tearDown(): void
{
CarbonImmutable::setTestNow();
parent::tearDown();
}
private function as(string $login = 'nikita'): array
{
return ['X-Remote-User' => $login];
}
private function event(User $user, EventState $state, string $topic = 't', string $due = '2026-09-30', string $fireOn = '2026-09-30'): Event
{
return Event::create([
'source_id' => $this->source->id, 'user_id' => $user->id, 'source_ref' => 'r', 'topic' => $topic,
'due_mode' => DueMode::Local, 'due_date' => $due, 'due_time' => '24:00:00', 'fire_on' => $fireOn,
'after_due' => AfterDue::Keep, 'payload' => ['title' => 'T'], 'state' => $state,
'quiet_until' => CarbonImmutable::now()->subDay(),
]);
}
public function test_requires_remote_user_header(): void
{
$this->getJson('/me')->assertStatus(401);
}
public function test_first_visit_creates_user_with_web_channel(): void
{
$this->getJson('/me', $this->as())->assertOk()
->assertJsonPath('login', 'nikita')
->assertJsonPath('tz', 'UTC')
->assertJsonPath('quiet_start', '22:00')
->assertJsonPath('quiet_end', '09:00')
->assertJsonPath('badge', 0);
$this->assertSame(ChannelType::Web, User::sole()->channels()->sole()->type);
}
public function test_badge_counts_today_and_overdue_only(): void
{
$this->getJson('/me', $this->as());
$user = User::sole();
$this->event($user, EventState::Today, 'a');
$this->event($user, EventState::Overdue, 'b');
$this->event($user, EventState::Preparing, 'c');
$this->event($user, EventState::Done, 'd');
$this->getJson('/me', $this->as())->assertJsonPath('badge', 2);
}
public function test_patch_profile_validates_timezone_and_quiet_hours(): void
{
$this->patchJson('/me', ['tz' => 'Asia/Shanghai', 'quiet_start' => '23:00', 'quiet_end' => '08:00'], $this->as())
->assertOk()->assertJsonPath('tz', 'Asia/Shanghai')->assertJsonPath('quiet_start', '23:00');
$this->patchJson('/me', ['tz' => 'Mars/Olympus'], $this->as())->assertStatus(422);
$this->patchJson('/me', ['quiet_start' => '08:00', 'quiet_end' => '22:00'], $this->as())
->assertStatus(422)->assertJsonValidationErrors(['quiet_end']);
}
public function test_lists_own_non_terminal_events(): void
{
$this->getJson('/me', $this->as());
$this->getJson('/me', $this->as('other'));
$me = User::where('login', 'nikita')->sole();
$other = User::where('login', 'other')->sole();
$this->event($me, EventState::Today, 'a');
$this->event($me, EventState::Done, 'b');
$this->event($other, EventState::Today, 'c');
$this->getJson('/me/events', $this->as())->assertOk()->assertJsonCount(1, 'events')->assertJsonPath('events.0.topic', 'a');
$this->getJson('/me/events?state=done', $this->as())->assertJsonCount(1, 'events')->assertJsonPath('events.0.topic', 'b');
$this->getJson('/me/events?state=garbage', $this->as())->assertStatus(422);
}
public function test_ack_sets_quiet_until_and_done_closes(): void
{
$this->getJson('/me', $this->as());
$user = User::sole();
$today = $this->event($user, EventState::Today, 'a');
$done = $this->event($user, EventState::Preparing, 'b', '2026-10-30', '2026-09-30');
$this->postJson("/me/events/{$today->id}/ack", [], $this->as())->assertOk()->assertJsonPath('state', 'today');
$this->assertSame('2026-09-30T13:00:00+00:00', $today->fresh()->quiet_until->toIso8601String());
$this->postJson("/me/events/{$done->id}/done", [], $this->as())->assertOk()->assertJsonPath('state', 'done');
$this->assertNotNull($done->fresh()->done_at);
$this->postJson("/me/events/{$done->id}/done", [], $this->as())->assertOk(); // идемпотентно
}
public function test_cannot_touch_someone_elses_event(): void
{
$this->getJson('/me', $this->as('other'));
$event = $this->event(User::sole(), EventState::Today);
$this->postJson("/me/events/{$event->id}/done", [], $this->as('nikita'))->assertNotFound();
}
public function test_heartbeat_marks_web_presence(): void
{
$this->postJson('/me/heartbeat', [], $this->as())->assertNoContent();
$this->assertTrue(Cache::has('heartbeat:'.User::sole()->id));
}
}