Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
279
tests/Feature/Git/GiteaSyncTest.php
Normal file
279
tests/Feature/Git/GiteaSyncTest.php
Normal file
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Git;
|
||||
|
||||
use App\Enums\GitProvider;
|
||||
use App\Models\Repository;
|
||||
use App\Services\Git\GitException;
|
||||
use App\Services\Git\GitManager;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The collective hosts its own Gitea, so this is the driver that matters.
|
||||
*/
|
||||
class GiteaSyncTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected string $instance = 'https://git.pireon.test';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config()->set('git.providers.gitea.url', $this->instance);
|
||||
config()->set('git.providers.gitea.token', 'a-token');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $repo
|
||||
* @param array<int, array<string, mixed>> $issues
|
||||
* @param array<int, array<string, mixed>> $commits
|
||||
*/
|
||||
protected function fakeGitea(array $repo = [], array $issues = [], array $commits = []): void
|
||||
{
|
||||
Http::fake([
|
||||
"{$this->instance}/api/v1/repos/*/languages" => Http::response(['PHP' => 90000, 'Blade' => 12000]),
|
||||
"{$this->instance}/api/v1/repos/*/topics" => Http::response(['topics' => ['laravel', 'comunidad']]),
|
||||
"{$this->instance}/api/v1/repos/*/issues*" => Http::response($issues),
|
||||
"{$this->instance}/api/v1/repos/*/commits*" => Http::response($commits ?: [
|
||||
['author' => ['login' => 'ada'], 'commit' => ['author' => ['email' => 'ada@x.test']]],
|
||||
['author' => ['login' => 'grace'], 'commit' => ['author' => ['email' => 'grace@x.test']]],
|
||||
['author' => ['login' => 'ada'], 'commit' => ['author' => ['email' => 'ada@x.test']]],
|
||||
]),
|
||||
"{$this->instance}/api/v1/repos/*" => Http::response([
|
||||
'full_name' => 'colectivo/web',
|
||||
'html_url' => "{$this->instance}/colectivo/web",
|
||||
'description' => 'La web del colectivo',
|
||||
'website' => 'https://colectivo.test',
|
||||
'default_branch' => 'main',
|
||||
'language' => 'PHP',
|
||||
// Gitea's counter names differ from GitHub's.
|
||||
'stars_count' => 42,
|
||||
'forks_count' => 3,
|
||||
'watchers_count' => 7,
|
||||
'open_issues_count' => 5,
|
||||
'archived' => false,
|
||||
'fork' => false,
|
||||
'updated_at' => '2026-08-01T10:00:00Z',
|
||||
'created_at' => '2024-01-01T00:00:00Z',
|
||||
...$repo,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_reads_giteas_counter_names(): void
|
||||
{
|
||||
$this->fakeGitea();
|
||||
|
||||
$repository = Repository::factory()->create([
|
||||
'provider' => GitProvider::Gitea,
|
||||
'full_name' => 'colectivo/web',
|
||||
'stars' => 0,
|
||||
]);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
$repository->refresh();
|
||||
|
||||
$this->assertSame(42, $repository->stars);
|
||||
$this->assertSame(3, $repository->forks);
|
||||
$this->assertSame(7, $repository->watchers);
|
||||
$this->assertSame(5, $repository->open_issues_count);
|
||||
$this->assertSame('PHP', $repository->primary_language);
|
||||
$this->assertSame(['laravel', 'comunidad'], $repository->topics);
|
||||
$this->assertSame('https://colectivo.test', $repository->homepage);
|
||||
$this->assertNull($repository->sync_error);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_authenticates_with_giteas_token_scheme(): void
|
||||
{
|
||||
$this->fakeGitea();
|
||||
|
||||
app(GitManager::class)->sync(
|
||||
Repository::factory()->create(['provider' => GitProvider::Gitea, 'full_name' => 'colectivo/web'])
|
||||
);
|
||||
|
||||
// Gitea expects "token <t>", not "Bearer <t>".
|
||||
Http::assertSent(fn ($request) => $request->hasHeader('Authorization', 'token a-token'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_asks_gitea_to_exclude_pull_requests(): void
|
||||
{
|
||||
$this->fakeGitea();
|
||||
|
||||
app(GitManager::class)->sync(
|
||||
Repository::factory()->create(['provider' => GitProvider::Gitea, 'full_name' => 'colectivo/web'])
|
||||
);
|
||||
|
||||
// Unlike GitHub, Gitea can filter PRs server-side.
|
||||
Http::assertSent(fn ($request) => ! str_contains($request->url(), '/issues')
|
||||
|| str_contains($request->url(), 'type=issues'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_mirrors_only_newcomer_friendly_issues(): void
|
||||
{
|
||||
$this->fakeGitea(issues: [
|
||||
[
|
||||
'number' => 1, 'title' => 'Buena para empezar', 'html_url' => "{$this->instance}/x/1",
|
||||
'labels' => [['name' => 'good first issue']], 'comments' => 2,
|
||||
'user' => ['login' => 'ada'], 'assignees' => [],
|
||||
'created_at' => '2026-07-01T00:00:00Z', 'updated_at' => '2026-07-02T00:00:00Z',
|
||||
],
|
||||
[
|
||||
'number' => 2, 'title' => 'Se busca ayuda', 'html_url' => "{$this->instance}/x/2",
|
||||
'labels' => [['name' => 'Se busca ayuda']], 'comments' => 0,
|
||||
'user' => ['login' => 'ada'],
|
||||
// Gitea reports assignees as a list.
|
||||
'assignees' => [['login' => 'grace']],
|
||||
'created_at' => '2026-07-01T00:00:00Z', 'updated_at' => '2026-07-02T00:00:00Z',
|
||||
],
|
||||
[
|
||||
'number' => 3, 'title' => 'Refactor interno', 'html_url' => "{$this->instance}/x/3",
|
||||
'labels' => [['name' => 'chore']], 'comments' => 0,
|
||||
'user' => ['login' => 'ada'], 'assignees' => [],
|
||||
'created_at' => '2026-07-01T00:00:00Z', 'updated_at' => '2026-07-02T00:00:00Z',
|
||||
],
|
||||
]);
|
||||
|
||||
$repository = Repository::factory()->create([
|
||||
'provider' => GitProvider::Gitea,
|
||||
'full_name' => 'colectivo/web',
|
||||
]);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
$this->assertSame(2, $repository->issues()->count());
|
||||
$this->assertSame(1, $repository->goodFirstIssues()->count());
|
||||
|
||||
// The Spanish label spelling from config/git.php matches too.
|
||||
$claimed = $repository->issues()->where('number', 2)->first();
|
||||
$this->assertTrue($claimed->is_help_wanted);
|
||||
$this->assertSame('grace', $claimed->assignee);
|
||||
$this->assertFalse($claimed->isUnclaimed());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_contributor_count_is_derived_from_distinct_commit_authors(): void
|
||||
{
|
||||
$this->fakeGitea();
|
||||
|
||||
$repository = Repository::factory()->create([
|
||||
'provider' => GitProvider::Gitea,
|
||||
'full_name' => 'colectivo/web',
|
||||
]);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
// Gitea has no contributors endpoint: three commits by two people.
|
||||
$this->assertSame(2, $repository->fresh()->contributors_count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function commit_authors_without_an_account_still_count_once(): void
|
||||
{
|
||||
$this->fakeGitea(commits: [
|
||||
['commit' => ['author' => ['email' => 'sin-cuenta@x.test']]],
|
||||
['commit' => ['author' => ['email' => 'Sin-Cuenta@x.test']]],
|
||||
['author' => ['login' => 'ada'], 'commit' => ['author' => ['email' => 'ada@x.test']]],
|
||||
]);
|
||||
|
||||
$repository = Repository::factory()->create([
|
||||
'provider' => GitProvider::Gitea,
|
||||
'full_name' => 'colectivo/web',
|
||||
]);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
$this->assertSame(2, $repository->fresh()->contributors_count);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_private_repository_without_a_token_reports_the_real_cause(): void
|
||||
{
|
||||
Http::fake(["{$this->instance}/api/v1/repos/*" => Http::response([], 403)]);
|
||||
|
||||
$repository = Repository::factory()->create([
|
||||
'provider' => GitProvider::Gitea,
|
||||
'full_name' => 'colectivo/privado',
|
||||
]);
|
||||
|
||||
try {
|
||||
app(GitManager::class)->sync($repository);
|
||||
$this->fail('Expected a GitException.');
|
||||
} catch (GitException $e) {
|
||||
$this->assertSame(403, $e->status);
|
||||
$this->assertStringContainsString('GITEA_TOKEN', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function an_instance_that_is_down_is_treated_as_transient(): void
|
||||
{
|
||||
Http::fake(["{$this->instance}/api/v1/repos/*" => Http::response([], 502)]);
|
||||
|
||||
$repository = Repository::factory()->create([
|
||||
'provider' => GitProvider::Gitea,
|
||||
'full_name' => 'colectivo/web',
|
||||
]);
|
||||
|
||||
try {
|
||||
app(GitManager::class)->sync($repository);
|
||||
$this->fail('Expected a GitException.');
|
||||
} catch (GitException $e) {
|
||||
// Self-hosted instances restart; the job should retry, not give up.
|
||||
$this->assertTrue($e->isTransient);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function without_an_instance_url_gitea_is_not_offered_as_a_provider(): void
|
||||
{
|
||||
config()->set('git.providers.gitea.url', null);
|
||||
|
||||
// A driver with nowhere to point is worse than no driver: it would
|
||||
// only produce failures later.
|
||||
$this->assertFalse(GitProvider::Gitea->isSupported());
|
||||
|
||||
$repository = Repository::factory()->create(['provider' => GitProvider::Gitea]);
|
||||
|
||||
$this->expectException(GitException::class);
|
||||
$this->expectExceptionMessage('GITEA_URL');
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_bare_name_is_qualified_with_the_configured_org(): void
|
||||
{
|
||||
config()->set('git.providers.gitea.default_owner', 'colectivo');
|
||||
|
||||
$this->fakeGitea();
|
||||
|
||||
$repository = Repository::factory()->create([
|
||||
'provider' => GitProvider::Gitea,
|
||||
'full_name' => 'web',
|
||||
]);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
$this->assertSame('colectivo/web', $repository->fresh()->full_name);
|
||||
Http::assertSent(fn ($r) => str_contains($r->url(), '/repos/colectivo/web'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function gitea_is_the_default_provider(): void
|
||||
{
|
||||
$this->assertSame(GitProvider::Gitea, GitProvider::default());
|
||||
$this->assertSame('gitea', Repository::factory()->create()->provider->value);
|
||||
}
|
||||
}
|
||||
281
tests/Feature/Git/RepositorySyncTest.php
Normal file
281
tests/Feature/Git/RepositorySyncTest.php
Normal file
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Git;
|
||||
|
||||
use App\Enums\GitProvider;
|
||||
use App\Models\Repository;
|
||||
use App\Services\Git\GitException;
|
||||
use App\Services\Git\GitManager;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The GitHub driver is kept for mirrors and the odd repository hosted
|
||||
* elsewhere; Gitea is the collective's own instance (see GiteaSyncTest).
|
||||
* These tests also stand as the proof that the provider interface really is
|
||||
* substitutable.
|
||||
*/
|
||||
class RepositorySyncTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Gitea is the default, so GitHub has to be asked for explicitly.
|
||||
$this->githubDefaults = ['provider' => GitProvider::GitHub];
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
protected array $githubDefaults = [];
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $repo
|
||||
* @param array<int, array<string, mixed>> $issues
|
||||
*/
|
||||
protected function fakeGitHub(array $repo = [], array $issues = []): void
|
||||
{
|
||||
Http::fake([
|
||||
'api.github.com/repos/*/languages' => Http::response(['PHP' => 90000, 'Blade' => 12000]),
|
||||
'api.github.com/repos/*/contributors*' => Http::response([['login' => 'a']], 200, [
|
||||
// GitHub reports the total through the Link header's last page.
|
||||
'Link' => '<https://api.github.com/repositories/1/contributors?per_page=1&page=7>; rel="last"',
|
||||
]),
|
||||
'api.github.com/repos/*/issues*' => Http::response($issues),
|
||||
'api.github.com/repos/*' => Http::response([
|
||||
'full_name' => 'colectivo/web',
|
||||
'html_url' => 'https://github.com/colectivo/web',
|
||||
'description' => 'La web del colectivo',
|
||||
'default_branch' => 'main',
|
||||
'language' => 'PHP',
|
||||
'license' => ['spdx_id' => 'AGPL-3.0'],
|
||||
'topics' => ['laravel', 'community'],
|
||||
'stargazers_count' => 128,
|
||||
'forks_count' => 9,
|
||||
'subscribers_count' => 5,
|
||||
'open_issues_count' => 12,
|
||||
'archived' => false,
|
||||
'fork' => false,
|
||||
'pushed_at' => '2026-08-01T10:00:00Z',
|
||||
'created_at' => '2024-01-01T00:00:00Z',
|
||||
...$repo,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_mirrors_the_repository_stats(): void
|
||||
{
|
||||
$this->fakeGitHub();
|
||||
|
||||
$repository = Repository::factory()->create([
|
||||
...$this->githubDefaults,
|
||||
'full_name' => 'colectivo/web',
|
||||
'stars' => 0,
|
||||
'last_synced_at' => null,
|
||||
]);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
$repository->refresh();
|
||||
|
||||
$this->assertSame(128, $repository->stars);
|
||||
$this->assertSame('PHP', $repository->primary_language);
|
||||
$this->assertSame('AGPL-3.0', $repository->license);
|
||||
$this->assertSame(['laravel', 'community'], $repository->topics);
|
||||
$this->assertSame(['PHP' => 90000, 'Blade' => 12000], $repository->languages);
|
||||
|
||||
// Contributor count is read from the Link header's last page rather
|
||||
// than by paging through every contributor.
|
||||
$this->assertSame(7, $repository->contributors_count);
|
||||
|
||||
$this->assertNotNull($repository->last_synced_at);
|
||||
$this->assertNull($repository->sync_error);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function it_only_mirrors_newcomer_friendly_issues(): void
|
||||
{
|
||||
$this->fakeGitHub(issues: [
|
||||
[
|
||||
'number' => 1, 'title' => 'Good one', 'html_url' => 'https://x/1',
|
||||
'labels' => [['name' => 'good first issue']], 'comments' => 2,
|
||||
'user' => ['login' => 'ada'], 'assignee' => null,
|
||||
'created_at' => '2026-07-01T00:00:00Z', 'updated_at' => '2026-07-02T00:00:00Z',
|
||||
],
|
||||
[
|
||||
'number' => 2, 'title' => 'Help please', 'html_url' => 'https://x/2',
|
||||
'labels' => [['name' => 'Help Wanted']], 'comments' => 0,
|
||||
'user' => ['login' => 'ada'], 'assignee' => ['login' => 'bob'],
|
||||
'created_at' => '2026-07-01T00:00:00Z', 'updated_at' => '2026-07-02T00:00:00Z',
|
||||
],
|
||||
// Ordinary issue: noise for a newcomer, so it is not mirrored.
|
||||
[
|
||||
'number' => 3, 'title' => 'Refactor internals', 'html_url' => 'https://x/3',
|
||||
'labels' => [['name' => 'chore']], 'comments' => 0,
|
||||
'user' => ['login' => 'ada'], 'assignee' => null,
|
||||
'created_at' => '2026-07-01T00:00:00Z', 'updated_at' => '2026-07-02T00:00:00Z',
|
||||
],
|
||||
// A pull request is not something anyone picks up.
|
||||
[
|
||||
'number' => 4, 'title' => 'A PR', 'html_url' => 'https://x/4',
|
||||
'labels' => [['name' => 'good first issue']], 'pull_request' => ['url' => 'x'],
|
||||
'user' => ['login' => 'ada'], 'created_at' => '2026-07-01T00:00:00Z',
|
||||
'updated_at' => '2026-07-02T00:00:00Z',
|
||||
],
|
||||
]);
|
||||
|
||||
$repository = Repository::factory()->create([...$this->githubDefaults, 'full_name' => 'colectivo/web']);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
$this->assertSame(2, $repository->issues()->count());
|
||||
$this->assertSame(1, $repository->goodFirstIssues()->count());
|
||||
|
||||
// Label matching is case-insensitive, per config/git.php.
|
||||
$helpWanted = $repository->issues()->where('number', 2)->first();
|
||||
$this->assertTrue($helpWanted->is_help_wanted);
|
||||
$this->assertSame('bob', $helpWanted->assignee);
|
||||
$this->assertFalse($helpWanted->isUnclaimed());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function issues_closed_upstream_disappear_here(): void
|
||||
{
|
||||
$repository = Repository::factory()->create([...$this->githubDefaults, 'full_name' => 'colectivo/web']);
|
||||
|
||||
$repository->issues()->create([
|
||||
'number' => 99, 'title' => 'Closed since', 'html_url' => 'https://x/99',
|
||||
'is_good_first_issue' => true,
|
||||
]);
|
||||
|
||||
$this->fakeGitHub(issues: []);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
// Advertising an issue that is actually closed wastes a would-be
|
||||
// contributor's time, so the mirror is replaced, not merged.
|
||||
$this->assertSame(0, $repository->issues()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_missing_repository_is_recorded_and_not_retried(): void
|
||||
{
|
||||
Http::fake([
|
||||
'api.github.com/*' => Http::response(['message' => 'Not Found'], 404),
|
||||
]);
|
||||
|
||||
$repository = Repository::factory()->create([...$this->githubDefaults, 'full_name' => 'colectivo/gone']);
|
||||
|
||||
try {
|
||||
app(GitManager::class)->sync($repository);
|
||||
$this->fail('Expected a GitException.');
|
||||
} catch (GitException $e) {
|
||||
$this->assertSame(404, $e->status);
|
||||
$this->assertFalse($e->isTransient);
|
||||
}
|
||||
|
||||
$repository->refresh();
|
||||
|
||||
$this->assertStringContainsString('does not exist', $repository->sync_error);
|
||||
$this->assertSame(1, $repository->sync_failures);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function repeated_failures_flag_the_repository_in_the_panel(): void
|
||||
{
|
||||
Http::fake(['api.github.com/*' => Http::response([], 404)]);
|
||||
|
||||
$repository = Repository::factory()->create([...$this->githubDefaults, 'full_name' => 'colectivo/gone']);
|
||||
|
||||
foreach (range(1, 3) as $ignored) {
|
||||
rescue(fn () => app(GitManager::class)->sync($repository));
|
||||
}
|
||||
|
||||
$this->assertSame(3, $repository->fresh()->sync_failures);
|
||||
$this->assertTrue(Repository::query()->failing()->exists());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_rate_limit_is_reported_as_transient(): void
|
||||
{
|
||||
Http::fake([
|
||||
'api.github.com/*' => Http::response(['message' => 'rate limit'], 403, [
|
||||
'X-RateLimit-Remaining' => '0',
|
||||
'X-RateLimit-Reset' => (string) (time() + 120),
|
||||
]),
|
||||
]);
|
||||
|
||||
$repository = Repository::factory()->create([...$this->githubDefaults, 'full_name' => 'colectivo/web']);
|
||||
|
||||
try {
|
||||
app(GitManager::class)->sync($repository);
|
||||
$this->fail('Expected a GitException.');
|
||||
} catch (GitException $e) {
|
||||
// Transient means the job should back off and retry, not give up.
|
||||
$this->assertTrue($e->isTransient);
|
||||
$this->assertSame(429, $e->status);
|
||||
$this->assertStringContainsString('GITHUB_TOKEN', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_successful_sync_clears_a_previous_error(): void
|
||||
{
|
||||
$repository = Repository::factory()->failing()->create([...$this->githubDefaults, 'full_name' => 'colectivo/web']);
|
||||
|
||||
$this->fakeGitHub();
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
$repository->refresh();
|
||||
|
||||
$this->assertNull($repository->sync_error);
|
||||
$this->assertSame(0, $repository->sync_failures);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function synced_columns_cannot_be_mass_assigned(): void
|
||||
{
|
||||
// A hand-typed star count is a lie waiting to happen, so the sync
|
||||
// columns are deliberately absent from the fillable list.
|
||||
$repository = new Repository;
|
||||
$repository->fill(['full_name' => 'colectivo/web', 'stars' => 9999]);
|
||||
|
||||
$this->assertSame('colectivo/web', $repository->full_name);
|
||||
$this->assertNull($repository->stars);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function a_bare_name_is_qualified_with_the_configured_org(): void
|
||||
{
|
||||
config()->set('git.providers.github.default_owner', 'colectivo');
|
||||
|
||||
$this->fakeGitHub();
|
||||
|
||||
$repository = Repository::factory()->create([...$this->githubDefaults, 'full_name' => 'web']);
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
|
||||
// The provider's own answer wins, so the record self-corrects.
|
||||
$this->assertSame('colectivo/web', $repository->fresh()->full_name);
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/repos/colectivo/web'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function an_unsupported_provider_fails_clearly(): void
|
||||
{
|
||||
$repository = Repository::factory()->create(['provider' => 'gitlab']);
|
||||
|
||||
$this->expectException(GitException::class);
|
||||
$this->expectExceptionMessage('No driver is registered for the [gitlab] provider');
|
||||
|
||||
app(GitManager::class)->sync($repository);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user