fix account and SMTP onboarding workflows
This commit is contained in:
+68
-1
@@ -43,7 +43,7 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
class AccountWebIntegrationTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@@ -139,6 +139,73 @@ class AccountWebIntegrationTest {
|
||||
.andExpect(unauthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidAndDuplicateAccountFormsReturnActionableErrorsWithoutCreatingAnotherAccount() throws Exception {
|
||||
mockMvc.perform(post("/admin/accounts")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf())
|
||||
.param("email", "not-an-email")
|
||||
.param("displayName", "Safe display name")
|
||||
.param("role", "INTERN")
|
||||
.param("studentCode", "")
|
||||
.param("internshipStart", "")
|
||||
.param("internshipEnd", ""))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("accounts/new"))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("valid email address")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Intern details are required")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Safe display name")));
|
||||
|
||||
mockMvc.perform(post("/admin/accounts")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf())
|
||||
.param("email", "mentor@example.com")
|
||||
.param("displayName", "Mentor One")
|
||||
.param("role", "MENTOR"))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
|
||||
mockMvc.perform(post("/admin/accounts")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf())
|
||||
.param("email", " MENTOR@EXAMPLE.COM ")
|
||||
.param("displayName", "Duplicate Mentor")
|
||||
.param("role", "MENTOR"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("accounts/new"))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("already exists")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Duplicate Mentor")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void additionalAdminActivatesAndAuthenticatesWithoutChangingTheFirstAdmin() throws Exception {
|
||||
mockMvc.perform(post("/admin/accounts")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf())
|
||||
.param("email", "second-admin@example.com")
|
||||
.param("displayName", "Second Admin")
|
||||
.param("role", "ADMIN"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/admin/accounts/new?created"));
|
||||
|
||||
String rawToken = mail.activationTokenFor("second-admin@example.com");
|
||||
mockMvc.perform(post("/activate")
|
||||
.with(csrf())
|
||||
.param("token", rawToken)
|
||||
.param("password", "new secure admin password")
|
||||
.param("confirmPassword", "new secure admin password"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/login?activated"));
|
||||
|
||||
mockMvc.perform(post("/login")
|
||||
.with(csrf())
|
||||
.param("username", "second-admin@example.com")
|
||||
.param("password", "new secure admin password"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(authenticated().withRoles("ADMIN"));
|
||||
assertThat(accounts.requireIdentityByEmail("admin@example.com").status()).isEqualTo(AccountStatus.ACTIVE);
|
||||
assertThat(accounts.requireIdentityByEmail("admin@example.com").role()).isEqualTo(GlobalRole.ADMIN);
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class MailProbeConfiguration {
|
||||
@Bean
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
package com.lab.labtimesheet.feature.account.controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
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.header;
|
||||
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 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_EACH_TEST_METHOD)
|
||||
class BootstrapOnboardingWebIntegrationTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void bootstrapOffersSmtpAfterTheFirstAdminSignsIn() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
var bootstrapResult = mockMvc.perform(post("/bootstrap")
|
||||
.session(session)
|
||||
.with(csrf())
|
||||
.param("email", " ADMIN@EXAMPLE.COM ")
|
||||
.param("displayName", "First Admin")
|
||||
.param("password", "correct horse battery staple"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/admin/smtp?onboarding"))
|
||||
.andReturn();
|
||||
assertThat(bootstrapResult.getRequest().getSession(false)).isSameAs(session);
|
||||
|
||||
mockMvc.perform(get("/admin/smtp?onboarding").session(session))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/login"));
|
||||
|
||||
mockMvc.perform(post("/login")
|
||||
.session(session)
|
||||
.with(csrf())
|
||||
.param("username", " ADMIN@EXAMPLE.COM ")
|
||||
.param("password", "correct horse battery staple"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(header().string("Location", org.hamcrest.Matchers.containsString(
|
||||
"/admin/smtp?onboarding")));
|
||||
|
||||
mockMvc.perform(get("/admin/smtp?onboarding").with(user("admin@example.com").roles("ADMIN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Configure SMTP now")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Defer SMTP")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fiveDistinctDeferralConfirmationsAreSequentialAndOnlyTheLastCanFinish() throws Exception {
|
||||
initializeAdmin();
|
||||
var first = mockMvc.perform(get("/admin/smtp/defer")
|
||||
.with(user("admin@example.com").roles("ADMIN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("smtp/defer"))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Account onboarding is disabled")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Back")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Configure SMTP")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||
org.hamcrest.Matchers.containsString("Finish without SMTP"))))
|
||||
.andReturn();
|
||||
MockHttpSession session = (MockHttpSession) first.getRequest().getSession(false);
|
||||
assertThat(session).isNotNull();
|
||||
|
||||
assertStep(session, "Activation resend is disabled", false);
|
||||
assertStep(session, "Password recovery is disabled", false);
|
||||
assertStep(session, "Workflow email delivery is less immediate", false);
|
||||
assertStep(session, "I acknowledge this installation remains restricted", true);
|
||||
|
||||
mockMvc.perform(post("/admin/smtp/defer/finish")
|
||||
.session(session)
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf()))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/dashboard"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bootstrapValidationRetainsSafeFieldsButNeverThePassword() throws Exception {
|
||||
mockMvc.perform(post("/bootstrap")
|
||||
.with(csrf())
|
||||
.param("email", "not-an-email")
|
||||
.param("displayName", "Safe Admin Name")
|
||||
.param("password", "must-not-be-rendered"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("bootstrap/form"))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("valid email address")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Safe Admin Name")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||
org.hamcrest.Matchers.containsString("must-not-be-rendered"))));
|
||||
}
|
||||
|
||||
private void initializeAdmin() throws Exception {
|
||||
mockMvc.perform(post("/bootstrap")
|
||||
.with(csrf())
|
||||
.param("email", "admin@example.com")
|
||||
.param("displayName", "Admin")
|
||||
.param("password", "correct horse battery staple"))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
private void assertStep(MockHttpSession session, String warning, boolean finishVisible) throws Exception {
|
||||
mockMvc.perform(post("/admin/smtp/defer/next")
|
||||
.session(session)
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf()))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/admin/smtp/defer"));
|
||||
|
||||
var matcher = finishVisible
|
||||
? org.hamcrest.Matchers.containsString("Finish without SMTP")
|
||||
: org.hamcrest.Matchers.not(org.hamcrest.Matchers.containsString("Finish without SMTP"));
|
||||
mockMvc.perform(get("/admin/smtp/defer")
|
||||
.session(session)
|
||||
.with(user("admin@example.com").roles("ADMIN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString(warning)))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Back")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Configure SMTP")))
|
||||
.andExpect(content().string(matcher));
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package com.lab.labtimesheet.feature.integration.controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.lab.labtimesheet.config.TestcontainersConfiguration;
|
||||
import com.lab.labtimesheet.feature.account.service.BootstrapService;
|
||||
import com.lab.labtimesheet.feature.integration.model.dto.SmtpConnection;
|
||||
import com.lab.labtimesheet.feature.integration.service.SmtpProbe;
|
||||
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.test.context.TestConfiguration;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@Import({TestcontainersConfiguration.class, SmtpOnboardingWebIntegrationTest.ProbeConfiguration.class})
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
class SmtpOnboardingWebIntegrationTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private BootstrapService bootstrap;
|
||||
|
||||
@Autowired
|
||||
private RecordingProbe probe;
|
||||
|
||||
@BeforeEach
|
||||
void initializeAdmin() {
|
||||
bootstrap.bootstrap("admin@example.com", "Admin", "correct horse battery staple");
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminCanSaveTestAndActivateSmtpWithVisibleStatus() throws Exception {
|
||||
mockMvc.perform(post("/admin/smtp/draft")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf())
|
||||
.param("host", "mailpit")
|
||||
.param("port", "1025")
|
||||
.param("securityMode", "NONE")
|
||||
.param("username", "smtp-user")
|
||||
.param("password", "smtp-secret")
|
||||
.param("fromAddress", "notifications@example.com")
|
||||
.param("fromName", "Lab Timesheet"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/admin/smtp?saved"));
|
||||
|
||||
var draftPage = mockMvc.perform(get("/admin/smtp").with(user("admin@example.com").roles("ADMIN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Draft saved")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Test connection")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||
org.hamcrest.Matchers.containsString("Activate SMTP"))))
|
||||
.andReturn();
|
||||
String html = draftPage.getResponse().getContentAsString();
|
||||
String draftId = html.replaceAll("(?s).*name=\"draftId\" value=\"([0-9]+)\".*", "$1");
|
||||
assertThat(draftId).matches("[0-9]+");
|
||||
|
||||
mockMvc.perform(post("/admin/smtp/test")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf())
|
||||
.param("draftId", draftId))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/admin/smtp?tested"));
|
||||
assertThat(probe.recipients).contains("admin@example.com");
|
||||
|
||||
mockMvc.perform(get("/admin/smtp").with(user("admin@example.com").roles("ADMIN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Test passed")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Activate SMTP")));
|
||||
|
||||
mockMvc.perform(post("/admin/smtp/activate")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf())
|
||||
.param("draftId", draftId))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrl("/admin/smtp?activated"));
|
||||
|
||||
mockMvc.perform(get("/admin/smtp").with(user("admin@example.com").roles("ADMIN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("SMTP is active")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||
org.hamcrest.Matchers.containsString("restricted installation"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidDraftRetainsOnlySafeFieldsAndRendersValidationErrors() throws Exception {
|
||||
mockMvc.perform(post("/admin/smtp/draft")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.with(csrf())
|
||||
.param("host", "")
|
||||
.param("port", "70000")
|
||||
.param("securityMode", "STARTTLS")
|
||||
.param("username", "smtp-user")
|
||||
.param("password", "must-not-be-rendered")
|
||||
.param("fromAddress", "not-an-email")
|
||||
.param("fromName", "Safe sender name"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(view().name("smtp/form"))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Host is required")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Port must be between")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("valid email address")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Safe sender name")))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.not(
|
||||
org.hamcrest.Matchers.containsString("must-not-be-rendered"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restrictedWarningPersistsOnAdminPagesUntilActivationAndMutationsRequireCsrf() throws Exception {
|
||||
mockMvc.perform(get("/admin/accounts/new").with(user("admin@example.com").roles("ADMIN")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("restricted installation")));
|
||||
|
||||
mockMvc.perform(post("/admin/smtp/draft")
|
||||
.with(user("admin@example.com").roles("ADMIN"))
|
||||
.param("host", "mailpit")
|
||||
.param("port", "1025")
|
||||
.param("securityMode", "NONE")
|
||||
.param("fromAddress", "admin@example.com")
|
||||
.param("fromName", "Lab Timesheet"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class ProbeConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
RecordingProbe recordingProbe() {
|
||||
return new RecordingProbe();
|
||||
}
|
||||
}
|
||||
|
||||
static final class RecordingProbe implements SmtpProbe {
|
||||
private final List<String> recipients = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void send(SmtpConnection connection, String recipient, String subject, String body) {
|
||||
recipients.add(recipient);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user