/files/upload) or an absolute URL (for example
+ * https://upload.example.com/files/upload). When an absolute URL is provided, the Location
+ * header in creation responses will contain the full URL. Optionally, this URI may contain regex
+ * parameters in order to support endpoints that contain URL parameters, for example
+ * /users/[0-9]+/files/upload or https://upload.example.com/users/[0-9]+/files/upload
+ * .
+ *
+ * @param uploadUri The URI or URL of the main tus upload endpoint
* @return The current service
*/
public TusFileUploadService withUploadUri(String uploadUri) {
diff --git a/src/main/java/me/desair/tus/server/creation/CreationPostRequestHandler.java b/src/main/java/me/desair/tus/server/creation/CreationPostRequestHandler.java
index ee5b2279..853910a3 100644
--- a/src/main/java/me/desair/tus/server/creation/CreationPostRequestHandler.java
+++ b/src/main/java/me/desair/tus/server/creation/CreationPostRequestHandler.java
@@ -41,7 +41,7 @@ public void process(
UploadInfo info = buildUploadInfo(servletRequest);
info = uploadStorageService.create(info, ownerKey);
- String url = Utils.getUploadUriOnCreation(info, servletRequest, null);
+ String url = Utils.getUploadUriOnCreation(info, servletRequest, uploadStorageService);
servletResponse.setHeader(HttpHeader.LOCATION, url);
servletResponse.setStatus(HttpServletResponse.SC_CREATED);
diff --git a/src/main/java/me/desair/tus/server/creation/validation/PostUriValidator.java b/src/main/java/me/desair/tus/server/creation/validation/PostUriValidator.java
index 73bcb52a..67265ac6 100644
--- a/src/main/java/me/desair/tus/server/creation/validation/PostUriValidator.java
+++ b/src/main/java/me/desair/tus/server/creation/validation/PostUriValidator.java
@@ -8,6 +8,7 @@
import me.desair.tus.server.exception.PostOnInvalidRequestURIException;
import me.desair.tus.server.exception.TusException;
import me.desair.tus.server.upload.UploadStorageService;
+import me.desair.tus.server.util.Utils;
/**
* The Client MUST send a POST request against a known upload creation URL to request a new upload
@@ -41,8 +42,9 @@ public boolean supports(HttpMethod method) {
private Pattern getUploadUriPattern(UploadStorageService uploadStorageService) {
if (uploadUriPattern == null) {
- // A POST request should match the full URI
- uploadUriPattern = Pattern.compile("^" + uploadStorageService.getUploadUri() + "$");
+ // A POST request should match the full URI path
+ String path = Utils.extractUriPath(uploadStorageService.getUploadUri());
+ uploadUriPattern = Pattern.compile("^" + path + "$");
}
return uploadUriPattern;
}
diff --git a/src/main/java/me/desair/tus/server/upload/UploadIdFactory.java b/src/main/java/me/desair/tus/server/upload/UploadIdFactory.java
index 41c53371..1d71734b 100644
--- a/src/main/java/me/desair/tus/server/upload/UploadIdFactory.java
+++ b/src/main/java/me/desair/tus/server/upload/UploadIdFactory.java
@@ -3,6 +3,7 @@
import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import me.desair.tus.server.util.Utils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.apache.commons.lang3.Validate;
@@ -17,15 +18,20 @@ public abstract class UploadIdFactory {
private Pattern uploadUriPattern = null;
/**
- * Set the URI under which the main tus upload endpoint is hosted. Optionally, this URI may
- * contain regex parameters in order to support endpoints that contain URL parameters, for example
- * /users/[0-9]+/files/upload
+ * Set the URI or absolute URL under which the main tus upload endpoint is hosted. Optionally,
+ * this URI may contain regex parameters in order to support endpoints that contain URL
+ * parameters, for example /users/[0-9]+/files/upload or
+ * https://upload.example.com/users/[0-9]+/files/upload
*
- * @param uploadUri The URI of the main tus upload endpoint
+ * @param uploadUri The URI or URL of the main tus upload endpoint
*/
public void setUploadUri(String uploadUri) {
Validate.notBlank(uploadUri, "The upload URI pattern cannot be blank");
- Validate.isTrue(Strings.CS.startsWith(uploadUri, "/"), "The upload URI should start with /");
+ Validate.isTrue(
+ Strings.CS.startsWith(uploadUri, "/")
+ || Strings.CS.startsWith(uploadUri, "http://")
+ || Strings.CS.startsWith(uploadUri, "https://"),
+ "The upload URI should start with /, http://, or https://");
Validate.isTrue(!Strings.CS.endsWith(uploadUri, "$"), "The upload URI should not end with $");
this.uploadUri = uploadUri;
this.uploadUriPattern = null;
@@ -86,8 +92,9 @@ protected Pattern getUploadUriPattern() {
if (uploadUriPattern == null) {
// We will extract the upload ID's by removing the upload URI from the start of the
// request URI
+ String path = Utils.extractUriPath(uploadUri);
uploadUriPattern =
- Pattern.compile("^.*" + uploadUri + (Strings.CS.endsWith(uploadUri, "/") ? "" : "/?"));
+ Pattern.compile("^.*" + path + (Strings.CS.endsWith(path, "/") ? "" : "/?"));
}
return uploadUriPattern;
}
diff --git a/src/main/java/me/desair/tus/server/util/Utils.java b/src/main/java/me/desair/tus/server/util/Utils.java
index 6746d903..cf2373fc 100644
--- a/src/main/java/me/desair/tus/server/util/Utils.java
+++ b/src/main/java/me/desair/tus/server/util/Utils.java
@@ -22,6 +22,7 @@
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -404,6 +405,56 @@ public static ProtocolVersion detectProtocolVersion(
return ProtocolVersion.TUS_1_0_0;
}
+ /**
+ * Extracts the path component from an upload URI string, which may be a relative path (e.g.,
+ * "/files") or an absolute URL (e.g., "https://example.com/files").
+ *
+ * @param uploadUri The upload URI or URL string
+ * @return The path component starting with "/", or "/" if none is present
+ */
+ public static String extractUriPath(String uploadUri) {
+ if (StringUtils.isBlank(uploadUri)) {
+ return "/";
+ }
+ // For absolute URLs (http:// or https://), extract the path starting after the authority
+ // component
+ if (Strings.CS.startsWith(uploadUri, "http://")
+ || Strings.CS.startsWith(uploadUri, "https://")) {
+ int schemeEnd = uploadUri.indexOf("://");
+ int pathStart = uploadUri.indexOf('/', schemeEnd + 3);
+ if (pathStart == -1) {
+ return "/";
+ }
+ return uploadUri.substring(pathStart);
+ }
+ return uploadUri;
+ }
+
+ /**
+ * Extracts the origin component (scheme + host + port) from an upload URL string, or an empty
+ * string if the URI is relative or blank.
+ *
+ * @param uploadUri The upload URI or URL string
+ * @return The origin string (e.g. "https://example.com:8080"), or "" if uploadUri is relative or
+ * blank
+ */
+ public static String extractUriOrigin(String uploadUri) {
+ if (StringUtils.isBlank(uploadUri)) {
+ return "";
+ }
+ // Extract scheme + host[:port] for absolute HTTP and HTTPS URLs
+ if (Strings.CS.startsWith(uploadUri, "http://")
+ || Strings.CS.startsWith(uploadUri, "https://")) {
+ int schemeEnd = uploadUri.indexOf("://");
+ int pathStart = uploadUri.indexOf('/', schemeEnd + 3);
+ if (pathStart == -1) {
+ return uploadUri;
+ }
+ return uploadUri.substring(0, pathStart);
+ }
+ return "";
+ }
+
/**
* Determine if the given HTTP servlet request targets the upload creation base URI endpoint.
*
@@ -417,7 +468,7 @@ public static boolean isCreationEndpoint(
return false;
}
String requestUri = request.getRequestURI();
- String baseUri = uploadStorageService.getUploadUri();
+ String baseUri = extractUriPath(uploadStorageService.getUploadUri());
return requestUri != null
&& baseUri != null
&& (requestUri.equals(baseUri) || requestUri.equals(baseUri + "/"));
@@ -450,25 +501,36 @@ public static boolean isExistingUploadResource(
/**
* Builds the upload location URI for a newly created upload resource.
*
- * @param uploadInfo The UploadInfo object containing the upload ID
+ * @param uploadInfo The UploadInfo object containing the upload ID (must not be null and must
+ * have an ID)
* @param servletRequest The current HttpServletRequest or TusServletRequest
- * @param storageService The current UploadStorageService
+ * @param storageService The current UploadStorageService (must not be null and must have an
+ * upload URI)
* @return The location URI string for the created upload
*/
public static String getUploadUriOnCreation(
UploadInfo uploadInfo,
HttpServletRequest servletRequest,
UploadStorageService storageService) {
- String baseUri = storageService != null ? storageService.getUploadUri() : null;
- if (baseUri == null && servletRequest != null) {
- baseUri = servletRequest.getRequestURI();
+ Objects.requireNonNull(uploadInfo, "Upload info cannot be null");
+ Objects.requireNonNull(uploadInfo.getId(), "Upload ID cannot be null");
+ Objects.requireNonNull(storageService, "Storage service cannot be null");
+ String configuredUri =
+ Objects.requireNonNull(storageService.getUploadUri(), "Upload URI cannot be null");
+
+ String baseUri = configuredUri;
+
+ // When configuredUri contains regex patterns (e.g. /users/[0-9]+/files),
+ // resolve the concrete request path dynamically from the incoming servlet request
+ boolean hasRegex = configuredUri.contains("[") || configuredUri.contains("(");
+ if (hasRegex && servletRequest != null) {
+ String origin = extractUriOrigin(configuredUri);
+ String requestPath = servletRequest.getRequestURI();
+ baseUri = origin + (requestPath.startsWith("/") ? "" : "/") + requestPath;
}
- if (baseUri == null) {
- baseUri = "";
- }
- String idStr =
- uploadInfo != null && uploadInfo.getId() != null ? uploadInfo.getId().toString() : "";
- return baseUri + (baseUri.endsWith("/") ? "" : "/") + idStr;
+
+ // Append the generated upload ID to form the final location URI
+ return baseUri + (baseUri.endsWith("/") ? "" : "/") + uploadInfo.getId();
}
/**
diff --git a/src/test/java/me/desair/tus/server/AbstractITRufhProtocol.java b/src/test/java/me/desair/tus/server/AbstractITRufhProtocol.java
index 4a25227c..f27fd612 100644
--- a/src/test/java/me/desair/tus/server/AbstractITRufhProtocol.java
+++ b/src/test/java/me/desair/tus/server/AbstractITRufhProtocol.java
@@ -4,6 +4,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -43,6 +44,17 @@ public abstract class AbstractITRufhProtocol {
*/
protected abstract TusFileUploadService createTusFileUploadService() throws Exception;
+ /**
+ * Factory method implemented by subclasses to supply a {@link TusFileUploadService} instance
+ * configured with a specific upload URI.
+ *
+ * @param uploadUri The upload URI to configure
+ * @return configured TusFileUploadService instance
+ * @throws Exception if service creation fails
+ */
+ protected abstract TusFileUploadService createTusFileUploadService(String uploadUri)
+ throws Exception;
+
@Before
public void setUp() throws Exception {
reset();
@@ -608,6 +620,110 @@ public void testContentDigestValidation() throws Exception {
assertResponseHeader(HttpHeader.UPLOAD_OFFSET, "12");
}
+ @Test
+ public void testUploadWithAbsoluteUploadUri() throws Exception {
+ String absoluteBaseUri = "https://uploads.example.com";
+ TusFileUploadService service = createTusFileUploadService(absoluteBaseUri);
+
+ String uploadContent = "RUFH Absolute URL content";
+
+ // Step 1: POST to create upload on root endpoint "/"
+ servletRequest.setMethod("POST");
+ servletRequest.setRequestURI("/");
+ servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, "" + uploadContent.getBytes().length);
+ servletRequest.addHeader(HttpHeader.UPLOAD_COMPLETE, "?0");
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_CREATED));
+ String locationHeader = servletResponse.getHeader(HttpHeader.LOCATION);
+ assertNotNull(locationHeader);
+
+ // Retrieve upload info using the full Location header to verify ID lookup works with absolute
+ // URLs
+ UploadInfo infoByLocation = service.getUploadInfo(locationHeader, OWNER_KEY);
+ assertTrue(infoByLocation != null && infoByLocation.getId() != null);
+ assertThat(locationHeader, is("https://uploads.example.com/" + infoByLocation.getId()));
+
+ String uploadPath = "/" + infoByLocation.getId();
+
+ // Step 2: PATCH upload bytes
+ reset();
+ servletRequest.setMethod("PATCH");
+ servletRequest.setRequestURI(uploadPath);
+ servletRequest.addHeader(HttpHeader.CONTENT_TYPE, HttpHeader.CONTENT_TYPE_PARTIAL_UPLOAD);
+ servletRequest.addHeader(HttpHeader.UPLOAD_OFFSET, "0");
+ servletRequest.addHeader(HttpHeader.UPLOAD_COMPLETE, "?1");
+ servletRequest.setContent(uploadContent.getBytes());
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_OK));
+ assertThat(
+ servletResponse.getHeader(HttpHeader.UPLOAD_OFFSET),
+ is("" + uploadContent.getBytes().length));
+ assertThat(servletResponse.getHeader(HttpHeader.UPLOAD_COMPLETE), is("?1"));
+
+ // Verify upload info is also retrievable via relative path
+ UploadInfo infoByPath = service.getUploadInfo(uploadPath, OWNER_KEY);
+ assertTrue(infoByPath != null && infoByLocation.getId().equals(infoByPath.getId()));
+
+ // Step 3: Verify content
+ try (InputStream stream = service.getUploadedBytes(uploadPath, OWNER_KEY)) {
+ assertThat(IOUtils.toString(stream, StandardCharsets.UTF_8), is(uploadContent));
+ }
+ }
+
+ @Test
+ public void testUploadWithAbsoluteUploadUriWithPath() throws Exception {
+ String absoluteBaseUri = "https://uploads.example.com/api";
+ TusFileUploadService service = createTusFileUploadService(absoluteBaseUri);
+
+ String uploadContent = "RUFH Absolute URL with path content";
+
+ // Step 1: POST to create upload on endpoint "/api"
+ servletRequest.setMethod("POST");
+ servletRequest.setRequestURI("/api");
+ servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, "" + uploadContent.getBytes().length);
+ servletRequest.addHeader(HttpHeader.UPLOAD_COMPLETE, "?0");
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_CREATED));
+ String locationHeader = servletResponse.getHeader(HttpHeader.LOCATION);
+ assertNotNull(locationHeader);
+
+ // Retrieve upload info using the full Location header to verify ID lookup works with absolute
+ // URLs
+ UploadInfo infoByLocation = service.getUploadInfo(locationHeader, OWNER_KEY);
+ assertTrue(infoByLocation != null && infoByLocation.getId() != null);
+ assertThat(locationHeader, is("https://uploads.example.com/api/" + infoByLocation.getId()));
+
+ String uploadPath = "/api/" + infoByLocation.getId();
+
+ // Step 2: PATCH upload bytes
+ reset();
+ servletRequest.setMethod("PATCH");
+ servletRequest.setRequestURI(uploadPath);
+ servletRequest.addHeader(HttpHeader.CONTENT_TYPE, HttpHeader.CONTENT_TYPE_PARTIAL_UPLOAD);
+ servletRequest.addHeader(HttpHeader.UPLOAD_OFFSET, "0");
+ servletRequest.addHeader(HttpHeader.UPLOAD_COMPLETE, "?1");
+ servletRequest.setContent(uploadContent.getBytes());
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_OK));
+ assertThat(
+ servletResponse.getHeader(HttpHeader.UPLOAD_OFFSET),
+ is("" + uploadContent.getBytes().length));
+ assertThat(servletResponse.getHeader(HttpHeader.UPLOAD_COMPLETE), is("?1"));
+
+ // Verify upload info is also retrievable via relative path
+ UploadInfo infoByPath = service.getUploadInfo(uploadPath, OWNER_KEY);
+ assertTrue(infoByPath != null && infoByLocation.getId().equals(infoByPath.getId()));
+
+ // Step 3: Verify content
+ try (InputStream stream = service.getUploadedBytes(uploadPath, OWNER_KEY)) {
+ assertThat(IOUtils.toString(stream, StandardCharsets.UTF_8), is(uploadContent));
+ }
+ }
+
// ===============================================================================================
// ASSERTION HELPERS
// ===============================================================================================
diff --git a/src/test/java/me/desair/tus/server/AbstractITTusFileUploadService.java b/src/test/java/me/desair/tus/server/AbstractITTusFileUploadService.java
index 77afd56b..4da377ef 100644
--- a/src/test/java/me/desair/tus/server/AbstractITTusFileUploadService.java
+++ b/src/test/java/me/desair/tus/server/AbstractITTusFileUploadService.java
@@ -1950,6 +1950,132 @@ public void testCreationWithUploadChecksumMismatch() throws Exception {
assertResponseStatus(460); // Checksum mismatch
}
+ @Test
+ public void testUploadWithAbsoluteUploadUri() throws Exception {
+ String absoluteBaseUri = "https://uploads.example.com";
+ TusFileUploadService service = createTusFileUploadService(absoluteBaseUri);
+
+ String uploadContent = "Absolute URL upload content";
+
+ // Step 1: POST to create upload on root endpoint "/"
+ servletRequest.setMethod("POST");
+ servletRequest.setRequestURI("/");
+ servletRequest.addHeader(HttpHeader.CONTENT_LENGTH, 0);
+ servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, uploadContent.getBytes().length);
+ servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertResponseStatus(HttpServletResponse.SC_CREATED);
+ assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
+ String locationHeader = servletResponse.getHeader(HttpHeader.LOCATION);
+ assertResponseHeaderNotBlank(HttpHeader.LOCATION);
+
+ // Retrieve upload info using the full Location header to verify ID lookup works with absolute
+ // URLs
+ UploadInfo infoByLocation = service.getUploadInfo(locationHeader, OWNER_KEY);
+ assertTrue(infoByLocation != null && infoByLocation.getId() != null);
+ assertThat(locationHeader, is("https://uploads.example.com/" + infoByLocation.getId()));
+
+ String uploadPath = "/" + infoByLocation.getId();
+
+ // Step 2: PATCH bytes to the upload resource
+ reset();
+ servletRequest.setMethod("PATCH");
+ servletRequest.setRequestURI(uploadPath);
+ servletRequest.addHeader(HttpHeader.CONTENT_TYPE, "application/offset+octet-stream");
+ servletRequest.addHeader(HttpHeader.CONTENT_LENGTH, uploadContent.getBytes().length);
+ servletRequest.addHeader(HttpHeader.UPLOAD_OFFSET, 0);
+ servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
+ servletRequest.setContent(uploadContent.getBytes());
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertResponseStatus(HttpServletResponse.SC_NO_CONTENT);
+ assertResponseHeader(HttpHeader.UPLOAD_OFFSET, "" + uploadContent.getBytes().length);
+
+ // Step 3: HEAD request to verify completion
+ reset();
+ servletRequest.setMethod("HEAD");
+ servletRequest.setRequestURI(uploadPath);
+ servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertResponseStatus(HttpServletResponse.SC_NO_CONTENT);
+ assertResponseHeader(HttpHeader.UPLOAD_OFFSET, "" + uploadContent.getBytes().length);
+ assertResponseHeader(HttpHeader.UPLOAD_LENGTH, "" + uploadContent.getBytes().length);
+
+ // Verify upload info is also retrievable via relative path
+ UploadInfo infoByPath = service.getUploadInfo(uploadPath, OWNER_KEY);
+ assertTrue(infoByPath != null && infoByLocation.getId().equals(infoByPath.getId()));
+
+ // Step 4: Verify uploaded bytes
+ try (InputStream stream = service.getUploadedBytes(uploadPath, OWNER_KEY)) {
+ assertThat(IOUtils.toString(stream, StandardCharsets.UTF_8), is(uploadContent));
+ }
+ }
+
+ @Test
+ public void testUploadWithAbsoluteUploadUriWithPath() throws Exception {
+ String absoluteBaseUri = "https://uploads.example.com/api";
+ TusFileUploadService service = createTusFileUploadService(absoluteBaseUri);
+
+ String uploadContent = "Absolute URL with path upload content";
+
+ // Step 1: POST to create upload on endpoint "/api"
+ servletRequest.setMethod("POST");
+ servletRequest.setRequestURI("/api");
+ servletRequest.addHeader(HttpHeader.CONTENT_LENGTH, 0);
+ servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, uploadContent.getBytes().length);
+ servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertResponseStatus(HttpServletResponse.SC_CREATED);
+ assertResponseHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
+ String locationHeader = servletResponse.getHeader(HttpHeader.LOCATION);
+ assertResponseHeaderNotBlank(HttpHeader.LOCATION);
+
+ // Retrieve upload info using the full Location header to verify ID lookup works with absolute
+ // URLs
+ UploadInfo infoByLocation = service.getUploadInfo(locationHeader, OWNER_KEY);
+ assertTrue(infoByLocation != null && infoByLocation.getId() != null);
+ assertThat(locationHeader, is("https://uploads.example.com/api/" + infoByLocation.getId()));
+
+ String uploadPath = "/api/" + infoByLocation.getId();
+
+ // Step 2: PATCH bytes to the upload resource
+ reset();
+ servletRequest.setMethod("PATCH");
+ servletRequest.setRequestURI(uploadPath);
+ servletRequest.addHeader(HttpHeader.CONTENT_TYPE, "application/offset+octet-stream");
+ servletRequest.addHeader(HttpHeader.CONTENT_LENGTH, uploadContent.getBytes().length);
+ servletRequest.addHeader(HttpHeader.UPLOAD_OFFSET, 0);
+ servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
+ servletRequest.setContent(uploadContent.getBytes());
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertResponseStatus(HttpServletResponse.SC_NO_CONTENT);
+ assertResponseHeader(HttpHeader.UPLOAD_OFFSET, "" + uploadContent.getBytes().length);
+
+ // Step 3: HEAD request to verify completion
+ reset();
+ servletRequest.setMethod("HEAD");
+ servletRequest.setRequestURI(uploadPath);
+ servletRequest.addHeader(HttpHeader.TUS_RESUMABLE, "1.0.0");
+
+ service.process(servletRequest, servletResponse, OWNER_KEY);
+ assertResponseStatus(HttpServletResponse.SC_NO_CONTENT);
+ assertResponseHeader(HttpHeader.UPLOAD_OFFSET, "" + uploadContent.getBytes().length);
+ assertResponseHeader(HttpHeader.UPLOAD_LENGTH, "" + uploadContent.getBytes().length);
+
+ // Verify upload info is also retrievable via relative path
+ UploadInfo infoByPath = service.getUploadInfo(uploadPath, OWNER_KEY);
+ assertTrue(infoByPath != null && infoByLocation.getId().equals(infoByPath.getId()));
+
+ // Step 4: Verify uploaded bytes
+ try (InputStream stream = service.getUploadedBytes(uploadPath, OWNER_KEY)) {
+ assertThat(IOUtils.toString(stream, StandardCharsets.UTF_8), is(uploadContent));
+ }
+ }
+
protected void assertResponseHeader(final String header, final String value) {
assertThat(servletResponse.getHeader(header), is(value));
}
diff --git a/src/test/java/me/desair/tus/server/ITLeaseFileRufhProtocol.java b/src/test/java/me/desair/tus/server/ITLeaseFileRufhProtocol.java
index 39c9a67b..0ba2d781 100644
--- a/src/test/java/me/desair/tus/server/ITLeaseFileRufhProtocol.java
+++ b/src/test/java/me/desair/tus/server/ITLeaseFileRufhProtocol.java
@@ -32,8 +32,13 @@ public static void destroyDataFolder() throws IOException {
@Override
protected TusFileUploadService createTusFileUploadService() {
+ return createTusFileUploadService(UPLOAD_URI);
+ }
+
+ @Override
+ protected TusFileUploadService createTusFileUploadService(String uploadUri) {
return new TusFileUploadService()
- .withUploadUri(UPLOAD_URI)
+ .withUploadUri(uploadUri)
.withUploadStorageService(new DiskStorageService(storagePath.toAbsolutePath().toString()))
.withUploadLockingService(
new LeaseFileLockingService(storagePath.toAbsolutePath().toString()))
diff --git a/src/test/java/me/desair/tus/server/ITRufhProtocol.java b/src/test/java/me/desair/tus/server/ITRufhProtocol.java
index 0a675318..9f02cba0 100644
--- a/src/test/java/me/desair/tus/server/ITRufhProtocol.java
+++ b/src/test/java/me/desair/tus/server/ITRufhProtocol.java
@@ -28,8 +28,13 @@ public static void destroyDataFolder() throws IOException {
@Override
protected TusFileUploadService createTusFileUploadService() {
+ return createTusFileUploadService(UPLOAD_URI);
+ }
+
+ @Override
+ protected TusFileUploadService createTusFileUploadService(String uploadUri) {
return new TusFileUploadService()
- .withUploadUri(UPLOAD_URI)
+ .withUploadUri(uploadUri)
.withStoragePath(storagePath.toAbsolutePath().toString())
.withMaxUploadSize(1073741824L)
.withUploadExpirationPeriod(2L * 24 * 60 * 60 * 1000)
diff --git a/src/test/java/me/desair/tus/server/TusFileUploadServiceTest.java b/src/test/java/me/desair/tus/server/TusFileUploadServiceTest.java
index e8f12921..0cc57adf 100644
--- a/src/test/java/me/desair/tus/server/TusFileUploadServiceTest.java
+++ b/src/test/java/me/desair/tus/server/TusFileUploadServiceTest.java
@@ -611,4 +611,13 @@ public void testAcquireUploadLockWithConfiguredRetries() throws Exception {
assertNotNull(lock);
verify(mockLockingService, times(2)).requestLockRelease("/files/test");
}
+
+ @Test
+ public void testWithUploadUriAbsoluteUrl() {
+ TusFileUploadService service =
+ new TusFileUploadService().withUploadUri("https://upload.example.com/files/upload");
+ assertEquals(
+ "https://upload.example.com/files/upload",
+ service.getUploadStorageService().getUploadUri());
+ }
}
diff --git a/src/test/java/me/desair/tus/server/creation/CreationPostRequestHandlerTest.java b/src/test/java/me/desair/tus/server/creation/CreationPostRequestHandlerTest.java
index 0e28bb90..6b76a4e5 100644
--- a/src/test/java/me/desair/tus/server/creation/CreationPostRequestHandlerTest.java
+++ b/src/test/java/me/desair/tus/server/creation/CreationPostRequestHandlerTest.java
@@ -50,6 +50,7 @@ public void setUp() {
servletRequest = new MockHttpServletRequest();
servletResponse = new MockHttpServletResponse();
handler = new CreationPostRequestHandler();
+ when(uploadStorageService.getUploadUri()).thenReturn("/test/upload");
}
@Test
@@ -211,4 +212,69 @@ public UploadInfo answer(InvocationOnMock invocation) throws Throwable {
servletResponse.getHeader(HttpHeader.LOCATION), endsWith("/test/upload/" + id.toString()));
assertThat(servletResponse.getStatus(), is(HttpServletResponse.SC_CREATED));
}
+
+ @Test
+ public void processWithAbsoluteUploadUri() throws Exception {
+ servletRequest.setRequestURI("/test/upload");
+ servletRequest.addHeader(HttpHeader.UPLOAD_LENGTH, 100L);
+ when(uploadStorageService.getUploadUri()).thenReturn("https://upload.example.com/test/upload");
+
+ final UploadId id = new UploadId(UUID.randomUUID());
+ when(uploadStorageService.create(
+ ArgumentMatchers.any(UploadInfo.class), nullable(String.class)))
+ .then(
+ new AnswerTests upload creation when configured with an absolute base URL. + */ + @Test + public void testUploadCreationWithAbsoluteBaseUrl() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/files"); + request.addHeader(HttpHeader.UPLOAD_LENGTH, "10000"); + request.addHeader(HttpHeader.UPLOAD_COMPLETE, "?0"); + when(storageService.getUploadUri()).thenReturn("https://upload.example.com/files"); + + UploadInfo info = new UploadInfo(); + info.setLength(10000L); + info.setOffset(0L); + info.setId(new UuidUploadIdFactory().createId()); + + when(storageService.create(any(UploadInfo.class), nullable(String.class))).thenReturn(info); + when(storageService.append(any(UploadInfo.class), any())).thenReturn(info); + + protocol.validate( + HttpMethod.POST, request, storageService, lockingService, null, ProtocolVersion.RUFH); + protocol.process( + HttpMethod.POST, + new TusServletRequest(request, true), + new TusServletResponse(response), + storageService, + lockingService, + null, + ProtocolVersion.RUFH); + + assertThat(response.getStatus(), is(201)); + assertThat(response.getHeader(HttpHeader.UPLOAD_COMPLETE), is("?0")); + assertThat( + response.getHeader(HttpHeader.LOCATION), + is("https://upload.example.com/files/" + info.getId())); + } } diff --git a/src/test/java/me/desair/tus/server/rufh/handler/RufhCreationPostRequestHandlerTest.java b/src/test/java/me/desair/tus/server/rufh/handler/RufhCreationPostRequestHandlerTest.java index 84b6d36d..3db6a575 100644 --- a/src/test/java/me/desair/tus/server/rufh/handler/RufhCreationPostRequestHandlerTest.java +++ b/src/test/java/me/desair/tus/server/rufh/handler/RufhCreationPostRequestHandlerTest.java @@ -120,38 +120,6 @@ public void testProcessCreationRegistersInputStream() throws Exception { .registerInputStream(eq("/files/creation-id"), any(InterruptibleInputStream.class)); } - @Test - public void testProcessWithNullBaseUri() throws Exception { - handler = new RufhCreationPostRequestHandler(); - - request.setMethod("POST"); - request.setRequestURI("/files"); - request.addHeader(HttpHeader.UPLOAD_LENGTH, "5000"); - request.addHeader(HttpHeader.UPLOAD_COMPLETE, "?0"); - - UploadInfo info = new UploadInfo(); - info.setId(new UploadId("creation-id")); - info.setLength(5000L); - info.setOffset(0L); - - // Mock getUploadUri to return null to test fallback to requestURI - when(storageService.getUploadUri()).thenReturn(null); - when(storageService.create(any(UploadInfo.class), nullable(String.class))).thenReturn(info); - when(storageService.append(any(UploadInfo.class), any())).thenReturn(info); - - handler.process( - HttpMethod.POST, - new TusServletRequest(request), - new TusServletResponse(response), - storageService, - lockingService, - "owner", - null); - - assertThat(response.getStatus(), is(201)); - assertThat(response.getHeader(HttpHeader.LOCATION), is("/files/creation-id")); - } - @Test public void testProcessExistingUploadPatchReturnsEarly() throws Exception { request.setMethod("PATCH"); @@ -234,13 +202,13 @@ public void testProcessPatchCreationWhenUploadDoesNotExist() throws Exception { } @Test - public void testProcessNegativeLengthAndNullUploadId() throws Exception { + public void testProcessNegativeLength() throws Exception { request.setMethod("POST"); request.setRequestURI("/files"); request.addHeader(HttpHeader.UPLOAD_LENGTH, "-500"); UploadInfo info = new UploadInfo(); - info.setId(null); // Null ID + info.setId(new UploadId("neg-id")); info.setOffset(0L); when(storageService.create(any(UploadInfo.class), nullable(String.class))).thenReturn(info); @@ -255,8 +223,7 @@ public void testProcessNegativeLengthAndNullUploadId() throws Exception { null); assertThat(response.getStatus(), is(201)); - // Location header ends with "/" because ID is empty string - assertThat(response.getHeader(HttpHeader.LOCATION), is("/files/")); + assertThat(response.getHeader(HttpHeader.LOCATION), is("/files/neg-id")); } @Test @@ -438,4 +405,66 @@ public void testProcessWithPreCreatedUploadInfoNegativeLength() throws Exception verify(storageService).update(preCreated); assertThat(preCreated.getLength(), is(50L)); } + + @Test + public void testProcessPartialUploadCreationWithAbsoluteUploadUri() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/files"); + request.addHeader(HttpHeader.UPLOAD_LENGTH, "5000"); + request.addHeader(HttpHeader.UPLOAD_COMPLETE, "?0"); + when(storageService.getUploadUri()).thenReturn("https://upload.example.com/files"); + + UploadInfo info = new UploadInfo(); + info.setId(new UploadId("creation-id")); + info.setLength(5000L); + info.setOffset(0L); + when(storageService.create(any(UploadInfo.class), nullable(String.class))).thenReturn(info); + when(storageService.append(any(UploadInfo.class), any())).thenReturn(info); + + handler.process( + HttpMethod.POST, + new TusServletRequest(request), + new TusServletResponse(response), + storageService, + lockingService, + "owner", + null); + + assertThat(response.getStatus(), is(201)); + assertThat( + response.getHeader(HttpHeader.LOCATION), + is("https://upload.example.com/files/creation-id")); + assertThat(response.getHeader(HttpHeader.UPLOAD_OFFSET), is("0")); + assertThat(response.getHeader(HttpHeader.UPLOAD_COMPLETE), is("?0")); + } + + @Test + public void testProcessCompletedUploadCreationWithAbsoluteUploadUri() throws Exception { + request.setMethod("POST"); + request.setRequestURI("/files"); + request.addHeader(HttpHeader.UPLOAD_COMPLETE, "?1"); + when(storageService.getUploadUri()).thenReturn("https://upload.example.com/files"); + + UploadInfo info = new UploadInfo(); + info.setId(new UploadId("completed-id")); + info.setLength(100L); + info.setOffset(100L); + when(storageService.create(any(UploadInfo.class), nullable(String.class))).thenReturn(info); + + handler.process( + HttpMethod.POST, + new TusServletRequest(request), + new TusServletResponse(response), + storageService, + lockingService, + "owner", + null); + + assertThat(response.getStatus(), is(200)); + assertThat( + response.getHeader(HttpHeader.LOCATION), + is("https://upload.example.com/files/completed-id")); + assertThat(response.getHeader(HttpHeader.UPLOAD_OFFSET), is("100")); + assertThat(response.getHeader(HttpHeader.UPLOAD_COMPLETE), is("?1")); + } } diff --git a/src/test/java/me/desair/tus/server/rufh/util/RufhInterimResponseUtilTest.java b/src/test/java/me/desair/tus/server/rufh/util/RufhInterimResponseUtilTest.java index b0276d00..7a103041 100644 --- a/src/test/java/me/desair/tus/server/rufh/util/RufhInterimResponseUtilTest.java +++ b/src/test/java/me/desair/tus/server/rufh/util/RufhInterimResponseUtilTest.java @@ -103,6 +103,7 @@ public void testGetRawInterimResponseWithExistingUploadNotFoundAndNullHost() thr me.desair.tus.server.upload.UploadStorageService mockStorage = org.mockito.Mockito.mock(me.desair.tus.server.upload.UploadStorageService.class); + org.mockito.Mockito.when(mockStorage.getUploadUri()).thenReturn("/files"); me.desair.tus.server.upload.UploadInfo created = new me.desair.tus.server.upload.UploadInfo(); created.setId(new me.desair.tus.server.upload.UploadId("created-456")); @@ -115,7 +116,7 @@ public void testGetRawInterimResponseWithExistingUploadNotFoundAndNullHost() thr String raw = RufhInterimResponseUtil.getRawInterimResponse(request, mockStorage, "owner"); assertNotNull(raw); - assertTrue(raw.contains("Location: /files/not-found-123/created-456")); + assertTrue(raw.contains("Location: /files/created-456")); } @Test @@ -169,6 +170,7 @@ public void testGetRawInterimResponseWithNullMethodAndPartialSchemeHost() throws me.desair.tus.server.upload.UploadStorageService mockStorage = org.mockito.Mockito.mock(me.desair.tus.server.upload.UploadStorageService.class); + org.mockito.Mockito.when(mockStorage.getUploadUri()).thenReturn("/files"); assertNull(RufhInterimResponseUtil.getRawInterimResponse(request, mockStorage, "owner")); diff --git a/src/test/java/me/desair/tus/server/upload/UuidUploadIdFactoryTest.java b/src/test/java/me/desair/tus/server/upload/UuidUploadIdFactoryTest.java index 810df8a4..5049eb75 100644 --- a/src/test/java/me/desair/tus/server/upload/UuidUploadIdFactoryTest.java +++ b/src/test/java/me/desair/tus/server/upload/UuidUploadIdFactoryTest.java @@ -104,6 +104,57 @@ public void readUploadIdRegexNoMatch() throws Exception { is(nullValue())); } + @Test + public void setUploadUriAbsoluteHttp() throws Exception { + idFactory.setUploadUri("http://localhost:8080/test/upload"); + assertThat(idFactory.getUploadUri(), is("http://localhost:8080/test/upload")); + } + + @Test + public void setUploadUriAbsoluteHttps() throws Exception { + idFactory.setUploadUri("https://upload.example.com/test/upload/"); + assertThat(idFactory.getUploadUri(), is("https://upload.example.com/test/upload/")); + } + + @Test + public void readUploadIdAbsoluteUrlWithRelativeConfig() throws Exception { + idFactory.setUploadUri("/test/upload"); + + assertThat( + idFactory.readUploadId( + "https://upload.example.com/test/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), + hasToString("1911e8a4-6939-490c-b58b-a5d70f8d91fb")); + } + + @Test + public void readUploadIdAbsoluteUrlWithAbsoluteConfig() throws Exception { + idFactory.setUploadUri("https://upload.example.com/test/upload"); + + assertThat( + idFactory.readUploadId( + "https://upload.example.com/test/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), + hasToString("1911e8a4-6939-490c-b58b-a5d70f8d91fb")); + } + + @Test + public void readUploadIdRelativeUrlWithAbsoluteConfig() throws Exception { + idFactory.setUploadUri("https://upload.example.com/test/upload"); + + assertThat( + idFactory.readUploadId("/test/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), + hasToString("1911e8a4-6939-490c-b58b-a5d70f8d91fb")); + } + + @Test + public void readUploadIdRegexAbsoluteUrl() throws Exception { + idFactory.setUploadUri("https://upload.example.com/users/[0-9]+/files/upload"); + + assertThat( + idFactory.readUploadId( + "https://upload.example.com/users/42/files/upload/1911e8a4-6939-490c-b58b-a5d70f8d91fb"), + hasToString("1911e8a4-6939-490c-b58b-a5d70f8d91fb")); + } + @Test public void createId() throws Exception { assertThat(idFactory.createId(), not(nullValue())); diff --git a/src/test/java/me/desair/tus/server/upload/azure/ITAzureBlobRufhProtocol.java b/src/test/java/me/desair/tus/server/upload/azure/ITAzureBlobRufhProtocol.java index 02a9d47d..bbf24407 100644 --- a/src/test/java/me/desair/tus/server/upload/azure/ITAzureBlobRufhProtocol.java +++ b/src/test/java/me/desair/tus/server/upload/azure/ITAzureBlobRufhProtocol.java @@ -40,6 +40,11 @@ public static void tearDownClass() { @Override protected TusFileUploadService createTusFileUploadService() { + return createTusFileUploadService(UPLOAD_URI); + } + + @Override + protected TusFileUploadService createTusFileUploadService(String uploadUri) { org.junit.Assume.assumeTrue(TestUtils.isContainerRuntimeAvailable()); AzureBlobStorageService azureStorage = new AzureBlobStorageService(containerClient); @@ -49,7 +54,7 @@ protected TusFileUploadService createTusFileUploadService() { azureStorage.setUploadConcatenationService(azureConcat); return new TusFileUploadService() - .withUploadUri(UPLOAD_URI) + .withUploadUri(uploadUri) .withUploadStorageService(azureStorage) .withUploadLockingService(azureLocking) .withMaxUploadSize(1073741824L) diff --git a/src/test/java/me/desair/tus/server/upload/s3/ITS3RufhProtocol.java b/src/test/java/me/desair/tus/server/upload/s3/ITS3RufhProtocol.java index a064290b..f281926c 100644 --- a/src/test/java/me/desair/tus/server/upload/s3/ITS3RufhProtocol.java +++ b/src/test/java/me/desair/tus/server/upload/s3/ITS3RufhProtocol.java @@ -40,6 +40,11 @@ public static void tearDownClass() { @Override protected TusFileUploadService createTusFileUploadService() { + return createTusFileUploadService(UPLOAD_URI); + } + + @Override + protected TusFileUploadService createTusFileUploadService(String uploadUri) { org.junit.Assume.assumeTrue(TestUtils.isContainerRuntimeAvailable()); S3StorageService s3Storage = new S3StorageService(minioClient, BUCKET); @@ -48,7 +53,7 @@ protected TusFileUploadService createTusFileUploadService() { s3Storage.setUploadConcatenationService(s3Concat); return new TusFileUploadService() - .withUploadUri(UPLOAD_URI) + .withUploadUri(uploadUri) .withUploadStorageService(s3Storage) .withUploadLockingService(s3Locking) .withMaxUploadSize(1073741824L) diff --git a/src/test/java/me/desair/tus/server/util/UtilsTest.java b/src/test/java/me/desair/tus/server/util/UtilsTest.java index a8806a62..e8f96e1b 100644 --- a/src/test/java/me/desair/tus/server/util/UtilsTest.java +++ b/src/test/java/me/desair/tus/server/util/UtilsTest.java @@ -444,6 +444,31 @@ public void testDetectProtocolVersion() { is(me.desair.tus.server.ProtocolVersion.TUS_1_0_0)); } + @Test + public void testExtractUriPath() { + assertThat(Utils.extractUriPath("/api/files"), is("/api/files")); + assertThat(Utils.extractUriPath("https://upload.example.com/api/files"), is("/api/files")); + assertThat(Utils.extractUriPath("http://localhost:8080/files/upload"), is("/files/upload")); + assertThat(Utils.extractUriPath("https://test.example.com/uploads"), is("/uploads")); + assertThat(Utils.extractUriPath("https://upload.example.com"), is("/")); + assertThat(Utils.extractUriPath("https://upload.example.com/"), is("/")); + assertThat(Utils.extractUriPath(null), is("/")); + assertThat(Utils.extractUriPath(""), is("/")); + } + + @Test + public void testExtractUriOrigin() { + assertThat( + Utils.extractUriOrigin("https://upload.example.com/api/files"), + is("https://upload.example.com")); + assertThat(Utils.extractUriOrigin("http://localhost:8080/files"), is("http://localhost:8080")); + assertThat( + Utils.extractUriOrigin("https://upload.example.com"), is("https://upload.example.com")); + assertThat(Utils.extractUriOrigin("/api/files"), is("")); + assertThat(Utils.extractUriOrigin(null), is("")); + assertThat(Utils.extractUriOrigin(""), is("")); + } + @Test public void testGetUploadUriOnCreation() { me.desair.tus.server.upload.UploadInfo info = new me.desair.tus.server.upload.UploadInfo(); @@ -454,7 +479,7 @@ public void testGetUploadUriOnCreation() { when(storageService.getUploadUri()).thenReturn("/api/files"); HttpServletRequest request = mock(HttpServletRequest.class); - when(request.getRequestURI()).thenReturn("/files"); + when(request.getRequestURI()).thenReturn("/api/files"); // With requestURI set and storageService uploadUri set assertThat( @@ -462,17 +487,94 @@ public void testGetUploadUriOnCreation() { // With null request and storageService set assertThat(Utils.getUploadUriOnCreation(info, null, storageService), is("/api/files/test-id")); + } + + @Test(expected = NullPointerException.class) + public void testGetUploadUriOnCreationNullUploadInfoThrows() { + me.desair.tus.server.upload.UploadStorageService storageService = + mock(me.desair.tus.server.upload.UploadStorageService.class); + when(storageService.getUploadUri()).thenReturn("/api/files"); + + Utils.getUploadUriOnCreation(null, null, storageService); + } + + @Test(expected = NullPointerException.class) + public void testGetUploadUriOnCreationNullUploadIdThrows() { + me.desair.tus.server.upload.UploadStorageService storageService = + mock(me.desair.tus.server.upload.UploadStorageService.class); + when(storageService.getUploadUri()).thenReturn("/api/files"); + + Utils.getUploadUriOnCreation( + new me.desair.tus.server.upload.UploadInfo(), null, storageService); + } + + @Test(expected = NullPointerException.class) + public void testGetUploadUriOnCreationNullStorageServiceThrows() { + me.desair.tus.server.upload.UploadInfo info = new me.desair.tus.server.upload.UploadInfo(); + info.setId(new me.desair.tus.server.upload.UploadId("test-id")); + + Utils.getUploadUriOnCreation(info, null, null); + } + + @Test(expected = NullPointerException.class) + public void testGetUploadUriOnCreationNullUploadUriThrows() { + me.desair.tus.server.upload.UploadInfo info = new me.desair.tus.server.upload.UploadInfo(); + info.setId(new me.desair.tus.server.upload.UploadId("test-id")); + + me.desair.tus.server.upload.UploadStorageService storageService = + mock(me.desair.tus.server.upload.UploadStorageService.class); + when(storageService.getUploadUri()).thenReturn(null); + + Utils.getUploadUriOnCreation(info, null, storageService); + } + + @Test + public void testGetUploadUriOnCreationAbsoluteUrl() { + me.desair.tus.server.upload.UploadInfo info = new me.desair.tus.server.upload.UploadInfo(); + info.setId(new me.desair.tus.server.upload.UploadId("test-id")); + + me.desair.tus.server.upload.UploadStorageService storageService = + mock(me.desair.tus.server.upload.UploadStorageService.class); + when(storageService.getUploadUri()).thenReturn("https://upload.example.com/api/files"); - // With null request and null storageService - assertThat(Utils.getUploadUriOnCreation(info, null, null), is("/test-id")); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/api/files"); + + // Absolute URL with request + assertThat( + Utils.getUploadUriOnCreation(info, request, storageService), + is("https://upload.example.com/api/files/test-id")); - // With null uploadInfo - assertThat(Utils.getUploadUriOnCreation(null, null, null), is("/")); + // Absolute URL with regex path request + when(storageService.getUploadUri()).thenReturn("https://upload.example.com/users/[0-9]+/files"); + when(request.getRequestURI()).thenReturn("/users/42/files"); + assertThat( + Utils.getUploadUriOnCreation(info, request, storageService), + is("https://upload.example.com/users/42/files/test-id")); - // With uploadInfo having null id + // Absolute URL with regex path request not starting with / + when(request.getRequestURI()).thenReturn("users/42/files"); assertThat( - Utils.getUploadUriOnCreation(new me.desair.tus.server.upload.UploadInfo(), null, null), - is("/")); + Utils.getUploadUriOnCreation(info, request, storageService), + is("https://upload.example.com/users/42/files/test-id")); + + // Relative URL with regex path request + when(storageService.getUploadUri()).thenReturn("/users/[0-9]+/files"); + when(request.getRequestURI()).thenReturn("/users/42/files"); + assertThat( + Utils.getUploadUriOnCreation(info, request, storageService), is("/users/42/files/test-id")); + + // Absolute URL with null request + when(storageService.getUploadUri()).thenReturn("https://upload.example.com/api/files"); + assertThat( + Utils.getUploadUriOnCreation(info, null, storageService), + is("https://upload.example.com/api/files/test-id")); + + // Absolute URL with root path and null request + when(storageService.getUploadUri()).thenReturn("https://upload.example.com"); + assertThat( + Utils.getUploadUriOnCreation(info, null, storageService), + is("https://upload.example.com/test-id")); } @Test @@ -516,6 +618,17 @@ public void testIsCreationEndpoint() throws Exception { when(request.getRequestURI()).thenReturn("/files/123"); assertThat(Utils.isCreationEndpoint(request, storageService), is(false)); + + // Test with absolute URL configured + when(storageService.getUploadUri()).thenReturn("https://upload.example.com/files"); + when(request.getRequestURI()).thenReturn("/files"); + assertThat(Utils.isCreationEndpoint(request, storageService), is(true)); + + when(request.getRequestURI()).thenReturn("/files/"); + assertThat(Utils.isCreationEndpoint(request, storageService), is(true)); + + when(request.getRequestURI()).thenReturn("/files/123"); + assertThat(Utils.isCreationEndpoint(request, storageService), is(false)); } @Test