Added first OAuth request: client credentials

This commit is contained in:
Jonas Dellinger 2020-05-13 23:49:54 +02:00
parent fee995f984
commit 9b8a4cd2c9
16 changed files with 140 additions and 31 deletions

View File

@ -1,7 +1,7 @@
{ {
"model-class": { "class-model": {
"scope": "csharp", "scope": "csharp",
"prefix": "model-class", "prefix": "class-model",
"body": [ "body": [
"namespace SpotifyAPI.Web", "namespace SpotifyAPI.Web",
"{", "{",
@ -15,9 +15,9 @@
], ],
"description": "Creates a new model" "description": "Creates a new model"
}, },
"request-class": { "class-request": {
"scope": "csharp", "scope": "csharp",
"prefix": "request-class", "prefix": "class-request",
"body": [ "body": [
"namespace SpotifyAPI.Web", "namespace SpotifyAPI.Web",
"{", "{",

View File

@ -37,9 +37,9 @@ namespace SpotifyAPI.Web
Assert.AreEqual(null, defaultConfig.HTTPLogger); Assert.AreEqual(null, defaultConfig.HTTPLogger);
Assert.AreEqual(null, defaultConfig.RetryHandler); Assert.AreEqual(null, defaultConfig.RetryHandler);
Assert.IsInstanceOf(typeof(TokenHeaderAuthenticator), defaultConfig.Authenticator); Assert.IsInstanceOf(typeof(TokenAuthenticator), defaultConfig.Authenticator);
var tokenHeaderAuth = defaultConfig.Authenticator as TokenHeaderAuthenticator; var tokenHeaderAuth = defaultConfig.Authenticator as TokenAuthenticator;
Assert.AreEqual(token, tokenHeaderAuth.Token); Assert.AreEqual(token, tokenHeaderAuth.Token);
Assert.AreEqual(tokenType, tokenHeaderAuth.TokenType); Assert.AreEqual(tokenType, tokenHeaderAuth.TokenType);
} }
@ -51,7 +51,7 @@ namespace SpotifyAPI.Web
var defaultConfig = SpotifyClientConfig.CreateDefault(); var defaultConfig = SpotifyClientConfig.CreateDefault();
var tokenConfig = defaultConfig.WithToken(token); var tokenConfig = defaultConfig.WithToken(token);
Assert.AreEqual(token, (tokenConfig.Authenticator as TokenHeaderAuthenticator).Token); Assert.AreEqual(token, (tokenConfig.Authenticator as TokenAuthenticator).Token);
Assert.AreNotEqual(defaultConfig, tokenConfig); Assert.AreNotEqual(defaultConfig, tokenConfig);
Assert.AreEqual(null, defaultConfig.Authenticator); Assert.AreEqual(null, defaultConfig.Authenticator);
} }

View File

@ -46,7 +46,7 @@ namespace SpotifyAPI.Web.Tests
); );
await apiConnector.SendAPIRequest<string>(new Uri("/me", UriKind.Relative), HttpMethod.Get).ConfigureAwait(false); await apiConnector.SendAPIRequest<string>(new Uri("/me", UriKind.Relative), HttpMethod.Get).ConfigureAwait(false);
authenticator.Verify(a => a.Apply(It.IsAny<IRequest>()), Times.Once); authenticator.Verify(a => a.Apply(It.IsAny<IRequest>(), It.IsAny<IAPIConnector>()), Times.Once);
httpClient.Verify(h => h.DoRequest(It.IsAny<IRequest>()), Times.Once); httpClient.Verify(h => h.DoRequest(It.IsAny<IRequest>()), Times.Once);
serializer.Verify(s => s.DeserializeResponse<string>(response.Object), Times.Once); serializer.Verify(s => s.DeserializeResponse<string>(response.Object), Times.Once);
} }
@ -89,7 +89,7 @@ namespace SpotifyAPI.Web.Tests
await apiConnector.SendAPIRequest<string>(new Uri("/me", UriKind.Relative), HttpMethod.Get).ConfigureAwait(false); await apiConnector.SendAPIRequest<string>(new Uri("/me", UriKind.Relative), HttpMethod.Get).ConfigureAwait(false);
serializer.Verify(s => s.SerializeRequest(It.IsAny<IRequest>()), Times.Once); serializer.Verify(s => s.SerializeRequest(It.IsAny<IRequest>()), Times.Once);
authenticator.Verify(a => a.Apply(It.IsAny<IRequest>()), Times.Exactly(2)); authenticator.Verify(a => a.Apply(It.IsAny<IRequest>(), It.IsAny<IAPIConnector>()), Times.Exactly(2));
httpClient.Verify(h => h.DoRequest(It.IsAny<IRequest>()), Times.Exactly(2)); httpClient.Verify(h => h.DoRequest(It.IsAny<IRequest>()), Times.Exactly(2));
serializer.Verify(s => s.DeserializeResponse<string>(response.Object), Times.Once); serializer.Verify(s => s.DeserializeResponse<string>(response.Object), Times.Once);
} }

