using FluentAssertions; using Jellyfin.Plugin.SRFPlay.Api; using Microsoft.Extensions.Logging; using Xunit; namespace Jellyfin.Plugin.SRFPlay.Tests.IntegrationTests; /// /// Integration tests to validate SRF API spec compliance. /// These tests make real API calls to ensure the API is still working as expected. /// [Trait("Category", "Integration")] [Trait("Category", "APISpec")] public class SRFApiSpecTests : IDisposable { private readonly ILoggerFactory _loggerFactory; private readonly SRFApiClient _apiClient; private readonly CancellationToken _cancellationToken = CancellationToken.None; public SRFApiSpecTests() { _loggerFactory = LoggerFactory.Create(builder => { builder.AddConsole(); builder.SetMinimumLevel(LogLevel.Information); }); _apiClient = new SRFApiClient(_loggerFactory); } [Fact] public async Task GetAllShows_SRF_ReturnsShows() { // Act var shows = await _apiClient.GetAllShowsAsync("srf", _cancellationToken); // Assert shows.Should().NotBeNull(); shows.Should().NotBeEmpty(); var firstShow = shows.FirstOrDefault(); firstShow.Should().NotBeNull(); firstShow!.Id.Should().NotBeNullOrEmpty(); firstShow.Title.Should().NotBeNullOrEmpty(); } [Theory] [InlineData("srf")] [InlineData("rts")] [InlineData("rsi")] public async Task GetAllShows_MultipleBusinessUnits_ReturnsValidData(string businessUnit) { // Act var shows = await _apiClient.GetAllShowsAsync(businessUnit, _cancellationToken); // Assert shows.Should().NotBeNull(); // If shows exist, validate their structure if (shows != null && shows.Count > 0) { var firstShow = shows.FirstOrDefault(); firstShow.Should().NotBeNull(); firstShow!.Id.Should().NotBeNullOrEmpty(); // Validate URN format if present if (!string.IsNullOrEmpty(firstShow.Urn)) { firstShow.Urn.Should().Contain($":{businessUnit}:"); } } } [Fact] public async Task GetVideosForShow_ValidShowId_ReturnsVideos() { // Arrange - First get a show var shows = await _apiClient.GetAllShowsAsync("srf", _cancellationToken); shows.Should().NotBeNull().And.NotBeEmpty(); var showWithEpisodes = shows!.FirstOrDefault(s => s != null && s.NumberOfEpisodes > 0); showWithEpisodes.Should().NotBeNull("at least one show should have episodes"); // Act var videos = await _apiClient.GetVideosForShowAsync("srf", showWithEpisodes!.Id!, _cancellationToken); // Assert videos.Should().NotBeNull(); videos.Should().NotBeEmpty(); var firstVideo = videos!.FirstOrDefault(); firstVideo.Should().NotBeNull(); firstVideo!.Id.Should().NotBeNullOrEmpty(); firstVideo.Title.Should().NotBeNullOrEmpty(); firstVideo.Urn.Should().NotBeNullOrEmpty(); firstVideo.Urn.Should().StartWith("urn:srf:video:"); } [Fact] public async Task GetMediaCompositionByUrn_ValidVideoUrn_ReturnsMediaComposition() { // Arrange - Get a video URN var shows = await _apiClient.GetAllShowsAsync("srf", _cancellationToken); shows.Should().NotBeNull().And.NotBeEmpty(); var showWithEpisodes = shows!.FirstOrDefault(s => s != null && s.NumberOfEpisodes > 0); var videos = await _apiClient.GetVideosForShowAsync("srf", showWithEpisodes!.Id!, _cancellationToken); videos.Should().NotBeNull().And.NotBeEmpty(); var videoUrn = videos!.First()!.Urn!; // Act var mediaComposition = await _apiClient.GetMediaCompositionByUrnAsync(videoUrn, _cancellationToken); // Assert mediaComposition.Should().NotBeNull(); mediaComposition!.ChapterList.Should().NotBeNull(); mediaComposition.ChapterList.Should().NotBeEmpty(); var chapter = mediaComposition.ChapterList.First(); chapter.Should().NotBeNull(); chapter.Urn.Should().Be(videoUrn); chapter.ResourceList.Should().NotBeNull(); } [Fact] public async Task GetMediaCompositionByUrn_ValidVideoUrn_HasHLSResources() { // Arrange - Get a video URN var shows = await _apiClient.GetAllShowsAsync("srf", _cancellationToken); var showWithEpisodes = shows!.FirstOrDefault(s => s != null && s.NumberOfEpisodes > 0); var videos = await _apiClient.GetVideosForShowAsync("srf", showWithEpisodes!.Id!, _cancellationToken); var videoUrn = videos!.First()!.Urn!; // Act var mediaComposition = await _apiClient.GetMediaCompositionByUrnAsync(videoUrn, _cancellationToken); // Assert var chapter = mediaComposition!.ChapterList.First(); var hlsResources = chapter.ResourceList!.Where(r => r.Protocol == "HLS" || r.Streaming == "HLS" || r.Url.Contains(".m3u8")).ToList(); hlsResources.Should().NotBeEmpty("video should have HLS streaming resources"); var hlsResource = hlsResources.First(); hlsResource.Url.Should().NotBeNullOrEmpty(); hlsResource.Url.Should().Contain(".m3u8"); } [Fact] public async Task VideoResource_Url_IsAccessible() { // Arrange - Get a video URN and stream URL var shows = await _apiClient.GetAllShowsAsync("srf", _cancellationToken); var showWithEpisodes = shows!.FirstOrDefault(s => s != null && s.NumberOfEpisodes > 0); var videos = await _apiClient.GetVideosForShowAsync("srf", showWithEpisodes!.Id!, _cancellationToken); var videoUrn = videos!.First()!.Urn!; var mediaComposition = await _apiClient.GetMediaCompositionByUrnAsync(videoUrn, _cancellationToken); var chapter = mediaComposition!.ChapterList.First(); var hlsResource = chapter.ResourceList!.FirstOrDefault(r => (r.DrmList == null || r.DrmList.ToString() == "[]") && (r.Protocol == "HLS" || r.Url.Contains(".m3u8"))); hlsResource.Should().NotBeNull("at least one non-DRM HLS resource should exist"); // Act - Try to access the URL using var httpClient = new HttpClient(); var response = await httpClient.GetAsync(hlsResource!.Url, _cancellationToken); // Assert response.Should().NotBeNull(); response.IsSuccessStatusCode.Should().BeTrue("stream URL should be accessible"); var content = await response.Content.ReadAsStringAsync(_cancellationToken); content.Should().Contain("#EXTM3U", "should be a valid M3U8 playlist"); } [Fact] public async Task GetAllShows_ResponseStructure_MatchesExpectedSchema() { // Act var shows = await _apiClient.GetAllShowsAsync("srf", _cancellationToken); // Assert - Validate response structure shows.Should().NotBeNull(); shows.Should().NotBeEmpty(); var show = shows!.First()!; // Required fields show.Id.Should().NotBeNullOrEmpty(); show.Title.Should().NotBeNullOrEmpty(); // Optional but commonly present fields show.NumberOfEpisodes.Should().BeGreaterThanOrEqualTo(0); // If URN is present, validate format if (!string.IsNullOrEmpty(show.Urn)) { show.Urn.Should().MatchRegex(@"^urn:srf:(show|video):.+$"); } } [Fact] public async Task GetVideosForShow_ResponseStructure_MatchesExpectedSchema() { // Arrange var shows = await _apiClient.GetAllShowsAsync("srf", _cancellationToken); var showWithEpisodes = shows!.FirstOrDefault(s => s != null && s.NumberOfEpisodes > 0); // Act var videos = await _apiClient.GetVideosForShowAsync("srf", showWithEpisodes!.Id!, _cancellationToken); // Assert - Validate response structure var video = videos!.First()!; // Required fields video.Id.Should().NotBeNullOrEmpty(); video.Title.Should().NotBeNullOrEmpty(); video.Urn.Should().NotBeNullOrEmpty(); video.Urn.Should().MatchRegex(@"^urn:srf:(video|scheduled_livestream:video):.+$"); // Duration should be positive video.Duration.Should().BeGreaterThan(0); // Date should be valid video.Date.Should().BeAfter(DateTime.MinValue); } [Fact] public async Task GetScheduledLivestreams_ReturnsValidData() { // Act var livestreams = await _apiClient.GetScheduledLivestreamsAsync("srf", "SPORT", _cancellationToken); // Assert livestreams.Should().NotBeNull(); // If there are livestreams, validate their structure if (livestreams != null && livestreams.Count > 0) { var livestream = livestreams.First(); livestream.Should().NotBeNull(); livestream.Urn.Should().NotBeNullOrEmpty(); livestream.Urn.Should().Contain("scheduled_livestream"); } } [Fact] public async Task ApiEndpoints_AreResponsive() { // This test ensures all major endpoints are responsive var tasks = new List { _apiClient.GetAllShowsAsync("srf", _cancellationToken), _apiClient.GetScheduledLivestreamsAsync("srf", "SPORT", _cancellationToken) }; // Act - All API calls should complete without exceptions var act = async () => await Task.WhenAll(tasks); // Assert await act.Should().NotThrowAsync("all API endpoints should be responsive"); } [Fact] public async Task ApiPerformance_ReasonableResponseTime() { // Arrange var stopwatch = System.Diagnostics.Stopwatch.StartNew(); // Act var shows = await _apiClient.GetAllShowsAsync("srf", _cancellationToken); // Assert stopwatch.Stop(); stopwatch.Elapsed.Should().BeLessThan(TimeSpan.FromSeconds(30), "API should respond within 30 seconds"); shows.Should().NotBeEmpty(); } public void Dispose() { _apiClient?.Dispose(); _loggerFactory?.Dispose(); } }