Fix tests compilation errors

This commit is contained in:
Zoe Roux 2023-08-07 14:49:42 +09:00
parent 93b36f1bd4
commit 5446dbce83
No known key found for this signature in database
36 changed files with 326 additions and 1840 deletions

View File

@ -389,11 +389,23 @@ namespace Kyoo.Abstractions.Controllers
/// Edit a resource
/// </summary>
/// <param name="item">The resource to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <typeparam name="T">The type of resources</typeparam>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
Task<T> Edit<T>(T item, bool resetOld)
Task<T> Edit<T>(T item)
where T : class, IResource;
/// <summary>
/// Edit only specific properties of a resource
/// </summary>
/// <param name="id">The id of the resource to edit</param>
/// <param name="patch">
/// A method that will be called when you need to update every properties that you want to
/// persist. It can return false to abort the process via an ArgumentException
/// </param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
Task<T> Patch<T>(int id, Func<T, Task<bool>> patch)
where T : class, IResource;
/// <summary>

View File

@ -129,13 +129,24 @@ namespace Kyoo.Abstractions.Controllers
event ResourceEventHandler OnCreated;
/// <summary>
/// Edit a resource
/// Edit a resource and replace every property
/// </summary>
/// <param name="edited">The resource to edit, it's ID can't change.</param>
/// <param name="resetOld">Should old properties of the resource be discarded or should null values considered as not changed?</param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
Task<T> Edit(T edited, bool resetOld);
Task<T> Edit(T edited);
/// <summary>
/// Edit only specific properties of a resource
/// </summary>
/// <param name="id">The id of the resource to edit</param>
/// <param name="patch">
/// A method that will be called when you need to update every properties that you want to
/// persist. It can return false to abort the process via an ArgumentException
/// </param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
Task<T> Patch(int id, Func<T, Task<bool>> patch);
/// <summary>
/// Called when a resource has been edited.

View File

@ -16,33 +16,11 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Models;
namespace Kyoo.Tests.Database
public class PartialResource
{
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class GenreTests : AGenreTests
{
public GenreTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public int? Id { get; set; }
public abstract class AGenreTests : RepositoryTests<Genre>
{
[SuppressMessage("ReSharper", "NotAccessedField.Local")]
private readonly IGenreRepository _repository;
protected AGenreTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.GenreRepository;
}
}
public string? Slug { get; set; }
}

View File

@ -43,7 +43,7 @@ namespace Kyoo.Abstractions.Models
{
if (ShowSlug != null || Show?.Slug != null)
return GetSlug(ShowSlug ?? Show.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
return GetSlug(ShowID.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
}
[UsedImplicitly]
@ -81,17 +81,17 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The ID of the Show containing this episode.
/// </summary>
[SerializeIgnore] public int ShowID { get; set; }
[SerializeIgnore] public int ShowId { get; set; }
/// <summary>
/// The show that contains this episode. This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[LoadableRelation(nameof(ShowID))] public Show? Show { get; set; }
[LoadableRelation(nameof(ShowId))] public Show? Show { get; set; }
/// <summary>
/// The ID of the Season containing this episode.
/// </summary>
[SerializeIgnore] public int? SeasonID { get; set; }
[SerializeIgnore] public int? SeasonId { get; set; }
/// <summary>
/// The season that contains this episode.
@ -101,7 +101,7 @@ namespace Kyoo.Abstractions.Models
/// This can be null if the season is unknown and the episode is only identified
/// by it's <see cref="AbsoluteNumber"/>.
/// </remarks>
[LoadableRelation(nameof(SeasonID))] public Season? Season { get; set; }
[LoadableRelation(nameof(SeasonId))] public Season? Season { get; set; }
/// <summary>
/// The season in witch this episode is in.

View File

@ -42,7 +42,7 @@ namespace Kyoo.Abstractions.Models
get
{
if (ShowSlug == null && Show == null)
return $"{ShowID}-s{SeasonNumber}";
return $"{ShowId}-s{SeasonNumber}";
return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}";
}
@ -67,13 +67,13 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The ID of the Show containing this season.
/// </summary>
[SerializeIgnore] public int ShowID { get; set; }
[SerializeIgnore] public int ShowId { get; set; }
/// <summary>
/// The show that contains this season.
/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[LoadableRelation(nameof(ShowID))] public Show? Show { get; set; }
[LoadableRelation(nameof(ShowId))] public Show? Show { get; set; }
/// <summary>
/// The number of this season. This can be set to 0 to indicate specials.

View File

@ -51,7 +51,7 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The list of alternative titles of this show.
/// </summary>
public string[] Aliases { get; set; } = Array.Empty<string>();
public List<string> Aliases { get; set; } = new();
/// <summary>
/// The summary of this show.
@ -61,12 +61,12 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// A list of tags that match this movie.
/// </summary>
public string[] Tags { get; set; } = Array.Empty<string>();
public List<string> Tags { get; set; } = new();
/// <summary>
/// The list of genres (themes) this show has.
/// </summary>
public Genre[] Genres { get; set; } = Array.Empty<Genre>();
public List<Genre> Genres { get; set; } = new();
/// <summary>
/// Is this show airing, not aired yet or finished?
@ -106,13 +106,13 @@ namespace Kyoo.Abstractions.Models
/// <summary>
/// The ID of the Studio that made this show.
/// </summary>
[SerializeIgnore] public int? StudioID { get; set; }
[SerializeIgnore] public int? StudioId { get; set; }
/// <summary>
/// The Studio that made this show.
/// This must be explicitly loaded via a call to <see cref="ILibraryManager.Load"/>.
/// </summary>
[LoadableRelation(nameof(StudioID))][EditableRelation] public Studio? Studio { get; set; }
[LoadableRelation(nameof(StudioId))][EditableRelation] public Studio? Studio { get; set; }
/// <summary>
/// The list of people that made this show.

View File

@ -17,9 +17,7 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo.Utils
@ -29,125 +27,6 @@ namespace Kyoo.Utils
/// </summary>
public static class EnumerableExtensions
{
/// <summary>
/// A Select where the index of the item can be used.
/// </summary>
/// <param name="self">The IEnumerable to map. If self is null, an empty list is returned</param>
/// <param name="mapper">The function that will map each items</param>
/// <typeparam name="T">The type of items in <paramref name="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped.</returns>
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
[LinqTunnel]
public static IEnumerable<T2> Map<T, T2>(this IEnumerable<T> self,
Func<T, int, T2> mapper)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
static IEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, T2> mapper)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return mapper(enumerator.Current, index);
index++;
}
}
return Generator(self, mapper);
}
/// <summary>
/// A map where the mapping function is asynchronous.
/// Note: <see cref="SelectAsync{T,T2}"/> might interest you.
/// </summary>
/// <param name="self">The IEnumerable to map.</param>
/// <param name="mapper">The asynchronous function that will map each items.</param>
/// <typeparam name="T">The type of items in <paramref name="self"/>.</typeparam>
/// <typeparam name="T2">The type of items in the returned list.</typeparam>
/// <returns>The list mapped as an AsyncEnumerable.</returns>
/// <exception cref="ArgumentNullException">The list or the mapper can't be null.</exception>
[LinqTunnel]
public static IAsyncEnumerable<T2> MapAsync<T, T2>(this IEnumerable<T> self,
Func<T, int, Task<T2>> mapper)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, int, Task<T2>> mapper)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
int index = 0;
while (enumerator.MoveNext())
{
yield return await mapper(enumerator.Current, index);
index++;
}
}
return Generator(self, mapper);
}
/// <summary>
/// An asynchronous version of Select.
/// </summary>
/// <param name="self">The IEnumerable to map</param>
/// <param name="mapper">The asynchronous function that will map each items</param>
/// <typeparam name="T">The type of items in <paramref name="self"/></typeparam>
/// <typeparam name="T2">The type of items in the returned list</typeparam>
/// <returns>The list mapped as an AsyncEnumerable</returns>
/// <exception cref="ArgumentNullException">The list or the mapper can't be null</exception>
[LinqTunnel]
public static IAsyncEnumerable<T2> SelectAsync<T, T2>(this IEnumerable<T> self,
Func<T, Task<T2>> mapper)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
if (mapper == null)
throw new ArgumentNullException(nameof(mapper));
static async IAsyncEnumerable<T2> Generator(IEnumerable<T> self, Func<T, Task<T2>> mapper)
{
using IEnumerator<T> enumerator = self.GetEnumerator();
while (enumerator.MoveNext())
yield return await mapper(enumerator.Current);
}
return Generator(self, mapper);
}
/// <summary>
/// Convert an AsyncEnumerable to a List by waiting for every item.
/// </summary>
/// <param name="self">The async list</param>
/// <typeparam name="T">The type of items in the async list and in the returned list.</typeparam>
/// <returns>A task that will return a simple list</returns>
/// <exception cref="ArgumentNullException">The list can't be null</exception>
[LinqTunnel]
public static Task<List<T>> ToListAsync<T>(this IAsyncEnumerable<T> self)
{
if (self == null)
throw new ArgumentNullException(nameof(self));
static async Task<List<T>> ToList(IAsyncEnumerable<T> self)
{
List<T> ret = new();
await foreach (T i in self)
ret.Add(i);
return ret;
}
return ToList(self);
}
/// <summary>
/// If the enumerable is empty, execute an action.
/// </summary>
@ -197,104 +76,5 @@ namespace Kyoo.Utils
foreach (T i in self)
action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
public static void ForEach(this IEnumerable? self, Action<object> action)
{
if (self == null)
return;
foreach (object i in self)
action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task ForEachAsync(this IEnumerable? self, Func<object, Task> action)
{
if (self == null)
return;
foreach (object i in self)
await action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The asynchronous action to execute for each arguments</param>
/// <typeparam name="T">The type of items in the list.</typeparam>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task ForEachAsync<T>(this IEnumerable<T>? self, Func<T, Task> action)
{
if (self == null)
return;
foreach (T i in self)
await action(i);
}
/// <summary>
/// A foreach used as a function with a little specificity: the list can be null.
/// </summary>
/// <param name="self">The async list to enumerate. If this is null, the function result in a no-op</param>
/// <param name="action">The action to execute for each arguments</param>
/// <typeparam name="T">The type of items in the list.</typeparam>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task ForEachAsync<T>(this IAsyncEnumerable<T>? self, Action<T> action)
{
if (self == null)
return;
await foreach (T i in self)
action(i);
}
/// <summary>
/// Split a list in a small chunk of data.
/// </summary>
/// <param name="list">The list to split</param>
/// <param name="countPerList">The number of items in each chunk</param>
/// <typeparam name="T">The type of data in the initial list.</typeparam>
/// <returns>A list of chunks</returns>
[LinqTunnel]
public static IEnumerable<List<T>> BatchBy<T>(this List<T> list, int countPerList)
{
for (int i = 0; i < list.Count; i += countPerList)
yield return list.GetRange(i, Math.Min(list.Count - i, countPerList));
}
/// <summary>
/// Split a list in a small chunk of data.
/// </summary>
/// <param name="list">The list to split</param>
/// <param name="countPerList">The number of items in each chunk</param>
/// <typeparam name="T">The type of data in the initial list.</typeparam>
/// <returns>A list of chunks</returns>
[LinqTunnel]
public static IEnumerable<T[]> BatchBy<T>(this IEnumerable<T> list, int countPerList)
{
T[] ret = new T[countPerList];
int i = 0;
using IEnumerator<T> enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
ret[i] = enumerator.Current;
i++;
if (i < countPerList)
continue;
i = 0;
yield return ret;
}
Array.Resize(ref ret, i);
yield return ret;
}
}
}

View File

