Skip to content
This repository has been archived by the owner on Sep 12, 2021. It is now read-only.

Commit

Permalink
Merge pull request #121 from akkie/master
Browse files Browse the repository at this point in the history
Tests for Google provider and support for new API
  • Loading branch information
akkie committed Mar 2, 2014
2 parents b82b6c1 + e8ca1c0 commit 9b7f5f4
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ import OAuth2Provider._
* @param cacheLayer The cache layer implementation.
* @param httpLayer The HTTP layer implementation.
* @param settings The provider settings.
* @see https://developers.google.com/+/api/auth-migration#timetable
* @see https://developers.google.com/+/api/auth-migration#oauth2login
* @see https://developers.google.com/accounts/docs/OAuth2Login
* @see https://developers.google.com/+/api/latest/people
*/
class GoogleProvider(
protected val authInfoService: AuthInfoService,
Expand All @@ -62,27 +66,37 @@ class GoogleProvider(
val json = response.json
(json \ Error).asOpt[JsObject] match {
case Some(error) =>
val errorType = (error \ Type).as[String]
val errorCode = (error \ Code).as[Int]
val errorMsg = (error \ Message).as[String]

throw new AuthenticationException(SpecifiedProfileError.format(id, errorType, errorMsg))
throw new AuthenticationException(SpecifiedProfileError.format(id, errorCode, errorMsg))
case _ =>
val userID = (json \ ID).as[String]
val firstName = (json \ GivenName).asOpt[String]
val lastName = (json \ FamilyName).asOpt[String]
val fullName = (json \ Name).asOpt[String]
val avatarURL = (json \ Picture).asOpt[String]
val email = (json \ Email).asOpt[String]
val firstName = (json \ Name \ GivenName).asOpt[String]
val lastName = (json \ Name \ FamilyName).asOpt[String]
val fullName = (json \ DisplayName).asOpt[String]
val avatarURL = (json \ Image \ URL).asOpt[String]

// https://developers.google.com/+/api/latest/people#emails.type
val emailIndex = (json \ Emails \\ Type).indexWhere(_.as[String] == Account)
val emailValue = if ((json \ Emails \\ Value).isDefinedAt(emailIndex)) {
(json \ Emails \\ Value)(emailIndex).asOpt[String]
} else {
None
}

SocialProfile(
loginInfo = LoginInfo(id, userID),
firstName = firstName,
lastName = lastName,
fullName = fullName,
avatarURL = avatarURL,
email = email)
email = emailValue)
}
}.recover { case e => throw new AuthenticationException(UnspecifiedProfileError.format(id), e) }
}.recover {
case e if !e.isInstanceOf[AuthenticationException] =>
throw new AuthenticationException(UnspecifiedProfileError.format(id), e)
}
}
}

Expand All @@ -95,19 +109,23 @@ object GoogleProvider {
* The error messages.
*/
val UnspecifiedProfileError = "[Silhouette][%s] Error retrieving profile information"
val SpecifiedProfileError = "[Silhouette][%s] Error retrieving profile information. Error type: %s, message: %s"
val SpecifiedProfileError = "[Silhouette][%s] Error retrieving profile information. Error code: %s, message: %s"

/**
* The Google constants.
*/
val Google = "google"
val API = "https://www.googleapis.com/oauth2/v1/userinfo?access_token=%s"
val Type = "type"
val API = "https://www.googleapis.com/plus/v1/people/me?access_token=%s"
val Message = "message"
val ID = "id"
val Name = "name"
val GivenName = "given_name"
val FamilyName = "family_name"
val Picture = "picture"
val Email = "email"
val GivenName = "givenName"
val FamilyName = "familyName"
val DisplayName = "displayName"
val Image = "image"
val URL = "url"
val Emails = "emails"
val Value = "value"
val Type = "type"
val Account = "account"
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,9 @@ trait OAuth2ProviderSpecContext extends Scope with Mockito with ThrownExpectatio
*/
lazy val oAuthInfo: JsValue = Json.obj(
AccessToken -> "my.access.token",
TokenType -> "",
ExpiresIn -> 5,
RefreshToken -> "")
TokenType -> "bearer",
ExpiresIn -> 3600,
RefreshToken -> "my.refresh.token")

