using System; using System.Net; using System.Text; using System.Threading.Tasks; using Xunit; using ytLive.Services; namespace ytLive.Tests; /// /// TASK 39 root-cause guard (2026-09-23): the YPP refresh's channels.list call /// must NOT request the `auditDetails` part — that part alone requires the /// `youtubepartner-channel-audit` scope the app doesn't (and mustn't) hold, and /// the whole request 403s `insufficientPermissions` over it (this exact failure /// sat in the real logs for weeks). One integration test: the snapshot still /// parses subscribers/views/videos/uploads from `statistics,contentDetails`, and /// the URL never mentions auditDetails. /// public class ChannelStatsServiceTests { private sealed class RecordingHandler : HttpMessageHandler { public string? LastBody; public Func? ResponseSelector; public readonly System.Collections.Generic.List Requests = new(); protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Requests.Add($"{request.Method.Method} {request.RequestUri}"); LastBody = request.Content?.ReadAsStringAsync().GetAwaiter().GetResult(); return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent( ResponseSelector?.Invoke(request) ?? "{}", Encoding.UTF8, "application/json"), }); } } [Fact] public async Task CaptureCurrent_RequestsNoAuditDetails_AndStillParsesTheSnapshot() { var auth = new YouTubeAuthService("test-client", "test-secret"); auth.SetSession(new Models.YouTubeChannel { AccessToken = "acc-123", TokenExpiry = DateTime.UtcNow.AddHours(1), }); var handler = new RecordingHandler { ResponseSelector = request => request.RequestUri!.ToString().Contains("channels") ? """ {"items":[{"id":"UC-1","statistics":{"subscriberCount":"350","viewCount":"12345","videoCount":"42"}, "contentDetails":{"relatedPlaylists":{"uploads":"UPL"}}}]} """ : """ {"items":[ {"snippet":{"publishedAt":"2026-08-01T00:00:00Z"}}, {"snippet":{"publishedAt":"2026-09-01T00:00:00Z"}}, {"snippet":{"publishedAt":"2025-01-01T00:00:00Z"}} ]} """, }; var service = new ChannelStatsService(auth, new System.Net.Http.HttpClient(handler)); var snapshot = await service.CaptureCurrentAsync(); Assert.NotNull(snapshot); Assert.Equal(2, handler.Requests.Count); Assert.StartsWith("GET ", handler.Requests[0]); Assert.Contains("channels?mine=true", handler.Requests[0]); Assert.Contains("part=statistics,contentDetails", handler.Requests[0]); Assert.DoesNotContain("auditDetails", handler.Requests[0]); Assert.Contains("playlistItems", handler.Requests[1]); Assert.Equal(350, snapshot!.SubscriberCount); Assert.Equal(42, snapshot.VideoCount); Assert.Equal(12_345, snapshot.TotalViews); Assert.Equal(2, snapshot.UploadsLast90Days); Assert.False(DateTime.UtcNow - snapshot.CapturedAtUtc > TimeSpan.FromMinutes(1)); } }