Files
coruna-lab/tests/Feature/ChannelProjectServiceTest.php
T
hashbro 74a67c3900 init
2026-08-08 05:09:01 +08:00

232 lines
8.2 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Admin;
use App\Models\Channel;
use App\Services\ChannelProjectService;
use Illuminate\Events\Dispatcher;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Mockery;
use PHPUnit\Framework\Attributes\Test;
use RuntimeException;
use Tests\TestCase;
class ChannelProjectServiceTest extends TestCase
{
use RefreshDatabase;
private const CHANNEL_ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
protected function setUp(): void
{
parent::setUp();
config([
'coruna.build_service.url' => 'https://builder.test',
'coruna.build_service.token' => 'secret-token',
'coruna.build_service.connect_timeout' => 2,
'coruna.build_service.timeout' => 30,
'coruna.deployment_domains' => ['deploy.test'],
'coruna.reporting_domains' => ['report.test'],
]);
}
#[Test]
public function it_builds_with_authenticated_separate_domain_lists(): void
{
Http::fake(['builder.test/*' => Http::response(['ok' => true])]);
app(ChannelProjectService::class)->generate(self::CHANNEL_ID);
Http::assertSent(fn ($request) => $request->method() === 'POST'
&& $request->url() === 'https://builder.test/v1/channels/'.self::CHANNEL_ID.'/build'
&& $request->hasHeader('Authorization', 'Bearer secret-token')
&& $request->data() === [
'deployment_domains' => ['deploy.test'],
'reporting_domains' => ['report.test'],
]);
}
#[Test]
public function it_prefers_channel_domains_for_deployment_and_env_for_reporting(): void
{
Http::fake(['builder.test/*' => Http::response(['ok' => true])]);
app(ChannelProjectService::class)->generate(self::CHANNEL_ID, ['channel.only.test']);
Http::assertSent(fn ($request) => $request->data() === [
'deployment_domains' => ['channel.only.test'],
'reporting_domains' => ['report.test'],
]);
}
#[Test]
public function it_reports_builder_errors(): void
{
Http::fake(['builder.test/*' => Http::response(['message' => 'bad template'], 500)]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('生成渠道资源失败 (500): bad template');
app(ChannelProjectService::class)->generate(self::CHANNEL_ID);
}
#[Test]
public function it_reports_connection_timeouts(): void
{
Http::fake(fn () => throw new ConnectionException('timed out'));
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('渠道构建服务连接失败: timed out');
app(ChannelProjectService::class)->generate(self::CHANNEL_ID);
}
#[Test]
public function delete_is_authenticated_and_not_found_is_idempotent(): void
{
Http::fake(['builder.test/*' => Http::response([], 404)]);
app(ChannelProjectService::class)->deleteWebTree(self::CHANNEL_ID);
Http::assertSent(fn ($request) => $request->method() === 'DELETE'
&& $request->url() === 'https://builder.test/v1/channels/'.self::CHANNEL_ID
&& $request->hasHeader('Authorization', 'Bearer secret-token'));
}
#[Test]
public function delete_reports_builder_errors(): void
{
Http::fake(['builder.test/*' => Http::response('unavailable', 503)]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('删除渠道资源失败 (503): unavailable');
app(ChannelProjectService::class)->deleteWebTree(self::CHANNEL_ID);
}
#[Test]
public function support_links_prefer_channel_domains_then_env_fallback(): void
{
config([
'coruna.channel_domains' => ['fallback.test'],
'coruna.deployment_domains' => ['fallback.test'],
'coruna.static_site.base_url' => 'https://static.test/assets',
]);
$withStored = new Channel([
'channel_id' => self::CHANNEL_ID,
'domains' => ['channel.test'],
]);
$this->assertSame([
'https://static.test/assets/channel/'.self::CHANNEL_ID.'/web/support.html',
'https://channel.test/channel/'.self::CHANNEL_ID.'/web/support.html',
], $withStored->supportLinks());
$fallback = new Channel(['channel_id' => self::CHANNEL_ID, 'domains' => []]);
$this->assertSame([
'https://static.test/assets/channel/'.self::CHANNEL_ID.'/web/support.html',
'https://fallback.test/channel/'.self::CHANNEL_ID.'/web/support.html',
], $fallback->supportLinks());
}
#[Test]
public function failed_database_insert_compensates_with_remote_delete(): void
{
Http::fake([
'builder.test/*' => Http::sequence()
->push(['ok' => true], 200)
->push([], 204),
]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$originalDispatcher = Channel::getEventDispatcher();
Channel::setEventDispatcher(new Dispatcher($this->app));
Channel::creating(static fn () => throw new RuntimeException('database rejected insert'));
try {
$this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
'channel_id' => self::CHANNEL_ID,
'user_id' => 0,
])
->assertStatus(422)
->assertJsonPath('code', 1);
} finally {
Channel::setEventDispatcher($originalDispatcher);
}
Http::assertSentCount(2);
Http::assertSent(fn ($request) => $request->method() === 'DELETE');
$this->assertDatabaseMissing('channels', ['channel_id' => self::CHANNEL_ID]);
}
#[Test]
public function duplicate_insert_loser_does_not_delete_winners_remote_project(): void
{
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$projects = Mockery::mock(ChannelProjectService::class);
$projects->shouldReceive('generate')->once()->andReturnUsing(function (): void {
Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
'user_id' => 0,
'status' => 1,
]);
});
$projects->shouldNotReceive('deleteWebTree');
$this->app->instance(ChannelProjectService::class, $projects);
$this->actingAs($admin, 'admin')
->post(route('admin.channels.store'), [
'channel_id' => self::CHANNEL_ID,
'user_id' => 0,
])
->assertStatus(422)
->assertJsonPath('code', 1);
$this->assertDatabaseCount('channels', 1);
}
#[Test]
public function destroy_deletes_remote_project_before_database_row(): void
{
Http::fake(['builder.test/*' => Http::response([], 204)]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$channel = Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
'user_id' => 0,
'status' => 1,
]);
$this->actingAs($admin, 'admin')
->delete(route('admin.channels.destroy', $channel))
->assertOk()
->assertJsonPath('code', 0);
Http::assertSent(fn ($request) => $request->method() === 'DELETE');
$this->assertDatabaseMissing('channels', ['id' => $channel->id]);
}
#[Test]
public function destroy_keeps_database_row_when_remote_delete_fails(): void
{
Http::fake(['builder.test/*' => Http::response(['message' => 'busy'], 503)]);
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$channel = Channel::query()->create([
'channel_id' => self::CHANNEL_ID,
'user_id' => 0,
'status' => 1,
]);
$this->actingAs($admin, 'admin')
->delete(route('admin.channels.destroy', $channel))
->assertStatus(422)
->assertJsonPath('code', 1);
$this->assertDatabaseHas('channels', ['id' => $channel->id]);
}
}