package com.ankurm.hibernatedemo.jndi; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NameAlreadyBoundException; import org.h2.jdbcx.JdbcDataSource; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Backs docs/10-mocking-jndi-datasources.md, chapter "'Name already bound' across two tests in the same JVM". * Simple-JNDI's {@code MemoryContextFactory}, with {@code org.osjava.sj.jndi.shared=true}, * backs its bindings with a single JVM-wide static map (see * {@code JndiDataSourceResolutionTest}'s javadoc). If test A binds a name and never unbinds it, * test B's attempt to bind the SAME name in the SAME JVM fails with * {@code NameAlreadyBoundException} -- even though the two tests share no code, no fixture, no * Spring context. {@link MethodOrderer.OrderAnnotation} pins the order here deliberately, so the * pollution is forced to happen rather than left to JUnit's default (effectively random) * ordering. */ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class CrossTestPollutionTest { private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); private static final String SHARED_NAME = "jdbc/SharedAcrossTests"; static { System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.osjava.sj.MemoryContextFactory"); System.setProperty("org.osjava.sj.jndi.shared", "true"); } @Test @Order(1) void testA_bindsAName_andDeliberatelyDoesNotCleanUp() throws Exception { JdbcDataSource ds = new JdbcDataSource(); ds.setURL("jdbc:h2:mem:pollution-a;DB_CLOSE_DELAY=-1"); Context ctx = new InitialContext(); ctx.createSubcontext("jdbc"); ctx.bind(SHARED_NAME, ds); DEMO.info("testA bound {} -- no @AfterEach unbind on purpose, to force the pollution", SHARED_NAME); // If this assertion passes, the bind succeeded. assertThat(new InitialContext().lookup(SHARED_NAME)).isNotNull(); } @Test @Order(2) void testB_bindingTheSameNameAgain_throwsNameAlreadyBoundException() throws Exception { JdbcDataSource ds2 = new JdbcDataSource(); ds2.setURL("jdbc:h2:mem:pollution-b;DB_CLOSE_DELAY=-1"); Context ctx = new InitialContext(); NameAlreadyBoundException ex = assertThrows(NameAlreadyBoundException.class, () -> ctx.bind(SHARED_NAME, ds2)); DEMO.info("testB's bind() of the SAME name testA left behind failed verbatim with: {}: {}", ex.getClass().getName(), ex.getMessage()); // The fix: rebind() instead of bind() is idempotent for this exact failure mode -- // it replaces whatever is there instead of throwing. ctx.rebind(SHARED_NAME, ds2); Object rebound = new InitialContext().lookup(SHARED_NAME); assertThat(rebound).isSameAs(ds2); DEMO.info("ctx.rebind() instead of ctx.bind() -- the standard fix -- succeeded: {}", rebound); } @Test @Order(3) void testC_theRealFix_isAfterEachUnbindNotRebind() throws Exception { // rebind() papers over the symptom but each test still leaks its binding into the next // one's JNDI namespace unless something cleans up. The actual fix is symmetry: whatever // a test binds, that same test unbinds, in @AfterEach, unconditionally. try { new InitialContext().unbind(SHARED_NAME); DEMO.info("cleanup: unbound {} so it does not leak into any test that runs after this class", SHARED_NAME); } catch (Exception e) { DEMO.info("cleanup: nothing to unbind ({})", e.toString()); } assertThrows(javax.naming.NameNotFoundException.class, () -> new InitialContext().lookup(SHARED_NAME)); } }