Skip to content
Open
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
6 changes: 6 additions & 0 deletions modules/authentication/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ dependencies {
implementation(project(":modules:common"))
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-security-oauth2-client")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.boot:spring-boot-starter-security-oauth2-client-test")
testImplementation("org.springframework.boot:spring-boot-starter-security-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.withType<Test> {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import tools.jackson.core.JacksonException;
import tools.jackson.databind.ObjectMapper;

/**
Expand Down Expand Up @@ -54,7 +55,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
);
setDetails(request, authentication);
return getAuthenticationManager().authenticate(authentication);
} catch (IOException e) {
} catch (JacksonException | IOException e) {
throw new BadCredentialsException("Invalid login request", e);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.workastra.authentication.controller;

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.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.workastra.common.api.ApiResponse;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

class AuthControllerTest {

private final AuthController controller = new AuthController();
private MockHttpServletRequest request;

@BeforeEach
void setUpRequestContext() {
request = new MockHttpServletRequest();
request.addHeader("X-Request-Id", "test-request");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
}

@AfterEach
void clearRequestContext() {
RequestContextHolder.resetRequestAttributes();
}

@Test
void getCsrfWrapsTokenInApiResponse() {
CsrfToken csrfToken = new DefaultCsrfToken("X-CSRF", "_csrf", "token-123");

ApiResponse<CsrfToken> response = controller.getCsrf(csrfToken);

assertTrue(response.success());
assertEquals("OK", response.code());
assertNull(response.message());
assertSame(csrfToken, response.data());
assertNull(response.errors());
assertNotNull(response.meta());
}

@Test
void meReturnsAuthenticatedPrincipal() {
Authentication authentication = new UsernamePasswordAuthenticationToken("user", "password");

ApiResponse<Authentication> response = controller.me(authentication);

assertTrue(response.success());
assertEquals("OK", response.code());
assertSame(authentication, response.data());
Comment on lines +58 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This test is missing some assertions to fully verify the structure of the ApiResponse. For consistency with getCsrfWrapsTokenInApiResponse and to make the test more robust, you should also assert that message and errors are null, and that meta is not null.

        assertTrue(response.success());
        assertEquals("OK", response.code());
        assertNull(response.message());
        assertSame(authentication, response.data());
        assertNull(response.errors());
        assertNotNull(response.meta());

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package com.workastra.authentication.controller;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.workastra.authentication.infrastructure.filter;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.util.Assert;
import org.mockito.Mockito;
import tools.jackson.databind.ObjectMapper;

class JsonLoginAuthenticationFilterTest {

private AuthenticationManager authenticationManager;

private JsonLoginAuthenticationFilter filter;

@BeforeEach
void setUp() {
authenticationManager = Mockito.mock(AuthenticationManager.class);
Assert.notNull(authenticationManager, "authenticationManager");
filter = new JsonLoginAuthenticationFilter(authenticationManager, new ObjectMapper());
}

@Test
void attemptAuthenticationDelegatesToManagerWhenJsonPayloadIsValid() {
Authentication authenticated = new TestingAuthenticationToken("alice", "password");
when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authenticated);

MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.POST.name(), "/api/v1/auth/login");
request.setContent("{\"username\":\"alice\",\"password\":\"secret\"}".getBytes(StandardCharsets.UTF_8));
MockHttpServletResponse response = new MockHttpServletResponse();

Authentication result = filter.attemptAuthentication(request, response);

verify(authenticationManager).authenticate(
argThat(auth -> {
assertTrue(auth instanceof UsernamePasswordAuthenticationToken);
assertEquals("alice", auth.getPrincipal());
assertEquals("secret", auth.getCredentials());
return true;
})
);
assertEquals(authenticated, result);
}

@Test
void attemptAuthenticationRejectsNonPostMethods() {
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.name(), "/api/v1/auth/login");
MockHttpServletResponse response = new MockHttpServletResponse();

AuthenticationServiceException exception = assertThrows(
AuthenticationServiceException.class,
() -> filter.attemptAuthentication(request, response)
);

assertEquals("Authentication method not supported: GET", exception.getMessage());
}

@Test
void attemptAuthenticationRejectsInvalidJson() {
MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.POST.name(), "/api/v1/auth/login");
request.setContent("not-a-json".getBytes(StandardCharsets.UTF_8));
MockHttpServletResponse response = new MockHttpServletResponse();

BadCredentialsException exception = assertThrows(
BadCredentialsException.class,
() -> filter.attemptAuthentication(request, response)
);

assertEquals("Invalid login request", exception.getMessage());
assertEquals(HttpServletResponse.SC_OK, response.getStatus());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion is misleading and should be removed. When attemptAuthentication throws an exception, it doesn't modify the HttpServletResponse. The status code is set later in the filter chain by an exception handler (e.g., an AuthenticationEntryPoint). The MockHttpServletResponse simply retains its default status of 200 (OK), which is not a behavior of the filter being tested.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package com.workastra.authentication.infrastructure.filter;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.workastra.authentication.infrastructure.handler;

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 jakarta.servlet.http.HttpServletResponse;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;

class JsonAccessDeniedHandlerTest {

private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};

@AfterEach
void tearDown() {
RequestContextHolder.resetRequestAttributes();
}

@Test
void handleWritesForbiddenApiResponse() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("X-Request-Id", "request-456");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));

MockHttpServletResponse response = new MockHttpServletResponse();
JsonAccessDeniedHandler handler = new JsonAccessDeniedHandler(new ObjectMapper());

handler.handle(request, response, new AccessDeniedException("denied"));

assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus());
assertEquals("application/json", response.getContentType());

Map<String, Object> body = new ObjectMapper().readValue(response.getContentAsString(), MAP_TYPE);
Object success = body.get("success");
assertTrue(success instanceof Boolean);
assertFalse(((Boolean) success).booleanValue());
assertEquals("FORBIDDEN", body.get("code"));
assertEquals("You do not have permission to access this resource.", body.get("message"));

Object metaObj = body.get("meta");
assertTrue(metaObj instanceof Map);
Map<?, ?> meta = (Map<?, ?>) metaObj;
assertEquals("request-456", meta.get("requestId"));
assertNotNull(meta.get("timestamp"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.workastra.authentication.infrastructure.handler;

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 jakarta.servlet.http.HttpServletResponse;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;

class JsonAuthenticationEntryPointTest {

private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};

@AfterEach
void tearDown() {
RequestContextHolder.resetRequestAttributes();
}

@Test
void commenceWritesUnauthorizedApiResponse() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("X-Request-Id", "request-123");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));

MockHttpServletResponse response = new MockHttpServletResponse();
JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint(new ObjectMapper());

entryPoint.commence(request, response, new BadCredentialsException("bad"));

assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus());
assertEquals("application/json", response.getContentType());

Map<String, Object> body = new ObjectMapper().readValue(response.getContentAsString(), MAP_TYPE);
Object success = body.get("success");
assertTrue(success instanceof Boolean);
assertFalse(((Boolean) success).booleanValue());
assertEquals("UNAUTHORIZED", body.get("code"));
assertEquals("Authentication is required to access this resource.", body.get("message"));

Object metaObj = body.get("meta");
assertTrue(metaObj instanceof Map);
Map<?, ?> meta = (Map<?, ?>) metaObj;
assertEquals("request-123", meta.get("requestId"));
assertNotNull(meta.get("timestamp"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package com.workastra.authentication.infrastructure.handler;

import org.jspecify.annotations.NullMarked;
Loading