using System.Text.Json; using Application.Abstractions.YouTube; using Microsoft.Extensions.Logging; namespace Infrastructure.YouTube; internal sealed class YouTubeApiConnectionTester( IHttpClientFactory httpClientFactory, ILogger logger ) : IYouTubeApiConnectionTester { public async Task TestAsync(string testChannelId, CancellationToken ct) { // "YouTubeApi" 명명 HttpClient = YouTubeApiKeyHandler가 자동으로 ?key= 붙여줌 var client = httpClientFactory.CreateClient("YouTubeApi"); var url = $"https://www.googleapis.com/youtube/v3/channels?part=snippet&id={Uri.EscapeDataString(testChannelId)}"; try { using var response = await client.GetAsync(url, ct); var body = await response.Content.ReadAsStringAsync(ct); var status = (int)response.StatusCode; if (!response.IsSuccessStatusCode) { var msg = ExtractGoogleErrorMessage(body) ?? Truncate(body, 300); logger.LogWarning("[YouTube] API key test failed: {Status} — {Body}", status, Truncate(body, 500)); return new YouTubeApiTestResult( Ok: false, HttpStatus: status, ChannelTitle: null, ChannelId: testChannelId, ErrorMessage: $"HTTP {status} {response.StatusCode} — {msg}" ); } try { var doc = JsonSerializer.Deserialize(body); if (!doc.TryGetProperty("items", out var items) || items.GetArrayLength() == 0) { return new YouTubeApiTestResult( Ok: false, HttpStatus: status, ChannelTitle: null, ChannelId: testChannelId, ErrorMessage: "응답의 items가 비어있습니다. API Key는 유효하나 채널 조회가 제한됐을 수 있음 (Cloud Console에서 API Key 제한 설정 확인)." ); } var title = items[0].GetProperty("snippet").GetProperty("title").GetString(); return new YouTubeApiTestResult( Ok: true, HttpStatus: status, ChannelTitle: title, ChannelId: testChannelId, ErrorMessage: null ); } catch (JsonException ex) { return new YouTubeApiTestResult( Ok: false, HttpStatus: status, ChannelTitle: null, ChannelId: testChannelId, ErrorMessage: $"응답 JSON 파싱 실패: {ex.Message}" ); } } catch (Exception ex) { logger.LogError(ex, "[YouTube] API key test threw"); return new YouTubeApiTestResult( Ok: false, HttpStatus: null, ChannelTitle: null, ChannelId: testChannelId, ErrorMessage: $"요청 실패: {ex.Message}" ); } } // Google API 에러 응답: { "error": { "code": 403, "message": "...", "errors": [{reason:"keyInvalid"}] } } private static string? ExtractGoogleErrorMessage(string body) { if (string.IsNullOrWhiteSpace(body)) { return null; } try { var doc = JsonSerializer.Deserialize(body); if (doc.TryGetProperty("error", out var err)) { var msg = err.TryGetProperty("message", out var m) ? m.GetString() : null; var reason = err.TryGetProperty("errors", out var errs) && errs.GetArrayLength() > 0 && errs[0].TryGetProperty("reason", out var r) ? r.GetString() : null; if (!string.IsNullOrEmpty(msg)) { return reason is not null ? $"[{reason}] {msg}" : msg; } } } catch { // ignore — fallback to raw body } return null; } private static string Truncate(string s, int max) => s.Length <= max ? s : s[..max] + "..."; }