Public Access
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:
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user