@ -17,13 +17,10 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
namespace Kyoo.Utils
@ -33,99 +30,9 @@ namespace Kyoo.Utils
/// </summary>
public static class Merger
{
/// <summary>
/// Merge two lists, can keep duplicates or remove them.
/// </summary>
/// <param name="first">The first enumerable to merge</param>
/// <param name="second">The second enumerable to merge, if items from this list are equals to one from the first, they are not kept</param>
/// <param name="isEqual">Equality function to compare items. If this is null, duplicated elements are kept</param>
/// <typeparam name="T">The type of items in the lists to merge.</typeparam>
/// <returns>The two list merged as an array</returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static T[] MergeLists<T>(IEnumerable<T>? first,
IEnumerable<T>? second,
Func<T, T, bool>? isEqual = null)
{
if (first == null)
return second?.ToArray();
if (second == null)
return first.ToArray();
if (isEqual == null)
return first.Concat(second).ToArray();
List<T> list = first.ToList();
return list.Concat(second.Where(x => !list.Any(y => isEqual(x, y)))).ToArray();
}
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
/// </summary>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
/// <seealso cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2},out bool)"/>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> MergeDictionaries<T, T2>(IDictionary<T, T2>? first,
IDictionary<T, T2>? second)
{
return MergeDictionaries(first, second, out bool _);
}
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the first one is kept.
/// </summary>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <param name="hasChanged">
/// <c>true</c> if a new items has been added to the dictionary, <c>false</c> otherwise.
/// </param>
/// <typeparam name="T">The type of the keys in dictionaries</typeparam>
/// <typeparam name="T2">The type of values in the dictionaries</typeparam>
/// <returns>The first dictionary with the missing elements of <paramref name="second"/>.</returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> MergeDictionaries<T, T2>(IDictionary<T, T2>? first,
IDictionary<T, T2>? second,
out bool hasChanged)
{
if (first == null)
{
hasChanged = true;
return second;
}
hasChanged = false;
if (second == null)
return first;
foreach ((T key, T2 value) in second)
{
bool success = first.TryAdd(key, value);
hasChanged |= success;
if (success || first[key]?.Equals(default) == false || value?.Equals(default) != false)
continue;
first[key] = value;
hasChanged = true;
}
return first;
}
/// <summary>
/// Merge two dictionary, if the same key is found on both dictionary, the values of the second one is kept.
/// </summary>
/// <remarks>
/// The only difference in this function compared to
/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2}, out bool)"/>
/// is the way <paramref name="hasChanged"/> is calculated and the order of the arguments.
/// <code>
/// MergeDictionaries(first, second);
/// </code>
/// will do the same thing as
/// <code>
/// CompleteDictionaries(second, first, out bool _);
/// </code>
/// </remarks>
/// <param name="first">The first dictionary to merge</param>
/// <param name="second">The second dictionary to merge</param>
/// <param name="hasChanged">
@ -138,7 +45,7 @@ namespace Kyoo.Utils
/// set to those of <paramref name="first"/>.
/// </returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static IDictionary<T, T2> CompleteDictionaries<T, T2>(IDictionary<T, T2>? first,
public static IDictionary<T, T2>? CompleteDictionaries<T, T2>(IDictionary<T, T2>? first,
IDictionary<T, T2>? second,
out bool hasChanged)
{
@ -160,14 +67,8 @@ namespace Kyoo.Utils
/// <summary>
/// Set every non-default values of seconds to the corresponding property of second.
/// Dictionaries are handled like anonymous objects with a property per key/pair value
/// (see
/// <see cref="MergeDictionaries{T,T2}(System.Collections.Generic.IDictionary{T,T2},System.Collections.Generic.IDictionary{T,T2})"/>
/// for more details).
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>
/// </summary>
/// <remarks>
/// This does the opposite of <see cref="Merge{T}"/>.
/// </remarks>
/// <example>
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "foo"}
/// </example>
@ -182,19 +83,16 @@ namespace Kyoo.Utils
/// </param>
/// <typeparam name="T">Fields of T will be completed</typeparam>
/// <returns><paramref name="first"/></returns>
/// <exception cref="ArgumentNullException">If first is null</exception>
public static T Complete<T>([NotNull] T first,
public static T Complete<T>(T first,
T? second,
[InstantHandle] Func<PropertyInfo, bool>? where = null)
{
if (first == null)
throw new ArgumentNullException(nameof(first));
if (second == null)
return first;
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
.Where(x => x is { CanRead: true, CanWrite: true }
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
if (where != null)
@ -202,17 +100,16 @@ namespace Kyoo.Utils
foreach (PropertyInfo property in properties)
{
object value = property.GetValue(second);
object defaultValue = property.GetCustomAttribute<DefaultValueAttribute>()?.Value
?? property.PropertyType.GetClrDefault();
object? value = property.GetValue(second);
if (value?.Equals(defaultValue) != false || value.Equals(property.GetValue(first)))
if (value?.Equals(property.GetValue(first)) == true)
continue;
if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
{
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
.GenericTypeArguments;
object[] parameters =
object?[] parameters =
{
property.GetValue(first),
value,
@ -222,8 +119,8 @@ namespace Kyoo.Utils
typeof(Merger),
nameof(CompleteDictionaries),
dictionaryTypes,
parameters);
if ((bool)parameters[2])
parameters)!;
if ((bool)parameters[2]!)
property.SetValue(first, newDictionary);
}
else
@ -234,109 +131,5 @@ namespace Kyoo.Utils
merge.OnMerge(second);
return first;
}
/// <summary>
/// This will set missing values of <paramref name="first"/> to the corresponding values of <paramref name="second"/>.
/// Enumerable will be merged (concatenated) and Dictionaries too.
/// At the end, the OnMerge method of first will be called if first is a <see cref="IOnMerge"/>.
/// </summary>
/// <example>
/// {id: 0, slug: "test"}, {id: 4, slug: "foo"} -> {id: 4, slug: "test"}
/// </example>
/// <param name="first">
/// The object to complete
/// </param>
/// <param name="second">
/// Missing fields of first will be completed by fields of this item. If second is null, the function no-op.
/// </param>
/// <param name="where">
/// Filter fields that will be merged
/// </param>
/// <typeparam name="T">Fields of T will be merged</typeparam>
/// <returns><paramref name="first"/></returns>
[ContractAnnotation("first:notnull => notnull; second:notnull => notnull", true)]
public static T Merge<T>(T? first,
T? second,
[InstantHandle] Func<PropertyInfo, bool>? where = null)
{
if (first == null)
return second;
if (second == null)
return first;
Type type = typeof(T);
IEnumerable<PropertyInfo> properties = type.GetProperties()
.Where(x => x.CanRead && x.CanWrite
&& Attribute.GetCustomAttribute(x, typeof(NotMergeableAttribute)) == null);
if (where != null)
properties = properties.Where(where);
foreach (PropertyInfo property in properties)
{
object oldValue = property.GetValue(first);
object newValue = property.GetValue(second);
object defaultValue = property.PropertyType.GetClrDefault();
if (oldValue?.Equals(defaultValue) != false)
property.SetValue(first, newValue);
else if (Utility.IsOfGenericType(property.PropertyType, typeof(IDictionary<,>)))
{
Type[] dictionaryTypes = Utility.GetGenericDefinition(property.PropertyType, typeof(IDictionary<,>))
.GenericTypeArguments;
object[] parameters =
{
oldValue,
newValue,
false
};
object newDictionary = Utility.RunGenericMethod<object>(
typeof(Merger),
nameof(MergeDictionaries),
dictionaryTypes,
parameters);
if ((bool)parameters[2])
property.SetValue(first, newDictionary);
}
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)
&& property.PropertyType != typeof(string))
{
Type enumerableType = Utility.GetGenericDefinition(property.PropertyType, typeof(IEnumerable<>))
.GenericTypeArguments
.First();
Func<IResource, IResource, bool> equalityComparer = enumerableType.IsAssignableTo(typeof(IResource))
? (x, y) => x.Slug == y.Slug
: null;
property.SetValue(first, Utility.RunGenericMethod<object>(
typeof(Merger),
nameof(MergeLists),
enumerableType,
oldValue, newValue, equalityComparer));
}
}
if (first is IOnMerge merge)
merge.OnMerge(second);
return first;
}
/// <summary>
/// Set every fields of <paramref name="obj"/> to the default value.
/// </summary>
/// <param name="obj">The object to nullify</param>
/// <typeparam name="T">Fields of T will be nullified</typeparam>
/// <returns><paramref name="obj"/></returns>
public static T Nullify<T>(T obj)
{
Type type = typeof(T);
foreach (PropertyInfo property in type.GetProperties())
{
if (!property.CanWrite || property.GetCustomAttribute<ComputedAttribute>() != null)
continue;
property.SetValue(obj, property.PropertyType.GetClrDefault());
}
return obj;
}
}
}

View File

@ -18,7 +18,6 @@
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Kyoo.Utils
{
@ -49,37 +48,5 @@ namespace Kyoo.Utils
return x.Result;
}, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
/// Map the result of a task to another result.
/// </summary>
/// <param name="task">The task to map.</param>
/// <param name="map">The mapper method, it take the task's result as a parameter and should return the new result.</param>
/// <typeparam name="T">The type of returns of the given task</typeparam>
/// <typeparam name="TResult">The resulting task after the mapping method</typeparam>
/// <returns>A task wrapping the initial task and mapping the initial result.</returns>
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
public static Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> map)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
return map(x.Result);
}, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
/// A method to return the a default value from a task if the initial task is null.
/// </summary>
/// <param name="value">The initial task</param>
/// <typeparam name="T">The type that the task will return</typeparam>
/// <returns>A non-null task.</returns>
public static Task<T> DefaultIfNull<T>(Task<T>? value)
{
return value ?? Task.FromResult<T>(default);
}
}
}

View File

@ -41,8 +41,6 @@ namespace Kyoo.Utils
/// <returns>True if the expression is a member, false otherwise</returns>
public static bool IsPropertyExpression(LambdaExpression ex)
{
if (ex == null)
return false;
return ex.Body is MemberExpression
|| (ex.Body.NodeType == ExpressionType.Convert && ((UnaryExpression)ex.Body).Operand is MemberExpression);
}
@ -57,7 +55,7 @@ namespace Kyoo.Utils
{
if (!IsPropertyExpression(ex))
throw new ArgumentException($"{ex} is not a property expression.");
MemberExpression member = ex.Body.NodeType == ExpressionType.Convert
MemberExpression? member = ex.Body.NodeType == ExpressionType.Convert
? ((UnaryExpression)ex.Body).Operand as MemberExpression
: ex.Body as MemberExpression;
return member!.Member.Name;
@ -92,18 +90,6 @@ namespace Kyoo.Utils
return str;
}
/// <summary>
/// Get the default value of a type.
/// </summary>
/// <param name="type">The type to get the default value</param>
/// <returns>The default value of the given type.</returns>
public static object GetClrDefault(this Type type)
{
return type.IsValueType
? Activator.CreateInstance(type)
: null;
}
/// <summary>
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
/// </summary>
@ -194,13 +180,6 @@ namespace Kyoo.Utils
Type[] generics,
object[] args)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (generics == null)
throw new ArgumentNullException(nameof(generics));
if (args == null)
throw new ArgumentNullException(nameof(args));
MethodInfo[] methods = type.GetMethods(flag | BindingFlags.Public)
.Where(x => x.Name == name)
.Where(x => x.GetGenericArguments().Length == generics.Length)
@ -295,12 +274,11 @@ namespace Kyoo.Utils
/// <returns>The return of the method you wanted to run.</returns>
/// <seealso cref="RunGenericMethod{T}(object,string,System.Type[],object[])"/>
/// <seealso cref="RunGenericMethod{T}(System.Type,string,System.Type,object[])"/>
[PublicAPI]
public static T RunGenericMethod<T>(
public static T? RunGenericMethod<T>(
Type owner,
string methodName,
Type[] types,
params object[] args)
params object?[] args)
{
if (owner == null)
throw new ArgumentNullException(nameof(owner));
@ -311,7 +289,7 @@ namespace Kyoo.Utils
if (types.Length < 1)
throw new ArgumentException($"The {nameof(types)} array is empty. At least one type is needed.");
MethodInfo method = GetMethod(owner, BindingFlags.Static, methodName, types, args);
return (T)method.MakeGenericMethod(types).Invoke(null, args);
return (T?)method.MakeGenericMethod(types).Invoke(null, args);
}
/// <summary>

