Merge commit 'a18d8e1d3dd02c8978033f09563d2ec9341926c7' into work/reports-ui

This commit is contained in:
sechmachine
2026-08-15 01:01:57 +07:00
5 changed files with 194 additions and 1 deletions
@@ -31,7 +31,7 @@ class SecurityConfiguration {
.permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.formLogin(form -> form.defaultSuccessUrl("/", true))
.formLogin(form -> form.loginPage("/login").defaultSuccessUrl("/", true))
.logout(logout -> logout.logoutSuccessUrl("/login?logout"))
.addFilterBefore(bootstrapAccessFilter, AuthorizationFilter.class)
.build();
@@ -0,0 +1,12 @@
package com.lab.labtimesheet.feature.account.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
class AuthenticationController {
@GetMapping("/login")
String login() {
return "accounts/login";
}
}
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"><title>Sign in · Lab Timesheet</title></head>
<body>
<main>
<h1>Sign in</h1>
<p th:if="${param.error}">Invalid email or password</p>
<p th:if="${param.logout}">You have signed out</p>
<p th:if="${param.activated}">Your account is active. Sign in to continue.</p>
<form method="post" th:action="@{/login}">
<label for="username">Email</label>
<input id="username" name="username" type="email" autocomplete="username" required autofocus>
<label for="password">Password</label>
<input id="password" name="password" type="password" autocomplete="current-password" required>
<button type="submit">Sign in</button>
</form>
</main>
</body>
</html>
@@ -0,0 +1,85 @@
package com.lab.labtimesheet.feature.account.controller;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
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.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import com.lab.labtimesheet.config.TestcontainersConfiguration;
import com.lab.labtimesheet.feature.account.service.BootstrapService;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.context.annotation.Import;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
@Import(TestcontainersConfiguration.class)
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class AuthenticationWebIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private BootstrapService bootstrap;
@BeforeEach
void initializeAdmin() {
bootstrap.bootstrap("admin@example.com", "Admin", "correct horse battery staple");
}
@Test
void projectLoginPageSupportsFailureNormalizedSuccessAndLogout() throws Exception {
mockMvc.perform(get("/login"))
.andExpect(status().isOk())
.andExpect(view().name("accounts/login"))
.andExpect(content().string(Matchers.containsString("Sign in")))
.andExpect(content().string(Matchers.containsString("action=\"/login\"")));
mockMvc.perform(post("/login")
.with(csrf())
.param("username", " ADMIN@EXAMPLE.COM ")
.param("password", "incorrect password"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/login?error"))
.andExpect(unauthenticated());
mockMvc.perform(get("/login").param("error", ""))
.andExpect(status().isOk())
.andExpect(view().name("accounts/login"))
.andExpect(content().string(Matchers.containsString("Invalid email or password")));
var login = mockMvc.perform(post("/login")
.with(csrf())
.param("username", " ADMIN@EXAMPLE.COM ")
.param("password", "correct horse battery staple"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/"))
.andExpect(authenticated().withUsername("admin@example.com"))
.andReturn();
var session = (MockHttpSession) login.getRequest().getSession(false);
mockMvc.perform(post("/logout").session(session).with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/login?logout"))
.andExpect(unauthenticated());
mockMvc.perform(get("/login").param("logout", ""))
.andExpect(status().isOk())
.andExpect(view().name("accounts/login"))
.andExpect(content().string(Matchers.containsString("You have signed out")));
}
}