Test cleanup

This commit is contained in:
2026-06-01 17:51:16 -06:00
parent 3cf345c85f
commit 5d2c4d62ac
5 changed files with 266 additions and 189 deletions

View File

@@ -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)));
}
}

View File

@@ -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)));
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}