This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-fileupload.git
The following commit(s) were added to refs/heads/master by this push:
new 22fd81b3 Add file cleaner tests
22fd81b3 is described below
commit 22fd81b371925101e6c415931e21d2b0a8d31384
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Jul 26 12:13:30 2026 -0400
Add file cleaner tests
---
commons-fileupload2-jakarta-servlet5/pom.xml | 5 +
.../jakarta/servlet5/JakartaFileCleanerTest.java | 225 +++++++++++++++++++++
commons-fileupload2-jakarta-servlet6/pom.xml | 5 +
.../jakarta/servlet6/JakartaFileCleanerTest.java | 225 +++++++++++++++++++++
commons-fileupload2-javax/pom.xml | 5 +
.../fileupload2/javax/JavaxFileCleanerTest.java | 225 +++++++++++++++++++++
6 files changed, 690 insertions(+)
diff --git a/commons-fileupload2-jakarta-servlet5/pom.xml
b/commons-fileupload2-jakarta-servlet5/pom.xml
index c4c82b40..dbfd2cf6 100644
--- a/commons-fileupload2-jakarta-servlet5/pom.xml
+++ b/commons-fileupload2-jakarta-servlet5/pom.xml
@@ -83,6 +83,11 @@
<artifactId>commons-lang3</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/commons-fileupload2-jakarta-servlet5/src/test/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaFileCleanerTest.java
b/commons-fileupload2-jakarta-servlet5/src/test/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaFileCleanerTest.java
new file mode 100644
index 00000000..b52e3eca
--- /dev/null
+++
b/commons-fileupload2-jakarta-servlet5/src/test/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaFileCleanerTest.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.fileupload2.jakarta.servlet5;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.FileCleaningTracker;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+
+/**
+ * Unit tests for {@link JakartaFileCleaner}.
+ */
+class JakartaFileCleanerTest {
+
+ /**
+ * Returns {@code true} if {@link FileCleaningTracker#exitWhenFinished()}
has been called on the given tracker (i.e. the {@code exitWhenFinished} field is
+ * {@code true}).
+ */
+ private static boolean isExitWhenFinished(final FileCleaningTracker
tracker) throws Exception {
+ final Field field =
FileCleaningTracker.class.getDeclaredField("exitWhenFinished");
+ field.setAccessible(true);
+ return field.getBoolean(tracker);
+ }
+
+ /**
+ * A simple {@link ServletContext} attribute store backed by a {@link
Map}. Used together with Mockito to give {@code getAttribute}/{@code
setAttribute}
+ * real behaviour.
+ */
+ private Map<String, Object> attributes;
+
+ private JakartaFileCleaner fileCleaner;
+
+ private ServletContext servletContext;
+
+ private ServletContextEvent servletContextEvent;
+
+ @BeforeEach
+ void setUp() {
+ attributes = new HashMap<>();
+ servletContext = mock(ServletContext.class);
+ // Wire getAttribute / setAttribute / removeAttribute to the backing
map.
+
when(servletContext.getAttribute(any(String.class))).thenAnswer(invocation ->
attributes.get(invocation.getArgument(0, String.class)));
+ Mockito.doAnswer(invocation -> {
+ attributes.put(invocation.getArgument(0),
invocation.getArgument(1));
+ return null;
+ }).when(servletContext).setAttribute(any(String.class), any());
+ servletContextEvent = mock(ServletContextEvent.class);
+
when(servletContextEvent.getServletContext()).thenReturn(servletContext);
+ fileCleaner = new JakartaFileCleaner();
+ }
+
+ @Test
+ void testConstructor() {
+ assertDoesNotThrow(JakartaFileCleaner::new, "Default constructor
should not throw");
+ }
+
+ @Test
+ void testConstructorCreatesDistinctInstances() {
+ final JakartaFileCleaner a = new JakartaFileCleaner();
+ final JakartaFileCleaner b = new JakartaFileCleaner();
+ assertEquals(a.getClass(), b.getClass());
+ assertTrue(a != b, "Each call to new JakartaFileCleaner() must yield a
distinct object");
+ }
+
+ @Test
+ void testContextDestroyedCallsExitWhenFinished() throws Exception {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker);
+ fileCleaner.contextDestroyed(servletContextEvent);
+ assertTrue(isExitWhenFinished(tracker), "After contextDestroyed the
tracker's exitWhenFinished flag should be true");
+ }
+
+ @Test
+ void testContextDestroyedDoesNotThrow() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ assertDoesNotThrow(() ->
fileCleaner.contextDestroyed(servletContextEvent), "contextDestroyed should not
throw any exception");
+ }
+
+ @Test
+ void testContextDestroyedUsesServletContextFromEvent() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ fileCleaner.contextDestroyed(servletContextEvent);
+ // getServletContext() must have been called at least twice: once for
init, once for destroy
+ verify(servletContextEvent, Mockito.atLeast(2)).getServletContext();
+ }
+
+ @Test
+ void testContextInitializedCreatesNewTrackerEachTime() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker first =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ first.exitWhenFinished();
+ // Re-initialise (simulate re-deployment)
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker second =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(second);
+ second.exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedStoresNewTracker() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final ArgumentCaptor<Object> captor =
ArgumentCaptor.forClass(Object.class);
+
verify(servletContext).setAttribute(eq(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE),
captor.capture());
+ assertNotNull(captor.getValue(), "contextInitialized should store a
non-null tracker");
+ assertInstanceOf(FileCleaningTracker.class, captor.getValue(), "Stored
object should be a FileCleaningTracker");
+ ((FileCleaningTracker) captor.getValue()).exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedTrackerIsRetrievableViaStaticHelper() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker, "Tracker stored by contextInitialized should be
retrievable via getFileCleaningTracker");
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedUsesServletContextFromEvent() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ verify(servletContextEvent).getServletContext();
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameContainsClassName() {
+
assertTrue(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.contains("JakartaFileCleaner"),
"Attribute name should contain the class name");
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameContainsSuffix() {
+
assertTrue(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.endsWith(".FileCleaningTracker"),
+ "Attribute name should end with '.FileCleaningTracker'");
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameIsNotEmpty() {
+ assertNotNull(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE);
+ assertTrue(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.length()
> 0);
+ }
+
+ @Test
+ void testFullLifecycle() throws Exception {
+ // Simulate web-application start
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker, "Tracker must be present after
contextInitialized");
+ // Simulate web-application stop
+ fileCleaner.contextDestroyed(servletContextEvent);
+ assertTrue(isExitWhenFinished(tracker), "Tracker's exitWhenFinished
must be true after contextDestroyed");
+ }
+
+ @Test
+ void testGetFileCleaningTrackerCallsGetAttributeOnContext() {
+ JakartaFileCleaner.getFileCleaningTracker(servletContext);
+
verify(servletContext).getAttribute(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE);
+ }
+
+ @Test
+ void testGetFileCleaningTrackerReturnsNullWhenNotSet() {
+ assertNull(JakartaFileCleaner.getFileCleaningTracker(servletContext),
"Should return null when no tracker has been stored");
+ }
+
+ @Test
+ void testSetAndGetFileCleaningTracker() {
+ final FileCleaningTracker tracker = new FileCleaningTracker();
+ JakartaFileCleaner.setFileCleaningTracker(servletContext, tracker);
+ assertSame(tracker,
JakartaFileCleaner.getFileCleaningTracker(servletContext),
"getFileCleaningTracker should return the same instance that was set");
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testSetFileCleaningTrackerCallsSetAttributeOnContext() {
+ final FileCleaningTracker tracker = new FileCleaningTracker();
+ JakartaFileCleaner.setFileCleaningTracker(servletContext, tracker);
+
verify(servletContext).setAttribute(eq(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE),
eq(tracker));
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testSetFileCleaningTrackerCanBeOverwritten() {
+ final FileCleaningTracker first = new FileCleaningTracker();
+ final FileCleaningTracker second = new FileCleaningTracker();
+ JakartaFileCleaner.setFileCleaningTracker(servletContext, first);
+ JakartaFileCleaner.setFileCleaningTracker(servletContext, second);
+ assertSame(second,
JakartaFileCleaner.getFileCleaningTracker(servletContext), "Second tracker
should replace the first");
+ first.exitWhenFinished();
+ second.exitWhenFinished();
+ }
+}
\ No newline at end of file
diff --git a/commons-fileupload2-jakarta-servlet6/pom.xml
b/commons-fileupload2-jakarta-servlet6/pom.xml
index a4611994..f3183a51 100644
--- a/commons-fileupload2-jakarta-servlet6/pom.xml
+++ b/commons-fileupload2-jakarta-servlet6/pom.xml
@@ -83,6 +83,11 @@
<artifactId>commons-lang3</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/commons-fileupload2-jakarta-servlet6/src/test/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaFileCleanerTest.java
b/commons-fileupload2-jakarta-servlet6/src/test/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaFileCleanerTest.java
new file mode 100644
index 00000000..5a82459c
--- /dev/null
+++
b/commons-fileupload2-jakarta-servlet6/src/test/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaFileCleanerTest.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.fileupload2.jakarta.servlet6;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.FileCleaningTracker;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletContextEvent;
+
+/**
+ * Unit tests for {@link JakartaFileCleaner}.
+ */
+class JakartaFileCleanerTest {
+
+ /**
+ * Returns {@code true} if {@link FileCleaningTracker#exitWhenFinished()}
has been called on the given tracker (i.e. the {@code exitWhenFinished} field is
+ * {@code true}).
+ */
+ private static boolean isExitWhenFinished(final FileCleaningTracker
tracker) throws Exception {
+ final Field field =
FileCleaningTracker.class.getDeclaredField("exitWhenFinished");
+ field.setAccessible(true);
+ return field.getBoolean(tracker);
+ }
+
+ /**
+ * A simple {@link ServletContext} attribute store backed by a {@link
Map}. Used together with Mockito to give {@code getAttribute}/{@code
setAttribute}
+ * real behaviour.
+ */
+ private Map<String, Object> attributes;
+
+ private JakartaFileCleaner fileCleaner;
+
+ private ServletContext servletContext;
+
+ private ServletContextEvent servletContextEvent;
+
+ @BeforeEach
+ void setUp() {
+ attributes = new HashMap<>();
+ servletContext = mock(ServletContext.class);
+ // Wire getAttribute / setAttribute / removeAttribute to the backing
map.
+
when(servletContext.getAttribute(any(String.class))).thenAnswer(invocation ->
attributes.get(invocation.getArgument(0, String.class)));
+ Mockito.doAnswer(invocation -> {
+ attributes.put(invocation.getArgument(0),
invocation.getArgument(1));
+ return null;
+ }).when(servletContext).setAttribute(any(String.class), any());
+ servletContextEvent = mock(ServletContextEvent.class);
+
when(servletContextEvent.getServletContext()).thenReturn(servletContext);
+ fileCleaner = new JakartaFileCleaner();
+ }
+
+ @Test
+ void testConstructor() {
+ assertDoesNotThrow(JakartaFileCleaner::new, "Default constructor
should not throw");
+ }
+
+ @Test
+ void testConstructorCreatesDistinctInstances() {
+ final JakartaFileCleaner a = new JakartaFileCleaner();
+ final JakartaFileCleaner b = new JakartaFileCleaner();
+ assertEquals(a.getClass(), b.getClass());
+ assertTrue(a != b, "Each call to new JakartaFileCleaner() must yield a
distinct object");
+ }
+
+ @Test
+ void testContextDestroyedCallsExitWhenFinished() throws Exception {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker);
+ fileCleaner.contextDestroyed(servletContextEvent);
+ assertTrue(isExitWhenFinished(tracker), "After contextDestroyed the
tracker's exitWhenFinished flag should be true");
+ }
+
+ @Test
+ void testContextDestroyedDoesNotThrow() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ assertDoesNotThrow(() ->
fileCleaner.contextDestroyed(servletContextEvent), "contextDestroyed should not
throw any exception");
+ }
+
+ @Test
+ void testContextDestroyedUsesServletContextFromEvent() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ fileCleaner.contextDestroyed(servletContextEvent);
+ // getServletContext() must have been called at least twice: once for
init, once for destroy
+ verify(servletContextEvent, Mockito.atLeast(2)).getServletContext();
+ }
+
+ @Test
+ void testContextInitializedCreatesNewTrackerEachTime() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker first =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ first.exitWhenFinished();
+ // Re-initialise (simulate re-deployment)
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker second =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(second);
+ second.exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedStoresNewTracker() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final ArgumentCaptor<Object> captor =
ArgumentCaptor.forClass(Object.class);
+
verify(servletContext).setAttribute(eq(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE),
captor.capture());
+ assertNotNull(captor.getValue(), "contextInitialized should store a
non-null tracker");
+ assertInstanceOf(FileCleaningTracker.class, captor.getValue(), "Stored
object should be a FileCleaningTracker");
+ ((FileCleaningTracker) captor.getValue()).exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedTrackerIsRetrievableViaStaticHelper() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker, "Tracker stored by contextInitialized should be
retrievable via getFileCleaningTracker");
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedUsesServletContextFromEvent() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ verify(servletContextEvent).getServletContext();
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameContainsClassName() {
+
assertTrue(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.contains("JakartaFileCleaner"),
"Attribute name should contain the class name");
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameContainsSuffix() {
+
assertTrue(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.endsWith(".FileCleaningTracker"),
+ "Attribute name should end with '.FileCleaningTracker'");
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameIsNotEmpty() {
+ assertNotNull(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE);
+ assertTrue(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.length()
> 0);
+ }
+
+ @Test
+ void testFullLifecycle() throws Exception {
+ // Simulate web-application start
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JakartaFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker, "Tracker must be present after
contextInitialized");
+ // Simulate web-application stop
+ fileCleaner.contextDestroyed(servletContextEvent);
+ assertTrue(isExitWhenFinished(tracker), "Tracker's exitWhenFinished
must be true after contextDestroyed");
+ }
+
+ @Test
+ void testGetFileCleaningTrackerCallsGetAttributeOnContext() {
+ JakartaFileCleaner.getFileCleaningTracker(servletContext);
+
verify(servletContext).getAttribute(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE);
+ }
+
+ @Test
+ void testGetFileCleaningTrackerReturnsNullWhenNotSet() {
+ assertNull(JakartaFileCleaner.getFileCleaningTracker(servletContext),
"Should return null when no tracker has been stored");
+ }
+
+ @Test
+ void testSetAndGetFileCleaningTracker() {
+ final FileCleaningTracker tracker = new FileCleaningTracker();
+ JakartaFileCleaner.setFileCleaningTracker(servletContext, tracker);
+ assertSame(tracker,
JakartaFileCleaner.getFileCleaningTracker(servletContext),
"getFileCleaningTracker should return the same instance that was set");
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testSetFileCleaningTrackerCallsSetAttributeOnContext() {
+ final FileCleaningTracker tracker = new FileCleaningTracker();
+ JakartaFileCleaner.setFileCleaningTracker(servletContext, tracker);
+
verify(servletContext).setAttribute(eq(JakartaFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE),
eq(tracker));
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testSetFileCleaningTrackerCanBeOverwritten() {
+ final FileCleaningTracker first = new FileCleaningTracker();
+ final FileCleaningTracker second = new FileCleaningTracker();
+ JakartaFileCleaner.setFileCleaningTracker(servletContext, first);
+ JakartaFileCleaner.setFileCleaningTracker(servletContext, second);
+ assertSame(second,
JakartaFileCleaner.getFileCleaningTracker(servletContext), "Second tracker
should replace the first");
+ first.exitWhenFinished();
+ second.exitWhenFinished();
+ }
+}
\ No newline at end of file
diff --git a/commons-fileupload2-javax/pom.xml
b/commons-fileupload2-javax/pom.xml
index b1b7c66d..d25bc771 100644
--- a/commons-fileupload2-javax/pom.xml
+++ b/commons-fileupload2-javax/pom.xml
@@ -77,6 +77,11 @@
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/commons-fileupload2-javax/src/test/java/org/apache/commons/fileupload2/javax/JavaxFileCleanerTest.java
b/commons-fileupload2-javax/src/test/java/org/apache/commons/fileupload2/javax/JavaxFileCleanerTest.java
new file mode 100644
index 00000000..aaf3345a
--- /dev/null
+++
b/commons-fileupload2-javax/src/test/java/org/apache/commons/fileupload2/javax/JavaxFileCleanerTest.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.fileupload2.javax;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+
+import org.apache.commons.io.FileCleaningTracker;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+/**
+ * Unit tests for {@link JavaxFileCleaner}.
+ */
+class JavaxFileCleanerTest {
+
+ /**
+ * Returns {@code true} if {@link FileCleaningTracker#exitWhenFinished()}
has been called on the given tracker (i.e. the {@code exitWhenFinished} field is
+ * {@code true}).
+ */
+ private static boolean isExitWhenFinished(final FileCleaningTracker
tracker) throws Exception {
+ final Field field =
FileCleaningTracker.class.getDeclaredField("exitWhenFinished");
+ field.setAccessible(true);
+ return field.getBoolean(tracker);
+ }
+
+ /**
+ * A simple {@link ServletContext} attribute store backed by a {@link
Map}. Used together with Mockito to give {@code getAttribute}/{@code
setAttribute}
+ * real behaviour.
+ */
+ private Map<String, Object> attributes;
+
+ private JavaxFileCleaner fileCleaner;
+
+ private ServletContext servletContext;
+
+ private ServletContextEvent servletContextEvent;
+
+ @BeforeEach
+ void setUp() {
+ attributes = new HashMap<>();
+ servletContext = mock(ServletContext.class);
+ // Wire getAttribute / setAttribute / removeAttribute to the backing
map.
+
when(servletContext.getAttribute(any(String.class))).thenAnswer(invocation ->
attributes.get(invocation.getArgument(0, String.class)));
+ Mockito.doAnswer(invocation -> {
+ attributes.put(invocation.getArgument(0),
invocation.getArgument(1));
+ return null;
+ }).when(servletContext).setAttribute(any(String.class), any());
+ servletContextEvent = mock(ServletContextEvent.class);
+
when(servletContextEvent.getServletContext()).thenReturn(servletContext);
+ fileCleaner = new JavaxFileCleaner();
+ }
+
+ @Test
+ void testConstructor() {
+ assertDoesNotThrow(JavaxFileCleaner::new, "Default constructor should
not throw");
+ }
+
+ @Test
+ void testConstructorCreatesDistinctInstances() {
+ final JavaxFileCleaner a = new JavaxFileCleaner();
+ final JavaxFileCleaner b = new JavaxFileCleaner();
+ assertEquals(a.getClass(), b.getClass());
+ assertTrue(a != b, "Each call to new JavaxFileCleaner() must yield a
distinct object");
+ }
+
+ @Test
+ void testContextDestroyedCallsExitWhenFinished() throws Exception {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JavaxFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker);
+ fileCleaner.contextDestroyed(servletContextEvent);
+ assertTrue(isExitWhenFinished(tracker), "After contextDestroyed the
tracker's exitWhenFinished flag should be true");
+ }
+
+ @Test
+ void testContextDestroyedDoesNotThrow() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ assertDoesNotThrow(() ->
fileCleaner.contextDestroyed(servletContextEvent), "contextDestroyed should not
throw any exception");
+ }
+
+ @Test
+ void testContextDestroyedUsesServletContextFromEvent() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ fileCleaner.contextDestroyed(servletContextEvent);
+ // getServletContext() must have been called at least twice: once for
init, once for destroy
+ verify(servletContextEvent, Mockito.atLeast(2)).getServletContext();
+ }
+
+ @Test
+ void testContextInitializedCreatesNewTrackerEachTime() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker first =
JavaxFileCleaner.getFileCleaningTracker(servletContext);
+ first.exitWhenFinished();
+ // Re-initialise (simulate re-deployment)
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker second =
JavaxFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(second);
+ second.exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedStoresNewTracker() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final ArgumentCaptor<Object> captor =
ArgumentCaptor.forClass(Object.class);
+
verify(servletContext).setAttribute(eq(JavaxFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE),
captor.capture());
+ assertNotNull(captor.getValue(), "contextInitialized should store a
non-null tracker");
+ assertInstanceOf(FileCleaningTracker.class, captor.getValue(), "Stored
object should be a FileCleaningTracker");
+ ((FileCleaningTracker) captor.getValue()).exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedTrackerIsRetrievableViaStaticHelper() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JavaxFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker, "Tracker stored by contextInitialized should be
retrievable via getFileCleaningTracker");
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testContextInitializedUsesServletContextFromEvent() {
+ fileCleaner.contextInitialized(servletContextEvent);
+ verify(servletContextEvent).getServletContext();
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameContainsClassName() {
+
assertTrue(JavaxFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.contains("JavaxFileCleaner"),
"Attribute name should contain the class name");
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameContainsSuffix() {
+
assertTrue(JavaxFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.endsWith(".FileCleaningTracker"),
+ "Attribute name should end with '.FileCleaningTracker'");
+ }
+
+ @Test
+ void testFileCleaningTrackerAttributeNameIsNotEmpty() {
+ assertNotNull(JavaxFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE);
+ assertTrue(JavaxFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE.length() >
0);
+ }
+
+ @Test
+ void testFullLifecycle() throws Exception {
+ // Simulate web-application start
+ fileCleaner.contextInitialized(servletContextEvent);
+ final FileCleaningTracker tracker =
JavaxFileCleaner.getFileCleaningTracker(servletContext);
+ assertNotNull(tracker, "Tracker must be present after
contextInitialized");
+ // Simulate web-application stop
+ fileCleaner.contextDestroyed(servletContextEvent);
+ assertTrue(isExitWhenFinished(tracker), "Tracker's exitWhenFinished
must be true after contextDestroyed");
+ }
+
+ @Test
+ void testGetFileCleaningTrackerCallsGetAttributeOnContext() {
+ JavaxFileCleaner.getFileCleaningTracker(servletContext);
+
verify(servletContext).getAttribute(JavaxFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE);
+ }
+
+ @Test
+ void testGetFileCleaningTrackerReturnsNullWhenNotSet() {
+ assertNull(JavaxFileCleaner.getFileCleaningTracker(servletContext),
"Should return null when no tracker has been stored");
+ }
+
+ @Test
+ void testSetAndGetFileCleaningTracker() {
+ final FileCleaningTracker tracker = new FileCleaningTracker();
+ JavaxFileCleaner.setFileCleaningTracker(servletContext, tracker);
+ assertSame(tracker,
JavaxFileCleaner.getFileCleaningTracker(servletContext),
"getFileCleaningTracker should return the same instance that was set");
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testSetFileCleaningTrackerCallsSetAttributeOnContext() {
+ final FileCleaningTracker tracker = new FileCleaningTracker();
+ JavaxFileCleaner.setFileCleaningTracker(servletContext, tracker);
+
verify(servletContext).setAttribute(eq(JavaxFileCleaner.FILE_CLEANING_TRACKER_ATTRIBUTE),
eq(tracker));
+ tracker.exitWhenFinished();
+ }
+
+ @Test
+ void testSetFileCleaningTrackerCanBeOverwritten() {
+ final FileCleaningTracker first = new FileCleaningTracker();
+ final FileCleaningTracker second = new FileCleaningTracker();
+ JavaxFileCleaner.setFileCleaningTracker(servletContext, first);
+ JavaxFileCleaner.setFileCleaningTracker(servletContext, second);
+ assertSame(second,
JavaxFileCleaner.getFileCleaningTracker(servletContext), "Second tracker should
replace the first");
+ first.exitWhenFinished();
+ second.exitWhenFinished();
+ }
+}
\ No newline at end of file