fix: ios 16
This commit is contained in:
@@ -7,8 +7,7 @@ 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 Illuminate\Support\Facades\Process;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use RuntimeException;
|
||||
@@ -25,137 +24,176 @@ class ChannelProjectServiceTest extends TestCase
|
||||
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'],
|
||||
'coruna.channel_builder.python' => 'python3',
|
||||
'coruna.channel_builder.artifact_root' => storage_path('app/channel-artifacts-test'),
|
||||
'coruna.channel_builder.timeout' => 30,
|
||||
'coruna.channel_domains' => ['fallback.test'],
|
||||
'coruna.static_site.base_url' => 'https://static.test/assets',
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_builds_with_authenticated_separate_domain_lists(): void
|
||||
private function fakeBuildResult(array $overrides = []): string
|
||||
{
|
||||
Http::fake(['builder.test/*' => Http::response(['ok' => true])]);
|
||||
$payload = array_merge([
|
||||
'schema_version' => 1,
|
||||
'status' => 'built',
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'seeds' => [
|
||||
'deployment_seed' => '11111111111111111111111111111111',
|
||||
'reporting_seed' => '11111111111111111111111111111111',
|
||||
],
|
||||
'domains' => [
|
||||
'deployment' => ['www.dep.example'],
|
||||
'reporting' => ['www.dep.example'],
|
||||
],
|
||||
'seeds_initialized' => true,
|
||||
'sync_rebuilt' => true,
|
||||
'support_path' => '/web/'.self::CHANNEL_ID.'/support.html',
|
||||
'daily_path' => '/sync/daily.html',
|
||||
], $overrides);
|
||||
|
||||
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',
|
||||
]);
|
||||
return "log line\n".ChannelProjectService::RESULT_MARKER.json_encode($payload)."\n";
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_prefers_channel_domains_for_deployment_and_env_for_reporting(): void
|
||||
public function it_invokes_new_project_script_and_returns_seeds(): 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',
|
||||
Process::fake([
|
||||
'*' => Process::result(output: $this->fakeBuildResult()),
|
||||
]);
|
||||
|
||||
$result = app(ChannelProjectService::class)->generate(self::CHANNEL_ID, 'blank');
|
||||
|
||||
$this->assertTrue($result['seeds_initialized']);
|
||||
$this->assertSame('11111111111111111111111111111111', $result['seeds']['deployment_seed']);
|
||||
$this->assertSame(['www.dep.example'], $result['domains']['deployment']);
|
||||
$this->assertSame('/web/'.self::CHANNEL_ID.'/support.html', $result['support_path']);
|
||||
|
||||
Process::assertRan(function ($process) {
|
||||
$cmd = $process->command;
|
||||
if (is_array($cmd)) {
|
||||
$joined = implode(' ', $cmd);
|
||||
} else {
|
||||
$joined = (string) $cmd;
|
||||
}
|
||||
|
||||
return str_contains($joined, 'new_project.py')
|
||||
&& str_contains($joined, self::CHANNEL_ID)
|
||||
&& str_contains($joined, '--support-template')
|
||||
&& str_contains($joined, 'blank');
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_forwards_support_template_to_builder(): void
|
||||
public function it_forwards_optional_seeds_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',
|
||||
Process::fake([
|
||||
'*' => Process::result(output: $this->fakeBuildResult(['seeds_initialized' => false, 'sync_rebuilt' => false])),
|
||||
]);
|
||||
|
||||
app(ChannelProjectService::class)->generate(
|
||||
self::CHANNEL_ID,
|
||||
'test',
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
);
|
||||
|
||||
Process::assertRan(function ($process) {
|
||||
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
|
||||
|
||||
return str_contains($joined, '--deployment-seed')
|
||||
&& str_contains($joined, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
|
||||
&& str_contains($joined, '--reporting-seed')
|
||||
&& substr_count($joined, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') >= 2;
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_rejects_partial_seed_pair(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('deployment_seed 与 reporting_seed 必须同时提供');
|
||||
|
||||
app(ChannelProjectService::class)->generate(self::CHANNEL_ID, 'test', 'only-one', null);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_rejects_mismatched_seed_pair(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('deployment_seed 与 reporting_seed 必须相同');
|
||||
|
||||
app(ChannelProjectService::class)->generate(
|
||||
self::CHANNEL_ID,
|
||||
'test',
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
);
|
||||
}
|
||||
|
||||
#[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',
|
||||
Process::fake([
|
||||
'*' => Process::result(output: '', errorOutput: 'boom', exitCode: 2),
|
||||
]);
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('生成渠道资源失败: boom');
|
||||
|
||||
app(ChannelProjectService::class)->generate(self::CHANNEL_ID);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function delete_invokes_delete_script(): void
|
||||
{
|
||||
Process::fake([
|
||||
'*' => Process::result(output: ChannelProjectService::RESULT_MARKER.json_encode([
|
||||
'status' => 'deleted',
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'removed' => true,
|
||||
])."\n"),
|
||||
]);
|
||||
|
||||
app(ChannelProjectService::class)->deleteWebTree(self::CHANNEL_ID);
|
||||
|
||||
Process::assertRan(function ($process) {
|
||||
$joined = is_array($process->command) ? implode(' ', $process->command) : (string) $process->command;
|
||||
|
||||
return str_contains($joined, 'delete_channel_web.py')
|
||||
&& str_contains($joined, self::CHANNEL_ID);
|
||||
});
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function support_links_use_online_web_path(): void
|
||||
{
|
||||
$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',
|
||||
'https://static.test/assets/web/'.self::CHANNEL_ID.'/support.html',
|
||||
'https://channel.test/web/'.self::CHANNEL_ID.'/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',
|
||||
'https://static.test/assets/web/'.self::CHANNEL_ID.'/support.html',
|
||||
'https://fallback.test/web/'.self::CHANNEL_ID.'/support.html',
|
||||
], $fallback->supportLinks());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function failed_database_insert_compensates_with_remote_delete(): void
|
||||
public function failed_database_insert_compensates_with_delete(): void
|
||||
{
|
||||
Http::fake([
|
||||
'builder.test/*' => Http::sequence()
|
||||
->push(['ok' => true], 200)
|
||||
->push([], 204),
|
||||
Process::fake([
|
||||
'*' => Process::sequence()
|
||||
->push(Process::result(output: $this->fakeBuildResult()))
|
||||
->push(Process::result(output: ChannelProjectService::RESULT_MARKER.json_encode([
|
||||
'status' => 'deleted',
|
||||
'removed' => true,
|
||||
])."\n")),
|
||||
]);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
|
||||
@@ -175,22 +213,34 @@ class ChannelProjectServiceTest extends TestCase
|
||||
Channel::setEventDispatcher($originalDispatcher);
|
||||
}
|
||||
|
||||
Http::assertSentCount(2);
|
||||
Http::assertSent(fn ($request) => $request->method() === 'DELETE');
|
||||
Process::assertRan(fn ($process) => str_contains(
|
||||
is_array($process->command) ? implode(' ', $process->command) : (string) $process->command,
|
||||
'delete_channel_web.py'
|
||||
));
|
||||
$this->assertDatabaseMissing('channels', ['channel_id' => self::CHANNEL_ID]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function duplicate_insert_loser_does_not_delete_winners_remote_project(): void
|
||||
public function duplicate_insert_loser_does_not_delete_winners_project(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$projects = Mockery::mock(ChannelProjectService::class);
|
||||
$projects->shouldReceive('generate')->once()->andReturnUsing(function (): void {
|
||||
$projects->shouldReceive('generate')->once()->andReturnUsing(function (): array {
|
||||
Channel::query()->create([
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'user_id' => 0,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
return [
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'seeds' => ['deployment_seed' => 'a', 'reporting_seed' => 'b'],
|
||||
'domains' => ['deployment' => [], 'reporting' => []],
|
||||
'seeds_initialized' => false,
|
||||
'sync_rebuilt' => false,
|
||||
'support_path' => '/web/'.self::CHANNEL_ID.'/support.html',
|
||||
'daily_path' => '/sync/daily.html',
|
||||
];
|
||||
});
|
||||
$projects->shouldNotReceive('deleteWebTree');
|
||||
$this->app->instance(ChannelProjectService::class, $projects);
|
||||
@@ -207,9 +257,14 @@ class ChannelProjectServiceTest extends TestCase
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function destroy_deletes_remote_project_before_database_row(): void
|
||||
public function destroy_deletes_web_tree_before_database_row(): void
|
||||
{
|
||||
Http::fake(['builder.test/*' => Http::response([], 204)]);
|
||||
Process::fake([
|
||||
'*' => Process::result(output: ChannelProjectService::RESULT_MARKER.json_encode([
|
||||
'status' => 'deleted',
|
||||
'removed' => true,
|
||||
])."\n"),
|
||||
]);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$channel = Channel::query()->create([
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
@@ -222,14 +277,19 @@ class ChannelProjectServiceTest extends TestCase
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0);
|
||||
|
||||
Http::assertSent(fn ($request) => $request->method() === 'DELETE');
|
||||
Process::assertRan(fn ($process) => str_contains(
|
||||
is_array($process->command) ? implode(' ', $process->command) : (string) $process->command,
|
||||
'delete_channel_web.py'
|
||||
));
|
||||
$this->assertDatabaseMissing('channels', ['id' => $channel->id]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function destroy_keeps_database_row_when_remote_delete_fails(): void
|
||||
public function destroy_keeps_database_row_when_delete_fails(): void
|
||||
{
|
||||
Http::fake(['builder.test/*' => Http::response(['message' => 'busy'], 503)]);
|
||||
Process::fake([
|
||||
'*' => Process::result(output: '', errorOutput: 'busy', exitCode: 1),
|
||||
]);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$channel = Channel::query()->create([
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
@@ -244,4 +304,29 @@ class ChannelProjectServiceTest extends TestCase
|
||||
|
||||
$this->assertDatabaseHas('channels', ['id' => $channel->id]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function store_returns_seeds_and_domains(): void
|
||||
{
|
||||
Process::fake([
|
||||
'*' => Process::result(output: $this->fakeBuildResult()),
|
||||
]);
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->post(route('admin.channels.store'), [
|
||||
'channel_id' => self::CHANNEL_ID,
|
||||
'user_id' => 0,
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.seeds.deployment_seed', '11111111111111111111111111111111')
|
||||
->assertJsonPath('data.domains.deployment.0', 'www.dep.example')
|
||||
->assertJsonPath('data.seeds_initialized', true)
|
||||
->assertJsonPath('data.daily_path', '/sync/daily.html');
|
||||
|
||||
$this->assertDatabaseHas('channels', ['channel_id' => self::CHANNEL_ID]);
|
||||
$channel = Channel::query()->where('channel_id', self::CHANNEL_ID)->first();
|
||||
$this->assertSame([], $channel->domains ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user