View File

@ -6,16 +6,18 @@ using SpotifyAPI.Web.Http;
namespace SpotifyAPI.Web.Tests namespace SpotifyAPI.Web.Tests
{ {
[TestFixture] [TestFixture]
public class TokenHeaderAuthenticatorTest public class TokenAuthenticatorTest
{ {
[Test] [Test]
public void Apply_AddsCorrectHeader() public void Apply_AddsCorrectHeader()
{ {
var authenticator = new TokenHeaderAuthenticator("MyToken", "Bearer"); var authenticator = new TokenAuthenticator("MyToken", "Bearer");
var request = new Mock<IRequest>(); var request = new Mock<IRequest>();
var apiConnector = new Mock<IAPIConnector>();
request.SetupGet(r => r.Headers).Returns(new Dictionary<string, string>()); request.SetupGet(r => r.Headers).Returns(new Dictionary<string, string>());
authenticator.Apply(request.Object); authenticator.Apply(request.Object, apiConnector.Object);
Assert.AreEqual(request.Object.Headers["Authorization"], "Bearer MyToken"); Assert.AreEqual(request.Object.Headers["Authorization"], "Bearer MyToken");
} }
} }

View File

@ -11,6 +11,6 @@ namespace SpotifyAPI.Web
API = apiConnector; API = apiConnector;
} }
public IAPIConnector API { get; set; } protected IAPIConnector API { get; set; }
} }
} }

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace SpotifyAPI.Web
{
public interface IOAuthClient
{
Task<TokenResponse> RequestToken(ClientCredentialsRequest request);
}
}

View File

@ -0,0 +1,49 @@
using System.Text;
using System;
using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks;
using SpotifyAPI.Web.Http;
namespace SpotifyAPI.Web
{
public class OAuthClient : APIClient, IOAuthClient
{
public OAuthClient(IAPIConnector apiConnector) : base(apiConnector) { }
public OAuthClient() : this(SpotifyClientConfig.CreateDefault()) { }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062")]
public OAuthClient(SpotifyClientConfig config) : base(ValidateConfig(config)) { }
public Task<TokenResponse> RequestToken(ClientCredentialsRequest request)
{
Ensure.ArgumentNotNull(request, nameof(request));
var form = new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "client_credentials")
};
var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{request.ClientId}:{request.ClientSecret}"));
var headers = new Dictionary<string, string>
{
{ "Authorization", $"Basic {base64}"}
};
return API.Post<TokenResponse>(SpotifyUrls.OAuthToken, null, new FormUrlEncodedContent(form), headers);
}
private static APIConnector ValidateConfig(SpotifyClientConfig config)
{
Ensure.ArgumentNotNull(config, nameof(config));
return new APIConnector(
config.BaseAddress,
config.Authenticator,
config.JSONSerializer,
config.HTTPClient,
config.RetryHandler,
config.HTTPLogger
);
}
}
}

View File

