added resource mapping validation tests

This commit is contained in:
kay.one 2013-04-21 14:04:09 -07:00
parent 1d49435675
commit c3214a2e88
17 changed files with 297 additions and 76 deletions

View File

@ -0,0 +1,31 @@
using System.Reflection;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Reflection;
using NzbDrone.Core.Datastore;
using NzbDrone.Test.Common;
namespace NzbDrone.Api.Test.MappingTests
{
public class ReflectionExtensionFixture : TestBase
{
[Test]
public void should_get_properties_from_models()
{
var models = Assembly.Load("NzbDrone.Core").ImplementationsOf<ModelBase>();
foreach (var model in models)
{
model.GetSimpleProperties().Should().NotBeEmpty();
}
}
[Test]
public void should_be_able_to_get_implementations()
{
var models = Assembly.Load("NzbDrone.Core").ImplementationsOf<ModelBase>();
models.Should().NotBeEmpty();
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using NUnit.Framework;
using NzbDrone.Api.Episodes;
using NzbDrone.Api.Mapping;
using NzbDrone.Api.Series;
using NzbDrone.Test.Common;
namespace NzbDrone.Api.Test.MappingTests
{
[TestFixture]
public class ResourceMappingFixture : TestBase
{
[TestCase(typeof(Core.Tv.Series), typeof(SeriesResource))]
[TestCase(typeof(Core.Tv.Episode), typeof(EpisodeResource))]
public void matching_fields(Type modelType, Type resourceType)
{
MappingValidation.ValidateMapping(modelType, resourceType);
}
}
}

View File

@ -50,9 +50,19 @@
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="FizzWare.NBuilder">
<HintPath>..\packages\NBuilder.3.0.1.1\lib\FizzWare.NBuilder.dll</HintPath>
</Reference>
<Reference Include="FluentAssertions, Version=2.0.1.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FluentAssertions.2.0.1\lib\net40\FluentAssertions.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<HintPath>..\packages\NUnit.2.6.2\lib\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="Omu.ValueInjecter">
<HintPath>..\packages\valueinjecter.2.3.3\lib\net35\Omu.ValueInjecter.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@ -62,6 +72,8 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="MappingTests\ReflectionExtensionFixture.cs" />
<Compile Include="MappingTests\ResourceMappingFixture.cs" />
<Compile Include="StaticResourceMapperFixture.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

View File

@ -1,4 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="FluentAssertions" version="2.0.1" targetFramework="net40" />
<package id="NBuilder" version="3.0.1.1" targetFramework="net40" />
<package id="NUnit" version="2.6.2" targetFramework="net40" />
<package id="valueinjecter" version="2.3.3" targetFramework="net40" />
</packages>

View File

@ -1,10 +1,12 @@
using System;
using System.Linq;
using NzbDrone.Api.REST;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Api.Episodes
{
public class EpisodeResource
public class EpisodeResource : RestResource
{
public Int32 Id { get; set; }
public Int32 SeriesId { get; set; }
@ -12,9 +14,19 @@ namespace NzbDrone.Api.Episodes
public Int32 SeasonNumber { get; set; }
public Int32 EpisodeNumber { get; set; }
public String Title { get; set; }
public DateTime AirDate { get; set; }
public Int32 Status { get; set; }
public DateTime? AirDate { get; set; }
public EpisodeStatuses Status { get; set; }
public String Overview { get; set; }
public EpisodeFile EpisodeFile { get; set; }
public Boolean HasFile { get; set; }
public Boolean Ignored { get; set; }
public Int32 SceneEpisodeNumber { get; set; }
public Int32 SceneSeasonNumber { get; set; }
public Int32 TvDbEpisodeId { get; set; }
public Int32? AbsoluteEpisodeNumber { get; set; }
public DateTime? EndTime { get; set; }
public DateTime? GrabDate { get; set; }
public PostDownloadStatusType PostDownloadStatus { get; set; }
}
}

View File

@ -0,0 +1,60 @@
using System;
using System.Linq;
using System.Reflection;
using NzbDrone.Api.REST;
using NzbDrone.Common.Reflection;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Api.Mapping
{
public static class MappingValidation
{
public static void ValidateMapping(Type modelType, Type resourceType)
{
var errors = modelType.GetSimpleProperties().Select(p => GetError(resourceType, p)).Where(c => c != null).ToList();
if (errors.Any())
{
throw new ResourceMappingException(errors);
}
PrintExtraProperties(modelType, resourceType);
}
private static void PrintExtraProperties(Type modelType, Type resourceType)
{
var resourceBaseProperties = typeof(RestResource).GetProperties().Select(c => c.Name);
var resourceProperties = resourceType.GetProperties().Select(c => c.Name).Except(resourceBaseProperties);
var modelProperties = modelType.GetProperties().Select(c => c.Name);
var extra = resourceProperties.Except(modelProperties);
foreach (var extraProp in extra)
{
Console.WriteLine("Extra: [{0}]", extraProp);
}
}
private static string GetError(Type resourceType, PropertyInfo modelProperty)
{
var resourceProperty = resourceType.GetProperties().FirstOrDefault(c => c.Name == modelProperty.Name);
if (resourceProperty == null)
{
return string.Format("public {0} {1} {{ get; set; }}", modelProperty.PropertyType.Name, modelProperty.Name);
}
if (resourceProperty.PropertyType != modelProperty.PropertyType)
{
return string.Format("Excpected {0}.{1} to have type of {2} but found {3}", resourceType.Name, resourceProperty.Name, modelProperty.PropertyType, resourceProperty.PropertyType);
}
return null;
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NzbDrone.Api.Mapping
{
public class ResourceMappingException : ApplicationException
{
public ResourceMappingException(IEnumerable<string> error)
: base(Environment.NewLine + String.Join(Environment.NewLine, error.OrderBy(c => c)))
{
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Omu.ValueInjecter;
namespace NzbDrone.Api.Mapping
{
public static class ValueInjectorExtensions
{
public static TTarget InjectTo<TTarget>(this object source) where TTarget : new()
{
var targetType = typeof(TTarget);
if (targetType.IsGenericType &&
targetType.GetGenericTypeDefinition() != null &&
targetType.GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable)) &&
source.GetType().IsGenericType &&
source.GetType().GetGenericTypeDefinition() != null &&
source.GetType().GetGenericTypeDefinition().GetInterfaces().Contains(typeof(IEnumerable)))
{
var result = new TTarget();
var listSubType = targetType.GetGenericArguments()[0];
var listType = typeof(List<>).MakeGenericType(listSubType);
var addMethod = listType.GetMethod("Add");
foreach (var sourceItem in (IEnumerable)source)
{
var e = Activator.CreateInstance(listSubType).InjectFrom(sourceItem);
addMethod.Invoke(result, new[] { e });
}
return result;
}
return (TTarget)new TTarget().InjectFrom(source);
}
}
}

View File

@ -88,6 +88,9 @@
<Compile Include="Frontend\IndexModule.cs" />
<Compile Include="Frontend\StaticResourceProvider.cs" />
<Compile Include="Frontend\StaticResourceMapper.cs" />
<Compile Include="Mapping\MappingValidation.cs" />
<Compile Include="Mapping\ResourceMappingException.cs" />
<Compile Include="Mapping\ValueInjectorExtensions.cs" />
<Compile Include="Missing\MissingResource.cs" />
<Compile Include="Missing\MissingModule.cs" />
<Compile Include="NzbDroneRestModule.cs" />

View File

@ -40,7 +40,7 @@ namespace NzbDrone.Api.REST
Get[ID_ROUTE] = options =>
{
EnsureImplementation(GetResourceById);
var resource = GetResourceById(options.Id);
var resource = GetResourceById((int)options.Id);
return resource.AsResponse();
};
@ -61,7 +61,7 @@ namespace NzbDrone.Api.REST
Delete[ID_ROUTE] = options =>
{
EnsureImplementation(DeleteResource);
DeleteResource(options.Id);
DeleteResource((int)options.Id);
return new Response { StatusCode = HttpStatusCode.OK };
};

View File

@ -1,14 +1,13 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using AutoMapper;
using FluentValidation;
using NzbDrone.Api.Extensions;
using NzbDrone.Common;
using NzbDrone.Common;
using NzbDrone.Core.SeriesStats;
using NzbDrone.Api.Mapping;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Model;
using NzbDrone.Api.Validation;
namespace NzbDrone.Api.Series
@ -43,7 +42,8 @@ namespace NzbDrone.Api.Series
{
var series = _seriesService.GetAllSeries().ToList();
var seriesStats = _seriesStatisticsService.SeriesStatistics();
var seriesModels = Mapper.Map<List<Core.Tv.Series>, List<SeriesResource>>(series);
var seriesModels = series.InjectTo<List<SeriesResource>>();
foreach (var s in seriesModels)
{
@ -52,7 +52,7 @@ namespace NzbDrone.Api.Series
s.EpisodeCount = stats.EpisodeCount;
s.EpisodeFileCount = stats.EpisodeFileCount;
s.NumberOfSeasons = stats.NumberOfSeasons;
s.SeasonsCount = stats.NumberOfSeasons;
s.NextAiring = stats.NextAiring;
}
@ -62,8 +62,7 @@ namespace NzbDrone.Api.Series
private SeriesResource GetSeries(int id)
{
var series = _seriesService.GetSeries(id);
var seriesModels = Mapper.Map<Core.Tv.Series, SeriesResource>(series);
return seriesModels;
return series.InjectTo<SeriesResource>();
}
private SeriesResource AddSeries(SeriesResource seriesResource)
@ -74,9 +73,9 @@ namespace NzbDrone.Api.Series
//Todo: We need to create the folder if the user is adding a new series
//(we can just create the folder and it won't blow up if it already exists)
//We also need to remove any special characters from the filename before attempting to create it
var series = Mapper.Map<SeriesResource, Core.Tv.Series>(seriesResource);
var series = seriesResource.InjectTo<Core.Tv.Series>();
_seriesService.AddSeries(series);
return Mapper.Map<Core.Tv.Series, SeriesResource>(series);
return series.InjectTo<SeriesResource>();
}
private SeriesResource UpdateSeries(SeriesResource seriesResource)
@ -91,17 +90,11 @@ namespace NzbDrone.Api.Series
series.RootFolderId = seriesResource.RootFolderId;
series.FolderName = seriesResource.FolderName;
series.BacklogSetting = (BacklogSettingType)seriesResource.BacklogSetting;
if (!String.IsNullOrWhiteSpace(seriesResource.CustomStartDate))
series.CustomStartDate = DateTime.Parse(seriesResource.CustomStartDate, null, DateTimeStyles.RoundtripKind);
else
series.CustomStartDate = null;
series.BacklogSetting = seriesResource.BacklogSetting;
_seriesService.UpdateSeries(series);
return Mapper.Map<Core.Tv.Series, SeriesResource>(series);
return series.InjectTo<SeriesResource>();
}
private void DeleteSeries(int id)
@ -111,23 +104,4 @@ namespace NzbDrone.Api.Series
}
}
public class SeriesValidator : AbstractValidator<Core.Tv.Series>
{
private readonly DiskProvider _diskProvider;
public SeriesValidator(DiskProvider diskProvider)
{
_diskProvider = diskProvider;
}
public SeriesValidator()
{
RuleSet("POST", () =>
{
RuleFor(s => s.Id).GreaterThan(0);
RuleFor(s => s.Path).NotEmpty().Must(_diskProvider.FolderExists);
RuleFor(s => s.QualityProfileId).GreaterThan(0);
});
}
}
}
}

View File

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using NzbDrone.Api.REST;
using NzbDrone.Core.Model;
using NzbDrone.Core.Tv;
namespace NzbDrone.Api.Series
{
@ -15,21 +17,17 @@ namespace NzbDrone.Api.Series
public Int32 SeasonsCount { get; set; }
public Int32 EpisodeCount { get; set; }
public Int32 EpisodeFileCount { get; set; }
public String Status { get; set; }
public String AirsDayOfWeek { get; set; }
public SeriesStatusType Status { get; set; }
public String QualityProfileName { get; set; }
public String Overview { get; set; }
public Int32 Episodes { get; set; }
public Boolean HasBanner { get; set; }
public DateTime? NextAiring { get; set; }
public String Details { get; set; }
public String Network { get; set; }
public String AirTime { get; set; }
public String Language { get; set; }
public Int32 NumberOfSeasons { get; set; }
public Int32 UtcOffset { get; set; }
public List<Core.MediaCover.MediaCover> Images { get; set; }
public String Path { get; set; }
//View & Edit
public int RootFolderId { get; set; }
public string FolderName { get; set; }
@ -38,7 +36,22 @@ namespace NzbDrone.Api.Series
//Editing Only
public Boolean SeasonFolder { get; set; }
public Boolean Monitored { get; set; }
public Int32 BacklogSetting { get; set; }
public String CustomStartDate { get; set; }
public BacklogSettingType BacklogSetting { get; set; }
public DateTime? CustomStartDate { get; set; }
public Boolean UseSceneNumbering { get; set; }
public Int32 Id { get; set; }
public Int32 Runtime { get; set; }
public Int32 TvdbId { get; set; }
public Int32 TvRageId { get; set; }
public DateTime? FirstAired { get; set; }
public DateTime? LastInfoSync { get; set; }
public SeriesTypes SeriesType { get; set; }
public String CleanTitle { get; set; }
public String ImdbId { get; set; }
public String TitleSlug { get; set; }
}
}

View File

@ -118,6 +118,7 @@
<Compile Include="HostController.cs" />
<Compile Include="IJsonSerializer.cs" />
<Compile Include="Instrumentation\VersionLayoutRenderer.cs" />
<Compile Include="Reflection\ReflectionExtensions.cs" />
<Compile Include="StringExtention.cs" />
<Compile Include="HttpProvider.cs" />
<Compile Include="ConfigFileProvider.cs" />

View File

@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace NzbDrone.Common.Reflection
{
public static class ReflectionExtensions
{
public static List<PropertyInfo> GetSimpleProperties(this Type type)
{
var properties = type.GetProperties();
return properties.Where(c => c.PropertyType.IsSimpleType()).ToList();
}
public static List<Type> ImplementationsOf<T>(this Assembly assembly)
{
return assembly.GetTypes().Where(c => typeof(T).IsAssignableFrom(c)).ToList();
}
public static bool IsSimpleType(this Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
type = type.GetGenericArguments()[0];
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(Decimal);
}
public static bool IsReadable(this PropertyInfo propertyInfo)
{
return propertyInfo.CanRead && propertyInfo.GetGetMethod(false) != null;
}
public static bool IsWritable(this PropertyInfo propertyInfo)
{
return propertyInfo.CanWrite && propertyInfo.GetSetMethod(false) != null;
}
}
}

View File

@ -2,6 +2,7 @@
using System.Reflection;
using Marr.Data;
using Marr.Data.Mapping;
using NzbDrone.Common.Reflection;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Datastore
@ -43,7 +44,7 @@ namespace NzbDrone.Core.Datastore
return false;
}
if (IsSimpleType(propertyInfo.PropertyType) || MapRepository.Instance.TypeConverters.ContainsKey(propertyInfo.PropertyType))
if (propertyInfo.PropertyType.IsSimpleType() || MapRepository.Instance.TypeConverters.ContainsKey(propertyInfo.PropertyType))
{
return true;
}
@ -51,28 +52,6 @@ namespace NzbDrone.Core.Datastore
return false;
}
public static bool IsSimpleType(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
type = type.GetGenericArguments()[0];
}
return type.IsPrimitive
|| type.IsEnum
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(Decimal);
}
private static bool IsReadable(this PropertyInfo propertyInfo)
{
return propertyInfo.CanRead && propertyInfo.GetGetMethod(false) != null;
}
private static bool IsWritable(this PropertyInfo propertyInfo)
{
return propertyInfo.CanWrite && propertyInfo.GetSetMethod(false) != null;
}
}
}

View File

@ -66,6 +66,7 @@
<option name="m_minLength" value="1" />
<option name="m_maxLength" value="32" />
</inspection_tool>
<inspection_tool class="LossyEncoding" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NestedAssignmentJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NestedFunctionCallJS" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="NestedSwitchStatementJS" enabled="true" level="WARNING" enabled_by_default="true" />
@ -85,6 +86,13 @@
</inspection_tool>
<inspection_tool class="SwitchStatementWithNoDefaultBranchJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TailRecursionJS" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TaskInspection" enabled="true" level="INFO" enabled_by_default="true">
<option name="suppressedTasks">
<set>
<option value="LESS" />
</set>
</option>
</inspection_tool>
<inspection_tool class="UnterminatedStatementJS" enabled="true" level="ERROR" enabled_by_default="true">
<option name="ignoreSemicolonAtEndOfBlock" value="true" />
</inspection_tool>

View File

@ -1,6 +1,6 @@
<td>{{{formatStatus status monitored}}}</td>
<td><a href="/series/details/{{id}}">{{title}}</a></td>
<td name="numberOfSeasons"></td>
<td name="seasonCount"></td>
<td name="qualityProfileName"></td>
<td name="network"></td>
<!-- If only DT could access the backbone model -->