assertSame('owner', $owner); $this->assertSame('repo', $repo); } #[Test] public function parseUrl_git_접미사_포함_url을_처리합니다(): void { [$owner, $repo] = GithubHelper::parseUrl('https://github.com/owner/repo.git'); $this->assertSame('owner', $owner); $this->assertSame('repo', $repo); } #[Test] public function parseUrl_끝에_슬래시가_있는_url을_처리합니다(): void { [$owner, $repo] = GithubHelper::parseUrl('https://github.com/owner/repo/'); $this->assertSame('owner', $owner); $this->assertSame('repo', $repo); } #[Test] public function parseUrl_잘못된_url이면_RuntimeException을_던집니다(): void { $this->expectException(\RuntimeException::class); GithubHelper::parseUrl('https://example.com/foo'); } #[Test] public function parseUrl_빈_문자열이면_RuntimeException을_던집니다(): void { $this->expectException(\RuntimeException::class); GithubHelper::parseUrl(''); } #[Test] public function parseUrl_경로가_부족한_url이면_RuntimeException을_던집니다(): void { $this->expectException(\RuntimeException::class); GithubHelper::parseUrl('https://github.com/owner'); } // ────────────────────────────────────────── // buildHeaders // ────────────────────────────────────────── #[Test] public function buildHeaders_토큰_없이_기본_헤더만_반환합니다(): void { $headers = GithubHelper::buildHeaders(); $this->assertCount(2, $headers); $this->assertSame('User-Agent: G7', $headers[0]); $this->assertSame('Accept: application/vnd.github.v3+json', $headers[1]); } #[Test] public function buildHeaders_빈_문자열_토큰이면_Authorization을_포함하지_않습니다(): void { $headers = GithubHelper::buildHeaders(''); $this->assertCount(2, $headers); foreach ($headers as $header) { $this->assertStringNotContainsString('Authorization', $header); } } #[Test] public function buildHeaders_토큰이_있으면_Authorization_Bearer를_포함합니다(): void { $headers = GithubHelper::buildHeaders('ghp_test_token'); $this->assertCount(3, $headers); $this->assertSame('Authorization: Bearer ghp_test_token', $headers[2]); } // ────────────────────────────────────────── // parseUrl 경계 케이스 // ────────────────────────────────────────── #[Test] public function parseUrl_대시와_언더스코어가_포함된_저장소명을_처리합니다(): void { [$owner, $repo] = GithubHelper::parseUrl('https://github.com/my-org/my_repo-name'); $this->assertSame('my-org', $owner); $this->assertSame('my_repo-name', $repo); } #[Test] public function parseUrl_ssh_스타일_url은_실패합니다(): void { // SSH URL도 정규식에서 매칭 가능 [$owner, $repo] = GithubHelper::parseUrl('git@github.com:owner/repo.git'); $this->assertSame('owner', $owner); $this->assertSame('repo', $repo); } // ────────────────────────────────────────── // checkRepoExists (Http::fake 기반) // ────────────────────────────────────────── #[Test] public function checkRepoExists_200_응답이면_true를_반환합니다(): void { Http::fake([ 'api.github.com/repos/owner/repo' => Http::response(['full_name' => 'owner/repo'], 200), ]); $this->assertTrue(GithubHelper::checkRepoExists('owner', 'repo')); Http::assertSent(function (Request $request) { return $request->url() === 'https://api.github.com/repos/owner/repo' && $request->hasHeader('User-Agent', 'G7') && $request->hasHeader('Accept', 'application/vnd.github.v3+json'); }); } #[Test] public function checkRepoExists_404_응답이면_false를_반환합니다(): void { Http::fake([ 'api.github.com/repos/*' => Http::response([], 404), ]); $this->assertFalse(GithubHelper::checkRepoExists('owner', 'repo')); } #[Test] public function checkRepoExists_연결_실패_시_false를_반환합니다(): void { Http::fake(function () { throw new ConnectionException('Connection refused'); }); $this->assertFalse(GithubHelper::checkRepoExists('owner', 'repo')); } #[Test] public function checkRepoExists_토큰이_있으면_Authorization_헤더를_포함합니다(): void { Http::fake([ 'api.github.com/*' => Http::response([], 200), ]); GithubHelper::checkRepoExists('owner', 'repo', 'ghp_test_token'); Http::assertSent(function (Request $request) { return $request->hasHeader('Authorization', 'Bearer ghp_test_token'); }); } // ────────────────────────────────────────── // fetchLatestRelease (Http::fake 기반) // ────────────────────────────────────────── #[Test] public function fetchLatestRelease_200_응답이면_버전과_zipball_url을_반환합니다(): void { Http::fake([ 'api.github.com/repos/owner/repo/releases/latest' => Http::response([ 'tag_name' => 'v7.0.1', 'zipball_url' => 'https://api.github.com/repos/owner/repo/zipball/v7.0.1', ], 200), ]); $result = GithubHelper::fetchLatestRelease('owner', 'repo'); $this->assertSame('7.0.1', $result['version']); $this->assertSame('https://api.github.com/repos/owner/repo/zipball/v7.0.1', $result['zipball_url']); $this->assertNull($result['error']); } #[Test] public function fetchLatestRelease_v접두사_없는_tag_name을_정상_처리합니다(): void { Http::fake([ 'api.github.com/*' => Http::response(['tag_name' => '1.2.3', 'zipball_url' => 'https://example.com/z'], 200), ]); $result = GithubHelper::fetchLatestRelease('owner', 'repo'); $this->assertSame('1.2.3', $result['version']); } #[Test] public function fetchLatestRelease_404_응답이면_version_null과_error_null을_반환합니다(): void { Http::fake([ 'api.github.com/*' => Http::response([], 404), ]); $result = GithubHelper::fetchLatestRelease('owner', 'repo'); $this->assertNull($result['version']); $this->assertNull($result['zipball_url']); $this->assertNull($result['error']); } #[Test] public function fetchLatestRelease_연결_실패_시_error_메시지를_반환합니다(): void { Http::fake(function () { throw new ConnectionException('Network unreachable'); }); $result = GithubHelper::fetchLatestRelease('owner', 'repo'); $this->assertNull($result['version']); $this->assertNull($result['zipball_url']); $this->assertNotNull($result['error']); $this->assertStringContainsString('GitHub', $result['error']); } #[Test] public function fetchLatestRelease_tag_name_누락이면_version_null을_반환합니다(): void { Http::fake([ 'api.github.com/*' => Http::response(['name' => 'release name only'], 200), ]); $result = GithubHelper::fetchLatestRelease('owner', 'repo'); $this->assertNull($result['version']); } // ────────────────────────────────────────── // downloadArchive (sink 기반) // ────────────────────────────────────────── #[Test] public function downloadArchive_성공_시_파일을_생성합니다(): void { $savePath = sys_get_temp_dir().'/github_test_'.uniqid().'.zip'; Http::fake([ 'example.com/*' => Http::response('fake-zip-binary', 200), ]); try { GithubHelper::downloadArchive('https://example.com/archive.zip', $savePath); $this->assertTrue(File::exists($savePath)); $this->assertSame('fake-zip-binary', File::get($savePath)); } finally { if (File::exists($savePath)) { File::delete($savePath); } } } #[Test] public function downloadArchive_실패_응답이면_RuntimeException을_던집니다(): void { $savePath = sys_get_temp_dir().'/github_test_'.uniqid().'.zip'; Http::fake([ 'example.com/*' => Http::response('', 404), ]); try { $this->expectException(\RuntimeException::class); GithubHelper::downloadArchive('https://example.com/missing.zip', $savePath); } finally { if (File::exists($savePath)) { File::delete($savePath); } } } #[Test] public function downloadArchive_연결_실패_시_RuntimeException을_던집니다(): void { Http::fake(function () { throw new ConnectionException('Timeout'); }); $this->expectException(\RuntimeException::class); GithubHelper::downloadArchive( 'https://example.com/archive.zip', sys_get_temp_dir().'/github_test_'.uniqid().'.zip' ); } // ────────────────────────────────────────── // downloadArchiveContent (메모리 반환) // ────────────────────────────────────────── #[Test] public function downloadArchiveContent_성공_시_바이너리_내용을_반환합니다(): void { Http::fake([ 'example.com/*' => Http::response('binary-content', 200), ]); $content = GithubHelper::downloadArchiveContent('https://example.com/archive.zip'); $this->assertSame('binary-content', $content); } #[Test] public function downloadArchiveContent_실패_응답이면_RuntimeException을_던집니다(): void { Http::fake([ 'example.com/*' => Http::response('', 500), ]); $this->expectException(\RuntimeException::class); GithubHelper::downloadArchiveContent('https://example.com/archive.zip'); } // ────────────────────────────────────────── // resolveArchiveUrl (HEAD 요청 폴백) // ────────────────────────────────────────── #[Test] public function resolveArchiveUrl_v접두사_태그_200이면_해당_URL을_반환합니다(): void { // resolveArchiveUrl 은 git/refs/tags/{tag} 로 태그 존재 여부를 먼저 검증한 뒤 // zipball URL 을 반환 (codeload 404 불가시 실패 방지 — GithubHelper.php 참조) Http::fake([ 'api.github.com/repos/owner/repo/git/refs/tags/v1.0.0' => Http::response('', 200), ]); $url = GithubHelper::resolveArchiveUrl('owner', 'repo', '1.0.0'); $this->assertSame('https://api.github.com/repos/owner/repo/zipball/v1.0.0', $url); } #[Test] public function resolveArchiveUrl_v접두사_404이면_순수버전으로_폴백합니다(): void { Http::fake([ 'api.github.com/repos/owner/repo/git/refs/tags/v1.0.0' => Http::response('', 404), 'api.github.com/repos/owner/repo/git/refs/tags/1.0.0' => Http::response('', 200), ]); $url = GithubHelper::resolveArchiveUrl('owner', 'repo', '1.0.0'); $this->assertSame('https://api.github.com/repos/owner/repo/zipball/1.0.0', $url); } #[Test] public function resolveArchiveUrl_둘다_404이면_null을_반환합니다(): void { Http::fake([ 'api.github.com/*' => Http::response('', 404), ]); $url = GithubHelper::resolveArchiveUrl('owner', 'repo', '1.0.0'); $this->assertNull($url); } #[Test] public function resolveArchiveUrl_302이면_해당_URL을_반환합니다(): void { // 리디렉션 상황: 태그 존재 확인(GET git/refs/tags)만 성공하면 zipball URL 반환. // zipball 실제 호출은 다운로드 시점에 발생 (resolveArchiveUrl 은 태그 검증 + URL 조립만 수행) Http::fake([ 'api.github.com/repos/owner/repo/git/refs/tags/v1.0.0' => Http::response('', 200), ]); $url = GithubHelper::resolveArchiveUrl('owner', 'repo', '1.0.0'); $this->assertSame('https://api.github.com/repos/owner/repo/zipball/v1.0.0', $url); } // ────────────────────────────────────────── // fetchRawFile (raw.githubusercontent.com) // ────────────────────────────────────────── #[Test] public function fetchRawFile_원본_ref로_200이면_내용을_반환합니다(): void { Http::fake([ 'raw.githubusercontent.com/owner/repo/main/CHANGELOG.md' => Http::response('# Changelog', 200), ]); $content = GithubHelper::fetchRawFile('owner', 'repo', 'main', 'CHANGELOG.md'); $this->assertSame('# Changelog', $content); } #[Test] public function fetchRawFile_v접두사_없는_태그는_v접두사로_폴백합니다(): void { Http::fake([ 'raw.githubusercontent.com/owner/repo/1.0.0/README.md' => Http::response('', 404), 'raw.githubusercontent.com/owner/repo/v1.0.0/README.md' => Http::response('readme body', 200), ]); $content = GithubHelper::fetchRawFile('owner', 'repo', '1.0.0', 'README.md'); $this->assertSame('readme body', $content); } #[Test] public function fetchRawFile_모두_404이면_null을_반환합니다(): void { Http::fake([ 'raw.githubusercontent.com/*' => Http::response('', 404), ]); $this->assertNull(GithubHelper::fetchRawFile('owner', 'repo', 'main', 'missing.md')); } #[Test] public function fetchRawFile_연결_실패_시_null을_반환합니다(): void { Http::fake(function () { throw new ConnectionException('DNS failure'); }); $this->assertNull(GithubHelper::fetchRawFile('owner', 'repo', 'main', 'CHANGELOG.md')); } }