【发布时间】:2023-02-22 04:03:29
【问题描述】:
我已经编写了一个测试方法来测试对 API 的 POST 请求。每次我运行它时,它都会返回此错误:无法创建 SecurityContext。
我是 Java 和 Spring 以及安全方面的初学者(所以请对我温柔一点)。我正在参加一个在线课程,该课程要求我们创建一个食谱 API,然后使用 Spring Security 对其进行保护。我已经使用 Postman 验证了所有端点,但无法通过使用 Mockito 和 mockMVC 的测试。
Java 11,弹簧引导 2.7.8
配方控制器
这里只包括 POST 请求。
package cn.RecipeAPI.Controllers;
import cn.RecipeAPI.Exceptions.NoSuchRecipeException;
import cn.RecipeAPI.Models.Recipe;
import cn.RecipeAPI.Services.RecipeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/recipes")
public class RecipeController {
@Autowired
RecipeService recipeService;
@PostMapping
public ResponseEntity<?> createNewRecipe(@RequestBody Recipe recipe, Authentication authentication) {
try {
Recipe insertedRecipe = recipeService.createNewRecipe(recipe, authentication);
return ResponseEntity.created(insertedRecipe.getLocationURI()).body(insertedRecipe);
} catch (IllegalStateException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
食谱服务
package cn.RecipeAPI.Services;
import cn.RecipeAPI.Exceptions.NoSuchRecipeException;
import cn.RecipeAPI.Models.CustomUserDetails;
import cn.RecipeAPI.Models.Recipe;
import cn.RecipeAPI.Models.Review;
import cn.RecipeAPI.Repositories.RecipeRepo;
import cn.RecipeAPI.Repositories.UserRepo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.security.core.Authentication;
import java.util.List;
import java.util.Optional;
@Service
public class RecipeService {
@Autowired
RecipeRepo recipeRepo;
@Autowired
UserRepo userRepo;
@Transactional
public Recipe createNewRecipe(Recipe recipe, Authentication authentication) throws IllegalStateException {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
recipe.setUser(userRepo.getReferenceById(userDetails.getId()));
recipe.validate();
recipe = recipeRepo.save(recipe);
recipe.generateLocationURI();
return recipe;
}
}
食谱 API 测试
这个类可能包含许多不必要的注释和东西。当我阅读许多 Stack Overflow 答案和 Spring Security 文档时,我尝试了各种修复。在这一点上,我不记得什么是必要的,什么不是。
package cn.RecipeAPI;
import cn.RecipeAPI.Controllers.RecipeController;
import cn.RecipeAPI.Exceptions.NoSuchRecipeException;
import cn.RecipeAPI.Models.*;
import cn.RecipeAPI.Security.CustomUserDetailsService;
import cn.RecipeAPI.Security.SecurityConfig;
import cn.RecipeAPI.Services.RecipeService;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Set;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(RecipeController.class)
@ContextConfiguration(classes = SecurityConfig.class)
@ActiveProfiles(profiles = "test")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ExtendWith(SpringExtension.class)
public class RecipeApiApplicationTests {
@Mock
private Authentication authentication;
@Autowired
private MockMvc mockMvc;
@MockBean
private RecipeService recipeService;
@MockBean
private CustomUserDetailsService customUserDetailsService;
// I'm not sure if either of these are needed, but I'm going to leave them in for now
@InjectMocks
private RecipeController recipeController;
@Autowired
private WebApplicationContext context;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
// Create some test users
// user for recipes
UserMeta userMeta = UserMeta.builder().email("recipe@gmail.com").name("recipeUser1").build();
Role role = Role.builder().role(Role.Roles.ROLE_USER).build();
Set<Role> roles = Set.of(role);
CustomUserDetails userRecipe = CustomUserDetails.builder().userMeta(userMeta).username("userRecipe").password("1234").authorities(roles).build();
// user for reviews
UserMeta userMeta1 = UserMeta.builder().email("review@gmail.com").name("reviewUser").build();
CustomUserDetails userReview = CustomUserDetails.builder().userMeta(userMeta1).username("userReview").password("1234").authorities(roles).build();
// Create some test recipes
Review review = Review.builder().description("was just caramel").rating(3).user(userReview).build();
Review review2 = Review.builder().description("was just egg").rating(4).user(userReview).build();
Recipe recipe = Recipe.builder().name("test name").difficultyRating(1).minutesToMake(5)
.ingredients(Set.of(Ingredient.builder().name("spam").amount("1 can").build()))
.steps(Set.of(Step.builder().stepNumber(1).description("eat spam").build()))
.locationURI(new URI("http://localhost:8080/recipes/1"))
.reviews(Set.of(review))
.id(1L)
.user(userRecipe)
.build();
Recipe recipe2 = Recipe.builder().name("test name2").difficultyRating(2).minutesToMake(6)
.ingredients(Set.of(Ingredient.builder().name("egg").amount("1 egg").build()))
.steps(Set.of(Step.builder().stepNumber(1).description("crack egg").build()))
.locationURI(new URI("http://localhost:8080/recipes/2"))
.reviews(Set.of(review2))
.id(2L)
.user(userRecipe)
.build();
ArrayList<Recipe> recipes = new ArrayList<>(Arrays.asList(recipe, recipe2));
public RecipeApiApplicationTests() throws URISyntaxException {
}
@Test
@Order(4)
@WithUserDetails(value="userRecipe", userDetailsServiceBeanName="customUserDetailsService")
public void testCreateNewRecipeSuccessBehavior() throws Exception {
when(recipeService.createNewRecipe(any(Recipe.class), any(Authentication.class))).thenReturn(recipe);
mockMvc.perform(post("/recipes")
//set request Content-Type header
.contentType("application/json")
//set HTTP body equal to JSON based on recipe object
.content(TestUtil.convertObjectToJsonBytes(recipe))
)
//confirm HTTP response meta
.andExpect(status().isCreated())
.andExpect(content().contentType("application/json"))
//confirm Location header with new location of object matches the correct URL structure
.andExpect(header().string("Location", containsString("http://localhost:8080/recipes/1")))
//confirm some recipe data
.andExpect(jsonPath("id").value(1))
.andExpect(jsonPath("name").value("test name"))
//confirm ingredient data
.andExpect(jsonPath("ingredients", hasSize(1)))
.andExpect(jsonPath("ingredients[0].name").value("spam"))
.andExpect(jsonPath("ingredients[0].amount").value("1 can"))
//confirm step data
.andExpect(jsonPath("steps", hasSize(1)))
// .andExpect(jsonPath("steps[0]").isNotEmpty())
//confirm review data
.andExpect(jsonPath("reviews", hasSize(1)))
.andExpect(jsonPath("reviews[0].username").value("idk"));
}
}
堆栈跟踪
java.lang.IllegalStateException: Unable to create SecurityContext using @org.springframework.security.test.context.support.WithUserDetails(setupBefore=TEST_METHOD, userDetailsServiceBeanName="customUserDetailsService", value="userRecipe")
at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.lambda$createTestSecurityContext$0(WithSecurityContextTestExecutionListener.java:126) ~[spring-security-test-5.7.6.jar:5.7.6]
at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.beforeTestMethod(WithSecurityContextTestExecutionListener.java:73) ~[spring-security-test-5.7.6.jar:5.7.6]
at org.springframework.test.context.TestContextManager.beforeTestMethod(TestContextManager.java:293) ~[spring-test-5.3.25.jar:5.3.25]
at org.springframework.test.context.junit.jupiter.SpringExtension.beforeEach(SpringExtension.java:174) ~[spring-test-5.3.25.jar:5.3.25]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachCallbacks$2(TestMethodTestDescriptor.java:163) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$6(TestMethodTestDescriptor.java:199) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:199) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachCallbacks(TestMethodTestDescriptor.java:162) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:129) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:66) ~[junit-jupiter-engine-5.8.2.jar:5.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541) ~[na:na]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at java.base/java.util.ArrayList.forEach(ArrayList.java:1541) ~[na:na]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.8.2.jar:1.8.2]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107) ~[na:na]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88) ~[na:na]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54) ~[na:na]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67) ~[na:na]
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52) ~[na:na]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114) ~[na:na]
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86) ~[na:na]
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86) ~[na:na]
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53) ~[na:na]
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99) ~[na:na]
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79) ~[na:na]
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75) ~[na:na]
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36) ~[na:na]
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24) ~[na:na]
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33) ~[na:na]
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94) ~[na:na]
at com.sun.proxy.$Proxy2.stop(Unknown Source) ~[na:na]
at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193) ~[na:na]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129) ~[na:na]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100) ~[na:na]
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60) ~[na:na]
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56) ~[na:na]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113) ~[na:na]
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65) ~[na:na]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69) ~[gradle-worker.jar:na]
at worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74) ~[gradle-worker.jar:na]
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'customUserDetailsService' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:874) ~[spring-beans-5.3.25.jar:5.3.25]
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1358) ~[spring-beans-5.3.25.jar:5.3.25]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:309) ~[spring-beans-5.3.25.jar:5.3.25]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) ~[spring-beans-5.3.25.jar:5.3.25]
at org.springframework.security.test.context.support.WithUserDetailsSecurityContextFactory.findUserDetailsService(WithUserDetailsSecurityContextFactory.java:76) ~[spring-security-test-5.7.6.jar:5.7.6]
at org.springframework.security.test.context.support.WithUserDetailsSecurityContextFactory.createSecurityContext(WithUserDetailsSecurityContextFactory.java:58) ~[spring-security-test-5.7.6.jar:5.7.6]
at org.springframework.security.test.context.support.WithUserDetailsSecurityContextFactory.createSecurityContext(WithUserDetailsSecurityContextFactory.java:43) ~[spring-security-test-5.7.6.jar:5.7.6]
at org.springframework.security.test.context.support.WithSecurityContextTestExecutionListener.lambda$createTestSecurityContext$0(WithSecurityContextTestExecutionListener.java:123) ~[spring-security-test-5.7.6.jar:5.7.6]
... 72 common frames omitted
其他
在阅读 Spring documentation on testing 时,我确实看到了这个注释。
我尝试将 spring-test-4.1.3.RELEASE 添加到我的依赖项中,这让一切都出错了。这是我见过的唯一提到的地方。我删除了它。
还有什么我应该包括的吗?
【问题讨论】:
-
你的测试一团糟。您正在混合使用 JUnit4 和 JUnit5,而我不知道您想使用它们做什么。您正在使用
@WebMvcTest,但也使用@ContextConfiguration,这实际上没有任何意义。您自动装配 mockmvc 但随后丢弃它并在设置方法中自己完成。尝试添加spring-test-4.1.3确实会让事情变得更糟,因为 spring boot 2.7 使用 Spring 5.3,混合来自不同版本框架的 jar 很麻烦。 -
如果向下滚动错误堆栈,您将看到实际的错误消息:NoSuchBeanDefinitionException: No bean named 'customUserDetailsService' available
-
谢谢帕维尔!我没有注意到。
-
在 Controller 中设置一些断点后,我意识到测试从未命中 Controller,所以现在我正在调查它。
标签: java spring testing spring-security