/**
* The OAuth2 settings.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Copyright 2014 Mohiva Organisation (license at mohiva dot com)
*
* 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.
*/
package com.mohiva.play.silhouette.core.providers.oauth2

import test.Helper
import java.util.UUID
import play.api.libs.ws.{ Response, WS }
import play.api.test.{ FakeRequest, WithApplication }
import scala.concurrent.Future
import com.mohiva.play.silhouette.core.providers._
import com.mohiva.play.silhouette.core.{ LoginInfo, AuthenticationException }
import GoogleProvider._
import OAuth2Provider._
import play.api.libs.json.Json

/**
* Test case for the [[com.mohiva.play.silhouette.core.providers.oauth2.GoogleProvider]] class.
*/
class GoogleProviderSpec extends OAuth2ProviderSpec {

"The authenticate method" should {
"throw AuthenticationException if OAuth2Info can be build because of an unexpected response" in new WithApplication with Context {
val cacheID = UUID.randomUUID().toString
val state = UUID.randomUUID().toString
val requestHolder = mock[WS.WSRequestHolder]
val response = mock[Response]
implicit val req = FakeRequest(GET, "?" + Code + "=my.code&" + State + "=" + state).withSession(CacheKey -> cacheID)
response.json returns Json.obj()
requestHolder.withHeaders(any) returns requestHolder
requestHolder.post[Map[String, Seq[String]]](any)(any, any) returns Future.successful(response)
cacheLayer.get[String](cacheID) returns Future.successful(Some(state))
httpLayer.url(oAuthSettings.accessTokenURL) returns requestHolder

await(provider.authenticate()) must throwAn[AuthenticationException].like {
case e => e.getMessage must startWith(InvalidResponseFormat.format(provider.id, ""))
}
}

"throw AuthenticationException if API returns error" in new WithApplication with Context {
val cacheID = UUID.randomUUID().toString
val state = UUID.randomUUID().toString
val requestHolder = mock[WS.WSRequestHolder]
val response = mock[Response]
implicit val req = FakeRequest(GET, "?" + Code + "=my.code&" + State + "=" + state).withSession(CacheKey -> cacheID)
response.json returns oAuthInfo thenReturns Helper.loadJson("providers/oauth2/google.error.json")
requestHolder.withHeaders(any) returns requestHolder
requestHolder.post[Map[String, Seq[String]]](any)(any, any) returns Future.successful(response)
requestHolder.get() returns Future.successful(response)
cacheLayer.get[String](cacheID) returns Future.successful(Some(state))
httpLayer.url(oAuthSettings.accessTokenURL) returns requestHolder
httpLayer.url(API.format("my.access.token")) returns requestHolder

await(provider.authenticate()) must throwAn[AuthenticationException].like {
case e => e.getMessage must equalTo(SpecifiedProfileError.format(
provider.id,
401,
"Invalid Credentials"))
}
}

"throw AuthenticationException if an unexpected error occurred" in new WithApplication with Context {
val cacheID = UUID.randomUUID().toString
val state = UUID.randomUUID().toString
val requestHolder = mock[WS.WSRequestHolder]
val response = mock[Response]
implicit val req = FakeRequest(GET, "?" + Code + "=my.code&" + State + "=" + state).withSession(CacheKey -> cacheID)
response.json returns oAuthInfo thenThrows new RuntimeException("")
requestHolder.withHeaders(any) returns requestHolder
requestHolder.post[Map[String, Seq[String]]](any)(any, any) returns Future.successful(response)
requestHolder.get() returns Future.successful(response)
cacheLayer.get[String](cacheID) returns Future.successful(Some(state))
httpLayer.url(oAuthSettings.accessTokenURL) returns requestHolder
httpLayer.url(API.format("my.access.token")) returns requestHolder

await(provider.authenticate()) must throwAn[AuthenticationException].like {
case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id))
}
}

