-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature/save identity provider id (#99)
* feat: save identity provider id when first login * feat: save multiple identity provider ids for same email * fix: oauth2 client get OidcUser * feat: generate random nickname * refactor: coding style
- Loading branch information
Showing
10 changed files
with
149 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,19 @@ | ||
package tw.waterballsa.gaas.domain | ||
import tw.waterballsa.gaas.domain.Room.Player | ||
|
||
class User( | ||
val id: Id? = null, | ||
val email: String, | ||
var nickname: String = "", | ||
val email: String = "", | ||
val nickname: String = "", | ||
val identities: MutableList<String> = mutableListOf() | ||
) { | ||
@JvmInline | ||
value class Id(val value: String) | ||
|
||
fun hasIdentity(identityProviderId: String): Boolean { | ||
return identities.contains(identityProviderId) | ||
} | ||
|
||
fun addIdentity(identityProviderId: String) { | ||
identities.add(identityProviderId) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,9 +6,12 @@ import org.springframework.beans.factory.annotation.Autowired | |
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc | ||
import org.springframework.boot.test.context.SpringBootTest | ||
import org.springframework.http.MediaType.APPLICATION_JSON | ||
import org.springframework.security.oauth2.jwt.Jwt | ||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt | ||
import org.springframework.test.web.servlet.MockMvc | ||
import org.springframework.test.web.servlet.ResultActions | ||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder | ||
import tw.waterballsa.gaas.domain.User | ||
|
||
@SpringBootTest | ||
@AutoConfigureMockMvc | ||
|
@@ -19,6 +22,20 @@ abstract class AbstractSpringBootTest { | |
@Autowired | ||
private lateinit var objectMapper: ObjectMapper | ||
|
||
protected final val mockUser: User = User( | ||
User.Id("1"), | ||
"[email protected]", | ||
"user-437b200d-da9c-449e-b147-114b4822b5aa", | ||
mutableListOf("google-oauth2|102527320242660434908") | ||
) | ||
|
||
protected final fun String.toJwt(): Jwt = | ||
Jwt.withTokenValue("mock-token") | ||
.header("alg", "none") | ||
.subject(this) | ||
.claim("email", mockUser.email) | ||
.build() | ||
|
||
protected fun <T> ResultActions.getBody(type: Class<T>): T = | ||
andReturn().response.contentAsString.let { objectMapper.readValue(it, type) } | ||
|
||
|
@@ -29,4 +46,8 @@ abstract class AbstractSpringBootTest { | |
|
||
protected fun MockHttpServletRequestBuilder.withJson(request: Any): MockHttpServletRequestBuilder = | ||
contentType(APPLICATION_JSON).content(request.toJson()) | ||
|
||
protected fun MockHttpServletRequestBuilder.withJwt(jwt: Jwt): MockHttpServletRequestBuilder = | ||
with(jwt().jwt(jwt)) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,11 +4,7 @@ import org.assertj.core.api.Assertions.assertThat | |
import org.junit.jupiter.api.BeforeEach | ||
import org.junit.jupiter.api.Test | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.security.oauth2.core.oidc.OidcIdToken | ||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo | ||
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser | ||
import org.springframework.security.oauth2.core.oidc.user.OidcUser | ||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oidcLogin | ||
import org.springframework.security.oauth2.jwt.Jwt | ||
import org.springframework.test.web.servlet.ResultActions | ||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get | ||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status | ||
|
@@ -20,63 +16,88 @@ class OAuth2ControllerTest @Autowired constructor( | |
val userRepository: UserRepository, | ||
) : AbstractSpringBootTest() { | ||
|
||
private final val googleIdentityProviderId = "google-oauth2|102527320242660434908" | ||
private final val discordIdentityProviderId = "discord|102527320242660434908" | ||
|
||
private final val googleOAuth2Jwt = googleIdentityProviderId.toJwt() | ||
private final val discordOAuth2Jwt = discordIdentityProviderId.toJwt() | ||
|
||
private final val invalidJwt = Jwt( | ||
"invalid_token", | ||
null, | ||
null, | ||
mapOf("alg" to "none"), | ||
mapOf("no_email" to "none") | ||
) | ||
|
||
@BeforeEach | ||
fun cleanUp() { | ||
userRepository.deleteAll() | ||
} | ||
|
||
@Test | ||
fun givenInvalidUserInfo_whenUserLogin_thenShouldLoginFailed() { | ||
givenInvalidUserInfo() | ||
.whenLogin() | ||
fun whenUserLoginWithInvalidJwt_thenShouldLoginFailed() { | ||
whenUserLogin(invalidJwt) | ||
.thenShouldLoginFailed() | ||
} | ||
|
||
@Test | ||
fun givenNewUser_whenUserLogin_thenCreateUser() { | ||
givenNewUserInfo().assertUserNotExists() | ||
.whenLogin() | ||
.thenShouldLoginSuccessfully() | ||
fun givenUserHasLoggedInViaGoogle_whenUserLoginWithGoogleOAuth2Jwt_thenLoginSuccessfully() { | ||
givenUserHasLoggedInViaGoogle() | ||
whenUserLogin(googleOAuth2Jwt) | ||
.thenLoginSuccessfully() | ||
} | ||
|
||
@Test | ||
fun givenOldUser_whenUserLogin_thenShouldLoginSuccessfully() { | ||
givenOldUserInfo().assertUserExists() | ||
.whenLogin() | ||
.thenShouldLoginSuccessfully() | ||
fun givenUserHasLoggedInViaGoogle_whenUserLoginWithDiscordOAuth2Jwt_thenUserHaveNewIdentity() { | ||
givenUserHasLoggedInViaGoogle() | ||
whenUserLogin(discordOAuth2Jwt) | ||
.thenUserHaveNewIdentity(googleIdentityProviderId, discordIdentityProviderId) | ||
} | ||
|
||
private fun givenInvalidUserInfo(): OidcUser = givenUserInfo(null) | ||
@Test | ||
fun whenUserLoginAtTheFirstTime_thenCreateNewUser() { | ||
whenUserLogin(googleOAuth2Jwt) | ||
.thenCreateNewUser() | ||
} | ||
|
||
private fun givenNewUserInfo(): OidcUser = givenUserInfo(OidcUserInfo(mapOf("email" to "[email protected]"))) | ||
private fun givenUserHasLoggedInViaGoogle(): User = | ||
userRepository.createUser(mockUser) | ||
|
||
private fun givenOldUserInfo(): OidcUser { | ||
val userInfo = givenUserInfo(OidcUserInfo(mapOf("email" to "[email protected]"))) | ||
userRepository.createUser(User(email = userInfo.email)) | ||
return userInfo | ||
} | ||
private fun whenUserLogin(jwt: Jwt): ResultActions = | ||
mockMvc.perform(get("/").withJwt(jwt)) | ||
|
||
private fun givenUserInfo(oidcUserInfo: OidcUserInfo?): OidcUser = DefaultOidcUser( | ||
listOf(), | ||
OidcIdToken("token", null, null, mapOf("sub" to "my_sub")), | ||
oidcUserInfo | ||
) | ||
private fun ResultActions.thenShouldLoginFailed() { | ||
andExpect(status().isBadRequest) | ||
} | ||
|
||
private fun OidcUser.assertUserExists(): OidcUser = this.also { | ||
assertThat(userRepository.existsUserByEmail(userInfo.email)).isTrue() | ||
private fun ResultActions.thenLoginSuccessfully() { | ||
andExpect(status().isOk) | ||
} | ||
|
||
private fun OidcUser.assertUserNotExists(): OidcUser = this.also { | ||
assertThat(userRepository.existsUserByEmail(userInfo.email)).isFalse() | ||
private fun ResultActions.thenUserHaveNewIdentity(vararg identityProviderIds: String) { | ||
thenLoginSuccessfully() | ||
userRepository.findByEmail(mockUser.email) | ||
?.thenWouldHaveIdentityProviderIds(*identityProviderIds) | ||
} | ||
|
||
private fun OidcUser.whenLogin(): ResultActions = | ||
mockMvc.perform(get("/").with(oidcLogin().oidcUser(this))) | ||
private fun ResultActions.thenCreateNewUser() { | ||
thenLoginSuccessfully() | ||
userRepository.findByEmail(mockUser.email) | ||
.thenNickNameShouldBeRandomName() | ||
.thenWouldHaveIdentityProviderIds(googleIdentityProviderId) | ||
} | ||
|
||
private fun ResultActions.thenShouldLoginSuccessfully(): ResultActions = | ||
andExpect(status().isOk) | ||
private fun User?.thenWouldHaveIdentityProviderIds(vararg identityProviderIds: String): User { | ||
assertThat(this).isNotNull | ||
assertThat(this!!.identities).containsAll(identityProviderIds.toList()) | ||
return this | ||
} | ||
|
||
private fun ResultActions.thenShouldLoginFailed(): ResultActions = | ||
andExpect(status().isBadRequest) | ||
private fun User?.thenNickNameShouldBeRandomName(): User { | ||
assertThat(this).isNotNull | ||
assertThat(this!!.nickname).startsWith("user_") | ||
return this | ||
} | ||
|
||
} |