Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ Each domain follows: `controller/`, `service/`, `repository/`, `entity/`, `dto/`

### Testing Patterns
- **Unit tests**: Use JUnit 5 + Mockito
- Use domain-specific factories (e.g., `ItemTestFactory`) with **Datafaker** to generate dynamic test data.
- `@WebMvcTest(Controller.class)` for controller tests
- `@AutoConfigureMockMvc(addFilters = false)` to disable security
- `@MockBean` for dependencies
Expand Down
23 changes: 10 additions & 13 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,20 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.4.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
Expand Down Expand Up @@ -145,19 +155,6 @@
<version>0.11.5</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version> <!-- Or latest version -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.3.1</version> <!-- Or latest version -->
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.omatheusmesmo.shoppmate.auth.service.JwtService;
import com.omatheusmesmo.shoppmate.shared.testutils.UserTestFactory;
import com.omatheusmesmo.shoppmate.user.dtos.RegisterUserDTO;
import com.omatheusmesmo.shoppmate.user.entity.User;
import com.omatheusmesmo.shoppmate.user.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -41,16 +41,17 @@ public class AuthControllerTest {
private ObjectMapper objectMapper;

@Test
void ShouldReturnOk_WhenPasswordFollowsRequirements() throws Exception {
var dto = new RegisterUserDTO("anakin@skywalker.com", "Anakin Skywalker", "CorrectPass@123");
void registerUser_ValidPassword_ReturnsOk() throws Exception {
RegisterUserDTO dto = UserTestFactory.createValidRegisterUserDTO();

mockMvc.perform(post("/auth/sign").contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(dto))).andExpect(status().isOk());
}

@Test
void ShouldReturnBadRequest_WhenPasswordDoesNotContainUppercaseLetters() throws Exception {
var dto = new RegisterUserDTO("anakin@skywalker.com", "Anakin Skywalker", "password-123");
void registerUser_NoUppercase_ReturnsBadRequest() throws Exception {
RegisterUserDTO validDto = UserTestFactory.createValidRegisterUserDTO();
RegisterUserDTO dto = new RegisterUserDTO(validDto.email(), validDto.fullName(), "password-123");

mockMvc.perform(post("/auth/sign").contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(dto))).andExpect(status().isBadRequest())
Expand All @@ -60,8 +61,9 @@ void ShouldReturnBadRequest_WhenPasswordDoesNotContainUppercaseLetters() throws
}

@Test
void ShouldReturnBadRequest_WhenPasswordDoesNotContainSpecialCharacters() throws Exception {
var dto = new RegisterUserDTO("anakin@skywalker.com", "Anakin Skywalker", "Password123");
void registerUser_NoSpecialChar_ReturnsBadRequest() throws Exception {
RegisterUserDTO validDto = UserTestFactory.createValidRegisterUserDTO();
RegisterUserDTO dto = new RegisterUserDTO(validDto.email(), validDto.fullName(), "Password123");

mockMvc.perform(post("/auth/sign").contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(dto))).andExpect(status().isBadRequest())
Expand All @@ -71,8 +73,9 @@ void ShouldReturnBadRequest_WhenPasswordDoesNotContainSpecialCharacters() throws
}

@Test
void ShouldReturnBadRequest_WhenPasswordDoesNotContainNumbers() throws Exception {
var dto = new RegisterUserDTO("anakin@skywalker.com", "Anakin Skywalker", "Password@");
void registerUser_NoNumbers_ReturnsBadRequest() throws Exception {
RegisterUserDTO validDto = UserTestFactory.createValidRegisterUserDTO();
RegisterUserDTO dto = new RegisterUserDTO(validDto.email(), validDto.fullName(), "Password@");

mockMvc.perform(post("/auth/sign").contentType(MediaType.APPLICATION_JSON_VALUE)
.content(objectMapper.writeValueAsString(dto))).andExpect(status().isBadRequest())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.omatheusmesmo.shoppmate.auth.service;

import com.omatheusmesmo.shoppmate.shared.testutils.UserTestFactory;
import com.omatheusmesmo.shoppmate.user.entity.User;
import com.omatheusmesmo.shoppmate.user.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -30,28 +31,29 @@ class CustomUserDetailsServiceTest {

@BeforeEach
void setUp() {
user = new User();
user.setId(1L);
user.setEmail("john@example.com");
user.setPassword("secret");
user.setFullName("John");
user = UserTestFactory.createValidUser();
}

@Test
void loadUserByUsername_ExistingEmail_ReturnsUserDetails() {
when(userRepository.findByEmail("john@example.com")).thenReturn(Optional.of(user));
// Arrange
when(userRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(user));

UserDetails userDetails = customUserDetailsService.loadUserByUsername("john@example.com");
// Act
UserDetails userDetails = customUserDetailsService.loadUserByUsername(user.getEmail());

assertEquals("john@example.com", userDetails.getUsername());
assertEquals("secret", userDetails.getPassword());
// Assert
assertEquals(user.getEmail(), userDetails.getUsername());
assertEquals(user.getPassword(), userDetails.getPassword());
}

@Test
void loadUserByUsername_MissingEmail_ThrowsUsernameNotFoundException() {
when(userRepository.findByEmail("missing@example.com")).thenReturn(Optional.empty());
// Arrange
String missingEmail = "missing-" + user.getEmail();
when(userRepository.findByEmail(missingEmail)).thenReturn(Optional.empty());

assertThrows(UsernameNotFoundException.class,
() -> customUserDetailsService.loadUserByUsername("missing@example.com"));
// Act & Assert
assertThrows(UsernameNotFoundException.class, () -> customUserDetailsService.loadUserByUsername(missingEmail));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.omatheusmesmo.shoppmate.auth.service;

import com.nimbusds.jwt.JWTClaimsSet;
import com.omatheusmesmo.shoppmate.shared.testutils.FakerUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.test.util.ReflectionTestUtils;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

class JwtServiceTest {

@Mock
private UserDetails userDetails;

private JwtService jwtService;
private AutoCloseable mocks;
private String agnosticUsername;
private static final String SECRET_KEY = "0123456789012345678901234567890123456789012345678901234567890123"; // 64
// chars
// for
// HS256

@BeforeEach
void setUp() {
mocks = MockitoAnnotations.openMocks(this);
agnosticUsername = FakerUtil.getFaker().internet().username();

jwtService = new JwtService();
ReflectionTestUtils.setField(jwtService, "tokenExpiration", 3600000L);
ReflectionTestUtils.setField(jwtService, "secretKey", SECRET_KEY);

when(userDetails.getUsername()).thenReturn(agnosticUsername);
}

@AfterEach
void tearDown() throws Exception {
mocks.close();
}

@Test
void generateToken_ValidUserDetails_ReturnsValidToken() {
// Act
String token = jwtService.generateToken(userDetails);

// Assert
assertNotNull(token);
assertFalse(token.isEmpty());
assertTrue(jwtService.validateToken(token), "Generated token should be valid");
}

@Test
void decryptToken_ValidToken_ExtractsUsername() {
// Arrange
String token = jwtService.generateToken(userDetails);

// Act
JWTClaimsSet claimsSet = jwtService.decryptToken(token);

// Assert
assertEquals(agnosticUsername, claimsSet.getSubject());
assertNotNull(claimsSet.getExpirationTime());
assertNotNull(claimsSet.getJWTID());
}

@Test
void generateToken_MultipleCalls_ReturnsUniqueJwtIds() {
// Act
String token1 = jwtService.generateToken(userDetails);
String token2 = jwtService.generateToken(userDetails);

JWTClaimsSet claims1 = jwtService.decryptToken(token1);
JWTClaimsSet claims2 = jwtService.decryptToken(token2);

// Assert
assertNotEquals(claims1.getJWTID(), claims2.getJWTID());
}

@Test
void validateToken_ExpiredToken_ReturnsFalse() {
// Arrange
JwtService shortLivedService = new JwtService();
ReflectionTestUtils.setField(shortLivedService, "tokenExpiration", -1000L);
ReflectionTestUtils.setField(shortLivedService, "secretKey", SECRET_KEY);

String token = shortLivedService.generateToken(userDetails);

// Act & Assert
assertFalse(shortLivedService.validateToken(token), "Expired token should not be valid");
}

@Test
void validateToken_InvalidInputs_ReturnsFalse() {
// Act & Assert
assertFalse(jwtService.validateToken("invalid_token"));
assertFalse(jwtService.validateToken(""));
assertFalse(jwtService.validateToken(null));
}

@Test
void validateToken_MalformedJwe_ReturnsFalse() {
// Arrange
String malformedJwe = String.join(".", "not", "a", "valid", "jwe", "token");

// Act & Assert
assertFalse(jwtService.validateToken(malformedJwe));
}

@Test
void validateToken_DifferentKeys_ReturnsFalse() {
// Arrange
JwtService otherService = new JwtService();
ReflectionTestUtils.setField(otherService, "tokenExpiration", 3600000L);
ReflectionTestUtils.setField(otherService, "secretKey",
"different_secret_key_different_secret_key_different_secret_key_64_chars");
String token = otherService.generateToken(userDetails);

// Act & Assert
assertFalse(jwtService.validateToken(token));
}

@Test
void decryptToken_InvalidToken_ThrowsException() {
// Act & Assert
assertThrows(JwtServiceException.class, () -> jwtService.decryptToken("invalid"));
}

@Test
void generateToken_DifferentUser_ReturnsTokenWithCorrectSubject() {
// Arrange
String anotherUser = agnosticUsername + "-different";
when(userDetails.getUsername()).thenReturn(anotherUser);

// Act
String token = jwtService.generateToken(userDetails);
JWTClaimsSet claims = jwtService.decryptToken(token);

// Assert
assertEquals(anotherUser, claims.getSubject());
}
}
Loading
Loading