Skip to content

Commit

Permalink
Finalize Discover and Get Recommended Movies from PR's #45 & #46 (#47)
Browse files Browse the repository at this point in the history
* get recommended and similar movies (#45)

* get recommended and similar movies

* make necessary changes

* Fix formatting and whitespace. This follows the repo's already defined code styles.

* Shore up the tests for movie recommendations. Add more tests for MovieInfo in the util.

* Discover movies (#46)

* create discover request api

* create discover movie parameter builders

* indentation changes

* test discover movies

* seperate movie parameter builder

* remove extra lines and add new line

* add editor config file

Co-authored-by: Barış Can YILMAZ <baris@bariscanyilmaz>
Co-authored-by: kindler chase <[email protected]>

* Fix formatting and whitespace. This follows the repo's already defined code styles.

* Fix class name spelling error.

* Add error checking in DiscoverMoviesAsync.

Add error checking in DiscoverMoviesAsync; update conventions to follow repo conventions.

* Fix spelling error in method name.

* Delete unneeded parameter builder; should be on the main interface.

* Update method name for clarity.

* Clean up parameter builder with reusable method.

* Improve discover movie tests.

Improve discover movie tests. 
* Use the response util to validate results.
* Follow conventions of repo styles
* Fix copy/paste variable names to be correct names.
* Fix method names to reflect actual intent.

* Set the default date converter with Epoch time as default date.

Set the default date converter with Epoch time as default date. Sometimes the MovieInfo json is missing the release date, so add the expected pre-defined value as the default value.

* Modify ApiRequestBase to set the default json serialization settings.

Modify ApiRequestBase to set the default json serialization settings. This is a one time operation that all deserialization will use. Clean up the overloads a bit as well.

* Add documentation to the IApiDiscoverRequest; update param name.

* Fix broken test due to new IApiDiscoverRequest.

* Fix broken integration test.

Fix broken integration test. Apparently Milla is no longer known for zoolander.

* Fix integration test for correct updated count for IApiRequest objects.

* Prefer arrays over lists.

* Improve GetSimilar tests.

* Simplify AssertCanPageSearchResponse.

Simplify AssertCanPageSearchResponse. No need to keep track of every single search result duplicated; just need to track the overall duplicate results.

* Show duplicate results for GetSimilarAsync_CanPage. Lower dup threshold.

* Write to the Trace instead of Debug.

* Simplify AssertCanPageSearchResponse by removing min total results param.

Simplify AssertCanPageSearchResponse by removing min total results param. The api always returns results with a page size of 20. Just calc the expected size in the util and not as a param.

Co-authored-by: Barış Can Yılmaz <[email protected]>
Co-authored-by: Barış Can YILMAZ <baris@bariscanyilmaz>
  • Loading branch information
3 people authored Aug 16, 2022
1 parent 0cd724a commit d543d98
Show file tree
Hide file tree
Showing 21 changed files with 637 additions and 107 deletions.
107 changes: 61 additions & 46 deletions DM.MovieApi.IntegrationTests/ApiResponseUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

/// <summary>
/// Slows down the starting of tests to keep themoviedb.org api from denying the request
Expand All @@ -29,35 +30,50 @@ 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(
path.EndsWith( ".jpg" ) || path.EndsWith( ".png" ),
$"Actual: {path}" );
}

public static async Task AssertCanPageSearchResponse<T, TSearch>( TSearch search, int minimumPageCount, int minimumTotalResultsCount,
Func<TSearch, int, Task<ApiSearchResponse<T>>> apiSearch, Func<T, int> keySelector )
public static void AssertNoSearchResults<T>( ApiSearchResponse<T> 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<T, TSearch>(
TSearch search,
int minimumPageCount,
Func<TSearch, int, Task<ApiSearchResponse<T>>> apiSearch,
Func<T, int> keySelector )
{
if( minimumPageCount < 2 )
{
Assert.Fail( "minimumPageCount must be greater than 1." );
}

var allFound = new List<T>();
var ids = new HashSet<int>();
var dups = new List<string>();
int totalResults = 0;
int pageNumber = 1;

var priorResults = new Dictionary<int, int>();

do
{
System.Diagnostics.Trace.WriteLine( $"search: {search} | page: {pageNumber}", "ApiResponseUti.AssertCanPageSearchResponse" );
Log( $"search: {search} | page: {pageNumber}", "AssertCanPage" );
ApiSearchResponse<T> response = await apiSearch( search, pageNumber );

AssertErrorIsNull( response );
Expand All @@ -66,58 +82,46 @@ public static async Task AssertCanPageSearchResponse<T, TSearch>( TSearch search

if( typeof( T ) == typeof( Movie ) )
{
AssertMovieStructure( ( IEnumerable<Movie> )response.Results );
AssertMovieStructure( (IEnumerable<Movie>)response.Results );
}
else if( typeof( T ) == typeof( PersonInfo ) )
{
AssertPersonInfoStructure( ( IEnumerable<PersonInfo> )response.Results );
AssertPersonInfoStructure( (IEnumerable<PersonInfo>)response.Results );
}

if( keySelector == null )
{
allFound.AddRange( response.Results );
totalResults += response.Results.Count;
}
else
{
var current = new List<T>();
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 );

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 )
{
Expand All @@ -126,16 +130,21 @@ public static async Task AssertCanPageSearchResponse<T, TSearch>( TSearch search
return;
}

List<IGrouping<int, T>> groupById = allFound
.ToLookup( keySelector )
.ToList();

List<string> 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<PersonInfo> people )
Expand Down Expand Up @@ -232,9 +241,15 @@ public static void AssertMovieInformationStructure( IEnumerable<MovieInfo> 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 );
}

Expand Down Expand Up @@ -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 );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IApiDiscoverRequest>().Value;

Assert.IsInstanceOfType( _api, typeof( ApiDiscoverRequest ) );
}

[TestMethod]
public async Task DiscoverMovies_WithCrew()
{
int directorId = 66212;

IDiscoverMovieParameterBuilder builder = CreateBuilder();
builder.WithCrew( directorId );

ApiSearchResponse<MovieInfo> 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<MovieInfo> response = await _api.DiscoverMoviesAsync( builder );

ApiResponseUtil.AssertNoSearchResults( response );
}

[TestMethod]
public async Task DiscoverMovies_WithCast()
{
int actorId = 66462;

IDiscoverMovieParameterBuilder builder = CreateBuilder();
builder.WithCast( actorId );

ApiSearchResponse<MovieInfo> 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<MovieInfo> response = await _api.DiscoverMoviesAsync( builder );

ApiResponseUtil.AssertNoSearchResults( response );
}

[TestMethod]
public async Task DiscoverMovies_WithGenre()
{
int genreId = 28;

IDiscoverMovieParameterBuilder builder = CreateBuilder();
builder.WithGenre( genreId );

ApiSearchResponse<MovieInfo> 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<MovieInfo> 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<MovieInfo> 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<MovieInfo> response = await _api.DiscoverMoviesAsync( builder );

ApiResponseUtil.AssertErrorIsNull( response );
ApiResponseUtil.AssertMovieInformationStructure( response.Results );
}

private IDiscoverMovieParameterBuilder CreateBuilder()
=> new DiscoverMovieParameterBuilder();
}
}
Loading

0 comments on commit d543d98

Please sign in to comment.