1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-16 21:15:33 -04:00

Compare commits

..

9 Commits

Author SHA1 Message Date
Taloth Saldono
52588509ed Fixed failing test and some flaky tests. 2018-03-16 22:00:59 +01:00
Thirrian
e607a67f00 Fixed: Sorting for series "A.P. Bio"
closes #2450
2018-03-12 17:03:59 +01:00
Kevin Richter
96d7382a1c Fixed: Parsing # in front of absolute numbers 2018-03-10 21:41:28 +01:00
Taloth Saldono
e15530cee1 Fixed: TheXEM mapping with one scene release to multiple tvdb episodes. 2018-03-09 23:10:30 +01:00
Taloth Saldono
940f59468a New: Required/Ignored restrictions now support /pattern/ regular expressions. 2018-03-09 23:10:29 +01:00
Marcelo Castagna
ff885ab3bd Fixed: Added errorcode 160 - Permission denied on FileStation for easier diagnostics 2018-03-09 22:53:15 +01:00
Steven
17cfaf170e Add missing error check when adding a magnet link to deluge (#2295)
* Add missing error check when adding a magnet link to deluge

* Fix typo.
2018-03-09 22:51:52 +01:00
Thirrian
f1b2186313 Fix typo 2018-03-09 22:49:54 +01:00
Mark McDowall
ac379e3b84 Fixed: Don't add category when removing torrent from qBittorrent
Fixes #2438
2018-03-02 06:32:24 -08:00
22 changed files with 281 additions and 51 deletions

View File

@@ -29,6 +29,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR"
}
};
Mocker.SetConstant<ITermMatcher>(Mocker.Resolve<TermMatcher>());
}
private void GivenRestictions(string required, string ignored)
@@ -123,5 +125,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
[TestCase("/WEB/", true)]
[TestCase("/WEB\b/", false)]
[TestCase("/WEb/", false)]
[TestCase(@"/\.WEB/", true)]
public void should_match_perl_regex(string pattern, bool expected)
{
GivenRestictions(pattern, null);
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().Be(expected);
}
}
}

View File

@@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
Mocker.GetMock<IDownloadClientStatusRepository>()
.Verify(v => v.UpdateMany(
It.Is<List<DownloadClientStatus>>(i => i.All(
s => s.DisabledTill.Value < DateTime.UtcNow.AddMinutes(disabledTillTime)))
s => s.DisabledTill.Value <= DateTime.UtcNow.AddMinutes(disabledTillTime)))
)
);
}

View File

@@ -137,6 +137,9 @@ namespace NzbDrone.Core.Test.Messaging.Commands
QueueAndWaitForExecution(commandModel);
VerifyEventPublished<CommandExecutedEvent>();
Thread.Sleep(10);
ExceptionVerification.ExpectedErrors(1);
}

View File

