'nikita']; protected function setUp(): void { parent::setUp(); config(['hado.default_tz' => 'UTC']); CarbonImmutable::setTestNow('2026-09-04T12:00:00Z'); } protected function tearDown(): void { CarbonImmutable::setTestNow(); parent::tearDown(); } public function test_creates_manual_event_grouped_by_date(): void { $this->postJson('/me/events', ['title' => 'Продлить страховку', 'subtitle' => 'раз в год', 'due_date' => '2026-09-20'], $this->as) ->assertCreated() ->assertJsonPath('state', 'preparing') ->assertJsonPath('source', 'manual') ->assertJsonPath('payload.title', 'Продлить страховку') ->assertJsonPath('payload.done_label', 'Сделано') ->assertJsonPath('fire_at', '2026-08-21'); $this->postJson('/me/events', ['title' => 'Далеко', 'due_date' => '2026-12-15'], $this->as)->assertJsonPath('state', 'scheduled'); $this->postJson('/me/events', ['title' => 'Сегодня', 'due_date' => '2026-09-04', 'due_time' => '19:40'], $this->as) ->assertJsonPath('state', 'today')->assertJsonPath('due_at', '2026-09-04T19:40'); $this->postJson('/me/events', ['title' => 'Прошло', 'due_date' => '2026-09-01'], $this->as)->assertJsonPath('state', 'overdue'); $this->assertSame(4, Event::count()); $this->assertSame('manual', Source::sole()->name); $this->assertSame(AfterDue::Keep, Event::first()->after_due); $this->assertSame(DueMode::Local, Event::first()->due_mode); } public function test_validation(): void { $this->postJson('/me/events', ['due_date' => '2026-09-20'], $this->as)->assertStatus(422)->assertJsonValidationErrors(['title']); $this->postJson('/me/events', ['title' => 'x'], $this->as)->assertStatus(422)->assertJsonValidationErrors(['due_date']); $this->postJson('/me/events', ['title' => 'x', 'due_date' => '2026-09-20', 'due_time' => '25:00'], $this->as)->assertStatus(422)->assertJsonValidationErrors(['due_time']); } public function test_manual_source_cannot_be_used_through_client_api(): void { $this->postJson('/me/events', ['title' => 'x', 'due_date' => '2026-09-20'], $this->as)->assertCreated(); $this->putJson('/api/events', ['events' => []], ['Authorization' => 'Bearer manual'])->assertStatus(401); } public function test_archive_lists_done_and_expired_newest_first(): void { $this->getJson('/me', $this->as); $user = User::sole(); $source = Source::create(['name' => 'docs', 'token_hash' => hash('sha256', 'x')]); $make = fn (string $topic, EventState $state, string $updatedAt, ?string $doneLabel = null) => tap(Event::create([ 'source_id' => $source->id, 'user_id' => $user->id, 'source_ref' => 'r', 'topic' => $topic, 'due_mode' => DueMode::Local, 'due_date' => '2026-08-28', 'due_time' => '24:00:00', 'fire_on' => '2026-08-28', 'after_due' => AfterDue::Expire, 'payload' => array_filter(['title' => "E $topic", 'done_label' => $doneLabel]), 'state' => $state, 'quiet_until' => CarbonImmutable::parse('2000-01-01Z'), ]), fn (Event $e) => Event::whereKey($e->id)->update(['updated_at' => $updatedAt])); $make('a', EventState::Done, '2026-09-01 10:00:00', 'Поздравил'); $make('b', EventState::Expired, '2026-09-02 10:00:00'); $make('c', EventState::Withdrawn, '2026-09-03 10:00:00'); $make('d', EventState::Today, '2026-09-03 10:00:00'); $r = $this->getJson('/me/archive', $this->as)->assertOk()->assertJsonCount(2, 'events'); $this->assertSame(['b', 'a'], array_column($r->json('events'), 'topic')); $this->assertSame(['Истекло', 'Поздравил'], array_column($r->json('events'), 'closed_as')); } }