diff --git a/server/application-server/src/test/java/de/tum/cit/aet/helios/filters/RepositoryTenantFilterIntegrationTest.java b/server/application-server/src/test/java/de/tum/cit/aet/helios/filters/RepositoryTenantFilterIntegrationTest.java new file mode 100644 index 000000000..e9cf56d54 --- /dev/null +++ b/server/application-server/src/test/java/de/tum/cit/aet/helios/filters/RepositoryTenantFilterIntegrationTest.java @@ -0,0 +1,149 @@ +package de.tum.cit.aet.helios.filters; + +import static org.assertj.core.api.Assertions.assertThat; + +import de.tum.cit.aet.helios.environment.Environment; +import de.tum.cit.aet.helios.environment.EnvironmentRepository; +import io.zonky.test.db.AutoConfigureEmbeddedDatabase; +import jakarta.persistence.EntityManagerFactory; +import java.util.List; +import javax.sql.DataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * Verifies Helios's per-repository tenant isolation: the Hibernate {@code gitRepositoryFilter} + * must actually scope a web-request query to the current repository. + * + *
This guards the regression that shipped in 1.7.0 and was fixed in 1.7.2: after the + * {@code RepositoryFilterAspect -> RepositoryFilterTransactionConfig} refactor, the filter was no + * longer enabled for web reads (under Open-Session-In-View the request transaction reuses the + * pre-opened EntityManager, so the transaction-manager initializer never fired), and every tenant + * query leaked across all repositories. {@link RepositoryInterceptor} now enables the filter on + * the request's EntityManager; this test drives that exact path against a real embedded Postgres + * and the real Flyway schema, so a future change that stops the filter from being enabled — or + * breaks the {@code @Filter} definition — fails here instead of silently leaking data. + * + *
Scope note: the servlet-level ordering that guarantees the EntityManager is bound before the
+ * interceptor runs (i.e. that OSIV runs first) is a wiring concern verified in staging/prod, not
+ * reproduced here; this test binds the EntityManager via the surrounding {@code @DataJpaTest}
+ * transaction, exactly as OSIV would at request time.
+ */
+@DataJpaTest(properties = {"spring.flyway.enabled=true", "spring.jpa.hibernate.ddl-auto=none"})
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@AutoConfigureEmbeddedDatabase(
+ type = AutoConfigureEmbeddedDatabase.DatabaseType.POSTGRES,
+ provider = AutoConfigureEmbeddedDatabase.DatabaseProvider.DOCKER)
+@Import(RepositoryTenantFilterIntegrationTest.CacheTestConfig.class)
+class RepositoryTenantFilterIntegrationTest {
+
+ /** The @DataJpaTest slice does not load cache auto-configuration; provide a simple one. */
+ @TestConfiguration
+ static class CacheTestConfig {
+ @Bean
+ CacheManager cacheManager() {
+ return new ConcurrentMapCacheManager();
+ }
+ }
+
+ private static final long REPO_A = 1L;
+ private static final long REPO_B = 2L;
+
+ @Autowired private EnvironmentRepository environmentRepository;
+ @Autowired private EntityManagerFactory entityManagerFactory;
+ @Autowired private DataSource dataSource;
+
+ private RepositoryInterceptor interceptor;
+
+ @BeforeEach
+ void seed() {
+ JdbcTemplate jdbc = new JdbcTemplate(dataSource);
+ insertRepository(jdbc, REPO_A, "ls1intum/repo-a");
+ insertRepository(jdbc, REPO_B, "ls1intum/repo-b");
+ // Two enabled environments in repo A, one in repo B.
+ insertEnabledEnvironment(jdbc, 1L, REPO_A, "repo-a-staging");
+ insertEnabledEnvironment(jdbc, 2L, REPO_A, "repo-a-production");
+ insertEnabledEnvironment(jdbc, 3L, REPO_B, "repo-b-production");
+ interceptor = new RepositoryInterceptor(entityManagerFactory);
+ }
+
+ @AfterEach
+ void clearContext() {
+ RepositoryContext.clear();
+ }
+
+ @Test
+ void scopesEnabledEnvironmentsToRepositoryA() {
+ enterWebRequestForRepository(REPO_A);
+
+ List