@@ -85,6 +85,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[Cthuyuu] Taimadou Gakuen 35 Shiken Shoutai - 03 [720p H264 AAC][8AD82C3A]", "Taimadou Gakuen 35 Shiken Shoutai", 3, 0, 0)]
[TestCase("Dragon Ball Super Episode 56 [VOSTFR V2][720p][AAC]-Mystic Z-Team", "Dragon Ball Super", 56, 0, 0)]
[TestCase("[Mystic Z-Team] Dragon Ball Super Episode 69 [VOSTFR_Finale][1080p][AAC].mp4", "Dragon Ball Super", 69, 0, 0)]
[TestCase("[Shark-Raws] Crayon Shin-chan #957 (NBN 1280x720 x264 AAC).mp4", "Crayon Shin-chan", 957, 0, 0)]
//[TestCase("", "", 0, 0, 0)]
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
{

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DataAugmentation.Scene;
@@ -117,6 +118,10 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
{
GivenAbsoluteNumberingSeries();
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.FindEpisodesBySceneNumbering(It.IsAny<int>(), It.IsAny<int>()))
.Returns(new List<Episode>());
Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId, _singleEpisodeSearchCriteria);
Mocker.GetMock<IEpisodeService>()
@@ -253,7 +258,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
public void should_find_episode_by_season_and_absolute_episode_number_when_scene_absolute_episode_number_returns_multiple_results(int seasonNumber)
public void should_return_episodes_when_scene_absolute_episode_number_returns_multiple_results(int seasonNumber)
{
GivenAbsoluteNumberingSeries();
@@ -265,6 +270,32 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
.Setup(s => s.FindEpisodesBySceneNumbering(It.IsAny<int>(), seasonNumber, It.IsAny<int>()))
.Returns(Builder<Episode>.CreateListOfSize(5).Build().ToList());
var result = Subject.GetEpisodes(_parsedEpisodeInfo, _series, true, null);
result.Should().HaveCount(5);
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisodesBySceneNumbering(It.IsAny<int>(), seasonNumber, It.IsAny<int>()), Times.Once());
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.FindEpisode(It.IsAny<int>(), seasonNumber, It.IsAny<int>()), Times.Never());
}
[TestCase(0)]
[TestCase(1)]
[TestCase(2)]
public void should_find_episode_by_season_and_absolute_episode_number_when_scene_absolute_episode_number_returns_no_results(int seasonNumber)
{
GivenAbsoluteNumberingSeries();
Mocker.GetMock<ISceneMappingService>()
.Setup(s => s.GetSceneSeasonNumber(_parsedEpisodeInfo.SeriesTitle, It.IsAny<string>()))
.Returns(seasonNumber);
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.FindEpisodesBySceneNumbering(It.IsAny<int>(), seasonNumber, It.IsAny<int>()))
.Returns(new List<Episode>());
Subject.GetEpisodes(_parsedEpisodeInfo, _series, true, null);
Mocker.GetMock<IEpisodeService>()

View File

@@ -8,7 +8,8 @@ namespace NzbDrone.Core.Test.TvTests
public class SeriesTitleNormalizerFixture
{
[TestCase("A to Z", 281588, "a to z")]
[TestCase("A. D. - The Trials & Triumph of the Early Church", 266757, "ad trials triumph early church")]
[TestCase("A.D. The Bible Continues", 289260, "ad bible continues")]
[TestCase("A.P. Bio", 328534, "ap bio")]
public void should_use_precomputed_title(string title, int tvdbId, string expected)
{
SeriesTitleNormalizer.Normalize(title, tvdbId).Should().Be(expected);

View File

@@ -11,13 +11,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
{
public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification
{
private readonly IRestrictionService _restrictionService;
private readonly Logger _logger;
private readonly IRestrictionService _restrictionService;
private readonly ITermMatcher _termMatcher;
public ReleaseRestrictionsSpecification(IRestrictionService restrictionService, Logger logger)
public ReleaseRestrictionsSpecification(ITermMatcher termMatcher, IRestrictionService restrictionService, Logger logger)
{
_restrictionService = restrictionService;
_logger = logger;
_restrictionService = restrictionService;
_termMatcher = termMatcher;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
@@ -63,9 +65,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
return Decision.Accept();
}
private static List<string> ContainsAny(List<string> terms, string title)
private List<string> ContainsAny(List<string> terms, string title)
{
return terms.Where(t => title.ToLowerInvariant().Contains(t.ToLowerInvariant())).ToList();
return terms.Where(t => _termMatcher.IsMatch(t, title)).ToList();
}
}
}

View File

@@ -31,7 +31,25 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
if (!criteriaEpisodes.Intersect(remoteEpisodes).Any())
{
_logger.Debug("Release rejected since the episode wasn't requested: {0}", remoteEpisode.ParsedEpisodeInfo);
return Decision.Reject("Episode wasn't requested");
if (remoteEpisodes.Any())
{
var episodes = remoteEpisode.Episodes.OrderBy(v => v.SeasonNumber).ThenBy(v => v.EpisodeNumber).ToList();
if (episodes.Count > 1)
{
return Decision.Reject($"Episode wasn't requested: {episodes.First().SeasonNumber}x{episodes.First().EpisodeNumber}-{episodes.Last().EpisodeNumber}");
}
else
{
return Decision.Reject($"Episode wasn't requested: {episodes.First().SeasonNumber}x{episodes.First().EpisodeNumber}");
}
}
else
{
return Decision.Reject("Episode wasn't requested");
}
}
return Decision.Accept();

View File

@@ -25,8 +25,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
}
var singleEpisodeSpec = searchCriteria as SingleEpisodeSearchCriteria;
if (singleEpisodeSpec == null) return Decision.Accept();
if (singleEpisodeSpec != null) return IsSatisfiedBy(remoteEpisode, singleEpisodeSpec);
var animeEpisodeSpec = searchCriteria as AnimeEpisodeSearchCriteria;
if (animeEpisodeSpec != null) return IsSatisfiedBy(remoteEpisode, animeEpisodeSpec);
return Decision.Accept();
}
private Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SingleEpisodeSearchCriteria singleEpisodeSpec)
{
if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber)
{
_logger.Debug("Season number does not match searched season number, skipping.");
@@ -47,5 +55,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
return Decision.Accept();
}
private Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, AnimeEpisodeSearchCriteria singleEpisodeSpec)
{
if (remoteEpisode.ParsedEpisodeInfo.FullSeason)
{
_logger.Debug("Full season result during single episode search, skipping.");
return Decision.Reject("Full season pack");
}
return Decision.Accept();
}
}
}

