Index: Src/GoogleApis.Auth.Tests/OAuth2/AuthorizationCodeFlowTests.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/AuthorizationCodeFlowTests.cs @@ -0,0 +1,425 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Moq; +using NUnit.Framework; + +using Google.Apis.Auth.OAuth2.Requests; +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Http; +using Google.Apis.Json; +using Google.Apis.Testing; +using Google.Apis.Tests; +using Google.Apis.Util; +using Google.Apis.Util.Store; + +namespace Google.Apis.Auth.OAuth2 +{ + /// Tests for . + [TestFixture] + public class AuthorizationCodeFlowTests + { + private const string TokenUrl = "https://token.com"; + private const string AuthorizationCodeUrl = "https://authorization.com"; + + #region Constructor + + [Test] + public void TestConstructor_ArgumentException() + { + // ClientSecrets are missing. + try + { + new AuthorizationCodeFlow(new AuthorizationCodeFlow.Initializer( + "https://authorization_code.com", "https://token.com")); + Assert.Fail(); + } + catch (ArgumentException ex) + { + Assert.True(ex.Message.Contains("You MUST set ClientSecret or ClientSecretStream")); + } + } + + [Test] + public void TestConstructor_DefaultValues() + { + var flow = CreateFlow(); + Assert.NotNull(flow.AccessMethod); + Assert.That(flow.AccessMethod, Is.InstanceOf()); + Assert.That(flow.AuthorizationServerUrl, Is.EqualTo("https://authorization.com")); + Assert.NotNull(flow.ClientSecrets); + Assert.That(flow.ClientSecrets.ClientId, Is.EqualTo("id")); + Assert.That(flow.ClientSecrets.ClientSecret, Is.EqualTo("secret")); + Assert.That(flow.Clock, Is.InstanceOf()); + Assert.Null(flow.DataStore); + Assert.NotNull(flow.HttpClient); + Assert.NotNull(flow.Scopes); + Assert.That(flow.TokenServerUrl, Is.EqualTo("https://token.com")); + + Assert.That(flow.HttpClient.MessageHandler.UnsuccessfulResponseHandlers.Count(), Is.EqualTo(1)); + Assert.That(flow.HttpClient.MessageHandler.UnsuccessfulResponseHandlers.First(), + Is.InstanceOf()); + } + + #endregion + + #region LoadToken + + [Test] + public void LoadTokenAsync_NoDataStore() + { + var flow = CreateFlow(); + Assert.Null(flow.LoadTokenAsync("user", CancellationToken.None).Result); + } + + [Test] + public void LoadTokenAsync_NullResponse() + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(null); + Assert.Null(SubtestLoadTokenAsync(tcs)); + } + + [Test] + public void LoadTokenAsync_TokenResponse() + { + TokenResponse response = new TokenResponse + { + AccessToken = "access" + }; + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(response); + var result = SubtestLoadTokenAsync(tcs); + Assert.That(result, Is.EqualTo(response)); + } + + private TokenResponse SubtestLoadTokenAsync(TaskCompletionSource tcs) + { + var mock = new Mock(); + mock.Setup(ds => ds.GetAsync("user")).Returns(tcs.Task); + var flow = CreateFlow(dataStore: mock.Object); + var result = flow.LoadTokenAsync("user", CancellationToken.None).Result; + mock.Verify(ds => ds.GetAsync("user")); + return result; + } + + #endregion + + #region CreateAuthorizationCodeRequest + + [Test] + public void TestCreateAuthorizationCodeRequest() + { + var request = CreateFlow(scopes: new[] { "a", "b" }).CreateAuthorizationCodeRequest("redirect"); + Assert.That(request.AuthorizationServerUrl, Is.EqualTo(new Uri(AuthorizationCodeUrl))); + Assert.That(request.ClientId, Is.EqualTo("id")); + Assert.That(request.RedirectUri, Is.EqualTo("redirect")); + Assert.That(request.ResponseType, Is.EqualTo("code")); + Assert.That(request.Scope, Is.EqualTo("a b")); + Assert.Null(request.State); + } + + #endregion + + [Test] + public void TestExchangeCodeForTokenAsync() + { + var mock = new Mock(); + var handler = new FetchTokenMessageHandler(); + handler.AuthorizationCodeTokenRequest = new AuthorizationCodeTokenRequest() + { + Code = "c0de", + RedirectUri = "redIrect", + Scope = "a" + }; + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(null); + mock.Setup(ds => ds.StoreAsync("uSer", It.IsAny())).Returns(tcs.Task); + + var flow = CreateFlow(httpClientFactory: mockFactory, scopes: new[] { "a" }, dataStore: mock.Object); + var response = flow.ExchangeCodeForTokenAsync("uSer", "c0de", "redIrect", CancellationToken.None).Result; + SubtestTokenResponse(response); + + mock.Verify(ds => ds.StoreAsync("uSer", It.IsAny())); + } + + [Test] + public void TestRefreshTokenAsync() + { + var mock = new Mock(); + var handler = new FetchTokenMessageHandler(); + handler.RefreshTokenRequest = new RefreshTokenRequest() + { + RefreshToken = "REFRESH", + Scope = "a" + }; + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(null); + mock.Setup(ds => ds.StoreAsync("uSer", It.IsAny())).Returns(tcs.Task); + + var flow = CreateFlow(httpClientFactory: mockFactory, scopes: new[] { "a" }, dataStore: mock.Object); + var response = flow.RefreshTokenAsync("uSer", "REFRESH", CancellationToken.None).Result; + SubtestTokenResponse(response); + + + mock.Verify(ds => ds.StoreAsync("uSer", It.IsAny())); + } + + #region FetchToken + + /// + /// Fetch token message handler, which expects an authorization code token request or a refresh token request. + /// It verifies all the query parameters are valid and return an error response in case + /// is true. + /// + public class FetchTokenMessageHandler : CountableMessageHandler + { + internal AuthorizationCodeTokenRequest AuthorizationCodeTokenRequest { get; set; } + internal RefreshTokenRequest RefreshTokenRequest { get; set; } + internal bool Error { get; set; } + + protected override async Task SendAsyncCore(HttpRequestMessage request, + CancellationToken taskCancellationToken) + { + Assert.That(request.RequestUri, Is.EqualTo(new Uri(TokenUrl))); + + if (AuthorizationCodeTokenRequest != null) + { + // Verify right parameters. + var content = await request.Content.ReadAsStringAsync(); + foreach (var parameter in content.Split('&')) + { + var keyValue = parameter.Split('='); + switch (keyValue[0]) + { + case "code": + Assert.That(keyValue[1], Is.EqualTo("c0de")); + break; + case "redirect_uri": + Assert.That(keyValue[1], Is.EqualTo("redIrect")); + break; + case "scope": + Assert.That(keyValue[1], Is.EqualTo("a")); + break; + case "grant_type": + Assert.That(keyValue[1], Is.EqualTo("authorization_code")); + break; + case "client_id": + Assert.That(keyValue[1], Is.EqualTo("id")); + break; + case "client_secret": + Assert.That(keyValue[1], Is.EqualTo("secret")); + break; + default: + throw new ArgumentOutOfRangeException("Invalid parameter!"); + } + } + } + else + { + // Verify right parameters. + var content = await request.Content.ReadAsStringAsync(); + foreach (var parameter in content.Split('&')) + { + var keyValue = parameter.Split('='); + switch (keyValue[0]) + { + case "refresh_token": + Assert.That(keyValue[1], Is.EqualTo("REFRESH")); + break; + case "scope": + Assert.That(keyValue[1], Is.EqualTo("a")); + break; + case "grant_type": + Assert.That(keyValue[1], Is.EqualTo("refresh_token")); + break; + case "client_id": + Assert.That(keyValue[1], Is.EqualTo("id")); + break; + case "client_secret": + Assert.That(keyValue[1], Is.EqualTo("secret")); + break; + default: + throw new ArgumentOutOfRangeException("Invalid parameter!"); + } + } + } + + var response = new HttpResponseMessage(); + if (Error) + { + response.StatusCode = System.Net.HttpStatusCode.BadRequest; + var serializedObject = NewtonsoftJsonSerializer.Instance.Serialize(new TokenErrorResponse + { + Error = "error", + ErrorDescription = "desc", + ErrorUri = "uri" + }); + response.Content = new StringContent(serializedObject, Encoding.UTF8); + } + else + { + var serializedObject = NewtonsoftJsonSerializer.Instance.Serialize(new TokenResponse + { + AccessToken = "a", + RefreshToken = "r", + ExpiresInSeconds = 100, + Scope = "b", + }); + response.Content = new StringContent(serializedObject, Encoding.UTF8); + } + + return response; + } + } + + [Test] + public void TestFetchTokenAsync_AuthorizationCodeRequest() + { + var handler = new FetchTokenMessageHandler(); + handler.AuthorizationCodeTokenRequest = new AuthorizationCodeTokenRequest() + { + Code = "c0de", + RedirectUri = "redIrect", + Scope = "a" + }; + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + + var flow = CreateFlow(httpClientFactory: mockFactory); + var response = flow.FetchTokenAsync("user", handler.AuthorizationCodeTokenRequest, + CancellationToken.None).Result; + SubtestTokenResponse(response); + } + + [Test] + public void TestFetchTokenAsync_RefreshTokenRequest() + { + var handler = new FetchTokenMessageHandler(); + handler.RefreshTokenRequest = new RefreshTokenRequest() + { + RefreshToken = "REFRESH", + Scope = "a" + }; + + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + + var flow = CreateFlow(httpClientFactory: mockFactory); + var response = flow.FetchTokenAsync("user", handler.RefreshTokenRequest, CancellationToken.None).Result; + SubtestTokenResponse(response); + } + + [Test] + public void TestFetchTokenAsync_AuthorizationCodeRequest_Error() + { + var handler = new FetchTokenMessageHandler(); + handler.AuthorizationCodeTokenRequest = new AuthorizationCodeTokenRequest() + { + Code = "c0de", + RedirectUri = "redIrect", + Scope = "a" + }; + handler.Error = true; + SubtestFetchTokenAsync_Error(handler); + } + + [Test] + public void TestFetchTokenAsync_RefreshTokenRequest_Error() + { + var handler = new FetchTokenMessageHandler(); + handler.RefreshTokenRequest = new RefreshTokenRequest() + { + RefreshToken = "REFRESH", + Scope = "a" + }; + handler.Error = true; + SubtestFetchTokenAsync_Error(handler); + } + + /// Subtest for receiving an error token response. + /// The message handler. + private void SubtestFetchTokenAsync_Error(FetchTokenMessageHandler handler) + { + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + var flow = CreateFlow(httpClientFactory: mockFactory); + try + { + var request = + (TokenRequest)handler.AuthorizationCodeTokenRequest ?? (TokenRequest)handler.RefreshTokenRequest; + var result = flow.FetchTokenAsync("user", request, CancellationToken.None).Result; + Assert.Fail(); + } + catch (AggregateException aex) + { + var ex = aex.InnerException as TokenResponseException; + Assert.IsNotNull(ex); + var result = ex.Error; + Assert.That(result.Error, Is.EqualTo("error")); + Assert.That(result.ErrorDescription, Is.EqualTo("desc")); + Assert.That(result.ErrorUri, Is.EqualTo("uri")); + } + } + + #endregion + + /// Creates an authorization code flow with the given parameters. + /// The data store. + /// The Scopes. + /// The HTTP client factory. If not set the default will be used. + /// Authorization code flow + private AuthorizationCodeFlow CreateFlow(IDataStore dataStore = null, IEnumerable scopes = null, + IHttpClientFactory httpClientFactory = null) + { + var secrets = new ClientSecrets() { ClientId = "id", ClientSecret = "secret" }; + var initializer = new AuthorizationCodeFlow.Initializer(AuthorizationCodeUrl, TokenUrl) + { + ClientSecrets = secrets, + HttpClientFactory = httpClientFactory + }; + + if (dataStore != null) + { + initializer.DataStore = dataStore; + } + if (scopes != null) + { + initializer.Scopes = scopes; + } + return new AuthorizationCodeFlow(initializer); + } + + /// Verifies that the token response contains the expected data. + /// The token response + private void SubtestTokenResponse(TokenResponse response) + { + Assert.That(response.RefreshToken, Is.EqualTo("r")); + Assert.That(response.ExpiresInSeconds, Is.EqualTo(100)); + Assert.That(response.Scope, Is.EqualTo("b")); + } + } +}