php-content/tests/Unit/ContentWebhookEndpointTest.php

286 lines
9.1 KiB
PHP
Raw Normal View History

2026-01-26 23:59:46 +00:00
<?php
declare(strict_types=1);
namespace Core\Mod\Content\Tests\Unit;
2026-01-26 23:59:46 +00:00
use Core\Mod\Content\Models\ContentWebhookEndpoint;
2026-01-26 23:59:46 +00:00
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
class ContentWebhookEndpointTest extends TestCase
{
#[Test]
public function it_generates_uuid_on_creation(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create();
$this->assertNotNull($endpoint->uuid);
$this->assertMatchesRegularExpression(
'/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/',
$endpoint->uuid
);
}
#[Test]
public function it_generates_secret_on_creation(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create(['secret' => null]);
$this->assertNotNull($endpoint->secret);
$this->assertEquals(64, strlen($endpoint->secret));
}
#[Test]
public function it_can_verify_valid_signature(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'secret' => 'test-secret-key',
]);
$payload = '{"event": "test", "data": {}}';
$signature = hash_hmac('sha256', $payload, 'test-secret-key');
$this->assertTrue($endpoint->verifySignature($payload, $signature));
}
#[Test]
public function it_can_verify_github_style_signature(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'secret' => 'test-secret-key',
]);
$payload = '{"event": "test"}';
$signature = 'sha256='.hash_hmac('sha256', $payload, 'test-secret-key');
$this->assertTrue($endpoint->verifySignature($payload, $signature));
}
#[Test]
public function it_rejects_invalid_signature(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'secret' => 'test-secret-key',
]);
$payload = '{"event": "test"}';
$invalidSignature = 'invalid-signature';
$this->assertFalse($endpoint->verifySignature($payload, $invalidSignature));
}
#[Test]
public function it_rejects_webhook_without_secret_when_signature_required(): void
2026-01-26 23:59:46 +00:00
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'secret' => null,
'require_signature' => true,
]);
// When signature is required but no secret configured, verification should fail
$result = $endpoint->verifySignatureWithDetails('any payload', null);
$this->assertFalse($result['verified']);
$this->assertEquals(ContentWebhookEndpoint::SIGNATURE_FAILURE_NO_SECRET, $result['reason']);
}
#[Test]
public function it_allows_webhook_without_secret_when_signature_not_required(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'secret' => null,
'require_signature' => false,
]);
// When signature is not required and no secret, verification should pass
$result = $endpoint->verifySignatureWithDetails('any payload', null);
$this->assertTrue($result['verified']);
$this->assertEquals(ContentWebhookEndpoint::SIGNATURE_SUCCESS_NOT_REQUIRED, $result['reason']);
}
#[Test]
public function it_provides_detailed_verification_result_on_success(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'secret' => 'test-secret-key',
]);
$payload = '{"event": "test"}';
$signature = hash_hmac('sha256', $payload, 'test-secret-key');
$result = $endpoint->verifySignatureWithDetails($payload, $signature);
$this->assertTrue($result['verified']);
$this->assertEquals(ContentWebhookEndpoint::SIGNATURE_SUCCESS, $result['reason']);
}
#[Test]
public function it_provides_detailed_verification_result_on_missing_signature(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'secret' => 'test-secret-key',
]);
$result = $endpoint->verifySignatureWithDetails('{"event": "test"}', null);
$this->assertFalse($result['verified']);
$this->assertEquals(ContentWebhookEndpoint::SIGNATURE_FAILURE_MISSING, $result['reason']);
}
#[Test]
public function it_provides_detailed_verification_result_on_invalid_signature(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'secret' => 'test-secret-key',
]);
$result = $endpoint->verifySignatureWithDetails('{"event": "test"}', 'invalid-signature');
$this->assertFalse($result['verified']);
$this->assertEquals(ContentWebhookEndpoint::SIGNATURE_FAILURE_INVALID, $result['reason']);
}
#[Test]
public function it_defaults_require_signature_to_true(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create();
2026-01-26 23:59:46 +00:00
$this->assertTrue($endpoint->requiresSignature());
2026-01-26 23:59:46 +00:00
}
#[Test]
public function it_checks_allowed_event_types(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'allowed_types' => ['wordpress.post_created', 'wordpress.post_updated'],
]);
$this->assertTrue($endpoint->isTypeAllowed('wordpress.post_created'));
$this->assertTrue($endpoint->isTypeAllowed('wordpress.post_updated'));
$this->assertFalse($endpoint->isTypeAllowed('wordpress.post_deleted'));
$this->assertFalse($endpoint->isTypeAllowed('cms.content_created'));
}
#[Test]
public function it_allows_all_types_when_empty(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'allowed_types' => [],
]);
$this->assertTrue($endpoint->isTypeAllowed('wordpress.post_created'));
$this->assertTrue($endpoint->isTypeAllowed('cms.content_created'));
$this->assertTrue($endpoint->isTypeAllowed('generic.payload'));
}
#[Test]
public function it_tracks_failure_count(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'failure_count' => 0,
'is_enabled' => true,
]);
$endpoint->incrementFailureCount();
$endpoint->refresh();
$this->assertEquals(1, $endpoint->failure_count);
$this->assertTrue($endpoint->is_enabled);
}
#[Test]
public function it_auto_disables_after_max_failures(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'failure_count' => ContentWebhookEndpoint::MAX_FAILURES - 1,
'is_enabled' => true,
]);
$endpoint->incrementFailureCount();
$endpoint->refresh();
$this->assertEquals(ContentWebhookEndpoint::MAX_FAILURES, $endpoint->failure_count);
$this->assertFalse($endpoint->is_enabled);
}
#[Test]
public function it_detects_circuit_breaker_state(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'failure_count' => ContentWebhookEndpoint::MAX_FAILURES,
]);
$this->assertTrue($endpoint->isCircuitBroken());
$endpoint->update(['failure_count' => 0]);
$endpoint->refresh();
$this->assertFalse($endpoint->isCircuitBroken());
}
#[Test]
public function it_resets_failure_count(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create([
'failure_count' => 5,
]);
$endpoint->resetFailureCount();
$endpoint->refresh();
$this->assertEquals(0, $endpoint->failure_count);
$this->assertNotNull($endpoint->last_received_at);
}
#[Test]
public function it_can_regenerate_secret(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create();
$originalSecret = $endpoint->secret;
$newSecret = $endpoint->regenerateSecret();
$endpoint->refresh();
$this->assertNotEquals($originalSecret, $newSecret);
$this->assertEquals($newSecret, $endpoint->secret);
}
#[Test]
public function it_generates_endpoint_url(): void
{
$endpoint = ContentWebhookEndpoint::factory()->create();
$url = $endpoint->getEndpointUrl();
$this->assertStringContainsString('/api/content/webhooks/', $url);
$this->assertStringContainsString($endpoint->uuid, $url);
}
#[Test]
public function it_provides_status_attributes(): void
{
// Active endpoint
$active = ContentWebhookEndpoint::factory()->create([
'is_enabled' => true,
'failure_count' => 0,
]);
$this->assertEquals('green', $active->status_color);
$this->assertEquals('Active', $active->status_label);
// Disabled endpoint
$disabled = ContentWebhookEndpoint::factory()->create([
'is_enabled' => false,
'failure_count' => 0,
]);
$this->assertEquals('zinc', $disabled->status_color);
$this->assertEquals('Disabled', $disabled->status_label);
// Circuit broken
$broken = ContentWebhookEndpoint::factory()->circuitBroken()->create();
$this->assertEquals('red', $broken->status_color);
$this->assertStringContainsString('Circuit', $broken->status_label);
}
}