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
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package backend.fullstack.config;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
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 java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import backend.fullstack.user.role.Role;
import io.swagger.v3.oas.models.OpenAPI;

class ConfigSimpleComponentsTest {

@Test
void apiResponseConstructorsFactoriesAndSettersWork() {
ApiResponse<String> empty = new ApiResponse<>();
empty.setSuccess(true);
empty.setMessage("ok");
empty.setData("payload");

assertTrue(empty.isSuccess());
assertEquals("ok", empty.getMessage());
assertEquals("payload", empty.getData());

ApiResponse<String> constructed = new ApiResponse<>(false, "nope", "x");
assertFalse(constructed.isSuccess());
assertEquals("nope", constructed.getMessage());
assertEquals("x", constructed.getData());

ApiResponse<String> successDefault = ApiResponse.success("data");
assertTrue(successDefault.isSuccess());
assertEquals("Success", successDefault.getMessage());
assertEquals("data", successDefault.getData());

ApiResponse<Integer> successCustom = ApiResponse.success("created", 42);
assertTrue(successCustom.isSuccess());
assertEquals("created", successCustom.getMessage());
assertEquals(42, successCustom.getData());

ApiResponse<Void> error = ApiResponse.error("boom");
assertFalse(error.isSuccess());
assertEquals("boom", error.getMessage());
}

@Test
void jwtPropertiesGettersSettersAndCompatibilityAccessorsWork() {
JwtProperties properties = new JwtProperties();
properties.setSecret("0123456789012345678901234567890123456789012345678901234567890123");
properties.setExpirationMs(1234L);
properties.setCookieName("auth");
properties.setCookieSecure(false);

assertEquals(properties.getSecret(), properties.secret());
assertEquals(properties.getExpirationMs(), properties.expirationMs());
assertEquals(properties.getCookieName(), properties.cookieName());
assertEquals(properties.isCookieSecure(), properties.cookieSecure());
}

@Test
void jwtPrincipalNormalizesAndDefensivelyCopiesLocationIds() {
JwtPrincipal withNull = new JwtPrincipal(1L, "u@example.com", Role.ADMIN, 10L, null);
assertTrue(withNull.locationIds().isEmpty());

List<Long> mutable = new ArrayList<>(List.of(1L, 2L));
JwtPrincipal withList = new JwtPrincipal(2L, "v@example.com", Role.MANAGER, 11L, mutable);
mutable.add(3L);

assertEquals(List.of(1L, 2L), withList.locationIds());
assertThrows(UnsupportedOperationException.class, () -> withList.locationIds().add(9L));
}

@Test
void passwordConfigProvidesBcryptEncoder() {
PasswordConfig config = new PasswordConfig();
PasswordEncoder encoder = config.passwordEncoder();

assertNotNull(encoder);
assertTrue(encoder instanceof BCryptPasswordEncoder);
assertTrue(encoder.matches("secret", encoder.encode("secret")));
}

@Test
void swaggerConfigBuildsOpenApiWithBearerScheme() {
SwaggerConfig config = new SwaggerConfig();
OpenAPI openApi = config.openAPI();

assertEquals("IK-Control API", openApi.getInfo().getTitle());
assertEquals("1.0.0", openApi.getInfo().getVersion());
assertEquals("Bearer Auth", openApi.getSecurity().get(0).keySet().iterator().next());
assertEquals("bearer", openApi.getComponents()
.getSecuritySchemes()
.get("Bearer Auth")
.getScheme());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package backend.fullstack.config;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import java.util.List;
import java.util.Map;
import java.lang.reflect.Method;

import org.junit.jupiter.api.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;

import backend.fullstack.dto.ErrorResponse;
import backend.fullstack.exceptions.AccessDeniedException;
import backend.fullstack.exceptions.InvalidThresholdException;
import backend.fullstack.exceptions.LocationException;
import backend.fullstack.exceptions.OrganizationConflictException;
import backend.fullstack.exceptions.PasswordException;
import backend.fullstack.exceptions.ResourceNotFoundException;
import backend.fullstack.exceptions.RoleException;
import backend.fullstack.exceptions.UnitInactiveException;
import backend.fullstack.exceptions.UnitNotFoundException;
import backend.fullstack.exceptions.UserConflictException;

class GlobalExceptionHandlerTest {

private final GlobalExceptionHandler handler = new GlobalExceptionHandler();

@Test
void appExceptionHandlerUsesExceptionMetadata() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/api/meta");

ResponseEntity<ErrorResponse> response = handler.handleAppException(new PasswordException("bad pwd"), request);

assertEquals(400, response.getStatusCode().value());
assertNotNull(response.getBody());
assertEquals("PASSWORD_ERROR", response.getBody().errorCode());
assertEquals("bad pwd", response.getBody().message());
}

@Test
void appExceptionDerivedHandlersBuildExpectedResponses() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/api/test");

assertError(handler.handleResourceNotFound(new ResourceNotFoundException("missing"), request), 404, "RESOURCE_NOT_FOUND");
assertError(handler.handleUnitNotFound(new UnitNotFoundException(7L), request), 404, "UNIT_NOT_FOUND");
assertError(handler.handleInvalidThreshold(new InvalidThresholdException(), request), 400, "INVALID_THRESHOLD");
assertError(handler.handleUnitInactive(new UnitInactiveException(8L), request), 409, "UNIT_INACTIVE");
}

@Test
void conflictAndBadRequestHandlersBuildExpectedResponses() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/api/test");

assertError(handler.handleOrganizationConflict(new OrganizationConflictException("org exists"), request), 409, "CONFLICT");
assertError(handler.handleUserConflict(new UserConflictException("dup"), request), 409, "CONFLICT");
assertError(handler.handleLocationException(new LocationException("loc err"), request), 409, "CONFLICT");
assertError(handler.handleRoleException(new RoleException("role err"), request), 400, "ROLE_ERROR");
assertError(handler.handlePasswordException(new PasswordException("pwd err"), request), 400, "PASSWORD_ERROR");
assertError(handler.handleIllegalArgument(new IllegalArgumentException("bad arg"), request), 400, "BAD_REQUEST");
}

