using System.Net; using System.Text; using SeerrAutoApprove.Seerr; namespace SeerrAutoApprove.Tests; public sealed class SeerrClientTests { private sealed class StubHandler(Func respond) : HttpMessageHandler { public List Requests { get; } = []; protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Requests.Add(request); return Task.FromResult(respond(request)); } } private sealed class StubFactory(HttpMessageHandler handler) : IHttpClientFactory { public HttpClient CreateClient(string name) => new(handler, disposeHandler: false) { BaseAddress = new Uri("http://seerr.local/api/v1/") }; } private static HttpResponseMessage Json(string json) => new(HttpStatusCode.OK) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; [Fact] public async Task Parses_pending_requests_across_pages() { const string page1 = """ { "pageInfo": { "pages": 2, "pageSize": 1, "results": 2, "page": 1 }, "results": [ { "id": 11, "status": 1, "type": "movie", "is4k": false, "createdAt": "2026-10-01T10:00:00.000Z", "requestedBy": { "id": 3, "displayName": "Alice", "email": "alice@example.com" }, "media": { "id": 5, "tmdbId": 603, "mediaType": "movie" }, "seasons": [] } ] } """; const string page2 = """ { "pageInfo": { "pages": 2, "pageSize": 1, "results": 2, "page": 2 }, "results": [ { "id": 12, "status": 1, "type": "tv", "is4k": true, "createdAt": "2026-10-02T10:00:00.000Z", "requestedBy": { "id": 4, "displayName": "Bob" }, "media": { "id": 6, "tmdbId": 1399, "mediaType": "tv" }, "seasons": [ { "id": 1, "seasonNumber": 1 }, { "id": 2, "seasonNumber": 2 } ] } ] } """; var handler = new StubHandler(req => Json(req.RequestUri!.Query.Contains("skip=0") ? page1 : page2)); var client = new SeerrClient(new StubFactory(handler)); var requests = await client.GetPendingRequestsAsync(CancellationToken.None); Assert.Equal(2, requests.Count); Assert.Equal(11, requests[0].Id); Assert.False(requests[0].IsSeries); Assert.Equal(3, requests[0].RequestedBy!.Id); Assert.Equal(12, requests[1].Id); Assert.True(requests[1].IsSeries); Assert.True(requests[1].Is4k); Assert.Equal(2, requests[1].Seasons.Count); Assert.All(handler.Requests, r => Assert.Contains("filter=pending", r.RequestUri!.Query)); Assert.Equal("/api/v1/request", handler.Requests[0].RequestUri!.AbsolutePath); } [Fact] public async Task Returns_empty_list_when_nothing_pending() { var handler = new StubHandler(_ => Json("""{ "pageInfo": { "pages": 0, "page": 1, "results": 0 }, "results": [] }""")); var requests = await new SeerrClient(new StubFactory(handler)).GetPendingRequestsAsync(CancellationToken.None); Assert.Empty(requests); Assert.Single(handler.Requests); } [Fact] public async Task Approve_posts_to_approve_endpoint() { var handler = new StubHandler(_ => Json("{}")); await new SeerrClient(new StubFactory(handler)).ApproveRequestAsync(42, CancellationToken.None); var request = Assert.Single(handler.Requests); Assert.Equal(HttpMethod.Post, request.Method); Assert.Equal("/api/v1/request/42/approve", request.RequestUri!.AbsolutePath); } [Fact] public async Task Approve_throws_on_error_status() { var handler = new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.Conflict)); await Assert.ThrowsAsync(() => new SeerrClient(new StubFactory(handler)).ApproveRequestAsync(42, CancellationToken.None)); } }