@ -49,7 +49,7 @@ namespace SpotifyAPI.Web
return new SpotifyClientConfig( return new SpotifyClientConfig(
BaseAddress, BaseAddress,
new TokenHeaderAuthenticator(token, tokenType), new TokenAuthenticator(token, tokenType),
JSONSerializer, JSONSerializer,
HTTPClient, HTTPClient,
RetryHandler, RetryHandler,
@ -147,7 +147,7 @@ namespace SpotifyAPI.Web
public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer") public static SpotifyClientConfig CreateDefault(string token, string tokenType = "Bearer")
{ {
return CreateDefault().WithAuthenticator(new TokenHeaderAuthenticator(token, tokenType)); return CreateDefault().WithAuthenticator(new TokenAuthenticator(token, tokenType));
} }
public static SpotifyClientConfig CreateDefault() public static SpotifyClientConfig CreateDefault()

View File

@ -106,6 +106,13 @@ namespace SpotifyAPI.Web.Http
return SendAPIRequest<T>(uri, HttpMethod.Post, parameters, body); return SendAPIRequest<T>(uri, HttpMethod.Post, parameters, body);
} }
public Task<T> Post<T>(Uri uri, IDictionary<string, string> parameters, object body, Dictionary<string, string> headers)
{
Ensure.ArgumentNotNull(uri, nameof(uri));
return SendAPIRequest<T>(uri, HttpMethod.Post, parameters, body, headers);
}
public async Task<HttpStatusCode> Post(Uri uri, IDictionary<string, string> parameters, object body) public async Task<HttpStatusCode> Post(Uri uri, IDictionary<string, string> parameters, object body)
{ {
Ensure.ArgumentNotNull(uri, nameof(uri)); Ensure.ArgumentNotNull(uri, nameof(uri));
@ -160,13 +167,14 @@ namespace SpotifyAPI.Web.Http
Uri uri, Uri uri,
HttpMethod method, HttpMethod method,
IDictionary<string, string> parameters, IDictionary<string, string> parameters,
object body object body,
IDictionary<string, string> headers
) )
{ {
Ensure.ArgumentNotNull(uri, nameof(uri)); Ensure.ArgumentNotNull(uri, nameof(uri));
Ensure.ArgumentNotNull(method, nameof(method)); Ensure.ArgumentNotNull(method, nameof(method));
return new Request(new Dictionary<string, string>(), parameters) return new Request(headers ?? new Dictionary<string, string>(), parameters ?? new Dictionary<string, string>())
{ {
BaseAddress = _baseAddress, BaseAddress = _baseAddress,
Endpoint = uri, Endpoint = uri,
@ -184,7 +192,10 @@ namespace SpotifyAPI.Web.Http
private async Task<IResponse> DoRequest(IRequest request) private async Task<IResponse> DoRequest(IRequest request)
{ {
await _authenticator.Apply(request).ConfigureAwait(false); if (_authenticator != null)
{
await _authenticator.Apply(request, this).ConfigureAwait(false);
}
_httpLogger?.OnRequest(request); _httpLogger?.OnRequest(request);
IResponse response = await _httpClient.DoRequest(request).ConfigureAwait(false); IResponse response = await _httpClient.DoRequest(request).ConfigureAwait(false);
_httpLogger?.OnResponse(response); _httpLogger?.OnResponse(response);
@ -192,7 +203,10 @@ namespace SpotifyAPI.Web.Http
{ {
response = await _retryHandler.HandleRetry(request, response, async (newRequest) => response = await _retryHandler.HandleRetry(request, response, async (newRequest) =>
{ {
await _authenticator.Apply(newRequest).ConfigureAwait(false); if (_authenticator != null)
{
await _authenticator.Apply(request, this).ConfigureAwait(false);
}
var newResponse = await _httpClient.DoRequest(request).ConfigureAwait(false); var newResponse = await _httpClient.DoRequest(request).ConfigureAwait(false);
_httpLogger?.OnResponse(newResponse); _httpLogger?.OnResponse(newResponse);
return newResponse; return newResponse;
@ -206,10 +220,11 @@ namespace SpotifyAPI.Web.Http
Uri uri, Uri uri,
HttpMethod method, HttpMethod method,
IDictionary<string, string> parameters = null, IDictionary<string, string> parameters = null,
object body = null object body = null,
IDictionary<string, string> headers = null
) )
{ {
var request = CreateRequest(uri, method, parameters, body); var request = CreateRequest(uri, method, parameters, body, headers);
return DoRequest(request); return DoRequest(request);
} }
@ -217,10 +232,11 @@ namespace SpotifyAPI.Web.Http
Uri uri, Uri uri,
HttpMethod method, HttpMethod method,
IDictionary<string, string> parameters = null, IDictionary<string, string> parameters = null,
object body = null object body = null,
IDictionary<string, string> headers = null
) )
{ {
var request = CreateRequest(uri, method, parameters, body); var request = CreateRequest(uri, method, parameters, body, headers);
IAPIResponse<T> apiResponse = await DoSerializedRequest<T>(request).ConfigureAwait(false); IAPIResponse<T> apiResponse = await DoSerializedRequest<T>(request).ConfigureAwait(false);
return apiResponse.Body; return apiResponse.Body;
} }
@ -229,10 +245,11 @@ namespace SpotifyAPI.Web.Http
Uri uri, Uri uri,
HttpMethod method, HttpMethod method,
IDictionary<string, string> parameters = null, IDictionary<string, string> parameters = null,
object body = null object body = null,
IDictionary<string, string> headers = null
) )
{ {
var request = CreateRequest(uri, method, parameters, body); var request = CreateRequest(uri, method, parameters, body, headers);
var response = await DoSerializedRequest<object>(request).ConfigureAwait(false); var response = await DoSerializedRequest<object>(request).ConfigureAwait(false);
return response.Response; return response.Response;
} }

View File

@ -2,9 +2,9 @@ using System.Threading.Tasks;
namespace SpotifyAPI.Web.Http namespace SpotifyAPI.Web.Http
{ {
public class TokenHeaderAuthenticator : IAuthenticator public class TokenAuthenticator : IAuthenticator
{ {
public TokenHeaderAuthenticator(string token, string tokenType) public TokenAuthenticator(string token, string tokenType)
{ {
Token = token; Token = token;
TokenType = tokenType; TokenType = tokenType;
@ -14,7 +14,7 @@ namespace SpotifyAPI.Web.Http
public string TokenType { get; set; } public string TokenType { get; set; }
public Task Apply(IRequest request) public Task Apply(IRequest request, IAPIConnector apiConnector)
{ {
Ensure.ArgumentNotNull(request, nameof(request)); Ensure.ArgumentNotNull(request, nameof(request));

View File

@ -24,6 +24,7 @@ namespace SpotifyAPI.Web.Http
Task<T> Post<T>(Uri uri); Task<T> Post<T>(Uri uri);
Task<T> Post<T>(Uri uri, IDictionary<string, string> parameters); Task<T> Post<T>(Uri uri, IDictionary<string, string> parameters);
Task<T> Post<T>(Uri uri, IDictionary<string, string> parameters, object body); Task<T> Post<T>(Uri uri, IDictionary<string, string> parameters, object body);
Task<T> Post<T>(Uri uri, IDictionary<string, string> parameters, object body, Dictionary<string, string> headers);
Task<HttpStatusCode> Post(Uri uri, IDictionary<string, string> parameters, object body); Task<HttpStatusCode> Post(Uri uri, IDictionary<string, string> parameters, object body);
Task<T> Put<T>(Uri uri); Task<T> Put<T>(Uri uri);
@ -37,7 +38,11 @@ namespace SpotifyAPI.Web.Http
Task<T> Delete<T>(Uri uri, IDictionary<string, string> parameters, object body); Task<T> Delete<T>(Uri uri, IDictionary<string, string> parameters, object body);
Task<HttpStatusCode> Delete(Uri uri, IDictionary<string, string> parameters, object body); Task<HttpStatusCode> Delete(Uri uri, IDictionary<string, string> parameters, object body);
Task<T> SendAPIRequest<T>(Uri uri, HttpMethod method, IDictionary<string, string> parameters = null, object body = null); Task<T> SendAPIRequest<T>(
Uri uri, HttpMethod method,
IDictionary<string, string> parameters = null,
object body = null,
IDictionary<string, string> headers = null);
void SetRequestTimeout(TimeSpan timeout); void SetRequestTimeout(TimeSpan timeout);
} }

View File

@ -4,6 +4,6 @@ namespace SpotifyAPI.Web.Http
{ {
public interface IAuthenticator public interface IAuthenticator
{ {
Task Apply(IRequest request); Task Apply(IRequest request, IAPIConnector apiConnector);
} }
} }

View File

@ -0,0 +1,16 @@
namespace SpotifyAPI.Web
{
public class ClientCredentialsRequest
{
public ClientCredentialsRequest(string clientId, string clientSecret)
{
Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret));
ClientId = clientId;
ClientSecret = clientSecret;
}
public string ClientId { get; }
public string ClientSecret { get; }
}
}

View File

@ -0,0 +1,9 @@
namespace SpotifyAPI.Web
{
public class TokenResponse
{
public string AccessToken { get; set; }
public string TokenType { get; set; }
public int ExpiresIn { get; set; }
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.1</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <!-- <GenerateDocumentationFile>true</GenerateDocumentationFile> -->
<PackageId>SpotifyAPI.Web</PackageId> <PackageId>SpotifyAPI.Web</PackageId>
<Title>SpotifyAPI.Web</Title> <Title>SpotifyAPI.Web</Title>
<Authors>Jonas Dellinger</Authors> <Authors>Jonas Dellinger</Authors>

View File

@ -7,6 +7,8 @@ namespace SpotifyAPI.Web
public static readonly Uri APIV1 = new Uri("https://api.spotify.com/v1/"); public static readonly Uri APIV1 = new Uri("https://api.spotify.com/v1/");
public static readonly Uri OAuthToken = new Uri("https://accounts.spotify.com/api/token");
public static Uri Me() => EUri($"me"); public static Uri Me() => EUri($"me");
public static Uri User(string userId) => EUri($"users/{userId}"); public static Uri User(string userId) => EUri($"users/{userId}");