diff --git a/DM.MovieApi.IntegrationTests/ApiResponseUtil.cs b/DM.MovieApi.IntegrationTests/ApiResponseUtil.cs
index 5f480c7..c66863b 100644
--- a/DM.MovieApi.IntegrationTests/ApiResponseUtil.cs
+++ b/DM.MovieApi.IntegrationTests/ApiResponseUtil.cs
@@ -17,6 +17,7 @@ internal static class ApiResponseUtil
{
internal const int TestInitThrottle = 375;
internal const int PagingThrottle = 225;
+ private static readonly DateTime MinDate = new( 1900, 1, 1 );
///
/// Slows down the starting of tests to keep themoviedb.org api from denying the request
@@ -29,12 +30,14 @@ public static void ThrottleTests()
public static void AssertErrorIsNull( ApiResponseBase response )
{
- Console.WriteLine( response.CommandText );
+ Log( response.CommandText );
Assert.IsNull( response.Error, response.Error?.ToString() ?? "Makes Compiler Happy" );
}
public static void AssertImagePath( string path )
{
+ if( path == null ) return;
+
Assert.IsTrue( path.StartsWith( "/" ), $"Actual: {path}" );
Assert.IsTrue(
@@ -42,22 +45,35 @@ public static void AssertImagePath( string path )
$"Actual: {path}" );
}
- public static async Task AssertCanPageSearchResponse( TSearch search, int minimumPageCount, int minimumTotalResultsCount,
- Func>> apiSearch, Func keySelector )
+ public static void AssertNoSearchResults( ApiSearchResponse response )
+ {
+ AssertErrorIsNull( response );
+
+ Assert.AreEqual( 0, response.Results.Count, $"Actual: {response}" );
+ Assert.AreEqual( 1, response.PageNumber, $"Actual: {response}" );
+ Assert.AreEqual( 0, response.TotalPages, $"Actual: {response}" );
+ Assert.AreEqual( 0, response.TotalResults, $"Actual: {response}" );
+ }
+
+ public static async Task AssertCanPageSearchResponse(
+ TSearch search,
+ int minimumPageCount,
+ Func>> apiSearch,
+ Func keySelector )
{
if( minimumPageCount < 2 )
{
Assert.Fail( "minimumPageCount must be greater than 1." );
}
- var allFound = new List();
+ var ids = new HashSet();
+ var dups = new List();
+ int totalResults = 0;
int pageNumber = 1;
- var priorResults = new Dictionary();
-
do
{
- System.Diagnostics.Trace.WriteLine( $"search: {search} | page: {pageNumber}", "ApiResponseUti.AssertCanPageSearchResponse" );
+ Log( $"search: {search} | page: {pageNumber}", "AssertCanPage" );
ApiSearchResponse response = await apiSearch( search, pageNumber );
AssertErrorIsNull( response );
@@ -66,41 +82,28 @@ public static async Task AssertCanPageSearchResponse( TSearch search
if( typeof( T ) == typeof( Movie ) )
{
- AssertMovieStructure( ( IEnumerable )response.Results );
+ AssertMovieStructure( (IEnumerable)response.Results );
}
else if( typeof( T ) == typeof( PersonInfo ) )
{
- AssertPersonInfoStructure( ( IEnumerable )response.Results );
+ AssertPersonInfoStructure( (IEnumerable)response.Results );
}
if( keySelector == null )
{
- allFound.AddRange( response.Results );
+ totalResults += response.Results.Count;
}
else
{
- var current = new List();
foreach( T res in response.Results )
{
+ totalResults++;
int key = keySelector( res );
-
- if( priorResults.TryAdd( key, 1 ) )
- {
- current.Add( res );
- continue;
- }
-
- System.Diagnostics.Trace.WriteLine( $"dup on page {response.PageNumber}: {res}" );
-
- if( ++priorResults[key] > 2 )
+ if( ids.Add( key ) == false )
{
- Assert.Fail( "Every now and then themoviedb.org API returns a duplicate from a prior page. " +
- "But this time it exceeded our tolerance of one dup.\r\n" +
- $"dup: {res}" );
+ dups.Add( res.ToString() );
}
}
-
- allFound.AddRange( current );
}
Assert.AreEqual( pageNumber, response.PageNumber );
@@ -108,16 +111,17 @@ public static async Task AssertCanPageSearchResponse( TSearch search
Assert.IsTrue( response.TotalPages >= minimumPageCount,
$"Expected minimum of {minimumPageCount} TotalPages. Actual TotalPages: {response.TotalPages}" );
- pageNumber++;
-
// keeps the system from being throttled
System.Threading.Thread.Sleep( PagingThrottle );
- } while( pageNumber <= minimumPageCount );
+ } while( ++pageNumber <= minimumPageCount );
// will be 1 greater than minimumPageCount in the last loop
- Assert.AreEqual( minimumPageCount + 1, pageNumber );
+ Assert.AreEqual( minimumPageCount, --pageNumber );
- Assert.IsTrue( allFound.Count >= minimumTotalResultsCount, $"Actual found count: {allFound.Count} | Expected min count: {minimumTotalResultsCount}" );
+ // 20 results per page
+ int minCount = pageNumber * 20;
+ Assert.IsTrue( totalResults >= minCount,
+ $"Actual results: {totalResults} | Expected min: {minCount}" );
if( keySelector == null )
{
@@ -126,16 +130,21 @@ public static async Task AssertCanPageSearchResponse( TSearch search
return;
}
- List> groupById = allFound
- .ToLookup( keySelector )
- .ToList();
-
- List dups = groupById
- .Where( x => x.Skip( 1 ).Any() )
- .Select( x => $"({x.Count()}) {string.Join( " | ", x.Select( y => y.ToString() ) )}" )
- .ToList();
+ // api tends to return duplicate results when paging
+ // shouldn't be more than 2 or 3 per page; at 20 per page,
+ // that's approximately 4-6; let's target 20%
+ int min = (int)(totalResults * 0.8);
+ var d = dups
+ .GroupBy( x => x )
+ .Select( x => $"{x.Key} (x {x.Count()})" );
+ Log( $"Results: {totalResults}, Dups: {dups.Count}\r\n{string.Join( "\r\n", d )}" );
- Assert.AreEqual( 0, dups.Count, "Duplicates: " + Environment.NewLine + string.Join( Environment.NewLine, dups ) );
+ if( min >= ids.Count )
+ {
+ Assert.Fail( "Every now and then themoviedb.org API returns a duplicate from a prior page. " +
+ "But this time it exceeded our tolerance of 20% dups.\r\n" +
+ $"Actual: {ids.Count} vs {min}" );
+ }
}
private static void AssertPersonInfoStructure( IEnumerable people )
@@ -232,9 +241,15 @@ public static void AssertMovieInformationStructure( IEnumerable movie
public static void AssertMovieInformationStructure( MovieInfo movie )
{
- Assert.IsFalse( string.IsNullOrWhiteSpace( movie.Title ) );
- Assert.IsTrue( movie.Id > 0 );
+ Assert.IsFalse( string.IsNullOrWhiteSpace( movie.Title ), $"Actual: {movie}" );
+ Assert.IsFalse( string.IsNullOrWhiteSpace( movie.OriginalTitle ), $"Actual {movie}" );
+ // movie.Overview is sometimes empty
+
+ Assert.IsTrue( movie.Id > 1 );
+ Assert.IsTrue( movie.ReleaseDate > MinDate, $"Actual: {movie.ReleaseDate} | {movie}" );
+ AssertImagePath( movie.BackdropPath );
+ AssertImagePath( movie.PosterPath );
AssertGenres( movie.GenreIds, movie.Genres );
}
@@ -333,10 +348,10 @@ private static void AssertTvShowGuestStarsStructure( GuestStars guestStars )
Assert.IsFalse( string.IsNullOrWhiteSpace( guestStars.OriginalName ) );
Assert.IsTrue( guestStars.Popularity > 0 );
- if( guestStars.ProfilePath != null )
- {
- AssertImagePath( guestStars.ProfilePath );
- }
+ AssertImagePath( guestStars.ProfilePath );
}
+
+ private static void Log( string msg, string category = null )
+ => System.Diagnostics.Trace.WriteLine( msg, category );
}
}
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Companies/ApiCompanyRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/Companies/ApiCompanyRequestTests.cs
index c72cbc7..78edc3a 100644
--- a/DM.MovieApi.IntegrationTests/MovieDb/Companies/ApiCompanyRequestTests.cs
+++ b/DM.MovieApi.IntegrationTests/MovieDb/Companies/ApiCompanyRequestTests.cs
@@ -68,9 +68,8 @@ public async Task GetMoviesAsync_CanPageResults()
{
const int companyId = 3;
const int minimumPageCount = 5;
- const int minimumTotalResultsCount = 95;
- await ApiResponseUtil.AssertCanPageSearchResponse( companyId, minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( companyId, minimumPageCount,
( id, pageNumber ) => _api.GetMoviesAsync( id, pageNumber ), x => x.Id );
}
}
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Discover/ApiDiscoverRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/Discover/ApiDiscoverRequestTests.cs
new file mode 100644
index 0000000..9db14ab
--- /dev/null
+++ b/DM.MovieApi.IntegrationTests/MovieDb/Discover/ApiDiscoverRequestTests.cs
@@ -0,0 +1,146 @@
+using System.Linq;
+using System.Threading.Tasks;
+using DM.MovieApi.ApiResponse;
+using DM.MovieApi.MovieDb.Discover;
+using DM.MovieApi.MovieDb.Movies;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace DM.MovieApi.IntegrationTests.MovieDb.Discover
+{
+ [TestClass]
+ public class ApiDiscoverRequestTests
+ {
+ private IApiDiscoverRequest _api;
+
+ [TestInitialize]
+ public void TestInit()
+ {
+ ApiResponseUtil.ThrottleTests();
+
+ _api = MovieDbFactory.Create().Value;
+
+ Assert.IsInstanceOfType( _api, typeof( ApiDiscoverRequest ) );
+ }
+
+ [TestMethod]
+ public async Task DiscoverMovies_WithCrew()
+ {
+ int directorId = 66212;
+
+ IDiscoverMovieParameterBuilder builder = CreateBuilder();
+ builder.WithCrew( directorId );
+
+ ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+ }
+
+ [TestMethod]
+ public async Task DiscoverMovies_WithCrew_HasNoResult_InvalidPersonId()
+ {
+ int personId = 0;
+
+ IDiscoverMovieParameterBuilder builder = CreateBuilder();
+ builder.WithCrew( personId );
+
+ ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder );
+
+ ApiResponseUtil.AssertNoSearchResults( response );
+ }
+
+ [TestMethod]
+ public async Task DiscoverMovies_WithCast()
+ {
+ int actorId = 66462;
+
+ IDiscoverMovieParameterBuilder builder = CreateBuilder();
+ builder.WithCast( actorId );
+
+ ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+ }
+
+ [TestMethod]
+ public async Task DiscoverMovies_WithCast_HasNoResult_InvalidPersonId()
+ {
+ int personId = 0;
+
+ IDiscoverMovieParameterBuilder builder = CreateBuilder();
+ builder.WithCast( personId );
+
+ ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder );
+
+ ApiResponseUtil.AssertNoSearchResults( response );
+ }
+
+ [TestMethod]
+ public async Task DiscoverMovies_WithGenre()
+ {
+ int genreId = 28;
+
+ IDiscoverMovieParameterBuilder builder = CreateBuilder();
+ builder.WithGenre( genreId );
+
+ ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+
+ Assert.IsTrue( response.Results
+ .All( r => r.Genres.Any( g => g.Id == genreId ) ), "No results with genre" );
+ }
+
+ [TestMethod]
+ public async Task DiscoverMovies_ExcludeGenre()
+ {
+ int genreId = 28;
+
+ IDiscoverMovieParameterBuilder builder = CreateBuilder();
+ builder.ExcludeGenre( genreId );
+
+ ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+
+ Assert.IsTrue( response.Results
+ .All( r => r.Genres.All( g => g.Id != genreId ) ), "Genre found in results" );
+ }
+
+ [TestMethod]
+ public async Task DiscoverMovies_WithOriginalLanguage_InFinnish()
+ {
+ int directorId = 66212;
+ string originalLanguage = "fi";
+
+ IDiscoverMovieParameterBuilder builder = CreateBuilder();
+ builder.WithOriginalLanguage( originalLanguage ).WithCrew( directorId );
+
+ ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+ }
+
+ [TestMethod]
+ public async Task DiscoverMovies_WithOriginalLanguage_InGerman()
+ {
+ int directorId = 66212;
+ string originalLanguage = "de";
+
+ IDiscoverMovieParameterBuilder builder = CreateBuilder();
+ builder.WithOriginalLanguage( originalLanguage ).WithCrew( directorId );
+
+ ApiSearchResponse response = await _api.DiscoverMoviesAsync( builder );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+ }
+
+ private IDiscoverMovieParameterBuilder CreateBuilder()
+ => new DiscoverMovieParameterBuilder();
+ }
+}
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Genres/ApiGenreRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/Genres/ApiGenreRequestTests.cs
index fd8cfb0..d406f2b 100644
--- a/DM.MovieApi.IntegrationTests/MovieDb/Genres/ApiGenreRequestTests.cs
+++ b/DM.MovieApi.IntegrationTests/MovieDb/Genres/ApiGenreRequestTests.cs
@@ -31,7 +31,7 @@ public async Task FindById_Foreign_Genre_NoLongerExists()
const int id = 10769;
const string name = "Foreign";
- CollectionAssert.DoesNotContain( _api.AllGenres.ToList(), new Genre( id, name ) );
+ CollectionAssert.DoesNotContain( _api.AllGenres.ToArray(), new Genre( id, name ) );
// FindById will add to AllGenres when it does not exist
ApiQueryResponse response = await _api.FindByIdAsync( id );
@@ -41,7 +41,7 @@ public async Task FindById_Foreign_Genre_NoLongerExists()
// the genre should not have been added to AllGenres since TMDB no longer recognizes
// the foreign genre category.
- CollectionAssert.DoesNotContain( _api.AllGenres.ToList(), new Genre( id, name ) );
+ CollectionAssert.DoesNotContain( _api.AllGenres.ToArray(), new Genre( id, name ) );
}
[TestMethod]
@@ -78,7 +78,7 @@ public async Task GetAllAsync_Returns_Known_Genres()
IReadOnlyList knownGenres = GenreFactory.GetAll();
- CollectionAssert.AreEquivalent( knownGenres.ToList(), response.Item.ToList() );
+ CollectionAssert.AreEquivalent( knownGenres.ToArray(), response.Item.ToArray() );
}
[TestMethod]
@@ -118,7 +118,7 @@ public async Task GetMoviesAsync_IsSubset_OfGetAll()
Assert.IsTrue( all.Item.Count > movies.Item.Count );
- CollectionAssert.IsSubsetOf( movies.Item.ToList(), all.Item.ToList() );
+ CollectionAssert.IsSubsetOf( movies.Item.ToArray(), all.Item.ToArray() );
}
[TestMethod]
@@ -134,7 +134,7 @@ public async Task GetTelevisionAsync_IsSubset_OfGetAll()
Assert.IsTrue( all.Item.Count > tv.Item.Count );
- CollectionAssert.IsSubsetOf( tv.Item.ToList(), all.Item.ToList() );
+ CollectionAssert.IsSubsetOf( tv.Item.ToArray(), all.Item.ToArray() );
}
[TestMethod]
@@ -152,7 +152,7 @@ public async Task FindMoviesByIdAsync_Returns_ValidResult()
foreach( MovieInfo info in response.Results )
{
- CollectionAssert.IsSubsetOf( expectedGenres, info.Genres.ToList() );
+ CollectionAssert.IsSubsetOf( expectedGenres, info.Genres.ToArray() );
}
}
@@ -161,10 +161,9 @@ public async Task FindMoviesByIdAsync_CanPageResults()
{
int genreId = GenreFactory.Comedy().Id;
// Comedy has upwards of 2k pages.
- const int minimumPageCount = 5;
- const int minimumTotalResultsCount = 100; // 20 results per page x 5 pages = 100
+ const int minimumPageCount = 10;
- await ApiResponseUtil.AssertCanPageSearchResponse( genreId, minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( genreId, minimumPageCount,
( id, page ) => _api.FindMoviesByIdAsync( id, page ), x => x.Id );
}
}
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests.cs
index 19e0a7e..1731d93 100644
--- a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests.cs
+++ b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests.cs
@@ -108,7 +108,7 @@ private void AssertRunLolaRun( ApiSearchResponse response, string exp
Assert.AreEqual( new DateTime( 1998, 03, 03 ), movie.ReleaseDate );
- var expectedGenres = new List
+ var expectedGenres = new[]
{
GenreFactory.Action(),
GenreFactory.Drama(),
@@ -122,9 +122,8 @@ public async Task SearchByTitleAsync_CanPageResults()
{
const string query = "Harry";
const int minimumPageCount = 8;
- const int minimumTotalResultsCount = 150;
- await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount,
( search, pageNumber ) => _api.SearchByTitleAsync( search, pageNumber ), x => x.Id );
}
@@ -202,7 +201,7 @@ public async Task FindByIdAsync_StarWarsTheForceAwakens_Returns_AllValues()
ApiResponseUtil.AssertImagePath( movie.MovieCollectionInfo.PosterPath );
// Genres
- var expectedGenres = new List
+ var expectedGenres = new[]
{
GenreFactory.Action(),
GenreFactory.Adventure(),
@@ -213,15 +212,15 @@ public async Task FindByIdAsync_StarWarsTheForceAwakens_Returns_AllValues()
"actual:\r\n" + string.Join( "\r\n", movie.Genres ) );
// Keywords
- var expectedKeywords = new List
+ var expectedKeywords = new Keyword[]
{
new(803, "android"),
new(1612, "spacecraft"),
new(161176, "space opera")
};
CollectionAssert.AreEquivalent( expectedKeywords, movie.Keywords.ToArray(),
- $"\r\nactual:\r\n\t{string.Join( "\r\n\t", expectedKeywords )}" +
- $"\r\nactual:\r\n\t{string.Join( "\r\n\t", movie.Keywords )}" );
+ $"\r\nactual:\r\n\t{string.Join( "\r\n\t", expectedKeywords.Select( x => x.ToString() ) )}" +
+ $"\r\nactual:\r\n\t{string.Join( "\r\n\t", movie.Keywords.Select( x => x.ToString() ) )}" );
}
[TestMethod]
@@ -279,9 +278,8 @@ public async Task GetNowPlayingAsync_CanPageResults()
{
// Now Playing typically has 25+ pages.
const int minimumPageCount = 5;
- const int minimumTotalResultsCount = 100; // 20 results per page x 5 pages = 100
- await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount,
( _, page ) => _api.GetNowPlayingAsync( page ), x => x.Id );
}
@@ -299,11 +297,10 @@ public async Task GetUpcomingAsync_Returns_ValidResults()
public async Task GetUpcomingAsync_CanPageResults()
{
// Now Playing typically has 5+ pages.
- // - intentionally setting minimumTotalResultsCount at 50; sometimes upcoming movies are scarce.
+ // note: sometimes upcoming movies are scarce and may occasionally fail.
const int minimumPageCount = 3;
- const int minimumTotalResultsCount = 50; // 20 results per page x 3 pages = 60
- await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount,
( _, page ) => _api.GetUpcomingAsync( page ), x => x.Id );
}
@@ -323,9 +320,8 @@ public async Task GetTopRatedAsync_Returns_ValidResults()
public async Task GetTopRatedAsync_CanPageResults()
{
const int minimumPageCount = 2;
- const int minimumTotalResultsCount = 40;
- await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount,
( _, page ) => _api.GetTopRatedAsync( page ), x => x.Id );
}
@@ -345,9 +341,8 @@ public async Task GetPopularAsync_Returns_ValidResults()
public async Task GetPopularAsync_CanPageResults()
{
const int minimumPageCount = 2;
- const int minimumTotalResultsCount = 40;
- await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( "unused", minimumPageCount,
( _, page ) => _api.GetPopularAsync( page ), x => x.Id );
}
}
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetCredits.cs b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetCredits.cs
index 4fc4ec4..f9d11f0 100644
--- a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetCredits.cs
+++ b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetCredits.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using System.Threading.Tasks;
using DM.MovieApi.ApiResponse;
using DM.MovieApi.MovieDb.Movies;
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetRecommendations.cs b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetRecommendations.cs
new file mode 100644
index 0000000..f995741
--- /dev/null
+++ b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetRecommendations.cs
@@ -0,0 +1,45 @@
+using System.Threading.Tasks;
+using DM.MovieApi.ApiResponse;
+using DM.MovieApi.MovieDb.Movies;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace DM.MovieApi.IntegrationTests.MovieDb.Movies
+{
+ [TestClass]
+ public class GetRecommendationsTests
+ {
+ private IApiMovieRequest _api;
+
+ [TestInitialize]
+ public void TestInit()
+ {
+ ApiResponseUtil.ThrottleTests();
+
+ _api = MovieDbFactory.Create().Value;
+ }
+
+ [TestMethod]
+ public async Task GetRecommendationsAsync_Returns_ValidResults()
+ {
+ const int movieIdRunLolaRun = 104;
+
+ ApiSearchResponse response = await _api.GetRecommendationsAsync( movieIdRunLolaRun );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+
+ Assert.IsTrue( response.TotalPages > 1 );
+ Assert.IsTrue( response.TotalResults > 20 );
+ Assert.AreEqual( 1, response.PageNumber );
+ }
+
+ [TestMethod]
+ public async Task GetRecommendationsAsync_HasError_InvalidMovieId()
+ {
+ const int movieId = 1;
+
+ ApiSearchResponse response = await _api.GetRecommendationsAsync( movieId );
+ Assert.IsNotNull( response.Error );
+ }
+ }
+}
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetSimilar.cs b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetSimilar.cs
new file mode 100644
index 0000000..dd9e7cb
--- /dev/null
+++ b/DM.MovieApi.IntegrationTests/MovieDb/Movies/ApiMovieRequestTests_GetSimilar.cs
@@ -0,0 +1,98 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using DM.MovieApi.ApiResponse;
+using DM.MovieApi.MovieDb.Movies;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace DM.MovieApi.IntegrationTests.MovieDb.Movies
+{
+ [TestClass]
+ public class GetSimilarTests
+ {
+ private IApiMovieRequest _api;
+
+ [TestInitialize]
+ public void TestInit()
+ {
+ ApiResponseUtil.ThrottleTests();
+
+ _api = MovieDbFactory.Create().Value;
+ }
+
+ [TestMethod]
+ public async Task GetSimilarAsync_Returns_ValidResults()
+ {
+ const int movieIdRunLolaRun = 104;
+
+ ApiSearchResponse response = await _api.GetSimilarAsync( movieIdRunLolaRun );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+
+ // get similar will return the max number of results
+ Assert.AreEqual( 20, response.Results.Count );
+ Assert.AreEqual( 500, response.TotalPages );
+ Assert.AreEqual( 10000, response.TotalResults );
+ Assert.AreEqual( 1, response.PageNumber );
+ }
+
+ [TestMethod]
+ public async Task GetSimilarAsync_CanPage()
+ {
+ const int movieIdRunLolaRun = 104;
+
+ // todo: move to ApiResponseUtil and refactor w/ existing AssertCanPageSearchResponse
+ // * each component may be able to become re-usable methods
+
+ int num = 0;
+ var ids = new HashSet();
+ var dups = new List();
+
+ for( int i = 1; i <= 10; i++ )
+ {
+ int pageNumber = i;
+ ApiSearchResponse response = await _api.GetSimilarAsync( movieIdRunLolaRun, pageNumber );
+
+ ApiResponseUtil.AssertErrorIsNull( response );
+ ApiResponseUtil.AssertMovieInformationStructure( response.Results );
+
+ // get similar will return the max number of results
+ Assert.AreEqual( 20, response.Results.Count );
+ Assert.AreEqual( 500, response.TotalPages );
+ Assert.AreEqual( 10000, response.TotalResults );
+ Assert.AreEqual( pageNumber, response.PageNumber );
+
+ foreach( MovieInfo m in response.Results )
+ {
+ num++;
+ if( ids.Add( m.Id ) == false )
+ {
+ dups.Add( m.ToString() );
+ }
+ }
+ }
+
+ // api tends to return duplicate results when paging
+ // shouldn't be more than 2 or 3 per page; at 20 per page,
+ // that's approximately 4-6; let's target 25%
+ int min = (int)(num * 0.75);
+ var d = dups
+ .GroupBy( x => x )
+ .Select( x => $"{x.Key} (x {x.Count()})" );
+ System.Diagnostics.Trace.WriteLine( $"Results: {num}, Dups: {dups.Count}" +
+ $"\r\n{string.Join( "\r\n", d )}" );
+
+ Assert.IsTrue( ids.Count >= min, $"Total: {num}.\r\nUnique: {ids.Count}, Dup Threshold {min}" );
+ }
+
+ [TestMethod]
+ public async Task GetSimilarAsync_HasError_InvalidMovieId()
+ {
+ const int movieId = 1;
+
+ ApiSearchResponse response = await _api.GetSimilarAsync( movieId );
+ Assert.IsNotNull( response.Error );
+ }
+ }
+}
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/People/ApiPeopleRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/People/ApiPeopleRequestTests.cs
index 3041b6a..4632a53 100644
--- a/DM.MovieApi.IntegrationTests/MovieDb/People/ApiPeopleRequestTests.cs
+++ b/DM.MovieApi.IntegrationTests/MovieDb/People/ApiPeopleRequestTests.cs
@@ -310,7 +310,7 @@ public async Task SearchByNameAsync_Milla_Jovovich_Returns_SingleResult_WithExpe
{
"The Fifth Element",
"Resident Evil",
- "Zoolander"
+ "Resident Evil: Apocalypse"
};
foreach( string role in roles )
@@ -326,9 +326,8 @@ public async Task SearchByNameAsync_CanPageResults()
{
const string query = "Cox";
const int minimumPageCount = 15;
- const int minimumTotalResultsCount = 300;
- await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount,
( search, pageNumber ) => _api.SearchByNameAsync( search, pageNumber ), null );
}
}
diff --git a/DM.MovieApi.IntegrationTests/MovieDb/TV/ApiTVShowRequestTests.cs b/DM.MovieApi.IntegrationTests/MovieDb/TV/ApiTVShowRequestTests.cs
index f192756..4a5f395 100644
--- a/DM.MovieApi.IntegrationTests/MovieDb/TV/ApiTVShowRequestTests.cs
+++ b/DM.MovieApi.IntegrationTests/MovieDb/TV/ApiTVShowRequestTests.cs
@@ -100,9 +100,8 @@ public async Task SearchByNameAsync_CanPageResults()
{
const string query = "full";
const int minimumPageCount = 4;
- const int minimumTotalResultsCount = 80;
- await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( query, minimumPageCount,
( search, pageNumber ) => _api.SearchByNameAsync( search, pageNumber ), x => x.Id );
}
@@ -224,9 +223,8 @@ public async Task GetTopRatedAsync_Returns_ValidResult()
public async Task GetTopRatedAsync_CanPageResults()
{
const int minimumPageCount = 5;
- const int minimumTotalResultsCount = 100;
- await ApiResponseUtil.AssertCanPageSearchResponse( "none", minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( "none", minimumPageCount,
( _, page ) => _api.GetTopRatedAsync( page ), x => x.Id );
}
@@ -246,9 +244,8 @@ public async Task GetPopularAsync_Returns_ValidResult()
public async Task GetPopularAsync_CanPageResults()
{
const int minimumPageCount = 5;
- const int minimumTotalResultsCount = 100;
- await ApiResponseUtil.AssertCanPageSearchResponse( "none", minimumPageCount, minimumTotalResultsCount,
+ await ApiResponseUtil.AssertCanPageSearchResponse( "none", minimumPageCount,
( _, page ) => _api.GetPopularAsync( page ), x => x.Id );
}
diff --git a/DM.MovieApi.IntegrationTests/MovieDbFactoryTests.cs b/DM.MovieApi.IntegrationTests/MovieDbFactoryTests.cs
index 27a4f67..22f4314 100644
--- a/DM.MovieApi.IntegrationTests/MovieDbFactoryTests.cs
+++ b/DM.MovieApi.IntegrationTests/MovieDbFactoryTests.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
@@ -88,13 +87,13 @@ public void Create_Returns_Lazy_IApiMovieRequest()
[TestMethod]
public void GetAllApiRequests_CanCreate_IMovieApi()
{
- List dbApi = typeof( IMovieDbApi )
+ PropertyInfo[] dbApi = typeof( IMovieDbApi )
.GetProperties()
.Where( x => typeof( IApiRequest ).IsAssignableFrom( x.PropertyType ) )
.Distinct()
- .ToList();
+ .ToArray();
- Assert.AreEqual( 8, dbApi.Count );
+ Assert.AreEqual( 9, dbApi.Length );
IMovieDbApi api;
diff --git a/DM.MovieApi/ApiRequest/ApiRequestBase.cs b/DM.MovieApi/ApiRequest/ApiRequestBase.cs
index 19ea1c8..1d4e6d0 100644
--- a/DM.MovieApi/ApiRequest/ApiRequestBase.cs
+++ b/DM.MovieApi/ApiRequest/ApiRequestBase.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
@@ -14,15 +14,7 @@ internal abstract class ApiRequestBase
{
private readonly IApiSettings _settings;
- protected ApiRequestBase( IApiSettings settings )
- {
- _settings = settings;
- }
-
- public async Task> QueryAsync( string command )
- => await QueryAsync( command, new Dictionary() );
-
- public async Task> QueryAsync( string command, IDictionary parameters )
+ static ApiRequestBase()
{
var settings = new JsonSerializerSettings
{
@@ -31,17 +23,25 @@ public async Task> QueryAsync( string command, IDictionar
};
settings.Converters.Add( new IsoDateTimeConverterEx() );
- return await QueryAsync( command, parameters, Deserializer );
-
- T Deserializer( string json )
- => JsonConvert.DeserializeObject( json, settings );
+ JsonConvert.DefaultSettings = () => settings;
}
+ protected ApiRequestBase( IApiSettings settings )
+ => _settings = settings;
+
+ public async Task> QueryAsync( string command )
+ => await QueryAsync( command, new Dictionary(), null );
+
+ public async Task> QueryAsync( string command, IDictionary parameters )
+ => await QueryAsync( command, parameters, null );
+
public async Task> QueryAsync( string command, Func deserializer )
=> await QueryAsync( command, new Dictionary(), deserializer );
public async Task> QueryAsync( string command, IDictionary parameters, Func deserializer )
{
+ deserializer ??= JsonConvert.DeserializeObject;
+
using HttpClient client = CreateClient();
string cmd = CreateCommand( command, parameters );
@@ -118,7 +118,7 @@ public async Task> SearchAsync( string command, int page
return error;
}
- var result = JsonConvert.DeserializeObject>( json, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
+ var result = JsonConvert.DeserializeObject>( json );
// ReSharper disable PossibleNullReferenceException
result.CommandText = response.RequestMessage.RequestUri.ToString();
diff --git a/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs b/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs
index a56ea14..805ba5f 100644
--- a/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs
+++ b/DM.MovieApi/ApiRequest/IsoDateTimeConverterEx.cs
@@ -37,7 +37,7 @@ public override object ReadJson( JsonReader reader, Type objectType, object exis
return new DateTime( year, 1, 1 );
}
- return default( DateTime );
+ return DateTime.UnixEpoch;
}
}
@@ -50,7 +50,7 @@ private void ConditionalTraceReaderValue( JsonReader reader )
val = "";
}
- Debug.WriteLine( $"IsoDateTimeConverterEx.JsonReader.Value: {val}" );
+ Trace.WriteLine( val, "IsoDateTimeConverterEx" );
}
}
}
diff --git a/DM.MovieApi/IMovieDbApi.cs b/DM.MovieApi/IMovieDbApi.cs
index bf525b4..5b1d0bc 100644
--- a/DM.MovieApi/IMovieDbApi.cs
+++ b/DM.MovieApi/IMovieDbApi.cs
@@ -1,6 +1,7 @@
-using DM.MovieApi.MovieDb.Certifications;
+using DM.MovieApi.MovieDb.Certifications;
using DM.MovieApi.MovieDb.Companies;
using DM.MovieApi.MovieDb.Configuration;
+using DM.MovieApi.MovieDb.Discover;
using DM.MovieApi.MovieDb.Genres;
using DM.MovieApi.MovieDb.IndustryProfessions;
using DM.MovieApi.MovieDb.Movies;
@@ -54,5 +55,10 @@ public interface IMovieDbApi
/// Provides access for retrieving information about People.
///
IApiPeopleRequest People { get; }
+
+ ///
+ /// Provides access for discovering movies based on a filtered set of parameters.
+ ///
+ IApiDiscoverRequest Discover { get; }
}
}
diff --git a/DM.MovieApi/MovieDb/Discover/ApiDiscoverRequest.cs b/DM.MovieApi/MovieDb/Discover/ApiDiscoverRequest.cs
new file mode 100644
index 0000000..0fb32c3
--- /dev/null
+++ b/DM.MovieApi/MovieDb/Discover/ApiDiscoverRequest.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using DM.MovieApi.ApiRequest;
+using DM.MovieApi.ApiResponse;
+using DM.MovieApi.MovieDb.Genres;
+using DM.MovieApi.MovieDb.Movies;
+using DM.MovieApi.Shims;
+
+namespace DM.MovieApi.MovieDb.Discover
+{
+ internal class ApiDiscoverRequest : ApiRequestBase, IApiDiscoverRequest
+ {
+ private readonly IApiGenreRequest _genreApi;
+
+ [ImportingConstructor]
+ public ApiDiscoverRequest( IApiSettings settings, IApiGenreRequest genreApi )
+ : base( settings )
+ {
+ _genreApi = genreApi;
+ }
+
+ public async Task> DiscoverMoviesAsync( IDiscoverMovieParameterBuilder builder, int pageNumber = 1, string language = "en" )
+ {
+ Dictionary param = builder.Build();
+ param.Add( "language", language );
+
+ const string command = "discover/movie";
+
+ ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param );
+
+ if( response.Error != null )
+ {
+ return response;
+ }
+
+ response.Results.PopulateGenres( _genreApi );
+
+ return response;
+ }
+ }
+}
diff --git a/DM.MovieApi/MovieDb/Discover/DiscoverMovieParameterBuilder.cs b/DM.MovieApi/MovieDb/Discover/DiscoverMovieParameterBuilder.cs
new file mode 100644
index 0000000..4fc44b2
--- /dev/null
+++ b/DM.MovieApi/MovieDb/Discover/DiscoverMovieParameterBuilder.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+
+namespace DM.MovieApi.MovieDb.Discover
+{
+ public class DiscoverMovieParameterBuilder : IDiscoverMovieParameterBuilder
+ {
+ private readonly Dictionary> _param;
+
+ public DiscoverMovieParameterBuilder()
+ {
+ _param = new Dictionary>();
+ }
+
+ public Dictionary Build()
+ {
+ var param = new Dictionary();
+
+ foreach( var kvp in _param )
+ {
+ param.Add( kvp.Key, string.Join( ",", kvp.Value ) );
+ }
+
+ return param;
+ }
+
+ public IDiscoverMovieParameterBuilder WithCast( int personId )
+ {
+ AddParamType( "with_cast" );
+
+ _param["with_cast"].Add( personId.ToString() );
+
+ return this;
+ }
+
+ public IDiscoverMovieParameterBuilder WithCrew( int personId )
+ {
+ AddParamType( "with_crew" );
+
+ _param["with_crew"].Add( personId.ToString() );
+
+ return this;
+ }
+
+ public IDiscoverMovieParameterBuilder WithGenre( int genreId )
+ {
+ AddParamType( "with_genres" );
+
+ _param["with_genres"].Add( genreId.ToString() );
+
+ return this;
+ }
+
+ public IDiscoverMovieParameterBuilder WithOriginalLanguage( string language )
+ {
+ AddParamType( "original_language" );
+
+ _param["original_language"].Add( language );
+
+ return this;
+ }
+
+ public IDiscoverMovieParameterBuilder ExcludeGenre( int genreId )
+ {
+ AddParamType( "without_genres" );
+
+ _param["without_genres"].Add( genreId.ToString() );
+
+ return this;
+ }
+
+ private void AddParamType( string name )
+ => _param.TryAdd( name, new List() );
+ }
+}
diff --git a/DM.MovieApi/MovieDb/Discover/IApiDiscoverRequest.cs b/DM.MovieApi/MovieDb/Discover/IApiDiscoverRequest.cs
new file mode 100644
index 0000000..29540b5
--- /dev/null
+++ b/DM.MovieApi/MovieDb/Discover/IApiDiscoverRequest.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+using DM.MovieApi.ApiRequest;
+using DM.MovieApi.ApiResponse;
+using DM.MovieApi.MovieDb.Movies;
+
+namespace DM.MovieApi.MovieDb.Discover
+{
+ ///
+ /// Interface for discovering movies based on the filter provided by the parameter builder.
+ ///
+ public interface IApiDiscoverRequest : IApiRequest
+ {
+ ///
+ /// Allows for the discovery of movies by various types of data provided to the parameter.
+ ///
+ /// Provides a method of adding several types of parameters to filter the query.
+ /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available.
+ /// Default is English. The ISO 639-1 language code to retrieve the result from.
+ Task> DiscoverMoviesAsync( IDiscoverMovieParameterBuilder builder, int pageNumber = 1, string language = "en" );
+ }
+}
diff --git a/DM.MovieApi/MovieDb/Discover/IDiscoverMovieParameterBuilder.cs b/DM.MovieApi/MovieDb/Discover/IDiscoverMovieParameterBuilder.cs
new file mode 100644
index 0000000..c3efc9e
--- /dev/null
+++ b/DM.MovieApi/MovieDb/Discover/IDiscoverMovieParameterBuilder.cs
@@ -0,0 +1,38 @@
+using System.Collections.Generic;
+
+namespace DM.MovieApi.MovieDb.Discover
+{
+ public interface IDiscoverMovieParameterBuilder
+ {
+ ///
+ /// Builds all parameters for use with an .
+ /// Typically called by the internal engine.
+ ///
+ Dictionary Build();
+
+ ///
+ /// Add for each language version to be returned in the query. May be invoked more than once.
+ ///
+ IDiscoverMovieParameterBuilder WithOriginalLanguage( string language );
+
+ ///
+ /// Add for each crew member to be returned in the query. May be invoked more than once.
+ ///
+ IDiscoverMovieParameterBuilder WithCrew( int personId );
+
+ ///
+ /// Add for each cast member to be returned in the query. May be invoked more than once.
+ ///
+ IDiscoverMovieParameterBuilder WithCast( int personId );
+
+ ///
+ /// Add for each genre to be included in the query. May be invoked more than once.
+ ///
+ IDiscoverMovieParameterBuilder WithGenre( int genre );
+
+ ///
+ /// Add for each genre to be excluded from the query. May be invoked more than once.
+ ///
+ IDiscoverMovieParameterBuilder ExcludeGenre( int genre );
+ }
+}
diff --git a/DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs b/DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs
index e7a1d60..ce758a5 100644
--- a/DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs
+++ b/DM.MovieApi/MovieDb/Movies/ApiMovieRequest.cs
@@ -156,5 +156,45 @@ public async Task> GetCreditsAsync( int movieId, s
return response;
}
+
+ public async Task> GetRecommendationsAsync( int movieId, int pageNumber = 1, string language = "en" )
+ {
+ var param = new Dictionary
+ {
+ {"language", language},
+ };
+
+ string command = $"movie/{movieId}/recommendations";
+ ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param );
+
+ if( response.Error != null )
+ {
+ return response;
+ }
+
+ response.Results.PopulateGenres( _genreApi );
+
+ return response;
+ }
+
+ public async Task> GetSimilarAsync( int movieId, int pageNumber = 1, string language = "en" )
+ {
+ var param = new Dictionary
+ {
+ {"language", language},
+ };
+
+ string command = $"movie/{movieId}/similar";
+ ApiSearchResponse response = await base.SearchAsync( command, pageNumber, param );
+
+ if( response.Error != null )
+ {
+ return response;
+ }
+
+ response.Results.PopulateGenres( _genreApi );
+
+ return response;
+ }
}
}
diff --git a/DM.MovieApi/MovieDb/Movies/IApiMovieRequest.cs b/DM.MovieApi/MovieDb/Movies/IApiMovieRequest.cs
index f919d5a..ea19faf 100644
--- a/DM.MovieApi/MovieDb/Movies/IApiMovieRequest.cs
+++ b/DM.MovieApi/MovieDb/Movies/IApiMovieRequest.cs
@@ -1,4 +1,4 @@
-using System.Threading.Tasks;
+using System.Threading.Tasks;
using DM.MovieApi.ApiRequest;
using DM.MovieApi.ApiResponse;
@@ -64,5 +64,22 @@ public interface IApiMovieRequest : IApiRequest
/// The movie Id is typically found from a more generic Movie query.
/// Default is English. The ISO 639-1 language code to retrieve the result from.
Task> GetCreditsAsync( int movieId, string language = "en" );
+
+ ///
+ /// Get a list of recommended movies for a movie.
+ ///
+ /// The movie Id is typically found from a more generic Movie query.
+ /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available.
+ /// Default is English. The ISO 639-1 language code to retrieve the result from.
+ Task> GetRecommendationsAsync( int movieId, int pageNumber = 1, string language = "en" );
+
+ ///
+ /// Get a list of similar movies. This is not the same as the "Recommendation" system you see on the website.
+ /// These items are assembled by looking at keywords and genres.
+ ///
+ /// The movie Id is typically found from a more generic Movie query.
+ /// Default is page 1. The page number to retrieve; the will contain the current page returned and the total number of pages available.
+ /// Default is English. The ISO 639-1 language code to retrieve the result from.
+ Task> GetSimilarAsync( int movieId, int pageNumber = 1, string language = "en" );
}
}
diff --git a/DM.MovieApi/MovieDb/Movies/MovieInfo.cs b/DM.MovieApi/MovieDb/Movies/MovieInfo.cs
index bca1e1f..31e6d93 100644
--- a/DM.MovieApi/MovieDb/Movies/MovieInfo.cs
+++ b/DM.MovieApi/MovieDb/Movies/MovieInfo.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using DM.MovieApi.MovieDb.Genres;
@@ -53,6 +53,7 @@ public MovieInfo()
{
GenreIds = Array.Empty();
Genres = Array.Empty();
+ ReleaseDate = DateTime.UnixEpoch;
}
public override string ToString()