Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
142 lines
6.3 KiB
PHP
142 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Enums\EventState;
|
|
use App\Models\Event;
|
|
use App\Models\Source;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class ClientApiTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const TOKEN = 'hado_testtoken';
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
CarbonImmutable::setTestNow('2026-09-01T12:00:00Z');
|
|
Source::create(['name' => 'docs', 'token_hash' => hash('sha256', self::TOKEN)]);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
CarbonImmutable::setTestNow();
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function item(array $over = []): array
|
|
{
|
|
return array_replace([
|
|
'user' => 'nikita', 'source_ref' => 'person:42', 'topic' => 'birthday:2026',
|
|
'due_at' => '2026-08-14', 'fire_at' => '2026-08-14', 'after_due' => 'expire',
|
|
'payload' => ['title' => 'Сегодня ДР — Вася', 'deep_link' => '/people/42'],
|
|
], $over);
|
|
}
|
|
|
|
private function auth(): array
|
|
{
|
|
return ['Authorization' => 'Bearer '.self::TOKEN];
|
|
}
|
|
|
|
public function test_rejects_missing_or_wrong_token(): void
|
|
{
|
|
$this->putJson('/api/events', ['events' => [$this->item()]])->assertStatus(401);
|
|
$this->putJson('/api/events', ['events' => [$this->item()]], ['Authorization' => 'Bearer nope'])->assertStatus(401);
|
|
}
|
|
|
|
public function test_upserts_batch_and_returns_ids_and_states(): void
|
|
{
|
|
$this->putJson('/api/events', ['events' => [$this->item()]], $this->auth())
|
|
->assertOk()
|
|
->assertJsonPath('events.0.source_ref', 'person:42')
|
|
->assertJsonPath('events.0.topic', 'birthday:2026')
|
|
->assertJsonPath('events.0.state', 'expired');
|
|
|
|
$this->assertSame(1, Event::count());
|
|
}
|
|
|
|
public function test_validation_rejects_whole_batch_with_index(): void
|
|
{
|
|
$bad = $this->item(['due_at' => 'tomorrow']);
|
|
$this->putJson('/api/events', ['events' => [$this->item(), $bad]], $this->auth())
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors(['events.1.due_at']);
|
|
|
|
$this->assertSame(0, Event::count(), 'ничего не применилось');
|
|
}
|
|
|
|
public function test_validation_rules(): void
|
|
{
|
|
$this->putJson('/api/events', ['events' => [$this->item(['fire_at' => '2026-08-15'])]], $this->auth())
|
|
->assertStatus(422)->assertJsonValidationErrors(['events.0.fire_at']);
|
|
$this->putJson('/api/events', ['events' => [$this->item(['after_due' => 'maybe'])]], $this->auth())
|
|
->assertStatus(422)->assertJsonValidationErrors(['events.0.after_due']);
|
|
$this->putJson('/api/events', ['events' => [$this->item(['payload' => ['subtitle' => 'x']])]], $this->auth())
|
|
->assertStatus(422)->assertJsonValidationErrors(['events.0.payload.title']);
|
|
$this->putJson('/api/events', ['events' => [$this->item(['user' => ''])]], $this->auth())
|
|
->assertStatus(422)->assertJsonValidationErrors(['events.0.user']);
|
|
$this->putJson('/api/events', ['events' => array_fill(0, 501, $this->item())], $this->auth())
|
|
->assertStatus(422)->assertJsonValidationErrors(['events']);
|
|
}
|
|
|
|
public function test_deep_link_must_be_relative_or_http(): void
|
|
{
|
|
$bad = $this->item(['payload' => ['title' => 'x', 'deep_link' => 'javascript:alert(1)']]);
|
|
$this->putJson('/api/events', ['events' => [$bad]], $this->auth())
|
|
->assertStatus(422)->assertJsonValidationErrors(['events.0.payload.deep_link']);
|
|
|
|
$protocolRelative = $this->item(['payload' => ['title' => 'x', 'deep_link' => '//evil.com/x']]);
|
|
$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
|
|
{
|
|
$item = $this->item();
|
|
unset($item['topic']);
|
|
$this->putJson('/api/events', ['events' => [$item]], $this->auth())->assertOk()->assertJsonPath('events.0.topic', '');
|
|
}
|
|
|
|
public function test_get_and_delete_by_identity(): void
|
|
{
|
|
$this->putJson('/api/events', ['events' => [$this->item(['due_at' => '2026-09-30', 'fire_at' => '2026-08-31', 'after_due' => 'keep'])]], $this->auth());
|
|
|
|
$this->getJson('/api/events?source_ref=person:42&topic=birthday:2026', $this->auth())
|
|
->assertOk()
|
|
->assertJsonPath('state', 'preparing')
|
|
->assertJsonPath('due_at', '2026-09-30')
|
|
->assertJsonPath('topic', 'birthday:2026');
|
|
|
|
$this->deleteJson('/api/events?source_ref=person:42&topic=birthday:2026', [], $this->auth())->assertNoContent();
|
|
$this->assertSame(EventState::Withdrawn, Event::sole()->state);
|
|
|
|
$this->getJson('/api/events?source_ref=person:42&topic=nope', $this->auth())->assertNotFound();
|
|
$this->deleteJson('/api/events?source_ref=person:42&topic=nope', [], $this->auth())->assertNotFound();
|
|
}
|
|
|
|
public function test_source_is_isolated_by_token(): void
|
|
{
|
|
Source::create(['name' => 'other', 'token_hash' => hash('sha256', 'other-token')]);
|
|
$this->putJson('/api/events', ['events' => [$this->item()]], $this->auth());
|
|
|
|
$this->getJson('/api/events?source_ref=person:42&topic=birthday:2026', ['Authorization' => 'Bearer other-token'])->assertNotFound();
|
|
}
|
|
}
|