diff --git a/src/main/java/org/patinanetwork/codebloom/api/auth/AuthController.java b/src/main/java/org/patinanetwork/codebloom/api/auth/AuthController.java index 1e4f0789c..0069e0b51 100644 --- a/src/main/java/org/patinanetwork/codebloom/api/auth/AuthController.java +++ b/src/main/java/org/patinanetwork/codebloom/api/auth/AuthController.java @@ -135,7 +135,9 @@ public RedirectView logout(final HttpServletRequest request, final HttpServletRe Session session = authenticationObject.getSession(); - boolean sessionDeleted = sessionRepository.deleteSessionById(session.getId()); + String sessionId = + session.getId().orElseThrow(() -> new IllegalStateException("Authenticated session has no id")); + boolean sessionDeleted = sessionRepository.deleteSessionById(sessionId); if (!sessionDeleted) { return new RedirectView("/login?success=false&message=You are not logged in."); @@ -190,8 +192,8 @@ public RedirectView logoutAll(final HttpServletRequest request, final HttpServle @Operation( summary = "Enroll with a school email (if supported)", description = """ - Allows users to submit a school-specific email if supported. Emails will be verified with a magic link sent to their email. - """, + Allows users to submit a school-specific email if supported. Emails will be verified with a magic link sent to their email. + """, responses = { @ApiResponse(responseCode = "200", description = "email send successfully"), @ApiResponse(responseCode = "500", description = "not implemented"), diff --git a/src/main/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandler.java b/src/main/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandler.java index 6794acd8a..2715db9ae 100644 --- a/src/main/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandler.java +++ b/src/main/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandler.java @@ -178,12 +178,12 @@ public void onAuthenticationSuccess( .build(); sessionRepository.createSession(session); - if (session == null || session.getId() == null) { + if (session.getId().isEmpty()) { response.sendRedirect("/login?success=false&message=Failed to log in."); throw new RuntimeException("Failed to create new session."); } - Cookie cookie = new Cookie("session_token", session.getId()); + Cookie cookie = new Cookie("session_token", session.getId().get()); cookie.setMaxAge(maxAgeSeconds); cookie.setHttpOnly(true); diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/models/Session.java b/src/main/java/org/patinanetwork/codebloom/common/db/models/Session.java index 54c2ea777..b829cc3e9 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/models/Session.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/models/Session.java @@ -1,6 +1,7 @@ package org.patinanetwork.codebloom.common.db.models; import java.time.LocalDateTime; +import java.util.Optional; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -15,8 +16,8 @@ @ToString public class Session { - @NotNullColumn - private String id; + @Builder.Default + private Optional id = Optional.empty(); @NotNullColumn private String userId; diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/repos/session/SessionRepository.java b/src/main/java/org/patinanetwork/codebloom/common/db/repos/session/SessionRepository.java index 93f1d0127..4a6504bc2 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/repos/session/SessionRepository.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/repos/session/SessionRepository.java @@ -1,6 +1,7 @@ package org.patinanetwork.codebloom.common.db.repos.session; import java.util.ArrayList; +import java.util.Optional; import org.patinanetwork.codebloom.common.db.models.Session; public interface SessionRepository { @@ -14,7 +15,7 @@ public interface SessionRepository { */ void createSession(Session session); - Session getSessionById(String id); + Optional getSessionById(String id); ArrayList getSessionsByUserId(String userId); diff --git a/src/main/java/org/patinanetwork/codebloom/common/db/repos/session/SessionSqlRepository.java b/src/main/java/org/patinanetwork/codebloom/common/db/repos/session/SessionSqlRepository.java index 399745afc..416be5f29 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/db/repos/session/SessionSqlRepository.java +++ b/src/main/java/org/patinanetwork/codebloom/common/db/repos/session/SessionSqlRepository.java @@ -5,6 +5,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Optional; import java.util.UUID; import javax.sql.DataSource; import org.patinanetwork.codebloom.common.db.helper.NamedPreparedStatement; @@ -22,14 +23,14 @@ public SessionSqlRepository(final DataSource ds) { private Session parseResultSetToSession(final ResultSet resultSet) throws SQLException { return Session.builder() - .id(resultSet.getString("id")) + .id(Optional.of(resultSet.getString("id"))) .userId(resultSet.getString("userId")) .expiresAt(resultSet.getTimestamp("expiresAt").toLocalDateTime()) .build(); } private void updateSessionWithResultSet(final ResultSet resultSet, final Session session) throws SQLException { - session.setId(resultSet.getString("id")); + session.setId(Optional.of(resultSet.getString("id"))); } @Override @@ -37,11 +38,14 @@ public void createSession(final Session session) { String sql = "INSERT INTO \"Session\" (id, \"userId\", \"expiresAt\") VALUES (?, ?, ?) RETURNING \"id\""; // Don't want dashes inside of the cookie, so better to just remove it from the // ID altogether. - session.setId(UUID.randomUUID().toString().replace("-", "")); + session.setId(Optional.of(UUID.randomUUID().toString().replace("-", ""))); try (Connection conn = ds.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, session.getId()); + stmt.setString( + 1, + session.getId() + .orElseThrow(() -> new IllegalStateException("Session ID must be present for insertion."))); stmt.setObject(2, UUID.fromString(session.getUserId())); stmt.setObject(3, session.getExpiresAt()); @@ -56,8 +60,8 @@ public void createSession(final Session session) { } @Override - public Session getSessionById(final String id) { - Session session = null; + public Optional getSessionById(final String id) { + Optional session = Optional.empty(); String sql = "SELECT id, \"userId\", \"expiresAt\" FROM \"Session\" WHERE id=?"; try (Connection conn = ds.getConnection(); @@ -65,7 +69,7 @@ public Session getSessionById(final String id) { stmt.setString(1, id); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { - return parseResultSetToSession(rs); + return Optional.of(parseResultSetToSession(rs)); } } } catch (SQLException e) { @@ -113,11 +117,11 @@ public boolean deleteSessionById(final String id) { @Override public boolean deleteSessionsByUserId(final String userId) { String sql = """ - DELETE FROM - "Session" - WHERE - "userId" = :userId - """; + DELETE FROM + "Session" + WHERE + "userId" = :userId + """; try (Connection conn = ds.getConnection(); NamedPreparedStatement stmt = new NamedPreparedStatement(conn, sql)) { diff --git a/src/main/java/org/patinanetwork/codebloom/common/dto/session/SessionDto.java b/src/main/java/org/patinanetwork/codebloom/common/dto/session/SessionDto.java index f4a399a86..c703c474e 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/dto/session/SessionDto.java +++ b/src/main/java/org/patinanetwork/codebloom/common/dto/session/SessionDto.java @@ -27,7 +27,7 @@ public class SessionDto { public static SessionDto fromSession(final Session session) { return SessionDto.builder() - .id(session.getId()) + .id(session.getId().orElseThrow()) .userId(session.getUserId()) .expiresAt(session.getExpiresAt()) .build(); diff --git a/src/main/java/org/patinanetwork/codebloom/common/security/Protector.java b/src/main/java/org/patinanetwork/codebloom/common/security/Protector.java index 5096889dc..f5bf019ee 100644 --- a/src/main/java/org/patinanetwork/codebloom/common/security/Protector.java +++ b/src/main/java/org/patinanetwork/codebloom/common/security/Protector.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import java.time.LocalDateTime; +import java.util.Optional; import org.patinanetwork.codebloom.common.db.models.Session; import org.patinanetwork.codebloom.common.db.models.user.User; import org.patinanetwork.codebloom.common.db.repos.session.SessionRepository; @@ -38,12 +39,14 @@ public AuthenticationObject validateSession(final HttpServletRequest request) { if ("session_token".equals(cookie.getName()) && !cookie.getValue().isEmpty()) { String sessionToken = cookie.getValue(); - Session session = sessionRepository.getSessionById(sessionToken); + Optional optSession = sessionRepository.getSessionById(sessionToken); - if (session == null) { + if (optSession.isEmpty()) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unauthorized"); } + Session session = optSession.get(); + LocalDateTime now = StandardizedLocalDateTime.now(); if (session.getExpiresAt().isBefore(now)) { diff --git a/src/test/java/org/patinanetwork/codebloom/api/auth/AuthControllerTest.java b/src/test/java/org/patinanetwork/codebloom/api/auth/AuthControllerTest.java index 6de7791b0..26ca7c498 100644 --- a/src/test/java/org/patinanetwork/codebloom/api/auth/AuthControllerTest.java +++ b/src/test/java/org/patinanetwork/codebloom/api/auth/AuthControllerTest.java @@ -17,6 +17,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.time.Duration; import java.time.LocalDateTime; +import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -96,7 +97,7 @@ private User createRandomUser() { private Session createRandomSession(final String userId) { return Session.builder() - .id(UUID.randomUUID().toString().replace("-", "")) + .id(Optional.of(UUID.randomUUID().toString().replace("-", ""))) .userId(userId) .expiresAt(LocalDateTime.now().plusDays(1)) .build(); @@ -135,7 +136,7 @@ void logoutHappyPath() { HttpServletResponse response = mock(HttpServletResponse.class); when(protector.validateSession(request)).thenReturn(authObj); - when(sessionRepository.deleteSessionById(session.getId())).thenReturn(true); + when(sessionRepository.deleteSessionById(session.getId().orElseThrow())).thenReturn(true); RedirectView redirectView = authController.logout(request, response); @@ -143,7 +144,7 @@ void logoutHappyPath() { assertEquals("/login?success=true&message=You have been logged out!", redirectView.getUrl()); verify(protector, times(1)).validateSession(request); - verify(sessionRepository, times(1)).deleteSessionById(session.getId()); + verify(sessionRepository, times(1)).deleteSessionById(session.getId().orElseThrow()); } @Test @@ -157,7 +158,7 @@ void logoutSessionNotFound() { HttpServletResponse response = mock(HttpServletResponse.class); when(protector.validateSession(request)).thenReturn(authObj); - when(sessionRepository.deleteSessionById(session.getId())).thenReturn(false); + when(sessionRepository.deleteSessionById(session.getId().orElseThrow())).thenReturn(false); RedirectView redirectView = authController.logout(request, response); @@ -165,7 +166,7 @@ void logoutSessionNotFound() { assertEquals("/login?success=false&message=You are not logged in.", redirectView.getUrl()); verify(protector, times(1)).validateSession(request); - verify(sessionRepository, times(1)).deleteSessionById(session.getId()); + verify(sessionRepository, times(1)).deleteSessionById(session.getId().orElseThrow()); } @Test diff --git a/src/test/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandlerTest.java b/src/test/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandlerTest.java index 543bc86cb..f77cea8b8 100644 --- a/src/test/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandlerTest.java +++ b/src/test/java/org/patinanetwork/codebloom/api/auth/security/CustomAuthenticationSuccessHandlerTest.java @@ -85,7 +85,7 @@ void updatesNameAndSetsCookie() throws Exception { when(userRepository.getUserByDiscordId(DISCORD_ID)).thenReturn(existingUser); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("session-abc"); + s.setId(Optional.of("session-abc")); return null; }) .when(sessionRepository) @@ -121,7 +121,7 @@ void updatesProfileUrl() throws Exception { when(leetcodeClient.getUserProfile("leet_user")).thenReturn(profile); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("session-xyz"); + s.setId(Optional.of("session-xyz")); return null; }) .when(sessionRepository) @@ -146,7 +146,7 @@ void survivesLeetcodeLookupFailure() throws Exception { when(leetcodeClient.getUserProfile("bad_user")).thenThrow(new RuntimeException("API down")); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("session-fail-safe"); + s.setId(Optional.of("session-fail-safe")); return null; }) .when(sessionRepository) @@ -165,7 +165,7 @@ void createsUserAndAddsToLeaderboard() throws Exception { when(leaderboardRepository.getRecentLeaderboardMetadata()).thenReturn(Optional.of(lb)); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("new-session-id"); + s.setId(Optional.of("new-session-id")); return null; }) .when(sessionRepository) @@ -194,7 +194,7 @@ void assignsTagForGuildMember() throws Exception { when(userRepository.getUserByDiscordId(DISCORD_ID)).thenReturn(existingUser); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("s-club"); + s.setId(Optional.of("s-club")); return null; }) .when(sessionRepository) @@ -236,7 +236,7 @@ void skipsExistingTag() throws Exception { when(userRepository.getUserByDiscordId(DISCORD_ID)).thenReturn(existingUser); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("s-club"); + s.setId(Optional.of("s-club")); return null; }) .when(sessionRepository) @@ -277,7 +277,7 @@ void setsNicknameForPatinaClub() throws Exception { when(userRepository.getUserByDiscordId(DISCORD_ID)).thenReturn(existingUser); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("s-club"); + s.setId(Optional.of("s-club")); return null; }) .when(sessionRepository) @@ -318,7 +318,7 @@ void fallsBackToGlobalNameForPatina() throws Exception { when(userRepository.getUserByDiscordId(DISCORD_ID)).thenReturn(existingUser); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("s-club"); + s.setId(Optional.of("s-club")); return null; }) .when(sessionRepository) @@ -362,7 +362,7 @@ void skipsClubWithNoGuildId() throws Exception { when(userRepository.getUserByDiscordId(DISCORD_ID)).thenReturn(existingUser); doAnswer(inv -> { Session s = inv.getArgument(0); - s.setId("s-club"); + s.setId(Optional.of("s-club")); return null; }) .when(sessionRepository) diff --git a/src/test/java/org/patinanetwork/codebloom/common/db/repos/BaseRepositoryTest.java b/src/test/java/org/patinanetwork/codebloom/common/db/repos/BaseRepositoryTest.java index 772637266..08325667a 100644 --- a/src/test/java/org/patinanetwork/codebloom/common/db/repos/BaseRepositoryTest.java +++ b/src/test/java/org/patinanetwork/codebloom/common/db/repos/BaseRepositoryTest.java @@ -4,6 +4,8 @@ import org.patinanetwork.codebloom.common.email.client.github.GithubOAuthEmailClient; import org.patinanetwork.codebloom.jda.JDAClientManager; import org.patinanetwork.codebloom.jda.command.JDASlashCommandInitializer; +import org.patinanetwork.codebloom.scheduled.auth.LeetcodeAuthStealer; +import org.patinanetwork.codebloom.scheduled.submission.SubmissionScheduler; import org.springframework.test.context.bean.override.mockito.MockitoBean; /** @@ -24,4 +26,10 @@ public class BaseRepositoryTest { @MockitoBean private GithubOAuthEmailClient githubOAuthEmailClient; + + @MockitoBean + private SubmissionScheduler submissionScheduler; + + @MockitoBean + private LeetcodeAuthStealer leetcodeAuthStealer; } diff --git a/src/test/java/org/patinanetwork/codebloom/common/db/repos/session/SessionRepositoryTest.java b/src/test/java/org/patinanetwork/codebloom/common/db/repos/session/SessionRepositoryTest.java index 4762a356a..f3c8ed8e3 100644 --- a/src/test/java/org/patinanetwork/codebloom/common/db/repos/session/SessionRepositoryTest.java +++ b/src/test/java/org/patinanetwork/codebloom/common/db/repos/session/SessionRepositoryTest.java @@ -29,7 +29,7 @@ public class SessionRepositoryTest extends BaseRepositoryTest { private SessionRepository sessionRepository; private Session testSession; - private String mockUserId = "ed3bfe18-e42a-467f-b4fa-07e8da4d2555"; + private String mockUserId = "0c9b2e77-74cc-4b9e-b7f9-cfe0fd05e50b"; @Autowired public SessionRepositoryTest(final SessionRepository sessionRepository) { @@ -44,25 +44,28 @@ void createSession() { .build(); sessionRepository.createSession(testSession); - log.info("Created test session with ID: {}", testSession.getId()); + log.info("Created test session with ID: {}", testSession.getId().get()); } @AfterAll void deleteSession() { - boolean isSuccessful = sessionRepository.deleteSessionById(testSession.getId()); + String sessionId = testSession.getId().get(); + log.info("The test session to be deleted has an id of {}", sessionId); + boolean isSuccessful = sessionRepository.deleteSessionById(sessionId); if (!isSuccessful) { fail("Failed to delete test announcement"); } else { - log.info("Deleted test session with ID: {}", testSession.getId()); + log.info("Deleted test session with ID: {}", sessionId); } } @Test void testGetSessionById() { - Session found = sessionRepository.getSessionById(testSession.getId()); + String sessionId = testSession.getId().get(); + Session found = sessionRepository.getSessionById(sessionId).get(); assertNotNull(found); - assertEquals(testSession.getId(), found.getId()); + assertEquals(testSession.getId().get(), found.getId().get()); } @Test @@ -82,9 +85,10 @@ void testDeleteSessionById() { sessionRepository.createSession(tempSession); - boolean isSuccessful = sessionRepository.deleteSessionById(tempSession.getId()); + String sessionId = tempSession.getId().get(); + boolean isSuccessful = sessionRepository.deleteSessionById(sessionId); assertTrue(isSuccessful); - log.info("Deleted session with ID: {}", tempSession.getId()); + log.info("Deleted session with ID: {}", tempSession.getId().get()); } @Test diff --git a/src/test/java/org/patinanetwork/codebloom/common/security/ProtectorTest.java b/src/test/java/org/patinanetwork/codebloom/common/security/ProtectorTest.java new file mode 100644 index 000000000..1866625ff --- /dev/null +++ b/src/test/java/org/patinanetwork/codebloom/common/security/ProtectorTest.java @@ -0,0 +1,202 @@ +package org.patinanetwork.codebloom.common.security; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.patinanetwork.codebloom.common.db.models.Session; +import org.patinanetwork.codebloom.common.db.models.user.User; +import org.patinanetwork.codebloom.common.db.repos.session.SessionRepository; +import org.patinanetwork.codebloom.common.db.repos.user.UserRepository; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +class ProtectorTest { + + private final SessionRepository sessionRepository = mock(SessionRepository.class); + private final UserRepository userRepository = mock(UserRepository.class); + private final HttpServletRequest request = mock(HttpServletRequest.class); + + private Protector protector; + + @BeforeEach + void setUp() { + protector = new Protector(sessionRepository, userRepository); + } + + @Test + @DisplayName("validateSession rejects null cookies") + void rejectsNullCookies() { + when(request.getCookies()).thenReturn(null); + + ResponseStatusException exception = + assertThrows(ResponseStatusException.class, () -> protector.validateSession(request)); + + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); + verify(sessionRepository, never()).getSessionById(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + @DisplayName("validateSession rejects when session_token cookie is missing") + void rejectsMissingSessionTokenCookie() { + when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("other_cookie", "value")}); + + ResponseStatusException exception = + assertThrows(ResponseStatusException.class, () -> protector.validateSession(request)); + + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); + } + + @Test + @DisplayName("validateSession rejects empty session token") + void rejectsEmptySessionToken() { + when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("session_token", "")}); + + ResponseStatusException exception = + assertThrows(ResponseStatusException.class, () -> protector.validateSession(request)); + + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); + verify(sessionRepository, never()).getSessionById(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + @DisplayName("validateSession rejects unknown session") + void rejectsUnknownSession() { + when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("session_token", "missing-session")}); + when(sessionRepository.getSessionById("missing-session")).thenReturn(Optional.empty()); + + ResponseStatusException exception = + assertThrows(ResponseStatusException.class, () -> protector.validateSession(request)); + + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); + } + + @Test + @DisplayName("validateSession rejects expired session") + void rejectsExpiredSession() { + Session expiredSession = Session.builder() + .id(Optional.of("expired-session")) + .userId("user-1") + .expiresAt(LocalDateTime.now().minusMinutes(1)) + .build(); + + when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("session_token", "expired-session")}); + when(sessionRepository.getSessionById("expired-session")).thenReturn(Optional.of(expiredSession)); + + ResponseStatusException exception = + assertThrows(ResponseStatusException.class, () -> protector.validateSession(request)); + + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); + } + + @Test + @DisplayName("validateSession rejects when user does not exist") + void rejectsMissingUser() { + Session validSession = Session.builder() + .id(Optional.of("session-1")) + .userId("missing-user") + .expiresAt(LocalDateTime.now().plusMinutes(10)) + .build(); + + when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("session_token", "session-1")}); + when(sessionRepository.getSessionById("session-1")).thenReturn(Optional.of(validSession)); + when(userRepository.getUserById("missing-user")).thenReturn(null); + + ResponseStatusException exception = + assertThrows(ResponseStatusException.class, () -> protector.validateSession(request)); + + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); + } + + @Test + @DisplayName("validateSession returns auth object for valid session") + void returnsAuthForValidSession() { + Session validSession = Session.builder() + .id(Optional.of("session-2")) + .userId("user-2") + .expiresAt(LocalDateTime.now().plusMinutes(10)) + .build(); + + User user = User.builder() + .id("user-2") + .discordId("discord-2") + .discordName("alice") + .verifyKey("verify") + .admin(false) + .build(); + + when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("session_token", "session-2")}); + when(sessionRepository.getSessionById("session-2")).thenReturn(Optional.of(validSession)); + when(userRepository.getUserById("user-2")).thenReturn(user); + + AuthenticationObject auth = protector.validateSession(request); + + assertSame(user, auth.getUser()); + assertSame(validSession, auth.getSession()); + } + + @Test + @DisplayName("validateAdminSession rejects non-admin user") + void rejectsNonAdmin() { + Session validSession = Session.builder() + .id(Optional.of("session-3")) + .userId("user-3") + .expiresAt(LocalDateTime.now().plusMinutes(10)) + .build(); + + User nonAdminUser = User.builder() + .id("user-3") + .discordId("discord-3") + .discordName("bob") + .verifyKey("verify") + .admin(false) + .build(); + + when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("session_token", "session-3")}); + when(sessionRepository.getSessionById("session-3")).thenReturn(Optional.of(validSession)); + when(userRepository.getUserById("user-3")).thenReturn(nonAdminUser); + + ResponseStatusException exception = + assertThrows(ResponseStatusException.class, () -> protector.validateAdminSession(request)); + + assertEquals(HttpStatus.UNAUTHORIZED, exception.getStatusCode()); + } + + @Test + @DisplayName("validateAdminSession returns auth object for admin user") + void returnsAuthForAdmin() { + Session validSession = Session.builder() + .id(Optional.of("session-4")) + .userId("user-4") + .expiresAt(LocalDateTime.now().plusMinutes(10)) + .build(); + + User adminUser = User.builder() + .id("user-4") + .discordId("discord-4") + .discordName("carol") + .verifyKey("verify") + .admin(true) + .build(); + + when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("session_token", "session-4")}); + when(sessionRepository.getSessionById("session-4")).thenReturn(Optional.of(validSession)); + when(userRepository.getUserById("user-4")).thenReturn(adminUser); + + AuthenticationObject auth = protector.validateAdminSession(request); + + assertSame(adminUser, auth.getUser()); + assertSame(validSession, auth.getSession()); + } +} diff --git a/src/test/java/org/patinanetwork/codebloom/config/TestProtector.java b/src/test/java/org/patinanetwork/codebloom/config/TestProtector.java index f2783cf0e..844695df6 100644 --- a/src/test/java/org/patinanetwork/codebloom/config/TestProtector.java +++ b/src/test/java/org/patinanetwork/codebloom/config/TestProtector.java @@ -37,7 +37,9 @@ public Protector protector() { @Override public AuthenticationObject validateSession(final HttpServletRequest request) { User mockAdminUser = userRepository.getUserById("ed3bfe18-e42a-467f-b4fa-07e8da4d2555"); - Session mockAdminSession = sesssionRepository.getSessionById("d99e10a2-6285-46f0-8150-ba4727b520f4"); + Session mockAdminSession = sesssionRepository + .getSessionById("d99e10a2-6285-46f0-8150-ba4727b520f4") + .orElseThrow(); return new AuthenticationObject(mockAdminUser, mockAdminSession); }