View File

@ -27,6 +27,7 @@ using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Authentication.Models;
using Kyoo.Authentication.Models.DTO;
using Kyoo.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
@ -229,7 +230,7 @@ namespace Kyoo.Authentication.Views
try
{
user.Id = userID;
return await _users.Edit(user, true);
return await _users.Edit(user);
}
catch (ItemNotFoundException)
{
@ -252,14 +253,15 @@ namespace Kyoo.Authentication.Views
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
public async Task<ActionResult<User>> PatchMe(User user)
public async Task<ActionResult<User>> PatchMe(PartialResource user)
{
if (!int.TryParse(User.FindFirstValue(Claims.Id), out int userID))
return Unauthorized(new RequestError("User not authenticated or token invalid."));
try
{
user.Id = userID;
return await _users.Edit(user, false);
if (user.Id.HasValue && user.Id != userID)
throw new ArgumentException("Can't edit your user id.");
return await _users.Patch(userID, TryUpdateModelAsync);
}
catch (ItemNotFoundException)
{

View File

@ -284,12 +284,12 @@ namespace Kyoo.Core.Controllers
(Show s, nameof(Show.Seasons)) => _SetRelation(s,
SeasonRepository.GetAll(x => x.Show.Id == obj.Id),
(x, y) => x.Seasons = y,
(x, y) => { x.Show = y; x.ShowID = y.Id; }),
(x, y) => { x.Show = y; x.ShowId = y.Id; }),
(Show s, nameof(Show.Episodes)) => _SetRelation(s,
EpisodeRepository.GetAll(x => x.Show.Id == obj.Id),
(x, y) => x.Episodes = y,
(x, y) => { x.Show = y; x.ShowID = y.Id; }),
(x, y) => { x.Show = y; x.ShowId = y.Id; }),
(Show s, nameof(Show.Collections)) => CollectionRepository
.GetAll(x => x.Shows.Any(y => y.Id == obj.Id))
@ -300,21 +300,21 @@ namespace Kyoo.Core.Controllers
.Then(x =>
{
s.Studio = x;
s.StudioID = x?.Id ?? 0;
s.StudioId = x?.Id ?? 0;
}),
(Season s, nameof(Season.Episodes)) => _SetRelation(s,
EpisodeRepository.GetAll(x => x.Season.Id == obj.Id),
(x, y) => x.Episodes = y,
(x, y) => { x.Season = y; x.SeasonID = y.Id; }),
(x, y) => { x.Season = y; x.SeasonId = y.Id; }),
(Season s, nameof(Season.Show)) => ShowRepository
.GetOrDefault(x => x.Seasons.Any(y => y.Id == obj.Id))
.Then(x =>
{
s.Show = x;
s.ShowID = x?.Id ?? 0;
s.ShowId = x?.Id ?? 0;
}),
@ -323,7 +323,7 @@ namespace Kyoo.Core.Controllers
.Then(x =>
{
e.Show = x;
e.ShowID = x?.Id ?? 0;
e.ShowId = x?.Id ?? 0;
}),
(Episode e, nameof(Episode.Season)) => SeasonRepository
@ -331,18 +331,18 @@ namespace Kyoo.Core.Controllers
.Then(x =>
{
e.Season = x;
e.SeasonID = x?.Id ?? 0;
e.SeasonId = x?.Id ?? 0;
}),
(Episode e, nameof(Episode.PreviousEpisode)) => EpisodeRepository
.GetAll(
where: x => x.ShowID == e.ShowID,
where: x => x.ShowId == e.ShowId,
limit: new Pagination(1, e.Id, true)
).Then(x => e.PreviousEpisode = x.FirstOrDefault()),
(Episode e, nameof(Episode.NextEpisode)) => EpisodeRepository
.GetAll(
where: x => x.ShowID == e.ShowID,
where: x => x.ShowId == e.ShowId,
limit: new Pagination(1, e.Id)
).Then(x => e.NextEpisode = x.FirstOrDefault()),
@ -438,10 +438,17 @@ namespace Kyoo.Core.Controllers
}
/// <inheritdoc />
public Task<T> Edit<T>(T item, bool resetOld)
public Task<T> Edit<T>(T item)
where T : class, IResource
{
return GetRepository<T>().Edit(item, resetOld);
return GetRepository<T>().Edit(item);
}
/// <inheritdoc />
public Task<T> Patch<T>(int id, Func<T, Task<bool>> patch)
where T : class, IResource
{
return GetRepository<T>().Patch(id, patch);
}
/// <inheritdoc />

View File

@ -64,7 +64,7 @@ namespace Kyoo.Core.Controllers
// Edit episode slugs when the show's slug changes.
shows.OnEdited += (show) =>
{
List<Episode> episodes = _database.Episodes.AsTracking().Where(x => x.ShowID == show.Id).ToList();
List<Episode> episodes = _database.Episodes.AsTracking().Where(x => x.ShowId == show.Id).ToList();
foreach (Episode ep in episodes)
{
ep.ShowSlug = show.Slug;
@ -77,7 +77,7 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc />
public Task<Episode> GetOrDefault(int showID, int seasonNumber, int episodeNumber)
{
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.SeasonNumber == seasonNumber
&& x.EpisodeNumber == episodeNumber);
}
@ -111,7 +111,7 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc />
public Task<Episode> GetAbsolute(int showID, int absoluteNumber)
{
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowID == showID
return _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.AbsoluteNumber == absoluteNumber);
}
@ -142,12 +142,12 @@ namespace Kyoo.Core.Controllers
public override async Task<Episode> Create(Episode obj)
{
await base.Create(obj);
obj.ShowSlug = obj.Show?.Slug ?? _database.Shows.First(x => x.Id == obj.ShowID).Slug;
obj.ShowSlug = obj.Show?.Slug ?? _database.Shows.First(x => x.Id == obj.ShowId).Slug;
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() =>
obj.SeasonNumber != null && obj.EpisodeNumber != null
? Get(obj.ShowID, obj.SeasonNumber.Value, obj.EpisodeNumber.Value)
: GetAbsolute(obj.ShowID, obj.AbsoluteNumber.Value));
? Get(obj.ShowId, obj.SeasonNumber.Value, obj.EpisodeNumber.Value)
: GetAbsolute(obj.ShowId, obj.AbsoluteNumber.Value));
OnResourceCreated(obj);
return obj;
}
@ -156,14 +156,14 @@ namespace Kyoo.Core.Controllers
protected override async Task Validate(Episode resource)
{
await base.Validate(resource);
if (resource.ShowID <= 0)
if (resource.ShowId <= 0)
{
if (resource.Show == null)
{
throw new ArgumentException($"Can't store an episode not related " +
$"to any show (showID: {resource.ShowID}).");
$"to any show (showID: {resource.ShowId}).");
}
resource.ShowID = resource.Show.Id;
resource.ShowId = resource.Show.Id;
}
}
@ -173,12 +173,12 @@ namespace Kyoo.Core.Controllers
if (obj == null)
throw new ArgumentNullException(nameof(obj));
int epCount = await _database.Episodes.Where(x => x.ShowID == obj.ShowID).Take(2).CountAsync();
int epCount = await _database.Episodes.Where(x => x.ShowId == obj.ShowId).Take(2).CountAsync();
_database.Entry(obj).State = EntityState.Deleted;
await _database.SaveChangesAsync();
await base.Delete(obj);
if (epCount == 1)
await _shows.Delete(obj.ShowID);
await _shows.Delete(obj.ShowId);
}
}
}

View File

@ -100,7 +100,11 @@ namespace Kyoo.Core.Controllers
=> throw new InvalidOperationException();
/// <inheritdoc />
public override Task<ILibraryItem> Edit(ILibraryItem obj, bool resetOld)
public override Task<ILibraryItem> Edit(ILibraryItem obj)
=> throw new InvalidOperationException();
/// <inheritdoc />
public override Task<ILibraryItem> Patch(int id, Func<ILibraryItem, Task<bool>> patch)
=> throw new InvalidOperationException();
/// <inheritdoc />

View File

