feat(dev): add the local profile with generated keys and a dev user

Profile "local" starts without keys: LocalKeysEnvironmentPostProcessor
makes a new signing key per start and refuses to run next to prod. A
Flyway callback in db/local adds dev@localhost as PLATFORM_ADMIN with a
linked Steam ID.
This commit is contained in:
lobo committed 2026-09-28 13:07:40 +02:00
1 parent 32d4f40eec
commit f86f3c2e63
8 files changed
+285 -1

No files matched your search

@@ -0,0 +1,51 @@
package auth_service.configuration;
import auth_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.mock.env.MockEnvironment;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class LocalKeysEnvironmentPostProcessorTest extends AbstractUnitTest {
private final LocalKeysEnvironmentPostProcessor processor = new LocalKeysEnvironmentPostProcessor();
@Test
void localProfile_GetsAFreshKeyPairAndKid() throws Exception {
MockEnvironment local = new MockEnvironment();
local.setActiveProfiles("local");
processor.postProcessEnvironment(local, new SpringApplication());
assertThat(local.getProperty("security.jwt.kid")).startsWith("local-");
byte[] privateKey = Base64.getDecoder().decode(local.getProperty("security.jwt.private-key"));
assertThat(KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(privateKey))).isNotNull();
assertThat(local.getProperty("security.jwt.public-key")).isNotBlank();
}
@Test
void otherProfiles_KeepTheirConfiguredKeys() {
MockEnvironment prod = new MockEnvironment().withProperty("security.jwt.kid", "tbp-auth-1");
prod.setActiveProfiles("prod");
processor.postProcessEnvironment(prod, new SpringApplication());
assertThat(prod.getProperty("security.jwt.kid")).isEqualTo("tbp-auth-1");
assertThat(prod.getProperty("security.jwt.private-key")).isNull();
}
@Test
void localTogetherWithProd_RefusesToStart() {
MockEnvironment both = new MockEnvironment();
both.setActiveProfiles("prod", "local");
assertThatThrownBy(() -> processor.postProcessEnvironment(both, new SpringApplication()))
.isInstanceOf(IllegalStateException.class);
}
}
@@ -0,0 +1,60 @@
package auth_service.configuration;
import auth_service.configuration.security.ClientHeaderFilter;
import auth_service.shared.api.AuthCookies;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import static org.hamcrest.Matchers.startsWith;
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.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* The committed local profile: it starts without keys, and the seeded dev user logs in.
*/
@Tag("IntegrationTest")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "app.account-cleanup.cron=-")
@AutoConfigureMockMvc
@ActiveProfiles("local")
class LocalProfileTest {
// Its own database, not the one of AbstractIntegrationTest: the seeded admin would stay there for every other
// test class.
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine");
static {
postgres.start();
}
@Autowired
private MockMvc mockMvc;
@Test
void devUser_LogsInAsAdminWithItsSteamId() throws Exception {
Cookie session = mockMvc.perform(post("/api/auth/login").header(ClientHeaderFilter.CLIENT_HEADER, "web")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"dev@localhost\",\"password\":\"password123\"}"))
.andExpect(status().isOk())
.andReturn().getResponse().getCookie(AuthCookies.ACCESS_COOKIE);
mockMvc.perform(get("/api/users/me").cookie(session))
.andExpect(jsonPath("$.role").value("PLATFORM_ADMIN"))
.andExpect(jsonPath("$.steamId").value("76561198000000001"))
.andExpect(jsonPath("$.steamIdVerified").value(true));
mockMvc.perform(get("/.well-known/jwks.json"))
.andExpect(jsonPath("$.keys[0].kid", startsWith("local-")));
}
}
@@ -0,0 +1,61 @@
package auth_service.configuration;
import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
/**
* The dev and local profiles share the Postgres of compose.yaml (readme, "Run locally").
*/
@Tag("IntegrationTest")
class LocalSeedFlywayTest {
private static final String DEFAULT_LOCATIONS = "classpath:db/migration";
private static final String LOCAL_LOCATIONS = "classpath:db/migration,classpath:db/local";
@Test
void devAfterLocal_StartsOnTheSameDatabase() {
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine")) {
postgres.start();
flyway(postgres, LOCAL_LOCATIONS).migrate();
assertThatCode(() -> flyway(postgres, DEFAULT_LOCATIONS).migrate()).doesNotThrowAnyException();
}
}
@Test
void localOnADatabaseWithAUserNamedDev_StillAddsTheDevUser() throws Exception {
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine")) {
postgres.start();
flyway(postgres, DEFAULT_LOCATIONS).migrate();
try (Connection connection = DriverManager.getConnection(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword());
Statement statement = connection.createStatement()) {
statement.execute("INSERT INTO users (email, password, display_name) VALUES ('me@example.com', 'x', 'dev')");
flyway(postgres, LOCAL_LOCATIONS).migrate();
ResultSet rows = statement.executeQuery("SELECT count(*) FROM users WHERE email = 'dev@localhost'");
rows.next();
assertThat(rows.getInt(1)).isEqualTo(1);
}
}
}
// Spring Boot's defaults for this repo (application.yml): baseline-on-migrate, validate on migrate.
private static Flyway flyway(PostgreSQLContainer<?> postgres, String locations) {
return Flyway.configure()
.dataSource(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword())
.locations(locations.split(","))
.baselineOnMigrate(true)
.load();
}
}