288 lines
10 KiB
C#
288 lines
10 KiB
C#
using FluentAssertions;
|
|
using Jellyfin.Plugin.SRFPlay.Api;
|
|
using Microsoft.Extensions.Logging;
|
|
using Xunit;
|
|
|
|
namespace Jellyfin.Plugin.SRFPlay.Tests.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Integration tests to validate SRF API spec compliance.
|
|
/// These tests make real API calls to ensure the API is still working as expected.
|
|
/// </summary>
|
|
[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<Task>
|
|
{
|
|
_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();
|
|
}
|
|
}
|