@ -144,7 +144,7 @@ namespace Kyoo.Core.Controllers
/// <param name="reference">The reference item (the AfterID query)</param>
/// <param name="next">True if the following page should be returned, false for the previous.</param>
/// <returns>An expression ready to be added to a Where close of a sorted query to handle the AfterID</returns>
protected Expression<Func<T, bool>> KeysetPaginatate(
protected Expression<Func<T, bool>> KeysetPaginate(
Sort<T> sort,
T reference,
bool next = true)
@ -155,22 +155,22 @@ namespace Kyoo.Core.Controllers
ParameterExpression x = Expression.Parameter(typeof(T), "x");
ConstantExpression referenceC = Expression.Constant(reference, typeof(T));
IEnumerable<Sort<T>.By> _GetSortsBy(Sort<T> sort)
IEnumerable<Sort<T>.By> GetSortsBy(Sort<T> sort)
{
return sort switch
{
Sort<T>.Default => _GetSortsBy(DefaultSort),
Sort<T>.Default => GetSortsBy(DefaultSort),
Sort<T>.By @sortBy => new[] { sortBy },
Sort<T>.Conglomerate(var list) => list.SelectMany(_GetSortsBy),
Sort<T>.Conglomerate(var list) => list.SelectMany(GetSortsBy),
_ => Array.Empty<Sort<T>.By>(),
};
}
// Don't forget that every sorts must end with a ID sort (to differenciate equalities).
// Don't forget that every sorts must end with a ID sort (to differentiate equalities).
Sort<T>.By id = new(x => x.Id);
IEnumerable<Sort<T>.By> sorts = _GetSortsBy(sort).Append(id);
IEnumerable<Sort<T>.By> sorts = GetSortsBy(sort).Append(id);
BinaryExpression filter = null;
BinaryExpression? filter = null;
List<Sort<T>.By> previousSteps = new();
// TODO: Add an outer query >= for perf
// PERF: See https://use-the-index-luke.com/sql/partial-results/fetch-next-page#sb-equivalent-logic
@ -180,9 +180,9 @@ namespace Kyoo.Core.Controllers
PropertyInfo property = typeof(T).GetProperty(key);
// Comparing a value with null always return false so we short opt < > comparisons with null.
if (property.GetValue(reference) == null)
if (property!.GetValue(reference) == null)
{
previousSteps.Add(new(key, desc));
previousSteps.Add(new Sort<T>.By(key, desc));
continue;
}
@ -206,7 +206,7 @@ namespace Kyoo.Core.Controllers
// Comparing a value with null always return false for nulls so we must add nulls to the results manually.
// Postgres sorts them after values so we will do the same
// We only add this condition if the collumn type is nullable
// We only add this condition if the column type is nullable
if (Nullable.GetUnderlyingType(property.PropertyType) != null)
{
BinaryExpression equalNull = Expression.Equal(xkey, Expression.Constant(null));
@ -223,7 +223,7 @@ namespace Kyoo.Core.Controllers
previousSteps.Add(new(key, desc));
}
return Expression.Lambda<Func<T, bool>>(filter, x);
return Expression.Lambda<Func<T, bool>>(filter!, x);
}
/// <summary>
@ -316,7 +316,7 @@ namespace Kyoo.Core.Controllers
if (limit?.AfterID != null)
{
T reference = await Get(limit.AfterID.Value);
query = query.Where(KeysetPaginatate(sort, reference, !limit.Reverse));
query = query.Where(KeysetPaginate(sort, reference, !limit.Reverse));
}
if (limit?.Reverse == true)
query = query.Reverse();
@ -343,12 +343,9 @@ namespace Kyoo.Core.Controllers
await Validate(obj);
if (obj is IThumbnails thumbs)
{
if (thumbs.Poster != null)
Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry.State = EntityState.Added;
if (thumbs.Thumbnail != null)
Database.Entry(thumbs).Reference(x => x.Thumbnail).TargetEntry.State = EntityState.Added;
if (thumbs.Logo != null)
Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry.State = EntityState.Added;
Database.Entry(thumbs).Reference(x => x.Poster).IsModified = thumbs.Poster != null;
Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified = thumbs.Thumbnail != null;
Database.Entry(thumbs).Reference(x => x.Logo).IsModified = thumbs.Logo != null;
}
return obj;
}
@ -376,9 +373,6 @@ namespace Kyoo.Core.Controllers
{
try
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
T old = await GetOrDefault(obj.Slug);
if (old != null)
return old;
@ -392,21 +386,16 @@ namespace Kyoo.Core.Controllers
}
/// <inheritdoc/>
public virtual async Task<T> Edit(T edited, bool resetOld)
public virtual async Task<T> Edit(T edited)
{
if (edited == null)
throw new ArgumentNullException(nameof(edited));
bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled;
Database.ChangeTracker.LazyLoadingEnabled = false;
try
{
T old = await GetWithTracking(edited.Id);
if (resetOld)
old = Merger.Nullify(old);
Merger.Complete(old, edited, x => x.GetCustomAttribute<LoadableRelationAttribute>() == null);
await EditRelations(old, edited, resetOld);
await EditRelations(old, edited, true);
await Database.SaveChangesAsync();
OnEdited?.Invoke(old);
return old;
@ -418,6 +407,28 @@ namespace Kyoo.Core.Controllers
}
}
/// <inheritdoc/>
public virtual async Task<T> Patch(int id, Func<T, Task<bool>> patch)
{
bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled;
Database.ChangeTracker.LazyLoadingEnabled = false;
try
{
T resource = await GetWithTracking(id);
if (!await patch(resource))
throw new ArgumentException("Could not patch resource");
await Database.SaveChangesAsync();
OnEdited?.Invoke(resource);
return resource;
}
finally
{
Database.ChangeTracker.LazyLoadingEnabled = lazyLoading;
Database.ChangeTracker.Clear();
}
}
/// <summary>
/// An overridable method to edit relation of a resource.
/// </summary>
@ -434,12 +445,11 @@ namespace Kyoo.Core.Controllers
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
protected virtual Task EditRelations(T resource, T changed, bool resetOld)
{
if (resource is IThumbnails thumbs)
if (resource is IThumbnails thumbs && changed is IThumbnails chng)
{
// FIXME: I think this throws if poster is set to null.
Database.Entry(thumbs).Reference(x => x.Poster).TargetEntry.State = EntityState.Modified;
Database.Entry(thumbs).Reference(x => x.Thumbnail).TargetEntry.State = EntityState.Modified;
Database.Entry(thumbs).Reference(x => x.Logo).TargetEntry.State = EntityState.Modified;
Database.Entry(thumbs).Reference(x => x.Poster).IsModified = thumbs.Poster != chng.Poster;
Database.Entry(thumbs).Reference(x => x.Thumbnail).IsModified = thumbs.Thumbnail != chng.Thumbnail;
Database.Entry(thumbs).Reference(x => x.Logo).IsModified = thumbs.Logo != chng.Logo;
}
return Validate(resource);
}

View File

@ -55,7 +55,7 @@ namespace Kyoo.Core.Controllers
// Edit seasons slugs when the show's slug changes.
shows.OnEdited += (show) =>
{
List<Season> seasons = _database.Seasons.AsTracking().Where(x => x.ShowID == show.Id).ToList();
List<Season> seasons = _database.Seasons.AsTracking().Where(x => x.ShowId == show.Id).ToList();
foreach (Season season in seasons)
{
season.ShowSlug = show.Slug;
@ -86,7 +86,7 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc/>
public Task<Season> GetOrDefault(int showID, int seasonNumber)
{
return _database.Seasons.FirstOrDefaultAsync(x => x.ShowID == showID
return _database.Seasons.FirstOrDefaultAsync(x => x.ShowId == showID
&& x.SeasonNumber == seasonNumber);
}
@ -112,9 +112,9 @@ namespace Kyoo.Core.Controllers
public override async Task<Season> Create(Season obj)
{
await base.Create(obj);
obj.ShowSlug = _database.Shows.First(x => x.Id == obj.ShowID).Slug;
obj.ShowSlug = _database.Shows.First(x => x.Id == obj.ShowId).Slug;
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(obj.ShowID, obj.SeasonNumber));
await _database.SaveChangesAsync(() => Get(obj.ShowId, obj.SeasonNumber));
OnResourceCreated(obj);
return obj;
}
@ -123,14 +123,14 @@ namespace Kyoo.Core.Controllers
protected override async Task Validate(Season resource)
{
await base.Validate(resource);
if (resource.ShowID <= 0)
if (resource.ShowId <= 0)
{
if (resource.Show == null)
{
throw new ArgumentException($"Can't store a season not related to any show " +
$"(showID: {resource.ShowID}).");
$"(showID: {resource.ShowId}).");
}
resource.ShowID = resource.Show.Id;
resource.ShowId = resource.Show.Id;
}
}

View File

@ -97,7 +97,7 @@ namespace Kyoo.Core.Controllers
if (resource.Studio != null)
{
resource.Studio = await _studios.CreateIfNotExists(resource.Studio);
resource.StudioID = resource.Studio.Id;
resource.StudioId = resource.Studio.Id;
}
if (resource.People != null)

View File

@ -16,12 +16,14 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -162,11 +164,11 @@ namespace Kyoo.Core.Api
public async Task<ActionResult<T>> Edit([FromBody] T resource)
{
if (resource.Id > 0)
return await Repository.Edit(resource, true);
return await Repository.Edit(resource);
T old = await Repository.Get(resource.Slug);
resource.Id = old.Id;
return await Repository.Edit(resource, true);
return await Repository.Edit(resource);
}
/// <summary>
@ -185,14 +187,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> Patch([FromBody] T resource)
public async Task<ActionResult<T>> Patch([FromBody] PartialResource resource)
{
if (resource.Id > 0)
return await Repository.Edit(resource, false);
if (resource.Id.HasValue)
return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync);
if (resource.Slug == null)
throw new ArgumentException("Either the Id or the slug of the resource has to be defined to edit it.");
T old = await Repository.Get(resource.Slug);
resource.Id = old.Id;
return await Repository.Edit(resource, false);
return await Repository.Patch(old.Id, TryUpdateModelAsync);
}
/// <summary>

View File

@ -83,7 +83,7 @@ namespace Kyoo.Core.Api
[FromQuery] Pagination pagination)
{
ICollection<Show> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioID, x => x.Studio.Slug)),
ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio.Slug)),
Sort<Show>.From(sortBy),
pagination
);

View File

@ -84,7 +84,7 @@ namespace Kyoo.Core.Api
[FromQuery] Pagination pagination)
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonID, x => x.Season.Slug)),
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season.Slug)),
Sort<Episode>.From(sortBy),
pagination
);

View File

@ -86,7 +86,7 @@ namespace Kyoo.Core.Api
[FromQuery] Pagination pagination)
{
ICollection<Season> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)),
ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowId, x => x.Show.Slug)),
Sort<Season>.From(sortBy),
pagination
);
@ -121,7 +121,7 @@ namespace Kyoo.Core.Api
[FromQuery] Pagination pagination)
{
ICollection<Episode> resources = await _libraryManager.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowID, x => x.Show.Slug)),
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show.Slug)),
Sort<Episode>.From(sortBy),
pagination
);

View File

@ -306,13 +306,13 @@ namespace Kyoo.Postgresql
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Season>()
.HasIndex(x => new { x.ShowID, x.SeasonNumber })
.HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber })
.IsUnique();
modelBuilder.Entity<Season>()
.HasIndex(x => x.Slug)
.IsUnique();
modelBuilder.Entity<Episode>()
.HasIndex(x => new { x.ShowID, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber })
.HasIndex(x => new { ShowID = x.ShowId, x.SeasonNumber, x.EpisodeNumber, x.AbsoluteNumber })
.IsUnique();
modelBuilder.Entity<Episode>()
.HasIndex(x => x.Slug)

View File