"return the social profile with an email" in new WithApplication with Context {
val cacheID = UUID.randomUUID().toString
val state = UUID.randomUUID().toString
val requestHolder = mock[WS.WSRequestHolder]
val response = mock[Response]
implicit val req = FakeRequest(GET, "?" + Code + "=my.code&" + State + "=" + state).withSession(CacheKey -> cacheID)
response.json returns oAuthInfo thenReturns Helper.loadJson("providers/oauth2/google.success.json")
requestHolder.withHeaders(any) returns requestHolder
requestHolder.post[Map[String, Seq[String]]](any)(any, any) returns Future.successful(response)
requestHolder.get() returns Future.successful(response)
cacheLayer.get[String](cacheID) returns Future.successful(Some(state))
httpLayer.url(oAuthSettings.accessTokenURL) returns requestHolder
httpLayer.url(API.format("my.access.token")) returns requestHolder

await(provider.authenticate()) must beRight.like {
case p =>
p must be equalTo new SocialProfile(
loginInfo = LoginInfo(provider.id, "109476598527568979481"),
firstName = Some("Apollonia"),
lastName = Some("Vanova"),
fullName = Some("Apollonia Vanova"),
email = Some("apollonia.vanova@watchmen.com"),
avatarURL = Some("https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50")
)
}
}

"return the social profile without an email" in new WithApplication with Context {
val cacheID = UUID.randomUUID().toString
val state = UUID.randomUUID().toString
val requestHolder = mock[WS.WSRequestHolder]
val response = mock[Response]
implicit val req = FakeRequest(GET, "?" + Code + "=my.code&" + State + "=" + state).withSession(CacheKey -> cacheID)
response.json returns oAuthInfo thenReturns Helper.loadJson("providers/oauth2/google.without.email.json")
requestHolder.withHeaders(any) returns requestHolder
requestHolder.post[Map[String, Seq[String]]](any)(any, any) returns Future.successful(response)
requestHolder.get() returns Future.successful(response)
cacheLayer.get[String](cacheID) returns Future.successful(Some(state))
httpLayer.url(oAuthSettings.accessTokenURL) returns requestHolder
httpLayer.url(API.format("my.access.token")) returns requestHolder

await(provider.authenticate()) must beRight.like {
case p =>
p must be equalTo new SocialProfile(
loginInfo = LoginInfo(provider.id, "109476598527568979481"),
firstName = Some("Apollonia"),
lastName = Some("Vanova"),
fullName = Some("Apollonia Vanova"),
email = None,
avatarURL = Some("https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50")
)
}
}
}

/**
* Defines the context for the abstract OAuth2 provider spec.
*
* @return The Context to use for the abstract OAuth2 provider spec.
*/
override protected def context: OAuth2ProviderSpecContext = new Context {}

/**
* The context.
*/
trait Context extends OAuth2ProviderSpecContext {

/**
* The OAuth2 settings.
*/
lazy val oAuthSettings = OAuth2Settings(
authorizationURL = "https://accounts.google.com/o/oauth2/auth",
accessTokenURL = "https://accounts.google.com/o/oauth2/token",
redirectURL = "https://www.mohiva.com",
clientID = "my.client.id",
clientSecret = "my.client.secret",
scope = Some("profile,email"))

/**
* The provider to test.
*/
lazy val provider = new GoogleProvider(authInfoService, cacheLayer, httpLayer, oAuthSettings)
}
}
15 changes: 15 additions & 0 deletions test/resources/providers/oauth2/google.error.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"error": {
"errors": [
{
"domain": "global",
"reason": "authError",
"message": "Invalid Credentials",
"locationType": "header",
"location": "Authorization"
}
],
"code": 401,
"message": "Invalid Credentials"
}
}
21 changes: 21 additions & 0 deletions test/resources/providers/oauth2/google.success.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"emails": [
{
"value": "home@watchmen.com",
"type": "home"
},
{
"value": "apollonia.vanova@watchmen.com",
"type": "account"
}
],
"id": "109476598527568979481",
"displayName": "Apollonia Vanova",
"name": {
"familyName": "Vanova",
"givenName": "Apollonia"
},
"image": {
"url": "https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50"
}
}
11 changes: 11 additions & 0 deletions test/resources/providers/oauth2/google.without.email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"id": "109476598527568979481",
"displayName": "Apollonia Vanova",
"name": {
"familyName": "Vanova",
"givenName": "Apollonia"
},
"image": {
"url": "https://lh6.googleusercontent.com/-m34A6I77dJU/ASASAASADAAI/AVABAAAAAJk/5cg1hcjo_4s/photo.jpg?sz=50"
}
}

0 comments on commit 9b7f5f4

Please sign in to comment.