From 2bc559424c992c7560688852c8d27002545b3887 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 30 May 2021 23:42:14 +0200 Subject: [PATCH] Splitting task tests and adding documentation to resources --- Kyoo.Common/Controllers/ILibraryManager.cs | 12 ++ .../Models/Attributes/LinkAttribute.cs | 13 +++ .../Models/Attributes/RelationAttributes.cs | 19 +++- Kyoo.Common/Models/Resources/Collection.cs | 56 +++++++--- Kyoo.Common/Models/Resources/Episode.cs | 103 ++++++++++++++++-- Kyoo.Common/Models/Resources/Genre.cs | 43 +++++--- Kyoo.Common/Models/Resources/IResource.cs | 6 + Kyoo.Common/Models/Resources/Library.cs | 48 +++++++- Kyoo.Common/Models/Resources/People.cs | 33 +++++- Kyoo.Common/Models/Resources/Provider.cs | 57 ++++++++-- Kyoo.Common/Models/Resources/Season.cs | 62 ++++++++++- Kyoo.Common/Models/Resources/User.cs | 3 +- Kyoo.Common/Utility/TaskUtils.cs | 69 ++++++++++++ Kyoo.Common/Utility/Utility.cs | 38 ------- Kyoo.Tests/Library/SpecificTests/ShowTests.cs | 56 ++++++++-- Kyoo.Tests/Utility/TaskTests.cs | 76 +++++++++++++ .../Repositories/ShowRepository.cs | 6 +- 17 files changed, 584 insertions(+), 116 deletions(-) create mode 100644 Kyoo.Common/Models/Attributes/LinkAttribute.cs create mode 100644 Kyoo.Common/Utility/TaskUtils.cs create mode 100644 Kyoo.Tests/Utility/TaskTests.cs diff --git a/Kyoo.Common/Controllers/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs index 2cd0c909..53c7061a 100644 --- a/Kyoo.Common/Controllers/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -242,6 +242,9 @@ namespace Kyoo.Controllers /// The type of the source object /// The related resource's type /// The param + /// + /// + /// Task Load([NotNull] T obj, Expression> member) where T : class, IResource where T2 : class, IResource, new(); @@ -254,6 +257,9 @@ namespace Kyoo.Controllers /// The type of the source object /// The related resource's type /// The param + /// + /// + /// Task Load([NotNull] T obj, Expression>> member) where T : class, IResource where T2 : class, new(); @@ -265,6 +271,9 @@ namespace Kyoo.Controllers /// The name of the resource to load (case sensitive) /// The type of the source object /// The param + /// + /// + /// Task Load([NotNull] T obj, string memberName) where T : class, IResource; @@ -273,6 +282,9 @@ namespace Kyoo.Controllers /// /// The source object. /// The name of the resource to load (case sensitive) + /// + /// + /// Task Load([NotNull] IResource obj, string memberName); /// diff --git a/Kyoo.Common/Models/Attributes/LinkAttribute.cs b/Kyoo.Common/Models/Attributes/LinkAttribute.cs new file mode 100644 index 00000000..d98ad90a --- /dev/null +++ b/Kyoo.Common/Models/Attributes/LinkAttribute.cs @@ -0,0 +1,13 @@ +using System; +using JetBrains.Annotations; +using Kyoo.Models.Attributes; + +namespace Kyoo.Common.Models.Attributes +{ + /// + /// An attribute to mark Link properties on resource. + /// + [AttributeUsage(AttributeTargets.Property)] + [MeansImplicitUse] + public class LinkAttribute : SerializeIgnoreAttribute { } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/Attributes/RelationAttributes.cs b/Kyoo.Common/Models/Attributes/RelationAttributes.cs index aac0e633..ef84f5e5 100644 --- a/Kyoo.Common/Models/Attributes/RelationAttributes.cs +++ b/Kyoo.Common/Models/Attributes/RelationAttributes.cs @@ -1,17 +1,34 @@ using System; +using Kyoo.Controllers; namespace Kyoo.Models.Attributes { - [AttributeUsage(AttributeTargets.Property, Inherited = false)] + /// + /// The targeted relation can be edited via calls to the repository's method. + /// + [AttributeUsage(AttributeTargets.Property)] public class EditableRelationAttribute : Attribute { } + /// + /// The targeted relation can be loaded via a call to . + /// [AttributeUsage(AttributeTargets.Property)] public class LoadableRelationAttribute : Attribute { + /// + /// The name of the field containing the related resource's ID. + /// public string RelationID { get; } + /// + /// Create a new . + /// public LoadableRelationAttribute() {} + /// + /// Create a new with a baking relationID field. + /// + /// The name of the RelationID field. public LoadableRelationAttribute(string relationID) { RelationID = relationID; diff --git a/Kyoo.Common/Models/Resources/Collection.cs b/Kyoo.Common/Models/Resources/Collection.cs index 3c7bed25..8162ff16 100644 --- a/Kyoo.Common/Models/Resources/Collection.cs +++ b/Kyoo.Common/Models/Resources/Collection.cs @@ -1,31 +1,59 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A class representing collections of . + /// A collection can also be stored in a . + /// public class Collection : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this collection. + /// public string Name { get; set; } + + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/collection/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The description of this collection. + /// public string Overview { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } - [LoadableRelation] public virtual ICollection Libraries { get; set; } + + /// + /// The list of shows contained in this collection. + /// + [LoadableRelation] public ICollection Shows { get; set; } + + /// + /// The list of libraries that contains this collection. + /// + [LoadableRelation] public ICollection Libraries { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } - [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } -#endif - public Collection() { } - - public Collection(string slug, string name, string overview, string poster) - { - Slug = slug; - Name = name; - Overview = overview; - Poster = poster; - } + /// + /// The internal link between this collection and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } + + /// + /// The internal link between this collection and libraries in the list. + /// + [Link] public ICollection> LibraryLinks { get; set; } +#endif } } diff --git a/Kyoo.Common/Models/Resources/Episode.cs b/Kyoo.Common/Models/Resources/Episode.cs index 29aeab06..e5d0d7f1 100644 --- a/Kyoo.Common/Models/Resources/Episode.cs +++ b/Kyoo.Common/Models/Resources/Episode.cs @@ -1,40 +1,126 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A class to represent a single show's episode. + /// This is also used internally for movies (their number is juste set to -1). + /// public class Episode : IResource, IOnMerge { + /// public int ID { get; set; } + + /// public string Slug => GetSlug(ShowSlug, SeasonNumber, EpisodeNumber, AbsoluteNumber); + + /// + /// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed. + /// [SerializeIgnore] public string ShowSlug { private get; set; } + + /// + /// The ID of the Show containing this episode. This value is only set when the has been loaded. + /// [SerializeIgnore] public int ShowID { get; set; } - [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } + /// + /// The show that contains this episode. This must be explicitly loaded via a call to . + /// + [LoadableRelation(nameof(ShowID))] public Show Show { get; set; } + + /// + /// The ID of the Season containing this episode. This value is only set when the has been loaded. + /// [SerializeIgnore] public int? SeasonID { get; set; } - [LoadableRelation(nameof(SeasonID))] public virtual Season Season { get; set; } + /// + /// The season that contains this episode. This must be explicitly loaded via a call to . + /// This can be null if the season is unknown and the episode is only identified by it's . + /// + [LoadableRelation(nameof(SeasonID))] public Season Season { get; set; } + /// + /// The season in witch this episode is in. This defaults to -1 if not specified. + /// public int SeasonNumber { get; set; } = -1; + + /// + /// The number of this episode is it's season. This defaults to -1 if not specified. + /// public int EpisodeNumber { get; set; } = -1; + + /// + /// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season. + /// This defaults to -1 if not specified. + /// public int AbsoluteNumber { get; set; } = -1; + + /// + /// The path of the video file for this episode. Any format supported by a is allowed. + /// [SerializeIgnore] public string Path { get; set; } + /// + /// The path of this episode's thumbnail. + /// By default, the http path for the thumbnail is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/episodes/{Slug}/thumb")] public string Thumb { get; set; } + + /// + /// The title of this episode. + /// public string Title { get; set; } + + /// + /// The overview of this episode. + /// public string Overview { get; set; } + + /// + /// The release date of this episode. It can be null if unknown. + /// public DateTime? ReleaseDate { get; set; } - public int Runtime { get; set; } //This runtime variable should be in minutes + /// + /// The link to metadata providers that this episode has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - - [EditableRelation] [LoadableRelation] public virtual ICollection Tracks { get; set; } + /// + /// The list of tracks this episode has. This lists video, audio and subtitles available. + /// + [EditableRelation] [LoadableRelation] public ICollection Tracks { get; set; } - public static string GetSlug(string showSlug, int seasonNumber, int episodeNumber, int absoluteNumber) + /// + /// Get the slug of an episode. + /// + /// The slug of the show. It can't be null. + /// + /// The season in which the episode is. + /// If this is a movie or if the episode should be referred by it's absolute number, set this to -1. + /// + /// + /// The number of the episode in it's season. + /// If this is a movie or if the episode should be referred by it's absolute number, set this to -1. + /// + /// + /// The absolute number of this show. + /// If you don't know it or this is a movie, use -1 + /// + /// The slug corresponding to the given arguments + /// The given show slug was null. + public static string GetSlug([NotNull] string showSlug, + int seasonNumber = -1, + int episodeNumber = -1, + int absoluteNumber = -1) { if (showSlug == null) - throw new ArgumentException("Show's slug is null. Can't find episode's slug."); + throw new ArgumentNullException(nameof(showSlug)); return seasonNumber switch { -1 when absoluteNumber == -1 => showSlug, @@ -43,6 +129,7 @@ namespace Kyoo.Models }; } + /// public void OnMerge(object merged) { Episode other = (Episode)merged; diff --git a/Kyoo.Common/Models/Resources/Genre.cs b/Kyoo.Common/Models/Resources/Genre.cs index cd6086df..c7aaa76f 100644 --- a/Kyoo.Common/Models/Resources/Genre.cs +++ b/Kyoo.Common/Models/Resources/Genre.cs @@ -1,40 +1,51 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A genre that allow one to specify categories for shows. + /// public class Genre : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this genre. + /// public string Name { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } + /// + /// The list of shows that have this genre. + /// + [LoadableRelation] public ICollection Shows { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } + /// + /// The internal link between this genre and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } #endif - + /// + /// Create a new, empty . + /// public Genre() {} + /// + /// Create a new and specify it's . + /// The is automatically calculated from it's name. + /// + /// The name of this genre. public Genre(string name) { Slug = Utility.ToSlug(name); Name = name; } - - public Genre(string slug, string name) - { - Slug = slug; - Name = name; - } - - public Genre(int id, string slug, string name) - { - ID = id; - Slug = slug; - Name = name; - } } } diff --git a/Kyoo.Common/Models/Resources/IResource.cs b/Kyoo.Common/Models/Resources/IResource.cs index c4c4231b..4867bc0a 100644 --- a/Kyoo.Common/Models/Resources/IResource.cs +++ b/Kyoo.Common/Models/Resources/IResource.cs @@ -1,3 +1,5 @@ +using Kyoo.Controllers; + namespace Kyoo.Models { /// @@ -8,6 +10,10 @@ namespace Kyoo.Models /// /// A unique ID for this type of resource. This can't be changed and duplicates are not allowed. /// + /// + /// You don't need to specify an ID manually when creating a new resource, + /// this field is automatically assigned by the . + /// public int ID { get; set; } /// diff --git a/Kyoo.Common/Models/Resources/Library.cs b/Kyoo.Common/Models/Resources/Library.cs index c8148544..a72d6a37 100644 --- a/Kyoo.Common/Models/Resources/Library.cs +++ b/Kyoo.Common/Models/Resources/Library.cs @@ -1,24 +1,60 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A library containing and . + /// public class Library : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this library. + /// public string Name { get; set; } + + /// + /// The list of paths that this library is responsible for. This is mainly used by the Scan task. + /// public string[] Paths { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection Providers { get; set; } + /// + /// The list of used for items in this library. + /// + [EditableRelation] [LoadableRelation] public ICollection Providers { get; set; } - [LoadableRelation] public virtual ICollection Shows { get; set; } - [LoadableRelation] public virtual ICollection Collections { get; set; } + /// + /// The list of shows in this library. + /// + [LoadableRelation] public ICollection Shows { get; set; } + + /// + /// The list of collections in this library. + /// + [LoadableRelation] public ICollection Collections { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> ProviderLinks { get; set; } - [SerializeIgnore] public virtual ICollection> ShowLinks { get; set; } - [SerializeIgnore] public virtual ICollection> CollectionLinks { get; set; } + /// + /// The internal link between this library and provider in the list. + /// + [Link] public ICollection> ProviderLinks { get; set; } + + /// + /// The internal link between this library and shows in the list. + /// + [Link] public ICollection> ShowLinks { get; set; } + + /// + /// The internal link between this library and collection in the list. + /// + [Link] public ICollection> CollectionLinks { get; set; } #endif } } diff --git a/Kyoo.Common/Models/Resources/People.cs b/Kyoo.Common/Models/Resources/People.cs index 46b86143..9fea0112 100644 --- a/Kyoo.Common/Models/Resources/People.cs +++ b/Kyoo.Common/Models/Resources/People.cs @@ -3,14 +3,37 @@ using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// An actor, voice actor, writer, animator, somebody who worked on a . + /// public class People : IResource { + /// public int ID { get; set; } - public string Slug { get; set; } - public string Name { get; set; } - [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection Roles { get; set; } + /// + public string Slug { get; set; } + + /// + /// The name of this person. + /// + public string Name { get; set; } + + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// + [SerializeAs("{HOST}/api/people/{Slug}/poster")] public string Poster { get; set; } + + /// + /// The link to metadata providers that this person has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } + + /// + /// The list of roles this person has played in. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection Roles { get; set; } } } diff --git a/Kyoo.Common/Models/Resources/Provider.cs b/Kyoo.Common/Models/Resources/Provider.cs index 6a19f27c..ac7d9ffc 100644 --- a/Kyoo.Common/Models/Resources/Provider.cs +++ b/Kyoo.Common/Models/Resources/Provider.cs @@ -1,37 +1,72 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// This class contains metadata about . + /// You can have providers even if you don't have the corresponding . + /// public class Provider : IResource { + /// public int ID { get; set; } + + /// public string Slug { get; set; } + + /// + /// The name of this provider. + /// public string Name { get; set; } + + /// + /// The path of this provider's logo. + /// By default, the http path for this logo is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/providers/{Slug}/logo")] public string Logo { get; set; } + + /// + /// The extension of the logo. This is used for http responses. + /// [SerializeIgnore] public string LogoExtension { get; set; } - [LoadableRelation] public virtual ICollection Libraries { get; set; } + + /// + /// The list of libraries that uses this provider. + /// + [LoadableRelation] public ICollection Libraries { get; set; } #if ENABLE_INTERNAL_LINKS - [SerializeIgnore] public virtual ICollection> LibraryLinks { get; set; } - [SerializeIgnore] public virtual ICollection MetadataLinks { get; set; } + /// + /// The internal link between this provider and libraries in the list. + /// + [Link] public ICollection> LibraryLinks { get; set; } + + /// + /// The internal link between this provider and related . + /// + [Link] public ICollection MetadataLinks { get; set; } #endif + /// + /// Create a new, default, + /// public Provider() { } + /// + /// Create a new and specify it's . + /// The is automatically calculated from it's name. + /// + /// The name of this provider. + /// The logo of this provider. public Provider(string name, string logo) { Slug = Utility.ToSlug(name); Name = name; Logo = logo; } - - public Provider(int id, string name, string logo) - { - ID = id; - Slug = Utility.ToSlug(name); - Name = name; - Logo = logo; - } } } \ No newline at end of file diff --git a/Kyoo.Common/Models/Resources/Season.cs b/Kyoo.Common/Models/Resources/Season.cs index b3f7ab27..369763ea 100644 --- a/Kyoo.Common/Models/Resources/Season.cs +++ b/Kyoo.Common/Models/Resources/Season.cs @@ -1,25 +1,75 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using Kyoo.Controllers; using Kyoo.Models.Attributes; namespace Kyoo.Models { + /// + /// A season of a . + /// public class Season : IResource { + /// public int ID { get; set; } + + /// public string Slug => $"{ShowSlug}-s{SeasonNumber}"; - [SerializeIgnore] public int ShowID { get; set; } + + /// + /// The slug of the Show that contain this episode. If this is not set, this season is ill-formed. + /// [SerializeIgnore] public string ShowSlug { private get; set; } - [LoadableRelation(nameof(ShowID))] public virtual Show Show { get; set; } + + /// + /// The ID of the Show containing this season. This value is only set when the has been loaded. + /// + [SerializeIgnore] public int ShowID { get; set; } + /// + /// The show that contains this season. This must be explicitly loaded via a call to . + /// + [LoadableRelation(nameof(ShowID))] public Show Show { get; set; } + /// + /// The number of this season. This can be set to 0 to indicate specials. This defaults to -1 for unset. + /// public int SeasonNumber { get; set; } = -1; + /// + /// The title of this season. + /// public string Title { get; set; } + + /// + /// A quick overview of this season. + /// public string Overview { get; set; } - public int? Year { get; set; } + + /// + /// The starting air date of this season. + /// + public DateTime? StartDate { get; set; } + + /// + /// The ending date of this season. + /// + public DateTime? EndDate { get; set; } + /// + /// The path of this poster. + /// By default, the http path for this poster is returned from the public API. + /// This can be disabled using the internal query flag. + /// [SerializeAs("{HOST}/api/seasons/{Slug}/thumb")] public string Poster { get; set; } - [EditableRelation] [LoadableRelation] public virtual ICollection ExternalIDs { get; set; } + + /// + /// The link to metadata providers that this episode has. See for more information. + /// + [EditableRelation] [LoadableRelation] public ICollection ExternalIDs { get; set; } - [LoadableRelation] public virtual ICollection Episodes { get; set; } + /// + /// The list of episodes that this season contains. + /// + [LoadableRelation] public ICollection Episodes { get; set; } } } diff --git a/Kyoo.Common/Models/Resources/User.cs b/Kyoo.Common/Models/Resources/User.cs index 94afa240..1f497541 100644 --- a/Kyoo.Common/Models/Resources/User.cs +++ b/Kyoo.Common/Models/Resources/User.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Kyoo.Common.Models.Attributes; namespace Kyoo.Models { @@ -52,7 +53,7 @@ namespace Kyoo.Models /// /// Links between Users and Shows. /// - public ICollection> ShowLinks { get; set; } + [Link] public ICollection> ShowLinks { get; set; } #endif } diff --git a/Kyoo.Common/Utility/TaskUtils.cs b/Kyoo.Common/Utility/TaskUtils.cs new file mode 100644 index 00000000..78332cc9 --- /dev/null +++ b/Kyoo.Common/Utility/TaskUtils.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Kyoo +{ + /// + /// A class containing helper method for tasks. + /// + public static class TaskUtils + { + /// + /// Run a method after the execution of the task. + /// + /// The task to wait. + /// + /// The method to run after the task finish. This will only be run if the task finished successfully. + /// + /// The type of the item in the task. + /// A continuation task wrapping the initial task and adding a continuation method. + /// + /// The source task has been canceled. + public static Task Then(this Task task, Action then) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + then(x.Result); + return x.Result; + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// Map the result of a task to another result. + /// + /// The task to map. + /// The mapper method, it take the task's result as a parameter and should return the new result. + /// The type of returns of the given task + /// The resulting task after the mapping method + /// A task wrapping the initial task and mapping the initial result. + /// The source task has been canceled. + public static Task Map(this Task task, Func map) + { + return task.ContinueWith(x => + { + if (x.IsFaulted) + x.Exception!.InnerException!.ReThrow(); + if (x.IsCanceled) + throw new TaskCanceledException(); + return map(x.Result); + }, TaskContinuationOptions.ExecuteSynchronously); + } + + /// + /// A method to return the a default value from a task if the initial task is null. + /// + /// The initial task + /// The type that the task will return + /// A non-null task. + [NotNull] + public static Task DefaultIfNull([CanBeNull] Task value) + { + return value ?? Task.FromResult(default); + } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Utility/Utility.cs b/Kyoo.Common/Utility/Utility.cs index e25b595b..e7d9600a 100644 --- a/Kyoo.Common/Utility/Utility.cs +++ b/Kyoo.Common/Utility/Utility.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -270,43 +269,6 @@ namespace Kyoo throw new ArgumentNullException(nameof(ex)); ExceptionDispatchInfo.Capture(ex).Throw(); } - - public static Task Then(this Task task, Action map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - map(x.Result); - return x.Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Map(this Task task, Func map) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return map(x.Result); - }, TaskContinuationOptions.ExecuteSynchronously); - } - - public static Task Cast(this Task task) - { - return task.ContinueWith(x => - { - if (x.IsFaulted) - x.Exception!.InnerException!.ReThrow(); - if (x.IsCanceled) - throw new TaskCanceledException(); - return (T)((dynamic)x).Result; - }, TaskContinuationOptions.ExecuteSynchronously); - } /// /// Get a friendly type name (supporting generics) diff --git a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs index 43c7fb39..cfae6a9e 100644 --- a/Kyoo.Tests/Library/SpecificTests/ShowTests.cs +++ b/Kyoo.Tests/Library/SpecificTests/ShowTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using System.Threading.Tasks; using Kyoo.Controllers; using Kyoo.Models; @@ -15,20 +16,61 @@ namespace Kyoo.Tests.SpecificTests { _repository = Repositories.LibraryManager.ShowRepository; } - // + + [Fact] + public async Task EditTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Path = "/super"; + value.Title = "New Title"; + Show edited = await _repository.Edit(value, false); + KAssert.DeepEqual(value, edited); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows.FirstAsync(); + + KAssert.DeepEqual(show, value); + } + + [Fact] + public async Task EditGenreTest() + { + Show value = await _repository.Get(TestSample.Get().Slug); + value.Genres = new[] {new Genre("test")}; + Show edited = await _repository.Edit(value, false); + + 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})); + + await using DatabaseContext database = Repositories.Context.New(); + Show show = await database.Shows + .Include(x => x.Genres) + .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})); + } + // [Fact] - // public async Task EditTest() + // public async Task EditPeopleTest() // { // Show value = await _repository.Get(TestSample.Get().Slug); - // value.Path = "/super"; - // value.Title = "New Title"; + // value.People = new[] {new People + // { + // Name = "test" + // }}; // Show edited = await _repository.Edit(value, false); - // KAssert.DeepEqual(value, edited); + // + // 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})); // // await using DatabaseContext database = Repositories.Context.New(); - // Show show = await database.Shows.FirstAsync(); + // Show show = await database.Shows + // .Include(x => x.Genres) + // .FirstAsync(); // - // KAssert.DeepEqual(show, value); + // 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})); // } } } \ No newline at end of file diff --git a/Kyoo.Tests/Utility/TaskTests.cs b/Kyoo.Tests/Utility/TaskTests.cs new file mode 100644 index 00000000..3a7baa48 --- /dev/null +++ b/Kyoo.Tests/Utility/TaskTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Kyoo.Tests +{ + public class TaskTests + { + [Fact] + public async Task DefaultIfNullTest() + { + Assert.Equal(0, await TaskUtils.DefaultIfNull(null)); + Assert.Equal(1, await TaskUtils.DefaultIfNull(Task.FromResult(1))); + } + + [Fact] + public async Task ThenTest() + { + await Assert.ThrowsAsync(() => Task.FromResult(1) + .Then(_ => throw new ArgumentException())); + Assert.Equal(1, await Task.FromResult(1) + .Then(_ => {})); + + static async Task Faulted() + { + await Task.Delay(1); + throw new ArgumentException(); + } + await Assert.ThrowsAsync(() => Faulted().Then(_ => KAssert.Fail())); + + static async Task Infinite() + { + await Task.Delay(100000); + return 1; + } + + CancellationTokenSource token = new(); + token.Cancel(); + await Assert.ThrowsAsync(() => Task.Run(Infinite, token.Token) + .Then(_ => {})); + } + + [Fact] + public async Task MapTest() + { + await Assert.ThrowsAsync(() => Task.FromResult(1) + .Map(_ => throw new ArgumentException())); + Assert.Equal(2, await Task.FromResult(1) + .Map(x => x + 1)); + + static async Task Faulted() + { + await Task.Delay(1); + throw new ArgumentException(); + } + await Assert.ThrowsAsync(() => Faulted() + .Map(x => + { + KAssert.Fail(); + return x; + })); + + static async Task Infinite() + { + await Task.Delay(100000); + return 1; + } + + CancellationTokenSource token = new(); + token.Cancel(); + await Assert.ThrowsAsync(() => Task.Run(Infinite, token.Token) + .Map(x => x)); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/Repositories/ShowRepository.cs b/Kyoo/Controllers/Repositories/ShowRepository.cs index eb3f36e4..595f2156 100644 --- a/Kyoo/Controllers/Repositories/ShowRepository.cs +++ b/Kyoo/Controllers/Repositories/ShowRepository.cs @@ -103,9 +103,9 @@ namespace Kyoo.Controllers await base.Validate(resource); if (resource.Studio != null) resource.Studio = await _studios.CreateIfNotExists(resource.Studio); - resource.Genres = await resource.Genres - .SelectAsync(x => _genres.CreateIfNotExists(x)) - .ToListAsync(); + resource.Genres = await TaskUtils.DefaultIfNull(resource.Genres + ?.SelectAsync(x => _genres.CreateIfNotExists(x)) + .ToListAsync()); resource.GenreLinks = resource.Genres? .Select(x => Link.UCreate(resource, x)) .ToList();