feat: web, webhook and telegram channel drivers

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 07:18:37 -03:00
parent 1b03fe0796
commit 1b0a559e3c
11 changed files with 487 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
<?php
namespace Tests\Unit\Channels;
use App\Channels\WebhookChannel;
use App\Enums\AfterDue;
use App\Enums\DueMode;
use App\Enums\EventState;
use App\Enums\Presence;
use App\Models\Delivery;
use App\Models\Event;
use App\Models\Source;
use App\Models\User;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
use Tests\TestCase;
class WebhookChannelTest extends TestCase
{
private array $config = ['deliver_url' => 'https://ha.test/api/webhook/hado', 'presence_url' => 'https://ha.test/api/hado/presence'];
protected function setUp(): void
{
parent::setUp();
Route::post('/a/{token}/ack', fn () => '')->name('action.ack');
Route::post('/a/{token}/done', fn () => '')->name('action.done');
// Laravel only refreshes the named-route lookup table once, right after the app's own
// route files finish loading during boot. Routes added later (e.g. here in setUp) are
// appended to the collection but never indexed by name unless we refresh explicitly,
// so route('action.ack', ...) would otherwise throw RouteNotFoundException. This does
// not affect production: Task 10's real routes live in the route files that already
// get refreshed automatically.
Route::getRoutes()->refreshNameLookups();
}
private function driver(): WebhookChannel
{
return new WebhookChannel(2);
}
public function test_presence_from_json(): void
{
Http::fake([
'ha.test/api/hado/presence' => Http::sequence()
->push(['present' => true])
->push(['present' => false])
->push('oops', 500),
]);
$user = new User(['login' => 'n', 'tz' => 'UTC']);
$this->assertSame(Presence::Present, $this->driver()->presence($user, $this->config));
$this->assertSame(Presence::Absent, $this->driver()->presence($user, $this->config));
$this->assertSame(Presence::Unknown, $this->driver()->presence($user, $this->config));
}
public function test_presence_unknown_on_timeout(): void
{
Http::fake(fn () => throw new ConnectionException('timeout'));
$this->assertSame(Presence::Unknown, $this->driver()->presence(new User(['login' => 'n', 'tz' => 'UTC']), $this->config));
}
public function test_deliver_posts_event_and_action_urls(): void
{
Http::fake(['ha.test/api/webhook/hado' => Http::response('', 200)]);
$event = new Event([
'due_mode' => DueMode::Local, 'due_date' => '2026-08-14', 'due_time' => '24:00:00', 'fire_on' => '2026-08-14',
'after_due' => AfterDue::Expire, 'state' => EventState::Today,
'payload' => ['title' => 'Сегодня ДР — Вася', 'subtitle' => 'исполняется 34', 'deep_link' => '/people/42', 'done_label' => 'Поздравил'],
]);
$event->id = 17;
$event->setRelation('source', new Source(['name' => 'docs']));
$delivery = new Delivery(['action_token' => 'tok123']);
$outcome = $this->driver()->deliver($event, new User(['login' => 'n', 'tz' => 'UTC']), $this->config, $delivery);
$this->assertTrue($outcome->ok);
Http::assertSent(function (Request $r) {
return $r->url() === 'https://ha.test/api/webhook/hado'
&& $r['event']['id'] === 17
&& $r['event']['title'] === 'Сегодня ДР — Вася'
&& $r['event']['done_label'] === 'Поздравил'
&& $r['event']['source'] === 'docs'
&& $r['event']['due_at'] === '2026-08-14'
&& str_ends_with($r['actions']['ack'], '/a/tok123/ack')
&& str_ends_with($r['actions']['done'], '/a/tok123/done');
});
}
public function test_deliver_reports_failure(): void
{
Http::fake(['ha.test/api/webhook/hado' => Http::response('nope', 503)]);
$event = new Event(['due_mode' => DueMode::Local, 'due_date' => '2026-08-14', 'due_time' => '24:00:00', 'fire_on' => '2026-08-14', 'after_due' => AfterDue::Expire, 'state' => EventState::Today, 'payload' => ['title' => 't']]);
$event->setRelation('source', new Source(['name' => 'docs']));
$outcome = $this->driver()->deliver($event, new User(['login' => 'n', 'tz' => 'UTC']), $this->config, new Delivery(['action_token' => 'x']));
$this->assertFalse($outcome->ok);
$this->assertStringContainsString('503', $outcome->error);
}
}