@ -140,16 +140,10 @@ namespace Kyoo.Tests.Database
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<T>()));
}
[Fact]
public async Task EditNullTest()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Edit(null!, false));
}
[Fact]
public async Task EditNonExistingTest()
{
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T { Id = 56 }, false));
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Edit(new T { Id = 56 }));
}
[Fact]
@ -170,12 +164,6 @@ namespace Kyoo.Tests.Database
await Assert.ThrowsAsync<ItemNotFoundException>(() => _repository.Get(x => x.Slug == "non-existing"));
}
[Fact]
public async Task GetExpressionNullTest()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => _repository.Get((Expression<Func<T, bool>>)null!));
}
[Fact]
public async Task GetOrDefaultTest()
{

View File

@ -66,29 +66,19 @@ namespace Kyoo.Tests.Database
Assert.Equal("2!", ret.Slug);
}
[Fact]
public async Task CreateWithoutNameTest()
{
Collection collection = TestSample.GetNew<Collection>();
collection.Name = null;
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(collection));
}
[Fact]
public async Task CreateWithExternalIdTest()
{
Collection collection = TestSample.GetNew<Collection>();
collection.ExternalId = new[]
collection.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["1"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
new MetadataId
["2"] = new()
{
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link",
DataId = "new-id"
}
@ -96,7 +86,6 @@ namespace Kyoo.Tests.Database
await _repository.Create(collection);
Collection retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalId);
Assert.Equal(2, retrieved.ExternalId.Count);
KAssert.DeepEqual(collection.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(collection.ExternalId.Last(), retrieved.ExternalId.Last());
@ -107,11 +96,8 @@ namespace Kyoo.Tests.Database
{
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.Name = "New Title";
value.Images = new Dictionary<int, string>
{
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
value.Poster = new Image("new-poster");
await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections.FirstAsync();
@ -123,21 +109,19 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest()
{
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.ExternalId = new[]
value.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["test"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
};
await _repository.Edit(value, false);
await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
@ -147,40 +131,36 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest()
{
Collection value = await _repository.Get(TestSample.Get<Collection>().Slug);
value.ExternalId = new List<MetadataId>
value.ExternalId = new Dictionary<string, MetadataId>
{
new()
["toto"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
};
await _repository.Edit(value, false);
await _repository.Edit(value);
{
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalId.Add(new MetadataId
value.ExternalId.Add("test", new MetadataId
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
DataId = "id"
});
await _repository.Edit(value, false);
await _repository.Edit(value);
{
await using DatabaseContext database = Repositories.Context.New();
Collection retrieved = await database.Collections
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
KAssert.DeepEqual(value, retrieved);

View File

@ -57,10 +57,10 @@ namespace Kyoo.Tests.Database
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e1", episode.Slug);
Show show = new()
{
Id = episode.ShowID,
Id = episode.ShowId,
Slug = "new-slug"
};
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
await Repositories.LibraryManager.ShowRepository.Edit(show);
episode = await _repository.Get(1);
Assert.Equal("new-slug-s1e1", episode.Slug);
}
@ -74,8 +74,8 @@ namespace Kyoo.Tests.Database
{
Id = 1,
SeasonNumber = 2,
ShowID = 1
}, false);
ShowId = 1
});
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2e1", episode.Slug);
@ -90,8 +90,8 @@ namespace Kyoo.Tests.Database
{
Id = 1,
EpisodeNumber = 2,
ShowID = 1
}, false);
ShowId = 1
});
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
@ -102,7 +102,7 @@ namespace Kyoo.Tests.Database
{
Episode episode = await _repository.Create(new Episode
{
ShowID = TestSample.Get<Show>().Id,
ShowId = TestSample.Get<Show>().Id,
SeasonNumber = 2,
EpisodeNumber = 4
});
@ -129,10 +129,10 @@ namespace Kyoo.Tests.Database
Episode episode = await _repository.Create(TestSample.GetAbsoluteEpisode());
Show show = new()
{
Id = episode.ShowID,
Id = episode.ShowId,
Slug = "new-slug"
};
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
await Repositories.LibraryManager.ShowRepository.Edit(show);
episode = await _repository.Get(2);
Assert.Equal($"new-slug-3", episode.Slug);
}
@ -145,8 +145,8 @@ namespace Kyoo.Tests.Database
{
Id = 2,
AbsoluteNumber = 56,
ShowID = 1
}, false);
ShowId = 1
});
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
episode = await _repository.Get(2);
Assert.Equal($"{TestSample.Get<Show>().Slug}-56", episode.Slug);
@ -161,8 +161,8 @@ namespace Kyoo.Tests.Database
Id = 2,
SeasonNumber = 1,
EpisodeNumber = 2,
ShowID = 1
}, false);
ShowId = 1
});
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
episode = await _repository.Get(2);
Assert.Equal($"{TestSample.Get<Show>().Slug}-s1e2", episode.Slug);
@ -174,49 +174,25 @@ namespace Kyoo.Tests.Database
Episode episode = await _repository.Get(1);
episode.SeasonNumber = null;
episode.AbsoluteNumber = 12;
episode = await _repository.Edit(episode, true);
episode = await _repository.Edit(episode);
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
episode = await _repository.Get(1);
Assert.Equal($"{TestSample.Get<Show>().Slug}-12", episode.Slug);
}
[Fact]
public async Task MovieEpisodeTest()
{
Episode episode = await _repository.Create(TestSample.GetMovieEpisode());
Assert.Equal(TestSample.Get<Show>().Slug, episode.Slug);
episode = await _repository.Get(3);
Assert.Equal(TestSample.Get<Show>().Slug, episode.Slug);
}
[Fact]
public async Task MovieEpisodeEditTest()
{
await _repository.Create(TestSample.GetMovieEpisode());
await Repositories.LibraryManager.Edit(new Show
{
Id = 1,
Slug = "john-wick"
}, false);
Episode episode = await _repository.Get(3);
Assert.Equal("john-wick", episode.Slug);
}
[Fact]
public async Task CreateWithExternalIdTest()
{
Episode value = TestSample.GetNew<Episode>();
value.ExternalId = new[]
value.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["2"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
new MetadataId
["3"] = new()
{
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link",
DataId = "new-id"
}
@ -224,7 +200,6 @@ namespace Kyoo.Tests.Database
await _repository.Create(value);
Episode retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalId);
Assert.Equal(2, retrieved.ExternalId.Count);
KAssert.DeepEqual(value.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(value.ExternalId.Last(), retrieved.ExternalId.Last());
@ -235,11 +210,8 @@ namespace Kyoo.Tests.Database
{
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.Name = "New Title";
value.Images = new Dictionary<int, string>
{
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
value.Poster = new Image("poster");
await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes.FirstAsync();
@ -251,22 +223,18 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest()
{
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.ExternalId = new[]
value.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["1"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
};
await _repository.Edit(value, false);
await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
Episode retrieved = await database.Episodes.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
@ -275,41 +243,33 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest()
{
Episode value = await _repository.Get(TestSample.Get<Episode>().Slug);
value.ExternalId = new List<MetadataId>
value.ExternalId = new Dictionary<string, MetadataId>
{
new()
["toto"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
};
await _repository.Edit(value, false);
await _repository.Edit(value);
{
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
Episode retrieved = await database.Episodes.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalId.Add(new MetadataId
value.ExternalId.Add("test", new MetadataId
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
DataId = "id"
});
await _repository.Edit(value, false);
await _repository.Edit(value);
{
await using DatabaseContext database = Repositories.Context.New();
Episode retrieved = await database.Episodes
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
Episode retrieved = await database.Episodes.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
@ -326,7 +286,7 @@ namespace Kyoo.Tests.Database
Episode value = new()
{
Name = "This is a test super title",
ShowID = 1,
ShowId = 1,
AbsoluteNumber = 2
};
await _repository.Create(value);
@ -343,8 +303,8 @@ namespace Kyoo.Tests.Database
Episode expected = TestSample.Get<Episode>();
expected.Id = 0;
expected.ShowID = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
expected.SeasonID = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
await _repository.Create(expected);
KAssert.DeepEqual(expected, await _repository.Get(expected.Slug));
}
@ -355,8 +315,8 @@ namespace Kyoo.Tests.Database
Episode expected = TestSample.Get<Episode>();
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(TestSample.Get<Episode>()));
await _repository.Delete(TestSample.Get<Episode>());
expected.ShowID = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
expected.SeasonID = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
expected.ShowId = (await Repositories.LibraryManager.ShowRepository.Create(TestSample.Get<Show>())).Id;
expected.SeasonId = (await Repositories.LibraryManager.SeasonRepository.Create(TestSample.Get<Season>())).Id;
KAssert.DeepEqual(expected, await _repository.CreateIfNotExists(expected));
}
}

View File

@ -1,98 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class LibraryItemTest : ALibraryItemTest
{
public LibraryItemTest(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ALibraryItemTest
{
private readonly ILibraryItemRepository _repository;
private readonly RepositoryActivator _repositories;
protected ALibraryItemTest(RepositoryActivator repositories)
{
_repositories = repositories;
_repository = repositories.LibraryManager.LibraryItemRepository;
}
[Fact]
public async Task CountTest()
{
Assert.Equal(2, await _repository.GetCount());
}
[Fact]
public async Task GetShowTests()
{
LibraryItem expected = new(TestSample.Get<Show>());
LibraryItem actual = await _repository.Get(1);
KAssert.DeepEqual(expected, actual);
}
[Fact]
public async Task GetCollectionTests()
{
LibraryItem expected = new(TestSample.Get<Collection>());
LibraryItem actual = await _repository.Get(-1);
KAssert.DeepEqual(expected, actual);
}
[Fact]
public async Task GetShowSlugTests()
{
LibraryItem expected = new(TestSample.Get<Show>());
LibraryItem actual = await _repository.Get(TestSample.Get<Show>().Slug);
KAssert.DeepEqual(expected, actual);
}
[Fact]
public async Task GetCollectionSlugTests()
{
LibraryItem expected = new(TestSample.Get<Collection>());
LibraryItem actual = await _repository.Get(TestSample.Get<Collection>().Slug);
KAssert.DeepEqual(expected, actual);
}
[Fact]
public async Task GetDuplicatedSlugTests()
{
await _repositories.LibraryManager.Create(new Collection
{
Slug = TestSample.Get<Show>().Slug,
Name = "name"
});
await Assert.ThrowsAsync<InvalidOperationException>(() => _repository.Get(TestSample.Get<Show>().Slug));
}
}
}

View File

@ -1,169 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Postgresql;
using Kyoo.Utils;
using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class LibraryTests : ALibraryTests
{
public LibraryTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class ALibraryTests : RepositoryTests<Library>
{
private readonly ILibraryRepository _repository;
protected ALibraryTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.LibraryRepository;
}
[Fact]
public async Task CreateWithoutPathTest()
{
Library library = TestSample.GetNew<Library>();
library.Paths = null;
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(library));
}
[Fact]
public async Task CreateWithEmptySlugTest()
{
Library library = TestSample.GetNew<Library>();
library.Slug = string.Empty;
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(library));
}
[Fact]
public async Task CreateWithNumberSlugTest()
{
Library library = TestSample.GetNew<Library>();
library.Slug = "2";
Library ret = await _repository.Create(library);
Assert.Equal("2!", ret.Slug);
}
[Fact]
public async Task CreateWithoutNameTest()
{
Library library = TestSample.GetNew<Library>();
library.Name = null;
await Assert.ThrowsAsync<ArgumentException>(() => _repository.Create(library));
}
[Fact]
public async Task CreateWithProvider()
{
Library library = TestSample.GetNew<Library>();
library.Providers = new[] { TestSample.Get<Provider>() };
await _repository.Create(library);
Library retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.Providers);
Assert.Single(retrieved.Providers);
Assert.Equal(TestSample.Get<Provider>().Slug, retrieved.Providers.First().Slug);
}
[Fact]
public async Task EditTest()
{
Library value = await _repository.Get(TestSample.Get<Library>().Slug);
value.Paths = new[] { "/super", "/test" };
value.Name = "New Title";
Library edited = await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Library show = await database.Libraries.FirstAsync();
KAssert.DeepEqual(show, edited);
}
[Fact]
public async Task EditProvidersTest()
{
Library value = await _repository.Get(TestSample.Get<Library>().Slug);
value.Providers = new[]
{
TestSample.GetNew<Provider>()
};
Library edited = await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Library show = await database.Libraries
.Include(x => x.Providers)
.FirstAsync();
show.Providers.ForEach(x => x.Libraries = null);
edited.Providers.ForEach(x => x.Libraries = null);
KAssert.DeepEqual(show, edited);
}
[Fact]
public async Task AddProvidersTest()
{
Library value = await _repository.Get(TestSample.Get<Library>().Slug);
await Repositories.LibraryManager.Load(value, x => x.Providers);
value.Providers.Add(TestSample.GetNew<Provider>());
Library edited = await _repository.Edit(value, false);
await using DatabaseContext database = Repositories.Context.New();
Library show = await database.Libraries
.Include(x => x.Providers)
.FirstAsync();
show.Providers.ForEach(x => x.Libraries = null);
edited.Providers.ForEach(x => x.Libraries = null);
KAssert.DeepEqual(show, edited);
}
[Theory]
[InlineData("test")]
[InlineData("super")]
[InlineData("title")]
[InlineData("TiTlE")]
[InlineData("SuPeR")]
public async Task SearchTest(string query)
{
Library value = new()
{
Slug = "super-test",
Name = "This is a test title",
Paths = new[] { "path" }
};
await _repository.Create(value);
ICollection<Library> ret = await _repository.Search(query);
KAssert.DeepEqual(value, ret.First());
}
}
}

View File

@ -52,17 +52,15 @@ namespace Kyoo.Tests.Database
public async Task CreateWithExternalIdTest()
{
People value = TestSample.GetNew<People>();
value.ExternalId = new[]
value.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["2"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
new MetadataId
["1"] = new()
{
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link",
DataId = "new-id"
}
@ -70,7 +68,6 @@ namespace Kyoo.Tests.Database
await _repository.Create(value);
People retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalId);
Assert.Equal(2, retrieved.ExternalId.Count);
KAssert.DeepEqual(value.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(value.ExternalId.Last(), retrieved.ExternalId.Last());
@ -81,11 +78,8 @@ namespace Kyoo.Tests.Database
{
People value = await _repository.Get(TestSample.Get<People>().Slug);
value.Name = "New Name";
value.Images = new Dictionary<int, string>
{
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
value.Poster = new Image("poster");
await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People.FirstAsync();
@ -97,22 +91,18 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest()
{
People value = await _repository.Get(TestSample.Get<People>().Slug);
value.ExternalId = new[]
value.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["toto"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
};
await _repository.Edit(value, false);
await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
People retrieved = await database.People.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
@ -121,41 +111,32 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest()
{
People value = await _repository.Get(TestSample.Get<People>().Slug);
value.ExternalId = new List<MetadataId>
value.ExternalId = new Dictionary<string, MetadataId>
{
new()
["1"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
};
await _repository.Edit(value, false);
await _repository.Edit(value);
{
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
People retrieved = await database.People.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalId.Add(new MetadataId
value.ExternalId.Add("toto", new MetadataId
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
DataId = "id"
});
await _repository.Edit(value, false);
await _repository.Edit(value);
{
await using DatabaseContext database = Repositories.Context.New();
People retrieved = await database.People
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
People retrieved = await database.People.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}

View File

@ -1,48 +0,0 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Xunit;
using Xunit.Abstractions;
namespace Kyoo.Tests.Database
{
namespace PostgreSQL
{
[Collection(nameof(Postgresql))]
public class ProviderTests : AProviderTests
{
public ProviderTests(PostgresFixture postgres, ITestOutputHelper output)
: base(new RepositoryActivator(output, postgres)) { }
}
}
public abstract class AProviderTests : RepositoryTests<Provider>
{
[SuppressMessage("ReSharper", "NotAccessedField.Local")]
private readonly IProviderRepository _repository;
protected AProviderTests(RepositoryActivator repositories)
: base(repositories)
{
_repository = Repositories.LibraryManager.ProviderRepository;
}
}
}

View File

@ -55,10 +55,10 @@ namespace Kyoo.Tests.Database
Assert.Equal("anohana-s1", season.Slug);
Show show = new()
{
Id = season.ShowID,
Id = season.ShowId,
Slug = "new-slug"
};
await Repositories.LibraryManager.ShowRepository.Edit(show, false);
await Repositories.LibraryManager.ShowRepository.Edit(show);
season = await _repository.Get(1);
Assert.Equal("new-slug-s1", season.Slug);
}
@ -72,8 +72,8 @@ namespace Kyoo.Tests.Database
{
Id = 1,
SeasonNumber = 2,
ShowID = 1
}, false);
ShowId = 1
});
season = await _repository.Get(1);
Assert.Equal("anohana-s2", season.Slug);
}
@ -83,7 +83,7 @@ namespace Kyoo.Tests.Database
{
Season season = await _repository.Create(new Season
{
ShowID = TestSample.Get<Show>().Id,
ShowId = TestSample.Get<Show>().Id,
SeasonNumber = 2
});
Assert.Equal($"{TestSample.Get<Show>().Slug}-s2", season.Slug);
@ -93,17 +93,15 @@ namespace Kyoo.Tests.Database
public async Task CreateWithExternalIdTest()
{
Season season = TestSample.GetNew<Season>();
season.ExternalId = new[]
season.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["2"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
new MetadataId
["1"] = new()
{
Provider = TestSample.GetNew<Provider>(),
Link = "new-provider-link",
DataId = "new-id"
}
@ -111,7 +109,6 @@ namespace Kyoo.Tests.Database
await _repository.Create(season);
Season retrieved = await _repository.Get(2);
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalId);
Assert.Equal(2, retrieved.ExternalId.Count);
KAssert.DeepEqual(season.ExternalId.First(), retrieved.ExternalId.First());
KAssert.DeepEqual(season.ExternalId.Last(), retrieved.ExternalId.Last());
@ -122,11 +119,8 @@ namespace Kyoo.Tests.Database
{
Season value = await _repository.Get(TestSample.Get<Season>().Slug);
value.Name = "New Title";
value.Images = new Dictionary<int, string>
{
[Images.Poster] = "new-poster"
};
await _repository.Edit(value, false);
value.Poster = new Image("test");
await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New();
Season retrieved = await database.Seasons.FirstAsync();
@ -138,22 +132,18 @@ namespace Kyoo.Tests.Database
public async Task EditMetadataTest()
{
Season value = await _repository.Get(TestSample.Get<Season>().Slug);
value.ExternalId = new[]
value.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["toto"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
};
await _repository.Edit(value, false);
await _repository.Edit(value);
await using DatabaseContext database = Repositories.Context.New();
Season retrieved = await database.Seasons
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
Season retrieved = await database.Seasons.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
@ -162,41 +152,33 @@ namespace Kyoo.Tests.Database
public async Task AddMetadataTest()
{
Season value = await _repository.Get(TestSample.Get<Season>().Slug);
value.ExternalId = new List<MetadataId>
value.ExternalId = new Dictionary<string, MetadataId>
{
new()
["1"] = new()
{
Provider = TestSample.Get<Provider>(),
Link = "link",
DataId = "id"
},
};
await _repository.Edit(value, false);
await _repository.Edit(value);
{
await using DatabaseContext database = Repositories.Context.New();
Season retrieved = await database.Seasons
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
Season retrieved = await database.Seasons.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
value.ExternalId.Add(new MetadataId
value.ExternalId.Add("toto", new MetadataId
{
Provider = TestSample.GetNew<Provider>(),
Link = "link",
DataId = "id"
});
await _repository.Edit(value, false);
await _repository.Edit(value);
{
await using DatabaseContext database = Repositories.Context.New();
Season retrieved = await database.Seasons
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
Season retrieved = await database.Seasons.FirstAsync();
KAssert.DeepEqual(value, retrieved);
}
@ -213,7 +195,7 @@ namespace Kyoo.Tests.Database
Season value = new()
{
Name = "This is a test super title",
ShowID = 1
ShowId = 1
};
await _repository.Create(value);
ICollection<Season> ret = await _repository.Search(query);

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -55,9 +54,8 @@ namespace Kyoo.Tests.Database
public async Task EditTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Path = "/super";
value.Name = "New Title";
Show edited = await _repository.Edit(value, false);
Show edited = await _repository.Edit(value);
KAssert.DeepEqual(value, edited);
await using DatabaseContext database = Repositories.Context.New();
@ -70,11 +68,11 @@ namespace Kyoo.Tests.Database
public async Task EditGenreTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Genres = new[] { new Genre("test") };
Show edited = await _repository.Edit(value, false);
value.Genres = new List<Genre> { Genre.Action };
Show edited = await _repository.Edit(value);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(value.Genres.Select(x => new { x.Slug, x.Name }), edited.Genres.Select(x => new { x.Slug, x.Name }));
Assert.Equal(value.Genres, edited.Genres);
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
@ -82,19 +80,18 @@ namespace Kyoo.Tests.Database
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(value.Genres.Select(x => new { x.Slug, x.Name }), show.Genres.Select(x => new { x.Slug, x.Name }));
Assert.Equal(value.Genres, show.Genres);
}
[Fact]
public async Task AddGenreTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
await Repositories.LibraryManager.Load(value, x => x.Genres);
value.Genres.Add(new Genre("test"));
Show edited = await _repository.Edit(value, false);
value.Genres.Add(Genre.Drama);
Show edited = await _repository.Edit(value);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(value.Genres.Select(x => new { x.Slug, x.Name }), edited.Genres.Select(x => new { x.Slug, x.Name }));
Assert.Equal(value.Genres, edited.Genres);
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
@ -102,7 +99,7 @@ namespace Kyoo.Tests.Database
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(value.Genres.Select(x => new { x.Slug, x.Name }), show.Genres.Select(x => new { x.Slug, x.Name }));
Assert.Equal(value.Genres, show.Genres);
}
[Fact]
@ -110,10 +107,10 @@ namespace Kyoo.Tests.Database
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Studio = new Studio("studio");
Show edited = await _repository.Edit(value, false);
Show edited = await _repository.Edit(value);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal("studio", edited.Studio.Slug);
Assert.Equal("studio", edited.Studio!.Slug);
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
@ -121,15 +118,15 @@ namespace Kyoo.Tests.Database
.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal("studio", show.Studio.Slug);
Assert.Equal("studio", show.Studio!.Slug);
}
[Fact]
public async Task EditAliasesTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.Aliases = new[] { "NiceNewAlias", "SecondAlias" };
Show edited = await _repository.Edit(value, false);
value.Aliases = new List<string>() { "NiceNewAlias", "SecondAlias" };
Show edited = await _repository.Edit(value);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(value.Aliases, edited.Aliases);
@ -156,10 +153,10 @@ namespace Kyoo.Tests.Database
Role = "NiceCharacter"
}
};
Show edited = await _repository.Edit(value, false);
Show edited = await _repository.Edit(value);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(edited.People.First().ShowID, value.Id);
Assert.Equal(edited.People!.First().ShowID, value.Id);
Assert.Equal(
value.People.Select(x => new { x.Role, x.Slug, x.People.Name }),
edited.People.Select(x => new { x.Role, x.Slug, x.People.Name }));
@ -173,62 +170,30 @@ namespace Kyoo.Tests.Database
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(
value.People.Select(x => new { x.Role, x.Slug, x.People.Name }),
show.People.Select(x => new { x.Role, x.Slug, x.People.Name }));
show.People!.Select(x => new { x.Role, x.Slug, x.People.Name }));
}
[Fact]
public async Task EditExternalIDsTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
value.ExternalId = new[]
value.ExternalId = new Dictionary<string, MetadataId>()
{
new MetadataId
["test"] = new()
{
Provider = new Provider("test", "test.png"),
DataId = "1234"
}
};
Show edited = await _repository.Edit(value, false);
Show edited = await _repository.Edit(value);
Assert.Equal(value.Slug, edited.Slug);
Assert.Equal(
value.ExternalId.Select(x => new { x.DataID, x.Provider.Slug }),
edited.ExternalId.Select(x => new { x.DataID, x.Provider.Slug }));
Assert.Equal(value.ExternalId, edited.ExternalId);
await using DatabaseContext database = Repositories.Context.New();
Show show = await database.Shows
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync();
Show show = await database.Shows.FirstAsync();
Assert.Equal(value.Slug, show.Slug);
Assert.Equal(
value.ExternalId.Select(x => new { x.DataID, x.Provider.Slug }),
show.ExternalId.Select(x => new { x.DataID, x.Provider.Slug }));
}
[Fact]
public async Task EditResetOldTest()
{
Show value = await _repository.Get(TestSample.Get<Show>().Slug);
Show newValue = new()
{
Id = value.Id,
Slug = "reset",
Name = "Reset"
};
Show edited = await _repository.Edit(newValue, true);
Assert.Equal(value.Id, edited.Id);
Assert.Null(edited.Overview);
Assert.Equal("reset", edited.Slug);
Assert.Equal("Reset", edited.Name);
Assert.Null(edited.Aliases);
Assert.Null(edited.ExternalId);
Assert.Null(edited.People);
Assert.Null(edited.Genres);
Assert.Null(edited.Studio);
Assert.Equal(value.ExternalId, show.ExternalId);
}
[Fact]
@ -237,22 +202,14 @@ namespace Kyoo.Tests.Database
Show expected = TestSample.Get<Show>();
expected.Id = 0;
expected.Slug = "created-relation-test";
expected.ExternalId = new[]
expected.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["test"] = new()
{
Provider = new Provider("provider", "provider.png"),
DataId = "ID"
}
};
expected.Genres = new[]
{
new Genre
{
Name = "Genre",
Slug = "genre"
}
};
expected.Genres = new List<Genre>() { Genre.Action };
expected.People = new[]
{
new PeopleRole
@ -270,7 +227,6 @@ namespace Kyoo.Tests.Database
await using DatabaseContext context = Repositories.Context.New();
Show retrieved = await context.Shows
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.Include(x => x.Genres)
.Include(x => x.People)
.ThenInclude(x => x.People)
@ -281,10 +237,7 @@ namespace Kyoo.Tests.Database
x.Show = null;
x.People.Roles = null;
});
retrieved.Studio.Shows = null;
retrieved.Genres.ForEach(x => x.Shows = null);
expected.Genres.ForEach(x => x.Shows = null);
retrieved.Studio!.Shows = null;
expected.People.ForEach(x =>
{
x.Show = null;
@ -300,11 +253,10 @@ namespace Kyoo.Tests.Database
Show expected = TestSample.Get<Show>();
expected.Id = 0;
expected.Slug = "created-relation-test";
expected.ExternalId = new[]
expected.ExternalId = new Dictionary<string, MetadataId>
{
new MetadataId
["test"] = new()
{
Provider = TestSample.Get<Provider>(),
DataId = "ID"
}
};
@ -313,11 +265,10 @@ namespace Kyoo.Tests.Database
await using DatabaseContext context = Repositories.Context.New();
Show retrieved = await context.Shows
.Include(x => x.ExternalId)
.ThenInclude(x => x.Provider)
.FirstAsync(x => x.Id == created.Id);
KAssert.DeepEqual(expected, retrieved);
Assert.Single(retrieved.ExternalId);
Assert.Equal("ID", retrieved.ExternalId.First().DataID);
Assert.Equal("ID", retrieved.ExternalId["test"].DataId);
}
[Fact]
@ -362,25 +313,12 @@ namespace Kyoo.Tests.Database
await Repositories.LibraryManager.Load(show, x => x.Seasons);
await Repositories.LibraryManager.Load(show, x => x.Episodes);
Assert.Equal(1, await _repository.GetCount());
Assert.Single(show.Seasons);
Assert.Single(show.Episodes);
Assert.Single(show.Seasons!);
Assert.Single(show.Episodes!);
await _repository.Delete(show);
Assert.Equal(0, await Repositories.LibraryManager.ShowRepository.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.SeasonRepository.GetCount());
Assert.Equal(0, await Repositories.LibraryManager.EpisodeRepository.GetCount());
}
[Fact]
public async Task AddShowLinkTest()
{
await Repositories.LibraryManager.Create(TestSample.GetNew<Library>());
await _repository.AddShowLink(1, 2, null);
await using DatabaseContext context = Repositories.Context.New();
Show show = context.Shows
.Include(x => x.Libraries)
.First(x => x.ID == 1);
Assert.Contains(2, show.Libraries.Select(x => x.ID));
}
}
}

