mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-05-27 19:08:27 +01:00
Merge branch 'master' into feature/season-provider-id-from-path
This commit is contained in:
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AutoFixture.Xunit2;
|
||||
using AutoFixture.Xunit3;
|
||||
using Jellyfin.Api.Controllers;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Common.Net;
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{A2FD0A10-8F62-4F9D-B171-FFDF9F0AFA9D}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" />
|
||||
<PackageReference Include="AutoFixture.Xunit3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{DF194677-DFD3-42AF-9F75-D44D5A416478}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
@@ -23,11 +24,6 @@ public class BaseItemTests
|
||||
[InlineData("/Movies/Deadpool 2 (2018)/Deadpool 2 (2018).mkv", "/Movies/Deadpool 2 (2018)/Deadpool 2 (2018) - Super Duper Cut.mkv", "Deadpool 2 (2018)", "Super Duper Cut")]
|
||||
public void GetMediaSourceName_Valid(string primaryPath, string altPath, string name, string altName)
|
||||
{
|
||||
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
||||
mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
|
||||
.Returns((string x) => MediaProtocol.File);
|
||||
BaseItem.MediaSourceManager = mediaSourceManager.Object;
|
||||
|
||||
var video = new Video()
|
||||
{
|
||||
Path = primaryPath
|
||||
@@ -38,7 +34,14 @@ public class BaseItemTests
|
||||
Path = altPath,
|
||||
};
|
||||
|
||||
video.LocalAlternateVersions = [videoAlt.Path];
|
||||
var mediaSourceManager = new Mock<IMediaSourceManager>();
|
||||
mediaSourceManager.Setup(x => x.GetPathProtocol(It.IsAny<string>()))
|
||||
.Returns((string x) => MediaProtocol.File);
|
||||
var libraryManager = new Mock<ILibraryManager>();
|
||||
libraryManager.Setup(x => x.GetLocalAlternateVersionIds(It.IsAny<Video>()))
|
||||
.Returns([Guid.Empty]);
|
||||
BaseItem.MediaSourceManager = mediaSourceManager.Object;
|
||||
BaseItem.LibraryManager = libraryManager.Object;
|
||||
|
||||
Assert.Equal(name, video.GetMediaSourceName(video));
|
||||
Assert.Equal(altName, video.GetMediaSourceName(videoAlt));
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{462584F7-5023-4019-9EAC-B98CA458C0A0}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -11,7 +15,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -14,12 +15,11 @@
|
||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xunit.SkippableFact" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -15,10 +15,17 @@ namespace Jellyfin.MediaEncoding.Hls.Tests.Playlist
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSegments_InvalidDuration_ThrowsArgumentException()
|
||||
public void ComputeSegments_ZeroDurationOvershoot_ClampsToDuration()
|
||||
{
|
||||
var keyframeData = new KeyframeData(0, new[] { MsToTicks(10000) });
|
||||
Assert.Throws<ArgumentException>(() => DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000));
|
||||
Assert.Equal(new[] { 10.0 }, DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeSegments_MinorDurationOvershoot_ClampsToDuration()
|
||||
{
|
||||
var keyframeData = new KeyframeData(MsToTicks(9900), new[] { 0L, MsToTicks(5000), MsToTicks(10000) });
|
||||
Assert.Equal(new[] { 10.0 }, DynamicHlsPlaylistGenerator.ComputeSegments(keyframeData, 6000));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{28464062-0939-4AA7-9F7B-24DDDA61A7C0}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -14,11 +15,11 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" />
|
||||
<PackageReference Include="AutoFixture.Xunit3" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -209,8 +209,8 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
|
||||
Assert.Equal("mkv,webm", res.Container);
|
||||
|
||||
Assert.Equal(2, res.MediaStreams.Count);
|
||||
|
||||
Assert.False(res.MediaStreams[0].IsAVC);
|
||||
Assert.Equal(540, res.MediaStreams[0].Width);
|
||||
Assert.Equal(360, res.MediaStreams[0].Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
using System;
|
||||
using AutoFixture;
|
||||
using AutoFixture.AutoMoq;
|
||||
using MediaBrowser.MediaEncoding.Subtitles;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.MediaEncoding.Subtitles.Tests
|
||||
{
|
||||
public class FilterEventsTests
|
||||
{
|
||||
private readonly SubtitleEncoder _encoder;
|
||||
|
||||
public FilterEventsTests()
|
||||
{
|
||||
var fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
|
||||
_encoder = fixture.Create<SubtitleEncoder>();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEvents_SubtitleSpanningSegmentBoundary_IsRetained()
|
||||
{
|
||||
// Subtitle starts at 5s, ends at 15s.
|
||||
// Segment requested from 10s to 20s.
|
||||
// The subtitle is still on screen at 10s and should NOT be dropped.
|
||||
var track = new SubtitleTrackInfo
|
||||
{
|
||||
TrackEvents = new[]
|
||||
{
|
||||
new SubtitleTrackEvent("1", "Still on screen")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
|
||||
},
|
||||
new SubtitleTrackEvent("2", "Next subtitle")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_encoder.FilterEvents(
|
||||
track,
|
||||
startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
|
||||
endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
|
||||
preserveTimestamps: true);
|
||||
|
||||
Assert.Equal(2, track.TrackEvents.Count);
|
||||
Assert.Equal("1", track.TrackEvents[0].Id);
|
||||
Assert.Equal("2", track.TrackEvents[1].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEvents_SubtitleFullyBeforeSegment_IsDropped()
|
||||
{
|
||||
// Subtitle starts at 2s, ends at 5s.
|
||||
// Segment requested from 10s.
|
||||
// The subtitle ended before the segment — should be dropped.
|
||||
var track = new SubtitleTrackInfo
|
||||
{
|
||||
TrackEvents = new[]
|
||||
{
|
||||
new SubtitleTrackEvent("1", "Already gone")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(5).Ticks
|
||||
},
|
||||
new SubtitleTrackEvent("2", "Visible")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_encoder.FilterEvents(
|
||||
track,
|
||||
startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
|
||||
endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
|
||||
preserveTimestamps: true);
|
||||
|
||||
Assert.Single(track.TrackEvents);
|
||||
Assert.Equal("2", track.TrackEvents[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEvents_SubtitleAfterSegment_IsDropped()
|
||||
{
|
||||
// Segment is 10s-20s, subtitle starts at 25s.
|
||||
var track = new SubtitleTrackInfo
|
||||
{
|
||||
TrackEvents = new[]
|
||||
{
|
||||
new SubtitleTrackEvent("1", "In range")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
|
||||
},
|
||||
new SubtitleTrackEvent("2", "After segment")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(25).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(30).Ticks
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_encoder.FilterEvents(
|
||||
track,
|
||||
startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
|
||||
endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
|
||||
preserveTimestamps: true);
|
||||
|
||||
Assert.Single(track.TrackEvents);
|
||||
Assert.Equal("1", track.TrackEvents[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEvents_PreserveTimestampsFalse_AdjustsTimestamps()
|
||||
{
|
||||
var track = new SubtitleTrackInfo
|
||||
{
|
||||
TrackEvents = new[]
|
||||
{
|
||||
new SubtitleTrackEvent("1", "Subtitle")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(15).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(20).Ticks
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_encoder.FilterEvents(
|
||||
track,
|
||||
startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
|
||||
endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
|
||||
preserveTimestamps: false);
|
||||
|
||||
Assert.Single(track.TrackEvents);
|
||||
// Timestamps should be shifted back by 10s
|
||||
Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].StartPositionTicks);
|
||||
Assert.Equal(TimeSpan.FromSeconds(10).Ticks, track.TrackEvents[0].EndPositionTicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEvents_PreserveTimestampsTrue_KeepsOriginalTimestamps()
|
||||
{
|
||||
var startTicks = TimeSpan.FromSeconds(15).Ticks;
|
||||
var endTicks = TimeSpan.FromSeconds(20).Ticks;
|
||||
|
||||
var track = new SubtitleTrackInfo
|
||||
{
|
||||
TrackEvents = new[]
|
||||
{
|
||||
new SubtitleTrackEvent("1", "Subtitle")
|
||||
{
|
||||
StartPositionTicks = startTicks,
|
||||
EndPositionTicks = endTicks
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_encoder.FilterEvents(
|
||||
track,
|
||||
startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
|
||||
endTimeTicks: TimeSpan.FromSeconds(30).Ticks,
|
||||
preserveTimestamps: true);
|
||||
|
||||
Assert.Single(track.TrackEvents);
|
||||
Assert.Equal(startTicks, track.TrackEvents[0].StartPositionTicks);
|
||||
Assert.Equal(endTicks, track.TrackEvents[0].EndPositionTicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEvents_SubtitleEndingExactlyAtSegmentStart_IsRetained()
|
||||
{
|
||||
// Subtitle ends exactly when the segment begins.
|
||||
// EndPositionTicks == startPositionTicks means (end - start) == 0, not < 0,
|
||||
// so SkipWhile stops and the subtitle is retained.
|
||||
var track = new SubtitleTrackInfo
|
||||
{
|
||||
TrackEvents = new[]
|
||||
{
|
||||
new SubtitleTrackEvent("1", "Boundary subtitle")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(10).Ticks
|
||||
},
|
||||
new SubtitleTrackEvent("2", "In range")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_encoder.FilterEvents(
|
||||
track,
|
||||
startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
|
||||
endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
|
||||
preserveTimestamps: true);
|
||||
|
||||
Assert.Equal(2, track.TrackEvents.Count);
|
||||
Assert.Equal("1", track.TrackEvents[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEvents_SpanningBoundaryWithTimestampAdjustment_DoesNotProduceNegativeTimestamps()
|
||||
{
|
||||
// Subtitle starts at 5s, ends at 15s.
|
||||
// Segment requested from 10s to 20s, preserveTimestamps = false.
|
||||
// The subtitle spans the boundary and is retained, but shifting
|
||||
// StartPositionTicks by -10s would produce -5s (negative).
|
||||
var track = new SubtitleTrackInfo
|
||||
{
|
||||
TrackEvents = new[]
|
||||
{
|
||||
new SubtitleTrackEvent("1", "Spans boundary")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(5).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
|
||||
},
|
||||
new SubtitleTrackEvent("2", "Fully in range")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(17).Ticks
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_encoder.FilterEvents(
|
||||
track,
|
||||
startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
|
||||
endTimeTicks: TimeSpan.FromSeconds(20).Ticks,
|
||||
preserveTimestamps: false);
|
||||
|
||||
Assert.Equal(2, track.TrackEvents.Count);
|
||||
// Subtitle 1: start should be clamped to 0, not -5s
|
||||
Assert.True(track.TrackEvents[0].StartPositionTicks >= 0, "StartPositionTicks must not be negative");
|
||||
Assert.Equal(TimeSpan.FromSeconds(5).Ticks, track.TrackEvents[0].EndPositionTicks);
|
||||
// Subtitle 2: normal shift (12s - 10s = 2s, 17s - 10s = 7s)
|
||||
Assert.Equal(TimeSpan.FromSeconds(2).Ticks, track.TrackEvents[1].StartPositionTicks);
|
||||
Assert.Equal(TimeSpan.FromSeconds(7).Ticks, track.TrackEvents[1].EndPositionTicks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterEvents_NoEndTimeTicks_ReturnsAllFromStartPosition()
|
||||
{
|
||||
var track = new SubtitleTrackInfo
|
||||
{
|
||||
TrackEvents = new[]
|
||||
{
|
||||
new SubtitleTrackEvent("1", "Before")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(2).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(4).Ticks
|
||||
},
|
||||
new SubtitleTrackEvent("2", "After")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(12).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(15).Ticks
|
||||
},
|
||||
new SubtitleTrackEvent("3", "Much later")
|
||||
{
|
||||
StartPositionTicks = TimeSpan.FromSeconds(500).Ticks,
|
||||
EndPositionTicks = TimeSpan.FromSeconds(505).Ticks
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_encoder.FilterEvents(
|
||||
track,
|
||||
startPositionTicks: TimeSpan.FromSeconds(10).Ticks,
|
||||
endTimeTicks: 0,
|
||||
preserveTimestamps: true);
|
||||
|
||||
Assert.Equal(2, track.TrackEvents.Count);
|
||||
Assert.Equal("2", track.TrackEvents[0].Id);
|
||||
Assert.Equal("3", track.TrackEvents[1].Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -617,5 +617,60 @@ namespace Jellyfin.Model.Tests
|
||||
|
||||
return (path, query, filename, extension);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
// EnableSubtitleExtraction = false, internal subtitles
|
||||
[InlineData("srt", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
|
||||
[InlineData("srt", "srt", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
|
||||
[InlineData("pgssub", "pgssub", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
|
||||
[InlineData("pgssub", "pgssub", false, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
|
||||
[InlineData("pgssub", "srt", false, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
|
||||
// EnableSubtitleExtraction = false, external subtitles
|
||||
[InlineData("srt", "srt", false, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
|
||||
// EnableSubtitleExtraction = true, internal subtitles
|
||||
[InlineData("srt", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
|
||||
[InlineData("pgssub", "pgssub", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
|
||||
[InlineData("pgssub", "pgssub", true, false, PlayMethod.DirectPlay, SubtitleDeliveryMethod.External)]
|
||||
[InlineData("pgssub", "srt", true, false, PlayMethod.Transcode, SubtitleDeliveryMethod.Encode)]
|
||||
// EnableSubtitleExtraction = true, external subtitles
|
||||
[InlineData("srt", "srt", true, true, PlayMethod.Transcode, SubtitleDeliveryMethod.External)]
|
||||
public void GetSubtitleProfile_RespectsExtractionSetting(
|
||||
string codec,
|
||||
string profileFormat,
|
||||
bool enableSubtitleExtraction,
|
||||
bool isExternal,
|
||||
PlayMethod playMethod,
|
||||
SubtitleDeliveryMethod expectedMethod)
|
||||
{
|
||||
var mediaSource = new MediaSourceInfo();
|
||||
var subtitleStream = new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Subtitle,
|
||||
Index = 0,
|
||||
IsExternal = isExternal,
|
||||
Path = isExternal ? "/media/sub." + codec : null,
|
||||
Codec = codec,
|
||||
SupportsExternalStream = MediaStream.IsTextFormat(codec)
|
||||
};
|
||||
|
||||
var subtitleProfiles = new[]
|
||||
{
|
||||
new SubtitleProfile { Format = profileFormat, Method = SubtitleDeliveryMethod.External }
|
||||
};
|
||||
|
||||
var transcoderSupport = new Mock<ITranscoderSupport>();
|
||||
transcoderSupport.Setup(t => t.CanExtractSubtitles(It.IsAny<string>())).Returns(enableSubtitleExtraction);
|
||||
|
||||
var result = StreamBuilder.GetSubtitleProfile(
|
||||
mediaSource,
|
||||
subtitleStream,
|
||||
subtitleProfiles,
|
||||
playMethod,
|
||||
transcoderSupport.Object,
|
||||
null,
|
||||
null);
|
||||
|
||||
Assert.Equal(expectedMethod, result.Method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{3998657B-1CCC-49DD-A19F-275DC8495F57}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -80,7 +80,9 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
[InlineData("[VCB-Studio] Re Zero kara Hajimeru Isekai Seikatsu [21][Ma10p_1080p][x265_flac].mkv", 21)]
|
||||
[InlineData("[CASO&Sumisora][Oda_Nobuna_no_Yabou][04][BDRIP][1920x1080][x264_AAC][7620E503].mp4", 4)]
|
||||
|
||||
// [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
|
||||
[InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number
|
||||
[InlineData("Season 2/Hunter X Hunter - 101.mkv", 101)] // triple digit episode number without brackets
|
||||
[InlineData("Season 1/Show Name - 1234 [720p].mkv", 1234)] // four digit episode number with quality tag
|
||||
// TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)]
|
||||
// TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)]
|
||||
// TODO: [InlineData("Season 2/7 12 Angry Men.avi", 7)]
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
[InlineData(2, "The Simpsons/The Simpsons - 02.avi")]
|
||||
[InlineData(2, "The Simpsons/The Simpsons - 02 Ep Name.avi")]
|
||||
[InlineData(7, "GJ Club (2013)/GJ Club - 07.mkv")]
|
||||
[InlineData(17, "Case Closed (1996-2007)/Case Closed - 317.mkv")]
|
||||
[InlineData(317, "Case Closed (1996-2007)/Case Closed - 317.mkv")]
|
||||
// TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 - Ep Name.avi")]
|
||||
// TODO: [InlineData(2, @"The Simpsons/The Simpsons 5 - 02 Ep Name.avi")]
|
||||
// TODO: [InlineData(7, @"Seinfeld/Seinfeld 0807 The Checks.avi")]
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Jellyfin.Naming.Tests.TV
|
||||
[InlineData("Season 2009/S2009E23-E24-E26 - The Woman.mp4", 2009)]
|
||||
[InlineData("Series/1-12 - The Woman.mp4", 1)]
|
||||
[InlineData("Running Man/Running Man S2017E368.mkv", 2017)]
|
||||
[InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 3)]
|
||||
[InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", null)]
|
||||
// TODO: [InlineData(@"Seinfeld/Seinfeld 0807 The Checks.avi", 8)]
|
||||
public void GetSeasonNumberFromEpisodeFileTest(string path, int? expected)
|
||||
{
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
[InlineData("[OCN] 애타는 로맨스 720p-NEXT", "애타는 로맨스")]
|
||||
[InlineData("[tvN] 혼술남녀.E01-E16.720p-NEXT", "혼술남녀")]
|
||||
[InlineData("[tvN] 연애말고 결혼 E01~E16 END HDTV.H264.720p-WITH", "연애말고 결혼")]
|
||||
[InlineData("2026年01月10日23時00分00秒-[新]TRIGUN STARGAZE[字].mp4", "2026年01月10日23時00分00秒-[新]TRIGUN STARGAZE")]
|
||||
// FIXME: [InlineData("After The Sunset - [0004].mkv", "After The Sunset")]
|
||||
public void CleanStringTest_NeedsCleaning_Success(string input, string expectedName)
|
||||
{
|
||||
@@ -44,6 +45,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
[InlineData("American.Psycho.mkv")]
|
||||
[InlineData("American Psycho.mkv")]
|
||||
[InlineData("Run lola run (lola rennt) (2009).mp4")]
|
||||
[InlineData("2026年01月05日00時55分00秒-[新]違国日記【ANiMiDNiGHT!!!】#1.mp4")]
|
||||
public void CleanStringTest_DoesntNeedCleaning_False(string? input)
|
||||
{
|
||||
Assert.False(VideoResolver.TryCleanString(input, _namingOptions, out var newName));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
@@ -269,8 +270,13 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
|
||||
Assert.Equal(7, result.Count);
|
||||
Assert.Empty(result[0].AlternateVersions);
|
||||
Assert.Single(result);
|
||||
Assert.Equal(6, result[0].AlternateVersions.Count);
|
||||
|
||||
// Verify 3D recognition is preserved on alternate versions
|
||||
var hsbs = result[0].AlternateVersions.First(v => v.Path.Contains("3d-hsbs", StringComparison.Ordinal));
|
||||
Assert.True(hsbs.Is3D);
|
||||
Assert.Equal("hsbs", hsbs.Format3D);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -435,5 +441,39 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GivenUnderscoreSeparator_GroupsVersions()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie (2020)/Movie (2020)_4K.mkv",
|
||||
"/movies/Movie (2020)/Movie (2020)_1080p.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_GivenDotSeparator_GroupsVersions()
|
||||
{
|
||||
var files = new[]
|
||||
{
|
||||
"/movies/Movie (2020)/Movie (2020).UHD.mkv",
|
||||
"/movies/Movie (2020)/Movie (2020).1080p.mkv"
|
||||
};
|
||||
|
||||
var result = VideoListResolver.Resolve(
|
||||
files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType<VideoFileInfo>().ToList(),
|
||||
_namingOptions).ToList();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Single(result[0].AlternateVersions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{42816EA8-4511-4CBF-A9C7-7791D5DDDAE6}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="FsCheck.Xunit" />
|
||||
<PackageReference Include="FsCheck.Xunit.v3" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -377,6 +377,8 @@ namespace Jellyfin.Networking.Tests
|
||||
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "10.0.0.1", "192.168.1.209", "10.0.0.1")] // LAN not bound, so return external.
|
||||
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "8.8.8.8", "10.0.0.1")] // return external bind address
|
||||
[InlineData("192.168.1.208/24,-16,eth16|10.0.0.1/24,10,eth7", "192.168.1.0/24", "192.168.1.208,10.0.0.1", "192.168.1.210", "192.168.1.208")] // return LAN bind address
|
||||
// Cross-subnet IPv4 request should return IPv4, not IPv6 (Issue #15898)
|
||||
[InlineData("192.168.1.208/24,-16,eth16|fd00::1/64,10,eth7", "192.168.1.0/24", "", "192.168.2.100", "192.168.1.208")]
|
||||
public void GetBindInterface_ValidSourceGiven_Success(string interfaces, string lan, string bind, string source, string result)
|
||||
{
|
||||
var conf = new NetworkConfiguration
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Test Data\**\*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
@@ -9,10 +13,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" />
|
||||
<PackageReference Include="AutoFixture.Xunit3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -45,8 +45,9 @@ public class FFProbeVideoInfoTests
|
||||
[Theory]
|
||||
[InlineData(null, 0)]
|
||||
[InlineData(0L, 0)]
|
||||
[InlineData(1L, 0)]
|
||||
[InlineData(TimeSpan.TicksPerMinute * 5, 0)]
|
||||
[InlineData(1L, 1)]
|
||||
[InlineData(TimeSpan.TicksPerMinute * 3, 1)]
|
||||
[InlineData(TimeSpan.TicksPerMinute * 5, 1)]
|
||||
[InlineData((TimeSpan.TicksPerMinute * 5) + 1, 1)]
|
||||
[InlineData(TimeSpan.TicksPerMinute * 50, 10)]
|
||||
public void CreateDummyChapters_ValidRuntime_CorrectChaptersCount(long? runtime, int chaptersCount)
|
||||
@@ -58,4 +59,20 @@ public class FFProbeVideoInfoTests
|
||||
|
||||
Assert.Equal(chaptersCount, chapters.Length);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1L)]
|
||||
[InlineData(TimeSpan.TicksPerMinute * 3)]
|
||||
[InlineData(TimeSpan.TicksPerMinute * 5)]
|
||||
[InlineData((TimeSpan.TicksPerMinute * 5) + 1)]
|
||||
[InlineData((TimeSpan.TicksPerMinute * 50) + 1)]
|
||||
public void CreateDummyChapters_PositiveRuntime_NoChapterBeyondRuntime(long runtime)
|
||||
{
|
||||
var chapters = _fFProbeVideoInfo.CreateDummyChapters(new Video()
|
||||
{
|
||||
RunTimeTicks = runtime
|
||||
});
|
||||
|
||||
Assert.All(chapters, chapter => Assert.True(chapter.StartPositionTicks < runtime));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,13 +123,13 @@ public class MediaInfoResolverTests
|
||||
|
||||
var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
|
||||
// any path other than test target exists and provides an empty listing
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
_subtitleResolver.GetExternalFiles(video.Object, directoryService.Object, false);
|
||||
|
||||
directoryService.Verify(
|
||||
ds => ds.GetFilePaths(It.IsRegex(pathNotFoundRegex), It.IsAny<bool>(), It.IsAny<bool>()),
|
||||
ds => ds.GetFilePaths(It.IsRegex(pathNotFoundRegex), It.IsAny<bool>()),
|
||||
Times.Never);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ public class MediaInfoResolverTests
|
||||
};
|
||||
|
||||
var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsAny<string>(), It.IsAny<bool>()))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
var mediaEncoder = Mock.Of<IMediaEncoder>(MockBehavior.Strict);
|
||||
@@ -341,9 +341,9 @@ public class MediaInfoResolverTests
|
||||
}
|
||||
|
||||
var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>()))
|
||||
.Returns(files);
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>()))
|
||||
.Returns(Array.Empty<string>());
|
||||
|
||||
List<MediaStream> GenerateMediaStreams()
|
||||
@@ -413,16 +413,16 @@ public class MediaInfoResolverTests
|
||||
var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict);
|
||||
if (useMetadataDirectory)
|
||||
{
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>()))
|
||||
.Returns(Array.Empty<string>());
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>()))
|
||||
.Returns(new[] { MetadataDirectoryPath + "/" + file });
|
||||
}
|
||||
else
|
||||
{
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(VideoDirectoryRegex), It.IsAny<bool>()))
|
||||
.Returns(new[] { VideoDirectoryPath + "/" + file });
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>(), It.IsAny<bool>()))
|
||||
directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(MetadataDirectoryRegex), It.IsAny<bool>()))
|
||||
.Returns(Array.Empty<string>());
|
||||
}
|
||||
|
||||
|
||||
110
tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs
Normal file
110
tests/Jellyfin.Providers.Tests/TV/EpisodeMetadataServiceTests.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Providers.Manager;
|
||||
using MediaBrowser.Providers.TV;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Providers.Tests.TV;
|
||||
|
||||
public class EpisodeMetadataServiceTests
|
||||
{
|
||||
private readonly TestEpisodeMetadataService _service = new();
|
||||
|
||||
[Fact]
|
||||
public void MergeData_ProviderSeasonOverridesPathDerivedSeason()
|
||||
{
|
||||
var source = new MetadataResult<Episode>
|
||||
{
|
||||
Item = new Episode
|
||||
{
|
||||
ParentIndexNumber = 2
|
||||
}
|
||||
};
|
||||
|
||||
var target = new MetadataResult<Episode>
|
||||
{
|
||||
Item = new Episode
|
||||
{
|
||||
ParentIndexNumber = 1
|
||||
}
|
||||
};
|
||||
|
||||
_service.Merge(source, target, replaceData: false, mergeMetadataSettings: true);
|
||||
|
||||
Assert.Equal(2, target.Item.ParentIndexNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeData_BackfillExistingMetadata_DoesNotOverrideProviderSeason()
|
||||
{
|
||||
var existingMetadata = new MetadataResult<Episode>
|
||||
{
|
||||
Item = new Episode
|
||||
{
|
||||
ParentIndexNumber = 1
|
||||
}
|
||||
};
|
||||
|
||||
var temp = new MetadataResult<Episode>
|
||||
{
|
||||
Item = new Episode
|
||||
{
|
||||
ParentIndexNumber = 2
|
||||
}
|
||||
};
|
||||
|
||||
_service.Merge(existingMetadata, temp, replaceData: false, mergeMetadataSettings: false);
|
||||
|
||||
Assert.Equal(2, temp.Item.ParentIndexNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MergeData_MissingProviderSeasonKeepsExistingSeason()
|
||||
{
|
||||
var source = new MetadataResult<Episode>
|
||||
{
|
||||
Item = new Episode()
|
||||
};
|
||||
|
||||
var target = new MetadataResult<Episode>
|
||||
{
|
||||
Item = new Episode
|
||||
{
|
||||
ParentIndexNumber = 1
|
||||
}
|
||||
};
|
||||
|
||||
_service.Merge(source, target, replaceData: false, mergeMetadataSettings: true);
|
||||
|
||||
Assert.Equal(1, target.Item.ParentIndexNumber);
|
||||
}
|
||||
|
||||
private sealed class TestEpisodeMetadataService : EpisodeMetadataService
|
||||
{
|
||||
public TestEpisodeMetadataService()
|
||||
: base(
|
||||
Mock.Of<IServerConfigurationManager>(),
|
||||
NullLogger<EpisodeMetadataService>.Instance,
|
||||
Mock.Of<IProviderManager>(),
|
||||
Mock.Of<IFileSystem>(),
|
||||
Mock.Of<ILibraryManager>(),
|
||||
Mock.Of<IExternalDataManager>(),
|
||||
Mock.Of<IItemRepository>())
|
||||
{
|
||||
}
|
||||
|
||||
public void Merge(MetadataResult<Episode> source, MetadataResult<Episode> target, bool replaceData, bool mergeMetadataSettings)
|
||||
{
|
||||
MergeData(source, target, Array.Empty<MetadataField>(), replaceData, mergeMetadataSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,12 @@ public class ManagedFileSystemTests
|
||||
public void MoveDirectory_SameFileSystem_Correct()
|
||||
=> MoveDirectoryInternal();
|
||||
|
||||
[SkippableFact]
|
||||
[Fact]
|
||||
public void MoveDirectory_DifferentFileSystem_Correct()
|
||||
{
|
||||
const string DestinationParent = "/dev/shm";
|
||||
|
||||
Skip.IfNot(Directory.Exists(DestinationParent));
|
||||
Assert.SkipUnless(Directory.Exists(DestinationParent), $"{DestinationParent} is not available");
|
||||
|
||||
MoveDirectoryInternal(DestinationParent);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public class ManagedFileSystemTests
|
||||
Directory.Delete(destinationDir, true);
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[Theory]
|
||||
[InlineData("/Volumes/Library/Sample/Music/Playlists/", "../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Beethoven/Misc/Moonlight Sonata.mp3")]
|
||||
[InlineData("/Volumes/Library/Sample/Music/Playlists/", "../../Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Beethoven/Misc/Moonlight Sonata.mp3")]
|
||||
[InlineData("/Volumes/Library/Sample/Music/Playlists/", "Beethoven/Misc/Moonlight Sonata.mp3", "/Volumes/Library/Sample/Music/Playlists/Beethoven/Misc/Moonlight Sonata.mp3")]
|
||||
@@ -67,13 +67,13 @@ public class ManagedFileSystemTests
|
||||
string filePath,
|
||||
string expectedAbsolutePath)
|
||||
{
|
||||
Skip.If(OperatingSystem.IsWindows());
|
||||
Assert.SkipWhen(OperatingSystem.IsWindows(), "Unix-only test");
|
||||
|
||||
var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
|
||||
Assert.Equal(expectedAbsolutePath, generatedPath);
|
||||
}
|
||||
|
||||
[SkippableTheory]
|
||||
[Theory]
|
||||
[InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Beethoven\Misc\Moonlight Sonata.mp3")]
|
||||
[InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"..\..\Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Beethoven\Misc\Moonlight Sonata.mp3")]
|
||||
[InlineData(@"C:\\Volumes\Library\Sample\Music\Playlists\", @"Beethoven\Misc\Moonlight Sonata.mp3", @"C:\Volumes\Library\Sample\Music\Playlists\Beethoven\Misc\Moonlight Sonata.mp3")]
|
||||
@@ -83,7 +83,7 @@ public class ManagedFileSystemTests
|
||||
string filePath,
|
||||
string expectedAbsolutePath)
|
||||
{
|
||||
Skip.IfNot(OperatingSystem.IsWindows());
|
||||
Assert.SkipUnless(OperatingSystem.IsWindows(), "Windows-only test");
|
||||
|
||||
var generatedPath = _sut.MakeAbsolutePath(folderPath, filePath);
|
||||
|
||||
@@ -100,10 +100,10 @@ public class ManagedFileSystemTests
|
||||
Assert.Equal(expectedFileName, _sut.GetValidFilename(filename));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
[Fact]
|
||||
public void GetFileInfo_DanglingSymlink_ExistsFalse()
|
||||
{
|
||||
Skip.If(OperatingSystem.IsWindows());
|
||||
Assert.SkipWhen(OperatingSystem.IsWindows(), "Unix-only test");
|
||||
|
||||
string testFileDir = Path.Combine(Path.GetTempPath(), "jellyfin-test-data");
|
||||
string testFileName = Path.Combine(testFileDir, Path.GetRandomFileName() + "-danglingsym.link");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{2E3A1B4B-4225-4AAA-8B29-0181A84E7AEE}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -16,12 +17,11 @@
|
||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xunit.SkippableFact" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Library;
|
||||
@@ -78,4 +83,391 @@ public class DotIgnoreIgnoreRuleTest
|
||||
// Without normalization, Windows paths with backslashes won't match patterns expecting forward slashes
|
||||
Assert.False(DotIgnoreIgnoreRule.CheckIgnoreRules(path, _rule1, isDirectory: false, normalizePath: false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CacheHit_RepeatedCallsDoNotRereadFiles()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var subDir = Path.Combine(tempDir, "subdir");
|
||||
Directory.CreateDirectory(subDir);
|
||||
|
||||
try
|
||||
{
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, "*.tmp");
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(subDir, "test.tmp"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
// First call - should cache
|
||||
var result1 = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result1);
|
||||
|
||||
// Second call - should use cache
|
||||
var result2 = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result2);
|
||||
|
||||
// Third call with different file in same directory - should use cache
|
||||
var fileInfo2 = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(subDir, "other.tmp"),
|
||||
IsDirectory = false
|
||||
};
|
||||
var result3 = rule.ShouldIgnore(fileInfo2, null);
|
||||
Assert.True(result3);
|
||||
|
||||
// Call with file that doesn't match pattern
|
||||
var fileInfo3 = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(subDir, "other.txt"),
|
||||
IsDirectory = false
|
||||
};
|
||||
var result4 = rule.ShouldIgnore(fileInfo3, null);
|
||||
Assert.False(result4);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CacheInvalidation_ModifyIgnoreFile_Reparses()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, "*.tmp");
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, "test.tmp"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
// First call - should ignore .tmp files
|
||||
var result1 = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result1);
|
||||
|
||||
// Modify the .ignore file to ignore .txt instead
|
||||
// Wait a bit to ensure the file modification time changes
|
||||
Thread.Sleep(50);
|
||||
File.WriteAllText(ignoreFilePath, "*.txt");
|
||||
|
||||
// Now .tmp files should NOT be ignored
|
||||
var result2 = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.False(result2);
|
||||
|
||||
// And .txt files SHOULD be ignored
|
||||
var txtFileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, "test.txt"),
|
||||
IsDirectory = false
|
||||
};
|
||||
var result3 = rule.ShouldIgnore(txtFileInfo, null);
|
||||
Assert.True(result3);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyIgnoreFile_IgnoresEverything()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, string.Empty);
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, "anyfile.mkv"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
// Empty .ignore file should ignore everything
|
||||
var result = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhitespaceOnlyIgnoreFile_IgnoresEverything()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, " \n\t\n ");
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, "anyfile.mkv"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
// Whitespace-only .ignore file should ignore everything
|
||||
var result = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoIgnoreFile_DoesNotIgnore()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, "anyfile.mkv"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
// No .ignore file means don't ignore
|
||||
var result = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.False(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConcurrentAccess_ThreadSafe()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, "*.tmp");
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
|
||||
// Run multiple parallel checks
|
||||
Parallel.For(0, 100, i =>
|
||||
{
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, $"test{i}.tmp"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
var result = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result);
|
||||
});
|
||||
|
||||
// Also test with non-matching files
|
||||
Parallel.For(0, 100, i =>
|
||||
{
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, $"test{i}.txt"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
var result = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.False(result);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearCache_ClearsAllCachedData()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, "*.tmp");
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, "test.tmp"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
// First call to populate cache
|
||||
var result1 = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result1);
|
||||
|
||||
// Clear cache
|
||||
rule.ClearDirectoryCache();
|
||||
|
||||
// Should still work (will re-populate cache)
|
||||
var result2 = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoreFileDeleted_HandlesGracefully()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
try
|
||||
{
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, "*.tmp");
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, "test.tmp"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
// First call - should ignore
|
||||
var result1 = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result1);
|
||||
|
||||
// Delete the .ignore file
|
||||
File.Delete(ignoreFilePath);
|
||||
|
||||
// Should not ignore anymore (file deleted)
|
||||
var result2 = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.False(result2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempDir))
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentDirectoryIgnoreFile_AppliesToSubdirectories()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var subDir1 = Path.Combine(tempDir, "sub1");
|
||||
var subDir2 = Path.Combine(tempDir, "sub1", "sub2");
|
||||
Directory.CreateDirectory(subDir1);
|
||||
Directory.CreateDirectory(subDir2);
|
||||
|
||||
try
|
||||
{
|
||||
// Put .ignore in root
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, "*.tmp");
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
|
||||
// Check file in sub2 - should find .ignore in parent
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(subDir2, "test.tmp"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
var result = rule.ShouldIgnore(fileInfo, null);
|
||||
Assert.True(result);
|
||||
|
||||
// Check file in sub1
|
||||
var fileInfo2 = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(subDir1, "test.tmp"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
var result2 = rule.ShouldIgnore(fileInfo2, null);
|
||||
Assert.True(result2);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DirectoryMatching_TrailingSlashPattern()
|
||||
{
|
||||
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(tempDir);
|
||||
var subDir = Path.Combine(tempDir, "videos");
|
||||
Directory.CreateDirectory(subDir);
|
||||
|
||||
try
|
||||
{
|
||||
var ignoreFilePath = Path.Combine(tempDir, ".ignore");
|
||||
File.WriteAllText(ignoreFilePath, "videos/");
|
||||
|
||||
var rule = new DotIgnoreIgnoreRule();
|
||||
|
||||
// Directory should be ignored
|
||||
var dirInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = subDir,
|
||||
IsDirectory = true
|
||||
};
|
||||
|
||||
var result = rule.ShouldIgnore(dirInfo, null);
|
||||
Assert.True(result);
|
||||
|
||||
// File named "videos" should NOT be ignored (pattern has trailing slash)
|
||||
var fileInfo = new FileSystemMetadata
|
||||
{
|
||||
FullName = Path.Combine(tempDir, "videos"),
|
||||
IsDirectory = false
|
||||
};
|
||||
|
||||
// Note: The Ignore library behavior may vary here, this tests the actual behavior
|
||||
var resultFile = rule.ShouldIgnore(fileInfo, null);
|
||||
// The file named "videos" without trailing slash might or might not match depending on the library
|
||||
// This test documents the actual behavior
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
});
|
||||
var countries = localizationManager.GetCountries().ToList();
|
||||
|
||||
Assert.Equal(139, countries.Count);
|
||||
Assert.Equal(140, countries.Count);
|
||||
|
||||
var germany = countries.FirstOrDefault(x => x.Name.Equals("DE", StringComparison.Ordinal));
|
||||
Assert.NotNull(germany);
|
||||
@@ -41,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
await localizationManager.LoadAll();
|
||||
var cultures = localizationManager.GetCultures().ToList();
|
||||
|
||||
Assert.Equal(194, cultures.Count);
|
||||
Assert.Equal(496, cultures.Count);
|
||||
|
||||
var germany = cultures.FirstOrDefault(x => x.TwoLetterISOLanguageName.Equals("de", StringComparison.Ordinal));
|
||||
Assert.NotNull(germany);
|
||||
@@ -99,6 +99,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
Assert.Contains("ger", germany.ThreeLetterISOLanguageNames);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("mul", "Multiple languages")]
|
||||
[InlineData("und", "Undetermined")]
|
||||
[InlineData("mis", "Uncoded languages")]
|
||||
[InlineData("zxx", "No linguistic content; Not applicable")]
|
||||
public async Task FindLanguageInfo_ISO6392Only_Success(string code, string expectedDisplayName)
|
||||
{
|
||||
var localizationManager = Setup(new ServerConfiguration
|
||||
{
|
||||
UICulture = "en-US"
|
||||
});
|
||||
await localizationManager.LoadAll();
|
||||
|
||||
var culture = localizationManager.FindLanguageInfo(code);
|
||||
Assert.NotNull(culture);
|
||||
Assert.Equal(expectedDisplayName, culture.DisplayName);
|
||||
Assert.Equal(code, culture.ThreeLetterISOLanguageName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetParentalRatings_Default_Success()
|
||||
{
|
||||
@@ -222,6 +241,40 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
|
||||
Assert.Equal(expectedSubScore, score.SubScore);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("US:INVALID", "US")] // Colon separator, known country code, unknown rating
|
||||
[InlineData("us:INVALID", "US")] // Colon separator, lowercase country code
|
||||
[InlineData("DE-INVALID", "US")] // Hyphen separator, known language prefix, unknown rating
|
||||
[InlineData("ca:INVALID", "US")] // Colon separator, known country code (Canada)
|
||||
public async Task GetRatingScore_UnknownRatingWithKnownCountry_ReturnsNull(string rating, string countryCode)
|
||||
{
|
||||
var localizationManager = Setup(new ServerConfiguration
|
||||
{
|
||||
MetadataCountryCode = countryCode
|
||||
});
|
||||
await localizationManager.LoadAll();
|
||||
|
||||
Assert.Null(localizationManager.GetRatingScore(rating));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("us:R", "DE", 17, 0)] // Colon separator, explicit US country, valid US rating
|
||||
[InlineData("US:PG-13", "DE", 13, 0)] // Colon separator, explicit US country, valid US rating
|
||||
[InlineData("ca:R", "US", 18, 1)] // Colon separator, Canada country code, valid CA rating
|
||||
public async Task GetRatingScore_ValidRatingWithCountrySeparator_ReturnsScore(string rating, string countryCode, int expectedScore, int? expectedSubScore)
|
||||
{
|
||||
var localizationManager = Setup(new ServerConfiguration
|
||||
{
|
||||
MetadataCountryCode = countryCode
|
||||
});
|
||||
await localizationManager.LoadAll();
|
||||
|
||||
var score = localizationManager.GetRatingScore(rating);
|
||||
Assert.NotNull(score);
|
||||
Assert.Equal(expectedScore, score.Score);
|
||||
Assert.Equal(expectedSubScore, score.SubScore);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Default", "Default")]
|
||||
[InlineData("HeaderLiveTV", "Live TV")]
|
||||
|
||||
@@ -192,13 +192,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
||||
};
|
||||
|
||||
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
|
||||
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options), TestContext.Current.CancellationToken);
|
||||
|
||||
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
|
||||
|
||||
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
|
||||
|
||||
var resultBytes = await File.ReadAllBytesAsync(metafilePath);
|
||||
var resultBytes = await File.ReadAllBytesAsync(metafilePath, TestContext.Current.CancellationToken);
|
||||
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
|
||||
|
||||
Assert.NotNull(result);
|
||||
@@ -232,7 +232,7 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
||||
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
|
||||
|
||||
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||
var resultBytes = await File.ReadAllBytesAsync(metafilePath);
|
||||
var resultBytes = await File.ReadAllBytesAsync(metafilePath, TestContext.Current.CancellationToken);
|
||||
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
|
||||
|
||||
Assert.NotNull(result);
|
||||
@@ -252,13 +252,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
||||
};
|
||||
|
||||
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
|
||||
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options), TestContext.Current.CancellationToken);
|
||||
|
||||
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
|
||||
|
||||
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
|
||||
|
||||
var resultBytes = await File.ReadAllBytesAsync(metafilePath);
|
||||
var resultBytes = await File.ReadAllBytesAsync(metafilePath, TestContext.Current.CancellationToken);
|
||||
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
|
||||
|
||||
Assert.NotNull(result);
|
||||
@@ -278,13 +278,13 @@ namespace Jellyfin.Server.Implementations.Tests.Plugins
|
||||
};
|
||||
|
||||
var metafilePath = Path.Combine(_pluginPath, "meta.json");
|
||||
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options));
|
||||
await File.WriteAllTextAsync(metafilePath, JsonSerializer.Serialize(partial, _options), TestContext.Current.CancellationToken);
|
||||
|
||||
var pluginManager = new PluginManager(new NullLogger<PluginManager>(), null!, null!, _tempPath, new Version(1, 0));
|
||||
|
||||
await pluginManager.PopulateManifest(packageInfo, new Version(1, 0), _pluginPath, PluginStatus.Active);
|
||||
|
||||
var resultBytes = await File.ReadAllBytesAsync(metafilePath);
|
||||
var resultBytes = await File.ReadAllBytesAsync(metafilePath, TestContext.Current.CancellationToken);
|
||||
var result = JsonSerializer.Deserialize<PluginManifest>(resultBytes, _options);
|
||||
|
||||
Assert.NotNull(result);
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
|
||||
PackageInfo[] packages = await _installationManager.GetPackages(
|
||||
"Jellyfin Stable",
|
||||
"https://repo.jellyfin.org/files/plugin/manifest.json",
|
||||
false);
|
||||
false,
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(25, packages.Length);
|
||||
}
|
||||
@@ -62,7 +63,8 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
|
||||
PackageInfo[] packages = await _installationManager.GetPackages(
|
||||
"Jellyfin Stable",
|
||||
"https://repo.jellyfin.org/files/plugin/manifest.json",
|
||||
false);
|
||||
false,
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
packages = _installationManager.FilterPackages(packages, "Anime").ToArray();
|
||||
Assert.Single(packages);
|
||||
@@ -74,7 +76,8 @@ namespace Jellyfin.Server.Implementations.Tests.Updates
|
||||
PackageInfo[] packages = await _installationManager.GetPackages(
|
||||
"Jellyfin Stable",
|
||||
"https://repo.jellyfin.org/files/plugin/manifest.json",
|
||||
false);
|
||||
false,
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
packages = _installationManager.FilterPackages(packages, id: new Guid("a4df60c5-6ab4-412a-8f79-2cab93fb2bc5")).ToArray();
|
||||
Assert.Single(packages);
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("System/ActivityLog/Entries");
|
||||
var response = await client.GetAsync("System/ActivityLog/Entries", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
|
||||
|
||||
@@ -25,13 +25,13 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/Branding/Configuration");
|
||||
var response = await client.GetAsync("/Branding/Configuration", TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
|
||||
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
|
||||
await response.Content.ReadFromJsonAsync<BrandingOptions>();
|
||||
await response.Content.ReadFromJsonAsync<BrandingOptions>(TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -43,7 +43,7 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync(url);
|
||||
var response = await client.GetAsync(url, TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
Assert.True(response.IsSuccessStatusCode);
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists");
|
||||
var response = await client.GetAsync("web/ConfigurationPage?name=ThisPageDoesntExists", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
@@ -37,12 +37,12 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin");
|
||||
var response = await client.GetAsync("/web/ConfigurationPage?name=TestPlugin", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType);
|
||||
StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!);
|
||||
Assert.Equal(await response.Content.ReadAsStringAsync(), await reader.ReadToEndAsync());
|
||||
Assert.Equal(await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken), await reader.ReadToEndAsync(TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -50,7 +50,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage");
|
||||
var response = await client.GetAsync("/web/ConfigurationPage?name=BrokenPage", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
@@ -61,11 +61,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("/web/ConfigurationPages");
|
||||
var response = await client.GetAsync("/web/ConfigurationPages", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
_ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions);
|
||||
_ = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
// TODO: check content
|
||||
}
|
||||
|
||||
@@ -75,13 +75,13 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true");
|
||||
var response = await client.GetAsync("/web/ConfigurationPages?enableInMainMenu=true", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
|
||||
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
|
||||
|
||||
var data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions);
|
||||
var data = await response.Content.ReadFromJsonAsync<ConfigurationPageInfo[]>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(data);
|
||||
Assert.Empty(data);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("Items");
|
||||
var response = await client.GetAsync("Items", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()));
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -55,9 +55,9 @@ public sealed class ItemsControllerTests : IClassFixture<JellyfinApplicationFact
|
||||
|
||||
var userDto = await AuthHelper.GetUserDtoAsync(client);
|
||||
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id));
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var items = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions);
|
||||
var items = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()));
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()));
|
||||
var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public sealed class LibraryControllerTests : IClassFixture<JellyfinApplicationFa
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()));
|
||||
var response = await client.DeleteAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Xunit;
|
||||
using Xunit.Priority;
|
||||
using Xunit.v3.Priority;
|
||||
|
||||
namespace Jellyfin.Server.Integration.Tests.Controllers;
|
||||
|
||||
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
||||
[TestCaseOrderer(typeof(PriorityOrderer))]
|
||||
public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinApplicationFactory>
|
||||
{
|
||||
private readonly JellyfinApplicationFactory _factory;
|
||||
@@ -40,7 +40,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
|
||||
}
|
||||
};
|
||||
|
||||
using var response = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", body, _jsonOptions);
|
||||
using var response = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", body, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
|
||||
LibraryOptions = new LibraryOptions()
|
||||
};
|
||||
|
||||
using var response = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions);
|
||||
using var response = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -76,16 +76,16 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
|
||||
}
|
||||
};
|
||||
|
||||
using var createResponse = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", createBody, _jsonOptions);
|
||||
using var createResponse = await client.PostAsJsonAsync("Library/VirtualFolders?name=test&refreshLibrary=true", createBody, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NoContent, createResponse.StatusCode);
|
||||
|
||||
await Task.Delay(2000).ConfigureAwait(true);
|
||||
await Task.Delay(2000, TestContext.Current.CancellationToken).ConfigureAwait(true);
|
||||
|
||||
using var response = await client.GetAsync("Library/VirtualFolders");
|
||||
using var response = await client.GetAsync("Library/VirtualFolders", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var library = await response.Content.ReadFromJsonAsAsyncEnumerable<VirtualFolderInfo>(_jsonOptions)
|
||||
.FirstOrDefaultAsync(x => string.Equals(x?.Name, "test", StringComparison.Ordinal));
|
||||
var library = await response.Content.ReadFromJsonAsAsyncEnumerable<VirtualFolderInfo>(_jsonOptions, TestContext.Current.CancellationToken)
|
||||
.FirstOrDefaultAsync(x => string.Equals(x?.Name, "test", StringComparison.Ordinal), TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(library);
|
||||
|
||||
var options = library.LibraryOptions;
|
||||
@@ -99,7 +99,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
|
||||
LibraryOptions = options
|
||||
};
|
||||
|
||||
using var response2 = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions);
|
||||
using var response2 = await client.PostAsJsonAsync("Library/VirtualFolders/LibraryOptions", body, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NoContent, response2.StatusCode);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var response = await client.DeleteAsync("Library/VirtualFolders?name=doesntExist");
|
||||
using var response = await client.DeleteAsync("Library/VirtualFolders?name=doesntExist", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ public sealed class LibraryStructureControllerTests : IClassFixture<JellyfinAppl
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var response = await client.DeleteAsync("Library/VirtualFolders?name=test&refreshLibrary=true");
|
||||
using var response = await client.DeleteAsync("Library/VirtualFolders?name=test&refreshLibrary=true", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFac
|
||||
Url = "Test Data/dummy.m3u8"
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
|
||||
var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
@@ -49,12 +49,12 @@ public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFac
|
||||
Url = "Test Data/dummy.m3u8"
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
|
||||
var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
|
||||
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
|
||||
var responseBody = await response.Content.ReadFromJsonAsync<TunerHostInfo>();
|
||||
var responseBody = await response.Content.ReadFromJsonAsync<TunerHostInfo>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(responseBody);
|
||||
Assert.Equal(body.Type, responseBody.Type);
|
||||
Assert.Equal(body.Url, responseBody.Url);
|
||||
@@ -72,7 +72,7 @@ public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFac
|
||||
Url = "Test Data/dummy.m3u8"
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
|
||||
var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
@@ -89,7 +89,7 @@ public sealed class LiveTvControllerTests : IClassFixture<JellyfinApplicationFac
|
||||
Url = "thisgoesnowhere"
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions);
|
||||
var response = await client.PostAsJsonAsync("/LiveTv/TunerHosts", body, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("Playback/BitrateTest");
|
||||
var response = await client.GetAsync("Playback/BitrateTest", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType);
|
||||
@@ -36,7 +36,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture));
|
||||
var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Octet, response.Content.Headers.ContentType?.MediaType);
|
||||
@@ -53,7 +53,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture));
|
||||
var response = await client.GetAsync("Playback/BitrateTest?size=" + size.ToString(CultureInfo.InvariantCulture), TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var postContent = new ByteArrayContent(Array.Empty<byte>());
|
||||
var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent);
|
||||
var response = await client.PostAsync("Library/VirtualFolders/Name?name=+&newName=test", postContent, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var postContent = new ByteArrayContent(Array.Empty<byte>());
|
||||
var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent);
|
||||
var response = await client.PostAsync("Library/VirtualFolders/Name?name=test&newName=+", postContent, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
@@ -53,7 +53,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var postContent = new ByteArrayContent(Array.Empty<byte>());
|
||||
var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent);
|
||||
var response = await client.PostAsync("Library/VirtualFolders/Name?name=doesnt+exist&newName=test", postContent, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
Path = "/this/path/doesnt/exist"
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions);
|
||||
var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths", data, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
PathInfo = new MediaPathInfo("test")
|
||||
};
|
||||
|
||||
var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions);
|
||||
var response = await client.PostAsJsonAsync("Library/VirtualFolders/Paths/Update", data, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
@@ -98,7 +98,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+");
|
||||
var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=+", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
@@ -109,7 +109,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist");
|
||||
var response = await client.DeleteAsync("Library/VirtualFolders/Paths?name=none&path=%2Fthis%2Fpath%2Fdoesnt%2Fexist", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class MusicGenreControllerTests : IClassFixture<JellyfinApplicatio
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("MusicGenres/Fake-MusicGenre");
|
||||
var response = await client.GetAsync("MusicGenres/Fake-MusicGenre", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory>
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var response = await client.GetAsync($"Persons/DoesntExist");
|
||||
using var response = await client.GetAsync($"Persons/DoesntExist", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}");
|
||||
using var response = await client.DeleteAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null);
|
||||
using var response = await client.PostAsync($"Users/{Guid.NewGuid()}/PlayedItems/{Guid.NewGuid()}", null, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
|
||||
|
||||
var userDto = await AuthHelper.GetUserDtoAsync(client);
|
||||
|
||||
using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}");
|
||||
using var response = await client.DeleteAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class PlaystateControllerTests : IClassFixture<JellyfinApplicationFactory
|
||||
|
||||
var userDto = await AuthHelper.GetUserDtoAsync(client);
|
||||
|
||||
using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null);
|
||||
using var response = await client.PostAsync($"Users/{userDto.Id}/PlayedItems/{Guid.NewGuid()}", null, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed class PluginsControllerTests : IClassFixture<JellyfinApplicationFa
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/Plugins");
|
||||
var response = await client.GetAsync("/Plugins", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
@@ -35,11 +35,11 @@ public sealed class PluginsControllerTests : IClassFixture<JellyfinApplicationFa
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync("/Plugins");
|
||||
var response = await client.GetAsync("/Plugins", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
|
||||
Assert.Equal(Encoding.UTF8.BodyName, response.Content.Headers.ContentType?.CharSet);
|
||||
_ = await response.Content.ReadFromJsonAsync<PluginInfo[]>(JsonDefaults.Options);
|
||||
_ = await response.Content.ReadFromJsonAsync<PluginInfo[]>(JsonDefaults.Options, TestContext.Current.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Models.StartupDtos;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Xunit;
|
||||
using Xunit.Priority;
|
||||
using Xunit.v3.Priority;
|
||||
|
||||
namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
||||
[TestCaseOrderer(typeof(PriorityOrderer))]
|
||||
public sealed class StartupControllerTests : IClassFixture<JellyfinApplicationFactory>
|
||||
{
|
||||
private readonly JellyfinApplicationFactory _factory;
|
||||
@@ -37,14 +37,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
PreferredMetadataLanguage = "nl"
|
||||
};
|
||||
|
||||
using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions);
|
||||
using var postResponse = await client.PostAsJsonAsync("/Startup/Configuration", config, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
|
||||
|
||||
using var getResponse = await client.GetAsync("/Startup/Configuration");
|
||||
using var getResponse = await client.GetAsync("/Startup/Configuration", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType);
|
||||
|
||||
var newConfig = await getResponse.Content.ReadFromJsonAsync<StartupConfigurationDto>(_jsonOptions);
|
||||
var newConfig = await getResponse.Content.ReadFromJsonAsync<StartupConfigurationDto>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(config.ServerName, newConfig!.ServerName);
|
||||
Assert.Equal(config.UICulture, newConfig.UICulture);
|
||||
Assert.Equal(config.MetadataCountryCode, newConfig.MetadataCountryCode);
|
||||
@@ -57,11 +57,11 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("/Startup/User");
|
||||
using var response = await client.GetAsync("/Startup/User", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Json, response.Content.Headers.ContentType?.MediaType);
|
||||
|
||||
var user = await response.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions);
|
||||
var user = await response.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(user);
|
||||
Assert.NotNull(user.Name);
|
||||
Assert.NotEmpty(user.Name);
|
||||
@@ -80,14 +80,14 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
Password = "NewPassword"
|
||||
};
|
||||
|
||||
var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions);
|
||||
var postResponse = await client.PostAsJsonAsync("/Startup/User", user, _jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NoContent, postResponse.StatusCode);
|
||||
|
||||
var getResponse = await client.GetAsync("/Startup/User");
|
||||
var getResponse = await client.GetAsync("/Startup/User", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode);
|
||||
Assert.Equal(MediaTypeNames.Application.Json, getResponse.Content.Headers.ContentType?.MediaType);
|
||||
|
||||
var newUser = await getResponse.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions);
|
||||
var newUser = await getResponse.Content.ReadFromJsonAsync<StartupUserDto>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(newUser);
|
||||
Assert.Equal(user.Name, newUser.Name);
|
||||
Assert.Null(newUser.Password);
|
||||
@@ -99,7 +99,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
|
||||
var response = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("/Startup/User");
|
||||
using var response = await client.GetAsync("/Startup/User", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ using Jellyfin.Api.Models.UserDtos;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using Xunit;
|
||||
using Xunit.Priority;
|
||||
using Xunit.v3.Priority;
|
||||
|
||||
namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
||||
[TestCaseOrderer(typeof(PriorityOrderer))]
|
||||
public sealed class UserControllerTests : IClassFixture<JellyfinApplicationFactory>
|
||||
{
|
||||
private const string TestUsername = "testUser01";
|
||||
@@ -41,9 +41,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync("Users/Public");
|
||||
using var response = await client.GetAsync("Users/Public", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions);
|
||||
var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
// User are hidden by default
|
||||
Assert.NotNull(users);
|
||||
Assert.Empty(users);
|
||||
@@ -56,9 +56,9 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var response = await client.GetAsync("Users");
|
||||
using var response = await client.GetAsync("Users", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions);
|
||||
var users = await response.Content.ReadFromJsonAsync<UserDto[]>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(users);
|
||||
Assert.Single(users);
|
||||
}
|
||||
@@ -89,7 +89,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
|
||||
using var response = await CreateUserByName(client, createRequest);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOptions);
|
||||
var user = await response.Content.ReadFromJsonAsync<UserDto>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(TestUsername, user!.Name);
|
||||
|
||||
_testUserId = user.Id;
|
||||
@@ -128,7 +128,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers
|
||||
// access token can't be null here as the previous test populated it
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken!);
|
||||
|
||||
using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}");
|
||||
using var response = await client.DeleteAsync($"User/{Guid.NewGuid()}", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root");
|
||||
var response = await client.GetAsync($"Users/{Guid.NewGuid()}/Items/Root", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
|
||||
|
||||
var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client);
|
||||
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id));
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, Guid.NewGuid(), rootFolderDto.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
|
||||
|
||||
var userDto = await AuthHelper.GetUserDtoAsync(client);
|
||||
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid()));
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, Guid.NewGuid()), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
@@ -84,9 +84,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
|
||||
var userDto = await AuthHelper.GetUserDtoAsync(client);
|
||||
var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id);
|
||||
|
||||
var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}");
|
||||
var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(_jsonOptions);
|
||||
var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(rootDto);
|
||||
}
|
||||
|
||||
@@ -99,9 +99,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
|
||||
var userDto = await AuthHelper.GetUserDtoAsync(client);
|
||||
var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id);
|
||||
|
||||
var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros");
|
||||
var response = await client.GetAsync($"Users/{userDto.Id}/Items/{rootFolderDto.Id}/Intros", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var rootDto = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions);
|
||||
var rootDto = await response.Content.ReadFromJsonAsync<QueryResult<BaseItemDto>>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(rootDto);
|
||||
}
|
||||
|
||||
@@ -116,9 +116,9 @@ public sealed class UserLibraryControllerTests : IClassFixture<JellyfinApplicati
|
||||
var userDto = await AuthHelper.GetUserDtoAsync(client);
|
||||
var rootFolderDto = await AuthHelper.GetRootFolderDtoAsync(client, userDto.Id);
|
||||
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id));
|
||||
var response = await client.GetAsync(string.Format(CultureInfo.InvariantCulture, format, userDto.Id, rootFolderDto.Id), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto[]>(_jsonOptions);
|
||||
var rootDto = await response.Content.ReadFromJsonAsync<BaseItemDto[]>(_jsonOptions, TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(rootDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class VideosControllerTests : IClassFixture<JellyfinApplicationFac
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}");
|
||||
var response = await client.DeleteAsync($"Videos/{Guid.NewGuid()}", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl);
|
||||
var response = await client.GetAsync("Encoder/UrlDecode?" + sourceUrl, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
string reply = await response.Content.ReadAsStringAsync();
|
||||
string reply = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Equal(unencodedUrl, reply);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,9 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl);
|
||||
var response = await client.GetAsync("Encoder/UrlArrayDecode?" + sourceUrl, TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
string reply = await response.Content.ReadAsStringAsync();
|
||||
string reply = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
||||
Assert.Equal(unencodedUrl, reply);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" />
|
||||
<PackageReference Include="AutoFixture.Xunit3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Xunit.Priority" />
|
||||
<PackageReference Include="Xunit.v3.Priority" />
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
var appHost = (TestAppHost)host.Services.GetRequiredService<IApplicationHost>();
|
||||
appHost.ServiceProvider = host.Services;
|
||||
var applicationPaths = appHost.ServiceProvider.GetRequiredService<IApplicationPaths>();
|
||||
Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService<IConfiguration>()).GetAwaiter().GetResult();
|
||||
Program.ApplyStartupMigrationAsync((ServerApplicationPaths)applicationPaths, appHost.ServiceProvider.GetRequiredService<IConfiguration>(), new()).GetAwaiter().GetResult();
|
||||
Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.CoreInitialisation).GetAwaiter().GetResult();
|
||||
appHost.InitializeServices(Mock.Of<IConfiguration>()).GetAwaiter().GetResult();
|
||||
Program.ApplyCoreMigrationsAsync(appHost.ServiceProvider, Migrations.Stages.JellyfinMigrationStageTypes.AppInitialisation).GetAwaiter().GetResult();
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Jellyfin.Server.Integration.Tests.Middleware
|
||||
AllowAutoRedirect = false
|
||||
});
|
||||
|
||||
var response = await client.GetAsync("robots.txt");
|
||||
var response = await client.GetAsync("robots.txt", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
|
||||
Assert.Equal("web/robots.txt", response.Headers.Location?.ToString());
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Jellyfin.Server.Integration.Tests
|
||||
{
|
||||
@@ -25,7 +24,7 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api-docs/openapi.json");
|
||||
var response = await client.GetAsync("/api-docs/openapi.json", TestContext.Current.CancellationToken);
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -35,7 +34,7 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
string outputPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "openapi.json"));
|
||||
_outputHelper.WriteLine("Writing OpenAPI Spec JSON to '{0}'.", outputPath);
|
||||
await using var fs = AsyncFile.Create(outputPath);
|
||||
await response.Content.CopyToAsync(fs);
|
||||
await response.Content.CopyToAsync(fs, TestContext.Current.CancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoFixture" />
|
||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||
<PackageReference Include="AutoFixture.Xunit2" />
|
||||
<PackageReference Include="AutoFixture.Xunit3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -23,8 +23,8 @@ namespace Jellyfin.Server.Tests
|
||||
true,
|
||||
true,
|
||||
new string[] { "192.168.t", "127.0.0.1", "::1", "1234.1232.12.1234" },
|
||||
new IPAddress[] { IPAddress.Loopback },
|
||||
new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) });
|
||||
new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback },
|
||||
Array.Empty<IPNetwork>());
|
||||
|
||||
data.Add(
|
||||
true,
|
||||
@@ -37,8 +37,8 @@ namespace Jellyfin.Server.Tests
|
||||
true,
|
||||
true,
|
||||
new string[] { "::1" },
|
||||
Array.Empty<IPAddress>(),
|
||||
new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) });
|
||||
new IPAddress[] { IPAddress.IPv6Loopback },
|
||||
Array.Empty<IPNetwork>());
|
||||
|
||||
data.Add(
|
||||
false,
|
||||
@@ -58,15 +58,15 @@ namespace Jellyfin.Server.Tests
|
||||
false,
|
||||
true,
|
||||
new string[] { "localhost" },
|
||||
Array.Empty<IPAddress>(),
|
||||
new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) });
|
||||
new IPAddress[] { IPAddress.IPv6Loopback },
|
||||
Array.Empty<IPNetwork>());
|
||||
|
||||
data.Add(
|
||||
true,
|
||||
true,
|
||||
new string[] { "localhost" },
|
||||
new IPAddress[] { IPAddress.Loopback },
|
||||
new IPNetwork[] { new IPNetwork(IPAddress.IPv6Loopback, 128) });
|
||||
new IPAddress[] { IPAddress.Loopback, IPAddress.IPv6Loopback },
|
||||
Array.Empty<IPNetwork>());
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Test Data\**\*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
@@ -9,7 +13,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -294,5 +294,48 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
||||
// Verify that the lowercase "tmdbcol" is NOT in the provider IDs
|
||||
Assert.False(item.ProviderIds.ContainsKey("tmdbcol"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_CommunityRating_ValidRating_Success()
|
||||
{
|
||||
var result = new MetadataResult<Video>()
|
||||
{
|
||||
Item = new Movie()
|
||||
};
|
||||
|
||||
_parser.Fetch(result, "Test Data/CommunityRating.nfo", CancellationToken.None);
|
||||
var item = (Movie)result.Item;
|
||||
|
||||
Assert.Equal(7.5f, item.CommunityRating);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_CommunityRating_OutOfRange_Ignored()
|
||||
{
|
||||
var result = new MetadataResult<Video>()
|
||||
{
|
||||
Item = new Movie()
|
||||
};
|
||||
|
||||
_parser.Fetch(result, "Test Data/CommunityRating_OutOfRange.nfo", CancellationToken.None);
|
||||
var item = (Movie)result.Item;
|
||||
|
||||
// Rating should not be set if outside 0-10 range
|
||||
Assert.Null(item.CommunityRating);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_CommunityRating_Comma()
|
||||
{
|
||||
var result = new MetadataResult<Video>()
|
||||
{
|
||||
Item = new Movie()
|
||||
};
|
||||
|
||||
_parser.Fetch(result, "Test Data/CommunityRating_Comma.nfo", CancellationToken.None);
|
||||
var item = (Movie)result.Item;
|
||||
|
||||
Assert.Equal(7.5f, item.CommunityRating);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<movie>
|
||||
<title>Test Movie</title>
|
||||
<communityrating>7.5</communityrating>
|
||||
</movie>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<movie>
|
||||
<title>Test Movie</title>
|
||||
<communityrating>7,5</communityrating>
|
||||
</movie>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<movie>
|
||||
<title>Test Movie</title>
|
||||
<communityrating>15.5</communityrating>
|
||||
</movie>
|
||||
Reference in New Issue
Block a user