I have an issue with Spring Boot's TestEntityManager
and @WithUserDetails
annotation.
Here is my test suite:
public class AdvertisementAuthorizationTest extends AbstractIntegrationTest {
private static final String IMPERSONATOR_EMAIL = "[email protected]";
private final static String OWNER_OF_ADVERTISEMENT_EMAIL = "[email protected]";
@Autowired
private TestEntityManager testEntityManager;
@Autowired
private MockMvc mockMvc;
private Advertisement advertisement;
private UserAccount impersonator;
private ObjectMapper mapper = new ObjectMapper();
@Before
public void setUp() {
advertisement = testEntityManager.persist(createAdvertisement(OWNER_OF_ADVERTISEMENT_EMAIL));
impersonator = testEntityManager.persist(createUserAccount(IMPERSONATOR_EMAIL));
}
@Test
@WithUserDetails(IMPERSONATOR_EMAIL)
public void shouldNotAllowAdvertisementModification() throws Exception {
String jsonAdvertisement = mapper.writeValueAsString(advertisement);
mockMvc.perform(put("/api/advertisement/{id}", advertisement.getId())//
.contentType(MediaType.APPLICATION_JSON)//
.content(jsonAdvertisement))//
.andDo(print())//
.andExpect(status().isForbidden());//
}
@Test
@WithUserDetails(OWNER_OF_ADVERTISEMENT_EMAIL)
public void shouldAllowAdvertisementModification() throws Exception {
String jsonAdvertisement = mapper.writeValueAsString(advertisement);
mockMvc.perform(put("/api/advertisement/{id}", advertisement.getId())//
.contentType(MediaType.APPLICATION_JSON)//
.content(jsonAdvertisement))//
.andDo(print())//
.andExpect(status().isOk());//
}
}
Here is the super class:
@AutoConfigureMockMvc
@AutoConfigureTestEntityManager
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {TestApplication.class})
@WebAppConfiguration
@ActiveProfiles(Profiles.TEST)
@Transactional
public abstract class AbstractIntegrationTest {
}
When I run the tests it seems the entities are not persisted to H2
by TestEntityManager
as indicated by this error message:
Hibernate: select useraccoun0_.id as id1_9_, useraccoun0_.address_id as address13_9_, useraccoun0_.email as email2_9_, useraccoun0_.email_notification as email_no3_9_, useraccoun0_.enabled as enabled4_9_, useraccoun0_.first_name as first_na5_9_, useraccoun0_.last_connection_date as last_con6_9_, useraccoun0_.password as password7_9_, useraccoun0_.registration_date as registra8_9_, useraccoun0_.role as role9_9_, useraccoun0_.token as token10_9_, useraccoun0_.user_account_type as user_ac11_9_, useraccoun0_.version as version12_9_ from user_account useraccoun0_ where useraccoun0_.email=?
22:52:39.943 [Test worker] WARN o.s.test.context.TestContextManager - Caught exception while allowing TestExecutionListener [org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener@43ec4dcd] to process 'before' execution of test method [public void com.bignibou.it.web.security.advertisement.AdvertisementAuthorizationTest.shouldNotAllowAdvertisementModification() throws java.lang.Exception] for test instance [com.bignibou.it.web.security.advertisement.AdvertisementAuthorizationTest@646496bc]
java.lang.IllegalStateException: Unable to create SecurityContext using @org.springframework.security.test.context.support.WithUserDetails([email protected], userDetailsServiceBeanName=)
at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.createSecurityContext(WithSecurityContextTestExecutionListener.java:79)
at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.beforeTestMethod(WithSecurityContextTestExecutionListener.java:56)
at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:269)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecuter.runTestClass(JUnitTestClassExecuter.java:112)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassExecuter.execute(JUnitTestClassExecuter.java:56)
at org.gradle.api.internal.tasks.testing.junit.JUnitTestClassProcessor.processTestClass(JUnitTestClassProcessor.java:66)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.processTestClass(SuiteTestClassProcessor.java:51)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.gradle.messaging.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
at org.gradle.messaging.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.messaging.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:32)
at org.gradle.messaging.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:93)
at com.sun.proxy.$Proxy2.processTestClass(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.processTestClass(TestWorker.java:109)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.gradle.messaging.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:35)
at org.gradle.messaging.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.messaging.remote.internal.hub.MessageHub$Handler.run(MessageHub.java:364)
at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:54)
at org.gradle.internal.concurrent.StoppableExecutorImpl$1.run(StoppableExecutorImpl.java:40)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.security.core.userdetails.UsernameNotFoundException: Username: [email protected] not found!
at com.bignibou.service.security.UserAccountUserDetailsService.loadUserByUsername(UserAccountUserDetailsService.java:21)
at org.springframework.security.test.context.support.WithUserDetailsSecurityContextFactory.createSecurityContext(WithUserDetailsSecurityContextFactory.java:56)
at org.springframework.security.test.context.support.WithUserDetailsSecurityContextFactory.createSecurityContext(WithUserDetailsSecurityContextFactory.java:39)
at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.createSecurityContext(WithSecurityContextTestExecutionListener.java:76)
... 43 common frames omitted
Can someone please help?
This is an issue with timing with regard to TestExecutionListener
callbacks and @Before
test methods.
@WithUserDetails
is supported by the Spring Security's WithSecurityContextTestExecutionListener
which will never run after a @Before
method. It is therefore impossible for Spring Security to see the user that you persist to the database in your setUp()
method. That's basically what the exception is telling you: Spring Security attempted to read the user from the database before it existed.
One way to fix this is to migrate to @Sql
support for inserting test data in the database. You might not find that as comfortable as simply persisting your entities, but the @Sql
approach allows the test data to be created within the test-managed transaction (i.e., does not require manual clean up). Note that you will have to upgrade to Spring Security 4.1.1 in order for this to work properly.
An alternative way to address this is to persist your entities in a user-managed transaction in a @BeforeTransaction
method -- for example, using Spring's TransactionTemplate
. However, you will then need to manually clean up the database in an @AfterTransaction
method in a similar fashion. Plus, you will still need to upgrade to Spring Security 4.1.1 in order for this to work.
Something like the following should do the trick:
@Autowired
private TestEntityManager testEntityManager;
@Autowired
PlatformTransactionManager transactionManager;
@BeforeTransaction
public void setUp() {
new TransactionTemplate(transactionManager).execute(status -> {
UserAccount owner = testEntityManager.persist(createUserAccount(OWNER_OF_ADVERTISEMENT_EMAIL));
Language language = testEntityManager.persist(createLanguage("Français"));
DayToTimeSlot dayToTimeSlot = testEntityManager.persist(createDayToTimeSlot());
advertisement = testEntityManager.persist(createAdvertisement(owner, language, dayToTimeSlot));
impersonator = testEntityManager.persist(createUserAccount(IMPERSONATOR_EMAIL));
return null;
});
}
@AfterTransaction
public void tearDown() {
new TransactionTemplate(transactionManager).execute(status -> {
testEntityManager.remove(testEntityManager.find(Advertisement.class, advertisement.getId()));
UserAccount owner = advertisement.getUserAccount();
testEntityManager.remove(testEntityManager.find(UserAccount.class, owner.getId()));
testEntityManager.remove(testEntityManager.find(UserAccount.class, impersonator.getId()));
return null;
});
}
Regards,
Sam
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With