View File

@ -35,10 +35,7 @@ namespace Kyoo.Tests
Slug = "new-collection",
Name = "New Collection",
Overview = "A collection created by new sample",
Images = new Dictionary<int, string>
{
[Images.Thumbnail] = "thumbnail"
}
Thumbnail = new Image("thumbnail")
}
},
{
@ -52,13 +49,9 @@ namespace Kyoo.Tests
Status = Status.Planned,
StartAir = new DateTime(2011, 1, 1).ToUniversalTime(),
EndAir = new DateTime(2011, 1, 1).ToUniversalTime(),
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster",
[Images.Logo] = "Logo",
[Images.Thumbnail] = "Thumbnail"
},
IsMovie = false,
Poster = new Image("Poster"),
Logo = new Image("Logo"),
Thumbnail = new Image("Thumbnail"),
Studio = null
}
},
@ -67,17 +60,14 @@ namespace Kyoo.Tests
() => new Season
{
Id = 2,
ShowID = 1,
ShowId = 1,
ShowSlug = Get<Show>().Slug,
Name = "New season",
Overview = "New overview",
EndDate = new DateTime(2000, 10, 10).ToUniversalTime(),
SeasonNumber = 2,
StartDate = new DateTime(2010, 10, 10).ToUniversalTime(),
Images = new Dictionary<int, string>
{
[Images.Logo] = "logo"
}
Logo = new Image("logo")
}
},
{
@ -85,9 +75,9 @@ namespace Kyoo.Tests
() => new Episode
{
Id = 2,
ShowID = 1,
ShowId = 1,
ShowSlug = Get<Show>().Slug,
SeasonID = 1,
SeasonId = 1,
SeasonNumber = Get<Season>().SeasonNumber,
EpisodeNumber = 3,
AbsoluteNumber = 4,
@ -95,23 +85,7 @@ namespace Kyoo.Tests
Name = "New Episode Title",
ReleaseDate = new DateTime(2000, 10, 10).ToUniversalTime(),
Overview = "new episode overview",
Images = new Dictionary<int, string>
{
[Images.Logo] = "new episode logo"
}
}
},
{
typeof(Provider),
() => new Provider
{
ID = 2,
Slug = "new-provider",
Name = "Provider NewSample",
Images = new Dictionary<int, string>
{
[Images.Logo] = "logo"
}
Logo = new Image("new episode logo")
}
},
{
@ -121,27 +95,14 @@ namespace Kyoo.Tests
Id = 2,
Slug = "new-person-name",
Name = "New person name",
Images = new Dictionary<int, string>
{
[Images.Logo] = "Old Logo",
[Images.Poster] = "Old poster"
}
Logo = new Image("Old Logo"),
Poster = new Image("Old poster")
}
}
};
private static readonly Dictionary<Type, Func<object>> Samples = new()
{
{
typeof(Library),
() => new Library
{
ID = 1,
Slug = "deck",
Name = "Deck",
Paths = new[] { "/path/to/deck" }
}
},
{
typeof(Collection),
() => new Collection
@ -150,10 +111,7 @@ namespace Kyoo.Tests
Slug = "collection",
Name = "Collection",
Overview = "A nice collection for tests",
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster"
}
Poster = new Image("Poster")
}
},
{
@ -163,7 +121,7 @@ namespace Kyoo.Tests
Id = 1,
Slug = "anohana",
Name = "Anohana: The Flower We Saw That Day",
Aliases = new[]
Aliases = new List<string>
{
"Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai.",
"AnoHana",
@ -173,16 +131,12 @@ namespace Kyoo.Tests
"In time, however, these childhood friends drifted apart, and when they became high " +
"school students, they had long ceased to think of each other as friends.",
Status = Status.Finished,
StudioID = 1,
StudioId = 1,
StartAir = new DateTime(2011, 1, 1).ToUniversalTime(),
EndAir = new DateTime(2011, 1, 1).ToUniversalTime(),
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster",
[Images.Logo] = "Logo",
[Images.Thumbnail] = "Thumbnail"
},
IsMovie = false,
Poster = new Image("Poster"),
Logo = new Image("Logo"),
Thumbnail = new Image("Thumbnail"),
Studio = null
}
},
@ -192,18 +146,15 @@ namespace Kyoo.Tests
{
Id = 1,
ShowSlug = "anohana",
ShowID = 1,
ShowId = 1,
SeasonNumber = 1,
Name = "Season 1",
Overview = "The first season",
StartDate = new DateTime(2020, 06, 05).ToUniversalTime(),
EndDate = new DateTime(2020, 07, 05).ToUniversalTime(),
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster",
[Images.Logo] = "Logo",
[Images.Thumbnail] = "Thumbnail"
},
Poster = new Image("Poster"),
Logo = new Image("Logo"),
Thumbnail = new Image("Thumbnail")
}
},
{
@ -212,18 +163,15 @@ namespace Kyoo.Tests
{
Id = 1,
ShowSlug = "anohana",
ShowID = 1,
SeasonID = 1,
ShowId = 1,
SeasonId = 1,
SeasonNumber = 1,
EpisodeNumber = 1,
AbsoluteNumber = 1,
Path = "/home/kyoo/anohana-s1e1",
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster",
[Images.Logo] = "Logo",
[Images.Thumbnail] = "Thumbnail"
},
Poster = new Image("Poster"),
Logo = new Image("Logo"),
Thumbnail = new Image("Thumbnail"),
Name = "Episode 1",
Overview = "Summary of the first episode",
ReleaseDate = new DateTime(2020, 06, 05).ToUniversalTime()
@ -236,12 +184,9 @@ namespace Kyoo.Tests
Id = 1,
Slug = "the-actor",
Name = "The Actor",
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster",
[Images.Logo] = "Logo",
[Images.Thumbnail] = "Thumbnail"
},
Poster = new Image("Poster"),
Logo = new Image("Logo"),
Thumbnail = new Image("Thumbnail")
}
},
{
@ -253,30 +198,6 @@ namespace Kyoo.Tests
Name = "Hyper studio",
}
},
{
typeof(Genre),
() => new Genre
{
ID = 1,
Slug = "action",
Name = "Action"
}
},
{
typeof(Provider),
() => new Provider
{
ID = 1,
Slug = "tvdb",
Name = "The TVDB",
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster",
[Images.Logo] = "path/tvdb.svg",
[Images.Thumbnail] = "Thumbnail"
}
}
},
{
typeof(User),
() => new User
@ -309,20 +230,20 @@ namespace Kyoo.Tests
Show show = Get<Show>();
show.Id = 0;
show.StudioID = 0;
show.StudioId = 0;
context.Shows.Add(show);
Season season = Get<Season>();
season.Id = 0;
season.ShowID = 0;
season.ShowId = 0;
season.Show = show;
context.Seasons.Add(season);
Episode episode = Get<Episode>();
episode.Id = 0;
episode.ShowID = 0;
episode.ShowId = 0;
episode.Show = show;
episode.SeasonID = 0;
episode.SeasonId = 0;
episode.Season = season;
context.Episodes.Add(episode);
@ -331,20 +252,10 @@ namespace Kyoo.Tests
studio.Shows = new List<Show> { show };
context.Studios.Add(studio);
Genre genre = Get<Genre>();
genre.ID = 0;
genre.Shows = new List<Show> { show };
context.Genres.Add(genre);
People people = Get<People>();
people.Id = 0;
context.People.Add(people);
Library library = Get<Library>();
library.ID = 0;
library.Collections = new List<Collection> { collection };
context.Libraries.Add(library);
User user = Get<User>();
user.Id = 0;
context.Users.Add(user);
@ -358,41 +269,18 @@ namespace Kyoo.Tests
{
Id = 2,
ShowSlug = "anohana",
ShowID = 1,
ShowId = 1,
SeasonNumber = null,
EpisodeNumber = null,
AbsoluteNumber = 3,
Path = "/home/kyoo/anohana-3",
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster",
[Images.Logo] = "Logo",
[Images.Thumbnail] = "Thumbnail"
},
Poster = new Image("Poster"),
Logo = new Image("Logo"),
Thumbnail = new Image("Thumbnail"),
Name = "Episode 3",
Overview = "Summary of the third absolute episode",
ReleaseDate = new DateTime(2020, 06, 05).ToUniversalTime()
};
}
public static Episode GetMovieEpisode()
{
return new()
{
Id = 3,
ShowSlug = "anohana",
ShowID = 1,
Path = "/home/kyoo/john-wick",
Images = new Dictionary<int, string>
{
[Images.Poster] = "Poster",
[Images.Logo] = "Logo",
[Images.Thumbnail] = "Thumbnail"
},
Name = "John wick",
Overview = "A movie episode test",
ReleaseDate = new DateTime(1595, 05, 12).ToUniversalTime()
};
}
}
}

