Test cleanup
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* AuthApiTestSupport.java
|
||||
*
|
||||
* Shared MockMvc helpers for authentication API integration tests.
|
||||
*/
|
||||
package com.seanstarkey.notesvault.auth;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.seanstarkey.notesvault.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
|
||||
/**
|
||||
* Provides common fixtures and request helpers used by focused auth controller
|
||||
* test classes.
|
||||
*/
|
||||
abstract class AuthApiTestSupport {
|
||||
|
||||
@Autowired
|
||||
protected MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
protected UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
protected PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
protected JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
protected ObjectMapper objectMapper;
|
||||
|
||||
protected ResultActions register(String username, String password) throws Exception {
|
||||
return mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "%s",
|
||||
"password": "%s"
|
||||
}
|
||||
""".formatted(username, password)));
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* AuthControllerTest.java
|
||||
*/
|
||||
package com.seanstarkey.notesvault.auth;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.seanstarkey.notesvault.repository.UserRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Transactional
|
||||
class AuthControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
void registerCreatesUserAndReturnsPublicProfile() throws Exception {
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "starkey",
|
||||
"password": "secret123"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.username").value("starkey"))
|
||||
.andExpect(jsonPath("$.createdAt").isNotEmpty())
|
||||
.andExpect(jsonPath("$.password").doesNotExist())
|
||||
.andExpect(jsonPath("$.passwordHash").doesNotExist());
|
||||
|
||||
var savedUser = userRepository.findByUsername("starkey").orElseThrow();
|
||||
assertThat(savedUser.getPasswordHash()).isNotEqualTo("secret123");
|
||||
assertThat(passwordEncoder.matches("secret123", savedUser.getPasswordHash())).isTrue();
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: duplicate usernames are rejected.
|
||||
* Verifies that the unique account invariant is enforced at the API boundary
|
||||
* with HTTP 409 instead of silently overwriting or exposing database details.
|
||||
*/
|
||||
@Test
|
||||
void registerRejectsDuplicateUsername() throws Exception {
|
||||
register("starkey", "secret123").andExpect(status().isCreated());
|
||||
|
||||
register("starkey", "another123")
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error").value("Username is already registered"));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: registered users can log in and receive a bearer token.
|
||||
* Verifies that POST /auth/login authenticates stored BCrypt credentials,
|
||||
* returns token metadata, and issues a JWT whose subject is the username.
|
||||
*/
|
||||
@Test
|
||||
void loginAuthenticatesUserAndReturnsBearerToken() throws Exception {
|
||||
register("starkey", "secret123").andExpect(status().isCreated());
|
||||
|
||||
String responseBody = mockMvc.perform(post("/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "starkey",
|
||||
"password": "secret123"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.tokenType").value("Bearer"))
|
||||
.andExpect(jsonPath("$.expiresInSeconds").value(86400))
|
||||
.andExpect(jsonPath("$.expiresAt").isNotEmpty())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
String token = objectMapper.readTree(responseBody).get("token").asText();
|
||||
|
||||
assertThat(jwtUtil.getUsernameFromToken(token)).isEqualTo("starkey");
|
||||
assertThat(jwtUtil.isTokenValid(token, "starkey")).isTrue();
|
||||
}
|
||||
|
||||
private org.springframework.test.web.servlet.ResultActions register(String username, String password) throws Exception {
|
||||
return mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "%s",
|
||||
"password": "%s"
|
||||
}
|
||||
""".formatted(username, password)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* AuthRegistrationControllerTest.java
|
||||
*
|
||||
* API-level tests for public user registration behavior.
|
||||
*/
|
||||
package com.seanstarkey.notesvault.auth;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Verifies registration behavior through the real Spring MVC, Security, JPA,
|
||||
* validation, and Flyway-backed H2 application stack.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Transactional
|
||||
class AuthRegistrationControllerTest extends AuthApiTestSupport {
|
||||
|
||||
/*
|
||||
* Test: registering a new user returns public account details.
|
||||
* Verifies that POST /auth/register creates a user, hashes the password,
|
||||
* returns HTTP 201, and never exposes credential material in the response.
|
||||
*/
|
||||
@Test
|
||||
void registerCreatesUserAndReturnsPublicProfile() throws Exception {
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "alice",
|
||||
"password": "secret123"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").isNotEmpty())
|
||||
.andExpect(jsonPath("$.username").value("alice"))
|
||||
.andExpect(jsonPath("$.createdAt").isNotEmpty())
|
||||
.andExpect(jsonPath("$.password").doesNotExist())
|
||||
.andExpect(jsonPath("$.passwordHash").doesNotExist());
|
||||
|
||||
var savedUser = userRepository.findByUsername("alice").orElseThrow();
|
||||
assertThat(savedUser.getPasswordHash()).isNotEqualTo("secret123");
|
||||
assertThat(passwordEncoder.matches("secret123", savedUser.getPasswordHash())).isTrue();
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: duplicate usernames are rejected.
|
||||
* Verifies that the unique account invariant is enforced at the API boundary
|
||||
* with HTTP 409 instead of silently overwriting or exposing database details.
|
||||
*/
|
||||
@Test
|
||||
void registerRejectsDuplicateUsername() throws Exception {
|
||||
register("alice", "secret123").andExpect(status().isCreated());
|
||||
|
||||
register("alice", "another123")
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.error").value("Username is already registered"));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: invalid registration payloads return field-level validation details.
|
||||
* Verifies that username and password constraints are enforced before
|
||||
* persistence and reported with the documented HTTP 422 response shape.
|
||||
*/
|
||||
@Test
|
||||
void registerRejectsInvalidPayload() throws Exception {
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "al",
|
||||
"password": "short"
|
||||
}
|
||||
"""))
|
||||
.andExpect(status().isUnprocessableEntity())
|
||||
.andExpect(jsonPath("$.error").value("Validation failed"))
|
||||
.andExpect(jsonPath("$.fields.username").exists())
|
||||
.andExpect(jsonPath("$.fields.password").exists());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* ControllerApiTestSupport.java
|
||||
*
|
||||
* Shared MockMvc helpers for note and sharing API integration tests.
|
||||
*/
|
||||
package com.seanstarkey.notesvault.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.seanstarkey.notesvault.auth.JwtUtil;
|
||||
import com.seanstarkey.notesvault.entity.Note;
|
||||
import com.seanstarkey.notesvault.entity.NoteShare;
|
||||
import com.seanstarkey.notesvault.entity.User;
|
||||
import com.seanstarkey.notesvault.repository.NoteRepository;
|
||||
import com.seanstarkey.notesvault.repository.NoteShareRepository;
|
||||
import com.seanstarkey.notesvault.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Provides common authenticated request setup for focused note controller test
|
||||
* classes.
|
||||
*/
|
||||
abstract class ControllerApiTestSupport {
|
||||
|
||||
@Autowired
|
||||
protected MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
protected JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
protected NoteRepository noteRepository;
|
||||
|
||||
@Autowired
|
||||
protected NoteShareRepository noteShareRepository;
|
||||
|
||||
@Autowired
|
||||
protected UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
protected ObjectMapper objectMapper;
|
||||
|
||||
protected void register(String username, String password) throws Exception {
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "%s",
|
||||
"password": "%s"
|
||||
}
|
||||
""".formatted(username, password)))
|
||||
.andExpect(status().isCreated());
|
||||
|
||||
assertThat(userRepository.findByUsername(username)).isPresent();
|
||||
}
|
||||
|
||||
protected UUID createNote(String token, String title, String content) throws Exception {
|
||||
String responseBody = mockMvc.perform(post("/notes")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"title": "%s",
|
||||
"content": "%s"
|
||||
}
|
||||
""".formatted(title, content)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
|
||||
return UUID.fromString(objectMapper.readTree(responseBody).get("id").asText());
|
||||
}
|
||||
|
||||
protected ResultActions shareNote(String token, UUID noteId, String targetUsername) throws Exception {
|
||||
return mockMvc.perform(post("/notes/{id}/share", noteId)
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "%s"
|
||||
}
|
||||
""".formatted(targetUsername)));
|
||||
}
|
||||
|
||||
protected void shareWith(UUID noteId, String username) {
|
||||
Note note = noteRepository.findById(noteId).orElseThrow();
|
||||
User sharedWith = userRepository.findByUsername(username).orElseThrow();
|
||||
|
||||
NoteShare share = new NoteShare();
|
||||
share.setNote(note);
|
||||
share.setSharedWith(sharedWith);
|
||||
noteShareRepository.saveAndFlush(share);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,34 @@
|
||||
/**
|
||||
* NoteControllerTest.java
|
||||
* NoteCreationControllerTest.java
|
||||
*
|
||||
* API-level tests for authenticated note endpoints exposed by NoteController.
|
||||
* API-level tests for authenticated note creation behavior.
|
||||
*/
|
||||
package com.seanstarkey.notesvault.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.seanstarkey.notesvault.auth.JwtUtil;
|
||||
import com.seanstarkey.notesvault.entity.Note;
|
||||
import com.seanstarkey.notesvault.entity.NoteShare;
|
||||
import com.seanstarkey.notesvault.entity.User;
|
||||
import com.seanstarkey.notesvault.repository.NoteRepository;
|
||||
import com.seanstarkey.notesvault.repository.NoteShareRepository;
|
||||
import com.seanstarkey.notesvault.repository.UserRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Verifies note CRUD, sharing, authentication, authorization, and validation
|
||||
* behavior through the real Spring MVC, Security, JPA, validation, and
|
||||
* Flyway-backed H2 application stack.
|
||||
* Verifies POST /notes through the real Spring MVC, Security, JPA, validation,
|
||||
* and Flyway-backed H2 application stack.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Transactional
|
||||
class NoteControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@Autowired
|
||||
private NoteRepository noteRepository;
|
||||
|
||||
@Autowired
|
||||
private NoteShareRepository noteShareRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
class NoteCreationControllerTest extends ControllerApiTestSupport {
|
||||
|
||||
/*
|
||||
* Test: authenticated users can create notes.
|
||||
@@ -119,45 +88,29 @@ class NoteControllerTest {
|
||||
.andExpect(jsonPath("$.error").value("Unauthorized"));
|
||||
}
|
||||
|
||||
private void register(String username, String password) throws Exception {
|
||||
mockMvc.perform(post("/auth/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "%s",
|
||||
"password": "%s"
|
||||
}
|
||||
""".formatted(username, password)))
|
||||
.andExpect(status().isCreated());
|
||||
/*
|
||||
* Test: invalid note creation payloads return field-level validation details.
|
||||
* Verifies that POST /notes rejects blank content before persistence and
|
||||
* reports the documented HTTP 422 response shape.
|
||||
*/
|
||||
@Test
|
||||
void createNoteRejectsInvalidPayload() throws Exception {
|
||||
register("alice", "secret123");
|
||||
String token = jwtUtil.generateToken("alice");
|
||||
|
||||
assertThat(userRepository.findByUsername(username)).isPresent();
|
||||
}
|
||||
|
||||
private UUID createNote(String token, String title, String content) throws Exception {
|
||||
String responseBody = mockMvc.perform(post("/notes")
|
||||
mockMvc.perform(post("/notes")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"title": "%s",
|
||||
"content": "%s"
|
||||
"title": "My Note",
|
||||
"content": ""
|
||||
}
|
||||
""".formatted(title, content)))
|
||||
.andExpect(status().isCreated())
|
||||
.andReturn()
|
||||
.getResponse()
|
||||
.getContentAsString();
|
||||
"""))
|
||||
.andExpect(status().isUnprocessableEntity())
|
||||
.andExpect(jsonPath("$.error").value("Validation failed"))
|
||||
.andExpect(jsonPath("$.fields.content").exists());
|
||||
|
||||
return UUID.fromString(objectMapper.readTree(responseBody).get("id").asText());
|
||||
}
|
||||
|
||||
private void shareWith(UUID noteId, String username) {
|
||||
Note note = noteRepository.findById(noteId).orElseThrow();
|
||||
User sharedWith = userRepository.findByUsername(username).orElseThrow();
|
||||
|
||||
NoteShare share = new NoteShare();
|
||||
share.setNote(note);
|
||||
share.setSharedWith(sharedWith);
|
||||
noteShareRepository.saveAndFlush(share);
|
||||
assertThat(noteRepository.findAll()).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user