View File

@@ -35,6 +35,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
{
var actualHash = _proxy.AddTorrentFromMagnet(magnetLink, Settings);
if (actualHash.IsNullOrWhiteSpace())
{
throw new DownloadClientException("Deluge failed to add magnet " + magnetLink);
}
if (!Settings.TvCategory.IsNullOrWhiteSpace())
{
_proxy.SetLabel(actualHash, Settings.TvCategory, Settings);

View File

@@ -47,6 +47,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation.Responses
FileStationMessages = new Dictionary<int, string>
{
{ 160, "Permission denied. Give your user access to FileStation."},
{ 400, "Invalid parameter of file operation" },
{ 401, "Unknown error of file operation" },
{ 402, "System is too busy" },

View File

@@ -97,6 +97,11 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
var result = ProcessRequest(request, settings);
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.TvCategory);
}
// Note: Current qbit versions return nothing, so we can't do != "Ok." here.
if (result == "Fails.")
{
@@ -110,11 +115,6 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
.Post()
.AddFormParameter("hashes", hash);
if (settings.TvCategory.IsNotNullOrWhiteSpace())
{
request.AddFormParameter("category", settings.TvCategory);
}
ProcessRequest(request, settings);
}

View File

@@ -349,7 +349,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
try
{
Logger.Debug("Formatiting audio channels using 'AudioChannelPositions', with a value of: '{0}'", audioChannelPositions);
Logger.Debug("Formatting audio channels using 'AudioChannelPositions', with a value of: '{0}'", audioChannelPositions);
if (audioChannelPositions.Contains("+"))
{

View File

@@ -1087,9 +1087,11 @@
<Compile Include="Queue\Queue.cs" />
<Compile Include="Queue\QueueService.cs" />
<Compile Include="Queue\QueueUpdatedEvent.cs" />
<Compile Include="Restrictions\PerlRegexFactory.cs" />
<Compile Include="Restrictions\Restriction.cs" />
<Compile Include="Restrictions\RestrictionRepository.cs" />
<Compile Include="Restrictions\RestrictionService.cs" />
<Compile Include="Restrictions\TermMatcher.cs" />
<Compile Include="Rest\JsonNetSerializer.cs" />
<Compile Include="Rest\RestClientFactory.cs" />
<Compile Include="Rest\RestException.cs" />

View File

@@ -59,7 +59,7 @@ namespace NzbDrone.Core.Parser
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - [SubGroup] Title Absolute Episode Number
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?(?<absoluteepisode>\d{2,3}(?!\d+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
new Regex(@"^\[(?<subgroup>.+?)\][-_. ]?(?<title>.+?)[-_. ]+\(?(?:[-_. ]?#?(?<absoluteepisode>\d{2,3}(?!\d+)))+\)?(?:[-_. ]+(?<special>special|ova|ovd))?.*?(?<hash>\[\w{8}\])?(?:$|\.mkv)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Anime - Title Season EpisodeNumber + Absolute Episode Number [SubGroup]

View File

@@ -363,13 +363,13 @@ namespace NzbDrone.Core.Parser
foreach (var absoluteEpisodeNumber in parsedEpisodeInfo.AbsoluteEpisodeNumbers)
{
Episode episode = null;
var episodes = new List<Episode>();
if (parsedEpisodeInfo.Special)
{
episode = _episodeService.FindEpisode(series.Id, 0, absoluteEpisodeNumber);
var episode = _episodeService.FindEpisode(series.Id, 0, absoluteEpisodeNumber);
episodes.AddIfNotNull(episode);
}
else if (sceneSource)
{
// Is there a reason why we excluded season 1 from this handling before?
@@ -377,31 +377,33 @@ namespace NzbDrone.Core.Parser
// If this needs to be reverted tests will need to be added
if (sceneSeasonNumber.HasValue)
{
var episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, sceneSeasonNumber.Value, absoluteEpisodeNumber);
episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, sceneSeasonNumber.Value, absoluteEpisodeNumber);
if (episodes.Count == 1)
if (episodes.Empty())
{
episode = episodes.First();
}
if (episode == null)
{
episode = _episodeService.FindEpisode(series.Id, sceneSeasonNumber.Value, absoluteEpisodeNumber);
var episode = _episodeService.FindEpisode(series.Id, sceneSeasonNumber.Value, absoluteEpisodeNumber);
episodes.AddIfNotNull(episode);
}
}
else
{
episode = _episodeService.FindEpisodeBySceneNumbering(series.Id, absoluteEpisodeNumber);
episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, absoluteEpisodeNumber);
// Don't allow multiple results without a scene name mapping.
if (episodes.Count > 1)
{
episodes.Clear();
}
}
}
if (episode == null)
if (episodes.Empty())
{
episode = _episodeService.FindEpisode(series.Id, absoluteEpisodeNumber);
var episode = _episodeService.FindEpisode(series.Id, absoluteEpisodeNumber);
episodes.AddIfNotNull(episode);
}
if (episode != null)
foreach (var episode in episodes)
{
_logger.Debug("Using absolute episode number {0} for: {1} - TVDB: {2}x{3:00}",
absoluteEpisodeNumber,

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Restrictions
{
public static class PerlRegexFactory
{
private static Regex _perlRegexFormat = new Regex(@"/(?<pattern>.*)/(?<modifiers>[a-z]*)", RegexOptions.Compiled);
public static bool TryCreateRegex(string pattern, out Regex regex)
{
var match = _perlRegexFormat.Match(pattern);
if (!match.Success)
{
regex = null;
return false;
}
regex = CreateRegex(match.Groups["pattern"].Value, match.Groups["modifiers"].Value);
return true;
}
public static Regex CreateRegex(string pattern, string modifiers)
{
var options = GetOptions(modifiers);
// For now we simply expect the pattern to be .net compliant. We should probably check and reject perl-specific constructs.
return new Regex(pattern, options | RegexOptions.Compiled);
}
private static RegexOptions GetOptions(string modifiers)
{
var options = RegexOptions.None;
foreach (var modifier in modifiers)
{
switch (modifier)
{
case 'm':
options |= RegexOptions.Multiline;
break;
case 's':
options |= RegexOptions.Singleline;
break;
case 'i':
options |= RegexOptions.IgnoreCase;
break;
case 'x':
options |= RegexOptions.IgnorePatternWhitespace;
break;
case 'n':
options |= RegexOptions.ExplicitCapture;
break;
default:
throw new ArgumentException("Unknown or unsupported perl regex modifier: " + modifier);
}
}
return options;
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NzbDrone.Common.Cache;
namespace NzbDrone.Core.Restrictions
{
public interface ITermMatcher
{
bool IsMatch(string term, string value);
}
public class TermMatcher : ITermMatcher
{
private ICached<Predicate<string>> _matcherCache;
public TermMatcher(ICacheManager cacheManager)
{
_matcherCache = cacheManager.GetCache<Predicate<string>>(GetType());
}
public bool IsMatch(string term, string value)
{
return GetMatcher(term)(value);
}
public Predicate<string> GetMatcher(string term)
{
return _matcherCache.Get(term, () => CreateMatcherInternal(term), TimeSpan.FromHours(24));
}
private Predicate<string> CreateMatcherInternal(string term)
{
Regex regex;
if (PerlRegexFactory.TryCreateRegex(term, out regex))
{
return regex.IsMatch;
}
else
{
return new CaseInsensitiveTermMatcher(term).IsMatch;
}
}
private sealed class CaseInsensitiveTermMatcher
{
private readonly string _term;
public CaseInsensitiveTermMatcher(string term)
{
_term = term.ToLowerInvariant();
}
public bool IsMatch(string value)
{
return value.ToLowerInvariant().Contains(_term);
}
}
}
}

View File

@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Tv
PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials);
PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber);
Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored);
void SetMonitoredFlat(Episode episode, bool monitored);
void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored);
@@ -134,21 +134,15 @@ namespace NzbDrone.Core.Tv
{
return Query.Where(s => s.SeriesId == seriesId)
.AndWhere(s => s.SceneSeasonNumber == seasonNumber)
.AndWhere(s => s.SceneEpisodeNumber == episodeNumber);
.AndWhere(s => s.SceneEpisodeNumber == episodeNumber)
.ToList();
}
public Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber)
public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber)
{
var episodes = Query.Where(s => s.SeriesId == seriesId)
return Query.Where(s => s.SeriesId == seriesId)
.AndWhere(s => s.SceneAbsoluteEpisodeNumber == sceneAbsoluteEpisodeNumber)
.ToList();
if (episodes.Empty() || episodes.Count > 1)
{
return null;
}
return episodes.Single();
}
public List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored)

View File

@@ -19,7 +19,7 @@ namespace NzbDrone.Core.Tv
Episode FindEpisode(int seriesId, int absoluteEpisodeNumber);
Episode FindEpisodeByTitle(int seriesId, int seasonNumber, string releaseTitle);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber);
Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber);
Episode GetEpisode(int seriesId, string date);
Episode FindEpisode(int seriesId, string date);
List<Episode> GetEpisodeBySeries(int seriesId);
@@ -78,9 +78,9 @@ namespace NzbDrone.Core.Tv
return _episodeRepository.FindEpisodesBySceneNumbering(seriesId, seasonNumber, episodeNumber);
}
public Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber)
public List<Episode> FindEpisodesBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber)
{
return _episodeRepository.FindEpisodeBySceneNumbering(seriesId, sceneAbsoluteEpisodeNumber);
return _episodeRepository.FindEpisodesBySceneNumbering(seriesId, sceneAbsoluteEpisodeNumber);
}
public Episode GetEpisode(int seriesId, string date)

View File

@@ -7,8 +7,8 @@ namespace NzbDrone.Core.Tv
private readonly static Dictionary<int, string> PreComputedTitles = new Dictionary<int, string>
{
{ 281588, "a to z" },
{ 266757, "ad trials triumph early church" },
{ 289260, "ad bible continues"}
{ 289260, "ad bible continues"},
{ 328534, "ap bio"}
};
public static string Normalize(string title, int tvdbId)

View File

@@ -13,8 +13,11 @@ namespace NzbDrone.Mono.Test.EnvironmentInfo
[Test]
public void should_get_framework_version()
{
Subject.Version.Major.Should().Be(4);
Subject.Version.Minor.Should().BeOneOf(0, 5, 6);
Subject.Version.Major.Should().BeOneOf(4, 5);
if (Subject.Version.Major == 4)
{
Subject.Version.Minor.Should().BeOneOf(0, 5, 6);
}
}
}
}