using System; using System.Collections.Generic; using System.Linq; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Clients; using NzbDrone.Core.Download.Clients.DownloadStation; using NzbDrone.Core.Download.Clients.DownloadStation.Proxies; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Test.Common; namespace NzbDrone.Core.Test.Download.DownloadClientTests.DownloadStationTests { [TestFixture] public class TorrentDownloadStationFixture : DownloadClientFixtureBase { protected DownloadStationSettings _settings; protected DownloadStationTask _queued; protected DownloadStationTask _downloading; protected DownloadStationTask _failed; protected DownloadStationTask _completed; protected DownloadStationTask _seeding; protected DownloadStationTask _magnet; protected DownloadStationTask _singleFile; protected DownloadStationTask _multipleFiles; protected DownloadStationTask _singleFileCompleted; protected DownloadStationTask _multipleFilesCompleted; protected string _serialNumber = "SERIALNUMBER"; protected string _category = "lidarr"; protected string _musicDirectory = @"music/Artist"; protected string _defaultDestination = "somepath"; protected OsPath _physicalPath = new OsPath("/mnt/sdb1/mydata"); protected Dictionary _downloadStationConfigItems; protected string DownloadURL => "magnet:?xt=urn:btih:5dee65101db281ac9c46344cd6b175cdcad53426&dn=download"; [SetUp] public void Setup() { _settings = new DownloadStationSettings() { Host = "127.0.0.1", Port = 5000, Username = "admin", Password = "pass" }; Subject.Definition = new DownloadClientDefinition(); Subject.Definition.Settings = _settings; _queued = new DownloadStationTask() { Id = "id1", Size = 1000, Status = DownloadStationTaskStatus.Waiting, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "0" }, { "size_uploaded", "0" }, { "speed_download", "0" } } } }; _completed = new DownloadStationTask() { Id = "id2", Size = 1000, Status = DownloadStationTaskStatus.Finished, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "1000" }, { "size_uploaded", "100" }, { "speed_download", "0" } }, } }; _seeding = new DownloadStationTask() { Id = "id2", Size = 1000, Status = DownloadStationTaskStatus.Seeding, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "1000" }, { "size_uploaded", "100" }, { "speed_download", "0" } } } }; _downloading = new DownloadStationTask() { Id = "id3", Size = 1000, Status = DownloadStationTaskStatus.Downloading, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "100" }, { "size_uploaded", "10" }, { "speed_download", "50" } } } }; _failed = new DownloadStationTask() { Id = "id4", Size = 1000, Status = DownloadStationTaskStatus.Error, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "10" }, { "size_uploaded", "1" }, { "speed_download", "0" } } } }; _singleFile = new DownloadStationTask() { Id = "id5", Size = 1000, Status = DownloadStationTaskStatus.Seeding, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "a.mkv", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "1000" }, { "size_uploaded", "100" }, { "speed_download", "0" } } } }; _multipleFiles = new DownloadStationTask() { Id = "id6", Size = 1000, Status = DownloadStationTaskStatus.Seeding, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "1000" }, { "size_uploaded", "100" }, { "speed_download", "0" } } } }; _singleFileCompleted = new DownloadStationTask() { Id = "id6", Size = 1000, Status = DownloadStationTaskStatus.Finished, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "a.mkv", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "1000" }, { "size_uploaded", "100" }, { "speed_download", "0" } } } }; _multipleFilesCompleted = new DownloadStationTask() { Id = "id6", Size = 1000, Status = DownloadStationTaskStatus.Finished, Type = DownloadStationTaskType.BT.ToString(), Username = "admin", Title = "title", Additional = new DownloadStationTaskAdditional { Detail = new Dictionary { { "destination", "shared/folder" }, { "uri", DownloadURL } }, Transfer = new Dictionary { { "size_downloaded", "1000" }, { "size_uploaded", "100" }, { "speed_download", "0" } } } }; Mocker.GetMock() .Setup(s => s.GetHashFromTorrentFile(It.IsAny())) .Returns("CBC2F069FE8BB2F544EAE707D75BCD3DE9DCF951"); Mocker.GetMock() .Setup(s => s.Get(It.IsAny())) .Returns(r => new HttpResponse(r, new HttpHeader(), Array.Empty())); _downloadStationConfigItems = new Dictionary { { "default_destination", _defaultDestination }, }; Mocker.GetMock() .Setup(v => v.GetConfig(It.IsAny())) .Returns(_downloadStationConfigItems); Mocker.GetMock() .Setup(s => s.GetProxy(It.IsAny())) .Returns(Mocker.GetMock().Object); } protected void GivenSharedFolder() { Mocker.GetMock() .Setup(s => s.RemapToFullPath(It.IsAny(), It.IsAny(), It.IsAny())) .Returns((path, setttings, serial) => _physicalPath); } protected void GivenSharedFolder(string share) { Mocker.GetMock() .Setup(s => s.RemapToFullPath(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new DownloadClientException("There is no matching shared folder")); Mocker.GetMock() .Setup(s => s.RemapToFullPath(It.Is(x => x.FullPath == share), It.IsAny(), It.IsAny())) .Returns((path, setttings, serial) => _physicalPath); } protected void GivenSerialNumber() { Mocker.GetMock() .Setup(s => s.GetSerialNumber(It.IsAny())) .Returns(_serialNumber); } protected void GivenMusicCategory() { _settings.MusicCategory = _category; } protected void GivenTvDirectory() { _settings.TvDirectory = _musicDirectory; } protected virtual void GivenTasks(List torrents) { if (torrents == null) { torrents = new List(); } Mocker.GetMock() .Setup(s => s.GetTasks(It.IsAny())) .Returns(torrents); } protected void PrepareClientToReturnQueuedItem() { GivenTasks(new List { _queued }); } protected void GivenSuccessfulDownload() { Mocker.GetMock() .Setup(s => s.Get(It.IsAny())) .Returns(r => new HttpResponse(r, new HttpHeader(), new byte[1000])); Mocker.GetMock() .Setup(s => s.AddTaskFromUrl(It.IsAny(), It.IsAny(), It.IsAny())) .Callback(PrepareClientToReturnQueuedItem); Mocker.GetMock() .Setup(s => s.AddTaskFromData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback(PrepareClientToReturnQueuedItem); } protected override RemoteAlbum CreateRemoteAlbum() { var album = base.CreateRemoteAlbum(); album.Release.DownloadUrl = DownloadURL; return album; } protected int GivenAllKindOfTasks() { var tasks = new List() { _queued, _completed, _failed, _downloading, _seeding }; Mocker.GetMock() .Setup(d => d.GetTasks(_settings)) .Returns(tasks); return tasks.Count; } [Test] public void Download_with_TvDirectory_should_force_directory() { GivenSerialNumber(); GivenTvDirectory(); GivenSuccessfulDownload(); var remoteAlbum = CreateRemoteAlbum(); var id = Subject.Download(remoteAlbum); id.Should().NotBeNullOrEmpty(); Mocker.GetMock() .Verify(v => v.AddTaskFromUrl(It.IsAny(), _musicDirectory, It.IsAny()), Times.Once()); } [Test] public void Download_with_category_should_force_directory() { GivenSerialNumber(); GivenMusicCategory(); GivenSuccessfulDownload(); var remoteAlbum = CreateRemoteAlbum(); var id = Subject.Download(remoteAlbum); id.Should().NotBeNullOrEmpty(); Mocker.GetMock() .Verify(v => v.AddTaskFromUrl(It.IsAny(), $"{_defaultDestination}/{_category}", It.IsAny()), Times.Once()); } [Test] public void Download_without_TvDirectory_and_Category_should_use_default() { GivenSerialNumber(); GivenSuccessfulDownload(); var remoteAlbum = CreateRemoteAlbum(); var id = Subject.Download(remoteAlbum); id.Should().NotBeNullOrEmpty(); Mocker.GetMock() .Verify(v => v.AddTaskFromUrl(It.IsAny(), _defaultDestination, It.IsAny()), Times.Once()); } [Test] public void GetItems_should_return_empty_list_if_no_tasks_available() { GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List()); Subject.GetItems().Should().BeEmpty(); } [Test] public void GetItems_should_return_ignore_tasks_of_unknown_type() { GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List { _completed }); _completed.Type = "ipfs"; Subject.GetItems().Should().BeEmpty(); } [Test] public void GetItems_should_ignore_downloads_in_wrong_folder() { _settings.TvDirectory = @"/shared/folder/sub"; GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List { _completed }); Subject.GetItems().Should().BeEmpty(); } [Test] public void GetItems_should_throw_if_shared_folder_resolve_fails() { Mocker.GetMock() .Setup(s => s.RemapToFullPath(It.IsAny(), It.IsAny(), It.IsAny())) .Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException")); GivenSerialNumber(); GivenAllKindOfTasks(); Assert.Throws(Is.InstanceOf(), () => Subject.GetItems()); ExceptionVerification.ExpectedErrors(0); } [Test] public void GetItems_should_throw_if_serial_number_unavailable() { Mocker.GetMock() .Setup(s => s.GetSerialNumber(_settings)) .Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException")); GivenSharedFolder(); GivenAllKindOfTasks(); Assert.Throws(Is.InstanceOf(), () => Subject.GetItems()); ExceptionVerification.ExpectedErrors(0); } [Test] public void Download_should_throw_and_not_add_task_if_cannot_get_serial_number() { var remoteAlbum = CreateRemoteAlbum(); Mocker.GetMock() .Setup(s => s.GetSerialNumber(_settings)) .Throws(new ApplicationException("Some unknown exception, HttpException or DownloadClientException")); Assert.Throws(Is.InstanceOf(), () => Subject.Download(remoteAlbum)); Mocker.GetMock() .Verify(v => v.AddTaskFromUrl(It.IsAny(), null, _settings), Times.Never()); } [Test] public void GetStatus_should_map_outputpath_when_using_default() { GivenSerialNumber(); GivenSharedFolder("/somepath"); var status = Subject.GetStatus(); status.OutputRootFolders.First().Should().Be(_physicalPath); } [Test] public void GetStatus_should_map_outputpath_when_using_destination() { GivenSerialNumber(); GivenTvDirectory(); GivenSharedFolder($"/{_musicDirectory}"); var status = Subject.GetStatus(); status.OutputRootFolders.First().Should().Be(_physicalPath); } [Test] public void GetStatus_should_map_outputpath_when_using_category() { GivenSerialNumber(); GivenMusicCategory(); GivenSharedFolder($"/somepath/{_category}"); var status = Subject.GetStatus(); status.OutputRootFolders.First().Should().Be(_physicalPath); } [Test] public void GetItems_should_set_outputPath_to_base_folder_when_single_file_non_finished_tasks() { GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List() { _singleFile }); var items = Subject.GetItems(); items.Should().HaveCount(1); items.First().OutputPath.Should().Be(_physicalPath + _singleFile.Title); } [Test] public void GetItems_should_set_outputPath_to_torrent_folder_when_multiple_files_non_finished_tasks() { GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List() { _multipleFiles }); var items = Subject.GetItems(); items.Should().HaveCount(1); items.First().OutputPath.Should().Be(_physicalPath + _multipleFiles.Title); } [Test] public void GetItems_should_set_outputPath_to_base_folder_when_single_file_finished_tasks() { GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List() { _singleFileCompleted }); var items = Subject.GetItems(); items.Should().HaveCount(1); items.First().OutputPath.Should().Be(_physicalPath + _singleFileCompleted.Title); } [Test] public void GetItems_should_set_outputPath_to_torrent_folder_when_multiple_files_finished_tasks() { GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List() { _multipleFilesCompleted }); var items = Subject.GetItems(); items.Should().HaveCount(1); items.First().OutputPath.Should().Be($"{_physicalPath}/{_multipleFiles.Title}"); } [Test] public void GetItems_should_not_map_outputpath_for_queued_or_downloading_tasks() { GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List { _queued, _downloading }); var items = Subject.GetItems(); items.Should().HaveCount(2); items.Should().OnlyContain(v => v.OutputPath.IsEmpty); } [Test] public void GetItems_should_map_outputpath_for_completed_or_failed_tasks() { GivenSerialNumber(); GivenSharedFolder(); GivenTasks(new List { _completed, _failed, _seeding }); var items = Subject.GetItems(); items.Should().HaveCount(3); items.Should().OnlyContain(v => !v.OutputPath.IsEmpty); } [TestCase(DownloadStationTaskStatus.Downloading, false, false)] [TestCase(DownloadStationTaskStatus.Finished, true, true)] [TestCase(DownloadStationTaskStatus.Seeding, true, false)] [TestCase(DownloadStationTaskStatus.Waiting, false, false)] public void GetItems_should_return_canBeMoved_and_canBeDeleted_as_expected(DownloadStationTaskStatus apiStatus, bool canMoveFilesExpected, bool canBeRemovedExpected) { GivenSerialNumber(); GivenSharedFolder(); _queued.Status = apiStatus; GivenTasks(new List() { _queued }); var items = Subject.GetItems(); items.Should().HaveCount(1); var item = items.First(); item.CanBeRemoved.Should().Be(canBeRemovedExpected); item.CanMoveFiles.Should().Be(canMoveFilesExpected); } [TestCase(DownloadStationTaskStatus.Downloading, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.Error, DownloadItemStatus.Failed)] [TestCase(DownloadStationTaskStatus.Extracting, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.Finished, DownloadItemStatus.Completed)] [TestCase(DownloadStationTaskStatus.Finishing, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.HashChecking, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.CaptchaNeeded, DownloadItemStatus.Downloading)] [TestCase(DownloadStationTaskStatus.Paused, DownloadItemStatus.Paused)] [TestCase(DownloadStationTaskStatus.Seeding, DownloadItemStatus.Completed)] [TestCase(DownloadStationTaskStatus.FilehostingWaiting, DownloadItemStatus.Queued)] [TestCase(DownloadStationTaskStatus.Waiting, DownloadItemStatus.Queued)] [TestCase(DownloadStationTaskStatus.Unknown, DownloadItemStatus.Queued)] public void GetItems_should_return_item_as_downloadItemStatus(DownloadStationTaskStatus apiStatus, DownloadItemStatus expectedItemStatus) { GivenSerialNumber(); GivenSharedFolder(); _queued.Status = apiStatus; GivenTasks(new List() { _queued }); var items = Subject.GetItems(); items.Should().HaveCount(1); items.First().Status.Should().Be(expectedItemStatus); } } }