feat: add bootstrap and SMTP onboarding
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package com.lab.labtimesheet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import com.lab.labtimesheet.accounts.BootstrapService;
|
||||
import com.lab.labtimesheet.accounts.BootstrapService.BootstrapOutcome;
|
||||
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.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@Import(TestcontainersConfiguration.class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class BootstrapIntegrationTest extends PlatformDatabaseTestSupport {
|
||||
|
||||
@Autowired
|
||||
private BootstrapService bootstrapService;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void onlyBootstrapAndHealthAreAvailableBeforeInitialization() throws Exception {
|
||||
mockMvc.perform(get("/bootstrap")).andExpect(status().isOk());
|
||||
mockMvc.perform(get("/actuator/health")).andExpect(status().isOk());
|
||||
mockMvc.perform(get("/")).andExpect(status().isNotFound());
|
||||
|
||||
bootstrapService.bootstrap("admin@example.com", "Admin", "correct horse battery staple");
|
||||
mockMvc.perform(get("/bootstrap")).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentBootstrapCreatesExactlyOneAdminAndPermanentlyCloses() throws Exception {
|
||||
CountDownLatch ready = new CountDownLatch(2);
|
||||
CountDownLatch start = new CountDownLatch(1);
|
||||
List<Future<BootstrapOutcome>> futures = new ArrayList<>();
|
||||
|
||||
try (var executor = Executors.newFixedThreadPool(2)) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
int suffix = i;
|
||||
futures.add(executor.submit(() -> {
|
||||
ready.countDown();
|
||||
start.await();
|
||||
return bootstrapService.bootstrap(
|
||||
"admin" + suffix + "@example.com", "First Admin", "correct horse battery staple");
|
||||
}));
|
||||
}
|
||||
ready.await();
|
||||
start.countDown();
|
||||
}
|
||||
|
||||
assertThat(futures).extracting(future -> future.get()).containsExactlyInAnyOrder(
|
||||
BootstrapOutcome.CREATED, BootstrapOutcome.ALREADY_INITIALIZED);
|
||||
assertThat(jdbc.queryForObject("select count(*) from app_users", Integer.class)).isEqualTo(1);
|
||||
assertThat(jdbc.queryForObject(
|
||||
"select count(*) from app_users where global_role = 'ADMIN' and account_status = 'ACTIVE'",
|
||||
Integer.class)).isEqualTo(1);
|
||||
assertThat(bootstrapService.bootstrap(
|
||||
"another@example.com", "Another", "correct horse battery staple"))
|
||||
.isEqualTo(BootstrapOutcome.ALREADY_INITIALIZED);
|
||||
assertThat(jdbc.queryForObject("select initialized from system_state where singleton_id = 1", Boolean.class))
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.lab.labtimesheet;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
abstract class PlatformDatabaseTestSupport {
|
||||
|
||||
@Autowired
|
||||
protected JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void resetPlatformData() {
|
||||
jdbc.execute("TRUNCATE smtp_configurations, user_action_tokens, intern_profiles, app_users RESTART IDENTITY CASCADE");
|
||||
jdbc.update("insert into system_state (singleton_id) values (1)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.lab.labtimesheet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import com.lab.labtimesheet.accounts.BootstrapService;
|
||||
import com.lab.labtimesheet.configuration.SmtpConfigurationService;
|
||||
import com.lab.labtimesheet.configuration.SmtpConfigurationService.SecurityMode;
|
||||
import com.lab.labtimesheet.configuration.SmtpConfigurationService.SmtpDraft;
|
||||
import com.lab.labtimesheet.configuration.SmtpProbe;
|
||||
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.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
@Import({TestcontainersConfiguration.class, SmtpIntegrationTest.MailProbeConfiguration.class})
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class SmtpIntegrationTest extends PlatformDatabaseTestSupport {
|
||||
|
||||
@Autowired
|
||||
private BootstrapService bootstrapService;
|
||||
|
||||
@Autowired
|
||||
private SmtpConfigurationService smtpService;
|
||||
|
||||
@Autowired
|
||||
private RecordingSmtpProbe smtpProbe;
|
||||
|
||||
@Test
|
||||
void failedSmtpTestNeverActivatesDraftAndSecretsRemainEncrypted() {
|
||||
bootstrapService.bootstrap("admin@example.com", "Admin", "correct horse battery staple");
|
||||
long adminId = jdbc.queryForObject("select id from app_users", Long.class);
|
||||
long draftId = smtpService.saveDraft(adminId, new SmtpDraft(
|
||||
"mailpit", 1025, SecurityMode.NONE, "smtp-user", "smtp-password", "admin@example.com", "Lab"));
|
||||
|
||||
byte[] ciphertext = jdbc.queryForObject(
|
||||
"select password_ciphertext from smtp_configurations where id = ?", byte[].class, draftId);
|
||||
assertThat(new String(ciphertext, StandardCharsets.ISO_8859_1)).doesNotContain("smtp-password");
|
||||
assertThat(jdbc.queryForObject("select octet_length(password_nonce) from smtp_configurations where id = ?",
|
||||
Integer.class, draftId)).isEqualTo(12);
|
||||
assertThat(jdbc.queryForObject("select secret_key_version from smtp_configurations where id = ?",
|
||||
Integer.class, draftId)).isEqualTo(1);
|
||||
smtpProbe.fail = true;
|
||||
assertThatThrownBy(() -> smtpService.testDraft(draftId, adminId, "admin@example.com"))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
assertThat(jdbc.queryForObject("select status from smtp_configurations where id = ?", String.class, draftId))
|
||||
.isEqualTo("DRAFT");
|
||||
assertThat(jdbc.queryForObject("select tested_at is null from smtp_configurations where id = ?", Boolean.class,
|
||||
draftId)).isTrue();
|
||||
assertThatThrownBy(() -> smtpService.activate(draftId, adminId)).isInstanceOf(IllegalStateException.class);
|
||||
|
||||
smtpProbe.fail = false;
|
||||
smtpService.testDraft(draftId, adminId, "admin@example.com");
|
||||
smtpService.activate(draftId, adminId);
|
||||
|
||||
assertThat(jdbc.queryForObject("select status from smtp_configurations where id = ?", String.class, draftId))
|
||||
.isEqualTo("ACTIVE");
|
||||
}
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
static class MailProbeConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
RecordingSmtpProbe recordingSmtpProbe() {
|
||||
return new RecordingSmtpProbe();
|
||||
}
|
||||
}
|
||||
|
||||
static final class RecordingSmtpProbe implements SmtpProbe {
|
||||
private boolean fail;
|
||||
|
||||
@Override
|
||||
public void send(SmtpConfigurationService.SmtpConnection connection, String recipient, String subject,
|
||||
String body) {
|
||||
if (fail) {
|
||||
throw new IllegalStateException("simulated SMTP failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user