Fixed: Automation/Integration/Unit Tests

This commit is contained in:
Qstick 2019-09-02 22:22:25 -04:00
parent 944f420270
commit 7f221c7834
123 changed files with 1384 additions and 1191 deletions

21
.gitattributes vendored
View File

@ -1,22 +1,9 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
*text eol=lf *text eol=lf
# Explicitly set bash scripts to have unix endings
*.sh text eol=lf
# Custom for Visual Studio # Custom for Visual Studio
*.cs diff=csharp *.cs diff=csharp
#*.sln merge=union *.sln merge=union
#*.csproj merge=union
#*.vbproj merge=union
#*.fsproj merge=union
#*.dbproj merge=union
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

4
.gitmodules vendored
View File

@ -1,4 +0,0 @@
[submodule "src/ExternalModules/CurlSharp"]
path = src/ExternalModules/CurlSharp
url = https://github.com/Sonarr/CurlSharp.git
branch = master

View File

@ -238,17 +238,16 @@ stages:
buildType: 'current' buildType: 'current'
artifactName: WindowsTests artifactName: WindowsTests
targetPath: $(testsFolder) targetPath: $(testsFolder)
- bash: |
wget https://mediaarea.net/repo/deb/repo-mediaarea_1.0-9_all.deb
sudo dpkg -i repo-mediaarea_1.0-9_all.deb
sudo apt-get update
sudo apt-get install -y libmediainfo-dev libmediainfo0v5 mediainfo
displayName: Install mediainfo
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- bash: |
sudo apt install dos2unix
dos2unix ${TESTSFOLDER}/test.sh
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
- bash: |
brew install dos2unix
dos2unix ${TESTSFOLDER}/test.sh
condition: and(succeeded(), eq(variables['osName'], 'Mac'))
- task: Bash@3 - task: Bash@3
displayName: Run Tests displayName: Run Tests
env: env:
@ -326,14 +325,6 @@ stages:
mkdir -p ./bin/ mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/ cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
displayName: Move Package Contents displayName: Move Package Contents
- bash: |
sudo apt install dos2unix
dos2unix ${TESTSFOLDER}/test.sh
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
- bash: |
brew install dos2unix
dos2unix ${TESTSFOLDER}/test.sh
condition: and(succeeded(), eq(variables['osName'], 'Mac'))
- task: Bash@3 - task: Bash@3
displayName: Run Integration Tests displayName: Run Integration Tests
inputs: inputs:

View File