@Test
void accessDeniedHandlersBuildExpectedResponses() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/api/secure");

assertError(handler.handleCustomAccessDeniedException(new AccessDeniedException("denied"), request), 403, "ACCESS_DENIED");
assertError(handler.handleSpringAccessDeniedException(new org.springframework.security.access.AccessDeniedException("denied"), request), 403, "ACCESS_DENIED");
}

@Test
void validationHandlerCollectsFirstFieldErrorPerField() throws Exception {
Object target = new ValidationTarget();
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, "validationTarget");
List<FieldError> fieldErrors = List.of(
new FieldError("obj", "email", "must be valid"),
new FieldError("obj", "email", "duplicate should be ignored"),
new FieldError("obj", "name", "must not be blank")
);
fieldErrors.forEach(bindingResult::addError);

Method method = ValidationTarget.class.getDeclaredMethod("submit", ValidationTarget.class);
MethodParameter parameter = new MethodParameter(method, 0);
MethodArgumentNotValidException exception = new MethodArgumentNotValidException(parameter, bindingResult);

MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI("/api/users");

ResponseEntity<ErrorResponse> response = handler.handleValidationException(exception, request);

assertEquals(400, response.getStatusCode().value());
assertNotNull(response.getBody());
assertEquals("VALIDATION_ERROR", response.getBody().errorCode());
assertEquals("Validation failed", response.getBody().message());
assertEquals(Map.of("email", "must be valid", "name", "must not be blank"), response.getBody().fieldErrors());
}

private static void assertError(ResponseEntity<ErrorResponse> response, int status, String code) {
assertEquals(status, response.getStatusCode().value());
assertNotNull(response.getBody());
assertEquals(code, response.getBody().errorCode());
}

