This repository has been archived by the owner on Dec 15, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add SpringMockUserExtension to fake users
Compatible with Spring 2 and Spring security 5.
- Loading branch information
Ronny Bräunlich
committed
Nov 30, 2024
1 parent
ea1da9b
commit 80347a0
Showing
9 changed files
with
312 additions
and
1 deletion.
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
13 changes: 13 additions & 0 deletions
13
src/main/kotlin/io/kotest/extensions/spring/BeforeSpringExtension.kt
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 |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package io.kotest.extensions.spring | ||
|
||
import io.kotest.core.extensions.Extension | ||
import io.kotest.core.test.TestCase | ||
|
||
internal interface BeforeSpringExtension: Extension { | ||
|
||
/** | ||
* Called before testContextManager().beforeTestMethod() in SpringTestExtension | ||
*/ | ||
suspend fun beforeSpring(testCase: TestCase): Unit = Unit | ||
|
||
} |
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
69 changes: 69 additions & 0 deletions
69
src/main/kotlin/io/kotest/extensions/spring/security/SpringMockUserExtension.kt
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 |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package io.kotest.extensions.spring.security | ||
|
||
import io.kotest.core.listeners.AfterTestListener | ||
import io.kotest.core.test.TestCase | ||
import io.kotest.core.test.TestResult | ||
import io.kotest.extensions.spring.BeforeSpringExtension | ||
import io.kotest.extensions.spring.testContextManager | ||
import org.springframework.security.core.GrantedAuthority | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority | ||
import org.springframework.security.core.context.SecurityContext | ||
import org.springframework.security.test.context.TestSecurityContextHolder | ||
import org.springframework.security.test.context.support.TestExecutionEvent | ||
import org.springframework.security.test.context.support.WithMockUser | ||
import org.springframework.security.test.context.support.WithSecurityContext | ||
import org.springframework.security.test.context.support.WithSecurityContextFactory | ||
import org.springframework.test.context.TestContextAnnotationUtils | ||
|
||
class SpringMockUserExtension( | ||
private val username: String = "user", | ||
private val password: String = "password", | ||
private val roles: List<String> = listOf("USER"), | ||
private val authorities: List<String> = listOf() | ||
) : BeforeSpringExtension, AfterTestListener { | ||
|
||
override suspend fun beforeSpring(testCase: TestCase) { | ||
TestSecurityContextHolder.setContext(createSecurityContext()) | ||
} | ||
|
||
override suspend fun afterAny(testCase: TestCase, result: TestResult) { | ||
TestSecurityContextHolder.clearContext() | ||
} | ||
|
||
/** | ||
* Copied from Spring's WithMockUserSecurityContextFactory | ||
*/ | ||
private suspend fun createSecurityContext(): SecurityContext { | ||
// val grantedAuthorities: MutableList<GrantedAuthority> = | ||
// authorities.map { SimpleGrantedAuthority(it) }.toMutableList() | ||
// if (grantedAuthorities.isEmpty()) { | ||
// for (role in roles) { | ||
// require(!role.startsWith("ROLE_")) { "roles cannot start with ROLE_ Got $role" } | ||
// grantedAuthorities.add(SimpleGrantedAuthority("ROLE_$role")) | ||
// } | ||
// } else check(roles.size == 1 && "USER" == roles[0]) { | ||
// ("You cannot define roles attribute " + roles | ||
// + " with authorities attribute " + authorities) | ||
// } | ||
val withSecurityContextAnnotationDescriptor = | ||
TestContextAnnotationUtils.findAnnotationDescriptor( | ||
WithMockUser::class.java, | ||
WithSecurityContext::class.java | ||
)!!.annotation | ||
val factoryClazz: Class<out WithSecurityContextFactory<out Annotation>> = | ||
withSecurityContextAnnotationDescriptor.factory.java | ||
val factory = | ||
testContextManager().testContext.applicationContext.autowireCapableBeanFactory.createBean( | ||
factoryClazz | ||
) as WithSecurityContextFactory<WithMockUser> | ||
val withMockUser = WithMockUser( | ||
value = username, | ||
username = username, | ||
roles = roles.toTypedArray(), | ||
authorities = authorities.toTypedArray(), | ||
password = password, | ||
setupBefore = TestExecutionEvent.TEST_METHOD | ||
) | ||
return factory.createSecurityContext(withMockUser) | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
...est/kotlin/io/kotest/extensions/spring/security/SpringMockUserExtensionIntegrationTest.kt
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 |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package io.kotest.extensions.spring.security | ||
|
||
import io.kotest.core.spec.style.DescribeSpec | ||
import io.kotest.core.spec.style.FunSpec | ||
import io.kotest.extensions.spring.Components | ||
import io.kotest.extensions.spring.SpringExtension | ||
import org.springframework.beans.factory.annotation.Autowired | ||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient | ||
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest | ||
import org.springframework.boot.test.context.SpringBootTest | ||
import org.springframework.security.core.context.SecurityContextHolder | ||
import org.springframework.test.context.ContextConfiguration | ||
import org.springframework.test.web.reactive.server.WebTestClient | ||
import java.util.UUID | ||
|
||
@SpringBootTest(classes = [SpringTestApplication::class]) | ||
@AutoConfigureWebTestClient | ||
class SpringMockUserExtensionIntegrationTest( | ||
@Autowired private val webTestClient: WebTestClient, | ||
) : DescribeSpec() { | ||
|
||
override fun extensions() = listOf(SpringExtension) | ||
|
||
init { | ||
describe("ADMIN") { | ||
extensions(SpringMockUserExtension(authorities = listOf("ADMIN"))) | ||
it("should provide mock authentication") { | ||
webTestClient | ||
.get() | ||
.uri("/secure") | ||
.exchange() | ||
.expectStatus() | ||
.isOk | ||
} | ||
} | ||
} | ||
} | ||
|
141 changes: 141 additions & 0 deletions
141
src/test/kotlin/io/kotest/extensions/spring/security/SpringMockUserExtensionTest.kt
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 |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package io.kotest.extensions.spring.security | ||
|
||
import io.kotest.assertions.throwables.shouldThrow | ||
import io.kotest.core.descriptors.append | ||
import io.kotest.core.descriptors.toDescriptor | ||
import io.kotest.core.names.TestName | ||
import io.kotest.core.source.sourceRef | ||
import io.kotest.core.spec.style.FunSpec | ||
import io.kotest.core.test.TestCase | ||
import io.kotest.core.test.TestResult | ||
import io.kotest.core.test.TestType | ||
import io.kotest.extensions.spring.SpringExtension | ||
import io.kotest.matchers.collections.shouldContainOnly | ||
import io.kotest.matchers.equals.shouldBeEqual | ||
import io.kotest.matchers.shouldBe | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority | ||
import org.springframework.security.core.context.SecurityContextHolder | ||
import org.springframework.security.core.userdetails.User | ||
import org.springframework.security.test.context.TestSecurityContextHolder | ||
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken | ||
|
||
class SpringMockUserExtensionTest : FunSpec() { | ||
|
||
override fun extensions() = listOf(SpringExtension) | ||
|
||
init { | ||
|
||
afterAny { | ||
SecurityContextHolder.getContext().authentication = null | ||
} | ||
|
||
test("should set user before Spring in Spring security context") { | ||
SpringMockUserExtension().beforeSpring( | ||
TestCase( | ||
descriptor = SpringMockUserExtensionTest::class.toDescriptor().append("aaa"), | ||
name = TestName("name"), | ||
spec = this@SpringMockUserExtensionTest, | ||
test = {}, | ||
source = sourceRef(), | ||
type = TestType.Test | ||
) | ||
) | ||
|
||
val expectedAuthorities = mutableListOf(SimpleGrantedAuthority("ROLE_USER")) | ||
val expectedPrincipal = User("user", "password", true, true, true, true, expectedAuthorities) | ||
val expectedAuthentication = UsernamePasswordAuthenticationToken.authenticated( | ||
expectedPrincipal, | ||
expectedPrincipal.password, | ||
expectedPrincipal.authorities | ||
) | ||
SecurityContextHolder.getContext().authentication shouldBeEqual expectedAuthentication | ||
TestSecurityContextHolder.getContext().authentication shouldBeEqual expectedAuthentication | ||
} | ||
|
||
test("should remove user after any from Spring security context") { | ||
SecurityContextHolder.getContext().authentication = | ||
PreAuthenticatedAuthenticationToken( | ||
null, | ||
null, | ||
emptyList() | ||
) | ||
SpringMockUserExtension().afterAny( | ||
TestCase( | ||
descriptor = SpringMockUserExtensionTest::class.toDescriptor().append("aaa"), | ||
name = TestName("name"), | ||
spec = this@SpringMockUserExtensionTest, | ||
test = {}, | ||
source = sourceRef(), | ||
type = TestType.Test | ||
), | ||
TestResult.Ignored | ||
) | ||
|
||
SecurityContextHolder.getContext().authentication shouldBe null | ||
TestSecurityContextHolder.getContext().authentication shouldBe null | ||
} | ||
|
||
test("should assign roles to authentication") { | ||
SpringMockUserExtension(roles = listOf("TEST")).beforeSpring( | ||
TestCase( | ||
descriptor = SpringMockUserExtensionTest::class.toDescriptor().append("aaa"), | ||
name = TestName("name"), | ||
spec = this@SpringMockUserExtensionTest, | ||
test = {}, | ||
source = sourceRef(), | ||
type = TestType.Test | ||
) | ||
) | ||
|
||
SecurityContextHolder.getContext().authentication.authorities shouldContainOnly listOf(SimpleGrantedAuthority("ROLE_TEST")) | ||
} | ||
|
||
|
||
test("should reject roles starting with ROLE") { | ||
shouldThrow<IllegalArgumentException> { | ||
SpringMockUserExtension(roles = listOf("ROLE_TEST")).beforeSpring( | ||
TestCase( | ||
descriptor = SpringMockUserExtensionTest::class.toDescriptor().append("aaa"), | ||
name = TestName("name"), | ||
spec = this@SpringMockUserExtensionTest, | ||
test = {}, | ||
source = sourceRef(), | ||
type = TestType.Test | ||
) | ||
) | ||
} | ||
} | ||
|
||
test("should assign authorities to authentication") { | ||
SpringMockUserExtension(authorities = listOf("ADMIN")).beforeSpring( | ||
TestCase( | ||
descriptor = SpringMockUserExtensionTest::class.toDescriptor().append("aaa"), | ||
name = TestName("name"), | ||
spec = this@SpringMockUserExtensionTest, | ||
test = {}, | ||
source = sourceRef(), | ||
type = TestType.Test | ||
) | ||
) | ||
|
||
SecurityContextHolder.getContext().authentication.authorities shouldContainOnly listOf(SimpleGrantedAuthority("ADMIN")) | ||
} | ||
|
||
test("should not assign roles and authorities") { | ||
shouldThrow<IllegalStateException> { | ||
SpringMockUserExtension(roles = listOf("ROLE_TEST"), authorities = listOf("ADMIN")).beforeSpring( | ||
TestCase( | ||
descriptor = SpringMockUserExtensionTest::class.toDescriptor().append("aaa"), | ||
name = TestName("name"), | ||
spec = this@SpringMockUserExtensionTest, | ||
test = {}, | ||
source = sourceRef(), | ||
type = TestType.Test | ||
) | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
26 changes: 26 additions & 0 deletions
26
src/test/kotlin/io/kotest/extensions/spring/security/SpringTestApplication.kt
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 |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package io.kotest.extensions.spring.security | ||
|
||
import org.springframework.boot.autoconfigure.SpringBootApplication | ||
import org.springframework.http.HttpStatus | ||
import org.springframework.security.access.prepost.PreAuthorize | ||
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity | ||
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity | ||
import org.springframework.web.bind.annotation.GetMapping | ||
import org.springframework.web.bind.annotation.RequestMapping | ||
import org.springframework.web.bind.annotation.ResponseStatus | ||
import org.springframework.web.bind.annotation.RestController | ||
|
||
@SpringBootApplication | ||
@EnableWebFluxSecurity | ||
@EnableReactiveMethodSecurity | ||
class SpringTestApplication | ||
|
||
@RestController | ||
@RequestMapping("/secure") | ||
class TestSecurityController { | ||
|
||
@PreAuthorize("hasAuthority('ADMIN')") | ||
@GetMapping | ||
@ResponseStatus(HttpStatus.OK) | ||
suspend fun test() = "Ok" | ||
} |