fix: final review wave — public routes, inbox 419, url schemes, deep_link, tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
nikita.hohlov
2026-09-04 09:09:07 -03:00
parent dd714a4be1
commit cbb43714ae
20 changed files with 123 additions and 20 deletions

View File

@@ -50,6 +50,11 @@ class ActionLinksTest extends TestCase
$this->assertSame('2026-09-30T13:00:00+00:00', $this->event->fresh()->quiet_until->toIso8601String());
}
public function test_public_route_sets_no_session_cookie(): void
{
$this->postJson('/a/tok/ack')->assertOk()->assertHeaderMissing('Set-Cookie');
}
public function test_done_by_token_then_gone(): void
{
$this->postJson('/a/tok/done')->assertOk()->assertJsonPath('state', 'done');

View File

@@ -38,6 +38,11 @@ class ChannelsApiTest extends TestCase
// telegram подключается только через /start, не через API
$this->postJson('/me/channels', ['type' => 'telegram', 'config' => ['chat_id' => 1]], $this->as)
->assertStatus(422);
// только http/https-схемы
$this->postJson('/me/channels', ['type' => 'webhook', 'config' => ['deliver_url' => 'ftp://x/y', 'presence_url' => 'ftp://x/y']], $this->as)
->assertStatus(422)->assertJsonValidationErrors(['config.deliver_url', 'config.presence_url']);
$this->postJson('/me/channels', ['type' => 'webhook', 'config' => ['deliver_url' => 'http://home.local/x', 'presence_url' => 'http://home.local/x']], $this->as)
->assertCreated();
}
public function test_web_channel_cannot_be_deleted_and_foreign_channel_is_404(): void

View File

@@ -93,11 +93,18 @@ class ClientApiTest extends TestCase
$this->putJson('/api/events', ['events' => [$protocolRelative]], $this->auth())
->assertStatus(422)->assertJsonValidationErrors(['events.0.payload.deep_link']);
$backslash = $this->item(['payload' => ['title' => 'x', 'deep_link' => '/\evil.com']]);
$this->putJson('/api/events', ['events' => [$backslash]], $this->auth())
->assertStatus(422)->assertJsonValidationErrors(['events.0.payload.deep_link']);
$ok1 = $this->item(['payload' => ['title' => 'x', 'deep_link' => '/people/42?x=1']]);
$this->putJson('/api/events', ['events' => [$ok1]], $this->auth())->assertOk();
$ok2 = $this->item(['topic' => 'birthday:2027', 'payload' => ['title' => 'x', 'deep_link' => 'https://docs.example/x']]);
$this->putJson('/api/events', ['events' => [$ok2]], $this->auth())->assertOk();
$ok3 = $this->item(['topic' => 'birthday:2028', 'payload' => ['title' => 'x', 'deep_link' => 'HTTPS://docs.example/x']]);
$this->putJson('/api/events', ['events' => [$ok3]], $this->auth())->assertOk();
}
public function test_topic_defaults_to_empty_string(): void

View File

@@ -80,6 +80,13 @@ class InboxPageTest extends TestCase
$this->assertStringContainsString('НОВОЕ СОБЫТИЕ', $html);
$this->assertStringContainsString('/inbox.js', $html);
$this->assertStringContainsString('csrf-token', $html);
// инлайн-ошибка карточки и обработка 419 в клиентском скрипте
$this->assertStringContainsString('card-err', $html);
$this->assertStringContainsString('419', file_get_contents(public_path('inbox.js')));
// Запланировано — витрина, без действий «Помню»/«Сделано»
$this->assertStringContainsString('>Помню<', $html);
$scheduledSection = substr($html, (int) strpos($html, '予定'));
$this->assertStringNotContainsString('>Помню<', $scheduledSection);
}
public function test_counter_hidden_when_zero(): void
@@ -106,12 +113,20 @@ class InboxPageTest extends TestCase
'after_due' => AfterDue::Keep, 'payload' => ['title' => 'Протокольная ссылка', 'deep_link' => '//evil.com/x'],
'state' => EventState::Today, 'quiet_until' => CarbonImmutable::parse('2000-01-01Z'),
]);
Event::create([
'source_id' => $source->id, 'user_id' => $user->id, 'source_ref' => 'r', 'topic' => 'c',
'due_mode' => DueMode::Local, 'due_date' => '2026-09-04', 'due_time' => '24:00:00', 'fire_on' => '2026-09-04',
'after_due' => AfterDue::Keep, 'payload' => ['title' => 'Обратный слэш', 'deep_link' => '/\evil.com'],
'state' => EventState::Today, 'quiet_until' => CarbonImmutable::parse('2000-01-01Z'),
]);
$html = $this->get('/', ['X-Remote-User' => 'nikita'])->assertOk()->getContent();
$this->assertStringNotContainsString('href="javascript:', $html);
$this->assertStringNotContainsString('href="//evil', $html);
$this->assertStringNotContainsString('href="/\\', $html);
$this->assertStringContainsString('Опасное событие', $html);
$this->assertStringContainsString('Протокольная ссылка', $html);
$this->assertStringContainsString('Обратный слэш', $html);
}
}

View File

@@ -100,6 +100,7 @@ class MeApiTest extends TestCase
$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

View File

@@ -110,5 +110,6 @@ class SchemaTest extends TestCase
$this->assertTrue(EventState::Overdue->countsInBadge());
$this->assertFalse(EventState::Preparing->countsInBadge());
$this->assertSame(['scheduled', 'preparing', 'today', 'overdue'], EventState::nonTerminalValues());
$this->assertSame(['today', 'overdue'], EventState::badgeValues());
}
}

View File

@@ -43,6 +43,13 @@ class TelegramWebhookTest extends TestCase
$this->postJson('/hooks/telegram', ['message' => []], ['X-Telegram-Bot-Api-Secret-Token' => 'wrong'])->assertStatus(403);
}
public function test_public_route_sets_no_session_cookie(): void
{
$this->postJson('/hooks/telegram', ['message' => []])
->assertStatus(403)
->assertHeaderMissing('Set-Cookie');
}
public function test_start_with_code_links_chat(): void
{
Cache::put('tg:link:ABC123', $this->user->id, 900);

View File

@@ -3,6 +3,7 @@
namespace Tests\Feature;
use App\Channels\ChannelRegistry;
use App\Delivery\DeliverJob;
use App\Delivery\Tick;
use App\Enums\AfterDue;
use App\Enums\ChannelType;
@@ -20,6 +21,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
use Tests\Support\FakeChannel;
use Tests\Support\FakeRegistry;
use Tests\TestCase;
@@ -29,9 +31,13 @@ class TickTest extends TestCase
use RefreshDatabase;
private User $user;
private Source $source;
private FakeChannel $web;
private FakeChannel $telegram;
private FakeChannel $webhook;
protected function setUp(): void
@@ -105,6 +111,18 @@ class TickTest extends TestCase
$this->assertSame(2, Delivery::count(), 'новое окно — новая доставка');
}
public function test_pending_row_exists_before_job_runs(): void
{
Queue::fake();
$this->event('2026-09-30', '2026-08-31');
$this->webhook->presence = Presence::Present;
$this->tickAt('2026-09-05T10:00:00Z');
$this->assertSame(DeliveryResult::Pending, Delivery::sole()->result);
Queue::assertPushed(DeliverJob::class, 1);
}
public function test_falls_back_to_all_channels_near_window_end(): void
{
$this->event('2026-09-30', '2026-08-31');