diff --git a/SpotifyAPI.Web.Examples.ASP/Startup.cs b/SpotifyAPI.Web.Examples.ASP/Startup.cs index 43167411..00f7a1b2 100644 --- a/SpotifyAPI.Web.Examples.ASP/Startup.cs +++ b/SpotifyAPI.Web.Examples.ASP/Startup.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using SpotifyAPI.Web.Enums; namespace SpotifyAPI.Web.Examples.ASP { @@ -26,8 +25,8 @@ namespace SpotifyAPI.Web.Examples.ASP .AddCookie() .AddSpotify(options => { - var scopes = Scope.UserLibraryRead | Scope.UserModifyPlaybackState; - options.Scope.Add(scopes.GetStringAttribute(",")); + // var scopes = Scope.UserLibraryRead | Scope.UserModifyPlaybackState; + // options.Scope.Add(scopes.GetStringAttribute(",")); options.SaveTokens = true; options.ClientId = Configuration["client_id"]; @@ -65,4 +64,4 @@ namespace SpotifyAPI.Web.Examples.ASP }); } } -} \ No newline at end of file +} diff --git a/SpotifyAPI.Web.Tests/Http/APIConnectorTest.cs b/SpotifyAPI.Web.Tests/Http/APIConnectorTest.cs new file mode 100644 index 00000000..44b83f97 --- /dev/null +++ b/SpotifyAPI.Web.Tests/Http/APIConnectorTest.cs @@ -0,0 +1,95 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using SpotifyAPI.Web.Http; + +namespace SpotifyAPI.Web.Tests +{ + [TestFixture] + public class APIConnectorTest + { + [Test] + public async Task RetryHandler_IsUsed() + { + var apiResponse = new Mock>(); + apiResponse.SetupGet(a => a.Body).Returns("Hello World"); + + var response = new Mock(); + response.SetupGet(r => r.ContentType).Returns("application/json"); + response.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.OK); + response.SetupGet(r => r.Body).Returns("\"Hello World\""); + + var authenticator = new Mock(); + var serializer = new Mock(); + serializer.Setup(s => s.DeserializeResponse(It.IsAny())).Returns(apiResponse.Object); + + var httpClient = new Mock(); + var retryHandler = new Mock(); + retryHandler.Setup(r => + r.HandleRetry( + It.IsAny(), + It.IsAny(), + It.IsAny>>() + ) + ).Returns(Task.FromResult(response.Object)); + + var apiConnector = new APIConnector( + new Uri("https://spotify.com"), + authenticator.Object, + serializer.Object, + httpClient.Object, + retryHandler.Object + ); + await apiConnector.SendAPIRequest(new Uri("/me", UriKind.Relative), HttpMethod.Get); + + authenticator.Verify(a => a.Apply(It.IsAny()), Times.Once); + httpClient.Verify(h => h.DoRequest(It.IsAny()), Times.Once); + serializer.Verify(s => s.DeserializeResponse(response.Object), Times.Once); + } + + [Test] + public async Task RetryHandler_CanRetry() + { + var apiResponse = new Mock>(); + apiResponse.SetupGet(a => a.Body).Returns("Hello World"); + + var response = new Mock(); + response.SetupGet(r => r.ContentType).Returns("application/json"); + response.SetupGet(r => r.StatusCode).Returns(HttpStatusCode.OK); + response.SetupGet(r => r.Body).Returns("\"Hello World\""); + + var authenticator = new Mock(); + var serializer = new Mock(); + serializer.Setup(s => s.DeserializeResponse(It.IsAny())).Returns(apiResponse.Object); + + var httpClient = new Mock(); + httpClient.Setup(h => h.DoRequest(It.IsAny())).Returns(Task.FromResult(response.Object)); + + var retryHandler = new Mock(); + retryHandler.Setup(r => + r.HandleRetry( + It.IsAny(), + It.IsAny(), + It.IsAny>>() + ) + ).Returns((IRequest request, IResponse response, Func> retry) => retry(request)); + + var apiConnector = new APIConnector( + new Uri("https://spotify.com"), + authenticator.Object, + serializer.Object, + httpClient.Object, + retryHandler.Object + ); + await apiConnector.SendAPIRequest(new Uri("/me", UriKind.Relative), HttpMethod.Get); + + serializer.Verify(s => s.SerializeRequest(It.IsAny()), Times.Once); + authenticator.Verify(a => a.Apply(It.IsAny()), Times.Exactly(2)); + httpClient.Verify(h => h.DoRequest(It.IsAny()), Times.Exactly(2)); + serializer.Verify(s => s.DeserializeResponse(response.Object), Times.Once); + } + } +} diff --git a/SpotifyAPI.Web.Tests/UtilTest.cs b/SpotifyAPI.Web.Tests/UtilTest.cs deleted file mode 100644 index 81c20f1d..00000000 --- a/SpotifyAPI.Web.Tests/UtilTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using NUnit.Framework; - -namespace SpotifyAPI.Web.Tests -{ - [TestFixture] - public class UtilTest - { - [Test] - public void TimestampShouldBeNoFloatingPoint() - { - string timestamp = DateTime.Now.ToUnixTimeMillisecondsPoly().ToString(); - - StringAssert.DoesNotContain(".", timestamp); - StringAssert.DoesNotContain(",", timestamp); - } - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs index 97d5ff3f..8a922605 100644 --- a/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs +++ b/SpotifyAPI.Web/Clients/SpotifyClientConfig.cs @@ -9,6 +9,7 @@ namespace SpotifyAPI.Web public IAuthenticator Authenticator { get; } public IJSONSerializer JSONSerializer { get; } public IHTTPClient HTTPClient { get; } + public IRetryHandler RetryHandler { get; } /// /// This config spefies the internal parts of the SpotifyClient. @@ -19,17 +20,20 @@ namespace SpotifyAPI.Web /// /// /// + /// public SpotifyClientConfig( Uri baseAddress, IAuthenticator authenticator, IJSONSerializer jsonSerializer, - IHTTPClient httpClient + IHTTPClient httpClient, + IRetryHandler retryHandler ) { BaseAddress = baseAddress; Authenticator = authenticator; JSONSerializer = jsonSerializer; HTTPClient = httpClient; + RetryHandler = retryHandler; } internal IAPIConnector CreateAPIConnector() @@ -40,7 +44,7 @@ namespace SpotifyAPI.Web Ensure.PropertyNotNull(JSONSerializer, nameof(JSONSerializer)); Ensure.PropertyNotNull(HTTPClient, nameof(HTTPClient)); - return new APIConnector(BaseAddress, Authenticator, JSONSerializer, HTTPClient); + return new APIConnector(BaseAddress, Authenticator, JSONSerializer, HTTPClient, RetryHandler); } public SpotifyClientConfig WithToken(string token, string tokenType = "Bearer") @@ -50,11 +54,14 @@ namespace SpotifyAPI.Web return WithAuthenticator(new TokenHeaderAuthenticator(token, tokenType)); } + public SpotifyClientConfig WithRetryHandler(IRetryHandler retryHandler) + { + return new SpotifyClientConfig(BaseAddress, Authenticator, JSONSerializer, HTTPClient, retryHandler); + } + public SpotifyClientConfig WithAuthenticator(IAuthenticator authenticator) { - Ensure.ArgumentNotNull(authenticator, nameof(authenticator)); - - return new SpotifyClientConfig(BaseAddress, Authenticator, JSONSerializer, HTTPClient); + return new SpotifyClientConfig(BaseAddress, authenticator, JSONSerializer, HTTPClient, RetryHandler); } public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer") @@ -79,7 +86,8 @@ namespace SpotifyAPI.Web SpotifyUrls.API_V1, authenticator, new NewtonsoftJSONSerializer(), - new NetHttpClient() + new NetHttpClient(), + null ); } } diff --git a/SpotifyAPI.Web/Enums/AlbumType.cs b/SpotifyAPI.Web/Enums/AlbumType.cs deleted file mode 100644 index 71f59b3f..00000000 --- a/SpotifyAPI.Web/Enums/AlbumType.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum AlbumType - { - [String("album")] - Album = 1, - - [String("single")] - Single = 2, - - [String("compilation")] - Compilation = 4, - - [String("appears_on")] - AppearsOn = 8, - - [String("album,single,compilation,appears_on")] - All = 16 - } -} diff --git a/SpotifyAPI.Web/Enums/FollowType.cs b/SpotifyAPI.Web/Enums/FollowType.cs deleted file mode 100644 index 4a1e76be..00000000 --- a/SpotifyAPI.Web/Enums/FollowType.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum FollowType - { - [String("artist")] - Artist = 1, - - [String("user")] - User = 2 - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web/Enums/RepeatState.cs b/SpotifyAPI.Web/Enums/RepeatState.cs deleted file mode 100644 index c97be5eb..00000000 --- a/SpotifyAPI.Web/Enums/RepeatState.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum RepeatState - { - [String("track")] - Track = 1, - - [String("context")] - Context = 2, - - [String("off")] - Off = 4 - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web/Enums/Scope.cs b/SpotifyAPI.Web/Enums/Scope.cs deleted file mode 100644 index bc66bc2c..00000000 --- a/SpotifyAPI.Web/Enums/Scope.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum Scope - { - [String("")] - None = 1, - - [String("playlist-modify-public")] - PlaylistModifyPublic = 2, - - [String("playlist-modify-private")] - PlaylistModifyPrivate = 4, - - [String("playlist-read-private")] - PlaylistReadPrivate = 8, - - [String("streaming")] - Streaming = 16, - - [String("user-read-private")] - UserReadPrivate = 32, - - [String("user-read-email")] - UserReadEmail = 64, - - [String("user-library-read")] - UserLibraryRead = 128, - - [String("user-library-modify")] - UserLibraryModify = 256, - - [String("user-follow-modify")] - UserFollowModify = 512, - - [String("user-follow-read")] - UserFollowRead = 1024, - - [String("user-read-birthdate")] - UserReadBirthdate = 2048, - - [String("user-top-read")] - UserTopRead = 4096, - - [String("playlist-read-collaborative")] - PlaylistReadCollaborative = 8192, - - [String("user-read-recently-played")] - UserReadRecentlyPlayed = 16384, - - [String("user-read-playback-state")] - UserReadPlaybackState = 32768, - - [String("user-modify-playback-state")] - UserModifyPlaybackState = 65536, - - [String("user-read-currently-playing")] - UserReadCurrentlyPlaying = 131072, - - [String("app-remote-control")] - AppRemoteControl = 262144, - - [String("ugc-image-upload")] - UgcImageUpload = 524288 - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web/Enums/SearchType.cs b/SpotifyAPI.Web/Enums/SearchType.cs deleted file mode 100644 index abf24c0a..00000000 --- a/SpotifyAPI.Web/Enums/SearchType.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum SearchType - { - [String("artist")] - Artist = 1, - - [String("album")] - Album = 2, - - [String("track")] - Track = 4, - - [String("playlist")] - Playlist = 8, - - [String("track,album,artist,playlist")] - All = 16 - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web/Enums/TimeRangeType.cs b/SpotifyAPI.Web/Enums/TimeRangeType.cs deleted file mode 100644 index 8c5fb4c4..00000000 --- a/SpotifyAPI.Web/Enums/TimeRangeType.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - /// - /// Only one value allowed - /// - [Flags] - public enum TimeRangeType - { - [String("long_term")] - LongTerm = 1, - - [String("medium_term")] - MediumTerm = 2, - - [String("short_term")] - ShortTerm = 4 - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web/Enums/TrackType.cs b/SpotifyAPI.Web/Enums/TrackType.cs deleted file mode 100644 index 74baea9a..00000000 --- a/SpotifyAPI.Web/Enums/TrackType.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace SpotifyAPI.Web.Enums -{ - [Flags] - public enum TrackType - { - [String("track")] - Track = 1, - - [String("episode")] - Episode = 2, - - [String("ad")] - Ad = 4, - - [String("unknown")] - Unknown = 8 - } -} \ No newline at end of file diff --git a/SpotifyAPI.Web/Http/APIConnector.cs b/SpotifyAPI.Web/Http/APIConnector.cs index 1c8a41b1..9d56a8b9 100644 --- a/SpotifyAPI.Web/Http/APIConnector.cs +++ b/SpotifyAPI.Web/Http/APIConnector.cs @@ -12,16 +12,23 @@ namespace SpotifyAPI.Web.Http private readonly IAuthenticator _authenticator; private readonly IJSONSerializer _jsonSerializer; private readonly IHTTPClient _httpClient; + private readonly IRetryHandler _retryHandler; public APIConnector(Uri baseAddress, IAuthenticator authenticator) : - this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient()) + this(baseAddress, authenticator, new NewtonsoftJSONSerializer(), new NetHttpClient(), null) { } - public APIConnector(Uri baseAddress, IAuthenticator authenticator, IJSONSerializer jsonSerializer, IHTTPClient httpClient) + public APIConnector( + Uri baseAddress, + IAuthenticator authenticator, + IJSONSerializer jsonSerializer, + IHTTPClient httpClient, + IRetryHandler retryHandler) { _baseAddress = baseAddress; _authenticator = authenticator; _jsonSerializer = jsonSerializer; _httpClient = httpClient; + _retryHandler = retryHandler; } public Task Delete(Uri uri) @@ -106,7 +113,7 @@ namespace SpotifyAPI.Web.Http _httpClient.SetRequestTimeout(timeout); } - private async Task SendAPIRequest( + public async Task SendAPIRequest( Uri uri, HttpMethod method, IDictionary parameters = null, @@ -128,6 +135,11 @@ namespace SpotifyAPI.Web.Http _jsonSerializer.SerializeRequest(request); await _authenticator.Apply(request).ConfigureAwait(false); IResponse response = await _httpClient.DoRequest(request).ConfigureAwait(false); + response = await _retryHandler?.HandleRetry(request, response, async (newRequest) => + { + await _authenticator.Apply(newRequest).ConfigureAwait(false); + return await _httpClient.DoRequest(request).ConfigureAwait(false); + }); ProcessErrors(response); IAPIResponse apiResponse = _jsonSerializer.DeserializeResponse(response); diff --git a/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs b/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs index c46fb4be..33f9c930 100644 --- a/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs +++ b/SpotifyAPI.Web/Http/Interfaces/IAPIConnector.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; namespace SpotifyAPI.Web.Http @@ -27,6 +28,8 @@ namespace SpotifyAPI.Web.Http Task Delete(Uri uri, IDictionary parameters); Task Delete(Uri uri, IDictionary parameters, object body); + Task SendAPIRequest(Uri uri, HttpMethod method, IDictionary parameters = null, object body = null); + void SetRequestTimeout(TimeSpan timeout); } } diff --git a/SpotifyAPI.Web/Http/Interfaces/IRetryHandler.cs b/SpotifyAPI.Web/Http/Interfaces/IRetryHandler.cs new file mode 100644 index 00000000..738b048a --- /dev/null +++ b/SpotifyAPI.Web/Http/Interfaces/IRetryHandler.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace SpotifyAPI.Web.Http +{ + /// + /// The Retry Handler will be directly called after the response is retrived and before errors and body are processed. + /// + public interface IRetryHandler + { + Task HandleRetry(IRequest request, IResponse response, Func> retry); + } +} diff --git a/SpotifyAPI.Web/Util.cs b/SpotifyAPI.Web/Util.cs deleted file mode 100644 index 2cb2ca5b..00000000 --- a/SpotifyAPI.Web/Util.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace SpotifyAPI.Web -{ - public static class Util - { - public static string GetStringAttribute(this T en, string separator = "") where T : struct, IConvertible - { - Enum e = (Enum) (object) en; - IEnumerable attributes = - Enum.GetValues(typeof(T)) - .Cast() - .Where(v => e.HasFlag((Enum) (object) v)) - .Select(v => typeof(T).GetField(v.ToString(CultureInfo.InvariantCulture))) - .Select(f => f.GetCustomAttributes(typeof(StringAttribute), false) [0]) - .Cast(); - - List list = new List(); - attributes.ToList().ForEach(element => list.Add(element.Text)); - return string.Join(separator, list); - } - - public static long ToUnixTimeMillisecondsPoly(this DateTime time) - { - return (long) time.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds; - } - } - - public sealed class StringAttribute : Attribute - { - public string Text { get; set; } - - public StringAttribute(string text) - { - Text = text; - } - } -} \ No newline at end of file