private static final class ValidationTarget {
@SuppressWarnings("unused")
void submit(ValidationTarget value) {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package backend.fullstack.config;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.List;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;

import backend.fullstack.user.role.Role;
import io.jsonwebtoken.Claims;

class JwtAuthFilterTest {

@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
}

@Test
void authenticatesFromCookieTokenWhenValid() throws Exception {
JwtUtil jwtUtil = mock(JwtUtil.class);
Claims claims = mock(Claims.class);
when(jwtUtil.getJwtFromCookies(org.mockito.ArgumentMatchers.any())).thenReturn("cookie-jwt");
when(jwtUtil.validateJwtToken("cookie-jwt")).thenReturn(true);
when(jwtUtil.getClaimsFromJwtToken("cookie-jwt")).thenReturn(claims);
when(claims.getSubject()).thenReturn("user@example.com");
when(claims.get("role", String.class)).thenReturn("MANAGER");
when(claims.get("userId")).thenReturn(10);
when(claims.get("organizationId")).thenReturn(20);
when(claims.get("locationIds")).thenReturn(List.of(1, 2));

JwtAuthFilter filter = new JwtAuthFilter(jwtUtil);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();

filter.doFilterInternal(request, response, new MockFilterChain());

assertNotNull(SecurityContextHolder.getContext().getAuthentication());
JwtPrincipal principal = (JwtPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
assertEquals("user@example.com", principal.email());
assertEquals(Role.MANAGER, principal.role());
assertEquals(List.of(1L, 2L), principal.locationIds());
}

@Test
void authenticatesFromBearerHeaderWhenCookieMissing() throws Exception {
JwtUtil jwtUtil = mock(JwtUtil.class);
Claims claims = mock(Claims.class);
when(jwtUtil.getJwtFromCookies(org.mockito.ArgumentMatchers.any())).thenReturn(null);
when(jwtUtil.validateJwtToken("header-jwt")).thenReturn(true);
when(jwtUtil.getClaimsFromJwtToken("header-jwt")).thenReturn(claims);
when(claims.getSubject()).thenReturn("admin@example.com");
when(claims.get("role", String.class)).thenReturn("ROLE_ADMIN");
when(claims.get("userId")).thenReturn(1L);
when(claims.get("organizationId")).thenReturn(99L);
when(claims.get("locationIds")).thenReturn("not-a-list");

JwtAuthFilter filter = new JwtAuthFilter(jwtUtil);
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Authorization", "Bearer header-jwt");

filter.doFilterInternal(request, new MockHttpServletResponse(), new MockFilterChain());

JwtPrincipal principal = (JwtPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
assertEquals(Role.ADMIN, principal.role());
assertEquals(List.of(), principal.locationIds());
}

@Test
void leavesSecurityContextEmptyForInvalidTokenOrErrors() throws Exception {
JwtUtil jwtUtil = mock(JwtUtil.class);
when(jwtUtil.getJwtFromCookies(org.mockito.ArgumentMatchers.any())).thenReturn("bad");
when(jwtUtil.validateJwtToken("bad")).thenReturn(false);

JwtAuthFilter filter = new JwtAuthFilter(jwtUtil);
filter.doFilterInternal(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain());
assertNull(SecurityContextHolder.getContext().getAuthentication());

when(jwtUtil.getJwtFromCookies(org.mockito.ArgumentMatchers.any())).thenThrow(new RuntimeException("boom"));
filter.doFilterInternal(new MockHttpServletRequest(), new MockHttpServletResponse(), new MockFilterChain());
assertNull(SecurityContextHolder.getContext().getAuthentication());
}
}
77 changes: 77 additions & 0 deletions backend/src/test/java/backend/fullstack/config/JwtUtilTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package backend.fullstack.config;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.ResponseCookie;
import org.springframework.mock.web.MockHttpServletRequest;

import io.jsonwebtoken.Claims;
import jakarta.servlet.http.Cookie;

class JwtUtilTest {

private JwtUtil jwtUtil;

@BeforeEach
void setUp() {
JwtProperties properties = new JwtProperties();
properties.setSecret("0123456789012345678901234567890123456789012345678901234567890123");
properties.setExpirationMs(60_000L);
properties.setCookieName("jwt");
properties.setCookieSecure(false);
jwtUtil = new JwtUtil(properties);
}

@Test
void generateValidateAndParseToken() {
String token = jwtUtil.generateToken("user@example.com", 10L, "MANAGER", 20L, List.of(1L, 2L));

assertTrue(jwtUtil.validateJwtToken(token));

Claims claims = jwtUtil.getClaimsFromJwtToken(token);
assertEquals("user@example.com", claims.getSubject());
assertEquals("MANAGER", claims.get("role", String.class));
assertEquals(10L, ((Number) claims.get("userId")).longValue());
assertEquals(20L, ((Number) claims.get("organizationId")).longValue());
assertNotNull(claims.get("locationIds"));
}

@Test
void getJwtFromCookiesReturnsCookieValueOrNull() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(new Cookie("jwt", "token-123"));
assertEquals("token-123", jwtUtil.getJwtFromCookies(request));

MockHttpServletRequest requestWithoutCookie = new MockHttpServletRequest();
assertEquals(null, jwtUtil.getJwtFromCookies(requestWithoutCookie));
}

@Test
void generateCookiesUseExpectedDefaults() {
ResponseCookie jwtCookie = jwtUtil.generateJwtCookie("u@example.com", 1L, "ADMIN", 2L, List.of(7L));
assertEquals("jwt", jwtCookie.getName());
assertTrue(jwtCookie.toString().contains("HttpOnly"));
assertTrue(jwtCookie.toString().contains("SameSite=Strict"));

ResponseCookie fromTokenCookie = jwtUtil.generateJwtCookieFromToken("abc");
assertEquals("jwt", fromTokenCookie.getName());
assertEquals("abc", fromTokenCookie.getValue());

ResponseCookie cleanCookie = jwtUtil.getCleanJwtCookie();
assertEquals("", cleanCookie.getValue());
assertTrue(cleanCookie.toString().contains("Max-Age=0"));
}

@Test
void validateJwtTokenReturnsFalseForInvalidInput() {
assertFalse(jwtUtil.validateJwtToken("not-a-jwt"));
assertFalse(jwtUtil.validateJwtToken(""));
}
}
Loading
Loading