Files
landing-api/tests/Feature/Git/GiteaSyncTest.php
Josep Piguillem 3897113313 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>
2026-08-19 17:55:27 +02:00

280 lines
10 KiB
PHP

<?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);
}
}