View File

@ -17,9 +17,7 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Kyoo.Utils;
using Xunit;
@ -27,53 +25,6 @@ namespace Kyoo.Tests.Utility
{
public class EnumerableTests
{
[Fact]
public void MapTest()
{
int[] list = { 1, 2, 3, 4 };
Assert.All(list.Map((x, i) => (x, i)), x => Assert.Equal(x.x - 1, x.i));
Assert.Throws<ArgumentNullException>(() => list.Map(((Func<int, int, int>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.Map((x, _) => x + 1));
}
[Fact]
public async Task MapAsyncTest()
{
int[] list = { 1, 2, 3, 4 };
await foreach ((int x, int i) in list.MapAsync((x, i) => Task.FromResult((x, i))))
{
Assert.Equal(x - 1, i);
}
Assert.Throws<ArgumentNullException>(() => list.MapAsync(((Func<int, int, Task<int>>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.MapAsync((x, _) => Task.FromResult(x + 1)));
}
[Fact]
public async Task SelectAsyncTest()
{
int[] list = { 1, 2, 3, 4 };
int i = 2;
await foreach (int x in list.SelectAsync(x => Task.FromResult(x + 1)))
{
Assert.Equal(i++, x);
}
Assert.Throws<ArgumentNullException>(() => list.SelectAsync(((Func<int, Task<int>>)null)!));
list = null;
Assert.Throws<ArgumentNullException>(() => list!.SelectAsync(x => Task.FromResult(x + 1)));
}
[Fact]
public async Task ToListAsyncTest()
{
int[] expected = { 1, 2, 3, 4 };
IAsyncEnumerable<int> list = expected.SelectAsync(Task.FromResult);
Assert.Equal(expected, await list.ToListAsync());
list = null;
await Assert.ThrowsAsync<ArgumentNullException>(() => list!.ToListAsync());
}
[Fact]
public void IfEmptyTest()
{

View File

@ -16,13 +16,9 @@
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils;
using Xunit;
@ -30,318 +26,21 @@ namespace Kyoo.Tests.Utility
{
public class MergerTests
{
[Fact]
public void NullifyTest()
{
Genre genre = new("test")
{
ID = 5
};
Merger.Nullify(genre);
Assert.Equal(0, genre.ID);
Assert.Null(genre.Name);
Assert.Null(genre.Slug);
}
[Fact]
public void MergeTest()
{
Genre genre = new()
{
ID = 5
};
Genre genre2 = new()
{
Name = "test"
};
Genre ret = Merger.Merge(genre, genre2);
Assert.True(ReferenceEquals(genre, ret));
Assert.Equal(5, ret.ID);
Assert.Equal("test", genre.Name);
Assert.Null(genre.Slug);
}
[Fact]
[SuppressMessage("ReSharper", "ExpressionIsAlwaysNull")]
public void MergeNullTests()
{
Genre genre = new()
{
ID = 5
};
Assert.True(ReferenceEquals(genre, Merger.Merge(genre, null)));
Assert.True(ReferenceEquals(genre, Merger.Merge(null, genre)));
Assert.Null(Merger.Merge<Genre>(null, null));
}
private class TestIOnMerge : IOnMerge
{
public void OnMerge(object other)
{
Exception exception = new();
exception.Data[0] = other;
throw exception;
}
}
[Fact]
public void OnMergeTest()
{
TestIOnMerge test = new();
TestIOnMerge test2 = new();
Assert.Throws<Exception>(() => Merger.Merge(test, test2));
try
{
Merger.Merge(test, test2);
}
catch (Exception ex)
{
Assert.True(ReferenceEquals(test2, ex.Data[0]));
}
}
private class Test
{
public int ID { get; set; }
public int[] Numbers { get; set; }
}
[Fact]
public void GlobalMergeListTest()
{
Test test = new()
{
ID = 5,
Numbers = new[] { 1 }
};
Test test2 = new()
{
Numbers = new[] { 3 }
};
Test ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(2, ret.Numbers.Length);
Assert.Equal(1, ret.Numbers[0]);
Assert.Equal(3, ret.Numbers[1]);
}
[Fact]
public void GlobalMergeListDuplicatesTest()
{
Test test = new()
{
ID = 5,
Numbers = new[] { 1 }
};
Test test2 = new()
{
Numbers = new[]
{
1,
3,
3
}
};
Test ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(4, ret.Numbers.Length);
Assert.Equal(1, ret.Numbers[0]);
Assert.Equal(1, ret.Numbers[1]);
Assert.Equal(3, ret.Numbers[2]);
Assert.Equal(3, ret.Numbers[3]);
}
private class MergeDictionaryTest
{
public int ID { get; set; }
public Dictionary<int, string> Dictionary { get; set; }
}
[Fact]
public void GlobalMergeDictionariesTest()
{
MergeDictionaryTest test = new()
{
ID = 5,
Dictionary = new Dictionary<int, string>
{
[2] = "two"
}
};
MergeDictionaryTest test2 = new()
{
Dictionary = new Dictionary<int, string>
{
[3] = "third"
}
};
MergeDictionaryTest ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(2, ret.Dictionary.Count);
Assert.Equal("two", ret.Dictionary[2]);
Assert.Equal("third", ret.Dictionary[3]);
}
[Fact]
public void GlobalMergeDictionariesDuplicatesTest()
{
MergeDictionaryTest test = new()
{
ID = 5,
Dictionary = new Dictionary<int, string>
{
[2] = "two"
}
};
MergeDictionaryTest test2 = new()
{
Dictionary = new Dictionary<int, string>
{
[2] = "nope",
[3] = "third"
}
};
MergeDictionaryTest ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(2, ret.Dictionary.Count);
Assert.Equal("two", ret.Dictionary[2]);
Assert.Equal("third", ret.Dictionary[3]);
}
[Fact]
public void GlobalMergeListDuplicatesResourcesTest()
{
Show test = new()
{
Id = 5,
Genres = new[] { new Genre("test") }
};
Show test2 = new()
{
Genres = new[]
{
new Genre("test"),
new Genre("test2")
}
};
Show ret = Merger.Merge(test, test2);
Assert.True(ReferenceEquals(test, ret));
Assert.Equal(5, ret.Id);
Assert.Equal(2, ret.Genres.Count);
Assert.Equal("test", ret.Genres.ToArray()[0].Slug);
Assert.Equal("test2", ret.Genres.ToArray()[1].Slug);
}
[Fact]
public void MergeListTest()
{
int[] first = { 1 };
int[] second = { 3, 3 };
int[] ret = Merger.MergeLists(first, second);
Assert.Equal(3, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(3, ret[1]);
Assert.Equal(3, ret[2]);
}
[Fact]
public void MergeListDuplicateTest()
{
int[] first = { 1 };
int[] second = {
1,
3,
3
};
int[] ret = Merger.MergeLists(first, second);
Assert.Equal(4, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(1, ret[1]);
Assert.Equal(3, ret[2]);
Assert.Equal(3, ret[3]);
}
[Fact]
public void MergeListDuplicateCustomEqualityTest()
{
int[] first = { 1 };
int[] second = { 3, 2 };
int[] ret = Merger.MergeLists(first, second, (x, y) => x % 2 == y % 2);
Assert.Equal(2, ret.Length);
Assert.Equal(1, ret[0]);
Assert.Equal(2, ret[1]);
}
[Fact]
public void MergeDictionariesTest()
{
Dictionary<int, string> first = new()
{
[1] = "test",
[5] = "value"
};
Dictionary<int, string> second = new()
{
[3] = "third",
};
IDictionary<int, string> ret = Merger.MergeDictionaries(first, second);
Assert.Equal(3, ret.Count);
Assert.Equal("test", ret[1]);
Assert.Equal("value", ret[5]);
Assert.Equal("third", ret[3]);
}
[Fact]
public void MergeDictionariesDuplicateTest()
{
Dictionary<int, string> first = new()
{
[1] = "test",
[5] = "value"
};
Dictionary<int, string> second = new()
{
[3] = "third",
[5] = "new-value",
};
IDictionary<int, string> ret = Merger.MergeDictionaries(first, second);
Assert.Equal(3, ret.Count);
Assert.Equal("test", ret[1]);
Assert.Equal("value", ret[5]);
Assert.Equal("third", ret[3]);
}
[Fact]
public void CompleteTest()
{
Genre genre = new()
Studio genre = new()
{
ID = 5,
Id = 5,
Name = "merged"
};
Genre genre2 = new()
Studio genre2 = new()
{
Name = "test"
};
Genre ret = Merger.Complete(genre, genre2);
Studio ret = Merger.Complete(genre, genre2);
Assert.True(ReferenceEquals(genre, ret));
Assert.Equal(5, ret.ID);
Assert.Equal(5, ret.Id);
Assert.Equal("test", genre.Name);
Assert.Null(genre.Slug);
}
@ -437,64 +136,6 @@ namespace Kyoo.Tests.Utility
// This should no call the setter of first so the test should pass.
}
[Fact]
public void MergeDictionaryNoChangeNoSetTest()
{
TestMergeSetter first = new()
{
Backing = new Dictionary<int, int>
{
[2] = 3
}
};
TestMergeSetter second = new()
{
Backing = new Dictionary<int, int>()
};
Merger.Merge(first, second);
// This should no call the setter of first so the test should pass.
}
[Fact]
public void MergeDictionaryNullValue()
{
Dictionary<string, string> first = new()
{
["logo"] = "logo",
["poster"] = null
};
Dictionary<string, string> second = new()
{
["poster"] = "new-poster",
["thumbnail"] = "thumbnails"
};
IDictionary<string, string> ret = Merger.MergeDictionaries(first, second, out bool changed);
Assert.True(changed);
Assert.Equal(3, ret.Count);
Assert.Equal("new-poster", ret["poster"]);
Assert.Equal("thumbnails", ret["thumbnail"]);
Assert.Equal("logo", ret["logo"]);
}
[Fact]
public void MergeDictionaryNullValueNoChange()
{
Dictionary<string, string> first = new()
{
["logo"] = "logo",
["poster"] = null
};
Dictionary<string, string> second = new()
{
["poster"] = null,
};
IDictionary<string, string> ret = Merger.MergeDictionaries(first, second, out bool changed);
Assert.False(changed);
Assert.Equal(2, ret.Count);
Assert.Null(ret["poster"]);
Assert.Equal("logo", ret["logo"]);
}
[Fact]
public void CompleteDictionaryNullValue()
{

View File

@ -26,13 +26,6 @@ namespace Kyoo.Tests.Utility
{
public class TaskTests
{
[Fact]
public async Task DefaultIfNullTest()
{
Assert.Equal(0, await TaskUtils.DefaultIfNull<int>(null));
Assert.Equal(1, await TaskUtils.DefaultIfNull(Task.FromResult(1)));
}
[Fact]
public async Task ThenTest()
{
@ -59,37 +52,5 @@ namespace Kyoo.Tests.Utility
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
.Then(_ => { }));
}
[Fact]
public async Task MapTest()
{
await Assert.ThrowsAsync<ArgumentException>(() => Task.FromResult(1)
.Map<int, int>(_ => throw new ArgumentException()));
Assert.Equal(2, await Task.FromResult(1)
.Map(x => x + 1));
static async Task<int> Faulted()
{
await Task.Delay(1);
throw new ArgumentException();
}
await Assert.ThrowsAsync<ArgumentException>(() => Faulted()
.Map(x =>
{
KAssert.Fail();
return x;
}));
static async Task<int> Infinite()
{
await Task.Delay(100000);
return 1;
}
CancellationTokenSource token = new();
token.Cancel();
await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Run(Infinite, token.Token)
.Map(x => x));
}
}
}

View File

@ -20,7 +20,6 @@ using System;
using System.Linq.Expressions;
using System.Reflection;
using Kyoo.Abstractions.Models;
using Kyoo.Utils;
using Xunit;
using KUtility = Kyoo.Utils.Utility;
@ -35,7 +34,6 @@ namespace Kyoo.Tests.Utility
Expression<Func<Show, int>> member = x => x.Id;
Expression<Func<Show, object>> memberCast = x => x.Id;
Assert.False(KUtility.IsPropertyExpression(null));
Assert.True(KUtility.IsPropertyExpression(member));
Assert.True(KUtility.IsPropertyExpression(memberCast));
@ -51,7 +49,6 @@ namespace Kyoo.Tests.Utility
Assert.Equal("ID", KUtility.GetPropertyName(member));
Assert.Equal("ID", KUtility.GetPropertyName(memberCast));
Assert.Throws<ArgumentException>(() => KUtility.GetPropertyName(null));
}
[Fact]
@ -84,16 +81,5 @@ namespace Kyoo.Tests.Utility
Array.Empty<Type>(),
new object[] { this }));
}
[Fact]
public void GetMethodTest2()
{
MethodInfo method = KUtility.GetMethod(typeof(Merger),
BindingFlags.Static | BindingFlags.Public,
nameof(Merger.MergeLists),
new[] { typeof(string) },
new object[] { "string", "string2", null });
Assert.Equal(nameof(Merger.MergeLists), method.Name);
}
}
}