248 lines
8.7 KiB
PHP
248 lines
8.7 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'],
|
|
'support_template' => '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'],
|
|
'support_template' => 'test',
|
|
]);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_forwards_support_template_to_builder(): void
|
|
{
|
|
Http::fake(['builder.test/*' => Http::response(['ok' => true])]);
|
|
|
|
app(ChannelProjectService::class)->generate(self::CHANNEL_ID, null, 'blank');
|
|
|
|
Http::assertSent(fn ($request) => $request->data() === [
|
|
'deployment_domains' => ['deploy.test'],
|
|
'reporting_domains' => ['report.test'],
|
|
'support_template' => 'blank',
|
|
]);
|
|
}
|
|
|
|
#[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]);
|
|
}
|
|
}
|