@ -139,7 +139,7 @@ Build()
RunGulp() RunGulp()
{ {
ProgressStart 'yarn install' ProgressStart 'yarn install'
yarn install yarn install --frozen-lockfile
ProgressEnd 'yarn install' ProgressEnd 'yarn install'
LintUI LintUI
@ -254,8 +254,6 @@ PackageTests()
echo "Adding Radarr.Core.dll.config (for dllmap)" echo "Adding Radarr.Core.dll.config (for dllmap)"
cp $sourceFolder/NzbDrone.Core/Radarr.Core.dll.config $testPackageFolder cp $sourceFolder/NzbDrone.Core/Radarr.Core.dll.config $testPackageFolder
echo "Copying CurlSharp libraries"
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
echo "Adding sqlite dylibs" echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $testPackageFolder cp $sourceFolder/Libraries/Sqlite/*.dylib $testPackageFolder

View File

@ -16,7 +16,9 @@ class About extends Component {
const { const {
version, version,
isMonoRuntime, isMonoRuntime,
isDocker,
runtimeVersion, runtimeVersion,
migrationVersion,
appData, appData,
startupPath, startupPath,
mode, mode,
@ -41,6 +43,19 @@ class About extends Component {
/> />
} }
{
isDocker &&
<DescriptionListItem
title="Docker"
data={'True'}
/>
}
<DescriptionListItem
title="DB Migration"
data={migrationVersion}
/>
<DescriptionListItem <DescriptionListItem
title="AppData directory" title="AppData directory"
data={appData} data={appData}
@ -77,6 +92,8 @@ About.propTypes = {
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
isMonoRuntime: PropTypes.bool.isRequired, isMonoRuntime: PropTypes.bool.isRequired,
runtimeVersion: PropTypes.string.isRequired, runtimeVersion: PropTypes.string.isRequired,
isDocker: PropTypes.bool.isRequired,
migrationVersion: PropTypes.number.isRequired,
appData: PropTypes.string.isRequired, appData: PropTypes.string.isRequired,
startupPath: PropTypes.string.isRequired, startupPath: PropTypes.string.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,

View File

@ -154,12 +154,26 @@ class Health extends Component {
const internalLink = getInternalLink(item.source); const internalLink = getInternalLink(item.source);
const testLink = getTestLink(item.source, this.props); const testLink = getTestLink(item.source, this.props);
let kind = kinds.WARNING;
switch (item.type.toLowerCase()) {
case 'error':
kind = kinds.DANGER;
break;
default:
case 'warning':
kind = kinds.WARNING;
break;
case 'notice':
kind = kinds.INFO;
break;
}
return ( return (
<TableRow key={`health${item.message}`}> <TableRow key={`health${item.message}`}>
<TableRowCell> <TableRowCell>
<Icon <Icon
name={icons.DANGER} name={icons.DANGER}
kind={item.type.toLowerCase() === 'error' ? kinds.DANGER : kinds.WARNING} kind={kind}
title={titleCase(item.type)} title={titleCase(item.type)}
/> />
</TableRowCell> </TableRowCell>

View File

@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react'; import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import formatDate from 'Utilities/Date/formatDate'; import formatDate from 'Utilities/Date/formatDate';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -26,7 +26,7 @@ class Updates extends Component {
generalSettingsError, generalSettingsError,
items, items,
isInstallingUpdate, isInstallingUpdate,
updateMechanism, isDocker,
shortDateFormat, shortDateFormat,
onInstallLatestPress onInstallLatestPress
} = this.props; } = this.props;
@ -37,12 +37,6 @@ class Updates extends Component {
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const externalUpdaterMessages = {
external: 'Unable to update Radarr directly, Radarr is configured to use an external update mechanism',
apt: 'Unable to update Radarr directly, use apt to install the update',
docker: 'Unable to update Radarr directly, update the docker container to receive the update'
};
return ( return (
<PageContent title="Updates"> <PageContent title="Updates">
<PageContentBodyConnector> <PageContentBodyConnector>
@ -58,29 +52,24 @@ class Updates extends Component {
{ {
hasUpdateToInstall && hasUpdateToInstall &&
<div className={styles.messageContainer}> <div className={styles.updateAvailable}>
{ {
updateMechanism === 'builtIn' || updateMechanism === 'script' ? !isDocker &&
<SpinnerButton <SpinnerButton
className={styles.updateAvailable} className={styles.updateAvailable}
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
isSpinning={isInstallingUpdate} isSpinning={isInstallingUpdate}
onPress={onInstallLatestPress} onPress={onInstallLatestPress}
> >
Install Latest Install Latest
</SpinnerButton> : </SpinnerButton>
}
<Fragment> {
<Icon isDocker &&
name={icons.WARNING} <div className={styles.upToDateMessage}>
kind={kinds.WARNING} An update is available. Please update your Docker image and re-create the container.
size={30} </div>
/>
<div className={styles.message}>
{externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external}
</div>
</Fragment>
} }
{ {
@ -209,6 +198,7 @@ Updates.propTypes = {
generalSettingsError: PropTypes.object, generalSettingsError: PropTypes.object,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
isInstallingUpdate: PropTypes.bool.isRequired, isInstallingUpdate: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
updateMechanism: PropTypes.string, updateMechanism: PropTypes.string,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
onInstallLatestPress: PropTypes.func.isRequired onInstallLatestPress: PropTypes.func.isRequired

View File

@ -7,6 +7,7 @@ import { fetchUpdates } from 'Store/Actions/systemActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import Updates from './Updates'; import Updates from './Updates';
@ -16,12 +17,14 @@ function createMapStateToProps() {
(state) => state.system.updates, (state) => state.system.updates,
(state) => state.settings.general, (state) => state.settings.general,
createUISettingsSelector(), createUISettingsSelector(),
createSystemStatusSelector(),
createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), createCommandExecutingSelector(commandNames.APPLICATION_UPDATE),
( (
currentVersion, currentVersion,
updates, updates,
generalSettings, generalSettings,
uiSettings, uiSettings,
systemStatus,
isInstallingUpdate isInstallingUpdate
) => { ) => {
const { const {
@ -40,6 +43,7 @@ function createMapStateToProps() {
generalSettingsError: generalSettings.error, generalSettingsError: generalSettings.error,
items, items,
isInstallingUpdate, isInstallingUpdate,
isDocker: systemStatus.isDocker,
updateMechanism: generalSettings.item.updateMechanism, updateMechanism: generalSettings.item.updateMechanism,
shortDateFormat: uiSettings.shortDateFormat shortDateFormat: uiSettings.shortDateFormat
}; };

View File

@ -20,8 +20,10 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Configuration Condition="'$(Configuration)'==''">Release</Configuration>
<!-- Centralize intermediate and default outputs --> <!-- Centralize intermediate and default outputs -->
<IntermediateOutputPath>$(RadarrRootDir)_temp\obj\$(Configuration)\$(MSBuildProjectName)\</IntermediateOutputPath> <BaseIntermediateOutputPath>$(RadarrRootDir)_temp\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
<IntermediateOutputPath>$(RadarrRootDir)_temp\obj\$(MSBuildProjectName)\$(Configuration)\</IntermediateOutputPath>
<OutputPath>$(RadarrRootDir)_temp\bin\$(Configuration)\$(MSBuildProjectName)\</OutputPath> <OutputPath>$(RadarrRootDir)_temp\bin\$(Configuration)\$(MSBuildProjectName)\</OutputPath>
<!-- Output to _output and _tests respectively --> <!-- Output to _output and _tests respectively -->
@ -30,6 +32,7 @@
<OutputPath Condition="'$(RadarrOutputType)'=='Update'">$(RadarrRootDir)_output\Radarr.Update\</OutputPath> <OutputPath Condition="'$(RadarrOutputType)'=='Update'">$(RadarrRootDir)_output\Radarr.Update\</OutputPath>
<!-- Paths relative to project file for better readability --> <!-- Paths relative to project file for better readability -->
<BaseIntermediateOutputPath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$(BaseIntermediateOutputPath)'))</BaseIntermediateOutputPath>
<IntermediateOutputPath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)'))</IntermediateOutputPath> <IntermediateOutputPath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)'))</IntermediateOutputPath>
<OutputPath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$(OutputPath)'))</OutputPath> <OutputPath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$(OutputPath)'))</OutputPath>

@ -1 +0,0 @@
Subproject commit cfdbbbd9c6b9612c2756245049a8234ce87dc576

View File

@ -1,9 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NzbDrone.Common.Disk;
using Nancy; using Nancy;
using Nancy.Responses; using Nancy.Responses;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using Radarr.Http; using Radarr.Http;
@ -54,6 +55,8 @@ namespace NzbDrone.Api.Logs
private Response GetLogFileResponse(string filename) private Response GetLogFileResponse(string filename)
{ {
LogManager.Flush();
var filePath = GetLogFilePath(filename); var filePath = GetLogFilePath(filename);
if (!_diskProvider.FileExists(filePath)) if (!_diskProvider.FileExists(filePath))

View File

@ -34,7 +34,9 @@ namespace NzbDrone.Automation.Test
[OneTimeSetUp] [OneTimeSetUp]
public void SmokeTestSetup() public void SmokeTestSetup()
{ {
driver = new FirefoxDriver(); var options = new FirefoxOptions();
options.AddArguments("--headless");
driver = new FirefoxDriver(options);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger()); _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
_runner.KillAll(); _runner.KillAll();
@ -45,7 +47,7 @@ namespace NzbDrone.Automation.Test
var page = new PageBase(driver); var page = new PageBase(driver);
page.WaitForNoSpinner(); page.WaitForNoSpinner();
driver.ExecuteScript("window.NzbDrone.NameViews = true;"); driver.ExecuteScript("window.Radarr.NameViews = true;");
GetPageErrors().Should().BeEmpty(); GetPageErrors().Should().BeEmpty();
} }

View File

@ -17,11 +17,11 @@ namespace NzbDrone.Automation.Test
} }
[Test] [Test]
public void series_page() public void movie_page()
{ {
page.SeriesNavIcon.Click(); page.MovieNavIcon.Click();
page.WaitForNoSpinner(); page.WaitForNoSpinner();
page.FindByClass("iv-series-index-seriesindexlayout").Should().NotBeNull(); page.Find(By.CssSelector("div[class*='MovieIndex']")).Should().NotBeNull();
} }
[Test] [Test]
@ -30,7 +30,7 @@ namespace NzbDrone.Automation.Test
page.CalendarNavIcon.Click(); page.CalendarNavIcon.Click();
page.WaitForNoSpinner(); page.WaitForNoSpinner();
page.FindByClass("iv-calendar-calendarlayout").Should().NotBeNull(); page.Find(By.CssSelector("div[class*='CalendarPage']")).Should().NotBeNull();
} }
[Test] [Test]
@ -39,16 +39,9 @@ namespace NzbDrone.Automation.Test
page.ActivityNavIcon.Click(); page.ActivityNavIcon.Click();
page.WaitForNoSpinner(); page.WaitForNoSpinner();
page.FindByClass("iv-activity-activitylayout").Should().NotBeNull(); page.Find(By.LinkText("Queue")).Should().NotBeNull();
} page.Find(By.LinkText("History")).Should().NotBeNull();
page.Find(By.LinkText("Blacklist")).Should().NotBeNull();
[Test]
public void wanted_page()
{
page.WantedNavIcon.Click();
page.WaitForNoSpinner();
page.FindByClass("iv-wanted-missing-missinglayout").Should().NotBeNull();
} }
[Test] [Test]
@ -57,20 +50,20 @@ namespace NzbDrone.Automation.Test
page.SystemNavIcon.Click(); page.SystemNavIcon.Click();
page.WaitForNoSpinner(); page.WaitForNoSpinner();
page.FindByClass("iv-system-systemlayout").Should().NotBeNull(); page.Find(By.CssSelector("div[class*='Health']")).Should().NotBeNull();
} }
[Test] [Test]
public void add_series_page() public void add_movie_page()
{ {
page.SeriesNavIcon.Click(); page.MovieNavIcon.Click();
page.WaitForNoSpinner(); page.WaitForNoSpinner();
page.Find(By.LinkText("Add Series")).Click(); page.Find(By.LinkText("Add New")).Click();
page.WaitForNoSpinner(); page.WaitForNoSpinner();
page.FindByClass("iv-addseries-addserieslayout").Should().NotBeNull(); page.Find(By.CssSelector("input[class*='AddNewMovie/searchInput']")).Should().NotBeNull();
} }
} }
} }

View File

@ -47,16 +47,14 @@ namespace NzbDrone.Automation.Test.PageModel
}); });
} }
public IWebElement SeriesNavIcon => FindByClass("x-series-nav"); public IWebElement MovieNavIcon => Find(By.LinkText("Movies"));
public IWebElement CalendarNavIcon => FindByClass("x-calendar-nav"); public IWebElement CalendarNavIcon => Find(By.LinkText("Calendar"));
public IWebElement ActivityNavIcon => FindByClass("x-activity-nav"); public IWebElement ActivityNavIcon => Find(By.LinkText("Activity"));
public IWebElement WantedNavIcon => FindByClass("x-wanted-nav"); public IWebElement SettingNavIcon => Find(By.LinkText("Settings"));
public IWebElement SettingNavIcon => FindByClass("x-settings-nav"); public IWebElement SystemNavIcon => Find(By.PartialLinkText("System"));
public IWebElement SystemNavIcon => FindByClass("x-system-nav");
} }
} }

View File

@ -4,6 +4,7 @@
<Platforms>x86</Platforms> <Platforms>x86</Platforms>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Selenium.Firefox.WebDriver" Version="0.24.0" />
<PackageReference Include="Selenium.Support" Version="3.141.0" /> <PackageReference Include="Selenium.Support" Version="3.141.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -248,12 +248,14 @@ namespace NzbDrone.Common.Test.DiskTests
} }
[Test] [Test]
[Ignore("No longer behaving this way in a Windows 10 Feature Update")]
public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_none() public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_none()
{ {
Assert.Throws<IOException>(() => DoHardLinkRename(FileShare.None)); Assert.Throws<IOException>(() => DoHardLinkRename(FileShare.None));
} }
[Test] [Test]
[Ignore("No longer behaving this way in a Windows 10 Feature Update")]
public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_write() public void should_not_be_able_to_rename_open_hardlinks_with_fileshare_write()
{ {
Assert.Throws<IOException>(() => DoHardLinkRename(FileShare.Read)); Assert.Throws<IOException>(() => DoHardLinkRename(FileShare.Read));

View File

@ -22,7 +22,6 @@ namespace NzbDrone.Common.Test.Http
{ {
[IntegrationTest] [IntegrationTest]
[TestFixture(typeof(ManagedHttpDispatcher))] [TestFixture(typeof(ManagedHttpDispatcher))]
[TestFixture(typeof(CurlHttpDispatcher))]
public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher public class HttpClientFixture<TDispatcher> : TestBase<HttpClient> where TDispatcher : IHttpDispatcher
{ {
private string[] _httpBinHosts; private string[] _httpBinHosts;

View File

@ -9,22 +9,21 @@ using NzbDrone.Common.Model;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
using NzbDrone.Test.Dummy; using NzbDrone.Test.Dummy;
using System.Reflection;
namespace NzbDrone.Common.Test namespace NzbDrone.Common.Test
{ {
[TestFixture] [TestFixture]
public class ProcessProviderTests : TestBase<ProcessProvider> public class ProcessProviderFixture : TestBase<ProcessProvider>
{ {
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
Process.GetProcessesByName(DummyApp.DUMMY_PROCCESS_NAME).ToList().ForEach(c => Process.GetProcessesByName(DummyApp.DUMMY_PROCCESS_NAME).ToList().ForEach(c =>
{ {
c.Kill(); c.Kill();
c.WaitForExit(); c.WaitForExit();
}); });
Process.GetProcessesByName(DummyApp.DUMMY_PROCCESS_NAME).Should().BeEmpty(); Process.GetProcessesByName(DummyApp.DUMMY_PROCCESS_NAME).Should().BeEmpty();
} }
@ -42,7 +41,7 @@ namespace NzbDrone.Common.Test
{ {
TestLogger.Warn(ex, "{0} when killing process", ex.Message); TestLogger.Warn(ex, "{0} when killing process", ex.Message);
} }
}); });
} }
@ -65,18 +64,9 @@ namespace NzbDrone.Common.Test
} }
[Test] [Test]
[Ignore("Shit appveyor")]
public void Should_be_able_to_start_process() public void Should_be_able_to_start_process()
{ {
string codeBase = Assembly.GetExecutingAssembly().CodeBase; var process = StartDummyProcess();
UriBuilder uri = new UriBuilder(codeBase);
string path = Uri.UnescapeDataString(uri.Path);
var rPath = Path.GetDirectoryName(path);
var root = Directory.GetParent(rPath).Parent.Parent.Parent;
var DummyAppDir = Path.Combine(root.FullName, "NzbDrone.Test.Dummy", "bin", "Release");
var process = Subject.Start(Path.Combine(DummyAppDir, DummyApp.DUMMY_PROCCESS_NAME + ".exe"));
Subject.Exists(DummyApp.DUMMY_PROCCESS_NAME).Should() Subject.Exists(DummyApp.DUMMY_PROCCESS_NAME).Should()
.BeTrue("excepted one dummy process to be already running"); .BeTrue("excepted one dummy process to be already running");
@ -88,6 +78,7 @@ namespace NzbDrone.Common.Test
} }
[Test] [Test]
[Explicit]
public void Should_be_able_to_start_powershell() public void Should_be_able_to_start_powershell()
{ {
WindowsOnly(); WindowsOnly();
@ -137,7 +128,6 @@ namespace NzbDrone.Common.Test
[Test] [Test]
[Ignore("Shit appveyor")]
public void kill_all_should_kill_all_process_with_name() public void kill_all_should_kill_all_process_with_name()
{ {
var dummy1 = StartDummyProcess(); var dummy1 = StartDummyProcess();
@ -151,7 +141,8 @@ namespace NzbDrone.Common.Test
private Process StartDummyProcess() private Process StartDummyProcess()
{ {
return Subject.Start(DummyApp.DUMMY_PROCCESS_NAME + ".exe"); var path = Path.Combine(TestContext.CurrentContext.TestDirectory, DummyApp.DUMMY_PROCCESS_NAME + ".exe");
return Subject.Start(path);
} }
[Test] [Test]
@ -161,4 +152,4 @@ namespace NzbDrone.Common.Test
ExceptionVerification.MarkInconclusive(typeof(Win32Exception)); ExceptionVerification.MarkInconclusive(typeof(Win32Exception));
} }
} }
} }

View File

@ -11,9 +11,4 @@
<ItemGroup> <ItemGroup>
<Reference Include="System.ServiceProcess" /> <Reference Include="System.ServiceProcess" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="..\ExternalModules\CurlSharp\libs\i386\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Security.Principal;
using System.ServiceProcess; using System.ServiceProcess;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
@ -61,6 +62,10 @@ namespace NzbDrone.Common.Test
[Test] [Test]
public void Service_should_be_installed_and_then_uninstalled() public void Service_should_be_installed_and_then_uninstalled()
{ {
if (!IsAnAdministrator())
{
Assert.Inconclusive("Can't run test without Administrator rights");
}
Subject.ServiceExist(TEMP_SERVICE_NAME).Should().BeFalse("Service already installed"); Subject.ServiceExist(TEMP_SERVICE_NAME).Should().BeFalse("Service already installed");
Subject.Install(TEMP_SERVICE_NAME); Subject.Install(TEMP_SERVICE_NAME);
@ -100,9 +105,13 @@ namespace NzbDrone.Common.Test
} }
[Test] [Test]
[Ignore("Shit appveyor")] public void should_throw_if_starting_a_running_service()
public void should_throw_if_starting_a_running_serivce()
{ {
if (!IsAnAdministrator())
{
Assert.Inconclusive("Can't run test without Administrator rights");
}
Subject.GetService(ALWAYS_INSTALLED_SERVICE).Status Subject.GetService(ALWAYS_INSTALLED_SERVICE).Status
.Should().NotBe(ServiceControllerStatus.Running); .Should().NotBe(ServiceControllerStatus.Running);
@ -128,5 +137,10 @@ namespace NzbDrone.Common.Test
ExceptionVerification.ExpectedWarns(1); ExceptionVerification.ExpectedWarns(1);
} }
private static bool IsAnAdministrator()
{
var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
} }
} }

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<dllmap dll="libcurl.dll" target="libcurl.so.4" />
<dllmap os="osx" dll="libcurl.dll" target="libcurl.4.dylib"/>
<!--<dllmap os="freebsd" dll="libcurl.dll" target="libcurl.so.4" />-->
<!--<dllmap os="solaris" dll="libcurl.dll" target="libcurl.so.4" />-->
</configuration>

View File

@ -27,6 +27,8 @@ namespace NzbDrone.Common.EnvironmentInfo
Release = $"{Version}-{Branch}"; Release = $"{Version}-{Branch}";
} }
public static string AppName { get; } = "Radarr";
public static Version Version { get; } public static Version Version { get; }
public static String Branch { get; } public static String Branch { get; }
public static string Release { get; } public static string Release { get; }

View File

@ -15,6 +15,9 @@ namespace NzbDrone.Common.EnvironmentInfo
public static bool IsOsx => Os == Os.Osx; public static bool IsOsx => Os == Os.Osx;
public static bool IsWindows => Os == Os.Windows; public static bool IsWindows => Os == Os.Windows;
// this needs to not be static so we can mock it
public bool IsDocker { get; }
public string Version { get; } public string Version { get; }
public string Name { get; } public string Name { get; }
public string FullName { get; } public string FullName { get; }
@ -83,8 +86,10 @@ namespace NzbDrone.Common.EnvironmentInfo
FullName = Name; FullName = Name;
} }
Environment.SetEnvironmentVariable("OS_NAME", Name); if (IsLinux && File.Exists("/proc/1/cgroup") && File.ReadAllText("/proc/1/cgroup").Contains("/docker/"))
Environment.SetEnvironmentVariable("OS_VERSION", Version); {
IsDocker = true;
}
} }
} }
@ -93,6 +98,7 @@ namespace NzbDrone.Common.EnvironmentInfo
string Version { get; } string Version { get; }
string Name { get; } string Name { get; }
string FullName { get; } string FullName { get; }
bool IsDocker { get; }
} }
public enum Os public enum Os

View File

@ -35,7 +35,16 @@ namespace NzbDrone.Common.EnvironmentInfo
static RuntimeInfo() static RuntimeInfo()
{ {
IsProduction = InternalIsProduction(); var officialBuild = InternalIsOfficialBuild();
// An build running inside of the testing environment. (Analytics disabled)
IsTesting = InternalIsTesting();
// An official build running outside of the testing environment. (Analytics configurable)
IsProduction = !IsTesting && officialBuild;
// An unofficial build running outside of the testing environment. (Analytics enabled)
IsDevelopment = !IsTesting && !officialBuild && !InternalIsDebug();
} }
public DateTime StartTime public DateTime StartTime
@ -104,23 +113,21 @@ namespace NzbDrone.Common.EnvironmentInfo
public bool RestartPending { get; set; } public bool RestartPending { get; set; }
public string ExecutingApplication { get; } public string ExecutingApplication { get; }
public static bool IsTesting { get; }
public static bool IsProduction { get; } public static bool IsProduction { get; }
public static bool IsDevelopment { get; }
private static bool InternalIsProduction()
private static bool InternalIsTesting()
{ {
if (BuildInfo.IsDebug || Debugger.IsAttached) return false;
//Official builds will never have such a high revision
if (BuildInfo.Version.Revision > 10000) return false;
try try
{ {
var lowerProcessName = Process.GetCurrentProcess().ProcessName.ToLower(); var lowerProcessName = Process.GetCurrentProcess().ProcessName.ToLower();
if (lowerProcessName.Contains("vshost")) return false; if (lowerProcessName.Contains("vshost")) return true;
if (lowerProcessName.Contains("nunit")) return false; if (lowerProcessName.Contains("nunit")) return true;
if (lowerProcessName.Contains("jetbrain")) return false; if (lowerProcessName.Contains("jetbrain")) return true;
if (lowerProcessName.Contains("resharper")) return false; if (lowerProcessName.Contains("resharper")) return true;
} }
catch catch
{ {
@ -130,7 +137,7 @@ namespace NzbDrone.Common.EnvironmentInfo
try try
{ {
var currentAssemblyLocation = typeof(RuntimeInfo).Assembly.Location; var currentAssemblyLocation = typeof(RuntimeInfo).Assembly.Location;
if (currentAssemblyLocation.ToLower().Contains("_output")) return false; if (currentAssemblyLocation.ToLower().Contains("_output")) return true;
} }
catch catch
{ {
@ -138,13 +145,28 @@ namespace NzbDrone.Common.EnvironmentInfo
} }
var lowerCurrentDir = Directory.GetCurrentDirectory().ToLower(); var lowerCurrentDir = Directory.GetCurrentDirectory().ToLower();
if (lowerCurrentDir.Contains("teamcity")) return false; if (lowerCurrentDir.Contains("vsts")) return true;
if (lowerCurrentDir.Contains("buildagent")) return false; if (lowerCurrentDir.Contains("buildagent")) return true;
if (lowerCurrentDir.Contains("_output")) return false; if (lowerCurrentDir.Contains("_output")) return true;
return false;
}
private static bool InternalIsDebug()
{
if (BuildInfo.IsDebug || Debugger.IsAttached) return true;
return false;
}
private static bool InternalIsOfficialBuild()
{
//Official builds will never have such a high revision
if (BuildInfo.Version.Major >= 10 || BuildInfo.Version.Revision > 10000) return false;
return true; return true;
} }
public bool IsWindowsTray { get; private set; } public bool IsWindowsTray { get; private set; }
} }
} }

View File

@ -1,342 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using CurlSharp;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
namespace NzbDrone.Common.Http.Dispatchers
{
public class CurlHttpDispatcher : IHttpDispatcher
{
private static readonly Regex ExpiryDate = new Regex(@"(expires=)([^;]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly Logger _logger;
private const string _caBundleFileName = "curl-ca-bundle.crt";
private static readonly string _caBundleFilePath;
static CurlHttpDispatcher()
{
if (Assembly.GetExecutingAssembly().Location.IsNotNullOrWhiteSpace())
{
_caBundleFilePath = Path.Combine(Assembly.GetExecutingAssembly().Location, "..", _caBundleFileName);
}
else
{
_caBundleFilePath = _caBundleFileName;
}
}
public CurlHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, IUserAgentBuilder userAgentBuilder, Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_userAgentBuilder = userAgentBuilder;
_logger = logger;
}
public bool CheckAvailability()
{
try
{
return CurlGlobalHandle.Instance.Initialize();
}
catch (Exception ex)
{
_logger.Trace(ex, "Initializing curl failed");
return false;
}
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
{
if (!CheckAvailability())
{
throw new ApplicationException("Curl failed to initialize.");
}
lock (CurlGlobalHandle.Instance)
{
Stream responseStream = new MemoryStream();
Stream headerStream = new MemoryStream();
using (var curlEasy = new CurlEasy())
{
curlEasy.AutoReferer = false;
curlEasy.WriteFunction = (b, s, n, o) =>
{
responseStream.Write(b, 0, s * n);
return s * n;
};
curlEasy.HeaderFunction = (b, s, n, o) =>
{
headerStream.Write(b, 0, s * n);
return s * n;
};
AddProxy(curlEasy, request);
curlEasy.Url = request.Url.FullUri;
switch (request.Method)
{
case HttpMethod.GET:
curlEasy.HttpGet = true;
break;
case HttpMethod.POST:
curlEasy.Post = true;
break;
case HttpMethod.PUT:
curlEasy.Put = true;
break;
default:
throw new NotSupportedException($"HttpCurl method {request.Method} not supported");
}
curlEasy.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
curlEasy.FollowLocation = false;
if (request.RequestTimeout != TimeSpan.Zero)
{
curlEasy.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalSeconds);
}
if (OsInfo.IsWindows)
{
curlEasy.CaInfo = _caBundleFilePath;
}
if (cookies != null)
{
curlEasy.Cookie = cookies.GetCookieHeader((Uri)request.Url);
}
if (request.ContentData != null)
{
curlEasy.PostFieldSize = request.ContentData.Length;
curlEasy.SetOpt(CurlOption.CopyPostFields, new string(Array.ConvertAll(request.ContentData, v => (char)v)));
}
// Yes, we have to keep a ref to the object to prevent corrupting the unmanaged state
using (var httpRequestHeaders = SerializeHeaders(request))
{
curlEasy.HttpHeader = httpRequestHeaders;
var result = curlEasy.Perform();
if (result != CurlCode.Ok)
{
switch (result)
{
case CurlCode.SslCaCert:
case (CurlCode)77:
throw new WebException(string.Format("Curl Error {0} for Url {1}, issues with your operating system SSL Root Certificate Bundle (ca-bundle).", result, curlEasy.Url));
default:
throw new WebException(string.Format("Curl Error {0} for Url {1}", result, curlEasy.Url));
}
}
}
var webHeaderCollection = ProcessHeaderStream(request, cookies, headerStream);
var responseData = ProcessResponseStream(request, responseStream, webHeaderCollection);
var httpHeader = new HttpHeader(webHeaderCollection);
return new HttpResponse(request, httpHeader, responseData, (HttpStatusCode)curlEasy.ResponseCode);
}
}
}
private void AddProxy(CurlEasy curlEasy, HttpRequest request)
{
var proxySettings = _proxySettingsProvider.GetProxySettings(request);
if (proxySettings != null)
{
switch (proxySettings.Type)
{
case ProxyType.Http:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Http);
curlEasy.SetOpt(CurlOption.ProxyAuth, CurlHttpAuth.Basic);
curlEasy.SetOpt(CurlOption.ProxyUserPwd, proxySettings.Username + ":" + proxySettings.Password.ToString());
break;
case ProxyType.Socks4:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Socks4);
curlEasy.SetOpt(CurlOption.ProxyUsername, proxySettings.Username);
curlEasy.SetOpt(CurlOption.ProxyPassword, proxySettings.Password);
break;
case ProxyType.Socks5:
curlEasy.SetOpt(CurlOption.ProxyType, CurlProxyType.Socks5);
curlEasy.SetOpt(CurlOption.ProxyUsername, proxySettings.Username);
curlEasy.SetOpt(CurlOption.ProxyPassword, proxySettings.Password);
break;
}
curlEasy.SetOpt(CurlOption.Proxy, proxySettings.Host + ":" + proxySettings.Port.ToString());
}
}
private CurlSlist SerializeHeaders(HttpRequest request)
{
if (!request.Headers.ContainsKey("Accept-Encoding"))
{
request.Headers.Add("Accept-Encoding", "gzip");
}
if (request.Headers.ContentType == null)
{
request.Headers.ContentType = string.Empty;
}
var curlHeaders = new CurlSlist();
foreach (var header in request.Headers)
{
curlHeaders.Append(header.Key + ": " + header.Value.ToString());
}
return curlHeaders;
}
private WebHeaderCollection ProcessHeaderStream(HttpRequest request, CookieContainer cookies, Stream headerStream)
{
headerStream.Position = 0;
var headerData = headerStream.ToBytes();
var headerString = Encoding.ASCII.GetString(headerData);
var webHeaderCollection = new WebHeaderCollection();
// following a redirect we could have two sets of headers, so only process the last one
foreach (var header in headerString.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Reverse())
{
if (!header.Contains(":")) break;
webHeaderCollection.Add(header);
}
var setCookie = webHeaderCollection.Get("Set-Cookie");
if (setCookie != null && setCookie.Length > 0 && cookies != null)
{
try
{
cookies.SetCookies((Uri)request.Url, FixSetCookieHeader(setCookie));
}
catch (CookieException ex)
{
_logger.Debug("Rejected cookie {0}: {1}", ex.InnerException.Message, setCookie);
}
}
return webHeaderCollection;
}
private string FixSetCookieHeader(string setCookie)
{
// fix up the date if it was malformed
var setCookieClean = ExpiryDate.Replace(setCookie, delegate(Match match)
{
string shortFormat = "ddd, dd-MMM-yy HH:mm:ss";
string longFormat = "ddd, dd-MMM-yyyy HH:mm:ss";
DateTime dt;
if (DateTime.TryParseExact(match.Groups[2].Value, longFormat, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt) ||
DateTime.TryParseExact(match.Groups[2].Value, shortFormat, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt) ||
DateTime.TryParse(match.Groups[2].Value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dt))
return match.Groups[1].Value + dt.ToUniversalTime().ToString(longFormat, CultureInfo.InvariantCulture) + " GMT";
else
return match.Value;
});
return setCookieClean;
}
private byte[] ProcessResponseStream(HttpRequest request, Stream responseStream, WebHeaderCollection webHeaderCollection)
{
byte[] bytes = null;
responseStream.Position = 0;
if (responseStream.Length != 0)
{
var encoding = webHeaderCollection["Content-Encoding"];
if (encoding != null)
{
if (encoding.IndexOf("gzip") != -1)
{
using (var zipStream = new GZipStream(responseStream, CompressionMode.Decompress))
{
bytes = zipStream.ToBytes();
}
webHeaderCollection.Remove("Content-Encoding");
}
else if (encoding.IndexOf("deflate") != -1)
{
using (var deflateStream = new DeflateStream(responseStream, CompressionMode.Decompress))
{
bytes = deflateStream.ToBytes();
}
webHeaderCollection.Remove("Content-Encoding");
}
}
}
if (bytes == null) bytes = responseStream.ToBytes();
return bytes;
}
}
internal sealed class CurlGlobalHandle : SafeHandle
{
public static readonly CurlGlobalHandle Instance = new CurlGlobalHandle();
private bool _initialized;
private bool _available;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
private CurlGlobalHandle()
: base(IntPtr.Zero, true)
{
}
public bool Initialize()
{
lock (CurlGlobalHandle.Instance)
{
if (_initialized)
return _available;
_initialized = true;
_available = Curl.GlobalInit(CurlInitFlag.All) == CurlCode.Ok;
return _available;
}
}
protected override bool ReleaseHandle()
{
if (_initialized && _available)
{
Curl.GlobalCleanup();
_available = false;
}
return true;
}
public override bool IsInvalid => !_initialized || !_available;
}
}

View File

@ -1,56 +0,0 @@
using System;
using System.Net;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Common.Http.Dispatchers
{
public class FallbackHttpDispatcher : IHttpDispatcher
{
private readonly ManagedHttpDispatcher _managedDispatcher;
private readonly CurlHttpDispatcher _curlDispatcher;
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger;
private readonly ICached<bool> _curlTLSFallbackCache;
public FallbackHttpDispatcher(ManagedHttpDispatcher managedDispatcher, CurlHttpDispatcher curlDispatcher, ICacheManager cacheManager, IPlatformInfo platformInfo, Logger logger)
{
_managedDispatcher = managedDispatcher;
_curlDispatcher = curlDispatcher;
_platformInfo = platformInfo;
_curlTLSFallbackCache = cacheManager.GetCache<bool>(GetType(), "curlTLSFallback");
_logger = logger;
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
{
if (PlatformInfo.IsMono && request.Url.Scheme == "https")
{
if (!_curlTLSFallbackCache.Find(request.Url.Host))
{
try
{
return _managedDispatcher.GetResponse(request, cookies);
}
catch (TlsFailureException)
{
_logger.Debug("https request failed in tls error for {0}, trying curl fallback.", request.Url.Host);
_curlTLSFallbackCache.Set(request.Url.Host, true);
}
}
if (_curlDispatcher.CheckAvailability())
{
return _curlDispatcher.GetResponse(request, cookies);
}
_logger.Trace("Curl not available, using default WebClient.");
}
return _managedDispatcher.GetResponse(request, cookies);
}
}
}

View File

@ -24,7 +24,7 @@ namespace NzbDrone.Common.Http
public HttpProvider(Logger logger) public HttpProvider(Logger logger)
{ {
_logger = logger; _logger = logger;
_userAgent = string.Format("Radarr {0}", BuildInfo.Version); _userAgent = $"{BuildInfo.AppName}/{BuildInfo.Version.ToString(2)}";
ServicePointManager.Expect100Continue = false; ServicePointManager.Expect100Continue = false;
} }

View File

@ -9,10 +9,10 @@ namespace NzbDrone.Common.Http
public class TlsFailureException : WebException public class TlsFailureException : WebException
{ {
public TlsFailureException(WebRequest request, WebException innerException) public TlsFailureException(WebRequest request, WebException innerException)
: base("Failed to establish secure https connection to '" + request.RequestUri + "', libcurl fallback might be unavailable.", innerException, WebExceptionStatus.SecureChannelFailure, innerException.Response) : base("Failed to establish secure https connection to '" + request.RequestUri + "'.", innerException, WebExceptionStatus.SecureChannelFailure, innerException.Response)
{ {
} }
} }
} }

View File

@ -33,8 +33,8 @@ namespace NzbDrone.Common.Http
var osVersion = osInfo.Version?.ToLower(); var osVersion = osInfo.Version?.ToLower();
_userAgent = $"Radarr/{BuildInfo.Version} ({osName} {osVersion})"; _userAgent = $"{BuildInfo.AppName}/{BuildInfo.Version} ({osName} {osVersion})";
_userAgentSimplified = $"Radarr/{BuildInfo.Version.ToString(2)}"; _userAgentSimplified = $"{BuildInfo.AppName}/{BuildInfo.Version.ToString(2)}";
} }
} }
} }

View File

@ -0,0 +1,28 @@
using System.Linq;
using NLog;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
using NzbDrone.Common.Instrumentation.Sentry;
namespace NzbDrone.Common.Instrumentation
{
public class InitializeLogger
{
private readonly IOsInfo _osInfo;
public InitializeLogger(IOsInfo osInfo)
{
_osInfo = osInfo;
}
public void Initialize()
{
var sentryTarget = LogManager.Configuration.AllTargets.OfType<SentryTarget>().FirstOrDefault();
if (sentryTarget != null)
{
sentryTarget.UpdateScope(_osInfo);
}
}
}
}

View File

@ -58,18 +58,6 @@ namespace NzbDrone.Common.Instrumentation
LogManager.ReconfigExistingLoggers(); LogManager.ReconfigExistingLoggers();
} }
public static void UnRegisterRemoteLoggers()
{
var sentryRules = LogManager.Configuration.LoggingRules.Where(r => r.Targets.Any(t => t.Name == "sentryTarget"));
foreach (var rules in sentryRules)
{
rules.Targets.Clear();
}
LogManager.ReconfigExistingLoggers();
}
private static void RegisterSentry(bool updateClient) private static void RegisterSentry(bool updateClient)
{ {
string dsn; string dsn;

View File

@ -8,6 +8,7 @@ using NLog;
using NLog.Common; using NLog.Common;
using NLog.Targets; using NLog.Targets;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using Sentry; using Sentry;
using Sentry.Protocol; using Sentry.Protocol;
@ -61,6 +62,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{LogLevel.Warn, BreadcrumbLevel.Warning}, {LogLevel.Warn, BreadcrumbLevel.Warning},
}; };
private readonly DateTime _startTime = DateTime.UtcNow;
private readonly IDisposable _sdk; private readonly IDisposable _sdk;
private bool _disposed; private bool _disposed;
@ -68,10 +70,8 @@ namespace NzbDrone.Common.Instrumentation.Sentry
private bool _unauthorized; private bool _unauthorized;
public bool FilterEvents { get; set; } public bool FilterEvents { get; set; }
public string UpdateBranch { get; set; } public bool SentryEnabled { get; set; }
public Version DatabaseVersion { get; set; }
public int DatabaseMigration { get; set; }
public SentryTarget(string dsn) public SentryTarget(string dsn)
{ {
_sdk = SentrySdk.Init(o => _sdk = SentrySdk.Init(o =>
@ -79,34 +79,81 @@ namespace NzbDrone.Common.Instrumentation.Sentry
o.Dsn = new Dsn(dsn); o.Dsn = new Dsn(dsn);
o.AttachStacktrace = true; o.AttachStacktrace = true;
o.MaxBreadcrumbs = 200; o.MaxBreadcrumbs = 200;
o.SendDefaultPii = true; o.SendDefaultPii = false;
o.Debug = false; o.Debug = false;
o.DiagnosticsLevel = SentryLevel.Debug; o.DiagnosticsLevel = SentryLevel.Debug;
o.Release = BuildInfo.Release; o.Release = BuildInfo.Release;
if (PlatformInfo.IsMono)
{
// Mono 6.0 broke GzipStream.WriteAsync
// TODO: Check specific version
o.RequestBodyCompressionLevel = System.IO.Compression.CompressionLevel.NoCompression;
}
o.BeforeSend = x => SentryCleanser.CleanseEvent(x); o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x); o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
o.Environment = BuildInfo.Branch;
}); });
SentrySdk.ConfigureScope(scope => InitializeScope();
{
scope.User = new User {
Username = HashUtil.AnonymousToken()
};
scope.SetTag("osfamily", OsInfo.Os.ToString());
scope.SetTag("runtime", PlatformInfo.PlatformName);
scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name);
scope.SetTag("branch", BuildInfo.Branch);
scope.SetTag("version", BuildInfo.Version.ToString());
scope.SetTag("production", RuntimeInfo.IsProduction.ToString());
});
_debounce = new SentryDebounce(); _debounce = new SentryDebounce();
// initialize to true and reconfigure later // initialize to true and reconfigure later
// Otherwise it will default to false and any errors occuring // Otherwise it will default to false and any errors occuring
// before config file gets read will not be filtered // before config file gets read will not be filtered
FilterEvents = true; FilterEvents = true;
SentryEnabled = true;
}
public void InitializeScope()
{
SentrySdk.ConfigureScope(scope =>
{
scope.User = new User
{
Id = HashUtil.AnonymousToken()
};
scope.Contexts.App.Name = BuildInfo.AppName;
scope.Contexts.App.Version = BuildInfo.Version.ToString();
scope.Contexts.App.StartTime = _startTime;
scope.Contexts.App.Hash = HashUtil.AnonymousToken();
scope.Contexts.App.Build = BuildInfo.Release; // Git commit cache?
scope.SetTag("culture", Thread.CurrentThread.CurrentCulture.Name);
scope.SetTag("branch", BuildInfo.Branch);
});
}
public void UpdateScope(IOsInfo osInfo)
{
SentrySdk.ConfigureScope(scope =>
{
scope.SetTag("is_docker", $"{osInfo.IsDocker}");
if (osInfo.Name != null && PlatformInfo.IsMono)
{
// Sentry auto-detection of non-Windows platforms isn't that accurate on certain devices.
scope.Contexts.OperatingSystem.Name = osInfo.Name.FirstCharToUpper();
scope.Contexts.OperatingSystem.RawDescription = osInfo.FullName;
scope.Contexts.OperatingSystem.Version = osInfo.Version.ToString();
}
});
}
public void UpdateScope(Version databaseVersion, int migration, string updateBranch, IPlatformInfo platformInfo)
{
SentrySdk.ConfigureScope(scope =>
{
scope.Environment = updateBranch;
scope.SetTag("runtime_version", $"{PlatformInfo.PlatformName} {platformInfo.Version}");
if (databaseVersion != default(Version))
{
scope.SetTag("sqlite_version", $"{databaseVersion}");
scope.SetTag("database_migration", $"{migration}");
}
});
} }
private void OnError(Exception ex) private void OnError(Exception ex)
@ -191,7 +238,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
protected override void Write(LogEventInfo logEvent) protected override void Write(LogEventInfo logEvent)
{ {
if (_unauthorized) if (_unauthorized || !SentryEnabled)
{ {
return; return;
} }
@ -227,27 +274,12 @@ namespace NzbDrone.Common.Instrumentation.Sentry
{ {
Level = LoggingLevelMap[logEvent.Level], Level = LoggingLevelMap[logEvent.Level],
Logger = logEvent.LoggerName, Logger = logEvent.LoggerName,
Message = logEvent.FormattedMessage, Message = logEvent.FormattedMessage
Environment = UpdateBranch
}; };
sentryEvent.SetExtras(extras); sentryEvent.SetExtras(extras);
sentryEvent.SetFingerprint(fingerPrint); sentryEvent.SetFingerprint(fingerPrint);
// this can't be in the constructor as at that point OsInfo won't have
// populated these values yet
var osName = Environment.GetEnvironmentVariable("OS_NAME");
var osVersion = Environment.GetEnvironmentVariable("OS_VERSION");
var isDocker = Environment.GetEnvironmentVariable("OS_IS_DOCKER");
var runTimeVersion = Environment.GetEnvironmentVariable("RUNTIME_VERSION");
sentryEvent.SetTag("os_name", osName);
sentryEvent.SetTag("os_version", $"{osName} {osVersion}");
sentryEvent.SetTag("is_docker", isDocker);
sentryEvent.SetTag("runtime_version", $"{PlatformInfo.PlatformName} {runTimeVersion}");
sentryEvent.SetTag("sqlite_version", $"{DatabaseVersion}");
sentryEvent.SetTag("database_migration", $"{DatabaseMigration}");
SentrySdk.CaptureEvent(sentryEvent); SentrySdk.CaptureEvent(sentryEvent);
} }
catch (Exception e) catch (Exception e)

View File

@ -328,6 +328,8 @@ namespace NzbDrone.Common.Processes
var monoProcesses = Process.GetProcessesByName("mono") var monoProcesses = Process.GetProcessesByName("mono")
.Union(Process.GetProcessesByName("mono-sgen")) .Union(Process.GetProcessesByName("mono-sgen"))
.Union(Process.GetProcessesByName("mono-sgen32"))
.Union(Process.GetProcessesByName("mono-sgen64"))
.Where(process => .Where(process =>
process.Modules.Cast<ProcessModule>() process.Modules.Cast<ProcessModule>()
.Any(module => .Any(module =>

View File

@ -11,9 +11,6 @@
<PackageReference Include="SharpZipLib" Version="1.2.0" /> <PackageReference Include="SharpZipLib" Version="1.2.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ExternalModules\CurlSharp\CurlSharp\CurlSharp.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System.Configuration.Install" /> <Reference Include="System.Configuration.Install" />
<Reference Include="System.ServiceProcess" /> <Reference Include="System.ServiceProcess" />

View File

@ -5,6 +5,7 @@ using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions; using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using Radarr.Host; using Radarr.Host;
using Radarr.Host.AccessControl;
namespace NzbDrone.Console namespace NzbDrone.Console
{ {
@ -50,6 +51,13 @@ namespace NzbDrone.Console
Logger.Fatal(ex.Message + ". This can happen if another instance of Radarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions"); Logger.Fatal(ex.Message + ". This can happen if another instance of Radarr is already running another application is using the same port (default: 7878) or the user has insufficient permissions");
Exit(ExitCodes.RecoverableFailure); Exit(ExitCodes.RecoverableFailure);
} }
catch (RemoteAccessException ex)
{
System.Console.WriteLine("");
System.Console.WriteLine("");
Logger.Fatal(ex, "EPIC FAIL!");
Exit(ExitCodes.Normal);
}
catch (Exception ex) catch (Exception ex)
{ {
System.Console.WriteLine(""); System.Console.WriteLine("");

View File

@ -30,10 +30,9 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>())); Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>())); Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<ManagedHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger)); Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger));
Mocker.SetConstant<CurlHttpDispatcher>(new CurlHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger)); Mocker.SetConstant<IHttpProvider>(new HttpProvider(TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<FallbackHttpDispatcher>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger)); Mocker.SetConstant<IHttpClient>(new HttpClient(new IHttpRequestInterceptor[0], Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), Mocker.Resolve<UserAgentBuilder>(), TestLogger));
Mocker.SetConstant<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder()); Mocker.SetConstant<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder());
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SQLite;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using FluentMigrator.Runner; using FluentMigrator.Runner;
@ -99,7 +100,6 @@ namespace NzbDrone.Core.Test.Framework
{ {
WithTempAsAppPath(); WithTempAsAppPath();
Mocker.SetConstant<IAnnouncer>(Mocker.Resolve<MigrationLogger>());
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>()); Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>()); Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
@ -116,22 +116,15 @@ namespace NzbDrone.Core.Test.Framework
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {
if (TestFolderInfo != null && Directory.Exists(TestFolderInfo.AppDataFolder)) // Make sure there are no lingering connections. (When this happens it means we haven't disposed something properly)
GC.Collect();
GC.WaitForPendingFinalizers();
SQLiteConnection.ClearAllPools();
if (TestFolderInfo != null)
{ {
var files = Directory.GetFiles(TestFolderInfo.AppDataFolder); DeleteTempFolder(TestFolderInfo.AppDataFolder);
foreach (var file in files)
{
try
{
File.Delete(file);
}
catch (Exception)
{
}
}
} }
} }
} }
} }

View File

@ -0,0 +1,57 @@
using System;
using NUnit.Framework;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class DotnetVersionCheckFixture : CoreTest<DotnetVersionCheck>
{
private void GivenOutput(string version)
{
WindowsOnly();
Mocker.GetMock<IPlatformInfo>()
.SetupGet(s => s.Version)
.Returns(new Version(version));
}
[TestCase("4.7.2")]
[TestCase("4.8")]
public void should_return_ok(string version)
{
GivenOutput(version);
Subject.Check().ShouldBeOk();
}
[TestCase("4.6.2")]
[TestCase("4.7")]
[TestCase("4.7.1")]
public void should_return_notice(string version)
{
GivenOutput(version);
Subject.Check().ShouldBeNotice();
}
public void should_return_warning(string version)
{
GivenOutput(version);
Subject.Check().ShouldBeWarning();
}
[TestCase("4.5")]
[TestCase("4.5.2")]
[TestCase("4.6.1")]
public void should_return_error(string version)
{
GivenOutput(version);
Subject.Check().ShouldBeError();
}
}
}

View File

@ -11,6 +11,16 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
result.Type.Should().Be(HealthCheckResult.Ok); result.Type.Should().Be(HealthCheckResult.Ok);
} }
public static void ShouldBeNotice(this Core.HealthCheck.HealthCheck result, string message = null)
{
result.Type.Should().Be(HealthCheckResult.Notice);
if (message.IsNotNullOrWhiteSpace())
{
result.Message.Should().Contain(message);
}
}
public static void ShouldBeWarning(this Core.HealthCheck.HealthCheck result, string message = null) public static void ShouldBeWarning(this Core.HealthCheck.HealthCheck result, string message = null)
{ {
result.Type.Should().Be(HealthCheckResult.Warning); result.Type.Should().Be(HealthCheckResult.Warning);

View File

@ -18,14 +18,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Returns(new Version(version)); .Returns(new Version(version));
} }
[TestCase("4.6")] [TestCase("5.18")]
[TestCase("4.4.2")] [TestCase("5.20")]
[TestCase("4.6")]
[TestCase("4.8")]
[TestCase("5.0")]
[TestCase("5.2")]
[TestCase("5.4")]
public void should_return_ok(string version) public void should_return_ok(string version)
{ {
GivenOutput(version); GivenOutput(version);
@ -33,6 +28,23 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Subject.Check().ShouldBeOk(); Subject.Check().ShouldBeOk();
} }
[TestCase("5.16")]
public void should_return_notice(string version)
{
GivenOutput(version);
Subject.Check().ShouldBeNotice();
}
[TestCase("5.4")]
[TestCase("5.8")]
public void should_return_warning(string version)
{
GivenOutput(version);
Subject.Check().ShouldBeWarning();
}
[TestCase("2.10.2")] [TestCase("2.10.2")]
[TestCase("2.10.8.1")] [TestCase("2.10.8.1")]
[TestCase("3.0.0.1")] [TestCase("3.0.0.1")]
@ -44,14 +56,6 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[TestCase("3.10")] [TestCase("3.10")]
[TestCase("4.0.0.0")] [TestCase("4.0.0.0")]
[TestCase("4.2")] [TestCase("4.2")]
public void should_return_warning(string version)
{
GivenOutput(version);
Subject.Check().ShouldBeWarning();
}
[TestCase("4.4.0")] [TestCase("4.4.0")]
[TestCase("4.4.1")] [TestCase("4.4.1")]
public void should_return_error(string version) public void should_return_error(string version)

View File

@ -1,121 +1,224 @@
//using System; using System;
//using System.Collections.Generic; using System.Collections.Concurrent;
//using Moq; using System.Collections.Generic;
//using NUnit.Framework; using System.Threading;
//using NzbDrone.Common; using Moq;
//using NzbDrone.Core.Messaging.Commands; using NUnit.Framework;
//using NzbDrone.Core.Messaging.Commands.Tracking; using NzbDrone.Common;
//using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Lifecycle;
//using NzbDrone.Test.Common; using NzbDrone.Core.Messaging.Commands;
// using NzbDrone.Core.Messaging.Events;
//namespace NzbDrone.Core.Test.Messaging.Commands using NzbDrone.Test.Common;
//{
// [TestFixture] namespace NzbDrone.Core.Test.Messaging.Commands
// public class CommandExecutorFixture : TestBase<CommandExecutor> {
// { [TestFixture]
// private Mock<IExecute<CommandA>> _executorA; public class CommandExecutorFixture : TestBase<CommandExecutor>
// private Mock<IExecute<CommandB>> _executorB; {
// private CommandQueue _commandQueue;
// [SetUp] private Mock<IExecute<CommandA>> _executorA;
// public void Setup() private Mock<IExecute<CommandB>> _executorB;
// {
// _executorA = new Mock<IExecute<CommandA>>(); [SetUp]
// _executorB = new Mock<IExecute<CommandB>>(); public void Setup()
// {
// Mocker.GetMock<IServiceFactory>() _executorA = new Mock<IExecute<CommandA>>();
// .Setup(c => c.Build(typeof(IExecute<CommandA>))) _executorB = new Mock<IExecute<CommandB>>();
// .Returns(_executorA.Object);
// Mocker.GetMock<IServiceFactory>()
// Mocker.GetMock<IServiceFactory>() .Setup(c => c.Build(typeof(IExecute<CommandA>)))
// .Setup(c => c.Build(typeof(IExecute<CommandB>))) .Returns(_executorA.Object);
// .Returns(_executorB.Object);
// Mocker.GetMock<IServiceFactory>()
// .Setup(c => c.Build(typeof(IExecute<CommandB>)))
// Mocker.GetMock<ITrackCommands>() .Returns(_executorB.Object);
// .Setup(c => c.FindExisting(It.IsAny<Command>())) }
// .Returns<Command>(null);
// } [TearDown]
// public void TearDown()
// [Test] {
// public void should_publish_command_to_executor() Subject.Handle(new ApplicationShutdownRequested());
// {
// var commandA = new CommandA(); // Give the threads a bit of time to shut down.
// Thread.Sleep(10);
// Subject.Push(commandA); }
//
// _executorA.Verify(c => c.Execute(commandA), Times.Once()); private void GivenCommandQueue()
// } {
// _commandQueue = new CommandQueue();
// [Test]
// public void should_publish_command_by_with_optional_arg_using_name() Mocker.GetMock<IManageCommandQueue>()
// { .Setup(s => s.Queue(It.IsAny<CancellationToken>()))
// Mocker.GetMock<IServiceFactory>().Setup(c => c.GetImplementations(typeof(Command))) .Returns(_commandQueue.GetConsumingEnumerable);
// .Returns(new List<Type> { typeof(CommandA), typeof(CommandB) }); }
//
// Subject.Push(typeof(CommandA).FullName); private void QueueAndWaitForExecution(CommandModel commandModel, bool waitPublish = false)
// _executorA.Verify(c => c.Execute(It.IsAny<CommandA>()), Times.Once()); {
// } var waitEventComplete = new ManualResetEventSlim();
// var waitEventPublish = new ManualResetEventSlim();
//
// [Test] Mocker.GetMock<IManageCommandQueue>()
// public void should_not_publish_to_incompatible_executor() .Setup(s => s.Complete(It.Is<CommandModel>(c => c == commandModel), It.IsAny<string>()))
// { .Callback(() => waitEventComplete.Set());
// var commandA = new CommandA();
// Mocker.GetMock<IManageCommandQueue>()
// Subject.Push(commandA); .Setup(s => s.Fail(It.Is<CommandModel>(c => c == commandModel), It.IsAny<string>(), It.IsAny<Exception>()))
// .Callback(() => waitEventComplete.Set());
// _executorA.Verify(c => c.Execute(commandA), Times.Once());
// _executorB.Verify(c => c.Execute(It.IsAny<CommandB>()), Times.Never()); Mocker.GetMock<IEventAggregator>()
// } .Setup(s => s.PublishEvent<CommandExecutedEvent>(It.IsAny<CommandExecutedEvent>()))
// .Callback(() => waitEventPublish.Set());
// [Test]
// public void broken_executor_should_throw_the_exception() _commandQueue.Add(commandModel);
// {
// var commandA = new CommandA(); if (!waitEventComplete.Wait(2000))
// {
// _executorA.Setup(c => c.Execute(It.IsAny<CommandA>())) Assert.Fail("Command did not Complete/Fail within 2 sec");
// .Throws(new NotImplementedException()); }
//
// Assert.Throws<NotImplementedException>(() => Subject.Push(commandA)); if (waitPublish && !waitEventPublish.Wait(500))
// } {
// Assert.Fail("Command did not Publish within 500 msec");
// }
// [Test] }
// public void broken_executor_should_publish_executed_event()
// { [Test]
// var commandA = new CommandA(); public void should_start_executor_threads()
// {
// _executorA.Setup(c => c.Execute(It.IsAny<CommandA>())) Subject.Handle(new ApplicationStartedEvent());
// .Throws(new NotImplementedException());
// Mocker.GetMock<IManageCommandQueue>()
// Assert.Throws<NotImplementedException>(() => Subject.Push(commandA)); .Verify(v => v.Queue(It.IsAny<CancellationToken>()), Times.AtLeastOnce());
// }
// VerifyEventPublished<CommandExecutedEvent>();
// } [Test]
// public void should_execute_on_executor()
// [Test] {
// public void should_publish_executed_event_on_success() GivenCommandQueue();
// { var commandA = new CommandA();
// var commandA = new CommandA(); var commandModel = new CommandModel
// Subject.Push(commandA); {
// Body = commandA
// VerifyEventPublished<CommandExecutedEvent>(); };
// }
// } Subject.Handle(new ApplicationStartedEvent());
//
// public class CommandA : Command QueueAndWaitForExecution(commandModel);
// {
// public CommandA(int id = 0) _executorA.Verify(c => c.Execute(commandA), Times.Once());
// { }
// }
// } [Test]
// public void should_not_execute_on_incompatible_executor()
// public class CommandB : Command {
// { GivenCommandQueue();
// var commandA = new CommandA();
// public CommandB() var commandModel = new CommandModel
// { {
// } Body = commandA
// } };
//
//} Subject.Handle(new ApplicationStartedEvent());
QueueAndWaitForExecution(commandModel);
_executorA.Verify(c => c.Execute(commandA), Times.Once());
_executorB.Verify(c => c.Execute(It.IsAny<CommandB>()), Times.Never());
}
[Test]
public void broken_executor_should_publish_executed_event()
{
GivenCommandQueue();
var commandA = new CommandA();
var commandModel = new CommandModel
{
Body = commandA
};
_executorA.Setup(s => s.Execute(It.IsAny<CommandA>()))
.Throws(new NotImplementedException());
Subject.Handle(new ApplicationStartedEvent());
QueueAndWaitForExecution(commandModel);
VerifyEventPublished<CommandExecutedEvent>();
ExceptionVerification.WaitForErrors(1, 500);
}
[Test]
public void should_publish_executed_event_on_success()
{
GivenCommandQueue();
var commandA = new CommandA();
var commandModel = new CommandModel
{
Body = commandA
};
Subject.Handle(new ApplicationStartedEvent());
QueueAndWaitForExecution(commandModel);
VerifyEventPublished<CommandExecutedEvent>();
}
[Test]
public void should_use_completion_message()
{
GivenCommandQueue();
var commandA = new CommandA();
var commandModel = new CommandModel
{
Body = commandA
};
Subject.Handle(new ApplicationStartedEvent());
QueueAndWaitForExecution(commandModel);
Mocker.GetMock<IManageCommandQueue>()
.Verify(s => s.Complete(It.Is<CommandModel>(c => c == commandModel), commandA.CompletionMessage), Times.Once());
}
[Test]
public void should_use_last_progress_message_if_completion_message_is_null()
{
GivenCommandQueue();
var commandB = new CommandB();
var commandModel = new CommandModel
{
Body = commandB,
Message = "Do work"
};
Subject.Handle(new ApplicationStartedEvent());
QueueAndWaitForExecution(commandModel);
Mocker.GetMock<IManageCommandQueue>()
.Verify(s => s.Complete(It.Is<CommandModel>(c => c == commandModel), commandModel.Message), Times.Once());
}
}
public class CommandA : Command
{
public CommandA(int id = 0)
{
}
}
public class CommandB : Command
{
public CommandB()
{
}
public override string CompletionMessage => null;
}
}

View File

@ -18,7 +18,8 @@ namespace NzbDrone.Core.Test.MetadataSource.SkyHook
} }
[TestCase("Prometheus", "Prometheus")] [TestCase("Prometheus", "Prometheus")]
[TestCase("The Man from U.N.C.L.E.", "The Man from U.N.C.L.E.")] // TODO: TMDB Doesn't like when we clean periods from this
// [TestCase("The Man from U.N.C.L.E.", "The Man from U.N.C.L.E.")]
[TestCase("imdb:tt2527336", "Star Wars: The Last Jedi")] [TestCase("imdb:tt2527336", "Star Wars: The Last Jedi")]
[TestCase("imdb:tt2798920", "Annihilation")] [TestCase("imdb:tt2798920", "Annihilation")]
public void successful_search(string title, string expected) public void successful_search(string title, string expected)

View File

@ -42,9 +42,9 @@ namespace NzbDrone.Core.Test.MovieTests
_movie.LastInfoSync = DateTime.UtcNow.AddDays(-1); _movie.LastInfoSync = DateTime.UtcNow.AddDays(-1);
} }
private void GivenMovieLastRefreshedHalfADayAgo() private void GivenMovieLastRefreshedADayAgo()
{ {
_movie.LastInfoSync = DateTime.UtcNow.AddHours(-12); _movie.LastInfoSync = DateTime.UtcNow.AddHours(-24);
} }
private void GivenMovieLastRefreshedRecently() private void GivenMovieLastRefreshedRecently()
@ -58,15 +58,15 @@ namespace NzbDrone.Core.Test.MovieTests
} }
[Test] [Test]
public void should_return_true_if_in_cinemas_movie_last_refreshed_more_than_6_hours_ago() public void should_return_true_if_in_cinemas_movie_last_refreshed_more_than_12_hours_ago()
{ {
GivenMovieLastRefreshedHalfADayAgo(); GivenMovieLastRefreshedADayAgo();
Subject.ShouldRefresh(_movie).Should().BeTrue(); Subject.ShouldRefresh(_movie).Should().BeTrue();
} }
[Test] [Test]
public void should_return_false_if_in_cinemas_movie_last_refreshed_less_than_6_hours_ago() public void should_return_false_if_in_cinemas_movie_last_refreshed_less_than_12_hours_ago()
{ {
GivenMovieLastRefreshedRecently(); GivenMovieLastRefreshedRecently();

View File

@ -37,6 +37,7 @@ namespace NzbDrone.Core.Test.ProviderTests.RecycleBinProviderTests
public void Setup() public void Setup()
{ {
Mocker.GetMock<IConfigService>().SetupGet(s => s.RecycleBin).Returns(RecycleBin); Mocker.GetMock<IConfigService>().SetupGet(s => s.RecycleBin).Returns(RecycleBin);
Mocker.GetMock<IConfigService>().SetupGet(s => s.RecycleBinCleanupDays).Returns(7);
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetDirectories(RecycleBin)) Mocker.GetMock<IDiskProvider>().Setup(s => s.GetDirectories(RecycleBin))
.Returns(new [] { @"C:\Test\RecycleBin\Folder1", @"C:\Test\RecycleBin\Folder2", @"C:\Test\RecycleBin\Folder3" }); .Returns(new [] { @"C:\Test\RecycleBin\Folder1", @"C:\Test\RecycleBin\Folder2", @"C:\Test\RecycleBin\Folder3" });

View File

@ -15,6 +15,9 @@
<Link>Files\1024.png</Link> <Link>Files\1024.png</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<Reference Include="System.Data.SQLite">
<HintPath>..\Libraries\Sqlite\System.Data.SQLite.dll</HintPath>
</Reference>
<None Update="Files\**\*.*"> <None Update="Files\**\*.*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>

View File

@ -87,6 +87,16 @@ namespace NzbDrone.Core.Test.UpdateTests
.Returns(true); .Returns(true);
} }
[Test]
public void should_not_update_if_inside_docker()
{
Mocker.GetMock<IOsInfo>().Setup(x => x.IsDocker).Returns(true);
Subject.Invoking(x => x.Execute(new ApplicationUpdateCommand()))
.Should().Throw<CommandFailedException>()
.WithMessage("Updating is disabled inside a docker container. Please update the container image.");
}
[Test] [Test]
public void should_delete_sandbox_before_update_if_folder_exists() public void should_delete_sandbox_before_update_if_folder_exists()
{ {

View File

@ -1,22 +1,40 @@
using NzbDrone.Common.EnvironmentInfo; using System;
using System.Linq;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.History;
namespace NzbDrone.Core.Analytics namespace NzbDrone.Core.Analytics
{ {
public interface IAnalyticsService public interface IAnalyticsService
{ {
bool IsEnabled { get; } bool IsEnabled { get; }
bool InstallIsActive { get; }
} }
public class AnalyticsService : IAnalyticsService public class AnalyticsService : IAnalyticsService
{ {
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly IHistoryService _historyService;
public AnalyticsService(IConfigFileProvider configFileProvider) public AnalyticsService(IHistoryService historyService, IConfigFileProvider configFileProvider)
{ {
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_historyService = historyService;
} }
public bool IsEnabled => _configFileProvider.AnalyticsEnabled; public bool IsEnabled => _configFileProvider.AnalyticsEnabled && RuntimeInfo.IsProduction || RuntimeInfo.IsDevelopment;
public bool InstallIsActive
{
get
{
var lastRecord = _historyService.Paged(new PagingSpec<History.History>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
var monthAgo = DateTime.UtcNow.AddMonths(-1);
return lastRecord.Records.Any(v => v.Date > monthAgo);
}
}
} }
} }

View File

@ -34,6 +34,7 @@ namespace NzbDrone.Core.Configuration
bool AnalyticsEnabled { get; } bool AnalyticsEnabled { get; }
string LogLevel { get; } string LogLevel { get; }
string ConsoleLogLevel { get; } string ConsoleLogLevel { get; }
bool FilterSentryEvents { get; }
string Branch { get; } string Branch { get; }
string ApiKey { get; } string ApiKey { get; }
string SslCertHash { get; } string SslCertHash { get; }
@ -182,7 +183,7 @@ namespace NzbDrone.Core.Configuration
public string LogLevel => GetValue("LogLevel", "info"); public string LogLevel => GetValue("LogLevel", "info");
public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false);
public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false);
public string SslCertHash => GetValue("SslCertHash", ""); public string SslCertHash => GetValue("SslCertHash", "");
public string UrlBase public string UrlBase
@ -364,11 +365,6 @@ namespace NzbDrone.Core.Configuration
{ {
EnsureDefaultConfigFile(); EnsureDefaultConfigFile();
DeleteOldValues(); DeleteOldValues();
if (!AnalyticsEnabled)
{
NzbDroneLogger.UnRegisterRemoteLoggers();
}
} }
public void Execute(ResetApiKeyCommand message) public void Execute(ResetApiKeyCommand message)

View File

@ -3,7 +3,7 @@ using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Datastore namespace NzbDrone.Core.Datastore
{ {
public class CorruptDatabaseException : NzbDroneException public class CorruptDatabaseException : RadarrStartupException
{ {
public CorruptDatabaseException(string message, params object[] args) : base(message, args) public CorruptDatabaseException(string message, params object[] args) : base(message, args)
{ {

View File

@ -9,6 +9,7 @@ namespace NzbDrone.Core.Datastore
{ {
IDataMapper GetDataMapper(); IDataMapper GetDataMapper();
Version Version { get; } Version Version { get; }
int Migration { get; }
void Vacuum(); void Vacuum();
} }
@ -42,6 +43,16 @@ namespace NzbDrone.Core.Datastore
} }
} }
public int Migration
{
get
{
var migration = _datamapperFactory()
.ExecuteScalar("SELECT version from VersionInfo ORDER BY version DESC LIMIT 1").ToString();
return Convert.ToInt32(migration);
}
}
public void Vacuum() public void Vacuum()
{ {
try try

View File

@ -6,6 +6,7 @@ using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Exceptions;
using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Datastore.Migration.Framework;
@ -124,7 +125,11 @@ namespace NzbDrone.Core.Datastore
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-use-sonarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName); throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-use-sonarr-on-a-mac-and-it-suddenly-stopped-working-what-happened", e, fileName);
} }
throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Sonarr/Sonarr/wiki/FAQ#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName); throw new CorruptDatabaseException("Database file: {0} is corrupt, restore from backup if available. See: https://github.com/Lidarr/Lidarr/wiki/FAQ#i-am-getting-an-error-database-disk-image-is-malformed", e, fileName);
}
catch (Exception e)
{
throw new RadarrStartupException(e, "Error creating main database");
} }
} }
@ -154,6 +159,10 @@ namespace NzbDrone.Core.Datastore
_migrationController.Migrate(connectionString, migrationContext); _migrationController.Migrate(connectionString, migrationContext);
} }
catch (Exception e)
{
throw new RadarrStartupException(e, "Error creating log database");
}
} }
} }
} }

View File

@ -24,6 +24,8 @@ namespace NzbDrone.Core.Datastore
public Version Version => _database.Version; public Version Version => _database.Version;
public int Migration => _database.Migration;
public void Vacuum() public void Vacuum()
{ {
_database.Vacuum(); _database.Vacuum();

View File

@ -24,6 +24,8 @@ namespace NzbDrone.Core.Datastore
public Version Version => _database.Version; public Version Version => _database.Version;
public int Migration => _database.Migration;
public void Vacuum() public void Vacuum()
{ {
_database.Vacuum(); _database.Vacuum();

View File

@ -57,7 +57,8 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
SQLiteConnection.ClearAllPools(); SQLiteConnection.ClearAllPools();
throw; throw;
} }
processor.Dispose();
sw.Stop(); sw.Stop();

View File

@ -237,7 +237,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
} }
catch (WebException ex) catch (WebException ex)
{ {
_logger.Error(ex, "Unble to test connection"); _logger.Error(ex, "Unable to test connection");
switch (ex.Status) switch (ex.Status)
{ {
case WebExceptionStatus.ConnectFailure: case WebExceptionStatus.ConnectFailure:

View File

@ -0,0 +1,52 @@
using System;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class DotnetVersionCheck : HealthCheckBase
{
private readonly IPlatformInfo _platformInfo;
private readonly Logger _logger;
public DotnetVersionCheck(IPlatformInfo platformInfo, Logger logger)
{
_platformInfo = platformInfo;
_logger = logger;
}
public override HealthCheck Check()
{
if (!PlatformInfo.IsDotNet)
{
return new HealthCheck(GetType());
}
var dotnetVersion = _platformInfo.Version;
// Target .Net version, which would allow us to increase our target framework
var targetVersion = new Version("4.7.2");
if (dotnetVersion >= targetVersion)
{
_logger.Debug("Dotnet version is {0} or better: {1}", targetVersion, dotnetVersion);
return new HealthCheck(GetType());
}
// Supported .net version but below our desired target
var stableVersion = new Version("4.6.2");
if (dotnetVersion >= stableVersion)
{
_logger.Debug("Dotnet version is {0} or better: {1}", stableVersion, dotnetVersion);
return new HealthCheck(GetType(), HealthCheckResult.Notice,
$"Currently installed .Net Framework {dotnetVersion} is supported but we recommend upgrading to at least {targetVersion}.",
"#currently-installed-net-framework-is-supported-but-upgrading-is-recommended");
}
return new HealthCheck(GetType(), HealthCheckResult.Error,
$"Currently installed .Net Framework {dotnetVersion} is old and unsupported. Please upgrade the .Net Framework to at least {targetVersion}.",
"#currently-installed-net-framework-is-old-and-unsupported");
}
public override bool CheckOnSchedule => false;
}
}

View File

@ -2,7 +2,9 @@
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using NLog; using NLog;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions;
namespace NzbDrone.Core.HealthCheck.Checks namespace NzbDrone.Core.HealthCheck.Checks
{ {
@ -26,11 +28,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
var monoVersion = _platformInfo.Version; var monoVersion = _platformInfo.Version;
if (monoVersion >= new Version("5.0.0") && Environment.GetEnvironmentVariable("MONO_TLS_PROVIDER") == "legacy") if (monoVersion >= new Version("5.8.0") && Environment.GetEnvironmentVariable("MONO_TLS_PROVIDER") == "legacy")
{ {
// Mono 5.0 still has issues in combination with libmediainfo, so disabling this check for now. _logger.Debug()
//_logger.Debug("Mono version 5.0.0 or higher and legacy TLS provider is selected, recommending user to switch to btls."); .Message("Mono version {0} and legacy TLS provider is selected, recommending user to switch to btls.", monoVersion)
//return new HealthCheck(GetType(), HealthCheckResult.Warning, "Radarr now supports Mono 5.x with btls enabled, consider removing MONO_TLS_PROVIDER=legacy option"); .WriteSentryDebug("LegacyTlsProvider", monoVersion.ToString())
.Write();
return new HealthCheck(GetType(), HealthCheckResult.Warning, "Sonarr Mono 4.x tls workaround still enabled, consider removing MONO_TLS_PROVIDER=legacy environment option");
} }
return new HealthCheck(GetType()); return new HealthCheck(GetType());

View File

@ -24,19 +24,47 @@ namespace NzbDrone.Core.HealthCheck.Checks
var monoVersion = _platformInfo.Version; var monoVersion = _platformInfo.Version;
// Known buggy Mono versions
if (monoVersion == new Version("4.4.0") || monoVersion == new Version("4.4.1")) if (monoVersion == new Version("4.4.0") || monoVersion == new Version("4.4.1"))
{ {
_logger.Debug("Mono version {0}", monoVersion); _logger.Debug("Mono version {0}", monoVersion);
return new HealthCheck(GetType(), HealthCheckResult.Error, $"Your Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version"); return new HealthCheck(GetType(), HealthCheckResult.Error,
$"Currently installed Mono version {monoVersion} has a bug that causes issues connecting to indexers/download clients. You should upgrade to a higher version",
"#currently-installed-mono-version-is-old-and-unsupported");
} }
if (monoVersion >= new Version("4.4.2")) // Currently best stable Mono version (5.18 gets us .net 4.7.2 support)
var bestVersion = new Version("5.20");
var targetVersion = new Version("5.18");
if (monoVersion >= targetVersion)
{ {
_logger.Debug("Mono version is 4.4.2 or better: {0}", monoVersion); _logger.Debug("Mono version is {0} or better: {1}", targetVersion, monoVersion);
return new HealthCheck(GetType()); return new HealthCheck(GetType());
} }
return new HealthCheck(GetType(), HealthCheckResult.Warning, "You are running an old and unsupported version of Mono. Please upgrade Mono for improved stability."); // Stable Mono versions
var stableVersion = new Version("5.16");
if (monoVersion >= stableVersion)
{
_logger.Debug("Mono version is {0} or better: {1}", stableVersion, monoVersion);
return new HealthCheck(GetType(), HealthCheckResult.Notice,
$"Currently installed Mono version {monoVersion} is supported but upgrading to {bestVersion} is recommended.",
"#currently-installed-mono-version-is-supported-but-upgrading-is-recommended");
}
// Old but supported Mono versions, there are known bugs
var supportedVersion = new Version("5.4");
if (monoVersion >= supportedVersion)
{
_logger.Debug("Mono version is {0} or better: {1}", supportedVersion, monoVersion);
return new HealthCheck(GetType(), HealthCheckResult.Warning,
$"Currently installed Mono version {monoVersion} is supported but has some known issues. Please upgrade Mono to version {bestVersion}.",
"#currently-installed-mono-version-is-supported-but-upgrading-is-recommended");
}
return new HealthCheck(GetType(), HealthCheckResult.Error,
$"Currently installed Mono version {monoVersion} is old and unsupported. Please upgrade Mono to version {bestVersion}.",
"#currently-installed-mono-version-is-old-and-unsupported");
} }
public override bool CheckOnSchedule => false; public override bool CheckOnSchedule => false;

View File

@ -46,7 +46,8 @@ namespace NzbDrone.Core.HealthCheck
public enum HealthCheckResult public enum HealthCheckResult
{ {
Ok = 0, Ok = 0,
Warning = 1, Notice = 1,
Error = 2 Warning = 2,
Error = 3
} }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Common.EnvironmentInfo;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
@ -107,7 +108,7 @@ namespace NzbDrone.Core.Indexers.Rarbg
requestBuilder.AddQueryParam("limit", "100"); requestBuilder.AddQueryParam("limit", "100");
requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings)); requestBuilder.AddQueryParam("token", _tokenProvider.GetToken(Settings));
requestBuilder.AddQueryParam("format", "json_extended"); requestBuilder.AddQueryParam("format", "json_extended");
requestBuilder.AddQueryParam("app_id", "Radarr"); requestBuilder.AddQueryParam("app_id", BuildInfo.AppName);
yield return new IndexerRequest(requestBuilder.Build()); yield return new IndexerRequest(requestBuilder.Build());
} }

View File

@ -2,7 +2,9 @@
using System.Linq; using System.Linq;
using NLog; using NLog;
using NLog.Config; using NLog.Config;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation.Sentry;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Configuration.Events;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@ -40,6 +42,9 @@ namespace NzbDrone.Core.Instrumentation
SetMinimumLogLevel(rules, "appFileDebug", minimumLogLevel <= LogLevel.Debug ? LogLevel.Debug : LogLevel.Off); SetMinimumLogLevel(rules, "appFileDebug", minimumLogLevel <= LogLevel.Debug ? LogLevel.Debug : LogLevel.Off);
SetMinimumLogLevel(rules, "appFileTrace", minimumLogLevel <= LogLevel.Trace ? LogLevel.Trace : LogLevel.Off); SetMinimumLogLevel(rules, "appFileTrace", minimumLogLevel <= LogLevel.Trace ? LogLevel.Trace : LogLevel.Off);
//Sentry
ReconfigureSentry();
LogManager.ReconfigExistingLoggers(); LogManager.ReconfigExistingLoggers();
} }
@ -67,6 +72,16 @@ namespace NzbDrone.Core.Instrumentation
} }
} }
private void ReconfigureSentry()
{
var sentryTarget = LogManager.Configuration.AllTargets.OfType<SentryTarget>().FirstOrDefault();
if (sentryTarget != null)
{
sentryTarget.SentryEnabled = RuntimeInfo.IsProduction && _configFileProvider.AnalyticsEnabled || RuntimeInfo.IsDevelopment;
sentryTarget.FilterEvents = _configFileProvider.FilterSentryEvents;
}
}
private List<LogLevel> GetLogLevels() private List<LogLevel> GetLogLevels()
{ {
return new List<LogLevel> return new List<LogLevel>

View File

@ -0,0 +1,42 @@
using System.Linq;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Sentry;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Lifecycle;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Instrumentation
{
public class ReconfigureSentry : IHandleAsync<ApplicationStartedEvent>
{
private readonly IConfigFileProvider _configFileProvider;
private readonly IPlatformInfo _platformInfo;
private readonly IMainDatabase _database;
public ReconfigureSentry(IConfigFileProvider configFileProvider,
IPlatformInfo platformInfo,
IMainDatabase database)
{
_configFileProvider = configFileProvider;
_platformInfo = platformInfo;
_database = database;
}
public void Reconfigure()
{
// Extended sentry config
var sentryTarget = LogManager.Configuration.AllTargets.OfType<SentryTarget>().FirstOrDefault();
if (sentryTarget != null)
{
sentryTarget.UpdateScope(_database.Version, _database.Migration, _configFileProvider.Branch, _platformInfo);
}
}
public void HandleAsync(ApplicationStartedEvent message)
{
Reconfigure();
}
}
}

View File

@ -4,6 +4,7 @@ using FluentValidation.Results;
using NLog; using NLog;
using RestSharp; using RestSharp;
using NzbDrone.Core.Rest; using NzbDrone.Core.Rest;
using NzbDrone.Common.EnvironmentInfo;
namespace NzbDrone.Core.Notifications.Boxcar namespace NzbDrone.Core.Notifications.Boxcar
{ {
@ -75,7 +76,7 @@ namespace NzbDrone.Core.Notifications.Boxcar
request.AddParameter("user_credentials", settings.Token); request.AddParameter("user_credentials", settings.Token);
request.AddParameter("notification[title]", title); request.AddParameter("notification[title]", title);
request.AddParameter("notification[long_message]", message); request.AddParameter("notification[long_message]", message);
request.AddParameter("notification[source_name]", "Radarr"); request.AddParameter("notification[source_name]", BuildInfo.AppName);
request.AddParameter("notification[icon_url]", "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/64.png"); request.AddParameter("notification[icon_url]", "https://raw.githubusercontent.com/Radarr/Radarr/develop/Logo/64.png");
client.ExecuteAndValidate(request); client.ExecuteAndValidate(request);

View File

@ -43,10 +43,10 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
var requestBuilder = new HttpRequestBuilder("https://plex.tv") var requestBuilder = new HttpRequestBuilder("https://plex.tv")
.Accept(HttpAccept.Json) .Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", clientIdentifier) .AddQueryParam("X-Plex-Client-Identifier", clientIdentifier)
.AddQueryParam("X-Plex-Product", "Radarr") .AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows") .AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7") .AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", "Radarr") .AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()); .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString());
return requestBuilder; return requestBuilder;

View File

@ -31,10 +31,10 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
var requestBuilder = new HttpRequestBuilder("https://plex.tv/api/v2/pins") var requestBuilder = new HttpRequestBuilder("https://plex.tv/api/v2/pins")
.Accept(HttpAccept.Json) .Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", clientIdentifier) .AddQueryParam("X-Plex-Client-Identifier", clientIdentifier)
.AddQueryParam("X-Plex-Product", "Radarr") .AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows") .AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7") .AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", "Radarr") .AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()) .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString())
.AddQueryParam("strong", true); .AddQueryParam("strong", true);
@ -57,7 +57,7 @@ namespace NzbDrone.Core.Notifications.Plex.PlexTv
.AddQueryParam("clientID", clientIdentifier) .AddQueryParam("clientID", clientIdentifier)
.AddQueryParam("forwardUrl", callbackUrl) .AddQueryParam("forwardUrl", callbackUrl)
.AddQueryParam("code", pinCode) .AddQueryParam("code", pinCode)
.AddQueryParam("context[device][product]", "Radarr") .AddQueryParam("context[device][product]", BuildInfo.AppName)
.AddQueryParam("context[device][platform]", "Windows") .AddQueryParam("context[device][platform]", "Windows")
.AddQueryParam("context[device][platformVersion]", "7") .AddQueryParam("context[device][platformVersion]", "7")
.AddQueryParam("context[device][version]", BuildInfo.Version.ToString()); .AddQueryParam("context[device][version]", BuildInfo.Version.ToString());

View File

@ -155,10 +155,10 @@ namespace NzbDrone.Core.Notifications.Plex.Server
var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host}:{settings.Port}") var requestBuilder = new HttpRequestBuilder($"{scheme}://{settings.Host}:{settings.Port}")
.Accept(HttpAccept.Json) .Accept(HttpAccept.Json)
.AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier) .AddQueryParam("X-Plex-Client-Identifier", _configService.PlexClientIdentifier)
.AddQueryParam("X-Plex-Product", "Radarr") .AddQueryParam("X-Plex-Product", BuildInfo.AppName)
.AddQueryParam("X-Plex-Platform", "Windows") .AddQueryParam("X-Plex-Platform", "Windows")
.AddQueryParam("X-Plex-Platform-Version", "7") .AddQueryParam("X-Plex-Platform-Version", "7")
.AddQueryParam("X-Plex-Device-Name", "Radarr") .AddQueryParam("X-Plex-Device-Name", BuildInfo.AppName)
.AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString()); .AddQueryParam("X-Plex-Version", BuildInfo.Version.ToString());
if (settings.AuthToken.IsNotNullOrWhiteSpace()) if (settings.AuthToken.IsNotNullOrWhiteSpace())

View File

@ -1,6 +1,7 @@
using System; using System;
using FluentValidation.Results; using FluentValidation.Results;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo;
using Prowlin; using Prowlin;
namespace NzbDrone.Core.Notifications.Prowl namespace NzbDrone.Core.Notifications.Prowl
@ -26,7 +27,7 @@ namespace NzbDrone.Core.Notifications.Prowl
{ {
var notification = new Prowlin.Notification var notification = new Prowlin.Notification
{ {
Application = "Radarr", Application = BuildInfo.AppName,
Description = message, Description = message,
Event = title, Event = title,
Priority = priority, Priority = priority,

View File

@ -9,7 +9,7 @@ namespace NzbDrone.Core.Rest
{ {
var restClient = new RestClient(baseUrl) var restClient = new RestClient(baseUrl)
{ {
UserAgent = $"Radarr/{BuildInfo.Version} ({OsInfo.Os})" UserAgent = $"{BuildInfo.AppName}/{BuildInfo.Version} ({OsInfo.Os})"
}; };

View File

@ -32,6 +32,7 @@ namespace NzbDrone.Core.Update
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly IBackupService _backupService; private readonly IBackupService _backupService;
private readonly IOsInfo _osInfo;
public InstallUpdateService(ICheckUpdateService checkUpdateService, public InstallUpdateService(ICheckUpdateService checkUpdateService,
@ -46,6 +47,7 @@ namespace NzbDrone.Core.Update
IConfigFileProvider configFileProvider, IConfigFileProvider configFileProvider,
IRuntimeInfo runtimeInfo, IRuntimeInfo runtimeInfo,
IBackupService backupService, IBackupService backupService,
IOsInfo osInfo,
Logger logger) Logger logger)
{ {
if (configFileProvider == null) if (configFileProvider == null)
@ -64,6 +66,7 @@ namespace NzbDrone.Core.Update
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_backupService = backupService; _backupService = backupService;
_osInfo = osInfo;
_logger = logger; _logger = logger;
} }
@ -209,6 +212,11 @@ namespace NzbDrone.Core.Update
return; return;
} }
if (_osInfo.IsDocker)
{
throw new CommandFailedException("Updating is disabled inside a docker container. Please update the container image.");
}
if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && message.Trigger != CommandTrigger.Manual) if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && message.Trigger != CommandTrigger.Manual)
{ {
_logger.ProgressDebug("Auto-update not enabled, not installing available update."); _logger.ProgressDebug("Auto-update not enabled, not installing available update.");

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using NzbDrone.Common.Cloud; using NzbDrone.Common.Cloud;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.Analytics;
namespace NzbDrone.Core.Update namespace NzbDrone.Core.Update
{ {
@ -15,14 +16,16 @@ namespace NzbDrone.Core.Update
public class UpdatePackageProvider : IUpdatePackageProvider public class UpdatePackageProvider : IUpdatePackageProvider
{ {
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IPlatformInfo _platformInfo;
private readonly IHttpRequestBuilderFactory _requestBuilder; private readonly IHttpRequestBuilderFactory _requestBuilder;
private readonly IPlatformInfo _platformInfo;
private readonly IAnalyticsService _analyticsService;
public UpdatePackageProvider(IHttpClient httpClient, IRadarrCloudRequestBuilder requestBuilder, IPlatformInfo platformInfo) public UpdatePackageProvider(IHttpClient httpClient, IRadarrCloudRequestBuilder requestBuilder, IAnalyticsService analyticsService, IPlatformInfo platformInfo)
{ {
_httpClient = httpClient;
_platformInfo = platformInfo; _platformInfo = platformInfo;
_analyticsService = analyticsService;
_requestBuilder = requestBuilder.Services; _requestBuilder = requestBuilder.Services;
_httpClient = httpClient;
} }
public UpdatePackage GetLatestUpdate(string branch, Version currentVersion) public UpdatePackage GetLatestUpdate(string branch, Version currentVersion)
@ -32,10 +35,15 @@ namespace NzbDrone.Core.Update
.AddQueryParam("version", currentVersion) .AddQueryParam("version", currentVersion)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()) .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("runtimeVer", _platformInfo.Version) .AddQueryParam("runtimeVer", _platformInfo.Version)
.SetSegment("branch", branch) .SetSegment("branch", branch);
.Build();
var update = _httpClient.Get<UpdatePackageAvailable>(request).Resource; if (_analyticsService.IsEnabled)
{
// Send if the system is active so we know which versions to deprecate/ignore
request.AddQueryParam("active", _analyticsService.InstallIsActive.ToString().ToLower());
}
var update = _httpClient.Get<UpdatePackageAvailable>(request.Build()).Resource;
if (!update.Available) return null; if (!update.Available) return null;
@ -49,12 +57,17 @@ namespace NzbDrone.Core.Update
.AddQueryParam("version", currentVersion) .AddQueryParam("version", currentVersion)
.AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()) .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant())
.AddQueryParam("runtimeVer", _platformInfo.Version) .AddQueryParam("runtimeVer", _platformInfo.Version)
.SetSegment("branch", branch) .SetSegment("branch", branch);
.Build();
var updates = _httpClient.Get<List<UpdatePackage>>(request); if (_analyticsService.IsEnabled)
{
// Send if the system is active so we know which versions to deprecate/ignore
request.AddQueryParam("active", _analyticsService.InstallIsActive.ToString().ToLower());
}
var updates = _httpClient.Get<List<UpdatePackage>>(request.Build());
return updates.Resource; return updates.Resource;
} }
} }
} }

View File

@ -29,7 +29,6 @@ namespace NzbDrone.App.Test
Mocker.GetMock<IProcessProvider>() Mocker.GetMock<IProcessProvider>()
.Setup(c => c.SpawnNewProcess("sc.exe", It.IsAny<string>(), null, true)); .Setup(c => c.SpawnNewProcess("sc.exe", It.IsAny<string>(), null, true));
Mocker.GetMock<IRuntimeInfo>().SetupGet(c => c.IsUserInteractive).Returns(true); Mocker.GetMock<IRuntimeInfo>().SetupGet(c => c.IsUserInteractive).Returns(true);
Subject.Route(ApplicationModes.InstallService); Subject.Route(ApplicationModes.InstallService);

View File

@ -0,0 +1,46 @@
using NzbDrone.Common.EnvironmentInfo;
namespace Radarr.Host.AccessControl
{
public interface IRemoteAccessAdapter
{
void MakeAccessible(bool passive);
}
public class RemoteAccessAdapter : IRemoteAccessAdapter
{
private readonly IRuntimeInfo _runtimeInfo;
private readonly IUrlAclAdapter _urlAclAdapter;
private readonly IFirewallAdapter _firewallAdapter;
private readonly ISslAdapter _sslAdapter;
public RemoteAccessAdapter(IRuntimeInfo runtimeInfo,
IUrlAclAdapter urlAclAdapter,
IFirewallAdapter firewallAdapter,
ISslAdapter sslAdapter)
{
_runtimeInfo = runtimeInfo;
_urlAclAdapter = urlAclAdapter;
_firewallAdapter = firewallAdapter;
_sslAdapter = sslAdapter;
}
public void MakeAccessible(bool passive)
{
if (OsInfo.IsWindows)
{
if (_runtimeInfo.IsAdmin)
{
_firewallAdapter.MakeAccessible();
_sslAdapter.Register();
}
else if (!passive)
{
throw new RemoteAccessException("Failed to register URLs for Radarr. Radarr will not be accessible remotely");
}
}
_urlAclAdapter.ConfigureUrls();
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using NzbDrone.Common.Exceptions;
namespace Radarr.Host.AccessControl
{
public class RemoteAccessException : NzbDroneException
{
public RemoteAccessException(string message, params object[] args) : base(message, args)
{
}
public RemoteAccessException(string message) : base(message)
{
}
public RemoteAccessException(string message, Exception innerException, params object[] args) : base(message, innerException, args)
{
}
public RemoteAccessException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -7,5 +7,6 @@
InstallService, InstallService,
UninstallService, UninstallService,
Service, Service,
RegisterUrl
} }
} }

View File

@ -29,6 +29,7 @@ namespace Radarr.Host
} }
_container = MainAppContainerBuilder.BuildContainer(startupContext); _container = MainAppContainerBuilder.BuildContainer(startupContext);
_container.Resolve<InitializeLogger>().Initialize();
_container.Resolve<IAppFolderFactory>().Register(); _container.Resolve<IAppFolderFactory>().Register();
_container.Resolve<IProvidePidFile>().Write(); _container.Resolve<IProvidePidFile>().Write();
@ -109,11 +110,17 @@ namespace Radarr.Host
private static ApplicationModes GetApplicationMode(IStartupContext startupContext) private static ApplicationModes GetApplicationMode(IStartupContext startupContext)
{ {
if (startupContext.Flags.Contains(StartupContext.HELP)) if (startupContext.Help)
{ {
return ApplicationModes.Help; return ApplicationModes.Help;
} }
if (OsInfo.IsWindows && startupContext.RegisterUrl)
{
return ApplicationModes.RegisterUrl;
}
if (OsInfo.IsWindows && startupContext.InstallService) if (OsInfo.IsWindows && startupContext.InstallService)
{ {
return ApplicationModes.InstallService; return ApplicationModes.InstallService;
@ -138,6 +145,7 @@ namespace Radarr.Host
{ {
case ApplicationModes.InstallService: case ApplicationModes.InstallService:
case ApplicationModes.UninstallService: case ApplicationModes.UninstallService:
case ApplicationModes.RegisterUrl:
case ApplicationModes.Help: case ApplicationModes.Help:
{ {
return true; return true;

View File

@ -31,7 +31,6 @@ namespace Radarr.Host
AutoRegisterImplementations<NzbDronePersistentConnection>(); AutoRegisterImplementations<NzbDronePersistentConnection>();
Container.Register<INancyBootstrapper, RadarrBootstrapper>(); Container.Register<INancyBootstrapper, RadarrBootstrapper>();
Container.Register<IHttpDispatcher, FallbackHttpDispatcher>();
} }
} }
} }

View File

@ -8,41 +8,26 @@ namespace Radarr.Host.Owin
public class OwinHostController : IHostController public class OwinHostController : IHostController
{ {
private readonly IOwinAppFactory _owinAppFactory; private readonly IOwinAppFactory _owinAppFactory;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRemoteAccessAdapter _removeAccessAdapter;
private readonly IUrlAclAdapter _urlAclAdapter; private readonly IUrlAclAdapter _urlAclAdapter;
private readonly IFirewallAdapter _firewallAdapter;
private readonly ISslAdapter _sslAdapter;
private readonly Logger _logger; private readonly Logger _logger;
private IDisposable _owinApp; private IDisposable _owinApp;
public OwinHostController( public OwinHostController(
IOwinAppFactory owinAppFactory, IOwinAppFactory owinAppFactory,
IRuntimeInfo runtimeInfo, IRemoteAccessAdapter removeAccessAdapter,
IUrlAclAdapter urlAclAdapter, IUrlAclAdapter urlAclAdapter,
IFirewallAdapter firewallAdapter,
ISslAdapter sslAdapter,
Logger logger) Logger logger)
{ {
_owinAppFactory = owinAppFactory; _owinAppFactory = owinAppFactory;
_runtimeInfo = runtimeInfo; _removeAccessAdapter = removeAccessAdapter;
_urlAclAdapter = urlAclAdapter; _urlAclAdapter = urlAclAdapter;
_firewallAdapter = firewallAdapter;
_sslAdapter = sslAdapter;
_logger = logger; _logger = logger;
} }
public void StartServer() public void StartServer()
{ {
if (OsInfo.IsWindows) _removeAccessAdapter.MakeAccessible(true);
{
if (_runtimeInfo.IsAdmin)
{
_firewallAdapter.MakeAccessible();
_sslAdapter.Register();
}
}
_urlAclAdapter.ConfigureUrls();
_logger.Info("Listening on the following URLs:"); _logger.Info("Listening on the following URLs:");
foreach (var url in _urlAclAdapter.Urls) foreach (var url in _urlAclAdapter.Urls)
@ -53,7 +38,6 @@ namespace Radarr.Host.Owin
_owinApp = _owinAppFactory.CreateApp(_urlAclAdapter.Urls); _owinApp = _owinAppFactory.CreateApp(_urlAclAdapter.Urls);
} }
public void StopServer() public void StopServer()
{ {
if (_owinApp == null) return; if (_owinApp == null) return;
@ -63,8 +47,5 @@ namespace Radarr.Host.Owin
_owinApp = null; _owinApp = null;
_logger.Info("Host has stopped"); _logger.Info("Host has stopped");
} }
} }
} }

View File

@ -11,6 +11,7 @@ using NLog;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using Radarr.Host.Owin.MiddleWare; using Radarr.Host.Owin.MiddleWare;
using Owin; using Owin;
using NzbDrone.Common.EnvironmentInfo;
namespace Radarr.Host.Owin namespace Radarr.Host.Owin
{ {
@ -70,7 +71,7 @@ namespace Radarr.Host.Owin
private void BuildApp(IAppBuilder appBuilder) private void BuildApp(IAppBuilder appBuilder)
{ {
appBuilder.Properties["host.AppName"] = "NzbDrone"; appBuilder.Properties["host.AppName"] = BuildInfo.AppName;
foreach (var middleWare in _owinMiddleWares.OrderBy(c => c.Order)) foreach (var middleWare in _owinMiddleWares.OrderBy(c => c.Order))
{ {

View File

@ -1,6 +1,11 @@
using System;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
using Radarr.Host.AccessControl;
using IServiceProvider = NzbDrone.Common.IServiceProvider;
namespace Radarr.Host namespace Radarr.Host
{ {
@ -10,15 +15,24 @@ namespace Radarr.Host
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IConsoleService _consoleService; private readonly IConsoleService _consoleService;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly IProcessProvider _processProvider;
private readonly IRemoteAccessAdapter _remoteAccessAdapter;
private readonly Logger _logger; private readonly Logger _logger;
public Router(INzbDroneServiceFactory nzbDroneServiceFactory, IServiceProvider serviceProvider, public Router(INzbDroneServiceFactory nzbDroneServiceFactory,
IConsoleService consoleService, IRuntimeInfo runtimeInfo, Logger logger) IServiceProvider serviceProvider,
IConsoleService consoleService,
IRuntimeInfo runtimeInfo,
IProcessProvider processProvider,
IRemoteAccessAdapter remoteAccessAdapter,
Logger logger)
{ {
_nzbDroneServiceFactory = nzbDroneServiceFactory; _nzbDroneServiceFactory = nzbDroneServiceFactory;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_consoleService = consoleService; _consoleService = consoleService;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_processProvider = processProvider;
_remoteAccessAdapter = remoteAccessAdapter;
_logger = logger; _logger = logger;
} }
@ -50,8 +64,13 @@ namespace Radarr.Host
} }
else else
{ {
_remoteAccessAdapter.MakeAccessible(true);
_serviceProvider.Install(ServiceProvider.SERVICE_NAME); _serviceProvider.Install(ServiceProvider.SERVICE_NAME);
_serviceProvider.Start(ServiceProvider.SERVICE_NAME); _serviceProvider.SetPermissions(ServiceProvider.SERVICE_NAME);
// Start the service and exit.
// Ensures that there isn't an instance of Radarr already running that the service account cannot stop.
_processProvider.SpawnNewProcess("sc.exe", $"start {ServiceProvider.SERVICE_NAME}", null, true);
} }
break; break;
} }
@ -67,6 +86,13 @@ namespace Radarr.Host
_serviceProvider.Uninstall(ServiceProvider.SERVICE_NAME); _serviceProvider.Uninstall(ServiceProvider.SERVICE_NAME);
} }
break;
}
case ApplicationModes.RegisterUrl:
{
_logger.Debug("Regiser URL selected");
_remoteAccessAdapter.MakeAccessible(false);
break; break;
} }
default: default:
@ -76,7 +102,5 @@ namespace Radarr.Host
} }
} }
} }
} }
} }

View File

@ -1,6 +1,6 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Api.Movies; using Radarr.Api.V2.Movies;
namespace NzbDrone.Integration.Test.ApiTests namespace NzbDrone.Integration.Test.ApiTests
{ {
@ -15,7 +15,7 @@ namespace NzbDrone.Integration.Test.ApiTests
{ {
_movie = EnsureMovie(11, "The Blacklist"); _movie = EnsureMovie(11, "The Blacklist");
Blacklist.Post(new Api.Blacklist.BlacklistResource Blacklist.Post(new Radarr.Api.V2.Blacklist.BlacklistResource
{ {
MovieId = _movie.Id, MovieId = _movie.Id,
SourceTitle = "Blacklist.S01E01.Brought.To.You.By-BoomBoxHD" SourceTitle = "Blacklist.S01E01.Brought.To.You.By-BoomBoxHD"

View File

@ -1,6 +1,6 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Api.Movies; using Radarr.Api.V2.Movies;
using NzbDrone.Integration.Test.Client; using NzbDrone.Integration.Test.Client;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -1,6 +1,6 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Api.Commands; using NzbDrone.Integration.Test.Client;
namespace NzbDrone.Integration.Test.ApiTests namespace NzbDrone.Integration.Test.ApiTests
{ {
@ -10,7 +10,7 @@ namespace NzbDrone.Integration.Test.ApiTests
[Test] [Test]
public void should_be_able_to_run_rss_sync() public void should_be_able_to_run_rss_sync()
{ {
var response = Commands.Post(new CommandResource { Name = "rsssync" }); var response = Commands.Post(new SimpleCommandResource { Name = "rsssync" });
response.Id.Should().NotBe(0); response.Id.Should().NotBe(0);
} }

View File

@ -1,7 +1,7 @@
using System.Linq; using System.Linq;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Api.DiskSpace; using Radarr.Api.V2.DiskSpace;
using NzbDrone.Integration.Test.Client; using NzbDrone.Integration.Test.Client;
namespace NzbDrone.Integration.Test.ApiTests namespace NzbDrone.Integration.Test.ApiTests

View File

@ -16,8 +16,8 @@ namespace NzbDrone.Integration.Test.ApiTests
var schema = DownloadClients.Schema().First(v => v.Implementation == "UsenetBlackhole"); var schema = DownloadClients.Schema().First(v => v.Implementation == "UsenetBlackhole");
schema.Enable = true; schema.Enable = true;
schema.Fields.First(v => v.Name == "WatchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); schema.Fields.First(v => v.Name == "watchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch");
schema.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); schema.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb");
DownloadClients.InvalidPost(schema); DownloadClients.InvalidPost(schema);
} }
@ -31,7 +31,7 @@ namespace NzbDrone.Integration.Test.ApiTests
schema.Enable = true; schema.Enable = true;
schema.Name = "Test UsenetBlackhole"; schema.Name = "Test UsenetBlackhole";
schema.Fields.First(v => v.Name == "WatchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); schema.Fields.First(v => v.Name == "watchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch");
DownloadClients.InvalidPost(schema); DownloadClients.InvalidPost(schema);
} }
@ -45,7 +45,7 @@ namespace NzbDrone.Integration.Test.ApiTests
schema.Enable = true; schema.Enable = true;
schema.Name = "Test UsenetBlackhole"; schema.Name = "Test UsenetBlackhole";
schema.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); schema.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb");
DownloadClients.InvalidPost(schema); DownloadClients.InvalidPost(schema);
} }
@ -59,8 +59,8 @@ namespace NzbDrone.Integration.Test.ApiTests
schema.Enable = true; schema.Enable = true;
schema.Name = "Test UsenetBlackhole"; schema.Name = "Test UsenetBlackhole";
schema.Fields.First(v => v.Name == "WatchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch"); schema.Fields.First(v => v.Name == "watchFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Watch");
schema.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb"); schema.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb");
var result = DownloadClients.Post(schema); var result = DownloadClients.Post(schema);
@ -99,7 +99,7 @@ namespace NzbDrone.Integration.Test.ApiTests
EnsureNoDownloadClient(); EnsureNoDownloadClient();
var client = EnsureDownloadClient(); var client = EnsureDownloadClient();
client.Fields.First(v => v.Name == "NzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb2"); client.Fields.First(v => v.Name == "nzbFolder").Value = GetTempDirectory("Download", "UsenetBlackhole", "Nzb2");
var result = DownloadClients.Put(client); var result = DownloadClients.Put(client);
result.Should().NotBeNull(); result.Should().NotBeNull();

View File

@ -1,6 +1,7 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using System.Linq; using System.Linq;
using Radarr.Api.V2.Movies;
using NzbDrone.Test.Common; using NzbDrone.Test.Common;
namespace NzbDrone.Integration.Test.ApiTests namespace NzbDrone.Integration.Test.ApiTests
@ -10,11 +11,11 @@ namespace NzbDrone.Integration.Test.ApiTests
{ {
private void GivenExistingMovie() private void GivenExistingMovie()
{ {
foreach (var title in new[] { "90210", "Dexter" }) foreach (var title in new[] { "The Dark Knight", "Pulp Fiction" })
{ {
var newMovie = Movies.Lookup(title).First(); var newMovie = Movies.Lookup(title).First();
newMovie.ProfileId = 1; newMovie.QualityProfileId = 1;
newMovie.Path = string.Format(@"C:\Test\{0}", title).AsOsAgnostic(); newMovie.Path = string.Format(@"C:\Test\{0}", title).AsOsAgnostic();
Movies.Post(newMovie); Movies.Post(newMovie);
@ -26,17 +27,18 @@ namespace NzbDrone.Integration.Test.ApiTests
{ {
GivenExistingMovie(); GivenExistingMovie();
var movie = Movies.All(); var movies = Movies.All();
foreach (var s in movie) var movieEditor = new MovieEditorResource
{ {
s.ProfileId = 2; QualityProfileId = 2,
} MovieIds = movies.Select(o => o.Id).ToList()
};
var result = Movies.Editor(movie); var result = Movies.Editor(movieEditor);
result.Should().HaveCount(2); result.Should().HaveCount(2);
result.TrueForAll(s => s.ProfileId == 2).Should().BeTrue(); result.TrueForAll(s => s.QualityProfileId == 2).Should().BeTrue();
} }
} }
} }

View File

@ -18,7 +18,7 @@ namespace NzbDrone.Integration.Test.ApiTests
var movie = Movies.Lookup("imdb:tt0110912").Single(); var movie = Movies.Lookup("imdb:tt0110912").Single();
movie.ProfileId = 1; movie.QualityProfileId = 1;
movie.Path = Path.Combine(MovieRootFolder, movie.Title); movie.Path = Path.Combine(MovieRootFolder, movie.Title);
movie.Tags = new HashSet<int>(); movie.Tags = new HashSet<int>();
movie.Tags.Add(tag.Id); movie.Tags.Add(tag.Id);
@ -48,7 +48,7 @@ namespace NzbDrone.Integration.Test.ApiTests
var movie = Movies.Lookup("imdb:tt0110912").Single(); var movie = Movies.Lookup("imdb:tt0110912").Single();
movie.ProfileId = 1; movie.QualityProfileId = 1;
Movies.InvalidPost(movie); Movies.InvalidPost(movie);
} }
@ -60,14 +60,14 @@ namespace NzbDrone.Integration.Test.ApiTests
var movie = Movies.Lookup("imdb:tt0110912").Single(); var movie = Movies.Lookup("imdb:tt0110912").Single();
movie.ProfileId = 1; movie.QualityProfileId = 1;
movie.Path = Path.Combine(MovieRootFolder, movie.Title); movie.Path = Path.Combine(MovieRootFolder, movie.Title);
var result = Movies.Post(movie); var result = Movies.Post(movie);
result.Should().NotBeNull(); result.Should().NotBeNull();
result.Id.Should().NotBe(0); result.Id.Should().NotBe(0);
result.ProfileId.Should().Be(1); result.QualityProfileId.Should().Be(1);
result.Path.Should().Be(Path.Combine(MovieRootFolder, movie.Title)); result.Path.Should().Be(Path.Combine(MovieRootFolder, movie.Title));
} }
@ -105,16 +105,16 @@ namespace NzbDrone.Integration.Test.ApiTests
var movie = EnsureMovie(680, "Pulp Fiction"); var movie = EnsureMovie(680, "Pulp Fiction");
var profileId = 1; var profileId = 1;
if (movie.ProfileId == profileId) if (movie.QualityProfileId == profileId)
{ {
profileId = 2; profileId = 2;
} }
movie.ProfileId = profileId; movie.QualityProfileId = profileId;
var result = Movies.Put(movie); var result = Movies.Put(movie);
Movies.Get(movie.Id).ProfileId.Should().Be(profileId); Movies.Get(movie.Id).QualityProfileId.Should().Be(profileId);
} }
[Test, Order(3)] [Test, Order(3)]

View File

@ -25,11 +25,11 @@ namespace NzbDrone.Integration.Test.ApiTests
public void should_be_able_to_update() public void should_be_able_to_update()
{ {
var config = NamingConfig.GetSingle(); var config = NamingConfig.GetSingle();
config.RenameEpisodes = false; config.RenameMovies = false;
config.StandardMovieFormat = "{Movie Title}"; config.StandardMovieFormat = "{Movie Title}";
var result = NamingConfig.Put(config); var result = NamingConfig.Put(config);
result.RenameEpisodes.Should().BeFalse(); result.RenameMovies.Should().BeFalse();
result.StandardMovieFormat.Should().Be(config.StandardMovieFormat); result.StandardMovieFormat.Should().Be(config.StandardMovieFormat);
} }
@ -38,7 +38,7 @@ namespace NzbDrone.Integration.Test.ApiTests
public void should_get_bad_request_if_standard_format_is_empty() public void should_get_bad_request_if_standard_format_is_empty()
{ {
var config = NamingConfig.GetSingle(); var config = NamingConfig.GetSingle();
config.RenameEpisodes = true; config.RenameMovies = true;
config.StandardMovieFormat = ""; config.StandardMovieFormat = "";
var errors = NamingConfig.InvalidPut(config); var errors = NamingConfig.InvalidPut(config);
@ -49,7 +49,7 @@ namespace NzbDrone.Integration.Test.ApiTests
public void should_get_bad_request_if_standard_format_doesnt_contain_title() public void should_get_bad_request_if_standard_format_doesnt_contain_title()
{ {
var config = NamingConfig.GetSingle(); var config = NamingConfig.GetSingle();
config.RenameEpisodes = true; config.RenameMovies = true;
config.StandardMovieFormat = "{quality}"; config.StandardMovieFormat = "{quality}";
var errors = NamingConfig.InvalidPut(config); var errors = NamingConfig.InvalidPut(config);
@ -60,7 +60,7 @@ namespace NzbDrone.Integration.Test.ApiTests
public void should_not_require_format_when_rename_episodes_is_false() public void should_not_require_format_when_rename_episodes_is_false()
{ {
var config = NamingConfig.GetSingle(); var config = NamingConfig.GetSingle();
config.RenameEpisodes = false; config.RenameMovies = false;
config.StandardMovieFormat = ""; config.StandardMovieFormat = "";
var errors = NamingConfig.InvalidPut(config); var errors = NamingConfig.InvalidPut(config);
@ -71,7 +71,7 @@ namespace NzbDrone.Integration.Test.ApiTests
public void should_require_format_when_rename_episodes_is_true() public void should_require_format_when_rename_episodes_is_true()
{ {
var config = NamingConfig.GetSingle(); var config = NamingConfig.GetSingle();
config.RenameEpisodes = true; config.RenameMovies = true;
config.StandardMovieFormat = ""; config.StandardMovieFormat = "";
var errors = NamingConfig.InvalidPut(config); var errors = NamingConfig.InvalidPut(config);
@ -82,7 +82,7 @@ namespace NzbDrone.Integration.Test.ApiTests
public void should_get_bad_request_if_movie_folder_format_does_not_contain_movie_title() public void should_get_bad_request_if_movie_folder_format_does_not_contain_movie_title()
{ {
var config = NamingConfig.GetSingle(); var config = NamingConfig.GetSingle();
config.RenameEpisodes = true; config.RenameMovies = true;
config.MovieFolderFormat = "This and That"; config.MovieFolderFormat = "This and That";
var errors = NamingConfig.InvalidPut(config); var errors = NamingConfig.InvalidPut(config);

View File

@ -34,7 +34,7 @@ namespace NzbDrone.Integration.Test.ApiTests
var xbmc = schema.Single(s => s.Implementation.Equals("Xbmc", StringComparison.InvariantCultureIgnoreCase)); var xbmc = schema.Single(s => s.Implementation.Equals("Xbmc", StringComparison.InvariantCultureIgnoreCase));
xbmc.Name = "Test XBMC"; xbmc.Name = "Test XBMC";
xbmc.Fields.Single(f => f.Name.Equals("Host")).Value = "localhost"; xbmc.Fields.Single(f => f.Name.Equals("host")).Value = "localhost";
var result = Notifications.Post(xbmc); var result = Notifications.Post(xbmc);
Notifications.Delete(result.Id); Notifications.Delete(result.Id);

View File

@ -1,6 +1,6 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Api.Indexers; using Radarr.Api.V2.Indexers;
using System.Linq; using System.Linq;
using System.Net; using System.Net;

View File

@ -1,7 +1,7 @@
using System; using System;
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Api.RootFolders; using Radarr.Api.V2.RootFolders;
namespace NzbDrone.Integration.Test.ApiTests namespace NzbDrone.Integration.Test.ApiTests
{ {

View File

@ -1,83 +0,0 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Integration.Test.ApiTests.WantedTests
{
[TestFixture]
public class CutoffUnmetFixture : IntegrationTest
{
[Test, Order(1)]
public void cutoff_should_have_monitored_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
var movie = EnsureMovie(680, "Pulp Fiction", true);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
result.Records.Should().NotBeEmpty();
}
[Test, Order(1)]
public void cutoff_should_not_have_unmonitored_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
var movie = EnsureMovie(680, "Pulp Fiction", false);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void cutoff_should_not_have_released_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
var movie = EnsureMovie(680, "Pulp Fiction", true);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "status", "inCinemas");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void cutoff_should_have_movie()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
var movie = EnsureMovie(680, "Pulp Fiction", true);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.First().Title.Should().Be("Pulp Fiction");
}
[Test, Order(2)]
public void cutoff_should_have_unmonitored_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
var movie = EnsureMovie(680, "Pulp Fiction", false);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "false");
result.Records.Should().NotBeEmpty();
}
[Test, Order(2)]
public void cutoff_should_have_released_items()
{
EnsureProfileCutoff(1, Quality.HDTV720p);
var movie = EnsureMovie(680, "Pulp Fiction", false);
EnsureMovieFile(movie, Quality.SDTV);
var result = WantedCutoffUnmet.GetPaged(0, 15, "physicalRelease", "desc", "status", "released");
result.Records.Should().NotBeEmpty();
}
}
}

View File

@ -1,81 +0,0 @@
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Integration.Test.ApiTests.WantedTests
{
[TestFixture]
public class MissingFixture : IntegrationTest
{
[Test, Order(0)]
public void missing_should_be_empty()
{
EnsureNoMovie(680, "Pulp Fiction");
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void missing_should_have_monitored_items()
{
EnsureMovie(680, "Pulp Fiction", true);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
result.Records.Should().NotBeEmpty();
}
[Test, Order(1)]
public void missing_should_have_movie()
{
EnsureMovie(680, "Pulp Fiction", true);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc");
result.Records.First().Title.Should().Be("Pulp Fiction");
}
[Test, Order(1)]
public void missing_should_not_have_unmonitored_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "true");
result.Records.Should().BeEmpty();
}
[Test, Order(1)]
public void missing_should_not_have_released_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "status", "inCinemas");
result.Records.Should().BeEmpty();
}
[Test, Order(2)]
public void missing_should_have_unmonitored_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "monitored", "false");
result.Records.Should().NotBeEmpty();
}
[Test, Order(2)]
public void missing_should_have_released_items()
{
EnsureMovie(680, "Pulp Fiction", false);
var result = WantedMissing.GetPaged(0, 15, "physicalRelease", "desc", "status", "released");
result.Records.Should().NotBeEmpty();
}
}
}

View File

@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.Net; using System.Net;
using FluentAssertions; using FluentAssertions;
using NLog; using NLog;
using NzbDrone.Api;
using Radarr.Http.REST; using Radarr.Http.REST;
using Radarr.Http; using Radarr.Http;
using NzbDrone.Common.Serializer; using NzbDrone.Common.Serializer;
@ -40,7 +39,7 @@ namespace NzbDrone.Integration.Test.Client
return request; return request;
} }
public T Execute<T>(IRestRequest request, HttpStatusCode statusCode) where T : class, new() public string Execute(IRestRequest request, HttpStatusCode statusCode)
{ {
_logger.Info("{0}: {1}", request.Method, _restClient.BuildUri(request)); _logger.Info("{0}: {1}", request.Method, _restClient.BuildUri(request));
@ -58,7 +57,14 @@ namespace NzbDrone.Integration.Test.Client
response.StatusCode.Should().Be(statusCode); response.StatusCode.Should().Be(statusCode);
return Json.Deserialize<T>(response.Content); return response.Content;
}
public T Execute<T>(IRestRequest request, HttpStatusCode statusCode) where T : class, new()
{
var content = Execute(request, statusCode);
return Json.Deserialize<T>(content);
} }
private static void AssertDisableCache(IList<Parameter> headers) private static void AssertDisableCache(IList<Parameter> headers)

View File

@ -1,23 +1,47 @@
using NzbDrone.Api.Commands; using RestSharp;
using RestSharp;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using FluentAssertions; using FluentAssertions;
using System.Threading; using System.Threading;
using NUnit.Framework; using NUnit.Framework;
using System.Linq; using System.Linq;
using System;
using Radarr.Http.REST;
using Newtonsoft.Json;
namespace NzbDrone.Integration.Test.Client namespace NzbDrone.Integration.Test.Client
{ {
public class CommandClient : ClientBase<CommandResource> public class SimpleCommandResource : RestResource
{
public string Name { get; set; }
public string CommandName { get; set; }
public string Message { get; set; }
public CommandPriority Priority { get; set; }
public CommandStatus Status { get; set; }
public DateTime Queued { get; set; }
public DateTime? Started { get; set; }
public DateTime? Ended { get; set; }
public TimeSpan? Duration { get; set; }
public string Exception { get; set; }
public CommandTrigger Trigger { get; set; }
[JsonIgnore]
public Command Body { get; set; }
[JsonProperty("body")]
public Command BodyReadOnly { get { return Body; } }
}
public class CommandClient : ClientBase<SimpleCommandResource>
{ {
public CommandClient(IRestClient restClient, string apiKey) public CommandClient(IRestClient restClient, string apiKey)
: base(restClient, apiKey) : base(restClient, apiKey, "command")
{ {
} }
public CommandResource PostAndWait(CommandResource command) public SimpleCommandResource PostAndWait<T>(T command) where T : Command, new()
{ {
var result = Post(command); var request = BuildRequest();
request.AddBody(command);
var result = Post<SimpleCommandResource>(request);
result.Id.Should().NotBe(0); result.Id.Should().NotBe(0);
for (var i = 0; i < 50; i++) for (var i = 0; i < 50; i++)

View File

@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Api.DownloadClient; using Radarr.Api.V2.DownloadClient;
using RestSharp; using RestSharp;
namespace NzbDrone.Integration.Test.Client namespace NzbDrone.Integration.Test.Client

View File

@ -1,4 +1,4 @@
using NzbDrone.Api.Indexers; using Radarr.Api.V2.Indexers;
using RestSharp; using RestSharp;
namespace NzbDrone.Integration.Test.Client namespace NzbDrone.Integration.Test.Client

View File

@ -0,0 +1,24 @@
using System;
using RestSharp;
namespace NzbDrone.Integration.Test.Client
{
public class LogsClient : ClientBase
{
public LogsClient(IRestClient restClient, string apiKey)
: base(restClient, apiKey, "log/file")
{
}
public string[] GetLogFileLines(string filename)
{
var request = BuildRequest(filename);
var content = Execute(request, System.Net.HttpStatusCode.OK);
var lines = content.Split('\n');
lines = Array.ConvertAll(lines, s => s.TrimEnd('\r'));
Array.Resize(ref lines, lines.Length - 1);
return lines;
}
}
}

View File

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using NzbDrone.Api.Movies; using Radarr.Api.V2.Movies;
using RestSharp; using RestSharp;
namespace NzbDrone.Integration.Test.Client namespace NzbDrone.Integration.Test.Client
@ -19,7 +19,7 @@ namespace NzbDrone.Integration.Test.Client
return Get<List<MovieResource>>(request); return Get<List<MovieResource>>(request);
} }
public List<MovieResource> Editor(List<MovieResource> movie) public List<MovieResource> Editor(MovieEditorResource movie)
{ {
var request = BuildRequest("editor"); var request = BuildRequest("editor");
request.AddJsonBody(movie); request.AddJsonBody(movie);

View File

@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Api.Notifications; using Radarr.Api.V2.Notifications;
using RestSharp; using RestSharp;
namespace NzbDrone.Integration.Test.Client namespace NzbDrone.Integration.Test.Client

View File

@ -1,4 +1,4 @@
using NzbDrone.Api.Indexers; using Radarr.Api.V2.Indexers;
using RestSharp; using RestSharp;
namespace NzbDrone.Integration.Test.Client namespace NzbDrone.Integration.Test.Client

Some files were not shown because too many files have changed in this diff Show More