feat: source tokens and client events API
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
117
core/tests/Feature/ClientApiTest.php
Normal file
117
core/tests/Feature/ClientApiTest.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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_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();
|
||||
}
|
||||
}
|
||||
29
core/tests/Feature/SourceCreateCommandTest.php
Normal file
29
core/tests/Feature/SourceCreateCommandTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Source;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SourceCreateCommandTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_creates_source_and_prints_token_once(): void
|
||||
{
|
||||
$this->artisan('hado:source:create', ['name' => 'docs'])
|
||||
->expectsOutputToContain('Token: hado_')
|
||||
->assertSuccessful();
|
||||
|
||||
$source = Source::where('name', 'docs')->sole();
|
||||
$this->assertSame(64, strlen($source->token_hash));
|
||||
}
|
||||
|
||||
public function test_refuses_duplicate_name(): void
|
||||
{
|
||||
Source::create(['name' => 'docs', 'token_hash' => hash('sha256', 'x')]);
|
||||
|
||||
$this->artisan('hado:source:create', ['name' => 'docs'])->assertFailed();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user