diff --git a/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/DigestAuthenticatorTestCase.java b/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/DigestAuthenticatorTestCase.java
new file mode 100644
index 0000000000..0d5a7698b6
--- /dev/null
+++ b/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/DigestAuthenticatorTestCase.java
@@ -0,0 +1,183 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.crypto;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.data.ChallengeRequest;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Reference;
+import org.restlet.security.MapVerifier;
+import org.restlet.security.Verifier;
+
+/** Unit tests for {@link DigestAuthenticator}. */
+class DigestAuthenticatorTestCase {
+
+ @Test
+ void constructor_withRealmAndServerKey_setsDefaults() {
+ DigestAuthenticator authenticator =
+ new DigestAuthenticator(new Context(), "realm", "mySecretKey");
+
+ assertFalse(authenticator.isOptional());
+ assertEquals(ChallengeScheme.HTTP_DIGEST, authenticator.getScheme());
+ assertEquals("realm", authenticator.getRealm());
+ assertEquals("mySecretKey", authenticator.getServerKey());
+ assertEquals(5 * 60 * 1000L, authenticator.getMaxServerNonceAge());
+ assertNotNull(authenticator.getVerifier());
+ }
+
+ @Test
+ void constructor_withExplicitDomainRefs_usesProvidedList() {
+ List domainRefs = Collections.singletonList(new Reference("/protected/"));
+ DigestAuthenticator authenticator =
+ new DigestAuthenticator(new Context(), true, "realm", domainRefs, "key");
+
+ assertTrue(authenticator.isOptional());
+ assertSame(domainRefs, authenticator.getDomainRefs());
+ }
+
+ @Test
+ void getDomainRefs_withNoDomainRefsProvided_lazilyDefaultsToRoot() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ List domainRefs = authenticator.getDomainRefs();
+
+ assertEquals(1, domainRefs.size());
+ assertEquals("/", domainRefs.getFirst().toString());
+ // Second call returns the same cached list instance.
+ assertSame(domainRefs, authenticator.getDomainRefs());
+ }
+
+ @Test
+ void setDomainRefs_updatesGetDomainRefs() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+ List domainRefs = Collections.singletonList(new Reference("/other/"));
+
+ authenticator.setDomainRefs(domainRefs);
+
+ assertSame(domainRefs, authenticator.getDomainRefs());
+ }
+
+ @Test
+ void generateServerNonce_returnsDecodableBase64Value() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ String nonce = authenticator.generateServerNonce();
+
+ assertNotNull(nonce);
+ String decoded = new String(Base64.getDecoder().decode(nonce));
+ assertTrue(decoded.contains(":"));
+ }
+
+ @Test
+ void createChallengeRequest_stale_populatesDomainRefsStaleAndNonce() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ ChallengeRequest challengeRequest = authenticator.createChallengeRequest(true);
+
+ assertEquals(ChallengeScheme.HTTP_DIGEST, challengeRequest.getScheme());
+ assertEquals("realm", challengeRequest.getRealm());
+ assertTrue(challengeRequest.isStale());
+ assertNotNull(challengeRequest.getServerNonce());
+ assertEquals(authenticator.getDomainRefs(), challengeRequest.getDomainRefs());
+ }
+
+ @Test
+ void createChallengeRequest_notStale_setsStaleFalse() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ ChallengeRequest challengeRequest = authenticator.createChallengeRequest(false);
+
+ assertFalse(challengeRequest.isStale());
+ }
+
+ @Test
+ void getHashedSecret_httpDigestScheme_hashesIdentifierRealmAndSecret() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ String expected = DigestUtils.toHttpDigest("scott", "tiger".toCharArray(), "realm");
+
+ assertEquals(expected, authenticator.getHashedSecret("scott", "tiger".toCharArray()));
+ }
+
+ @Test
+ void maxServerNonceAge_getterAndSetter() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ authenticator.setMaxServerNonceAge(12345L);
+
+ assertEquals(12345L, authenticator.getMaxServerNonceAge());
+ }
+
+ @Test
+ void serverKey_getterAndSetter() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ authenticator.setServerKey("anotherKey");
+
+ assertEquals("anotherKey", authenticator.getServerKey());
+ }
+
+ @Test
+ void getVerifier_returnsDigestVerifierSetByConstructor() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ assertNotNull(authenticator.getVerifier());
+ }
+
+ @Test
+ void setVerifier_withNonDigestVerifier_throwsIllegalArgumentException() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+ Verifier notADigestVerifier = new MapVerifier();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> authenticator.setVerifier(notADigestVerifier));
+ }
+
+ @Test
+ void setVerifier_withDigestVerifier_replacesIt() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+ DigestVerifier newVerifier =
+ new DigestVerifier<>(null, new MapVerifier(), null);
+
+ authenticator.setVerifier(newVerifier);
+
+ assertSame(newVerifier, authenticator.getVerifier());
+ }
+
+ @Test
+ void setWrappedAlgorithm_delegatesToVerifier() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+
+ authenticator.setWrappedAlgorithm("SHA-1");
+
+ assertEquals("SHA-1", authenticator.getVerifier().getWrappedAlgorithm());
+ }
+
+ @Test
+ void setWrappedVerifier_delegatesToVerifier() {
+ DigestAuthenticator authenticator = new DigestAuthenticator(new Context(), "realm", "key");
+ MapVerifier wrapped = new MapVerifier();
+
+ authenticator.setWrappedVerifier(wrapped);
+
+ assertSame(wrapped, authenticator.getVerifier().getWrappedVerifier());
+ }
+}
diff --git a/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/internal/CryptoUtilsTestCase.java b/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/internal/CryptoUtilsTestCase.java
new file mode 100644
index 0000000000..5f26ca49d6
--- /dev/null
+++ b/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/internal/CryptoUtilsTestCase.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.crypto.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.util.Base64;
+import org.junit.jupiter.api.Test;
+import org.restlet.ext.crypto.DigestUtils;
+
+/** Unit tests for {@link CryptoUtils}. */
+class CryptoUtilsTestCase {
+
+ private static final String ALGORITHM = "AES";
+
+ private static final byte[] SECRET_KEY_BYTES =
+ "0123456789ABCDEF".getBytes(StandardCharsets.UTF_8);
+
+ @Test
+ void encryptThenDecrypt_bytesKey_roundTripsContent() throws GeneralSecurityException {
+ byte[] encrypted = CryptoUtils.encrypt(ALGORITHM, SECRET_KEY_BYTES, "hello, world");
+
+ String decrypted = CryptoUtils.decrypt(ALGORITHM, SECRET_KEY_BYTES, encrypted);
+
+ assertEquals("hello, world", decrypted);
+ }
+
+ @Test
+ void encryptThenDecrypt_base64Key_roundTripsContent() throws GeneralSecurityException {
+ String base64Secret = Base64.getEncoder().encodeToString(SECRET_KEY_BYTES);
+
+ byte[] encrypted = CryptoUtils.encrypt(ALGORITHM, base64Secret, "another secret message");
+ String decrypted = CryptoUtils.decrypt(ALGORITHM, base64Secret, encrypted);
+
+ assertEquals("another secret message", decrypted);
+ }
+
+ @Test
+ void encrypt_unsupportedAlgorithm_throwsGeneralSecurityException() {
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> CryptoUtils.encrypt("NoSuchAlgorithm", SECRET_KEY_BYTES, "content"));
+ }
+
+ @Test
+ void decrypt_unsupportedAlgorithm_throwsGeneralSecurityException() {
+ assertThrows(
+ GeneralSecurityException.class,
+ () ->
+ CryptoUtils.decrypt(
+ "NoSuchAlgorithm",
+ SECRET_KEY_BYTES,
+ "content".getBytes(StandardCharsets.UTF_8)));
+ }
+
+ @Test
+ void makeNonce_returnsBase64EncodedTimestampAndDigest() {
+ String nonce = CryptoUtils.makeNonce("mySecretKey");
+
+ assertNotNull(nonce);
+ String decoded = new String(Base64.getDecoder().decode(nonce), StandardCharsets.UTF_8);
+ assertTrue(decoded.contains(":"));
+
+ String timestampPart = decoded.substring(0, decoded.indexOf(':'));
+ String digestPart = decoded.substring(decoded.indexOf(':') + 1);
+ assertEquals(DigestUtils.toMd5(timestampPart + ":" + "mySecretKey"), digestPart);
+ }
+}
diff --git a/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/internal/HttpDigestVerifierTestCase.java b/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/internal/HttpDigestVerifierTestCase.java
new file mode 100644
index 0000000000..47c6f60f0b
--- /dev/null
+++ b/org.restlet.ext.crypto/src/test/java/org/restlet/ext/crypto/internal/HttpDigestVerifierTestCase.java
@@ -0,0 +1,311 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.crypto.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Digest;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.engine.security.AuthenticatorUtils;
+import org.restlet.ext.crypto.DigestAuthenticator;
+import org.restlet.ext.crypto.DigestUtils;
+import org.restlet.security.MapVerifier;
+import org.restlet.security.Verifier;
+
+/** Unit tests for {@link HttpDigestVerifier}. */
+class HttpDigestVerifierTestCase {
+
+ private static final String REALM = "TestRealm";
+
+ private static final String SERVER_KEY = "mySecretServerKey";
+
+ private static final String RESOURCE_PATH = "/protected/resource";
+
+ private DigestAuthenticator authenticator;
+
+ private HttpDigestVerifier verifier;
+
+ private String serverNonce;
+
+ private String ha1;
+
+ private Request createRequest() {
+ Request request = new Request(Method.GET, "http://example.com" + RESOURCE_PATH);
+ return request;
+ }
+
+ private String computeResponse(String ha1Value, String nonce, String requestUri) {
+ String ha2 = DigestUtils.toMd5(Method.GET.toString() + ":" + requestUri);
+ return DigestUtils.toMd5(ha1Value + ":" + nonce + ":" + ha2);
+ }
+
+ private String computeResponseWithQop(
+ String ha1Value,
+ String nonce,
+ String nc,
+ String cnonce,
+ String qop,
+ String requestUri) {
+ String ha2 = DigestUtils.toMd5(Method.GET.toString() + ":" + requestUri);
+ return DigestUtils.toMd5(
+ ha1Value
+ + ":"
+ + nonce
+ + ":"
+ + AuthenticatorUtils.formatNonceCount(Integer.parseInt(nc))
+ + ":"
+ + cnonce
+ + ":"
+ + qop
+ + ":"
+ + ha2);
+ }
+
+ private ChallengeResponse createChallengeResponse(
+ String identifier,
+ char[] responseSecret,
+ String quality,
+ Reference digestRef,
+ String clientNonce,
+ String serverNonceValue,
+ int serverNonceCount) {
+ return new ChallengeResponse(
+ ChallengeScheme.HTTP_DIGEST,
+ null,
+ identifier,
+ responseSecret,
+ Digest.ALGORITHM_NONE,
+ REALM,
+ quality,
+ digestRef,
+ null,
+ null,
+ clientNonce,
+ serverNonceValue,
+ serverNonceCount,
+ 0L);
+ }
+
+ @BeforeEach
+ void setUpEach() {
+ authenticator = new DigestAuthenticator(new Context(), REALM, SERVER_KEY);
+ MapVerifier mapVerifier = new MapVerifier();
+ mapVerifier.getLocalSecrets().put("scott", "tiger".toCharArray());
+ authenticator.setWrappedVerifier(mapVerifier);
+ verifier = (HttpDigestVerifier) authenticator.getVerifier();
+
+ serverNonce = authenticator.generateServerNonce();
+ ha1 = DigestUtils.toHttpDigest("scott", "tiger".toCharArray(), REALM);
+ }
+
+ @Test
+ void verify_noChallengeResponse_returnsMissing() {
+ Request request = createRequest();
+ Response response = new Response(request);
+
+ assertEquals(Verifier.RESULT_MISSING, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_nullSecret_returnsInvalid() {
+ Request request = createRequest();
+ Response response = new Response(request);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "scott", null, null, new Reference(RESOURCE_PATH), null, serverNonce, 0);
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_INVALID, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_serverNonceForgedWithWrongSecretKey_returnsInvalid() {
+ Request request = createRequest();
+ Response response = new Response(request);
+ String forgedNonce = CryptoUtils.makeNonce("wrongServerKey");
+ String expected = computeResponse(ha1, forgedNonce, RESOURCE_PATH);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "scott",
+ expected.toCharArray(),
+ null,
+ new Reference(RESOURCE_PATH),
+ null,
+ forgedNonce,
+ 0);
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_INVALID, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_staleServerNonce_returnsStale() {
+ authenticator.setMaxServerNonceAge(-1L);
+ Request request = createRequest();
+ Response response = new Response(request);
+ String expected = computeResponse(ha1, serverNonce, RESOURCE_PATH);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "scott",
+ expected.toCharArray(),
+ null,
+ new Reference(RESOURCE_PATH),
+ null,
+ serverNonce,
+ 0);
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_STALE, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_missingDigestRef_returnsMissing() {
+ Request request = createRequest();
+ Response response = new Response(request);
+ String expected = computeResponse(ha1, serverNonce, RESOURCE_PATH);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "scott", expected.toCharArray(), null, null, null, serverNonce, 0);
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_MISSING, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_digestRefDoesNotMatchRequestUri_returnsInvalid() {
+ Request request = createRequest();
+ Response response = new Response(request);
+ String expected = computeResponse(ha1, serverNonce, RESOURCE_PATH);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "scott",
+ expected.toCharArray(),
+ null,
+ new Reference("/some/other/path"),
+ null,
+ serverNonce,
+ 0);
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_INVALID, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_unknownIdentifier_returnsInvalid() {
+ Request request = createRequest();
+ Response response = new Response(request);
+ String expected = computeResponse(ha1, serverNonce, RESOURCE_PATH);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "unknownUser",
+ expected.toCharArray(),
+ null,
+ new Reference(RESOURCE_PATH),
+ null,
+ serverNonce,
+ 0);
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_INVALID, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_wrongPassword_returnsInvalid() {
+ Request request = createRequest();
+ Response response = new Response(request);
+ String wrongHa1 = DigestUtils.toHttpDigest("scott", "wrongPassword".toCharArray(), REALM);
+ String computedWithWrongPassword = computeResponse(wrongHa1, serverNonce, RESOURCE_PATH);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "scott",
+ computedWithWrongPassword.toCharArray(),
+ null,
+ new Reference(RESOURCE_PATH),
+ null,
+ serverNonce,
+ 0);
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_INVALID, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_validCredentialsWithoutQop_returnsValid() {
+ Request request = createRequest();
+ Response response = new Response(request);
+ String expected = computeResponse(ha1, serverNonce, RESOURCE_PATH);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "scott",
+ expected.toCharArray(),
+ null,
+ new Reference(RESOURCE_PATH),
+ null,
+ serverNonce,
+ 0);
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_VALID, verifier.verify(request, response));
+ assertEquals("scott", request.getClientInfo().getUser().getIdentifier());
+ }
+
+ @Test
+ void verify_validCredentialsWithQop_returnsValid() {
+ Request request = createRequest();
+ Response response = new Response(request);
+ String nc = "1";
+ String clientNonce = "abcdef0123456789";
+ String qop = "auth";
+ String expected =
+ computeResponseWithQop(ha1, serverNonce, nc, clientNonce, qop, RESOURCE_PATH);
+ ChallengeResponse cr =
+ createChallengeResponse(
+ "scott",
+ expected.toCharArray(),
+ qop,
+ new Reference(RESOURCE_PATH),
+ clientNonce,
+ serverNonce,
+ Integer.parseInt(nc));
+ request.setChallengeResponse(cr);
+
+ assertEquals(Verifier.RESULT_VALID, verifier.verify(request, response));
+ }
+
+ @Test
+ void digest_withNonHttpDigestAlgorithm_fallsBackToSuper() {
+ verifier.setAlgorithm(Digest.ALGORITHM_MD5);
+ char[] result = verifier.getWrappedSecretDigest("scott");
+
+ assertEquals(DigestUtils.toMd5("tiger"), new String(result));
+ }
+
+ @Test
+ void getDigestAuthenticator_returnsConstructorValue() {
+ assertEquals(authenticator, verifier.getDigestAuthenticator());
+ }
+
+ @Test
+ void setDigestAuthenticator_updatesGetter() {
+ DigestAuthenticator other =
+ new DigestAuthenticator(new Context(), "OtherRealm", "otherKey");
+
+ verifier.setDigestAuthenticator(other);
+
+ assertEquals(other, verifier.getDigestAuthenticator());
+ }
+}
diff --git a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/ContextTemplateLoaderTestCase.java b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/ContextTemplateLoaderTestCase.java
index e7c56f1cfa..3165cabf5c 100644
--- a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/ContextTemplateLoaderTestCase.java
+++ b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/ContextTemplateLoaderTestCase.java
@@ -116,4 +116,15 @@ void getLastModified_withNullModificationDate_returnsMinusOne() {
StringRepresentation rep = new StringRepresentation("content");
assertEquals(-1L, loader.getLastModified(rep));
}
+
+ @Test
+ void getReader_returnsReaderWithGivenCharacterSet() throws IOException {
+ ContextTemplateLoader loader = new ContextTemplateLoader(null, "clap://test");
+ StringRepresentation rep = new StringRepresentation("template content");
+ try (java.io.Reader reader = loader.getReader(rep, "UTF-8")) {
+ char[] buffer = new char[64];
+ int read = reader.read(buffer);
+ assertEquals("template content", new String(buffer, 0, read));
+ }
+ }
}
diff --git a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java
index 520c3931e4..626f57d0a2 100644
--- a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java
+++ b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/FreeMarkerTestCase.java
@@ -11,19 +11,29 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.File;
import java.io.FileWriter;
+import java.io.IOException;
+import java.io.StringWriter;
import java.nio.file.Files;
import java.util.Map;
import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.CharacterSet;
import org.restlet.data.MediaType;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
import org.restlet.engine.io.IoUtils;
+import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.representation.Variant;
+import org.restlet.resource.ServerResource;
/**
* Unit test for the FreeMarker extension.
@@ -128,4 +138,105 @@ void testFreemarkerConverterScore() {
converter.toRepresentation(
"notATemplate", new Variant(MediaType.TEXT_PLAIN), null));
}
+
+ @Test
+ void testFreemarkerConverterWithTemplate() throws Exception {
+ FreemarkerConverter converter = new FreemarkerConverter();
+
+ Configuration fmc = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+ StringRepresentation templateSource = new StringRepresentation("Hello ${m}");
+ templateSource.setCharacterSet(CharacterSet.UTF_8);
+ Template template = TemplateRepresentation.getTemplate(fmc, templateSource);
+ assertNotNull(template);
+
+ assertEquals(1.0f, converter.score(template, new Variant(MediaType.TEXT_HTML), null));
+
+ Request request = new Request(Method.GET, "/test");
+ Response response = new Response(request);
+ ServerResource resource = new ServerResource() {};
+ resource.setRequest(request);
+ resource.setResponse(response);
+
+ Representation representation =
+ converter.toRepresentation(template, new Variant(MediaType.TEXT_HTML), resource);
+ assertNotNull(representation);
+ assertTrue(representation instanceof TemplateRepresentation);
+ assertEquals("Hello GET", representation.getText());
+ }
+
+ @Test
+ void testTemplateRepresentationConstructorsWithDefaultConfiguration() throws IOException {
+ StringRepresentation rep = new StringRepresentation("Hello ${name}");
+ rep.setCharacterSet(CharacterSet.UTF_8);
+
+ // Constructor(Representation, MediaType) - uses a default Configuration
+ TemplateRepresentation tr1 = new TemplateRepresentation(rep, MediaType.TEXT_PLAIN);
+ assertNotNull(tr1.getTemplate());
+ assertNull(tr1.getDataModel());
+
+ // Constructor(Representation, Object dataModel, MediaType) - default Configuration
+ StringRepresentation rep2 = new StringRepresentation("Hello ${name}");
+ rep2.setCharacterSet(CharacterSet.UTF_8);
+ TemplateRepresentation tr2 =
+ new TemplateRepresentation(rep2, Map.of("name", "world"), MediaType.TEXT_PLAIN);
+ assertEquals(Map.of("name", "world"), tr2.getDataModel());
+ assertEquals("Hello world", tr2.getText());
+
+ // Constructor(Representation, Configuration, Object dataModel, MediaType)
+ Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+ StringRepresentation rep3 = new StringRepresentation("Hello ${name}");
+ rep3.setCharacterSet(CharacterSet.UTF_8);
+ TemplateRepresentation tr3 =
+ new TemplateRepresentation(
+ rep3, cfg, Map.of("name", "restlet"), MediaType.TEXT_PLAIN);
+ assertEquals("Hello restlet", tr3.getText());
+ }
+
+ @Test
+ void testTemplateRepresentationWithoutExplicitCharacterSet() {
+ // No character set is set on the source representation: the UTF-8 fallback path is used.
+ StringRepresentation rep = new StringRepresentation("Hello ${name}");
+ Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+ Template template = TemplateRepresentation.getTemplate(cfg, rep);
+ assertNotNull(template);
+ }
+
+ @Test
+ void testTemplateRepresentationEqualsAndHashCode() {
+ TemplateRepresentation tr =
+ new TemplateRepresentation((Template) null, MediaType.TEXT_PLAIN);
+ assertEquals(tr, tr);
+ assertEquals(tr.hashCode(), tr.hashCode());
+ }
+
+ @Test
+ void testTemplateRepresentationSetDataModelFromRequestResponse() throws IOException {
+ Request request = new Request(Method.GET, "/test");
+ Response response = new Response(request);
+ response.setStatus(Status.SUCCESS_OK);
+
+ Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+ StringRepresentation rep = new StringRepresentation("Method=${m}");
+ rep.setCharacterSet(CharacterSet.UTF_8);
+ TemplateRepresentation tr = new TemplateRepresentation(rep, cfg, MediaType.TEXT_PLAIN);
+
+ Object dataModel = tr.setDataModel(request, response);
+ assertNotNull(dataModel);
+ assertEquals(dataModel, tr.getDataModel());
+ assertEquals("Method=GET", tr.getText());
+ }
+
+ @Test
+ void testTemplateRepresentationWriteWithTemplateException() {
+ // A missing variable used with the "?number" built-in raises a TemplateException while
+ // processing, which write() should translate into an IOException.
+ Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+ StringRepresentation rep = new StringRepresentation("${missingVar}");
+ rep.setCharacterSet(CharacterSet.UTF_8);
+ TemplateRepresentation tr =
+ new TemplateRepresentation(rep, cfg, Map.of(), MediaType.TEXT_PLAIN);
+
+ StringWriter writer = new StringWriter();
+ assertThrows(IOException.class, () -> tr.write(writer));
+ }
}
diff --git a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/TemplateFilterTestCase.java b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/TemplateFilterTestCase.java
index dec470bd1f..919b8a769e 100644
--- a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/TemplateFilterTestCase.java
+++ b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/TemplateFilterTestCase.java
@@ -9,7 +9,11 @@
package org.restlet.ext.freemarker;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import freemarker.template.Configuration;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.restlet.Application;
@@ -23,6 +27,7 @@
import org.restlet.data.Protocol;
import org.restlet.engine.Engine;
import org.restlet.resource.Directory;
+import org.restlet.util.Resolver;
/**
* Test case for template filters.
@@ -46,6 +51,85 @@ void representationShouldNotBeUsedAsTemplate() throws Exception {
assertEquals("Method=${m}/Path=${rp}", response.getEntity().getText());
}
+ @Test
+ void defaultConstructor_setsDefaultConfiguration() {
+ TemplateFilter filter = new TemplateFilter();
+ assertNotNull(filter.getConfiguration());
+ }
+
+ @Test
+ void contextConstructor_setsDefaultConfiguration() {
+ TemplateFilter filter = new TemplateFilter(new Context());
+ assertNotNull(filter.getConfiguration());
+ }
+
+ @Test
+ void contextAndNextConstructor_setsDefaultConfiguration() {
+ Directory directory =
+ new Directory(
+ new Context(),
+ LocalReference.createClapReference(
+ TemplateFilterTestCase.class.getPackage()));
+ TemplateFilter filter = new TemplateFilter(new Context(), directory);
+ assertNotNull(filter.getConfiguration());
+ }
+
+ @Test
+ void constructorWithObjectDataModel_setsDataModel() {
+ Object dataModel = "myDataModel";
+ TemplateFilter filter = new TemplateFilter(new Context(), null, dataModel);
+ assertSame(dataModel, filter.getDataModel());
+ }
+
+ @Test
+ void constructorWithResolverDataModel_setsDataModel() {
+ Resolver resolver =
+ new Resolver<>() {
+ @Override
+ public Object resolve(String name) {
+ return "resolved-" + name;
+ }
+ };
+ TemplateFilter filter = new TemplateFilter(new Context(), null, resolver);
+ assertSame(resolver, filter.getDataModel());
+ }
+
+ @Test
+ void setConfiguration_replacesConfiguration() {
+ TemplateFilter filter = new TemplateFilter();
+ Configuration original = filter.getConfiguration();
+ Configuration replacement =
+ new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
+ filter.setConfiguration(replacement);
+ assertNotSame(original, filter.getConfiguration());
+ assertSame(replacement, filter.getConfiguration());
+ }
+
+ @Test
+ void setDataModel_replacesDataModel() {
+ TemplateFilter filter = new TemplateFilter();
+ filter.setDataModel("myModel");
+ assertEquals("myModel", filter.getDataModel());
+ }
+
+ @Test
+ void createDataModel_withNoConfiguredDataModel_createsResolverHashModel() {
+ TemplateFilter filter = new TemplateFilter();
+ Request request = new Request(Method.GET, "/test");
+ Response response = new Response(request);
+ Object dataModel = filter.createDataModel(request, response);
+ assertNotNull(dataModel);
+ }
+
+ @Test
+ void createDataModel_withConfiguredDataModel_returnsConfiguredDataModel() {
+ TemplateFilter filter = new TemplateFilter();
+ filter.setDataModel("myModel");
+ Request request = new Request(Method.GET, "/test");
+ Response response = new Response(request);
+ assertEquals("myModel", filter.createDataModel(request, response));
+ }
+
@BeforeAll
static void setUpTestCases() {
Engine.clearThreadLocalVariables();
diff --git a/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/internal/ResolverHashModelTestCase.java b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/internal/ResolverHashModelTestCase.java
new file mode 100644
index 0000000000..8c728b15c4
--- /dev/null
+++ b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/internal/ResolverHashModelTestCase.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.freemarker.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateScalarModel;
+import org.junit.jupiter.api.Test;
+import org.restlet.util.Resolver;
+
+class ResolverHashModelTestCase {
+
+ @Test
+ void get_withUnresolvedKey_returnsNull() throws TemplateModelException {
+ ResolverHashModel model =
+ new ResolverHashModel(
+ new Resolver<>() {
+ @Override
+ public Object resolve(String name) {
+ return null;
+ }
+ });
+ assertNull(model.get("anyKey"));
+ }
+
+ @Test
+ void get_withScalarValue_returnsScalarModel() throws TemplateModelException {
+ ResolverHashModel model =
+ new ResolverHashModel(
+ new Resolver<>() {
+ @Override
+ public Object resolve(String name) {
+ return "value-" + name;
+ }
+ });
+ TemplateModel result = model.get("myKey");
+ assertEquals("value-myKey", ((TemplateScalarModel) result).getAsString());
+ }
+
+ @Test
+ void get_withTemplateModelValue_returnsSameInstance() throws TemplateModelException {
+ TemplateScalarModel scalar = () -> "already-a-template-model";
+ ResolverHashModel model =
+ new ResolverHashModel(
+ new Resolver<>() {
+ @Override
+ public Object resolve(String name) {
+ return scalar;
+ }
+ });
+ assertSame(scalar, model.get("myKey"));
+ }
+
+ @Test
+ void isEmpty_alwaysReturnsFalse() throws TemplateModelException {
+ ResolverHashModel model =
+ new ResolverHashModel(
+ new Resolver<>() {
+ @Override
+ public Object resolve(String name) {
+ return null;
+ }
+ });
+ assertFalse(model.isEmpty());
+ }
+}
diff --git a/org.restlet.ext.gson/src/test/java/org/restlet/ext/gson/GsonTestCase.java b/org.restlet.ext.gson/src/test/java/org/restlet/ext/gson/GsonTestCase.java
index 36b7535afd..464d65b88d 100644
--- a/org.restlet.ext.gson/src/test/java/org/restlet/ext/gson/GsonTestCase.java
+++ b/org.restlet.ext.gson/src/test/java/org/restlet/ext/gson/GsonTestCase.java
@@ -10,17 +10,25 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+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 com.google.gson.GsonBuilder;
import com.google.gson.annotations.Since;
import java.io.IOException;
+import java.text.DateFormat;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.restlet.data.MediaType;
+import org.restlet.data.Preference;
+import org.restlet.engine.resource.VariantInfo;
import org.restlet.representation.EmptyRepresentation;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
@@ -219,4 +227,185 @@ final void testToRepresentationObjectVariantResource() {
Representation rep1 = gsonConverter.toRepresentation(user, v1, null);
assertNull(rep1);
}
+
+ @Test
+ final void testToRepresentation_withGsonRepresentationSource_returnsSameInstance() {
+ Representation gsonRep = new GsonRepresentation<>(user);
+ Variant v = new Variant(MediaType.APPLICATION_JSON);
+
+ Representation result = gsonConverter.toRepresentation(gsonRep, v, null);
+
+ assertSame(gsonRep, result);
+ }
+
+ @Test
+ final void testToRepresentation_withNullTargetMediaType_setsJsonMediaType() {
+ Variant v = new Variant();
+ assertNull(v.getMediaType());
+
+ Representation result = gsonConverter.toRepresentation(user, v, null);
+
+ assertEquals(MediaType.APPLICATION_JSON, v.getMediaType());
+ assertNotNull(result);
+ assertEquals(MediaType.APPLICATION_JSON, result.getMediaType());
+ }
+
+ @Test
+ final void testGetObjectClasses_withJsonVariant_returnsObjectAndGsonRepresentationClasses() {
+ Variant jsonVariant = new Variant(MediaType.APPLICATION_JSON);
+
+ List> classes = gsonConverter.getObjectClasses(jsonVariant);
+
+ assertNotNull(classes);
+ assertTrue(classes.contains(Object.class));
+ assertTrue(classes.contains(GsonRepresentation.class));
+ }
+
+ @Test
+ final void testGetObjectClasses_withIncompatibleVariant_returnsNull() {
+ Variant xmlVariant = new Variant(MediaType.APPLICATION_XML);
+
+ List> classes = gsonConverter.getObjectClasses(xmlVariant);
+
+ assertNull(classes);
+ }
+
+ @Test
+ final void testGetVariants_withNonNullSource_returnsJsonVariant() {
+ List variants = gsonConverter.getVariants(Object.class);
+
+ assertNotNull(variants);
+ assertEquals(1, variants.size());
+ assertTrue(variants.get(0).isCompatible(new Variant(MediaType.APPLICATION_JSON)));
+ }
+
+ @Test
+ final void testGetVariants_withNullSource_returnsEmptyList() {
+ List variants = gsonConverter.getVariants(null);
+
+ assertNotNull(variants);
+ assertTrue(variants.isEmpty());
+ }
+
+ @Test
+ final void testScoreObjectVariantResource_withNullTarget_returnsHalf() {
+ float score = gsonConverter.score(user, null, null);
+
+ assertEquals(0.5F, score);
+ }
+
+ @Test
+ final void testScoreObjectVariantResource_withIncompatibleTarget_returnsHalf() {
+ Variant v = new Variant(MediaType.APPLICATION_XML);
+
+ float score = gsonConverter.score(user, v, null);
+
+ assertEquals(0.5F, score);
+ }
+
+ @Test
+ final void testScoreRepresentationClassOfTResource_withGsonRepresentationTarget_returnsOne() {
+ Representation source = new StringRepresentation("plain text", MediaType.TEXT_PLAIN);
+
+ float score = gsonConverter.score(source, GsonRepresentation.class, null);
+
+ assertEquals(1.0F, score);
+ }
+
+ @Test
+ final void testScoreRepresentationClassOfTResource_withNoMatch_returnsMinusOne() {
+ Representation source = new StringRepresentation("plain text", MediaType.TEXT_PLAIN);
+
+ float score = gsonConverter.score(source, User.class, null);
+
+ assertEquals(-1.0F, score);
+ }
+
+ @Test
+ final void testToObject_withPlainJsonRepresentation_createsGsonRepresentationInternally()
+ throws IOException {
+ final String userAsJsonString =
+ "{\"loginId\":\"hello\",\"password\":\"secret\",\"rate\":1,\"active\":true,\"createAt\":\"2012-05-20T15:41:01.489+08:00\",\"lastLogin\":\"2012-05-20T15:41:01.489+08:00\"}";
+ Representation source =
+ new StringRepresentation(userAsJsonString, MediaType.APPLICATION_JSON);
+
+ User result = gsonConverter.toObject(source, User.class, null);
+
+ assertNotNull(result);
+ assertEquals("hello", result.getLoginId());
+ }
+
+ @Test
+ final void testToObject_withTargetGsonRepresentationClass_returnsGsonRepresentation()
+ throws IOException {
+ final String userAsJsonString =
+ "{\"loginId\":\"hello\",\"password\":\"secret\",\"rate\":1,\"active\":true,\"createAt\":\"2012-05-20T15:41:01.489+08:00\",\"lastLogin\":\"2012-05-20T15:41:01.489+08:00\"}";
+ Representation source =
+ new StringRepresentation(userAsJsonString, MediaType.APPLICATION_JSON);
+
+ Object result = gsonConverter.toObject(source, GsonRepresentation.class, null);
+
+ assertInstanceOf(GsonRepresentation.class, result);
+ }
+
+ @Test
+ final void testToObject_withIncompatibleRepresentation_returnsNull() throws IOException {
+ Representation source = new StringRepresentation("plain text", MediaType.TEXT_PLAIN);
+
+ User result = gsonConverter.toObject(source, User.class, null);
+
+ assertNull(result);
+ }
+
+ @Test
+ final void testUpdatePreferences_addsJsonPreference() {
+ List> preferences = new ArrayList<>();
+
+ gsonConverter.updatePreferences(preferences, User.class);
+
+ assertFalse(preferences.isEmpty());
+ assertEquals(MediaType.APPLICATION_JSON, preferences.get(0).getMetadata());
+ }
+
+ @Test
+ final void testSetObject_updatesWrappedObject() throws IOException {
+ GsonRepresentation rep = new GsonRepresentation<>(user);
+ User other = new User("other", "pwd", 2, false, new Date(), new Date());
+
+ rep.setObject(other);
+
+ assertEquals(other, rep.getObject());
+ }
+
+ @Test
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ final void testSetObjectClass_updatesObjectClass() {
+ GsonRepresentation rep = new GsonRepresentation<>(user);
+
+ rep.setObjectClass(String.class);
+
+ assertEquals(String.class, rep.getObjectClass());
+ }
+
+ @Test
+ final void testSetBuilder_updatesBuilder() {
+ GsonRepresentation rep = new GsonRepresentation<>(user);
+ GsonBuilder customBuilder = new GsonBuilder();
+ customBuilder.setDateFormat(DateFormat.SHORT, DateFormat.SHORT);
+
+ rep.setBuilder(customBuilder);
+
+ assertSame(customBuilder, rep.getBuilder());
+ }
+
+ @Test
+ final void testWrite_withSourceRepresentation_delegatesToIt() throws IOException {
+ final String userAsJsonString =
+ "{\"loginId\":\"hello\",\"password\":\"secret\",\"rate\":1,\"active\":true,\"createAt\":\"2012-05-20T15:41:01.489+08:00\",\"lastLogin\":\"2012-05-20T15:41:01.489+08:00\"}";
+ Representation source =
+ new StringRepresentation(userAsJsonString, MediaType.APPLICATION_JSON);
+ GsonRepresentation gsonRep = new GsonRepresentation<>(source, User.class);
+
+ assertEquals(userAsJsonString, gsonRep.getText());
+ }
}
diff --git a/org.restlet.ext.jaas/src/test/java/org/restlet/ext/jaas/JaasVerifierTestCase.java b/org.restlet.ext.jaas/src/test/java/org/restlet/ext/jaas/JaasVerifierTestCase.java
index 1590d5639f..d68478e9a6 100644
--- a/org.restlet.ext.jaas/src/test/java/org/restlet/ext/jaas/JaasVerifierTestCase.java
+++ b/org.restlet.ext.jaas/src/test/java/org/restlet/ext/jaas/JaasVerifierTestCase.java
@@ -12,17 +12,97 @@
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.assertTrue;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Map;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.spi.LoginModule;
import org.junit.jupiter.api.Test;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.Method;
import org.restlet.data.Reference;
+import org.restlet.security.Role;
+import org.restlet.security.User;
import org.restlet.security.Verifier;
/** Unit tests for {@link JaasVerifier}. */
class JaasVerifierTestCase {
+ /** Simple principal implementation used to exercise the JAAS login flow. */
+ public static class TestPrincipal implements Principal {
+ private final String name;
+
+ public TestPrincipal(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+ }
+
+ /** Login module that always succeeds and adds a new principal to the subject. */
+ public static class AddingPrincipalLoginModule implements LoginModule {
+ private Subject subject;
+
+ @Override
+ public void initialize(
+ Subject subject,
+ CallbackHandler callbackHandler,
+ Map sharedState,
+ Map options) {
+ this.subject = subject;
+ }
+
+ @Override
+ public boolean login() {
+ return true;
+ }
+
+ @Override
+ public boolean commit() {
+ subject.getPrincipals().add(new TestPrincipal("newUser"));
+ return true;
+ }
+
+ @Override
+ public boolean abort() {
+ return false;
+ }
+
+ @Override
+ public boolean logout() {
+ return true;
+ }
+ }
+
+ /**
+ * Creates a JAAS {@link Configuration} backed by a single login module entry for the given
+ * login module class.
+ */
+ private static Configuration createConfiguration(
+ Class extends LoginModule> loginModuleClass) {
+ return new Configuration() {
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+ return new AppConfigurationEntry[] {
+ new AppConfigurationEntry(
+ loginModuleClass.getName(),
+ LoginModuleControlFlag.REQUIRED,
+ Collections.emptyMap())
+ };
+ }
+ };
+ }
+
@Test
void testConstructor() {
JaasVerifier verifier = new JaasVerifier("myApp");
@@ -67,4 +147,86 @@ void testVerifyReturnsInvalidWhenLoginFails() {
assertEquals(Verifier.RESULT_INVALID, result);
}
+
+ @Test
+ void testSetConfiguration() {
+ JaasVerifier verifier = new JaasVerifier("myApp");
+ Configuration configuration = createConfiguration(AddingPrincipalLoginModule.class);
+
+ verifier.setConfiguration(configuration);
+
+ assertEquals(configuration, verifier.getConfiguration());
+ }
+
+ @Test
+ void testVerifySucceedsAndMergesPrincipalsWithoutDuplicates() {
+ JaasVerifier verifier = new JaasVerifier("testRealm");
+ verifier.setConfiguration(createConfiguration(AddingPrincipalLoginModule.class));
+
+ Request request = new Request(Method.GET, new Reference("http://localhost/test"));
+ Response response = new Response(request);
+
+ User user = new User("alice");
+ Role role = new Role(null, "admin");
+ Principal extraPrincipal = new TestPrincipal("extra");
+
+ request.getClientInfo().setUser(user);
+ request.getClientInfo().getRoles().add(role);
+ request.getClientInfo().getPrincipals().add(extraPrincipal);
+
+ int result = verifier.verify(request, response);
+
+ assertEquals(Verifier.RESULT_VALID, result);
+ // The user and existing principals must not be duplicated in the principals list.
+ assertEquals(
+ 1, Collections.frequency(request.getClientInfo().getPrincipals(), extraPrincipal));
+ assertTrue(
+ request.getClientInfo().getPrincipals().stream()
+ .noneMatch(principal -> principal.equals(user)));
+ // The principal added by the login module must be merged into the principals list.
+ assertTrue(
+ request.getClientInfo().getPrincipals().stream()
+ .anyMatch(
+ principal ->
+ principal instanceof TestPrincipal
+ && "newUser".equals(principal.getName())));
+ // No new user was created since one was already set.
+ assertEquals(user, request.getClientInfo().getUser());
+ }
+
+ @Test
+ void testVerifyCreatesUserWhenPrincipalMatchesConfiguredClassName() {
+ JaasVerifier verifier = new JaasVerifier("testRealm");
+ verifier.setConfiguration(createConfiguration(AddingPrincipalLoginModule.class));
+ verifier.setUserPrincipalClassName(TestPrincipal.class.getName());
+
+ Request request = new Request(Method.GET, new Reference("http://localhost/test"));
+ Response response = new Response(request);
+
+ assertNull(request.getClientInfo().getUser());
+
+ int result = verifier.verify(request, response);
+
+ assertEquals(Verifier.RESULT_VALID, result);
+ assertNotNull(request.getClientInfo().getUser());
+ assertEquals("newUser", request.getClientInfo().getUser().getIdentifier());
+ }
+
+ @Test
+ void testVerifyDoesNotOverwriteExistingUserEvenWhenPrincipalClassMatches() {
+ JaasVerifier verifier = new JaasVerifier("testRealm");
+ verifier.setConfiguration(createConfiguration(AddingPrincipalLoginModule.class));
+ verifier.setUserPrincipalClassName(TestPrincipal.class.getName());
+
+ Request request = new Request(Method.GET, new Reference("http://localhost/test"));
+ Response response = new Response(request);
+
+ User existingUser = new User("bob");
+ request.getClientInfo().setUser(existingUser);
+
+ int result = verifier.verify(request, response);
+
+ assertEquals(Verifier.RESULT_VALID, result);
+ assertEquals(existingUser, request.getClientInfo().getUser());
+ }
}
diff --git a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringComponentTestCase.java b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringComponentTestCase.java
new file mode 100644
index 0000000000..267226886e
--- /dev/null
+++ b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringComponentTestCase.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.spring;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Client;
+import org.restlet.Restlet;
+import org.restlet.Server;
+import org.restlet.data.Protocol;
+
+/** Unit tests for {@link SpringComponent}. */
+class SpringComponentTestCase {
+
+ private static class TestRestlet extends Restlet {}
+
+ private SpringComponent component;
+
+ @BeforeEach
+ void setUpEach() {
+ this.component = new SpringComponent();
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ this.component = null;
+ }
+
+ @Test
+ void testSetClientAddsClientInstance() {
+ Client client = new Client(Protocol.FILE);
+ this.component.setClient(client);
+ assertEquals(1, this.component.getClients().size(), "Wrong number of clients");
+ assertTrue(this.component.getClients().contains(client), "Client instance not added");
+ }
+
+ @Test
+ void testSetClientAddsProtocolInstance() {
+ this.component.setClient(Protocol.HTTP);
+ assertEquals(1, this.component.getClients().size(), "Wrong number of clients");
+ assertEquals(Protocol.HTTP, this.component.getClients().get(0).getProtocols().get(0));
+ }
+
+ @Test
+ void testSetClientAddsProtocolNamedByString() {
+ this.component.setClient("FILE");
+ assertEquals(1, this.component.getClients().size(), "Wrong number of clients");
+ assertEquals(Protocol.FILE, this.component.getClients().get(0).getProtocols().get(0));
+ }
+
+ @Test
+ void testSetClientsListIgnoresNullAndUnknownEntries() {
+ List clients = Arrays.asList("FILE", null, 42);
+ this.component.setClientsList(clients);
+ assertEquals(1, this.component.getClients().size(), "Only the valid entry should be added");
+ }
+
+ @Test
+ void testSetClientsListWithMultipleEntries() {
+ Client client = new Client(Protocol.FILE);
+ List clients = Arrays.asList("HTTP", Protocol.HTTPS, client);
+ this.component.setClientsList(clients);
+ assertEquals(3, this.component.getClients().size(), "Wrong number of clients");
+ }
+
+ @Test
+ void testSetDefaultTargetAttachesToDefaultHost() {
+ TestRestlet target = new TestRestlet();
+ this.component.setDefaultTarget(target);
+ assertEquals(
+ target,
+ this.component.getDefaultHost().getRoutes().get(0).getNext(),
+ "Default target not attached to the default host");
+ }
+
+ @Test
+ void testSetServerAddsServerInstance() {
+ Server server = new Server(Protocol.HTTP, 0);
+ this.component.setServer(server);
+ assertEquals(1, this.component.getServers().size(), "Wrong number of servers");
+ assertTrue(this.component.getServers().contains(server), "Server instance not added");
+ }
+
+ @Test
+ void testSetServerAddsProtocolNamedByString() {
+ this.component.setServer("HTTP");
+ assertEquals(1, this.component.getServers().size(), "Wrong number of servers");
+ assertEquals(Protocol.HTTP, this.component.getServers().get(0).getProtocols().get(0));
+ }
+
+ @Test
+ void testSetServersListIgnoresNullAndUnknownEntries() {
+ List servers = Arrays.asList("HTTP", null, 3.14);
+ this.component.setServersList(servers);
+ assertEquals(1, this.component.getServers().size(), "Only the valid entry should be added");
+ }
+
+ @Test
+ void testSetServersListWithEmptyList() {
+ this.component.setServersList(Collections.emptyList());
+ assertEquals(0, this.component.getServers().size(), "No servers should be added");
+ }
+}
diff --git a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringFinderTestCase.java b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringFinderTestCase.java
new file mode 100644
index 0000000000..4bb0ef607f
--- /dev/null
+++ b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringFinderTestCase.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.spring;
+
+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 org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.resource.ServerResource;
+import org.restlet.routing.Router;
+
+/** Unit tests for {@link SpringFinder}. */
+class SpringFinderTestCase {
+
+ public static class SamplePrivateConstructorResource extends ServerResource {
+ private SamplePrivateConstructorResource() {}
+ }
+
+ public static class SampleResource extends ServerResource {}
+
+ @Test
+ void testConstructorWithContext() {
+ Context context = new Context();
+ SpringFinder finder = new SpringFinder(context);
+ assertNotNull(finder.getContext(), "Finder context should not be null");
+ }
+
+ @Test
+ void testConstructorWithContextAndTargetClass() {
+ Context context = new Context();
+ SpringFinder finder = new SpringFinder(context, SampleResource.class);
+ assertInstanceOf(
+ SampleResource.class,
+ finder.create(),
+ "Finder should create instances of the target class");
+ }
+
+ @Test
+ void testConstructorWithNoArguments() {
+ SpringFinder finder = new SpringFinder();
+ assertNull(finder.getTargetClass(), "Target class should be null by default");
+ }
+
+ @Test
+ void testConstructorWithParentRestlet() {
+ Context context = new Context();
+ Restlet parent = new Router(context);
+ SpringFinder finder = new SpringFinder(parent);
+ assertNotNull(finder.getContext(), "Finder context should come from the parent Restlet");
+ }
+
+ @Test
+ void testCreateDelegatesToNoArgCreate() {
+ SpringFinder finder = new SpringFinder();
+ finder.setTargetClass(SampleResource.class);
+
+ Request request = new Request();
+ Response response = new Response(request);
+
+ ServerResource resource = finder.create(request, response);
+ assertInstanceOf(
+ SampleResource.class, resource, "Should create an instance of SampleResource");
+ }
+
+ @Test
+ void testCreateLongFormDelegatesToNoArgCreate() {
+ SpringFinder finder = new SpringFinder();
+ finder.setTargetClass(SampleResource.class);
+
+ Request request = new Request();
+ Response response = new Response(request);
+
+ ServerResource resource = finder.create(SampleResource.class, request, response);
+ assertInstanceOf(
+ SampleResource.class, resource, "Should create an instance of SampleResource");
+ }
+
+ @Test
+ void testCreateReturnsInstanceOfTargetClass() {
+ SpringFinder finder = new SpringFinder();
+ finder.setTargetClass(SampleResource.class);
+
+ ServerResource resource = finder.create();
+ assertInstanceOf(
+ SampleResource.class, resource, "Should create an instance of SampleResource");
+ }
+
+ @Test
+ void testCreateReturnsNullWhenInstantiationFails() {
+ SpringFinder finder = new SpringFinder();
+ finder.setTargetClass(SamplePrivateConstructorResource.class);
+
+ ServerResource resource = finder.create();
+ assertNull(resource, "Should return null when the target class cannot be instantiated");
+ }
+
+ @Test
+ void testCreateReturnsNullWhenNoTargetClassSet() {
+ SpringFinder finder = new SpringFinder();
+ assertNull(finder.create(), "Should return null when no target class is set");
+ }
+}
diff --git a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringHostTestCase.java b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringHostTestCase.java
new file mode 100644
index 0000000000..083ada46a9
--- /dev/null
+++ b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringHostTestCase.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.spring;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Component;
+import org.restlet.Context;
+import org.restlet.Restlet;
+import org.restlet.ext.spring.resources.UserResource;
+
+/** Unit tests for {@link SpringHost}. */
+class SpringHostTestCase {
+
+ private static class TestRestlet extends Restlet {}
+
+ private SpringHost host;
+
+ @BeforeEach
+ void setUpEach() {
+ this.host = new SpringHost(new Context());
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ this.host = null;
+ }
+
+ @Test
+ void testConstructorWithComponent() {
+ Component component = new Component();
+ SpringHost componentHost = new SpringHost(component);
+ assertNotNull(componentHost.getContext(), "Host context should come from the component");
+ }
+
+ @Test
+ void testSetAttachmentSetsContextOnContextlessRestlet() {
+ TestRestlet target = new TestRestlet();
+ this.host.setAttachment("/target", target);
+
+ assertNotNull(target.getContext(), "Target should have received a context");
+ assertEquals(1, this.host.getRoutes().size(), "One route should have been attached");
+ }
+
+ @Test
+ void testSetAttachmentWithClass() {
+ this.host.setAttachment("/user", UserResource.class);
+ assertEquals(1, this.host.getRoutes().size(), "One route should have been attached");
+ }
+
+ @Test
+ void testSetAttachmentsAttachesAllRoutes() {
+ Map routes = new HashMap<>();
+ routes.put("/one", new TestRestlet());
+ routes.put("/two", new TestRestlet());
+ this.host.setAttachments(routes);
+ assertEquals(2, this.host.getRoutes().size(), "Both routes should have been attached");
+ }
+
+ @Test
+ void testSetDefaultAttachment() {
+ TestRestlet target = new TestRestlet();
+ this.host.setDefaultAttachment(target);
+ assertEquals(1, this.host.getRoutes().size(), "Default route should have been attached");
+ assertEquals(
+ target,
+ this.host.getRoutes().get(0).getNext(),
+ "Default route should point to target");
+ }
+
+ @Test
+ void testSetAttachmentInstanceMethodSingleRoute() {
+ this.host.setAttachments(Collections.singletonMap("/one", new TestRestlet()));
+ assertEquals(1, this.host.getRoutes().size(), "One route should have been attached");
+ }
+}
diff --git a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringRouterTestCase.java b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringRouterTestCase.java
new file mode 100644
index 0000000000..b28da36336
--- /dev/null
+++ b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringRouterTestCase.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.spring;
+
+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 java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Restlet;
+import org.restlet.ext.spring.resources.UserResource;
+import org.restlet.routing.Route;
+import org.restlet.routing.Router;
+import org.restlet.routing.TemplateRoute;
+
+/** Unit tests for {@link SpringRouter}. */
+class SpringRouterTestCase {
+
+ private static class TestRestlet extends Restlet {}
+
+ private SpringRouter router;
+
+ @BeforeEach
+ void setUpEach() {
+ this.router = new SpringRouter();
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ this.router = null;
+ }
+
+ @Test
+ void testConstructorWithContext() {
+ SpringRouter contextRouter = new SpringRouter(new Context());
+ assertNotNull(contextRouter.getContext(), "Router context should not be null");
+ }
+
+ @Test
+ void testConstructorWithNoArguments() {
+ assertEquals(0, this.router.getRoutes().size(), "New router should have no routes");
+ }
+
+ @Test
+ void testConstructorWithParentRestlet() {
+ Router parent = new Router(new Context());
+ SpringRouter childRouter = new SpringRouter(parent);
+ assertNotNull(
+ childRouter.getContext(), "Router context should come from the parent Restlet");
+ }
+
+ @Test
+ void testSetAttachmentIgnoresUnknownObjectType() {
+ SpringRouter.setAttachment(this.router, "/unknown", 42);
+ assertEquals(0, this.router.getRoutes().size(), "No route should have been attached");
+ }
+
+ @Test
+ void testSetAttachmentIgnoresUnresolvableClassName() {
+ SpringRouter.setAttachment(this.router, "/missing", "com.example.DoesNotExist");
+ assertEquals(0, this.router.getRoutes().size(), "No route should have been attached");
+ }
+
+ @Test
+ void testSetAttachmentIgnoresStringForNonResourceClass() {
+ SpringRouter.setAttachment(this.router, "/notaresource", "java.lang.String");
+ assertEquals(0, this.router.getRoutes().size(), "No route should have been attached");
+ }
+
+ @Test
+ void testSetAttachmentWithClass() {
+ SpringRouter.setAttachment(this.router, "/user", UserResource.class);
+ assertEquals(1, this.router.getRoutes().size(), "One route should have been attached");
+ }
+
+ @Test
+ void testSetAttachmentWithClassName() {
+ SpringRouter.setAttachment(
+ this.router, "/user", "org.restlet.ext.spring.resources.UserResource");
+ assertEquals(1, this.router.getRoutes().size(), "One route should have been attached");
+ }
+
+ @Test
+ void testSetAttachmentWithRestlet() {
+ TestRestlet target = new TestRestlet();
+ SpringRouter.setAttachment(this.router, "/target", target);
+
+ Route route = this.router.getRoutes().get(0);
+ assertInstanceOf(TemplateRoute.class, route, "Attached route should be a TemplateRoute");
+ assertEquals(target, route.getNext(), "Attached route should point to the given Restlet");
+ }
+
+ @Test
+ void testSetAttachmentsAttachesAllRoutes() {
+ Map routes = new HashMap<>();
+ routes.put("/one", new TestRestlet());
+ routes.put("/two", new TestRestlet());
+ SpringRouter.setAttachments(this.router, routes);
+ assertEquals(2, this.router.getRoutes().size(), "Both routes should have been attached");
+ }
+
+ @Test
+ void testSetAttachmentsInstanceMethod() {
+ this.router.setAttachments(Collections.singletonMap("/one", new TestRestlet()));
+ assertEquals(1, this.router.getRoutes().size(), "One route should have been attached");
+ }
+
+ @Test
+ void testSetDefaultAttachment() {
+ TestRestlet target = new TestRestlet();
+ this.router.setDefaultAttachment(target);
+ assertEquals(1, this.router.getRoutes().size(), "Default route should have been attached");
+ assertEquals(
+ target,
+ this.router.getRoutes().get(0).getNext(),
+ "Default route should point to target");
+ }
+}
diff --git a/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringServerTestCase.java b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringServerTestCase.java
new file mode 100644
index 0000000000..21333d076c
--- /dev/null
+++ b/org.restlet.ext.spring/src/test/java/org/restlet/ext/spring/SpringServerTestCase.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.spring;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Properties;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Protocol;
+
+/** Unit tests for {@link SpringServer}. */
+class SpringServerTestCase {
+
+ @Test
+ void testConstructorWithProtocol() {
+ SpringServer server = new SpringServer("HTTP");
+ assertTrue(
+ server.getProtocols().contains(Protocol.HTTP),
+ "Server should support HTTP protocol");
+ }
+
+ @Test
+ void testConstructorWithProtocolAndAddressAndPort() {
+ SpringServer server = new SpringServer("HTTP", "127.0.0.1", 8112);
+ assertTrue(
+ server.getProtocols().contains(Protocol.HTTP),
+ "Server should support HTTP protocol");
+ assertEquals(8112, server.getPort(), "Wrong server port");
+ assertEquals("127.0.0.1", server.getAddress(), "Wrong server address");
+ }
+
+ @Test
+ void testConstructorWithProtocolAndPort() {
+ SpringServer server = new SpringServer("HTTP", 8111);
+ assertTrue(
+ server.getProtocols().contains(Protocol.HTTP),
+ "Server should support HTTP protocol");
+ assertEquals(8111, server.getPort(), "Wrong server port");
+ }
+
+ @Test
+ void testSetParametersAddsAllProperties() {
+ SpringServer server = new SpringServer("HTTP");
+ Properties parameters = new Properties();
+ parameters.setProperty("key1", "value1");
+ parameters.setProperty("key2", "value2");
+
+ server.setParameters(parameters);
+
+ assertEquals("value1", server.getContext().getParameters().getFirstValue("key1"));
+ assertEquals("value2", server.getContext().getParameters().getFirstValue("key2"));
+ }
+
+ @Test
+ void testSetParametersWithEmptyProperties() {
+ SpringServer server = new SpringServer("HTTP");
+ server.setParameters(new Properties());
+ assertEquals(
+ 0,
+ server.getContext().getParameters().subList("key1").size(),
+ "No parameters should have been added");
+ }
+}
diff --git a/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/RepresentationResourceLoaderTestCase.java b/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/RepresentationResourceLoaderTestCase.java
new file mode 100644
index 0000000000..b5bafac839
--- /dev/null
+++ b/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/RepresentationResourceLoaderTestCase.java
@@ -0,0 +1,153 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.velocity;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Date;
+import org.apache.velocity.Template;
+import org.apache.velocity.exception.ResourceNotFoundException;
+import org.apache.velocity.util.ExtProperties;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.engine.io.IoUtils;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+/** Unit tests for {@link RepresentationResourceLoader}. */
+class RepresentationResourceLoaderTestCase {
+
+ /** A representation whose reader always fails, to simulate an I/O error while loading. */
+ private static class BrokenRepresentation extends StringRepresentation {
+ BrokenRepresentation(String text, MediaType mediaType) {
+ super(text, mediaType);
+ }
+
+ @Override
+ public Reader getReader() throws IOException {
+ throw new IOException("Simulated I/O error");
+ }
+ }
+
+ @AfterEach
+ void tearDown() {
+ RepresentationResourceLoader.getStore().clear();
+ }
+
+ @Test
+ void getResourceReader_sourceInStore_returnsStoredRepresentationContent() throws Exception {
+ RepresentationResourceLoader.getStore()
+ .put("storedKey", new StringRepresentation("stored-content"));
+ RepresentationResourceLoader loader = new RepresentationResourceLoader(null);
+
+ Reader reader = loader.getResourceReader("storedKey", "UTF-8");
+
+ assertEquals("stored-content", IoUtils.toString(reader));
+ }
+
+ @Test
+ void getResourceReader_sourceNotInStoreWithDefault_returnsDefaultRepresentationContent()
+ throws Exception {
+ Representation defaultRepresentation = new StringRepresentation("default-content");
+ RepresentationResourceLoader loader =
+ new RepresentationResourceLoader(defaultRepresentation);
+
+ Reader reader = loader.getResourceReader("unknownKey", "UTF-8");
+
+ assertEquals("default-content", IoUtils.toString(reader));
+ }
+
+ @Test
+ void getResourceReader_sourceNotInStoreAndNoDefault_throwsResourceNotFoundException() {
+ RepresentationResourceLoader loader = new RepresentationResourceLoader(null);
+
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> loader.getResourceReader("missingKey", "UTF-8"));
+ }
+
+ @Test
+ void getResourceReader_whenReadingFails_wrapsIOExceptionInResourceNotFoundException() {
+ RepresentationResourceLoader loader =
+ new RepresentationResourceLoader(
+ new BrokenRepresentation("content", MediaType.TEXT_PLAIN));
+
+ ResourceNotFoundException e =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> loader.getResourceReader("anyKey", "UTF-8"));
+ assertTrue(e.getCause() instanceof IOException);
+ }
+
+ @Test
+ void getLastModified_resourceInStore_returnsStoredModificationDateMillis() {
+ Date date = new Date(1_000_000L);
+ Representation representation = new StringRepresentation("content");
+ representation.setModificationDate(date);
+ RepresentationResourceLoader.getStore().put("modKey", representation);
+
+ Template resource = new Template();
+ resource.setName("modKey");
+
+ RepresentationResourceLoader loader = new RepresentationResourceLoader(null);
+ assertEquals(date.getTime(), loader.getLastModified(resource));
+ }
+
+ @Test
+ void getLastModified_resourceNotInStore_returnsZero() {
+ Template resource = new Template();
+ resource.setName("absentKey");
+
+ RepresentationResourceLoader loader = new RepresentationResourceLoader(null);
+ assertEquals(0L, loader.getLastModified(resource));
+ }
+
+ @Test
+ void isSourceModified_whenLastModifiedDiffers_returnsTrue() {
+ Date date = new Date(2_000_000L);
+ Representation representation = new StringRepresentation("content");
+ representation.setModificationDate(date);
+ RepresentationResourceLoader.getStore().put("modKey2", representation);
+
+ Template resource = new Template();
+ resource.setName("modKey2");
+ resource.setLastModified(date.getTime() - 1000);
+
+ RepresentationResourceLoader loader = new RepresentationResourceLoader(null);
+ assertTrue(loader.isSourceModified(resource));
+ }
+
+ @Test
+ void isSourceModified_whenLastModifiedMatches_returnsFalse() {
+ Date date = new Date(3_000_000L);
+ Representation representation = new StringRepresentation("content");
+ representation.setModificationDate(date);
+ RepresentationResourceLoader.getStore().put("modKey3", representation);
+
+ Template resource = new Template();
+ resource.setName("modKey3");
+ resource.setLastModified(date.getTime());
+
+ RepresentationResourceLoader loader = new RepresentationResourceLoader(null);
+ assertFalse(loader.isSourceModified(resource));
+ }
+
+ @Test
+ void init_doesNotThrow() {
+ RepresentationResourceLoader loader = new RepresentationResourceLoader(null);
+ assertDoesNotThrow(() -> loader.init(new ExtProperties()));
+ }
+}
diff --git a/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/TemplateFilterTestCase.java b/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/TemplateFilterTestCase.java
index 10a065535f..4a0451afec 100644
--- a/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/TemplateFilterTestCase.java
+++ b/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/TemplateFilterTestCase.java
@@ -9,7 +9,12 @@
package org.restlet.ext.velocity;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.restlet.Application;
@@ -18,11 +23,16 @@
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
+import org.restlet.data.Encoding;
import org.restlet.data.LocalReference;
+import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
+import org.restlet.data.Status;
import org.restlet.engine.Engine;
+import org.restlet.representation.StringRepresentation;
import org.restlet.resource.Directory;
+import org.restlet.util.Resolver;
/**
* Test case for template filters.
@@ -46,6 +56,113 @@ void representationShouldNotBeUsedAsTemplate() throws Exception {
assertEquals("Method=${m}/Path=${rp}", response.getEntity().getText());
}
+ /** A Restlet that always answers with a fixed Velocity-encoded entity. */
+ private static class VelocityEntityRestlet extends Restlet {
+ private final String content;
+
+ VelocityEntityRestlet(String content) {
+ this.content = content;
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+ StringRepresentation entity = new StringRepresentation(content, MediaType.TEXT_PLAIN);
+ entity.getEncodings().add(Encoding.VELOCITY);
+ response.setEntity(entity);
+ }
+ }
+
+ /** A representation whose reader always fails, used to trigger a ResourceNotFoundException. */
+ private static class BrokenVelocityRepresentation extends StringRepresentation {
+ BrokenVelocityRepresentation(String text, MediaType mediaType) {
+ super(text, mediaType);
+ getEncodings().add(Encoding.VELOCITY);
+ }
+
+ @Override
+ public Reader getReader() throws IOException {
+ throw new IOException("Simulated I/O error");
+ }
+ }
+
+ @Test
+ void defaultConstructor_createsUsableFilter() {
+ TemplateFilter filter = new TemplateFilter();
+ assertNull(filter.getContext());
+ }
+
+ @Test
+ void constructorWithContextOnly_setsContext() {
+ Context context = new Context();
+ TemplateFilter filter = new TemplateFilter(context);
+ assertEquals(context, filter.getContext());
+ }
+
+ @Test
+ void constructorWithMapDataModel_usesMapDataModelWhenHandling() {
+ Map dataModel = new HashMap<>();
+ dataModel.put("value", "fromMap");
+
+ TemplateFilter filter =
+ new TemplateFilter(
+ new Context(), new VelocityEntityRestlet("Value=$value"), dataModel);
+
+ Response response = filter.handle(new Request(Method.GET, "/"));
+
+ assertEquals(MediaType.TEXT_PLAIN, response.getEntity().getMediaType());
+ assertEquals(true, response.getEntity() instanceof TemplateRepresentation);
+ }
+
+ @Test
+ void constructorWithResolverDataModel_usesResolverDataModelWhenHandling() {
+ Resolver resolver =
+ new Resolver() {
+ @Override
+ public Object resolve(String name) {
+ return "value".equals(name) ? "fromResolver" : null;
+ }
+ };
+
+ TemplateFilter filter =
+ new TemplateFilter(
+ new Context(), new VelocityEntityRestlet("Value=$value"), resolver);
+
+ Response response = filter.handle(new Request(Method.GET, "/"));
+
+ assertEquals(MediaType.TEXT_PLAIN, response.getEntity().getMediaType());
+ assertEquals(true, response.getEntity() instanceof TemplateRepresentation);
+ }
+
+ @Test
+ void afterHandle_whenTemplateResourceCannotBeRead_setsStatusNotFound() {
+ TemplateFilter filter =
+ new TemplateFilter(
+ new Context(),
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setEntity(
+ new BrokenVelocityRepresentation(
+ "content", MediaType.TEXT_PLAIN));
+ }
+ });
+
+ Response response = filter.handle(new Request(Method.GET, "/"));
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, response.getStatus());
+ }
+
+ @Test
+ void afterHandle_whenTemplateHasParseError_setsStatusInternalServerError() {
+ TemplateFilter filter =
+ new TemplateFilter(
+ new Context(), new VelocityEntityRestlet("#if($unclosedDirective)"));
+
+ Response response = filter.handle(new Request(Method.GET, "/"));
+
+ assertEquals(Status.SERVER_ERROR_INTERNAL, response.getStatus());
+ }
+
/**
* Internal class used for test purpose
*
diff --git a/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/TemplateRepresentationTestCase.java b/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/TemplateRepresentationTestCase.java
new file mode 100644
index 0000000000..f8f5ecdc43
--- /dev/null
+++ b/org.restlet.ext.velocity/src/test/java/org/restlet/ext/velocity/TemplateRepresentationTestCase.java
@@ -0,0 +1,198 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.velocity;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.TreeMap;
+import org.apache.velocity.Template;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.data.MediaType;
+import org.restlet.engine.io.IoUtils;
+import org.restlet.representation.WriterRepresentation;
+import org.restlet.util.Resolver;
+
+/**
+ * Unit tests for {@link TemplateRepresentation}, focused on its less commonly exercised branches.
+ */
+class TemplateRepresentationTestCase {
+
+ /** A minimal representation that never exposes a character set nor a modification date. */
+ private static class RawRepresentation extends WriterRepresentation {
+ private final String content;
+
+ RawRepresentation(String content, MediaType mediaType) {
+ super(mediaType);
+ this.content = content;
+ }
+
+ @Override
+ public void write(Writer writer) throws IOException {
+ writer.write(content);
+ }
+ }
+
+ private File testDir;
+
+ @AfterEach
+ void tearDown() {
+ if (testDir != null) {
+ IoUtils.delete(testDir, true);
+ }
+ }
+
+ private static org.apache.velocity.context.Context getInternalContext(TemplateRepresentation tr)
+ throws Exception {
+ Field field = TemplateRepresentation.class.getDeclaredField("context");
+ field.setAccessible(true);
+ return (org.apache.velocity.context.Context) field.get(tr);
+ }
+
+ private static Resolver knownValueResolver() {
+ return new Resolver() {
+ @Override
+ public Object resolve(String name) {
+ return "known".equals(name) ? "value1" : null;
+ }
+ };
+ }
+
+ @Test
+ void resolverContext_containsKey_reflectsResolverPresenceOfValue() throws Exception {
+ TemplateRepresentation tr = new TemplateRepresentation("template.vm", MediaType.TEXT_PLAIN);
+ tr.setDataModel(knownValueResolver());
+
+ org.apache.velocity.context.Context context = getInternalContext(tr);
+
+ assertTrue(context.containsKey("known"));
+ assertFalse(context.containsKey("unknown"));
+ }
+
+ @Test
+ void resolverContext_get_delegatesToResolver() throws Exception {
+ TemplateRepresentation tr = new TemplateRepresentation("template.vm", MediaType.TEXT_PLAIN);
+ tr.setDataModel(knownValueResolver());
+
+ org.apache.velocity.context.Context context = getInternalContext(tr);
+
+ assertEquals("value1", context.get("known"));
+ }
+
+ @Test
+ void resolverContext_getKeys_returnsNull() throws Exception {
+ TemplateRepresentation tr = new TemplateRepresentation("template.vm", MediaType.TEXT_PLAIN);
+ tr.setDataModel(knownValueResolver());
+
+ org.apache.velocity.context.Context context = getInternalContext(tr);
+
+ assertNull(context.getKeys());
+ }
+
+ @Test
+ void resolverContext_put_returnsNullAndDoesNotStoreValue() throws Exception {
+ TemplateRepresentation tr = new TemplateRepresentation("template.vm", MediaType.TEXT_PLAIN);
+ tr.setDataModel(knownValueResolver());
+
+ org.apache.velocity.context.Context context = getInternalContext(tr);
+
+ assertNull(context.put("known", "other"));
+ // The resolver is still consulted; nothing was actually stored.
+ assertEquals("value1", context.get("known"));
+ }
+
+ @Test
+ void resolverContext_remove_returnsNull() throws Exception {
+ TemplateRepresentation tr = new TemplateRepresentation("template.vm", MediaType.TEXT_PLAIN);
+ tr.setDataModel(knownValueResolver());
+
+ org.apache.velocity.context.Context context = getInternalContext(tr);
+
+ assertNull(context.remove("known"));
+ }
+
+ @Test
+ void constructorWithTemplateAndMapDataModel_mergesProvidedValues() throws Exception {
+ testDir = new File(System.getProperty("java.io.tmpdir"), "TemplateRepresentationTestCase");
+ testDir.mkdir();
+ File testFile = File.createTempFile("test", ".vm", testDir);
+
+ try (FileWriter fw = new FileWriter(testFile)) {
+ fw.write("Value=$value");
+ }
+
+ TemplateRepresentation base =
+ new TemplateRepresentation(testFile.getName(), MediaType.TEXT_PLAIN);
+ base.getEngine().setProperty("file.resource.loader.path", testDir.getAbsolutePath());
+ Template template = base.getTemplate();
+
+ Map map = new TreeMap<>();
+ map.put("value", "myValue2");
+
+ TemplateRepresentation tr = new TemplateRepresentation(template, map, MediaType.TEXT_PLAIN);
+
+ assertEquals(MediaType.TEXT_PLAIN, tr.getMediaType());
+ assertEquals("Value=myValue2", tr.getText());
+ }
+
+ @Test
+ void constructorWithRepresentationAndMediaType_withoutCharsetOrModDate_usesDefaults()
+ throws Exception {
+ RawRepresentation raw = new RawRepresentation("Value=fixed", MediaType.TEXT_PLAIN);
+
+ TemplateRepresentation tr = new TemplateRepresentation(raw, MediaType.TEXT_PLAIN);
+
+ assertEquals("Value=fixed", tr.getText());
+ }
+
+ @Test
+ void constructorWithRepresentationMapAndMediaType_withoutCharset_usesDefaultCharset()
+ throws Exception {
+ RawRepresentation raw = new RawRepresentation("Value=$value", MediaType.TEXT_PLAIN);
+ Map map = new TreeMap<>();
+ map.put("value", "myValue3");
+
+ TemplateRepresentation tr = new TemplateRepresentation(raw, map, MediaType.TEXT_PLAIN);
+
+ assertEquals("Value=myValue3", tr.getText());
+ }
+
+ @Test
+ void getTemplate_whenTemplateCannotBeLoaded_logsWarningAndReturnsNull() {
+ Context previous = Context.getCurrent();
+ try {
+ Context.setCurrent(new Context());
+ TemplateRepresentation tr =
+ new TemplateRepresentation("doesNotExist.vm", MediaType.TEXT_PLAIN);
+ assertNull(tr.getTemplate());
+ } finally {
+ Context.setCurrent(previous);
+ }
+ }
+
+ @Test
+ void getText_whenTemplateCannotBeLoaded_throwsIOException() {
+ TemplateRepresentation tr =
+ new TemplateRepresentation("doesNotExist2.vm", MediaType.TEXT_PLAIN);
+
+ IOException e = assertThrows(IOException.class, tr::getText);
+ assertTrue(e.getMessage().startsWith("Template processing error."));
+ }
+}
diff --git a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/DomRepresentationTestCase.java b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/DomRepresentationTestCase.java
index 662901e8d9..cb59dc67e7 100644
--- a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/DomRepresentationTestCase.java
+++ b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/DomRepresentationTestCase.java
@@ -12,15 +12,20 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
+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 java.io.StringReader;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.dom.DOMSource;
import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
import org.restlet.data.MediaType;
+import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
/** Unit tests for {@link DomRepresentation}. */
class DomRepresentationTestCase {
@@ -91,4 +96,53 @@ void release_onDocumentWrapper_createsNewDocumentOnNextAccess() throws Exception
assertNotNull(newDoc);
assertNotSame(doc, newDoc);
}
+
+ @Test
+ void constructor_withNullRepresentation_hasNullMediaTypeAndIsUnavailable() {
+ DomRepresentation dr = new DomRepresentation((Representation) null);
+ assertNull(dr.getMediaType());
+ assertFalse(dr.isAvailable());
+ }
+
+ @Test
+ void getInputSource_withoutXmlRepresentation_returnsInputSourceWithNullStream()
+ throws Exception {
+ DomRepresentation dr = new DomRepresentation();
+ InputSource inputSource = dr.getInputSource();
+ assertNotNull(inputSource);
+ assertNull(inputSource.getByteStream());
+ }
+
+ @Test
+ void getInputSource_withUnavailableXmlRepresentation_returnsInputSourceWithNullStream()
+ throws Exception {
+ StringRepresentation rep = new StringRepresentation(XML, MediaType.TEXT_XML);
+ rep.setAvailable(false);
+ DomRepresentation dr = new DomRepresentation(rep);
+ InputSource inputSource = dr.getInputSource();
+ assertNull(inputSource.getByteStream());
+ }
+
+ @Test
+ void write_withExplicitCharacterSet_usesItForEncoding() throws Exception {
+ DomRepresentation dr =
+ new DomRepresentation(new StringRepresentation(XML, MediaType.TEXT_XML));
+ dr.setCharacterSet(CharacterSet.UTF_8);
+ String result = dr.getText();
+ assertTrue(result.contains("root"));
+ }
+
+ @Test
+ void write_withDoctypePublicAndSystemIds_appliesDoctypeOutputProperties() throws Exception {
+ String xmlWithDoctype =
+ ""
+ + ""
+ + "hello ";
+ DomRepresentation dr =
+ new DomRepresentation(new StringRepresentation(xmlWithDoctype, MediaType.TEXT_XML));
+ // Avoid any network/filesystem access for the external DTD referenced above.
+ dr.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader("")));
+ String result = dr.getText();
+ assertTrue(result.contains("root"));
+ }
}
diff --git a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/NodeListTestCase.java b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/NodeListTestCase.java
new file mode 100644
index 0000000000..50a0e3f7f2
--- /dev/null
+++ b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/NodeListTestCase.java
@@ -0,0 +1,86 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.ext.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+/** Unit tests for {@link NodeList}. */
+class NodeListTestCase {
+
+ private static final String XML = " ";
+
+ private static org.w3c.dom.NodeList domNodeList() throws Exception {
+ Document document =
+ DocumentBuilderFactory.newInstance()
+ .newDocumentBuilder()
+ .parse(new org.xml.sax.InputSource(new java.io.StringReader(XML)));
+ return document.getDocumentElement().getChildNodes();
+ }
+
+ @Test
+ void get_returnsWrappedNodeAtIndex() throws Exception {
+ NodeList nodeList = new NodeList(domNodeList());
+ assertEquals("a", nodeList.get(0).getNodeName());
+ assertEquals("b", nodeList.get(1).getNodeName());
+ assertEquals("c", nodeList.get(2).getNodeName());
+ }
+
+ @Test
+ void item_returnsSameNodeAsGet() throws Exception {
+ NodeList nodeList = new NodeList(domNodeList());
+ Node viaGet = nodeList.get(1);
+ Node viaItem = nodeList.item(1);
+ assertEquals(viaGet, viaItem);
+ }
+
+ @Test
+ void getLength_matchesUnderlyingNodeListLength() throws Exception {
+ org.w3c.dom.NodeList wrapped = domNodeList();
+ NodeList nodeList = new NodeList(wrapped);
+ assertEquals(wrapped.getLength(), nodeList.getLength());
+ }
+
+ @Test
+ void size_matchesGetLength() throws Exception {
+ NodeList nodeList = new NodeList(domNodeList());
+ assertEquals(nodeList.getLength(), nodeList.size());
+ }
+
+ @Test
+ void isEmpty_falseWhenNodesPresent() throws Exception {
+ NodeList nodeList = new NodeList(domNodeList());
+ assertFalse(nodeList.isEmpty());
+ }
+
+ @Test
+ void iterator_iteratesOverAllWrappedNodes() throws Exception {
+ NodeList nodeList = new NodeList(domNodeList());
+ int count = 0;
+ for (Node node : nodeList) {
+ assertNotNull(node);
+ count++;
+ }
+ assertEquals(3, count);
+ }
+
+ @Test
+ void asList_supportsStandardListOperations() throws Exception {
+ NodeList nodeList = new NodeList(domNodeList());
+ assertTrue(nodeList.contains(nodeList.get(0)));
+ assertEquals(0, nodeList.indexOf(nodeList.get(0)));
+ }
+}
diff --git a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/SaxRepresentationTestCase.java b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/SaxRepresentationTestCase.java
index f3e0aa27fe..09f06b94d5 100644
--- a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/SaxRepresentationTestCase.java
+++ b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/SaxRepresentationTestCase.java
@@ -24,6 +24,7 @@
import javax.xml.transform.sax.SAXSource;
import org.junit.jupiter.api.Test;
import org.restlet.data.MediaType;
+import org.restlet.data.Reference;
import org.restlet.representation.StringRepresentation;
import org.w3c.dom.Document;
import org.xml.sax.Attributes;
@@ -139,4 +140,46 @@ void setSaxSource_overridesSource() throws Exception {
sr.setSaxSource(source);
assertEquals(source, sr.getSaxSource());
}
+
+ @Test
+ void getSaxSource_withXmlRepresentationSource_delegatesToItsSaxSource() throws Exception {
+ DomRepresentation domSource =
+ new DomRepresentation(new StringRepresentation(XML, MediaType.TEXT_XML));
+ SaxRepresentation sr = new SaxRepresentation(domSource);
+ assertNotNull(sr.getSaxSource());
+ }
+
+ @Test
+ void getSaxSource_withCustomFeatures_stillBuildsSource() throws Exception {
+ StringRepresentation rep = new StringRepresentation(XML, MediaType.TEXT_XML);
+ SaxRepresentation sr = new SaxRepresentation(rep);
+ sr.setNamespaceAware(true);
+ sr.setXIncludeAware(true);
+ sr.setSecureProcessing(true);
+ sr.setExpandingEntityRefs(false);
+ assertNotNull(sr.getSaxSource());
+ }
+
+ @Test
+ void getSaxSource_withLocationRef_setsSystemId() throws Exception {
+ StringRepresentation rep = new StringRepresentation(XML, MediaType.TEXT_XML);
+ rep.setLocationRef(new Reference("http://example.com/loc.xml"));
+ SaxRepresentation sr = new SaxRepresentation(rep);
+ assertEquals("http://example.com/loc.xml", sr.getSaxSource().getSystemId());
+ }
+
+ @Test
+ void parse_withMalformedXml_throwsIOException() {
+ SaxRepresentation sr =
+ new SaxRepresentation(new StringRepresentation("not xml", MediaType.TEXT_XML));
+ assertThrows(IOException.class, () -> sr.parse(new DefaultHandler()));
+ }
+
+ @Test
+ void release_releasesUnderlyingXmlRepresentation() throws Exception {
+ StringRepresentation rep = new StringRepresentation(XML, MediaType.TEXT_XML);
+ SaxRepresentation sr = new SaxRepresentation(rep);
+ sr.release();
+ assertFalse(rep.isAvailable());
+ }
}
diff --git a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/TransformRepresentationTestCase.java b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/TransformRepresentationTestCase.java
index 8bd92bd79f..aebc8eaff0 100644
--- a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/TransformRepresentationTestCase.java
+++ b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/TransformRepresentationTestCase.java
@@ -9,11 +9,31 @@
package org.restlet.ext.xml;
import static org.junit.jupiter.api.Assertions.assertEquals;
+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.assertThrows;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+import javax.xml.transform.ErrorListener;
+import javax.xml.transform.Templates;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.URIResolver;
+import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
import org.junit.jupiter.api.Test;
import org.restlet.data.MediaType;
+import org.restlet.data.Reference;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
+import org.xml.sax.XMLFilter;
/**
* Test case for the {@link TransformRepresentation} class.
@@ -74,4 +94,189 @@ void testDoubleTransform() throws Exception {
final String result = tr2.getText();
assertEquals(this.output2, result);
}
+
+ @Test
+ void toSaxSource_withXmlRepresentationSource_usesItsSaxSource() throws Exception {
+ DomRepresentation domSource = new DomRepresentation(this.source);
+ TransformRepresentation tr = new TransformRepresentation(domSource, this.xslt1);
+ assertEquals(this.output1, tr.getText());
+ }
+
+ @Test
+ void toSaxSource_defaultCase_setsSystemIdFromLocationRef() throws Exception {
+ StringRepresentation rep = new StringRepresentation(" ", MediaType.TEXT_XML);
+ rep.setLocationRef(new Reference("http://example.com/src.xml"));
+ assertEquals(
+ "http://example.com/src.xml",
+ TransformRepresentation.toSaxSource(rep).getSystemId());
+ }
+
+ @Test
+ void constructor_withPrecompiledTemplates_transformsUsingThem() throws Exception {
+ Templates templates =
+ TransformerFactory.newInstance()
+ .newTemplates(new StreamSource(this.xslt1.getStream()));
+ TransformRepresentation tr =
+ new TransformRepresentation((URIResolver) null, this.source, templates);
+ assertEquals(this.output1, tr.getText());
+ }
+
+ @Test
+ void getTransformerHandler_returnsHandlerWhenTemplatesAvailable() throws Exception {
+ TransformRepresentation tr = new TransformRepresentation(this.source, this.xslt1);
+ TransformerHandler handler = tr.getTransformerHandler();
+ assertNotNull(handler);
+ }
+
+ @Test
+ void getTransformerHandler_withoutTransformSheetOrTemplates_returnsNull() throws Exception {
+ TransformRepresentation tr =
+ new TransformRepresentation(this.source, (Representation) null);
+ assertNull(tr.getTransformerHandler());
+ }
+
+ @Test
+ void getXmlFilter_returnsFilterWhenTemplatesAvailable() throws Exception {
+ TransformRepresentation tr = new TransformRepresentation(this.source, this.xslt1);
+ XMLFilter filter = tr.getXmlFilter();
+ assertNotNull(filter);
+ }
+
+ @Test
+ void getXmlFilter_withoutTransformSheetOrTemplates_returnsNull() throws Exception {
+ TransformRepresentation tr =
+ new TransformRepresentation(this.source, (Representation) null);
+ assertNull(tr.getXmlFilter());
+ }
+
+ @Test
+ void getTemplates_withTransformSheetLocationRef_setsSystemId() throws Exception {
+ StringRepresentation sheetWithLoc =
+ new StringRepresentation(
+ ""
+ + ""
+ + ""
+ + " "
+ + " "
+ + " ",
+ MediaType.TEXT_XML);
+ sheetWithLoc.setLocationRef(new Reference("http://example.com/sheet.xsl"));
+ TransformRepresentation tr = new TransformRepresentation(this.source, sheetWithLoc);
+ assertNotNull(tr.getTemplates());
+ }
+
+ @Test
+ void getTransformer_appliesErrorListenerUriResolverParametersAndOutputProperties()
+ throws Exception {
+ TransformRepresentation tr = new TransformRepresentation(this.source, this.xslt1);
+ ErrorListener listener =
+ new ErrorListener() {
+ @Override
+ public void warning(TransformerException exception) {}
+
+ @Override
+ public void error(TransformerException exception) {}
+
+ @Override
+ public void fatalError(TransformerException exception)
+ throws TransformerException {
+ throw exception;
+ }
+ };
+ tr.setErrorListener(listener);
+ tr.setUriResolver((href, base) -> null);
+ tr.getParameters().put("p1", "v1");
+ tr.getOutputProperties().put(javax.xml.transform.OutputKeys.INDENT, "yes");
+
+ Transformer transformer = tr.getTransformer();
+
+ assertNotNull(transformer);
+ assertEquals("v1", transformer.getParameter("p1"));
+ }
+
+ @Test
+ void release_clearsSourceTransformSheetTemplatesAndUriResolver() throws Exception {
+ TransformRepresentation tr =
+ new TransformRepresentation((href, base) -> null, this.source, this.xslt1);
+ // Force templates to be lazily created before release.
+ assertNotNull(tr.getTemplates());
+ assertNotNull(tr.getSourceRepresentation());
+ assertNotNull(tr.getTransformSheet());
+ assertNotNull(tr.getUriResolver());
+
+ tr.release();
+
+ assertNull(tr.getSourceRepresentation());
+ assertNull(tr.getTransformSheet());
+ assertNull(tr.getUriResolver());
+ }
+
+ @Test
+ void settersAndGetters_roundTrip() throws Exception {
+ TransformRepresentation tr = new TransformRepresentation(this.source, this.xslt1);
+
+ ErrorListener listener =
+ new ErrorListener() {
+ @Override
+ public void warning(TransformerException exception) {}
+
+ @Override
+ public void error(TransformerException exception) {}
+
+ @Override
+ public void fatalError(TransformerException exception) {}
+ };
+ tr.setErrorListener(listener);
+ assertSame(listener, tr.getErrorListener());
+
+ Map outputProps = new HashMap<>();
+ tr.setOutputProperties(outputProps);
+ assertSame(outputProps, tr.getOutputProperties());
+
+ Map params = new HashMap<>();
+ tr.setParameters(params);
+ assertSame(params, tr.getParameters());
+
+ Representation newSource = new StringRepresentation(" ", MediaType.TEXT_XML);
+ tr.setSourceRepresentation(newSource);
+ assertSame(newSource, tr.getSourceRepresentation());
+
+ Templates templates =
+ TransformerFactory.newInstance()
+ .newTemplates(new StreamSource(this.xslt1.getStream()));
+ tr.setTemplates(templates);
+ assertSame(templates, tr.getTemplates());
+
+ Representation newSheet = new StringRepresentation(" ", MediaType.TEXT_XML);
+ tr.setTransformSheet(newSheet);
+ assertSame(newSheet, tr.getTransformSheet());
+
+ URIResolver resolver = (href, base) -> null;
+ tr.setUriResolver(resolver);
+ assertSame(resolver, tr.getUriResolver());
+ }
+
+ @Test
+ void transform_withoutTransformSheetOrTemplates_logsWarningAndDoesNothing() throws Exception {
+ TransformRepresentation tr =
+ new TransformRepresentation(this.source, (Representation) null);
+ StreamResult result = new StreamResult(new StringWriter());
+ tr.transform(new StreamSource(new StringReader(" ")), result);
+ }
+
+ @Test
+ void doubleTransform_withFailingInnerTransform_wrapsTransformerExceptionAsIOException() {
+ Representation failingXslt =
+ new StringRepresentation(
+ ""
+ + ""
+ + ""
+ + "boom "
+ + " "
+ + " ",
+ MediaType.TEXT_XML);
+ TransformRepresentation tr1 = new TransformRepresentation(this.source, failingXslt);
+ TransformRepresentation tr2 = new TransformRepresentation(tr1, this.xslt2);
+ assertThrows(IOException.class, tr2::getText);
+ }
}
diff --git a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/XmlRepresentationTestCase.java b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/XmlRepresentationTestCase.java
index 4863c4fee6..248b372487 100644
--- a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/XmlRepresentationTestCase.java
+++ b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/XmlRepresentationTestCase.java
@@ -8,17 +8,28 @@
*/
package org.restlet.ext.xml;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.StringReader;
import java.util.Iterator;
+import javax.xml.XMLConstants;
import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
import org.junit.jupiter.api.Test;
import org.restlet.data.MediaType;
+import org.restlet.data.Reference;
+import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Node;
/**
@@ -143,4 +154,288 @@ void setIgnoringExtraWhitespaces_alsoSetsValidatingDtd() {
assertTrue(dr.isIgnoringExtraWhitespaces());
assertTrue(dr.isValidatingDtd());
}
+
+ private static final String XSD =
+ ""
+ + ""
+ + ""
+ + ""
+ + ""
+ + " "
+ + " "
+ + " ";
+
+ private static final String VALID_FOR_XSD = "hello ";
+
+ @Test
+ void getSaxSource_static_withRepresentation_returnsNonNull() throws Exception {
+ assertNotNull(
+ XmlRepresentation.getSaxSource(new StringRepresentation(XML, MediaType.TEXT_XML)));
+ }
+
+ @Test
+ void getSaxSource_static_withNullRepresentation_returnsNull() throws Exception {
+ assertNull(XmlRepresentation.getSaxSource(null));
+ }
+
+ @Test
+ void getSaxSource_static_withLocationRef_setsSystemId() throws Exception {
+ StringRepresentation rep = new StringRepresentation(XML, MediaType.TEXT_XML);
+ rep.setLocationRef(new Reference("http://example.com/doc.xml"));
+ assertEquals(
+ "http://example.com/doc.xml", XmlRepresentation.getSaxSource(rep).getSystemId());
+ }
+
+ @Test
+ void getSaxSource_instance_returnsNonNull() throws Exception {
+ assertNotNull(dom().getSaxSource());
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ DomRepresentation dr = dom();
+ assertTrue(dr.equals(dr));
+ }
+
+ @Test
+ void hashCode_isStable() {
+ DomRepresentation dr = dom();
+ assertEquals(dr.hashCode(), dr.hashCode());
+ }
+
+ @Test
+ void release_clearsNamespacesMap() {
+ DomRepresentation dr = dom();
+ dr.getNamespaces().put("ex", "http://example.com/");
+ dr.release();
+ assertTrue(dr.getNamespaces().isEmpty());
+ }
+
+ @Test
+ void setEntityResolver_getEntityResolver_roundTrip() {
+ DomRepresentation dr = dom();
+ org.xml.sax.EntityResolver resolver = (publicId, systemId) -> null;
+ dr.setEntityResolver(resolver);
+ assertEquals(resolver, dr.getEntityResolver());
+ }
+
+ @Test
+ void setErrorHandler_getErrorHandler_roundTrip() {
+ DomRepresentation dr = dom();
+ org.xml.sax.ErrorHandler handler = new org.xml.sax.helpers.DefaultHandler();
+ dr.setErrorHandler(handler);
+ assertEquals(handler, dr.getErrorHandler());
+ }
+
+ @Test
+ void setExpandingEntityRefs_isExpandingEntityRefs_roundTrip() {
+ DomRepresentation dr = dom();
+ dr.setExpandingEntityRefs(true);
+ assertTrue(dr.isExpandingEntityRefs());
+ }
+
+ @Test
+ void setIgnoringComments_isIgnoringComments_roundTrip() {
+ DomRepresentation dr = dom();
+ dr.setIgnoringComments(true);
+ assertTrue(dr.isIgnoringComments());
+ }
+
+ @Test
+ void setNamespaces_getNamespaces_roundTrip() {
+ DomRepresentation dr = dom();
+ java.util.Map map = new java.util.HashMap<>();
+ map.put("a", "b");
+ dr.setNamespaces(map);
+ assertEquals(map, dr.getNamespaces());
+ }
+
+ @Test
+ void setXIncludeAware_isXIncludeAware_roundTrip() {
+ DomRepresentation dr = dom();
+ assertFalse(dr.isXIncludeAware());
+ dr.setXIncludeAware(true);
+ assertTrue(dr.isXIncludeAware());
+ }
+
+ @Test
+ void setSchema_compiledSchema_getSchema_roundTrip() throws Exception {
+ Schema schema =
+ SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
+ .newSchema(new StreamSource(new StringReader(XSD)));
+ DomRepresentation dr = dom();
+ dr.setSchema(schema);
+ assertEquals(schema, dr.getSchema());
+ }
+
+ @Test
+ void setSchema_withW3cSchemaRepresentation_compilesSchema() {
+ DomRepresentation dr = dom();
+ dr.setSchema(new StringRepresentation(XSD, MediaType.APPLICATION_W3C_SCHEMA));
+ assertNotNull(dr.getSchema());
+ }
+
+ @Test
+ void setSchema_withNullRepresentation_leavesSchemaNull() {
+ DomRepresentation dr = dom();
+ dr.setSchema((Representation) null);
+ assertNull(dr.getSchema());
+ }
+
+ @Test
+ void setSchema_withRelaxNgCompactMediaType_doesNotThrow() {
+ DomRepresentation dr = dom();
+ assertDoesNotThrow(
+ () ->
+ dr.setSchema(
+ new StringRepresentation(
+ "not a schema", MediaType.APPLICATION_RELAXNG_COMPACT)));
+ }
+
+ @Test
+ void setSchema_withRelaxNgXmlMediaType_doesNotThrow() {
+ DomRepresentation dr = dom();
+ assertDoesNotThrow(
+ () ->
+ dr.setSchema(
+ new StringRepresentation(
+ " ", MediaType.APPLICATION_RELAXNG_XML)));
+ }
+
+ @Test
+ void setSchema_withUnknownMediaType_doesNotThrow() {
+ DomRepresentation dr = dom();
+ assertDoesNotThrow(
+ () -> dr.setSchema(new StringRepresentation("data", MediaType.TEXT_PLAIN)));
+ assertNull(dr.getSchema());
+ }
+
+ @Test
+ void validate_withCompiledSchema_succeedsForValidDocument() throws Exception {
+ Schema schema =
+ SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
+ .newSchema(new StreamSource(new StringReader(XSD)));
+ DomRepresentation dr =
+ new DomRepresentation(new StringRepresentation(VALID_FOR_XSD, MediaType.TEXT_XML));
+ dr.validate(schema);
+ }
+
+ @Test
+ void validate_withSchemaRepresentation_succeedsForValidDocument() throws Exception {
+ DomRepresentation dr =
+ new DomRepresentation(new StringRepresentation(VALID_FOR_XSD, MediaType.TEXT_XML));
+ dr.validate(new StringRepresentation(XSD, MediaType.APPLICATION_W3C_SCHEMA));
+ }
+
+ @Test
+ void validate_withSchemaRepresentationAndResult_writesAugmentedResult() throws Exception {
+ DomRepresentation dr =
+ new DomRepresentation(new StringRepresentation(VALID_FOR_XSD, MediaType.TEXT_XML));
+ // The validated source is a SAXSource, so the result must be null or a SAXResult.
+ javax.xml.transform.sax.SAXResult result =
+ new javax.xml.transform.sax.SAXResult(new org.xml.sax.helpers.DefaultHandler());
+ dr.validate(new StringRepresentation(XSD, MediaType.APPLICATION_W3C_SCHEMA), result);
+ }
+
+ @Test
+ void getStreamSource_withLocationRef_setsSystemId() throws Exception {
+ DomRepresentation dr = dom();
+ dr.setLocationRef(new Reference("http://example.com/catalog.xml"));
+ assertEquals("http://example.com/catalog.xml", dr.getStreamSource().getSystemId());
+ }
+
+ @Test
+ void getText_withMalformedXml_throwsIllegalArgumentException() {
+ DomRepresentation dr =
+ new DomRepresentation(new StringRepresentation("not xml", MediaType.TEXT_XML));
+ assertThrows(IllegalArgumentException.class, () -> dr.getText("/a"));
+ }
+
+ @Test
+ void getText_withInvalidXPathExpression_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> dom().getText("///invalid[["));
+ }
+
+ @Test
+ void getDomSource_baseImplementation_returnsWrappedDocument() throws Exception {
+ SaxRepresentation sr =
+ new SaxRepresentation(new StringRepresentation(XML, MediaType.TEXT_XML));
+ javax.xml.transform.dom.DOMSource source = sr.getDomSource();
+ assertNotNull(source);
+ assertNotNull(source.getNode());
+ }
+
+ @Test
+ void getDomSource_withLocationRef_setsSystemId() throws Exception {
+ StringRepresentation rep = new StringRepresentation(XML, MediaType.TEXT_XML);
+ SaxRepresentation sr = new SaxRepresentation(rep);
+ sr.setLocationRef(new Reference("http://example.com/loc.xml"));
+ assertEquals("http://example.com/loc.xml", sr.getDomSource().getSystemId());
+ }
+
+ @Test
+ void getDomSource_withMalformedXml_throwsIOException() {
+ SaxRepresentation sr =
+ new SaxRepresentation(new StringRepresentation("not xml", MediaType.TEXT_XML));
+ assertThrows(java.io.IOException.class, sr::getDomSource);
+ }
+
+ @Test
+ void getDocument_baseImplementation_parsesSourceRepresentation() throws Exception {
+ SaxRepresentation sr =
+ new SaxRepresentation(new StringRepresentation(XML, MediaType.TEXT_XML));
+ Document document = sr.getDocument();
+ assertNotNull(document);
+ assertEquals("catalog", document.getDocumentElement().getTagName());
+ }
+
+ @Test
+ void getDocumentBuilder_withVariousOptions_stillBuilds() throws Exception {
+ DomRepresentation dr = dom();
+ dr.setCoalescing(true);
+ dr.setIgnoringComments(true);
+ dr.setXIncludeAware(true);
+ assertNotNull(dr.getDocumentBuilder());
+ }
+
+ @Test
+ void appendTextContent_coversCommentCdataAndProcessingInstructionNodes() throws Exception {
+ Document doc =
+ javax.xml.parsers.DocumentBuilderFactory.newInstance()
+ .newDocumentBuilder()
+ .newDocument();
+ assertEquals("a comment", XmlRepresentation.getTextContent(doc.createComment("a comment")));
+ assertEquals(
+ "cdata text",
+ XmlRepresentation.getTextContent(doc.createCDATASection("cdata text")));
+ assertEquals(
+ "pi data",
+ XmlRepresentation.getTextContent(
+ doc.createProcessingInstruction("target", "pi data")));
+ }
+
+ @Test
+ void appendTextContent_coversAttributeAndDocumentFragmentNodes() throws Exception {
+ Document doc =
+ javax.xml.parsers.DocumentBuilderFactory.newInstance()
+ .newDocumentBuilder()
+ .newDocument();
+ Attr attr = doc.createAttribute("attr");
+ attr.setValue("attr-value");
+ assertEquals("attr-value", XmlRepresentation.getTextContent(attr));
+
+ DocumentFragment fragment = doc.createDocumentFragment();
+ fragment.appendChild(doc.createTextNode("frag-text"));
+ assertEquals("frag-text", XmlRepresentation.getTextContent(fragment));
+ }
+
+ @Test
+ void appendTextContent_unhandledNodeType_returnsEmptyString() throws Exception {
+ Document doc =
+ javax.xml.parsers.DocumentBuilderFactory.newInstance()
+ .newDocumentBuilder()
+ .newDocument();
+ // Document nodes are not text-bearing and fall into the default (no-op) branch.
+ assertEquals("", XmlRepresentation.getTextContent(doc));
+ }
}
diff --git a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/XmlWriterTestCase.java b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/XmlWriterTestCase.java
index a11b05b763..d129e41a23 100644
--- a/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/XmlWriterTestCase.java
+++ b/org.restlet.ext.xml/src/test/java/org/restlet/ext/xml/XmlWriterTestCase.java
@@ -15,7 +15,11 @@
import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import javax.xml.parsers.SAXParserFactory;
import org.junit.jupiter.api.Test;
+import org.xml.sax.XMLReader;
import org.xml.sax.helpers.AttributesImpl;
/** Unit tests for {@link XmlWriter}. */
@@ -245,4 +249,225 @@ void dataFormat_withIndentStep_producesNewlinesAndIndentation() throws Exception
assertTrue(result.contains("\n"));
assertTrue(result.contains(" "));
}
+
+ @Test
+ void constructor_noArgs_usesStandardOutByDefault() {
+ XmlWriter w = new XmlWriter();
+ assertNotNull(w.getWriter());
+ }
+
+ @Test
+ void constructor_nullWriter_fallsBackToStandardOut() {
+ XmlWriter w = new XmlWriter((Writer) null);
+ assertNotNull(w.getWriter());
+ }
+
+ @Test
+ void setOutput_null_fallsBackToStandardOut() {
+ XmlWriter w = new XmlWriter(sw());
+ w.setOutput(null);
+ assertNotNull(w.getWriter());
+ }
+
+ @Test
+ void constructor_outputStreamAndCharset_writesDocument() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ XmlWriter w = new XmlWriter(baos, StandardCharsets.UTF_8);
+ w.startDocument();
+ w.dataElement("root", "ok");
+ w.endDocument();
+ assertTrue(baos.toString(StandardCharsets.UTF_8).contains("ok "));
+ }
+
+ @Test
+ void constructor_outputStreamAndCharsetEncoder_writesDocument() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ XmlWriter w = new XmlWriter(baos, StandardCharsets.UTF_8.newEncoder());
+ w.startDocument();
+ w.dataElement("root", "ok");
+ w.endDocument();
+ assertTrue(baos.toString(StandardCharsets.UTF_8).contains("ok "));
+ }
+
+ @Test
+ void constructor_outputStreamAndCharsetName_writesDocument() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ XmlWriter w = new XmlWriter(baos, "UTF-8");
+ w.startDocument();
+ w.dataElement("root", "ok");
+ w.endDocument();
+ assertTrue(baos.toString("UTF-8").contains("ok "));
+ }
+
+ @Test
+ void constructor_withXmlReaderParent_canBeUsedAsFilter() throws Exception {
+ XMLReader parent = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
+ XmlWriter w = new XmlWriter(parent);
+ StringWriter out = sw();
+ w.setOutput(out);
+ w.startDocument();
+ w.startElement("root");
+ w.endElement("root");
+ w.endDocument();
+ assertTrue(out.toString().contains(""));
+ }
+
+ @Test
+ void constructor_withXmlReaderParentAndWriter_writesDocument() throws Exception {
+ XMLReader parent = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(parent, out);
+ w.startDocument();
+ w.dataElement("root", "value");
+ w.endDocument();
+ assertTrue(out.toString().contains("value "));
+ }
+
+ @Test
+ void startElement_uriAndLocalName_writesStartTag() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.startDocument();
+ w.startElement("", "root");
+ w.endElement("", "root");
+ w.endDocument();
+ assertTrue(out.toString().contains(""));
+ assertTrue(out.toString().contains(" "));
+ }
+
+ @Test
+ void emptyElement_asRootElement_forcesNamespaceDeclarationOnRoot() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.forceNSDecl("http://example.com/root-ns", "r");
+ w.startDocument();
+ w.emptyElement("http://example.com/root-ns", "root", "r:root", new AttributesImpl());
+ w.endDocument();
+ String result = out.toString();
+ assertTrue(result.contains("xmlns:r=\"http://example.com/root-ns\""));
+ assertTrue(result.contains(" "));
+ }
+
+ @Test
+ void writeAttributes_withXmlnsAttribute_forcesDefaultNamespaceDecl() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.setPrefix("http://example.com/default-ns", "");
+ AttributesImpl atts = new AttributesImpl();
+ atts.addAttribute("", "", "xmlns", "CDATA", "http://example.com/default-ns");
+ w.startDocument();
+ w.startElement("", "root", "root", atts);
+ w.endElement("root");
+ w.endDocument();
+ assertTrue(out.toString().contains("xmlns=\"http://example.com/default-ns\""));
+ }
+
+ @Test
+ void writeAttributes_withXmlnsPrefixAttribute_forcesPrefixedNamespaceDecl() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ AttributesImpl atts = new AttributesImpl();
+ atts.addAttribute("", "foo", "xmlns:foo", "CDATA", "http://example.com/foo-ns");
+ w.startDocument();
+ w.startElement("", "root", "root", atts);
+ w.endElement("root");
+ w.endDocument();
+ assertTrue(out.toString().contains("xmlns:foo=\"http://example.com/foo-ns\""));
+ }
+
+ @Test
+ void characters_withQuoteChar_notEscaped() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.startDocument();
+ w.startElement("root");
+ w.characters("say \"hi\"");
+ w.endElement("root");
+ w.endDocument();
+ assertTrue(out.toString().contains("say \"hi\""));
+ }
+
+ @Test
+ void startElement_withQNameNoColon_usesDefaultNamespace() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ AttributesImpl atts = new AttributesImpl();
+ w.startDocument();
+ w.startElement("http://example.com/newns", "root", "root", atts);
+ w.endElement("http://example.com/newns", "root", "root");
+ w.endDocument();
+ assertTrue(out.toString().contains("xmlns=\"http://example.com/newns\""));
+ }
+
+ @Test
+ void startElement_withQNameColon_usesQNamePrefix() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ AttributesImpl atts = new AttributesImpl();
+ w.startDocument();
+ w.startElement("http://example.com/newns2", "root", "ns:root", atts);
+ w.endElement("http://example.com/newns2", "root", "ns:root");
+ w.endDocument();
+ String result = out.toString();
+ assertTrue(result.contains("xmlns:ns=\"http://example.com/newns2\""));
+ assertTrue(result.contains(" void updatePreferences(List> preferences, Class entity) {
if (Form.class.isAssignableFrom(entity)) {
updatePreferences(preferences, MediaType.APPLICATION_WWW_FORM, 1.0F);
+ } else if (String.class.isAssignableFrom(entity) || Reader.class.isAssignableFrom(entity)) {
+ updatePreferences(preferences, TEXT_PLAIN, 1.0F);
+ updatePreferences(preferences, MediaType.TEXT_ALL, 0.5F);
} else if (Serializable.class.isAssignableFrom(entity)) {
if (ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED) {
updatePreferences(preferences, MediaType.APPLICATION_JAVA_OBJECT, 1.0F);
@@ -296,9 +299,6 @@ public void updatePreferences(List> preferences, Class
if (ObjectRepresentation.VARIANT_OBJECT_XML_SUPPORTED) {
updatePreferences(preferences, MediaType.APPLICATION_JAVA_OBJECT_XML, 1.0F);
}
- } else if (String.class.isAssignableFrom(entity) || Reader.class.isAssignableFrom(entity)) {
- updatePreferences(preferences, TEXT_PLAIN, 1.0F);
- updatePreferences(preferences, MediaType.TEXT_ALL, 0.5F);
} else if (InputStream.class.isAssignableFrom(entity)) {
updatePreferences(preferences, APPLICATION_OCTET_STREAM, 1.0F);
updatePreferences(preferences, MediaType.APPLICATION_ALL, 0.5F);
diff --git a/org.restlet/src/main/java/org/restlet/engine/header/HeaderReader.java b/org.restlet/src/main/java/org/restlet/engine/header/HeaderReader.java
index 02eba3f670..b5346e955f 100644
--- a/org.restlet/src/main/java/org/restlet/engine/header/HeaderReader.java
+++ b/org.restlet/src/main/java/org/restlet/engine/header/HeaderReader.java
@@ -387,13 +387,14 @@ public String readComment() throws IOException {
while (result == null) {
next = read();
- if (isCommentText(next)) {
- buffer.append((char) next);
- } else if (isQuoteCharacter(next)) {
+ if (isQuoteCharacter(next)) {
// Start of a quoted pair (escape sequence)
buffer.append((char) read());
+ } else if (isCommentText(next)) {
+ buffer.append((char) next);
} else if (next == '(') {
// Nested comment
+ unread();
buffer.append('(').append(readComment()).append(')');
} else if (next == ')') {
// End of comment
@@ -490,11 +491,11 @@ public String readQuotedString() throws IOException {
while (result == null) {
next = read();
- if (isQuotedText(next)) {
- buffer.append((char) next);
- } else if (isQuoteCharacter(next)) {
+ if (isQuoteCharacter(next)) {
// Start of a quoted pair (escape sequence)
buffer.append((char) read());
+ } else if (isQuotedText(next)) {
+ buffer.append((char) next);
} else if (isDoubleQuote(next)) {
// End of quoted string
result = buffer.toString();
diff --git a/org.restlet/src/main/java/org/restlet/engine/header/WarningWriter.java b/org.restlet/src/main/java/org/restlet/engine/header/WarningWriter.java
index 1d1fb4d130..2402cbf37b 100644
--- a/org.restlet/src/main/java/org/restlet/engine/header/WarningWriter.java
+++ b/org.restlet/src/main/java/org/restlet/engine/header/WarningWriter.java
@@ -54,6 +54,7 @@ public WarningWriter append(Warning warning) {
appendQuotedString(text);
if (warning.getDate() != null) {
+ append(" ");
appendQuotedString(DateUtils.format(warning.getDate()));
}
diff --git a/org.restlet/src/main/java/org/restlet/representation/Variant.java b/org.restlet/src/main/java/org/restlet/representation/Variant.java
index d6d14a780f..f68650032b 100644
--- a/org.restlet/src/main/java/org/restlet/representation/Variant.java
+++ b/org.restlet/src/main/java/org/restlet/representation/Variant.java
@@ -199,8 +199,7 @@ public MediaType getMediaType() {
@Override
public int hashCode() {
- return SystemUtils.hashCode(
- super.hashCode(), characterSet, encodings, locationRef, languages, mediaType);
+ return SystemUtils.hashCode(characterSet, encodings, locationRef, languages, mediaType);
}
/**
diff --git a/org.restlet/src/main/java/org/restlet/util/NonNullItemsList.java b/org.restlet/src/main/java/org/restlet/util/NonNullItemsList.java
index 34b300cc21..99b2c939fb 100644
--- a/org.restlet/src/main/java/org/restlet/util/NonNullItemsList.java
+++ b/org.restlet/src/main/java/org/restlet/util/NonNullItemsList.java
@@ -62,8 +62,9 @@ public boolean addAll(final int index, final Collection extends T> elements) {
@Override
public boolean equals(final Object obj) {
- return super.equals(obj)
- && Objects.equals(exceptionMessage, ((NonNullItemsList>) obj).exceptionMessage);
+ return (obj instanceof NonNullItemsList> that)
+ && super.equals(obj)
+ && Objects.equals(exceptionMessage, that.exceptionMessage);
}
@Override
diff --git a/org.restlet/src/test/java/org/restlet/data/CacheDirectiveTestCase.java b/org.restlet/src/test/java/org/restlet/data/CacheDirectiveTestCase.java
new file mode 100644
index 0000000000..081ea717a2
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/CacheDirectiveTestCase.java
@@ -0,0 +1,214 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.CacheDirective}. */
+class CacheDirectiveTestCase {
+
+ @Test
+ void maxAge_setsDigitValue() {
+ CacheDirective directive = CacheDirective.maxAge(3600);
+ assertEquals("max-age", directive.getName());
+ assertEquals("3600", directive.getValue());
+ assertTrue(directive.isDigit());
+ }
+
+ @Test
+ void maxStale_noArgument_hasNullValue() {
+ CacheDirective directive = CacheDirective.maxStale();
+ assertEquals("max-stale", directive.getName());
+ assertNull(directive.getValue());
+ assertFalse(directive.isDigit());
+ }
+
+ @Test
+ void maxStale_withArgument_setsDigitValue() {
+ CacheDirective directive = CacheDirective.maxStale(120);
+ assertEquals("max-stale", directive.getName());
+ assertEquals("120", directive.getValue());
+ assertTrue(directive.isDigit());
+ }
+
+ @Test
+ void minFresh_setsDigitValue() {
+ CacheDirective directive = CacheDirective.minFresh(30);
+ assertEquals("min-fresh", directive.getName());
+ assertEquals("30", directive.getValue());
+ assertTrue(directive.isDigit());
+ }
+
+ @Test
+ void mustRevalidate_hasNoValue() {
+ CacheDirective directive = CacheDirective.mustRevalidate();
+ assertEquals("must-revalidate", directive.getName());
+ assertNull(directive.getValue());
+ }
+
+ @Test
+ void noCache_noArgument_hasNoValue() {
+ CacheDirective directive = CacheDirective.noCache();
+ assertEquals("no-cache", directive.getName());
+ assertNull(directive.getValue());
+ }
+
+ @Test
+ void noCache_withFieldName_quotesTheName() {
+ CacheDirective directive = CacheDirective.noCache("X-Custom");
+ assertEquals("no-cache", directive.getName());
+ assertEquals("\"X-Custom\"", directive.getValue());
+ }
+
+ @Test
+ void noCache_withFieldNames_joinsWithComma() {
+ CacheDirective directive = CacheDirective.noCache(Arrays.asList("A", "B"));
+ assertEquals("\"A\",\"B\"", directive.getValue());
+ }
+
+ @Test
+ void noCache_withNullFieldNames_producesEmptyValue() {
+ CacheDirective directive = CacheDirective.noCache((java.util.List) null);
+ assertEquals("", directive.getValue());
+ }
+
+ @Test
+ void noStore_hasNoValue() {
+ CacheDirective directive = CacheDirective.noStore();
+ assertEquals("no-store", directive.getName());
+ assertNull(directive.getValue());
+ }
+
+ @Test
+ void noTransform_hasNoValue() {
+ CacheDirective directive = CacheDirective.noTransform();
+ assertEquals("no-transform", directive.getName());
+ }
+
+ @Test
+ void onlyIfCached_hasNoValue() {
+ CacheDirective directive = CacheDirective.onlyIfCached();
+ assertEquals("only-if-cached", directive.getName());
+ }
+
+ @Test
+ void privateInfo_noArgument_hasNoValue() {
+ CacheDirective directive = CacheDirective.privateInfo();
+ assertEquals("private", directive.getName());
+ assertNull(directive.getValue());
+ }
+
+ @Test
+ void privateInfo_withFieldName_quotesTheName() {
+ CacheDirective directive = CacheDirective.privateInfo("X-Custom");
+ assertEquals("private", directive.getName());
+ assertEquals("\"X-Custom\"", directive.getValue());
+ }
+
+ @Test
+ void privateInfo_withFieldNames_joinsWithComma() {
+ CacheDirective directive = CacheDirective.privateInfo(Arrays.asList("A", "B"));
+ assertEquals("\"A\",\"B\"", directive.getValue());
+ }
+
+ @Test
+ void proxyMustRevalidate_hasNoValue() {
+ CacheDirective directive = CacheDirective.proxyMustRevalidate();
+ assertEquals("proxy-revalidate", directive.getName());
+ }
+
+ @Test
+ void publicInfo_hasNoValue() {
+ CacheDirective directive = CacheDirective.publicInfo();
+ assertEquals("public", directive.getName());
+ }
+
+ @Test
+ void sharedMaxAge_setsDigitValue() {
+ CacheDirective directive = CacheDirective.sharedMaxAge(600);
+ assertEquals("s-maxage", directive.getName());
+ assertEquals("600", directive.getValue());
+ assertTrue(directive.isDigit());
+ }
+
+ @Test
+ void constructor_withNameOnly_hasNoValueAndNotDigit() {
+ CacheDirective directive = new CacheDirective("custom");
+ assertEquals("custom", directive.getName());
+ assertNull(directive.getValue());
+ assertFalse(directive.isDigit());
+ }
+
+ @Test
+ void constructor_withNameAndValue_isNotDigitByDefault() {
+ CacheDirective directive = new CacheDirective("custom", "value");
+ assertEquals("value", directive.getValue());
+ assertFalse(directive.isDigit());
+ }
+
+ @Test
+ void settersUpdateState() {
+ CacheDirective directive = new CacheDirective("custom", "value");
+ directive.setName("other");
+ directive.setValue("otherValue");
+ directive.setDigit(true);
+ assertEquals("other", directive.getName());
+ assertEquals("otherValue", directive.getValue());
+ assertTrue(directive.isDigit());
+ }
+
+ @Test
+ void equals_sameNameValueDigit_returnsTrue() {
+ CacheDirective d1 = new CacheDirective("max-age", "10", true);
+ CacheDirective d2 = new CacheDirective("max-age", "10", true);
+ assertEquals(d1, d2);
+ assertEquals(d1.hashCode(), d2.hashCode());
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ CacheDirective directive = CacheDirective.noStore();
+ assertEquals(directive, directive);
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ CacheDirective directive = CacheDirective.noStore();
+ assertNotEquals(directive, "no-store");
+ }
+
+ @Test
+ void equals_differentDigitFlag_returnsFalse() {
+ CacheDirective d1 = new CacheDirective("max-age", "10", true);
+ CacheDirective d2 = new CacheDirective("max-age", "10", false);
+ assertNotEquals(d1, d2);
+ }
+
+ @Test
+ void equals_differentValue_returnsFalse() {
+ CacheDirective d1 = CacheDirective.maxAge(10);
+ CacheDirective d2 = CacheDirective.maxAge(20);
+ assertNotEquals(d1, d2);
+ }
+
+ @Test
+ void toString_containsFields() {
+ CacheDirective directive = new CacheDirective("no-store", null, false);
+ String result = directive.toString();
+ assertTrue(result.contains("no-store"));
+ assertTrue(result.contains("digit=false"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ChallengeMessageTestCase.java b/org.restlet/src/test/java/org/restlet/data/ChallengeMessageTestCase.java
new file mode 100644
index 0000000000..f235330093
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ChallengeMessageTestCase.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+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 org.junit.jupiter.api.Test;
+import org.restlet.util.Series;
+
+/**
+ * Test {@link org.restlet.data.ChallengeMessage}, exercised through the concrete {@link
+ * ChallengeRequest} subclass since ChallengeMessage is abstract.
+ */
+class ChallengeMessageTestCase {
+
+ @Test
+ void constructor_withSchemeOnly_hasNullRealmAndDigestAlgorithm() {
+ ChallengeMessage message = new ChallengeRequest(ChallengeScheme.HTTP_BASIC);
+ assertEquals(ChallengeScheme.HTTP_BASIC, message.getScheme());
+ assertNull(message.getRealm());
+ assertEquals(Digest.ALGORITHM_MD5, message.getDigestAlgorithm());
+ assertNull(message.getOpaque());
+ assertNull(message.getServerNonce());
+ assertNull(message.getRawValue());
+ }
+
+ @Test
+ void constructor_withSchemeAndRealm() {
+ ChallengeMessage message = new ChallengeRequest(ChallengeScheme.HTTP_BASIC, "myRealm");
+ assertEquals("myRealm", message.getRealm());
+ }
+
+ @Test
+ void getParameters_lazilyCreatesEmptySeries() {
+ ChallengeMessage message = new ChallengeRequest(ChallengeScheme.HTTP_BASIC);
+ assertNotNull(message.getParameters());
+ assertEquals(0, message.getParameters().size());
+ }
+
+ @Test
+ void setParameters_replacesSeries() {
+ ChallengeMessage message = new ChallengeRequest(ChallengeScheme.HTTP_BASIC);
+ Series parameters = new Series<>(Parameter.class);
+ parameters.add("k", "v");
+ message.setParameters(parameters);
+ assertSame(parameters, message.getParameters());
+ }
+
+ @Test
+ void settersUpdateState() {
+ ChallengeMessage message = new ChallengeRequest(ChallengeScheme.HTTP_BASIC);
+ message.setDigestAlgorithm(Digest.ALGORITHM_SHA_256);
+ message.setOpaque("opaque-value");
+ message.setRawValue("raw-value");
+ message.setRealm("realm");
+ message.setScheme(ChallengeScheme.HTTP_DIGEST);
+ message.setServerNonce("nonce");
+
+ assertEquals(Digest.ALGORITHM_SHA_256, message.getDigestAlgorithm());
+ assertEquals("opaque-value", message.getOpaque());
+ assertEquals("raw-value", message.getRawValue());
+ assertEquals("realm", message.getRealm());
+ assertEquals(ChallengeScheme.HTTP_DIGEST, message.getScheme());
+ assertEquals("nonce", message.getServerNonce());
+ }
+
+ @Test
+ void equals_sameSchemeRealmAndParameters_returnsTrue() {
+ ChallengeMessage m1 = new ChallengeRequest(ChallengeScheme.HTTP_BASIC, "realm");
+ ChallengeMessage m2 = new ChallengeRequest(ChallengeScheme.HTTP_BASIC, "realm");
+ assertEquals(m1, m2);
+ assertEquals(m1.hashCode(), m2.hashCode());
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ ChallengeMessage message = new ChallengeRequest(ChallengeScheme.HTTP_BASIC);
+ assertEquals(message, message);
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ ChallengeMessage message = new ChallengeRequest(ChallengeScheme.HTTP_BASIC);
+ assertNotEquals(message, "not a challenge message");
+ }
+
+ @Test
+ void equals_differentRealm_returnsFalse() {
+ ChallengeMessage m1 = new ChallengeRequest(ChallengeScheme.HTTP_BASIC, "realm1");
+ ChallengeMessage m2 = new ChallengeRequest(ChallengeScheme.HTTP_BASIC, "realm2");
+ assertNotEquals(m1, m2);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ChallengeRequestTestCase.java b/org.restlet/src/test/java/org/restlet/data/ChallengeRequestTestCase.java
new file mode 100644
index 0000000000..02b93da36d
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ChallengeRequestTestCase.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.ChallengeRequest}. */
+class ChallengeRequestTestCase {
+
+ @Test
+ void constructor_withSchemeOnly_hasDefaults() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ assertEquals(ChallengeScheme.HTTP_DIGEST, request.getScheme());
+ assertFalse(request.isStale());
+ assertNull(request.getRealm());
+ }
+
+ @Test
+ void constructor_withRealm() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST, "myRealm");
+ assertEquals("myRealm", request.getRealm());
+ }
+
+ @Test
+ void getDomainRefs_defaultsToRootReference() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ List refs = request.getDomainRefs();
+ assertEquals(1, refs.size());
+ assertEquals("/", refs.get(0).toString());
+ }
+
+ @Test
+ void setDomainRefs_replacesList() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ request.setDomainRefs(Arrays.asList(new Reference("/a"), new Reference("/b")));
+ assertEquals(2, request.getDomainRefs().size());
+ }
+
+ @Test
+ void setDomainUris_convertsStringsToReferences() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ request.setDomainUris(Arrays.asList("/a", "/b"));
+ List refs = request.getDomainRefs();
+ assertEquals(2, refs.size());
+ assertEquals("/a", refs.get(0).toString());
+ assertEquals("/b", refs.get(1).toString());
+ }
+
+ @Test
+ void setDomainUris_withNull_setsNullDomainRefs() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ request.setDomainUris(Arrays.asList("/a"));
+ request.setDomainUris(null);
+ // getDomainRefs() re-initializes the lazy default when null.
+ assertEquals(1, request.getDomainRefs().size());
+ assertEquals("/", request.getDomainRefs().get(0).toString());
+ }
+
+ @Test
+ void getQualityOptions_defaultsToAuthentication() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ List options = request.getQualityOptions();
+ assertEquals(1, options.size());
+ assertEquals(ChallengeMessage.QUALITY_AUTHENTICATION, options.get(0));
+ }
+
+ @Test
+ void setQualityOptions_replacesList() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ request.setQualityOptions(Arrays.asList(ChallengeMessage.QUALITY_AUTHENTICATION_INTEGRITY));
+ assertEquals(1, request.getQualityOptions().size());
+ assertEquals(
+ ChallengeMessage.QUALITY_AUTHENTICATION_INTEGRITY,
+ request.getQualityOptions().get(0));
+ }
+
+ @Test
+ void setStale_updatesFlag() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ request.setStale(true);
+ assertTrue(request.isStale());
+ }
+
+ @Test
+ void equals_sameSchemeRealmAndParameters_returnsTrue() {
+ ChallengeRequest r1 = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST, "realm");
+ ChallengeRequest r2 = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST, "realm");
+ assertEquals(r1, r2);
+ assertEquals(r1.hashCode(), r2.hashCode());
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ assertEquals(request, request);
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ ChallengeRequest request = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ assertNotEquals(request, "not a challenge request");
+ }
+
+ @Test
+ void equals_differentRealm_returnsFalse() {
+ ChallengeRequest r1 = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST, "realm1");
+ ChallengeRequest r2 = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST, "realm2");
+ assertNotEquals(r1, r2);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ChallengeResponseTestCase.java b/org.restlet/src/test/java/org/restlet/data/ChallengeResponseTestCase.java
new file mode 100644
index 0000000000..783adb9ef7
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ChallengeResponseTestCase.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.ChallengeResponse}. */
+class ChallengeResponseTestCase {
+
+ @Test
+ void constructor_withSchemeOnly_hasNoCredentials() {
+ ChallengeResponse response = new ChallengeResponse(ChallengeScheme.HTTP_BASIC);
+ assertEquals(ChallengeScheme.HTTP_BASIC, response.getScheme());
+ assertNull(response.getIdentifier());
+ assertNull(response.getSecret());
+ }
+
+ @Test
+ void constructor_withIdentifierAndCharArraySecret() {
+ ChallengeResponse response =
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass".toCharArray());
+ assertEquals("user", response.getIdentifier());
+ assertArrayEquals("pass".toCharArray(), response.getSecret());
+ }
+
+ @Test
+ void constructor_withIdentifierAndStringSecret() {
+ ChallengeResponse response =
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass");
+ assertEquals("user", response.getIdentifier());
+ assertArrayEquals("pass".toCharArray(), response.getSecret());
+ }
+
+ @Test
+ void constructor_withNullStringSecret_hasNullSecret() {
+ ChallengeResponse response =
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", (String) null);
+ assertNull(response.getSecret());
+ }
+
+ @Test
+ void getPrincipal_returnsIdentifierBackedPrincipal() {
+ ChallengeResponse response =
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass");
+ assertEquals("user", response.getPrincipal().getName());
+ }
+
+ @Test
+ void settersUpdateState() {
+ ChallengeResponse response = new ChallengeResponse(ChallengeScheme.HTTP_BASIC);
+ Reference digestRef = new Reference("http://example.com/resource");
+ response.setClientNonce("clientNonce");
+ response.setDigestRef(digestRef);
+ response.setIdentifier("user");
+ response.setQuality("auth");
+ response.setSecret("secret");
+ response.setSecretAlgorithm(Digest.ALGORITHM_MD5);
+ response.setServerNonceCount(5);
+ response.setTimeIssued(12345L);
+
+ assertEquals("clientNonce", response.getClientNonce());
+ assertEquals(digestRef, response.getDigestRef());
+ assertEquals("user", response.getIdentifier());
+ assertEquals("auth", response.getQuality());
+ assertArrayEquals("secret".toCharArray(), response.getSecret());
+ assertEquals(Digest.ALGORITHM_MD5, response.getSecretAlgorithm());
+ assertEquals(5, response.getServerNonceCount());
+ assertEquals(12345L, response.getTimeIssued());
+ }
+
+ @Test
+ void getServerNonceCountAsHex_formatsAsEightHexCharacters() {
+ ChallengeResponse response = new ChallengeResponse(ChallengeScheme.HTTP_BASIC);
+ response.setServerNonceCount(255);
+ assertEquals("000000ff", response.getServerNonceCountAsHex());
+ }
+
+ @Test
+ void equals_sameRawValueIdentifierSchemeAndSecret_returnsTrue() {
+ ChallengeResponse r1 = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass");
+ ChallengeResponse r2 = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass");
+ assertEquals(r1, r2);
+ assertEquals(r1.hashCode(), r2.hashCode());
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ ChallengeResponse response =
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass");
+ assertEquals(response, response);
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ ChallengeResponse response =
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass");
+ assertNotEquals(response, "not a challenge response");
+ }
+
+ @Test
+ void equals_differentIdentifier_returnsFalse() {
+ ChallengeResponse r1 = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user1", "pass");
+ ChallengeResponse r2 = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user2", "pass");
+ assertNotEquals(r1, r2);
+ }
+
+ @Test
+ void equals_differentSecret_returnsFalse() {
+ ChallengeResponse r1 = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass1");
+ ChallengeResponse r2 = new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "user", "pass2");
+ assertNotEquals(r1, r2);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ChallengeSchemeTestCase.java b/org.restlet/src/test/java/org/restlet/data/ChallengeSchemeTestCase.java
new file mode 100644
index 0000000000..0cebb4cbe6
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ChallengeSchemeTestCase.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.ChallengeScheme}. */
+class ChallengeSchemeTestCase {
+
+ @Test
+ void valueOf_knownName_returnsSharedConstant() {
+ assertSame(ChallengeScheme.HTTP_BASIC, ChallengeScheme.valueOf("HTTP_BASIC"));
+ }
+
+ @Test
+ void valueOf_isCaseInsensitive() {
+ assertSame(ChallengeScheme.HTTP_BASIC, ChallengeScheme.valueOf("http_basic"));
+ }
+
+ @Test
+ void valueOf_unknownName_createsNewInstance() {
+ ChallengeScheme scheme = ChallengeScheme.valueOf("CUSTOM_SCHEME");
+ assertEquals("CUSTOM_SCHEME", scheme.getName());
+ assertNull(scheme.getTechnicalName());
+ assertNull(scheme.getDescription());
+ }
+
+ @Test
+ void valueOf_nullName_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> ChallengeScheme.valueOf(null));
+ }
+
+ @Test
+ void constructor_withNameAndTechnicalName() {
+ ChallengeScheme scheme = new ChallengeScheme("SCHEME", "TECH");
+ assertEquals("SCHEME", scheme.getName());
+ assertEquals("TECH", scheme.getTechnicalName());
+ assertNull(scheme.getDescription());
+ }
+
+ @Test
+ void constructor_withDescription() {
+ ChallengeScheme scheme = new ChallengeScheme("SCHEME", "TECH", "A description");
+ assertEquals("A description", scheme.getDescription());
+ }
+
+ @Test
+ void equals_isCaseInsensitiveOnName() {
+ ChallengeScheme scheme1 = new ChallengeScheme("scheme", "tech");
+ ChallengeScheme scheme2 = new ChallengeScheme("SCHEME", "other");
+ assertEquals(scheme1, scheme2);
+ assertEquals(scheme1.hashCode(), scheme2.hashCode());
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ ChallengeScheme scheme = ChallengeScheme.HTTP_BASIC;
+ assertNotEquals(scheme, "Basic");
+ }
+
+ @Test
+ void equals_differentName_returnsFalse() {
+ assertNotEquals(ChallengeScheme.HTTP_BASIC, ChallengeScheme.HTTP_DIGEST);
+ }
+
+ @Test
+ void toString_returnsName() {
+ assertEquals(ChallengeScheme.HTTP_BASIC.getName(), ChallengeScheme.HTTP_BASIC.toString());
+ }
+
+ @Test
+ void hashCode_isCaseInsensitive() {
+ ChallengeScheme scheme1 = new ChallengeScheme("scheme", "tech");
+ ChallengeScheme scheme2 = new ChallengeScheme("SCHEME", "tech");
+ assertEquals(scheme1.hashCode(), scheme2.hashCode());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/CharacterSetTestCase.java b/org.restlet/src/test/java/org/restlet/data/CharacterSetTestCase.java
new file mode 100644
index 0000000000..b91c1499b2
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/CharacterSetTestCase.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+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 org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.CharacterSet}. */
+class CharacterSetTestCase {
+
+ @Test
+ void constructor_withName_uppercasesName() {
+ CharacterSet characterSet = new CharacterSet("utf-8");
+ assertEquals("UTF-8", characterSet.getName());
+ }
+
+ @Test
+ void constructor_withNameAndDescription() {
+ CharacterSet characterSet = new CharacterSet("MY-CHARSET", "a description");
+ assertEquals("MY-CHARSET", characterSet.getName());
+ assertEquals("a description", characterSet.getDescription());
+ }
+
+ @Test
+ void constructor_withJavaCharset_usesCharsetNameAndDisplayName() {
+ java.nio.charset.Charset charset = java.nio.charset.Charset.forName("UTF-8");
+ CharacterSet characterSet = new CharacterSet(charset);
+ assertEquals(charset.name(), characterSet.getName());
+ assertEquals(charset.displayName(), characterSet.getDescription());
+ }
+
+ @Test
+ void valueOf_returnsSharedConstant() {
+ assertSame(CharacterSet.UTF_8, CharacterSet.valueOf("UTF-8"));
+ assertSame(CharacterSet.ISO_8859_1, CharacterSet.valueOf("ISO-8859-1"));
+ assertSame(CharacterSet.US_ASCII, CharacterSet.valueOf("US-ASCII"));
+ assertSame(CharacterSet.ALL, CharacterSet.valueOf("*"));
+ }
+
+ @Test
+ void valueOf_resolvesAliasNames() {
+ assertSame(CharacterSet.US_ASCII, CharacterSet.valueOf("ASCII"));
+ assertSame(CharacterSet.ISO_8859_1, CharacterSet.valueOf("latin1"));
+ assertSame(CharacterSet.MACINTOSH, CharacterSet.valueOf("MACROMAN"));
+ // "arabic" resolves (via the IANA name mapping) to the ISO-8859-6 name,
+ // but valueOf() only returns the shared singleton for a subset of
+ // constants, so a new (but equals()-equivalent) instance is created.
+ assertEquals(CharacterSet.ISO_8859_6, CharacterSet.valueOf("arabic"));
+ }
+
+ @Test
+ void valueOf_unknownName_createsNewInstance() {
+ CharacterSet characterSet = CharacterSet.valueOf("CUSTOM-CHARSET");
+ assertEquals("CUSTOM-CHARSET", characterSet.getName());
+ }
+
+ @Test
+ void valueOf_nullOrEmptyName_returnsNull() {
+ assertNull(CharacterSet.valueOf((String) null));
+ assertNull(CharacterSet.valueOf(""));
+ }
+
+ @Test
+ void equals_isCaseInsensitive() {
+ CharacterSet cs1 = new CharacterSet("utf-8");
+ CharacterSet cs2 = new CharacterSet("UTF-8");
+ assertEquals(cs1, cs2);
+ assertEquals(cs1.hashCode(), cs2.hashCode());
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ assertNotEquals(CharacterSet.UTF_8, "UTF-8");
+ }
+
+ @Test
+ void getParent_forAll_returnsNull() {
+ assertNull(CharacterSet.ALL.getParent());
+ }
+
+ @Test
+ void getParent_forSpecificCharset_returnsAll() {
+ assertEquals(CharacterSet.ALL, CharacterSet.UTF_8.getParent());
+ }
+
+ @Test
+ void includes_allIncludesEverything() {
+ assertTrue(CharacterSet.ALL.includes(CharacterSet.UTF_8));
+ assertFalse(CharacterSet.UTF_8.includes(CharacterSet.ALL));
+ }
+
+ @Test
+ void includes_nullIsAlwaysIncluded() {
+ assertTrue(CharacterSet.UTF_8.includes(null));
+ }
+
+ @Test
+ void includes_sameCharacterSet_returnsTrue() {
+ assertTrue(CharacterSet.UTF_8.includes(CharacterSet.UTF_8));
+ }
+
+ @Test
+ void isCompatible_reflexiveAndSymmetricViaAll() {
+ assertTrue(CharacterSet.UTF_8.isCompatible(CharacterSet.ALL));
+ assertTrue(CharacterSet.ALL.isCompatible(CharacterSet.UTF_8));
+ assertFalse(CharacterSet.UTF_8.isCompatible(CharacterSet.ISO_8859_1));
+ }
+
+ @Test
+ void toCharset_returnsMatchingNioCharset() {
+ assertEquals(java.nio.charset.StandardCharsets.UTF_8, CharacterSet.UTF_8.toCharset());
+ }
+
+ @Test
+ void toString_returnsName() {
+ assertEquals("UTF-8", CharacterSet.UTF_8.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ConditionsTestCase.java b/org.restlet/src/test/java/org/restlet/data/ConditionsTestCase.java
new file mode 100644
index 0000000000..8a9b2ed84b
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ConditionsTestCase.java
@@ -0,0 +1,188 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+import java.util.Date;
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.Conditions}. */
+class ConditionsTestCase {
+
+ @Test
+ void newInstance_hasNoConditions() {
+ Conditions conditions = new Conditions();
+ assertFalse(conditions.hasSome());
+ assertFalse(conditions.hasSomeRange());
+ assertTrue(conditions.getMatch().isEmpty());
+ assertTrue(conditions.getNoneMatch().isEmpty());
+ assertNull(conditions.getModifiedSince());
+ assertNull(conditions.getUnmodifiedSince());
+ assertNull(conditions.getRangeDate());
+ assertNull(conditions.getRangeTag());
+ }
+
+ @Test
+ void setMatch_marksConditionsAsPresent() {
+ Conditions conditions = new Conditions();
+ conditions.setMatch(Collections.singletonList(Tag.ALL));
+ assertTrue(conditions.hasSome());
+ assertEquals(1, conditions.getMatch().size());
+ }
+
+ @Test
+ void setNoneMatch_marksConditionsAsPresent() {
+ Conditions conditions = new Conditions();
+ conditions.setNoneMatch(Collections.singletonList(new Tag("abc")));
+ assertTrue(conditions.hasSome());
+ }
+
+ @Test
+ void setModifiedSince_marksConditionsAsPresent() {
+ Conditions conditions = new Conditions();
+ conditions.setModifiedSince(new Date());
+ assertTrue(conditions.hasSome());
+ assertNotNull(conditions.getModifiedSince());
+ }
+
+ @Test
+ void setUnmodifiedSince_marksConditionsAsPresent() {
+ Conditions conditions = new Conditions();
+ conditions.setUnmodifiedSince(new Date());
+ assertTrue(conditions.hasSome());
+ assertNotNull(conditions.getUnmodifiedSince());
+ }
+
+ @Test
+ void setRangeTag_marksRangeConditionsAsPresent() {
+ Conditions conditions = new Conditions();
+ conditions.setRangeTag(new Tag("abc"));
+ assertTrue(conditions.hasSomeRange());
+ }
+
+ @Test
+ void setRangeDate_marksRangeConditionsAsPresent() {
+ Conditions conditions = new Conditions();
+ conditions.setRangeDate(new Date());
+ assertTrue(conditions.hasSomeRange());
+ }
+
+ @Test
+ void getRangeStatus_matchingTag_returnsOk() {
+ Conditions conditions = new Conditions();
+ Tag tag = new Tag("abc");
+ conditions.setRangeTag(tag);
+ assertEquals(Status.SUCCESS_OK, conditions.getRangeStatus(tag, null));
+ }
+
+ @Test
+ void getRangeStatus_allTag_returnsOk() {
+ Conditions conditions = new Conditions();
+ conditions.setRangeTag(Tag.ALL);
+ assertEquals(Status.SUCCESS_OK, conditions.getRangeStatus(new Tag("anything"), null));
+ }
+
+ @Test
+ void getRangeStatus_nonMatchingTag_returnsPreconditionFailed() {
+ Conditions conditions = new Conditions();
+ conditions.setRangeTag(new Tag("abc"));
+ assertEquals(
+ Status.CLIENT_ERROR_PRECONDITION_FAILED,
+ conditions.getRangeStatus(new Tag("other"), null));
+ }
+
+ @Test
+ void getRangeStatus_matchingDate_returnsOk() {
+ Conditions conditions = new Conditions();
+ Date date = new Date(1000);
+ conditions.setRangeDate(date);
+ assertEquals(Status.SUCCESS_OK, conditions.getRangeStatus(null, date));
+ }
+
+ @Test
+ void getRangeStatus_noConditionsSet_returnsPreconditionFailed() {
+ Conditions conditions = new Conditions();
+ assertEquals(
+ Status.CLIENT_ERROR_PRECONDITION_FAILED, conditions.getRangeStatus(null, null));
+ }
+
+ @Test
+ void getStatus_ifMatchNotSatisfied_returnsPreconditionFailed() {
+ Conditions conditions = new Conditions();
+ conditions.setMatch(Collections.singletonList(new Tag("expected")));
+ Status status = conditions.getStatus(Method.GET, true, new Tag("actual"), null);
+ assertEquals(Status.CLIENT_ERROR_PRECONDITION_FAILED, status);
+ }
+
+ @Test
+ void getStatus_ifMatchSatisfied_returnsNull() {
+ Conditions conditions = new Conditions();
+ Tag tag = new Tag("abc");
+ conditions.setMatch(Collections.singletonList(tag));
+ Status status = conditions.getStatus(Method.GET, true, tag, null);
+ assertNull(status);
+ }
+
+ @Test
+ void getStatus_ifMatchStarWithNoEntity_returnsPreconditionFailed() {
+ Conditions conditions = new Conditions();
+ conditions.setMatch(Collections.singletonList(Tag.ALL));
+ Status status = conditions.getStatus(Method.GET, false, null, null);
+ assertEquals(Status.CLIENT_ERROR_PRECONDITION_FAILED.getCode(), status.getCode());
+ }
+
+ @Test
+ void getStatus_ifNoneMatchSatisfiedOnGet_returnsNotModified() {
+ Conditions conditions = new Conditions();
+ Tag tag = new Tag("abc");
+ conditions.setNoneMatch(Collections.singletonList(tag));
+ Status status = conditions.getStatus(Method.GET, true, tag, null);
+ assertEquals(Status.REDIRECTION_NOT_MODIFIED, status);
+ }
+
+ @Test
+ void getStatus_ifNoneMatchSatisfiedOnPut_returnsPreconditionFailed() {
+ Conditions conditions = new Conditions();
+ Tag tag = new Tag("abc");
+ conditions.setNoneMatch(Collections.singletonList(tag));
+ Status status = conditions.getStatus(Method.PUT, true, tag, null);
+ assertEquals(Status.CLIENT_ERROR_PRECONDITION_FAILED, status);
+ }
+
+ @Test
+ void getStatus_ifModifiedSinceInFuture_returnsNull() {
+ Conditions conditions = new Conditions();
+ Date future = new Date(System.currentTimeMillis() + 1_000_000_000L);
+ conditions.setModifiedSince(future);
+ Status status = conditions.getStatus(Method.GET, true, null, new Date());
+ assertNull(status);
+ }
+
+ @Test
+ void getStatus_ifUnmodifiedSinceViolated_returnsPreconditionFailed() {
+ Conditions conditions = new Conditions();
+ Date past = new Date(1000);
+ conditions.setUnmodifiedSince(past);
+ Date modificationDate = new Date(2000);
+ Status status = conditions.getStatus(Method.GET, true, null, modificationDate);
+ assertEquals(Status.CLIENT_ERROR_PRECONDITION_FAILED, status);
+ }
+
+ @Test
+ void getStatus_noConditions_returnsNull() {
+ Conditions conditions = new Conditions();
+ assertNull(conditions.getStatus(Method.GET, true, new Tag("abc"), new Date()));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/CookieSettingTestCase.java b/org.restlet/src/test/java/org/restlet/data/CookieSettingTestCase.java
new file mode 100644
index 0000000000..b95ded8f41
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/CookieSettingTestCase.java
@@ -0,0 +1,155 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.CookieSetting}. */
+class CookieSettingTestCase {
+
+ @Test
+ void defaultConstructor_hasDefaultValues() {
+ CookieSetting cookie = new CookieSetting();
+ assertEquals(0, cookie.getVersion());
+ assertNull(cookie.getName());
+ assertNull(cookie.getValue());
+ assertEquals(-1, cookie.getMaxAge());
+ assertFalse(cookie.isSecure());
+ assertFalse(cookie.isAccessRestricted());
+ }
+
+ @Test
+ void preferredConstructor_setsNameAndValue() {
+ CookieSetting cookie = new CookieSetting("session", "abc123");
+ assertEquals("session", cookie.getName());
+ assertEquals("abc123", cookie.getValue());
+ assertEquals(0, cookie.getVersion());
+ }
+
+ @Test
+ void constructor_withVersionNameValue() {
+ CookieSetting cookie = new CookieSetting(1, "session", "abc123");
+ assertEquals(1, cookie.getVersion());
+ assertEquals("session", cookie.getName());
+ assertEquals("abc123", cookie.getValue());
+ assertEquals(-1, cookie.getMaxAge());
+ }
+
+ @Test
+ void constructor_withPathAndDomain() {
+ CookieSetting cookie = new CookieSetting(1, "session", "abc123", "/app", "example.com");
+ assertEquals("/app", cookie.getPath());
+ assertEquals("example.com", cookie.getDomain());
+ }
+
+ @Test
+ void constructor_withCommentMaxAgeSecure() {
+ CookieSetting cookie =
+ new CookieSetting(
+ 1, "session", "abc123", "/app", "example.com", "a comment", 3600, true);
+ assertEquals("a comment", cookie.getComment());
+ assertEquals(3600, cookie.getMaxAge());
+ assertTrue(cookie.isSecure());
+ assertFalse(cookie.isAccessRestricted());
+ }
+
+ @Test
+ void constructor_withAccessRestricted() {
+ CookieSetting cookie =
+ new CookieSetting(
+ 1,
+ "session",
+ "abc123",
+ "/app",
+ "example.com",
+ "a comment",
+ 3600,
+ true,
+ true);
+ assertTrue(cookie.isAccessRestricted());
+ }
+
+ @Test
+ void settersUpdateState() {
+ CookieSetting cookie = new CookieSetting();
+ cookie.setComment("comment");
+ cookie.setMaxAge(60);
+ cookie.setSecure(true);
+ cookie.setAccessRestricted(true);
+
+ assertEquals("comment", cookie.getComment());
+ assertEquals(60, cookie.getMaxAge());
+ assertTrue(cookie.isSecure());
+ assertTrue(cookie.isAccessRestricted());
+ }
+
+ @Test
+ void getDescription_returnsFixedText() {
+ assertEquals("Cookie setting", new CookieSetting().getDescription());
+ }
+
+ @Test
+ void equals_sameFields_returnsTrue() {
+ CookieSetting c1 =
+ new CookieSetting(
+ 1, "session", "abc123", "/app", "example.com", "comment", 3600, true);
+ CookieSetting c2 =
+ new CookieSetting(
+ 1, "session", "abc123", "/app", "example.com", "comment", 3600, true);
+ assertEquals(c1, c2);
+ assertEquals(c1.hashCode(), c2.hashCode());
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ CookieSetting cookie = new CookieSetting("session", "abc123");
+ assertEquals(cookie, cookie);
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ CookieSetting cookie = new CookieSetting("session", "abc123");
+ assertNotEquals(cookie, new Cookie("session", "abc123"));
+ }
+
+ @Test
+ void equals_differentMaxAge_returnsFalse() {
+ CookieSetting c1 = new CookieSetting(0, "session", "abc123", null, null, null, 10, false);
+ CookieSetting c2 = new CookieSetting(0, "session", "abc123", null, null, null, 20, false);
+ assertNotEquals(c1, c2);
+ }
+
+ @Test
+ void equals_differentComment_returnsFalse() {
+ CookieSetting c1 = new CookieSetting(0, "session", "abc123", null, null, "a", 10, false);
+ CookieSetting c2 = new CookieSetting(0, "session", "abc123", null, null, "b", 10, false);
+ assertNotEquals(c1, c2);
+ }
+
+ @Test
+ void toString_containsFields() {
+ CookieSetting cookie =
+ new CookieSetting(
+ 1, "session", "abc123", "/app", "example.com", "comment", 3600, true, true);
+ String result = cookie.toString();
+ assertTrue(result.contains("session"));
+ assertTrue(result.contains("abc123"));
+ assertTrue(result.contains("example.com"));
+ assertTrue(result.contains("/app"));
+ assertTrue(result.contains("comment"));
+ assertTrue(result.contains("secure=true"));
+ assertTrue(result.contains("accessRestricted=true"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/DigestTestCase.java b/org.restlet/src/test/java/org/restlet/data/DigestTestCase.java
new file mode 100644
index 0000000000..cc30299095
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/DigestTestCase.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.Digest}. */
+class DigestTestCase {
+
+ @Test
+ void constructor_withValueOnly_usesMd5Algorithm() {
+ Digest digest = new Digest(new byte[] {1, 2, 3});
+ assertEquals(Digest.ALGORITHM_MD5, digest.getAlgorithm());
+ assertArrayEquals(new byte[] {1, 2, 3}, digest.getValue());
+ }
+
+ @Test
+ void constructor_withAlgorithm_setsAlgorithm() {
+ Digest digest = new Digest(Digest.ALGORITHM_SHA_256, new byte[] {4, 5, 6});
+ assertEquals(Digest.ALGORITHM_SHA_256, digest.getAlgorithm());
+ assertArrayEquals(new byte[] {4, 5, 6}, digest.getValue());
+ }
+
+ @Test
+ void getValue_returnsDefensiveCopy() {
+ byte[] source = {1, 2, 3};
+ Digest digest = new Digest(source);
+ source[0] = 99;
+ assertArrayEquals(new byte[] {1, 2, 3}, digest.getValue());
+
+ byte[] returned = digest.getValue();
+ returned[0] = 42;
+ assertArrayEquals(new byte[] {1, 2, 3}, digest.getValue());
+ }
+
+ @Test
+ void equals_sameAlgorithmAndValue_returnsTrue() {
+ Digest digest1 = new Digest(Digest.ALGORITHM_MD5, new byte[] {1, 2, 3});
+ Digest digest2 = new Digest(Digest.ALGORITHM_MD5, new byte[] {1, 2, 3});
+ assertEquals(digest1, digest2);
+ assertEquals(digest1.hashCode(), digest2.hashCode());
+ }
+
+ @Test
+ void equals_differentAlgorithm_returnsFalse() {
+ Digest digest1 = new Digest(Digest.ALGORITHM_MD5, new byte[] {1, 2, 3});
+ Digest digest2 = new Digest(Digest.ALGORITHM_SHA_1, new byte[] {1, 2, 3});
+ assertNotEquals(digest1, digest2);
+ }
+
+ @Test
+ void equals_differentValue_returnsFalse() {
+ Digest digest1 = new Digest(Digest.ALGORITHM_MD5, new byte[] {1, 2, 3});
+ Digest digest2 = new Digest(Digest.ALGORITHM_MD5, new byte[] {9, 9, 9});
+ assertNotEquals(digest1, digest2);
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ Digest digest = new Digest(new byte[] {1});
+ assertNotEquals(digest, "not a digest");
+ }
+
+ @Test
+ void toString_containsAlgorithmAndValue() {
+ Digest digest = new Digest(Digest.ALGORITHM_MD5, new byte[] {1, 2, 3});
+ String result = digest.toString();
+ assertTrue(result.contains(Digest.ALGORITHM_MD5));
+ assertTrue(result.contains("[1, 2, 3]"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/DimensionTestCase.java b/org.restlet/src/test/java/org/restlet/data/DimensionTestCase.java
new file mode 100644
index 0000000000..903f1f704d
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/DimensionTestCase.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.Dimension}. */
+class DimensionTestCase {
+
+ @Test
+ void values_containsAllExpectedConstants() {
+ Dimension[] values = Dimension.values();
+ assertEquals(10, values.length);
+ assertArrayEquals(
+ new Dimension[] {
+ Dimension.AUTHORIZATION,
+ Dimension.CHARACTER_SET,
+ Dimension.CLIENT_ADDRESS,
+ Dimension.CLIENT_AGENT,
+ Dimension.UNSPECIFIED,
+ Dimension.ENCODING,
+ Dimension.LANGUAGE,
+ Dimension.MEDIA_TYPE,
+ Dimension.TIME,
+ Dimension.ORIGIN
+ },
+ values);
+ }
+
+ @Test
+ void valueOf_returnsMatchingConstant() {
+ assertEquals(Dimension.MEDIA_TYPE, Dimension.valueOf("MEDIA_TYPE"));
+ }
+
+ @Test
+ void valueOf_unknownName_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> Dimension.valueOf("UNKNOWN"));
+ }
+
+ @Test
+ void name_returnsDeclaredConstantName() {
+ assertEquals("TIME", Dimension.TIME.name());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/DispositionTestCase.java b/org.restlet/src/test/java/org/restlet/data/DispositionTestCase.java
new file mode 100644
index 0000000000..7cdff40c91
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/DispositionTestCase.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.util.Date;
+import org.junit.jupiter.api.Test;
+import org.restlet.util.Series;
+
+/** Test {@link org.restlet.data.Disposition}. */
+class DispositionTestCase {
+
+ @Test
+ void defaultConstructor_usesNoneType() {
+ Disposition disposition = new Disposition();
+ assertEquals(Disposition.TYPE_NONE, disposition.getType());
+ }
+
+ @Test
+ void constructor_withType() {
+ Disposition disposition = new Disposition(Disposition.TYPE_ATTACHMENT);
+ assertEquals(Disposition.TYPE_ATTACHMENT, disposition.getType());
+ }
+
+ @Test
+ void constructor_withTypeAndParameters() {
+ Series parameters = new Form();
+ parameters.add(Disposition.NAME_FILENAME, "file.txt");
+ Disposition disposition = new Disposition(Disposition.TYPE_ATTACHMENT, parameters);
+ assertEquals("file.txt", disposition.getFilename());
+ }
+
+ @Test
+ void getParameters_lazilyCreatesEmptySeries() {
+ Disposition disposition = new Disposition();
+ assertNotNull(disposition.getParameters());
+ assertEquals(0, disposition.getParameters().size());
+ }
+
+ @Test
+ void setType_updatesType() {
+ Disposition disposition = new Disposition();
+ disposition.setType(Disposition.TYPE_INLINE);
+ assertEquals(Disposition.TYPE_INLINE, disposition.getType());
+ }
+
+ @Test
+ void setFilename_andGetFilename_roundTrip() {
+ Disposition disposition = new Disposition();
+ disposition.setFilename("report.pdf");
+ assertEquals("report.pdf", disposition.getFilename());
+ }
+
+ @Test
+ void setSize_storesSizeParameter() {
+ Disposition disposition = new Disposition();
+ disposition.setSize(1024L);
+ assertEquals(
+ "1024", disposition.getParameters().getFirstValue(Disposition.NAME_SIZE, true));
+ }
+
+ @Test
+ void setCreationDate_storesFormattedDate() {
+ Disposition disposition = new Disposition();
+ Date date = new Date(0);
+ disposition.setCreationDate(date);
+ assertNotNull(
+ disposition.getParameters().getFirstValue(Disposition.NAME_CREATION_DATE, true));
+ }
+
+ @Test
+ void setModificationDate_storesFormattedDate() {
+ Disposition disposition = new Disposition();
+ disposition.setModificationDate(new Date(0));
+ assertNotNull(
+ disposition
+ .getParameters()
+ .getFirstValue(Disposition.NAME_MODIFICATION_DATE, true));
+ }
+
+ @Test
+ void setReadDate_storesFormattedDate() {
+ Disposition disposition = new Disposition();
+ disposition.setReadDate(new Date(0));
+ assertNotNull(disposition.getParameters().getFirstValue(Disposition.NAME_READ_DATE, true));
+ }
+
+ @Test
+ void addDate_appendsAdditionalDateParameter() {
+ Disposition disposition = new Disposition();
+ disposition.addDate(Disposition.NAME_CREATION_DATE, new Date(0));
+ disposition.addDate(Disposition.NAME_CREATION_DATE, new Date(1000));
+ assertEquals(2, disposition.getParameters().subList(Disposition.NAME_CREATION_DATE).size());
+ }
+
+ @Test
+ void setParameters_replacesParameterSeries() {
+ Disposition disposition = new Disposition();
+ Series parameters = new Form();
+ parameters.add("custom", "value");
+ disposition.setParameters(parameters);
+ assertSame(parameters, disposition.getParameters());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/EncodingTestCase.java b/org.restlet/src/test/java/org/restlet/data/EncodingTestCase.java
new file mode 100644
index 0000000000..3ad113e612
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/EncodingTestCase.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+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 org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.Encoding}. */
+class EncodingTestCase {
+
+ @Test
+ void constructor_withName_setsDefaultDescription() {
+ Encoding encoding = new Encoding("custom");
+ assertEquals("custom", encoding.getName());
+ assertEquals("Encoding applied to a representation", encoding.getDescription());
+ }
+
+ @Test
+ void constructor_withNameAndDescription() {
+ Encoding encoding = new Encoding("custom", "my description");
+ assertEquals("my description", encoding.getDescription());
+ }
+
+ @Test
+ void valueOf_returnsSharedConstant() {
+ assertSame(Encoding.GZIP, Encoding.valueOf("gzip"));
+ assertSame(Encoding.ZIP, Encoding.valueOf("ZIP"));
+ assertSame(Encoding.COMPRESS, Encoding.valueOf("compress"));
+ assertSame(Encoding.DEFLATE, Encoding.valueOf("deflate"));
+ assertSame(Encoding.DEFLATE_NOWRAP, Encoding.valueOf("deflate-no-wrap"));
+ assertSame(Encoding.IDENTITY, Encoding.valueOf("identity"));
+ assertSame(Encoding.FREEMARKER, Encoding.valueOf("freemarker"));
+ assertSame(Encoding.VELOCITY, Encoding.valueOf("velocity"));
+ assertSame(Encoding.ALL, Encoding.valueOf("*"));
+ }
+
+ @Test
+ void valueOf_unknownName_createsNewInstance() {
+ Encoding encoding = Encoding.valueOf("custom-encoding");
+ assertEquals("custom-encoding", encoding.getName());
+ }
+
+ @Test
+ void valueOf_nullOrEmptyName_returnsNull() {
+ assertNull(Encoding.valueOf(null));
+ assertNull(Encoding.valueOf(""));
+ }
+
+ @Test
+ void equals_isCaseInsensitive() {
+ Encoding e1 = new Encoding("gzip");
+ Encoding e2 = new Encoding("GZIP");
+ assertEquals(e1, e2);
+ assertEquals(e1.hashCode(), e2.hashCode());
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ assertNotEquals(Encoding.GZIP, "gzip");
+ }
+
+ @Test
+ void getParent_forAll_returnsNull() {
+ assertNull(Encoding.ALL.getParent());
+ }
+
+ @Test
+ void getParent_forSpecificEncoding_returnsAll() {
+ assertEquals(Encoding.ALL, Encoding.GZIP.getParent());
+ }
+
+ @Test
+ void includes_allIncludesEverything() {
+ assertTrue(Encoding.ALL.includes(Encoding.GZIP));
+ assertFalse(Encoding.GZIP.includes(Encoding.ALL));
+ }
+
+ @Test
+ void includes_nullIsAlwaysIncluded() {
+ assertTrue(Encoding.GZIP.includes(null));
+ }
+
+ @Test
+ void includes_sameEncoding_returnsTrue() {
+ assertTrue(Encoding.GZIP.includes(Encoding.GZIP));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/HeaderTestCase.java b/org.restlet/src/test/java/org/restlet/data/HeaderTestCase.java
new file mode 100644
index 0000000000..720dc44993
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/HeaderTestCase.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.Header}. */
+class HeaderTestCase {
+
+ @Test
+ void defaultConstructor_hasNullNameAndValue() {
+ Header header = new Header();
+ assertNull(header.getName());
+ assertNull(header.getValue());
+ }
+
+ @Test
+ void constructor_setsNameAndValue() {
+ Header header = new Header("Content-Type", "text/plain");
+ assertEquals("Content-Type", header.getName());
+ assertEquals("text/plain", header.getValue());
+ }
+
+ @Test
+ void settersUpdateState() {
+ Header header = new Header();
+ header.setName("X-Custom");
+ header.setValue("value");
+ assertEquals("X-Custom", header.getName());
+ assertEquals("value", header.getValue());
+ }
+
+ @Test
+ void equals_sameNameAndValue_returnsTrue() {
+ Header header1 = new Header("X-Custom", "value");
+ Header header2 = new Header("X-Custom", "value");
+ assertEquals(header1, header2);
+ assertEquals(header1.hashCode(), header2.hashCode());
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ Header header = new Header("X-Custom", "value");
+ assertEquals(header, header);
+ }
+
+ @Test
+ void equals_differentValue_returnsFalse() {
+ Header header1 = new Header("X-Custom", "value1");
+ Header header2 = new Header("X-Custom", "value2");
+ assertNotEquals(header1, header2);
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ Header header = new Header("X-Custom", "value");
+ assertNotEquals(header, "X-Custom");
+ }
+
+ @Test
+ void toString_containsNameAndValue() {
+ Header header = new Header("X-Custom", "value");
+ assertEquals("[X-Custom: value]", header.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/LocalReferenceTestCase.java b/org.restlet/src/test/java/org/restlet/data/LocalReferenceTestCase.java
new file mode 100644
index 0000000000..579ce6c9d2
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/LocalReferenceTestCase.java
@@ -0,0 +1,163 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.LocalReference}. */
+class LocalReferenceTestCase {
+
+ @Test
+ void constructor_fromUriString() {
+ LocalReference reference = new LocalReference("file:///tmp/test.txt");
+ assertEquals("file", reference.getScheme());
+ }
+
+ @Test
+ void constructor_fromReference() {
+ Reference base = new Reference("file:///tmp/test.txt");
+ LocalReference reference = new LocalReference(base);
+ assertEquals("file", reference.getScheme());
+ }
+
+ @Test
+ void createFileReference_fromFile() {
+ LocalReference reference = LocalReference.createFileReference(new File("/tmp/test.txt"));
+ assertEquals("file", reference.getScheme());
+ assertNotNull(reference.getFile());
+ }
+
+ @Test
+ void createFileReference_fromPath() {
+ LocalReference reference = LocalReference.createFileReference("/tmp/test.txt");
+ assertEquals("file", reference.getScheme());
+ assertEquals("", reference.getAuthority());
+ }
+
+ @Test
+ void createFileReference_withHostName() {
+ LocalReference reference = LocalReference.createFileReference("localhost", "/tmp/test.txt");
+ assertEquals("localhost", reference.getAuthority());
+ }
+
+ @Test
+ void createClapReference_fromPackage() {
+ LocalReference reference = LocalReference.createClapReference(String.class.getPackage());
+ assertEquals("clap", reference.getScheme());
+ assertTrue(reference.toString().contains("java/lang"));
+ }
+
+ @Test
+ void createClapReference_fromPath() {
+ LocalReference reference = LocalReference.createClapReference("/org/restlet/Restlet.class");
+ assertEquals("clap", reference.getScheme());
+ }
+
+ @Test
+ void createClapReference_withAuthorityType() {
+ LocalReference reference =
+ LocalReference.createClapReference(
+ LocalReference.CLAP_SYSTEM, "/org/restlet/Restlet.class");
+ assertEquals("system", reference.getAuthority());
+ assertEquals(LocalReference.CLAP_SYSTEM, reference.getClapAuthorityType());
+ }
+
+ @Test
+ void getClapAuthorityType_defaultsToClapDefault() {
+ LocalReference reference = LocalReference.createClapReference("/org/restlet/Restlet.class");
+ assertEquals(LocalReference.CLAP_DEFAULT, reference.getClapAuthorityType());
+ }
+
+ @Test
+ void getClapAuthorityType_forNonClapReference_returnsZero() {
+ LocalReference reference = new LocalReference("http://example.com");
+ assertEquals(0, reference.getClapAuthorityType());
+ }
+
+ @Test
+ void createRiapReference_buildsRiapUri() {
+ LocalReference reference =
+ LocalReference.createRiapReference(LocalReference.RIAP_COMPONENT, "/myApp/path");
+ assertEquals("riap", reference.getScheme());
+ assertEquals(LocalReference.RIAP_COMPONENT, reference.getRiapAuthorityType());
+ }
+
+ @Test
+ void getRiapAuthorityType_forNonRiapReference_returnsZero() {
+ LocalReference reference = new LocalReference("http://example.com");
+ assertEquals(0, reference.getRiapAuthorityType());
+ }
+
+ @Test
+ void createJarReference_buildsJarUri() {
+ Reference jarFile = new Reference("http://example.com/archive.jar");
+ LocalReference reference = LocalReference.createJarReference(jarFile, "entry/path.txt");
+ assertEquals("jar", reference.getScheme());
+ assertEquals("entry/path.txt", reference.getJarEntryPath());
+ assertEquals("http://example.com/archive.jar", reference.getJarFileRef().toString());
+ }
+
+ @Test
+ void getJarEntryPath_forNonJarReference_returnsNull() {
+ LocalReference reference = new LocalReference("http://example.com");
+ assertNull(reference.getJarEntryPath());
+ }
+
+ @Test
+ void createZipReference_buildsZipUri() {
+ Reference zipFile = new Reference("file:///tmp/archive.zip");
+ LocalReference reference = LocalReference.createZipReference(zipFile, "entry.txt");
+ assertEquals("zip", reference.getScheme());
+ }
+
+ @Test
+ void getFile_forRemoteHost_throwsRuntimeException() {
+ LocalReference reference =
+ LocalReference.createFileReference("remotehost", "/tmp/test.txt");
+ assertThrows(RuntimeException.class, reference::getFile);
+ }
+
+ @Test
+ void getFile_forNonFileReference_returnsNull() {
+ LocalReference reference = new LocalReference("http://example.com");
+ assertNull(reference.getFile());
+ }
+
+ @Test
+ void getAuthorityName_mapsAuthorityConstants() {
+ assertEquals("", LocalReference.getAuthorityName(LocalReference.CLAP_DEFAULT));
+ assertEquals("class", LocalReference.getAuthorityName(LocalReference.CLAP_CLASS));
+ assertEquals("system", LocalReference.getAuthorityName(LocalReference.CLAP_SYSTEM));
+ assertEquals("thread", LocalReference.getAuthorityName(LocalReference.CLAP_THREAD));
+ assertEquals(
+ "application", LocalReference.getAuthorityName(LocalReference.RIAP_APPLICATION));
+ assertEquals("component", LocalReference.getAuthorityName(LocalReference.RIAP_COMPONENT));
+ assertEquals("host", LocalReference.getAuthorityName(LocalReference.RIAP_HOST));
+ }
+
+ @Test
+ void localizePath_convertsSlashesToSystemSeparator() {
+ String result = LocalReference.localizePath("a/b/c");
+ assertEquals("a" + File.separatorChar + "b" + File.separatorChar + "c", result);
+ }
+
+ @Test
+ void normalizePath_convertsSystemSeparatorToSlash() {
+ String path = "a" + File.separatorChar + "b";
+ String result = LocalReference.normalizePath(path);
+ assertEquals("a/b", result);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/MetadataTestCase.java b/org.restlet/src/test/java/org/restlet/data/MetadataTestCase.java
new file mode 100644
index 0000000000..3f2f968761
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/MetadataTestCase.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test {@link org.restlet.data.Metadata}, exercised through the concrete {@link CharacterSet} and
+ * {@link Encoding} subclasses since Metadata is abstract.
+ */
+class MetadataTestCase {
+
+ @Test
+ void getName_returnsConfiguredValue() {
+ Metadata metadata = new CharacterSet("UTF-8", "description");
+ assertEquals("UTF-8", metadata.getName());
+ }
+
+ @Test
+ void getDescription_returnsConfiguredValue() {
+ Metadata metadata = new CharacterSet("UTF-8", "description");
+ assertEquals("description", metadata.getDescription());
+ }
+
+ @Test
+ void constructor_withNameOnly_hasNullDescription() {
+ Encoding encoding = new Encoding("custom", null);
+ assertNull(encoding.getDescription());
+ }
+
+ @Test
+ void equals_acrossDifferentMetadataSubtypes_returnsFalse() {
+ // Although the abstract Metadata.equals() only compares names, every
+ // concrete subclass (CharacterSet, Encoding, ...) overrides equals()
+ // with its own "instanceof" check, so two different Metadata subtypes
+ // sharing the same name are never considered equal.
+ Metadata characterSet = new CharacterSet("SAME-NAME");
+ Metadata encoding = new Encoding("SAME-NAME");
+ assertFalse(characterSet.equals(encoding));
+ assertFalse(encoding.equals(characterSet));
+ }
+
+ @Test
+ void equals_differentName_returnsFalse() {
+ Metadata cs1 = new CharacterSet("A");
+ Metadata cs2 = new CharacterSet("B");
+ assertFalse(((Metadata) cs1).equals(cs2));
+ }
+
+ @Test
+ void hashCode_matchesNameHashCode() {
+ // CharacterSet overrides hashCode() to be case-insensitive (lowercases
+ // the name), so this exercises that override rather than the base
+ // Metadata implementation.
+ Metadata metadata = new CharacterSet("UTF-8");
+ assertEquals("utf-8".hashCode(), metadata.hashCode());
+ }
+
+ @Test
+ void hashCode_forNullName_returnsZero() {
+ Metadata metadata = new CharacterSet((String) null);
+ assertEquals(0, metadata.hashCode());
+ }
+
+ @Test
+ void toString_returnsName() {
+ Metadata metadata = new Encoding("gzip");
+ assertEquals("gzip", metadata.toString());
+ }
+
+ @Test
+ void isCompatible_withNull_returnsFalse() {
+ Metadata metadata = CharacterSet.UTF_8;
+ assertFalse(metadata.isCompatible(null));
+ }
+
+ @Test
+ void isCompatible_delegatesToIncludes() {
+ assertTrue(CharacterSet.ALL.isCompatible(CharacterSet.UTF_8));
+ assertTrue(CharacterSet.UTF_8.isCompatible(CharacterSet.ALL));
+ assertFalse(CharacterSet.UTF_8.isCompatible(CharacterSet.ISO_8859_1));
+ }
+
+ @Test
+ void getParent_isAbstractButImplementedBySubclasses() {
+ assertNull(CharacterSet.ALL.getParent());
+ assertEquals(CharacterSet.ALL, CharacterSet.UTF_8.getParent());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ParameterTestCase.java b/org.restlet/src/test/java/org/restlet/data/ParameterTestCase.java
new file mode 100644
index 0000000000..53b77aa068
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ParameterTestCase.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+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 org.junit.jupiter.api.Test;
+import org.restlet.util.Series;
+
+/** Test {@link org.restlet.data.Parameter}. */
+class ParameterTestCase {
+
+ @Test
+ void defaultConstructor_hasNullNameAndValue() {
+ Parameter parameter = new Parameter();
+ assertNull(parameter.getName());
+ assertNull(parameter.getValue());
+ }
+
+ @Test
+ void constructor_setsNameAndValue() {
+ Parameter parameter = new Parameter("name", "value");
+ assertEquals("name", parameter.getName());
+ assertEquals("value", parameter.getValue());
+ }
+
+ @Test
+ void create_withNonNullValue_createsParameter() throws Exception {
+ Parameter parameter = Parameter.create("name", "value");
+ assertEquals("name", parameter.getName());
+ assertEquals("value", parameter.getValue());
+ }
+
+ @Test
+ void create_withNullValue_createsParameterWithNullValue() throws Exception {
+ Parameter parameter = Parameter.create("name", null);
+ assertEquals("name", parameter.getName());
+ assertNull(parameter.getValue());
+ }
+
+ @Test
+ void settersUpdateState() {
+ Parameter parameter = new Parameter();
+ parameter.setName("name");
+ parameter.setValue("value");
+ assertEquals("name", parameter.getName());
+ assertEquals("value", parameter.getValue());
+ }
+
+ @Test
+ void compareTo_ordersByName() {
+ Parameter a = new Parameter("a", "1");
+ Parameter b = new Parameter("b", "2");
+ assertTrue(a.compareTo(b) < 0);
+ assertTrue(b.compareTo(a) > 0);
+ assertEquals(0, a.compareTo(new Parameter("a", "other")));
+ }
+
+ @Test
+ void equals_sameNameAndValue_returnsTrue() {
+ Parameter p1 = new Parameter("name", "value");
+ Parameter p2 = new Parameter("name", "value");
+ assertEquals(p1, p2);
+ assertEquals(p1.hashCode(), p2.hashCode());
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ Parameter parameter = new Parameter("name", "value");
+ assertEquals(parameter, parameter);
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ Parameter parameter = new Parameter("name", "value");
+ assertNotEquals(parameter, "name=value");
+ }
+
+ @Test
+ void equals_differentValue_returnsFalse() {
+ Parameter p1 = new Parameter("name", "value1");
+ Parameter p2 = new Parameter("name", "value2");
+ assertNotEquals(p1, p2);
+ }
+
+ @Test
+ void toString_containsNameAndValue() {
+ Parameter parameter = new Parameter("name", "value");
+ assertEquals("[name=value]", parameter.toString());
+ }
+
+ @Test
+ void encode_appendsNameAndEncodedValue() throws Exception {
+ Parameter parameter = new Parameter("name", "a value");
+ String encoded = parameter.encode(CharacterSet.UTF_8);
+ assertEquals("name=a%20value", encoded);
+ }
+
+ @Test
+ void encode_withNullValue_omitsEqualsSign() throws Exception {
+ Parameter parameter = new Parameter("name", null);
+ String encoded = parameter.encode(CharacterSet.UTF_8);
+ assertEquals("name", encoded);
+ }
+
+ @Test
+ void createSeries_returnsSeriesContainingThisParameter() {
+ Parameter parameter = new Parameter("name", "value");
+ Series series = parameter.createSeries();
+ assertEquals(1, series.size());
+ assertSame(parameter, series.get(0));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/PreferenceTestCase.java b/org.restlet/src/test/java/org/restlet/data/PreferenceTestCase.java
new file mode 100644
index 0000000000..0b4de6f2e8
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/PreferenceTestCase.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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 org.junit.jupiter.api.Test;
+import org.restlet.util.Series;
+
+/** Test {@link org.restlet.data.Preference}. */
+class PreferenceTestCase {
+
+ @Test
+ void defaultConstructor_hasNullMetadataAndFullQuality() {
+ Preference preference = new Preference<>();
+ assertNull(preference.getMetadata());
+ assertEquals(1F, preference.getQuality());
+ }
+
+ @Test
+ void constructor_withMetadata_hasFullQuality() {
+ Preference preference = new Preference<>(MediaType.TEXT_PLAIN);
+ assertEquals(MediaType.TEXT_PLAIN, preference.getMetadata());
+ assertEquals(1F, preference.getQuality());
+ }
+
+ @Test
+ void constructor_withMetadataAndQuality() {
+ Preference preference = new Preference<>(MediaType.TEXT_PLAIN, 0.5F);
+ assertEquals(0.5F, preference.getQuality());
+ }
+
+ @Test
+ void constructor_withParameters() {
+ Series parameters = new Form();
+ parameters.add("q", "0.8");
+ Preference preference = new Preference<>(MediaType.TEXT_PLAIN, 0.8F, parameters);
+ assertEquals(1, preference.getParameters().size());
+ }
+
+ @Test
+ void getParameters_lazilyCreatesEmptySeries() {
+ Preference preference = new Preference<>(MediaType.TEXT_PLAIN);
+ assertNotNull(preference.getParameters());
+ assertEquals(0, preference.getParameters().size());
+ }
+
+ @Test
+ void settersUpdateState() {
+ Preference preference = new Preference<>();
+ preference.setMetadata(MediaType.APPLICATION_JSON);
+ preference.setQuality(0.3F);
+ Series parameters = new Form();
+ preference.setParameters(parameters);
+
+ assertEquals(MediaType.APPLICATION_JSON, preference.getMetadata());
+ assertEquals(0.3F, preference.getQuality());
+ assertSame(parameters, preference.getParameters());
+ }
+
+ @Test
+ void toString_withMetadata_includesNameAndQuality() {
+ Preference preference = new Preference<>(MediaType.TEXT_PLAIN, 0.5F);
+ assertEquals("text/plain:0.5", preference.toString());
+ }
+
+ @Test
+ void toString_withoutMetadata_returnsEmptyString() {
+ Preference preference = new Preference<>();
+ assertEquals("", preference.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ProductTestCase.java b/org.restlet/src/test/java/org/restlet/data/ProductTestCase.java
new file mode 100644
index 0000000000..3fc83deecd
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ProductTestCase.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.Product}. */
+class ProductTestCase {
+
+ @Test
+ void constructor_setsAllProperties() {
+ Product product = new Product("Restlet", "2.5", "a framework");
+ assertEquals("Restlet", product.getName());
+ assertEquals("2.5", product.getVersion());
+ assertEquals("a framework", product.getComment());
+ }
+
+ @Test
+ void constructor_withNullComment_returnsNullComment() {
+ Product product = new Product("Restlet", "2.5", null);
+ assertNull(product.getComment());
+ }
+
+ @Test
+ void settersUpdateState() {
+ Product product = new Product("Restlet", "2.5", "a framework");
+ product.setName("Jetty");
+ product.setVersion("9.0");
+ product.setComment("a server");
+ assertEquals("Jetty", product.getName());
+ assertEquals("9.0", product.getVersion());
+ assertEquals("a server", product.getComment());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ProtocolTestCase.java b/org.restlet/src/test/java/org/restlet/data/ProtocolTestCase.java
new file mode 100644
index 0000000000..bfaeaab312
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ProtocolTestCase.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+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 org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.Protocol}. */
+class ProtocolTestCase {
+
+ @Test
+ void constructor_withSchemeNameOnly_derivesNameAndDescription() {
+ Protocol protocol = new Protocol("myproto");
+ assertEquals("myproto", protocol.getSchemeName());
+ assertEquals("MYPROTO", protocol.getName());
+ assertEquals("MYPROTO Protocol", protocol.getDescription());
+ assertEquals(Protocol.UNKNOWN_PORT, protocol.getDefaultPort());
+ assertFalse(protocol.isConfidential());
+ assertNull(protocol.getVersion());
+ }
+
+ @Test
+ void constructor_withDefaultPort() {
+ Protocol protocol = new Protocol("proto", "PROTO", "description", 1234);
+ assertEquals(1234, protocol.getDefaultPort());
+ assertFalse(protocol.isConfidential());
+ }
+
+ @Test
+ void constructor_withConfidentiality() {
+ Protocol protocol = new Protocol("proto", "PROTO", "description", 1234, true);
+ assertTrue(protocol.isConfidential());
+ }
+
+ @Test
+ void constructor_withVersion() {
+ Protocol protocol = new Protocol("proto", "PROTO", "description", 1234, true, "1.0");
+ assertEquals("1.0", protocol.getVersion());
+ assertEquals("PROTO", protocol.getTechnicalName());
+ }
+
+ @Test
+ void constructor_withDefaultPortAndVersion() {
+ Protocol protocol = new Protocol("proto", "PROTO", "description", 1234, "2.0");
+ assertEquals("2.0", protocol.getVersion());
+ assertFalse(protocol.isConfidential());
+ }
+
+ @Test
+ void constructor_withDistinctTechnicalName() {
+ Protocol protocol = new Protocol("https", "HTTPS", "HTTP", "description", 443, true, "1.1");
+ assertEquals("HTTPS", protocol.getName());
+ assertEquals("HTTP", protocol.getTechnicalName());
+ }
+
+ @Test
+ void valueOf_knownScheme_returnsSharedConstant() {
+ assertSame(Protocol.HTTP, Protocol.valueOf("http"));
+ assertSame(Protocol.HTTPS, Protocol.valueOf("HTTPS"));
+ assertSame(Protocol.FTP, Protocol.valueOf("ftp"));
+ assertSame(Protocol.FILE, Protocol.valueOf("file"));
+ assertSame(Protocol.CLAP, Protocol.valueOf("clap"));
+ assertSame(Protocol.JAR, Protocol.valueOf("jar"));
+ assertSame(Protocol.JDBC, Protocol.valueOf("jdbc"));
+ assertSame(Protocol.RIAP, Protocol.valueOf("riap"));
+ assertSame(Protocol.WAR, Protocol.valueOf("war"));
+ assertSame(Protocol.ZIP, Protocol.valueOf("zip"));
+ }
+
+ @Test
+ void valueOf_unknownScheme_createsNewProtocol() {
+ Protocol protocol = Protocol.valueOf("custom");
+ assertEquals("CUSTOM", protocol.getName());
+ }
+
+ @Test
+ void valueOf_nullOrEmptyScheme_returnsNull() {
+ assertNull(Protocol.valueOf((String) null));
+ assertNull(Protocol.valueOf(""));
+ }
+
+ @Test
+ void valueOf_withMatchingVersion_returnsSameConstant() {
+ Protocol protocol = Protocol.valueOf("http", "1.1");
+ assertSame(Protocol.HTTP, protocol);
+ }
+
+ @Test
+ void valueOf_withDifferentVersion_createsNewInstanceWithVersion() {
+ Protocol protocol = Protocol.valueOf("http", "2.0");
+ assertNotSame(Protocol.HTTP, protocol);
+ assertEquals("2.0", protocol.getVersion());
+ assertEquals(Protocol.HTTP.getSchemeName(), protocol.getSchemeName());
+ assertEquals(Protocol.HTTP.getDefaultPort(), protocol.getDefaultPort());
+ }
+
+ @Test
+ void equals_isCaseInsensitiveOnName() {
+ Protocol p1 = new Protocol("proto", "NAME", "d", 1, false);
+ Protocol p2 = new Protocol("other", "name", "d2", 2, true);
+ assertEquals(p1, p2);
+ assertEquals(p1.hashCode(), p2.hashCode());
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ assertNotEquals(Protocol.HTTP, "HTTP");
+ }
+
+ @Test
+ void equals_differentName_returnsFalse() {
+ assertNotEquals(Protocol.HTTP, Protocol.HTTPS);
+ }
+
+ @Test
+ void toString_withVersion_includesVersion() {
+ assertEquals("HTTP/1.1", Protocol.HTTP.toString());
+ }
+
+ @Test
+ void toString_withoutVersion_omitsSlash() {
+ assertEquals("FTP", Protocol.FTP.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ReferenceListTestCase.java b/org.restlet/src/test/java/org/restlet/data/ReferenceListTestCase.java
new file mode 100644
index 0000000000..189910507d
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ReferenceListTestCase.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+/** Test {@link org.restlet.data.ReferenceList}. */
+class ReferenceListTestCase {
+
+ @Test
+ void defaultConstructor_isEmpty() {
+ ReferenceList list = new ReferenceList();
+ assertTrue(list.isEmpty());
+ assertNull(list.getIdentifier());
+ }
+
+ @Test
+ void constructor_withInitialCapacity_isEmpty() {
+ ReferenceList list = new ReferenceList(10);
+ assertTrue(list.isEmpty());
+ }
+
+ @Test
+ void constructor_withDelegate_wrapsGivenList() {
+ List delegate = new ArrayList<>();
+ delegate.add(new Reference("http://example.com/a"));
+ ReferenceList list = new ReferenceList(delegate);
+ assertEquals(1, list.size());
+ }
+
+ @Test
+ void addUri_createsReferenceAndAppendsIt() {
+ ReferenceList list = new ReferenceList();
+ assertTrue(list.add("http://example.com/a"));
+ assertEquals(1, list.size());
+ assertEquals("http://example.com/a", list.get(0).toString());
+ }
+
+ @Test
+ void setAndGetIdentifier_withReference() {
+ ReferenceList list = new ReferenceList();
+ Reference identifier = new Reference("http://example.com/");
+ list.setIdentifier(identifier);
+ assertEquals(identifier, list.getIdentifier());
+ }
+
+ @Test
+ void setIdentifier_withStringUri_createsReference() {
+ ReferenceList list = new ReferenceList();
+ list.setIdentifier("http://example.com/");
+ assertEquals("http://example.com/", list.getIdentifier().toString());
+ }
+
+ @Test
+ void constructor_fromUriListRepresentation_parsesLines() throws IOException {
+ String content =
+ "# http://example.com/\r\n"
+ + "http://example.com/a\r\n"
+ + "http://example.com/b\r\n";
+ Representation representation = new StringRepresentation(content, MediaType.TEXT_URI_LIST);
+ ReferenceList list = new ReferenceList(representation);
+
+ assertEquals("http://example.com/", list.getIdentifier().toString());
+ assertEquals(2, list.size());
+ assertEquals("http://example.com/a", list.get(0).toString());
+ assertEquals("http://example.com/b", list.get(1).toString());
+ }
+
+ @Test
+ void constructor_fromUriListRepresentation_withoutIdentifierComment() throws IOException {
+ String content = "http://example.com/a\r\n";
+ Representation representation = new StringRepresentation(content, MediaType.TEXT_URI_LIST);
+ ReferenceList list = new ReferenceList(representation);
+
+ assertNull(list.getIdentifier());
+ assertEquals(1, list.size());
+ }
+
+ @Test
+ void getTextRepresentation_containsIdentifierAndReferences() throws IOException {
+ ReferenceList list = new ReferenceList();
+ list.setIdentifier("http://example.com/");
+ list.add("http://example.com/a");
+
+ Representation representation = list.getTextRepresentation();
+ assertEquals(MediaType.TEXT_URI_LIST, representation.getMediaType());
+ String text = representation.getText();
+ assertTrue(text.contains("# http://example.com/"));
+ assertTrue(text.contains("http://example.com/a"));
+ }
+
+ @Test
+ void getWebRepresentation_producesHtmlListing() throws IOException {
+ ReferenceList list = new ReferenceList();
+ list.setIdentifier("http://example.com/dir/");
+ list.add("http://example.com/dir/a");
+
+ Representation representation = list.getWebRepresentation();
+ assertEquals(MediaType.TEXT_HTML, representation.getMediaType());
+ String text = representation.getText();
+ assertTrue(text.contains(""));
+ assertTrue(text.contains("http://example.com/dir/a"));
+ }
+
+ @Test
+ void getWebRepresentation_withoutIdentifier_usesGenericTitle() throws IOException {
+ ReferenceList list = new ReferenceList();
+ list.add("http://example.com/a");
+
+ Representation representation = list.getWebRepresentation();
+ String text = representation.getText();
+ assertTrue(text.contains("List of references"));
+ }
+
+ @Test
+ void subList_returnsReferenceListInstance() {
+ ReferenceList list = new ReferenceList();
+ list.add("http://example.com/a");
+ list.add("http://example.com/b");
+ list.add("http://example.com/c");
+
+ ReferenceList sub = list.subList(1, 3);
+ assertEquals(2, sub.size());
+ assertEquals("http://example.com/b", sub.get(0).toString());
+ assertEquals("http://example.com/c", sub.get(1).toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/data/ServerInfoTestCase.java b/org.restlet/src/test/java/org/restlet/data/ServerInfoTestCase.java
new file mode 100644
index 0000000000..9cbf8f85ac
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/data/ServerInfoTestCase.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/** Test {@link org.restlet.data.ServerInfo}. */
+class ServerInfoTestCase {
+
+ @Test
+ void constructor_setsDefaultValues() {
+ ServerInfo info = new ServerInfo();
+ assertNull(info.getAddress());
+ assertNull(info.getAgent());
+ assertEquals(-1, info.getPort());
+ assertFalse(info.isAcceptingRanges());
+ }
+
+ @Test
+ void settersUpdateState() {
+ ServerInfo info = new ServerInfo();
+ info.setAddress("127.0.0.1");
+ info.setAgent("Restlet-Framework/2.5");
+ info.setPort(8080);
+ info.setAcceptingRanges(true);
+
+ assertEquals("127.0.0.1", info.getAddress());
+ assertEquals("Restlet-Framework/2.5", info.getAgent());
+ assertEquals(8080, info.getPort());
+ assertTrue(info.isAcceptingRanges());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/CompositeHelperTestCase.java b/org.restlet/src/test/java/org/restlet/engine/CompositeHelperTestCase.java
new file mode 100644
index 0000000000..8077680963
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/CompositeHelperTestCase.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.routing.Filter;
+
+class CompositeHelperTestCase {
+
+ private static class TestCompositeHelper extends CompositeHelper {
+ TestCompositeHelper(Restlet helped) {
+ super(helped);
+ }
+
+ @Override
+ public void start() {}
+
+ @Override
+ public void stop() {}
+
+ @Override
+ public void update() {}
+ }
+
+ private static Filter markerFilter(java.util.concurrent.atomic.AtomicBoolean invoked) {
+ return new Filter() {
+ @Override
+ protected int doHandle(Request request, Response response) {
+ invoked.set(true);
+ return CONTINUE;
+ }
+ };
+ }
+
+ @Test
+ void addInboundFilter_singleFilter_becomesFirstAndLast() {
+ Context context = new Context();
+ TestCompositeHelper helper = new TestCompositeHelper(new Restlet(context) {});
+ Filter filter = new Filter() {};
+
+ helper.addInboundFilter(filter);
+
+ assertEquals(filter, helper.getFirstInboundFilter());
+ }
+
+ @Test
+ void addOutboundFilter_singleFilter_becomesFirstAndLast() {
+ Context context = new Context();
+ TestCompositeHelper helper = new TestCompositeHelper(new Restlet(context) {});
+ Filter filter = new Filter() {};
+
+ helper.addOutboundFilter(filter);
+
+ assertEquals(filter, helper.getFirstOutboundFilter());
+ }
+
+ @Test
+ void addOutboundFilter_secondFilter_chainsAfterFirst() {
+ Context context = new Context();
+ TestCompositeHelper helper = new TestCompositeHelper(new Restlet(context) {});
+ Filter first = new Filter() {};
+ Filter second = new Filter() {};
+
+ helper.addOutboundFilter(first);
+ helper.addOutboundFilter(second);
+
+ assertEquals(first, helper.getFirstOutboundFilter());
+ assertEquals(second, first.getNext());
+ }
+
+ @Test
+ void clear_resetsAllFiltersAndNextRestlets() {
+ Context context = new Context();
+ TestCompositeHelper helper = new TestCompositeHelper(new Restlet(context) {});
+ helper.addInboundFilter(new Filter() {});
+ helper.addOutboundFilter(new Filter() {});
+
+ helper.clear();
+
+ assertNull(helper.getFirstInboundFilter());
+ assertNull(helper.getFirstOutboundFilter());
+ assertNull(helper.getOutboundNext());
+ }
+
+ @Test
+ void handle_withInboundFilter_delegatesToFilter() {
+ Context context = new Context();
+ TestCompositeHelper helper = new TestCompositeHelper(new Restlet(context) {});
+ java.util.concurrent.atomic.AtomicBoolean invoked =
+ new java.util.concurrent.atomic.AtomicBoolean();
+ helper.addInboundFilter(markerFilter(invoked));
+
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ helper.handle(request, response);
+
+ assertEquals(true, invoked.get());
+ }
+
+ @Test
+ void handle_withoutFilterOrNext_setsInternalServerError() {
+ Context context = new Context();
+ TestCompositeHelper helper = new TestCompositeHelper(new Restlet(context) {});
+
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ helper.handle(request, response);
+
+ assertEquals(Status.SERVER_ERROR_INTERNAL, response.getStatus());
+ }
+
+ @Test
+ void getOutboundNext_withoutFilters_returnsSetNext() {
+ Context context = new Context();
+ TestCompositeHelper helper = new TestCompositeHelper(new Restlet(context) {});
+ Restlet next = new Restlet(context) {};
+
+ helper.setOutboundNext(next);
+
+ assertEquals(next, helper.getOutboundNext());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/EditionTestCase.java b/org.restlet/src/test/java/org/restlet/engine/EditionTestCase.java
new file mode 100644
index 0000000000..919f1b7a58
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/EditionTestCase.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class EditionTestCase {
+
+ @AfterEach
+ void tearDownEach() {
+ Edition.CURRENT = Edition.JSE;
+ }
+
+ @Test
+ void getFullName_returnsConfiguredName() {
+ assertEquals("Java Standard Edition", Edition.JSE.getFullName());
+ }
+
+ @Test
+ void getMediumName_returnsConfiguredName() {
+ assertEquals("Java SE", Edition.JSE.getMediumName());
+ }
+
+ @Test
+ void getShortName_returnsConfiguredName() {
+ assertEquals("JSE", Edition.JSE.getShortName());
+ }
+
+ @Test
+ void isCurrentEdition_matchesCurrentStaticField() {
+ Edition.CURRENT = Edition.JSE;
+ assertTrue(Edition.JSE.isCurrentEdition());
+ assertFalse(Edition.ANDROID.isCurrentEdition());
+ }
+
+ @Test
+ void isNotCurrentEdition_isInverseOfIsCurrentEdition() {
+ Edition.CURRENT = Edition.JSE;
+ assertFalse(Edition.JSE.isNotCurrentEdition());
+ assertTrue(Edition.ANDROID.isNotCurrentEdition());
+ }
+
+ @Test
+ void setCurrentEdition_updatesCurrentField() {
+ Edition.ANDROID.setCurrentEdition();
+ assertEquals(Edition.ANDROID, Edition.CURRENT);
+ }
+
+ @Test
+ void isCurrentEditionOneOf_containingCurrentEdition_returnsTrue() {
+ assertTrue(Edition.isCurrentEditionOneOf(Edition.ANDROID, Edition.JSE));
+ }
+
+ @Test
+ void isCurrentEditionOneOf_notContainingCurrentEdition_returnsFalse() {
+ Edition.CURRENT = Edition.JSE;
+ assertFalse(Edition.isCurrentEditionOneOf(Edition.ANDROID));
+ }
+
+ @Test
+ void isCurrentEditionOneOf_nullArray_returnsFalse() {
+ assertFalse(Edition.isCurrentEditionOneOf((Edition[]) null));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/RestletHelperTestCase.java b/org.restlet/src/test/java/org/restlet/engine/RestletHelperTestCase.java
new file mode 100644
index 0000000000..ecc2da2659
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/RestletHelperTestCase.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Method;
+import org.restlet.service.MetadataService;
+
+class RestletHelperTestCase {
+
+ private static class TestRestletHelper extends RestletHelper {
+ TestRestletHelper(Restlet helped) {
+ super(helped);
+ }
+
+ @Override
+ public void start() {}
+
+ @Override
+ public void stop() {}
+
+ @Override
+ public void update() {}
+ }
+
+ @Test
+ void getHelpedAndSetHelped_roundTrip() {
+ Restlet restlet = new Restlet() {};
+ TestRestletHelper helper = new TestRestletHelper(restlet);
+ assertEquals(restlet, helper.getHelped());
+
+ Restlet other = new Restlet() {};
+ helper.setHelped(other);
+ assertEquals(other, helper.getHelped());
+ }
+
+ @Test
+ void getAttributes_returnsMutableMap() {
+ TestRestletHelper helper = new TestRestletHelper(new Restlet() {});
+ helper.getAttributes().put("key", "value");
+ assertEquals("value", helper.getAttributes().get("key"));
+ }
+
+ @Test
+ void getContext_delegatesToHelpedRestlet() {
+ Context context = new Context();
+ Restlet restlet = new Restlet(context) {};
+ TestRestletHelper helper = new TestRestletHelper(restlet);
+ assertEquals(context, helper.getContext());
+ }
+
+ @Test
+ void getHelpedParameters_withContext_returnsContextParameters() {
+ Context context = new Context();
+ Restlet restlet = new Restlet(context) {};
+ TestRestletHelper helper = new TestRestletHelper(restlet);
+ assertEquals(context.getParameters(), helper.getHelpedParameters());
+ }
+
+ @Test
+ void getHelpedParameters_withoutContext_returnsEmptySeries() {
+ Restlet restlet = new Restlet() {};
+ TestRestletHelper helper = new TestRestletHelper(restlet);
+ assertTrue(helper.getHelpedParameters().isEmpty());
+ }
+
+ @Test
+ void getLogger_withContext_returnsContextLogger() {
+ Context context = new Context();
+ Restlet restlet = new Restlet(context) {};
+ TestRestletHelper helper = new TestRestletHelper(restlet);
+ assertEquals(context.getLogger(), helper.getLogger());
+ }
+
+ @Test
+ void getLogger_withoutContext_returnsCurrentLogger() {
+ Restlet restlet = new Restlet() {};
+ TestRestletHelper helper = new TestRestletHelper(restlet);
+ assertNotNull(helper.getLogger());
+ }
+
+ @Test
+ void getMetadataService_withoutApplication_returnsNewInstance() {
+ Restlet restlet = new Restlet() {};
+ TestRestletHelper helper = new TestRestletHelper(restlet);
+ assertNotNull(helper.getMetadataService());
+ }
+
+ @Test
+ void getMetadataService_withApplication_returnsApplicationMetadataService() {
+ org.restlet.Application application = new org.restlet.Application(new Context());
+ org.restlet.Application.setCurrent(application);
+ try {
+ TestRestletHelper helper = new TestRestletHelper(application);
+ MetadataService result = helper.getMetadataService();
+ assertEquals(application.getMetadataService(), result);
+ } finally {
+ org.restlet.Application.setCurrent(null);
+ }
+ }
+
+ @Test
+ void handle_setsCurrentResponseAndContext() {
+ Context context = new Context();
+ Restlet restlet = new Restlet(context) {};
+ TestRestletHelper helper = new TestRestletHelper(restlet);
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ helper.handle(request, response);
+
+ assertEquals(response, Response.getCurrent());
+ assertEquals(context, Context.getCurrent());
+
+ Response.setCurrent(null);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/application/DecoderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/application/DecoderTestCase.java
new file mode 100644
index 0000000000..ee3c29dec4
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/application/DecoderTestCase.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.application;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Encoding;
+import org.restlet.data.Method;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+class DecoderTestCase {
+
+ @Test
+ void constructor_singleArg_decodesRequestOnly() {
+ Decoder decoder = new Decoder(new Context());
+ assertTrue(decoder.isDecodingRequest());
+ assertFalse(decoder.isDecodingResponse());
+ }
+
+ @Test
+ void constructor_explicitFlags_setsBoth() {
+ Decoder decoder = new Decoder(new Context(), false, true);
+ assertFalse(decoder.isDecodingRequest());
+ assertTrue(decoder.isDecodingResponse());
+ }
+
+ @Test
+ void canDecode_nullRepresentation_returnsFalse() {
+ Decoder decoder = new Decoder(new Context());
+ assertFalse(decoder.canDecode(null));
+ }
+
+ @Test
+ void canDecode_noEncodings_returnsFalse() {
+ Decoder decoder = new Decoder(new Context());
+ Representation representation = new StringRepresentation("body");
+ assertFalse(decoder.canDecode(representation));
+ }
+
+ @Test
+ void canDecode_identityEncodingOnly_returnsFalse() {
+ Decoder decoder = new Decoder(new Context());
+ Representation representation = new StringRepresentation("body");
+ representation.getEncodings().add(Encoding.IDENTITY);
+ assertFalse(decoder.canDecode(representation));
+ }
+
+ @Test
+ void canDecode_gzipEncoding_returnsTrue() {
+ Decoder decoder = new Decoder(new Context());
+ Representation representation = new StringRepresentation("body");
+ representation.getEncodings().add(Encoding.GZIP);
+ assertTrue(decoder.canDecode(representation));
+ }
+
+ @Test
+ void decode_unsupportedEncoding_returnsOriginalRepresentation() {
+ Decoder decoder = new Decoder(new Context());
+ Representation representation = new StringRepresentation("body");
+ representation.getEncodings().add(Encoding.COMPRESS);
+ assertEquals(representation, decoder.decode(representation));
+ }
+
+ @Test
+ void decode_identityEncodingOnly_returnsOriginalRepresentation() {
+ Decoder decoder = new Decoder(new Context());
+ Representation representation = new StringRepresentation("body");
+ representation.getEncodings().add(Encoding.IDENTITY);
+ assertEquals(representation, decoder.decode(representation));
+ }
+
+ @Test
+ void decode_supportedEncoding_wrapsInDecodeRepresentation() {
+ Decoder decoder = new Decoder(new Context());
+ Representation representation = new StringRepresentation("body");
+ representation.getEncodings().add(Encoding.GZIP);
+ assertTrue(decoder.decode(representation) instanceof DecodeRepresentation);
+ }
+
+ @Test
+ void beforeHandle_decodingRequestEnabled_decodesRequestEntity() {
+ Decoder decoder = new Decoder(new Context(), true, false);
+ Request request = new Request(Method.POST, "http://localhost/test");
+ Representation entity = new StringRepresentation("body");
+ entity.getEncodings().add(Encoding.GZIP);
+ request.setEntity(entity);
+ Response response = new Response(request);
+
+ int result = decoder.beforeHandle(request, response);
+
+ assertTrue(request.getEntity() instanceof DecodeRepresentation);
+ assertEquals(org.restlet.routing.Filter.CONTINUE, result);
+ }
+
+ @Test
+ void beforeHandle_decodingRequestDisabled_leavesRequestEntityUnchanged() {
+ Decoder decoder = new Decoder(new Context(), false, false);
+ Request request = new Request(Method.POST, "http://localhost/test");
+ Representation entity = new StringRepresentation("body");
+ entity.getEncodings().add(Encoding.GZIP);
+ request.setEntity(entity);
+ Response response = new Response(request);
+
+ decoder.beforeHandle(request, response);
+
+ assertEquals(entity, request.getEntity());
+ }
+
+ @Test
+ void afterHandle_decodingResponseEnabled_decodesResponseEntity() {
+ Decoder decoder = new Decoder(new Context(), false, true);
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ Representation entity = new StringRepresentation("body");
+ entity.getEncodings().add(Encoding.GZIP);
+ response.setEntity(entity);
+
+ decoder.afterHandle(request, response);
+
+ assertTrue(response.getEntity() instanceof DecodeRepresentation);
+ }
+
+ @Test
+ void afterHandle_decodingResponseDisabled_leavesResponseEntityUnchanged() {
+ Decoder decoder = new Decoder(new Context(), false, false);
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ Representation entity = new StringRepresentation("body");
+ entity.getEncodings().add(Encoding.GZIP);
+ response.setEntity(entity);
+
+ decoder.afterHandle(request, response);
+
+ assertEquals(entity, response.getEntity());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/application/MetadataExtensionTestCase.java b/org.restlet/src/test/java/org/restlet/engine/application/MetadataExtensionTestCase.java
new file mode 100644
index 0000000000..964d718c98
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/application/MetadataExtensionTestCase.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.application;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.Encoding;
+import org.restlet.data.Language;
+import org.restlet.data.MediaType;
+
+class MetadataExtensionTestCase {
+
+ @Test
+ void getName_returnsConstructedName() {
+ MetadataExtension extension = new MetadataExtension("json", MediaType.APPLICATION_JSON);
+ assertEquals("json", extension.getName());
+ }
+
+ @Test
+ void getMetadata_returnsConstructedMetadata() {
+ MetadataExtension extension = new MetadataExtension("json", MediaType.APPLICATION_JSON);
+ assertEquals(MediaType.APPLICATION_JSON, extension.getMetadata());
+ }
+
+ @Test
+ void getMediaType_castsMetadataToMediaType() {
+ MetadataExtension extension = new MetadataExtension("json", MediaType.APPLICATION_JSON);
+ assertEquals(MediaType.APPLICATION_JSON, extension.getMediaType());
+ }
+
+ @Test
+ void getCharacterSet_castsMetadataToCharacterSet() {
+ MetadataExtension extension = new MetadataExtension("utf8", CharacterSet.UTF_8);
+ assertEquals(CharacterSet.UTF_8, extension.getCharacterSet());
+ }
+
+ @Test
+ void getEncoding_castsMetadataToEncoding() {
+ MetadataExtension extension = new MetadataExtension("gzip", Encoding.GZIP);
+ assertEquals(Encoding.GZIP, extension.getEncoding());
+ }
+
+ @Test
+ void getLanguage_castsMetadataToLanguage() {
+ MetadataExtension extension = new MetadataExtension("en", Language.ENGLISH);
+ assertEquals(Language.ENGLISH, extension.getLanguage());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/application/RangeFilterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/application/RangeFilterTestCase.java
new file mode 100644
index 0000000000..e762b8e8c2
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/application/RangeFilterTestCase.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.application;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Application;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Method;
+import org.restlet.data.Range;
+import org.restlet.data.Status;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+class RangeFilterTestCase {
+
+ private Application application;
+
+ private RangeFilter filter;
+
+ @BeforeEach
+ void setUpEach() {
+ application = new Application(new Context());
+ Application.setCurrent(application);
+ filter = new RangeFilter(application.getContext());
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ Application.setCurrent(null);
+ }
+
+ private Response successResponse(Method method, Representation entity) {
+ Request request = new Request(method, "http://localhost/test");
+ Response response = new Response(request);
+ response.setStatus(Status.SUCCESS_OK);
+ response.setEntity(entity);
+ return response;
+ }
+
+ @Test
+ void afterHandle_rangeServiceDisabled_marksServerInfoNotAcceptingRanges() {
+ application.getRangeService().setEnabled(false);
+ Response response = successResponse(Method.GET, new StringRepresentation("body"));
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertFalse(response.getServerInfo().isAcceptingRanges());
+ }
+
+ @Test
+ void afterHandle_rangeServiceEnabled_marksServerInfoAcceptingRanges() {
+ application.getRangeService().setEnabled(true);
+ Response response = successResponse(Method.GET, new StringRepresentation("body"));
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertTrue(response.getServerInfo().isAcceptingRanges());
+ }
+
+ @Test
+ void afterHandle_unsafeMethod_leavesEntityUnchanged() {
+ Response response = successResponse(Method.POST, new StringRepresentation("body"));
+ Representation original = response.getEntity();
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertEquals(original, response.getEntity());
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ }
+
+ @Test
+ void afterHandle_nonSuccessStatus_leavesStatusUnchanged() {
+ Response response = successResponse(Method.GET, new StringRepresentation("body"));
+ response.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, response.getStatus());
+ }
+
+ @Test
+ void afterHandle_multipleRequestedRanges_setsNotImplemented() {
+ Response response = successResponse(Method.GET, new StringRepresentation("0123456789"));
+ response.getRequest().getRanges().add(new Range(0, 2));
+ response.getRequest().getRanges().add(new Range(3, 2));
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertEquals(Status.SERVER_ERROR_NOT_IMPLEMENTED, response.getStatus());
+ }
+
+ @Test
+ void afterHandle_noRequestedRange_leavesResponseUnchanged() {
+ Response response = successResponse(Method.GET, new StringRepresentation("0123456789"));
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ }
+
+ @Test
+ void afterHandle_singleMatchingRange_wrapsEntityAsPartialContent() {
+ Response response = successResponse(Method.GET, new StringRepresentation("0123456789"));
+ Range requested = new Range(0, 5);
+ response.getRequest().getRanges().add(requested);
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertEquals(Status.SUCCESS_PARTIAL_CONTENT, response.getStatus());
+ }
+
+ @Test
+ void getRangeService_returnsApplicationRangeService() {
+ assertEquals(application.getRangeService(), filter.getRangeService());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/application/StatusFilterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/application/StatusFilterTestCase.java
new file mode 100644
index 0000000000..e30bf832bf
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/application/StatusFilterTestCase.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.application;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.service.StatusService;
+
+class StatusFilterTestCase {
+
+ private Response newResponse() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ return new Response(request);
+ }
+
+ @Test
+ void constructor_withOverwritingFlag_setsFlagAndNullStatusService() {
+ StatusFilter filter = new StatusFilter(new Context(), true);
+ assertTrue(filter.isOverwriting());
+ assertEquals(null, filter.getStatusService());
+ }
+
+ @Test
+ void constructor_withStatusService_copiesOverwritingFlag() {
+ StatusService service = new StatusService();
+ service.setOverwriting(true);
+ StatusFilter filter = new StatusFilter(new Context(), service);
+ assertTrue(filter.isOverwriting());
+ assertEquals(service, filter.getStatusService());
+ }
+
+ @Test
+ void setOverwriting_updatesFlag() {
+ StatusFilter filter = new StatusFilter(new Context(), false);
+ filter.setOverwriting(true);
+ assertTrue(filter.isOverwriting());
+ }
+
+ @Test
+ void setStatusService_updatesService() {
+ StatusFilter filter = new StatusFilter(new Context(), false);
+ StatusService service = new StatusService();
+ filter.setStatusService(service);
+ assertEquals(service, filter.getStatusService());
+ }
+
+ @Test
+ void afterHandle_nullStatus_defaultsToSuccessOk() {
+ StatusFilter filter = new StatusFilter(new Context(), new StatusService());
+ Response response = newResponse();
+ response.setStatus(null);
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ }
+
+ @Test
+ void afterHandle_errorStatusWithoutEntity_setsRepresentationFromStatusService() {
+ StatusFilter filter = new StatusFilter(new Context(), new StatusService());
+ Response response = newResponse();
+ response.setStatus(Status.SERVER_ERROR_INTERNAL);
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ void afterHandle_errorStatusWithEntityNotOverwriting_leavesEntityUnchanged() {
+ StatusFilter filter = new StatusFilter(new Context(), false);
+ filter.setStatusService(new StatusService());
+ Response response = newResponse();
+ response.setStatus(Status.SERVER_ERROR_INTERNAL);
+ response.setEntity(new StringRepresentation("original"));
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertEquals("original", getText(response));
+ }
+
+ @Test
+ void afterHandle_errorStatusWithEntityOverwriting_replacesEntity() {
+ StatusFilter filter = new StatusFilter(new Context(), true);
+ filter.setStatusService(new StatusService());
+ Response response = newResponse();
+ response.setStatus(Status.SERVER_ERROR_INTERNAL);
+ response.setEntity(new StringRepresentation("original"));
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertFalse("original".equals(getText(response)));
+ }
+
+ @Test
+ void afterHandle_successStatus_leavesEntityUnchanged() {
+ StatusFilter filter = new StatusFilter(new Context(), new StatusService());
+ Response response = newResponse();
+ response.setStatus(Status.SUCCESS_OK);
+ response.setEntity(new StringRepresentation("original"));
+
+ filter.afterHandle(response.getRequest(), response);
+
+ assertEquals("original", getText(response));
+ }
+
+ private static String getText(Response response) {
+ try {
+ return response.getEntity() == null ? null : response.getEntity().getText();
+ } catch (java.io.IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/application/StrictConnegTestCase.java b/org.restlet/src/test/java/org/restlet/engine/application/StrictConnegTestCase.java
new file mode 100644
index 0000000000..c97deb49c0
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/application/StrictConnegTestCase.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.application;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.Language;
+import org.restlet.data.MediaType;
+import org.restlet.data.Method;
+import org.restlet.data.Preference;
+import org.restlet.representation.Variant;
+
+class StrictConnegTestCase {
+
+ private static StrictConneg newConneg(Request request) {
+ return new StrictConneg(request, null);
+ }
+
+ @Test
+ void scoreMediaType_nullMediaType_returnsZero() {
+ StrictConneg conneg = newConneg(new Request(Method.GET, "http://localhost/test"));
+ assertEquals(0.0F, conneg.scoreMediaType(null));
+ }
+
+ @Test
+ void scoreMediaType_noMatchingPreference_returnsNegativeOne() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ request.getClientInfo().getAcceptedMediaTypes().add(new Preference<>(MediaType.TEXT_PLAIN));
+ StrictConneg conneg = newConneg(request);
+
+ assertEquals(-1.0F, conneg.scoreMediaType(MediaType.APPLICATION_JSON));
+ }
+
+ @Test
+ void scoreMediaType_matchingPreference_returnsQuality() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ request.getClientInfo()
+ .getAcceptedMediaTypes()
+ .add(new Preference<>(MediaType.TEXT_PLAIN, 0.8F));
+ StrictConneg conneg = newConneg(request);
+
+ assertEquals(0.8F, conneg.scoreMediaType(MediaType.TEXT_PLAIN));
+ }
+
+ @Test
+ void scoreCharacterSet_matchingPreference_returnsQuality() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ request.getClientInfo()
+ .getAcceptedCharacterSets()
+ .add(new Preference<>(CharacterSet.UTF_8, 0.9F));
+ StrictConneg conneg = newConneg(request);
+
+ assertEquals(0.9F, conneg.scoreCharacterSet(CharacterSet.UTF_8));
+ }
+
+ @Test
+ void scoreEncodings_emptyList_returnsZero() {
+ StrictConneg conneg = newConneg(new Request(Method.GET, "http://localhost/test"));
+ assertEquals(0.0F, conneg.scoreEncodings(List.of()));
+ }
+
+ @Test
+ void scoreLanguages_emptyList_returnsZero() {
+ StrictConneg conneg = newConneg(new Request(Method.GET, "http://localhost/test"));
+ assertEquals(0.0F, conneg.scoreLanguages(List.of()));
+ }
+
+ @Test
+ void scoreVariant_noAcceptedMediaType_returnsNegativeOne() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ request.getClientInfo().getAcceptedMediaTypes().clear();
+ request.getClientInfo()
+ .getAcceptedMediaTypes()
+ .add(new Preference<>(MediaType.TEXT_PLAIN, 1.0F));
+ StrictConneg conneg = newConneg(request);
+
+ Variant variant = new Variant(MediaType.APPLICATION_JSON);
+ assertEquals(-1.0F, conneg.scoreVariant(variant));
+ }
+
+ @Test
+ void scoreVariant_fullyAcceptedVariant_returnsPositiveScore() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ request.getClientInfo()
+ .getAcceptedMediaTypes()
+ .add(new Preference<>(MediaType.TEXT_PLAIN, 1.0F));
+ request.getClientInfo()
+ .getAcceptedCharacterSets()
+ .add(new Preference<>(CharacterSet.UTF_8, 1.0F));
+ request.getClientInfo()
+ .getAcceptedLanguages()
+ .add(new Preference<>(Language.ENGLISH, 1.0F));
+ StrictConneg conneg = newConneg(request);
+
+ Variant variant = new Variant(MediaType.TEXT_PLAIN);
+ variant.setCharacterSet(CharacterSet.UTF_8);
+ variant.setLanguages(Arrays.asList(Language.ENGLISH));
+
+ float score = conneg.scoreVariant(variant);
+ assertTrue(score >= 0.0F);
+ }
+
+ @Test
+ void getPreferredVariant_choosesHighestScoringVariant() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ request.getClientInfo().getAcceptedMediaTypes().clear();
+ request.getClientInfo()
+ .getAcceptedMediaTypes()
+ .add(new Preference<>(MediaType.APPLICATION_JSON, 1.0F));
+ request.getClientInfo()
+ .getAcceptedMediaTypes()
+ .add(new Preference<>(MediaType.TEXT_PLAIN, 0.5F));
+ StrictConneg conneg = newConneg(request);
+
+ Variant jsonVariant = new Variant(MediaType.APPLICATION_JSON);
+ Variant textVariant = new Variant(MediaType.TEXT_PLAIN);
+
+ Variant preferred = conneg.getPreferredVariant(Arrays.asList(textVariant, jsonVariant));
+
+ assertEquals(jsonVariant, preferred);
+ }
+
+ @Test
+ void getPreferredVariant_emptyList_returnsNull() {
+ StrictConneg conneg = newConneg(new Request(Method.GET, "http://localhost/test"));
+ assertNull(conneg.getPreferredVariant(List.of()));
+ }
+
+ @Test
+ void getPreferredVariant_nullList_returnsNull() {
+ StrictConneg conneg = newConneg(new Request(Method.GET, "http://localhost/test"));
+ assertNull(conneg.getPreferredVariant(null));
+ }
+
+ @Test
+ void getRequest_returnsConstructorRequest() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ StrictConneg conneg = newConneg(request);
+ assertEquals(request, conneg.getRequest());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/converter/ConverterUtilsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/converter/ConverterUtilsTestCase.java
new file mode 100644
index 0000000000..a28377d3eb
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/converter/ConverterUtilsTestCase.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.converter;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.engine.application.StatusInfo;
+import org.restlet.engine.resource.VariantInfo;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.Variant;
+
+class ConverterUtilsTestCase {
+
+ @Test
+ void getBestHelper_objectSource_findsMatchingConverter() {
+ StatusInfo source = new StatusInfo(Status.SUCCESS_OK);
+ ConverterHelper helper =
+ ConverterUtils.getBestHelper(source, new Variant(MediaType.TEXT_HTML), null);
+ assertNotNull(helper);
+ assertTrue(helper instanceof StatusInfoHtmlConverter);
+ }
+
+ @Test
+ void getBestHelper_representationSource_findsMatchingConverter() {
+ StringRepresentation source = new StringRepresentation("body", MediaType.TEXT_PLAIN);
+ ConverterHelper helper = ConverterUtils.getBestHelper(source, String.class, null);
+ assertNotNull(helper);
+ }
+
+ @Test
+ void getVariants_statusInfoClass_returnsVariants() {
+ List variants = ConverterUtils.getVariants(StatusInfo.class, null);
+ assertNotNull(variants);
+ assertTrue(variants.size() >= 2);
+ }
+
+ @Test
+ void getVariants_unsupportedClass_returnsNull() {
+ assertNull(ConverterUtils.getVariants(ConverterUtilsTestCase.class, null));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/converter/DefaultConverterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/converter/DefaultConverterTestCase.java
new file mode 100644
index 0000000000..a3bfbce041
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/converter/DefaultConverterTestCase.java
@@ -0,0 +1,249 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.converter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Preference;
+import org.restlet.representation.InputRepresentation;
+import org.restlet.representation.ObjectRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.Variant;
+
+class DefaultConverterTestCase {
+
+ private final DefaultConverter converter = new DefaultConverter();
+
+ @Test
+ void getObjectClasses_defaultVariant_includesBaseClasses() {
+ List> classes = converter.getObjectClasses(new Variant());
+ assertTrue(classes.contains(String.class));
+ assertTrue(classes.contains(InputStream.class));
+ assertTrue(classes.contains(Reader.class));
+ }
+
+ @Test
+ void getObjectClasses_formVariant_includesFormClass() {
+ List> classes =
+ converter.getObjectClasses(new Variant(MediaType.APPLICATION_WWW_FORM));
+ assertTrue(classes.contains(Form.class));
+ }
+
+ @Test
+ void getVariants_stringClass_returnsAllVariant() throws IOException {
+ assertEquals(1, converter.getVariants(String.class).size());
+ }
+
+ @Test
+ void getVariants_formClass_returnsFormVariant() throws IOException {
+ assertEquals(1, converter.getVariants(Form.class).size());
+ }
+
+ @Test
+ void getVariants_nullClass_returnsEmptyList() throws IOException {
+ assertTrue(converter.getVariants(null).isEmpty());
+ }
+
+ @Test
+ void getVariants_unrelatedClass_returnsEmptyList() throws IOException {
+ assertTrue(converter.getVariants(Object.class).isEmpty());
+ }
+
+ @Test
+ void score_string_returnsMaxScore() {
+ assertEquals(1.0F, converter.score("text", null, null));
+ }
+
+ @Test
+ void score_inputStream_returnsMaxScore() {
+ assertEquals(1.0F, converter.score(new ByteArrayInputStream(new byte[0]), null, null));
+ }
+
+ @Test
+ void score_form_compatibleTarget_returnsMaxScore() {
+ Form form = new Form();
+ assertEquals(
+ 1.0F, converter.score(form, new Variant(MediaType.APPLICATION_WWW_FORM), null));
+ }
+
+ @Test
+ void score_form_incompatibleTarget_returnsLowerScore() {
+ Form form = new Form();
+ assertEquals(0.6F, converter.score(form, new Variant(MediaType.TEXT_PLAIN), null));
+ }
+
+ @Test
+ void score_null_returnsNegative() {
+ assertEquals(-1.0F, converter.score((Object) null, (Variant) null, null));
+ }
+
+ @Test
+ void score_representationTarget_stringClass_returnsMaxScore() throws IOException {
+ Representation source = new StringRepresentation("body");
+ assertEquals(1.0F, converter.score(source, String.class, null));
+ }
+
+ @Test
+ void score_representationTarget_nullTargetNonObjectRepresentation_returnsNegative() {
+ Representation source = new StringRepresentation("body");
+ assertEquals(-1.0F, converter.score(source, (Class>) null, null));
+ }
+
+ @Test
+ void score_representationTarget_fileClassWithNonFileRepresentation_returnsNegative() {
+ Representation source = new StringRepresentation("body");
+ assertEquals(-1.0F, converter.score(source, java.io.File.class, null));
+ }
+
+ @Test
+ void score_representationTarget_formClassCompatible_returnsMaxScore() {
+ Representation source = new StringRepresentation("a=b", MediaType.APPLICATION_WWW_FORM);
+ assertEquals(1.0F, converter.score(source, Form.class, null));
+ }
+
+ @Test
+ void toObject_targetIsSourceType_returnsSourceUnchanged() throws IOException {
+ Representation source = new StringRepresentation("body");
+ assertEquals(source, converter.toObject(source, Representation.class, null));
+ }
+
+ @Test
+ void toObject_targetIsString_returnsText() throws IOException {
+ Representation source = new StringRepresentation("hello");
+ assertEquals("hello", converter.toObject(source, String.class, null));
+ }
+
+ @Test
+ void toObject_targetIsForm_parsesFormEntity() throws IOException {
+ Representation source = new StringRepresentation("a=b", MediaType.APPLICATION_WWW_FORM);
+ Form form = converter.toObject(source, Form.class, null);
+ assertEquals("b", form.getFirstValue("a"));
+ }
+
+ @Test
+ void toObject_targetIsInputStream_returnsStream() throws IOException {
+ Representation source = new StringRepresentation("hello");
+ assertTrue(converter.toObject(source, InputStream.class, null) instanceof InputStream);
+ }
+
+ @Test
+ void toObject_targetIsInputRepresentation_wrapsStream() throws IOException {
+ Representation source = new StringRepresentation("hello");
+ assertTrue(
+ converter.toObject(source, InputRepresentation.class, null)
+ instanceof InputRepresentation);
+ }
+
+ @Test
+ void toObject_targetIsReader_returnsReader() throws IOException {
+ Representation source = new StringRepresentation("hello");
+ assertTrue(converter.toObject(source, Reader.class, null) instanceof Reader);
+ }
+
+ @Test
+ void toObject_unrelatedNullTarget_nonObjectRepresentation_returnsNull() throws IOException {
+ Representation source = new StringRepresentation("hello");
+ assertNull(converter.toObject(source, null, null));
+ }
+
+ @Test
+ void toRepresentation_string_usesTextPlainByDefault() throws IOException {
+ Representation result = converter.toRepresentation("hello", new Variant(), null);
+ assertEquals("hello", result.getText());
+ assertEquals(MediaType.TEXT_PLAIN, result.getMediaType());
+ }
+
+ @Test
+ void toRepresentation_inputStream_usesOctetStreamByDefault() throws IOException {
+ Representation result =
+ converter.toRepresentation(
+ new ByteArrayInputStream("hi".getBytes()), new Variant(), null);
+ assertEquals(MediaType.APPLICATION_OCTET_STREAM, result.getMediaType());
+ }
+
+ @Test
+ void toRepresentation_reader_usesTextPlainByDefault() throws IOException {
+ Representation result =
+ converter.toRepresentation(new StringReader("hi"), new Variant(), null);
+ assertEquals("hi", result.getText());
+ }
+
+ @Test
+ void toRepresentation_representation_returnsUnchanged() throws IOException {
+ Representation source = new StringRepresentation("hi");
+ assertEquals(source, converter.toRepresentation(source, new Variant(), null));
+ }
+
+ @Test
+ void toRepresentation_form_returnsWebRepresentation() throws IOException {
+ Form form = new Form();
+ form.add("a", "b");
+ Representation result = converter.toRepresentation(form, new Variant(), null);
+ assertEquals(MediaType.APPLICATION_WWW_FORM, result.getMediaType());
+ }
+
+ @Test
+ void toRepresentation_serializable_returnsObjectRepresentation() throws IOException {
+ Representation result =
+ converter.toRepresentation(
+ "hello", new Variant(MediaType.APPLICATION_JAVA_OBJECT), null);
+ assertTrue(
+ result instanceof ObjectRepresentation>
+ || result instanceof StringRepresentation);
+ }
+
+ @Test
+ void toRepresentation_null_returnsNull() throws IOException {
+ assertNull(converter.toRepresentation(null, new Variant(), null));
+ }
+
+ @Test
+ void updatePreferences_form_addsFormPreference() {
+ List> preferences = new ArrayList<>();
+ converter.updatePreferences(preferences, Form.class);
+ assertEquals(MediaType.APPLICATION_WWW_FORM, preferences.get(0).getMetadata());
+ }
+
+ @Test
+ void updatePreferences_string_addsTextPreferences() {
+ List> preferences = new ArrayList<>();
+ converter.updatePreferences(preferences, String.class);
+ assertTrue(
+ preferences.stream().anyMatch(p -> p.getMetadata().equals(MediaType.TEXT_PLAIN)));
+ }
+
+ @Test
+ void updatePreferences_inputStream_addsOctetStreamPreferences() {
+ List> preferences = new ArrayList<>();
+ converter.updatePreferences(preferences, InputStream.class);
+ assertTrue(
+ preferences.stream()
+ .anyMatch(p -> p.getMetadata().equals(MediaType.APPLICATION_OCTET_STREAM)));
+ }
+
+ @Test
+ void updatePreferences_unrelatedClass_leavesPreferencesEmpty() {
+ List> preferences = new ArrayList<>();
+ converter.updatePreferences(preferences, Object.class);
+ assertTrue(preferences.isEmpty());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/converter/StatusInfoHtmlConverterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/converter/StatusInfoHtmlConverterTestCase.java
new file mode 100644
index 0000000000..d41a4f816d
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/converter/StatusInfoHtmlConverterTestCase.java
@@ -0,0 +1,113 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.converter;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.data.Status;
+import org.restlet.engine.application.StatusInfo;
+import org.restlet.representation.Representation;
+import org.restlet.representation.Variant;
+
+class StatusInfoHtmlConverterTestCase {
+
+ private final StatusInfoHtmlConverter converter = new StatusInfoHtmlConverter();
+
+ @Test
+ void getObjectClasses_compatibleVariant_returnsStatusInfo() {
+ List> classes = converter.getObjectClasses(new Variant(MediaType.TEXT_HTML));
+ assertTrue(classes.contains(StatusInfo.class));
+ }
+
+ @Test
+ void getObjectClasses_incompatibleVariant_returnsNull() {
+ assertNull(converter.getObjectClasses(new Variant(MediaType.APPLICATION_JSON)));
+ }
+
+ @Test
+ void getVariants_statusInfoClass_returnsHtmlVariants() throws IOException {
+ List> variants = converter.getVariants(StatusInfo.class);
+ assertEquals(2, variants.size());
+ }
+
+ @Test
+ void getVariants_otherClass_returnsEmptyList() throws IOException {
+ assertTrue(converter.getVariants(String.class).isEmpty());
+ }
+
+ @Test
+ void getVariants_nullClass_returnsEmptyList() throws IOException {
+ assertTrue(converter.getVariants(null).isEmpty());
+ }
+
+ @Test
+ void score_statusInfoSourceAndCompatibleTarget_returnsMaxScore() {
+ StatusInfo status = new StatusInfo(Status.SUCCESS_OK);
+ float score = converter.score(status, new Variant(MediaType.TEXT_HTML), null);
+ assertEquals(1.0F, score);
+ }
+
+ @Test
+ void score_nonStatusInfoSource_returnsNegative() {
+ float score = converter.score("not status info", new Variant(MediaType.TEXT_HTML), null);
+ assertTrue(score < 0);
+ }
+
+ @Test
+ void score_representationSourceOverload_alwaysReturnsNegative() throws IOException {
+ Representation rep = new org.restlet.representation.StringRepresentation("body");
+ assertTrue(converter.score(rep, StatusInfo.class, null) < 0);
+ }
+
+ @Test
+ void toObject_alwaysReturnsNull() throws IOException {
+ Representation rep = new org.restlet.representation.StringRepresentation("body");
+ assertNull(converter.toObject(rep, StatusInfo.class, null));
+ }
+
+ @Test
+ void toRepresentation_statusInfoWithDescriptionAndContact_includesAllSections()
+ throws IOException {
+ StatusInfo status = new StatusInfo(Status.SERVER_ERROR_INTERNAL);
+ status.setDescription("Something broke");
+ status.setContactEmail("admin@example.com");
+ status.setHomeRef("http://example.com/home");
+
+ Representation result =
+ converter.toRepresentation(status, new Variant(MediaType.TEXT_HTML), null);
+ String html = result.getText();
+
+ assertTrue(html.contains("Something broke"));
+ assertTrue(html.contains("admin@example.com"));
+ assertTrue(html.contains("home page"));
+ assertEquals(MediaType.TEXT_HTML, result.getMediaType());
+ }
+
+ @Test
+ void toRepresentation_statusInfoWithoutOptionalFields_omitsThoseSections() throws IOException {
+ StatusInfo status = new StatusInfo(0, null, null);
+ Representation result =
+ converter.toRepresentation(status, new Variant(MediaType.TEXT_HTML), null);
+ String html = result.getText();
+ assertTrue(html.contains("No information available"));
+ }
+
+ @Test
+ void toRepresentation_nonStatusInfoSource_returnsNull() throws IOException {
+ assertNull(
+ converter.toRepresentation(
+ "not status info", new Variant(MediaType.TEXT_HTML), null));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/CacheDirectiveReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/CacheDirectiveReaderTestCase.java
new file mode 100644
index 0000000000..7118ee664d
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/CacheDirectiveReaderTestCase.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CacheDirective;
+import org.restlet.data.Header;
+
+class CacheDirectiveReaderTestCase {
+
+ @Test
+ void readValue_nameOnlyDirective_parsesName() throws IOException {
+ CacheDirectiveReader reader = new CacheDirectiveReader("no-cache");
+ CacheDirective directive = reader.readValue();
+ assertEquals("no-cache", directive.getName());
+ }
+
+ @Test
+ void readValue_nameValueDirective_parsesNameAndValue() throws IOException {
+ CacheDirectiveReader reader = new CacheDirectiveReader("max-age=3600");
+ CacheDirective directive = reader.readValue();
+ assertEquals("max-age", directive.getName());
+ assertEquals("3600", directive.getValue());
+ }
+
+ @Test
+ void addValues_multipleDirectives_populatesCollection() {
+ List directives = new ArrayList<>();
+ new CacheDirectiveReader("no-cache, max-age=3600").addValues(directives);
+ assertEquals(2, directives.size());
+ assertEquals("no-cache", directives.get(0).getName());
+ assertEquals("max-age", directives.get(1).getName());
+ }
+
+ @Test
+ void addValuesStatic_fromHeader_populatesCollection() {
+ List directives = new ArrayList<>();
+ Header header = new Header("Cache-Control", "no-cache");
+ CacheDirectiveReader.addValues(header, directives);
+ assertEquals(1, directives.size());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/CacheDirectiveWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/CacheDirectiveWriterTestCase.java
new file mode 100644
index 0000000000..9714e3c163
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/CacheDirectiveWriterTestCase.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CacheDirective;
+
+class CacheDirectiveWriterTestCase {
+
+ @Test
+ void write_nameOnlyDirective_writesNameOnly() {
+ String result = CacheDirectiveWriter.write(Arrays.asList(CacheDirective.noCache()));
+ assertEquals("no-cache", result);
+ }
+
+ @Test
+ void write_nameValueDirective_writesNameEqualsValue() {
+ String result = CacheDirectiveWriter.write(Arrays.asList(CacheDirective.maxAge(3600)));
+ assertTrue(result.startsWith("max-age="));
+ }
+
+ @Test
+ void write_multipleDirectives_joinsWithSeparator() {
+ String result =
+ CacheDirectiveWriter.write(
+ Arrays.asList(CacheDirective.noCache(), CacheDirective.noStore()));
+ assertEquals("no-cache, no-store", result);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/ChallengeRequestReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/ChallengeRequestReaderTestCase.java
new file mode 100644
index 0000000000..6737670f73
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/ChallengeRequestReaderTestCase.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.ChallengeRequest;
+
+class ChallengeRequestReaderTestCase {
+
+ @Test
+ void readValue_schemeOnly_parsesSchemeName() throws IOException {
+ ChallengeRequestReader reader = new ChallengeRequestReader("Basic");
+ ChallengeRequest result = reader.readValue();
+ assertEquals("Basic", result.getScheme().getTechnicalName());
+ }
+
+ @Test
+ void readValue_schemeWithParameters_capturesRawValue() throws IOException {
+ ChallengeRequestReader reader = new ChallengeRequestReader("Digest realm=\"testrealm\"");
+ ChallengeRequest result = reader.readValue();
+ assertEquals("Digest", result.getScheme().getTechnicalName());
+ assertTrue(result.getRawValue().contains("realm"));
+ }
+
+ @Test
+ void readValue_emptyHeader_returnsNull() throws IOException {
+ ChallengeRequestReader reader = new ChallengeRequestReader("");
+ assertNull(reader.readValue());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/ChallengeWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/ChallengeWriterTestCase.java
new file mode 100644
index 0000000000..44c76513bd
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/ChallengeWriterTestCase.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Parameter;
+
+class ChallengeWriterTestCase {
+
+ @Test
+ void append_challengeRequest_isNoOp() {
+ ChallengeWriter writer = new ChallengeWriter();
+ writer.append((org.restlet.data.ChallengeRequest) null);
+ assertEquals("", writer.toString());
+ }
+
+ @Test
+ void isFirstChallengeParameter_initiallyTrue() {
+ assertTrue(new ChallengeWriter().isFirstChallengeParameter());
+ }
+
+ @Test
+ void appendChallengeParameter_name_writesTokenWithoutLeadingComma() {
+ ChallengeWriter writer = new ChallengeWriter();
+ writer.appendChallengeParameter("realm");
+ assertEquals("realm", writer.toString());
+ assertFalse(writer.isFirstChallengeParameter());
+ }
+
+ @Test
+ void appendChallengeParameter_nameAndValue_joinsWithEquals() {
+ ChallengeWriter writer = new ChallengeWriter();
+ writer.appendChallengeParameter("realm", "test");
+ assertEquals("realm=test", writer.toString());
+ }
+
+ @Test
+ void appendChallengeParameter_secondParameter_prefixedWithComma() {
+ ChallengeWriter writer = new ChallengeWriter();
+ writer.appendChallengeParameter("a", "1");
+ writer.appendChallengeParameter("b", "2");
+ assertEquals("a=1, b=2", writer.toString());
+ }
+
+ @Test
+ void appendChallengeParameter_fromParameterObject_delegatesToNameValueOverload() {
+ ChallengeWriter writer = new ChallengeWriter();
+ writer.appendChallengeParameter(new Parameter("realm", "test"));
+ assertEquals("realm=test", writer.toString());
+ }
+
+ @Test
+ void appendQuotedChallengeParameter_quotesValue() {
+ ChallengeWriter writer = new ChallengeWriter();
+ writer.appendQuotedChallengeParameter("realm", "test realm");
+ assertEquals("realm=\"test realm\"", writer.toString());
+ }
+
+ @Test
+ void appendQuotedChallengeParameter_fromParameterObject_delegatesToNameValueOverload() {
+ ChallengeWriter writer = new ChallengeWriter();
+ writer.appendQuotedChallengeParameter(new Parameter("realm", "test realm"));
+ assertEquals("realm=\"test realm\"", writer.toString());
+ }
+
+ @Test
+ void setFirstChallengeParameter_updatesState() {
+ ChallengeWriter writer = new ChallengeWriter();
+ writer.setFirstChallengeParameter(false);
+ assertFalse(writer.isFirstChallengeParameter());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/ContentTypeReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/ContentTypeReaderTestCase.java
new file mode 100644
index 0000000000..820455af95
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/ContentTypeReaderTestCase.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.MediaType;
+
+class ContentTypeReaderTestCase {
+
+ @Test
+ void readValue_mediaTypeOnly_parsesMediaType() throws IOException {
+ ContentType result = new ContentTypeReader("text/plain").readValue();
+ assertEquals(MediaType.TEXT_PLAIN, result.getMediaType());
+ assertNull(result.getCharacterSet());
+ }
+
+ @Test
+ void readValue_withCharsetParameter_extractsCharacterSet() throws IOException {
+ ContentType result = new ContentTypeReader("text/plain; charset=UTF-8").readValue();
+ assertEquals(MediaType.TEXT_PLAIN, result.getMediaType());
+ assertEquals(CharacterSet.UTF_8, result.getCharacterSet());
+ }
+
+ @Test
+ void readValue_withOtherParameter_keepsItOnMediaType() throws IOException {
+ ContentType result =
+ new ContentTypeReader("multipart/form-data; boundary=abc123").readValue();
+ assertEquals("abc123", result.getMediaType().getParameters().getFirstValue("boundary"));
+ }
+
+ @Test
+ void readValue_withQuotedParameterValue_unquotesIt() throws IOException {
+ ContentType result =
+ new ContentTypeReader("multipart/form-data; boundary=\"abc 123\"").readValue();
+ assertEquals("abc 123", result.getMediaType().getParameters().getFirstValue("boundary"));
+ }
+
+ @Test
+ void readValue_emptyMediaTypeName_throws() {
+ ContentTypeReader reader = new ContentTypeReader("; charset=UTF-8");
+ assertThrows(IOException.class, reader::readValue);
+ }
+
+ @Test
+ void readValue_emptyParameterName_throws() {
+ ContentTypeReader reader = new ContentTypeReader("text/plain; =UTF-8");
+ assertThrows(IOException.class, reader::readValue);
+ }
+
+ @Test
+ void readValue_parameterWithoutValue_isRecordedWithNullValue() throws IOException {
+ ContentType result = new ContentTypeReader("text/plain; charset").readValue();
+ assertNull(result.getMediaType().getParameters().getFirstValue("charset"));
+ }
+
+ @Test
+ void readValue_emptyHeader_returnsNull() throws IOException {
+ assertNull(new ContentTypeReader("").readValue());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/CookieSettingReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/CookieSettingReaderTestCase.java
new file mode 100644
index 0000000000..eed6357ed5
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/CookieSettingReaderTestCase.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CookieSetting;
+
+class CookieSettingReaderTestCase {
+
+ @Test
+ void read_nameAndValue_parsesBoth() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value");
+ assertEquals("name", cookieSetting.getName());
+ assertEquals("value", cookieSetting.getValue());
+ }
+
+ @Test
+ void read_emptyHeader_returnsNull() {
+ assertNull(CookieSettingReader.read(""));
+ }
+
+ @Test
+ void read_dollarPrefixedAttribute_isSkipped() {
+ CookieSetting cookieSetting = CookieSettingReader.read("$Version=1; name=value");
+ assertEquals("name", cookieSetting.getName());
+ }
+
+ @Test
+ void read_path_setsPath() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; path=/app");
+ assertEquals("/app", cookieSetting.getPath());
+ }
+
+ @Test
+ void read_domain_setsDomain() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; domain=example.com");
+ assertEquals("example.com", cookieSetting.getDomain());
+ }
+
+ @Test
+ void read_comment_setsComment() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; comment=hello");
+ assertEquals("hello", cookieSetting.getComment());
+ }
+
+ @Test
+ void read_discard_setsMaxAgeToNegativeOne() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; discard");
+ assertEquals(-1, cookieSetting.getMaxAge());
+ }
+
+ @Test
+ void read_validMaxAge_setsMaxAge() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; max-age=3600");
+ assertEquals(3600, cookieSetting.getMaxAge());
+ }
+
+ @Test
+ void read_invalidMaxAge_defaultsToIntegerMax() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; max-age=notanumber");
+ assertEquals(Integer.MAX_VALUE, cookieSetting.getMaxAge());
+ }
+
+ @Test
+ void read_secureFlag_setsSecureTrue() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; secure");
+ assertTrue(cookieSetting.isSecure());
+ }
+
+ @Test
+ void read_httpOnlyFlag_setsAccessRestrictedTrue() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; httpOnly");
+ assertTrue(cookieSetting.isAccessRestricted());
+ }
+
+ @Test
+ void read_version_setsVersion() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; version=1");
+ assertEquals(1, cookieSetting.getVersion());
+ }
+
+ @Test
+ void read_unknownAttribute_isIgnored() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; unknown=foo");
+ assertEquals("name", cookieSetting.getName());
+ }
+
+ @Test
+ void read_validExpiresDate_setsPositiveMaxAge() {
+ java.util.Date future = new java.util.Date(System.currentTimeMillis() + 3_600_000L);
+ String expires =
+ org.restlet.engine.util.DateUtils.format(
+ future, org.restlet.engine.util.DateUtils.FORMAT_RFC_1123.get(0));
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; expires=" + expires);
+ assertTrue(cookieSetting.getMaxAge() > 0);
+ }
+
+ @Test
+ void read_invalidExpiresDate_leavesMaxAgeAtDefault() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=value; expires=not-a-date");
+ assertEquals(-1, cookieSetting.getMaxAge());
+ }
+
+ @Test
+ void read_quotedValue_isUnquoted() {
+ CookieSetting cookieSetting = CookieSettingReader.read("name=\"quoted value\"");
+ assertEquals("quoted value", cookieSetting.getValue());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/CookieSettingWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/CookieSettingWriterTestCase.java
new file mode 100644
index 0000000000..e8eb73549e
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/CookieSettingWriterTestCase.java
@@ -0,0 +1,117 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CookieSetting;
+
+class CookieSettingWriterTestCase {
+
+ @Test
+ void write_simpleCookie_writesNameEqualsValue() {
+ assertEquals("name=value", CookieSettingWriter.write(new CookieSetting("name", "value")));
+ }
+
+ @Test
+ void write_missingName_throwsIllegalArgumentException() {
+ CookieSetting cookieSetting = new CookieSetting(0, "", "value");
+ assertThrows(
+ IllegalArgumentException.class, () -> CookieSettingWriter.write(cookieSetting));
+ }
+
+ @Test
+ void write_versionOne_appendsQuotedVersion() {
+ CookieSetting cookieSetting = new CookieSetting(1, "name", "value");
+ assertTrue(
+ CookieSettingWriter.write(cookieSetting)
+ .startsWith("name=\"value\"; Version=\"1\""));
+ }
+
+ @Test
+ void write_pathVersionZero_appendsUnquotedPath() {
+ CookieSetting cookieSetting = new CookieSetting(0, "name", "value", "/path", null);
+ assertEquals("name=value; Path=/path", CookieSettingWriter.write(cookieSetting));
+ }
+
+ @Test
+ void write_pathVersionOne_appendsQuotedPath() {
+ CookieSetting cookieSetting = new CookieSetting(1, "name", "value", "/path", null);
+ assertTrue(CookieSettingWriter.write(cookieSetting).contains("; Path=\"/path\""));
+ }
+
+ @Test
+ void write_maxAgeVersionZero_appendsExpiresDate() {
+ CookieSetting cookieSetting = new CookieSetting("name", "value");
+ cookieSetting.setMaxAge(3600);
+ assertTrue(CookieSettingWriter.write(cookieSetting).contains("; Expires="));
+ }
+
+ @Test
+ void write_maxAgeVersionOne_appendsMaxAge() {
+ CookieSetting cookieSetting = new CookieSetting(1, "name", "value");
+ cookieSetting.setMaxAge(3600);
+ assertTrue(CookieSettingWriter.write(cookieSetting).contains("; Max-Age=\"3600\""));
+ }
+
+ @Test
+ void write_discardVersionOne_appendsDiscard() {
+ CookieSetting cookieSetting = new CookieSetting(1, "name", "value");
+ cookieSetting.setMaxAge(-1);
+ assertTrue(CookieSettingWriter.write(cookieSetting).contains("; Discard"));
+ }
+
+ @Test
+ void write_domain_isLowercased() {
+ CookieSetting cookieSetting = new CookieSetting(0, "name", "value", null, "EXAMPLE.COM");
+ assertTrue(CookieSettingWriter.write(cookieSetting).contains("; Domain=example.com"));
+ }
+
+ @Test
+ void write_secureFlag_appendsSecure() {
+ CookieSetting cookieSetting = new CookieSetting("name", "value");
+ cookieSetting.setSecure(true);
+ assertTrue(CookieSettingWriter.write(cookieSetting).contains("; Secure"));
+ }
+
+ @Test
+ void write_accessRestrictedFlag_appendsHttpOnly() {
+ CookieSetting cookieSetting = new CookieSetting("name", "value");
+ cookieSetting.setAccessRestricted(true);
+ assertTrue(CookieSettingWriter.write(cookieSetting).contains("; HttpOnly"));
+ }
+
+ @Test
+ void write_commentVersionOne_appendsQuotedComment() {
+ CookieSetting cookieSetting = new CookieSetting(1, "name", "value");
+ cookieSetting.setComment("a comment");
+ assertTrue(CookieSettingWriter.write(cookieSetting).contains("; Comment=\"a comment\""));
+ }
+
+ @Test
+ void write_commentVersionZero_isIgnored() {
+ CookieSetting cookieSetting = new CookieSetting("name", "value");
+ cookieSetting.setComment("a comment");
+ assertTrue(!CookieSettingWriter.write(cookieSetting).contains("Comment"));
+ }
+
+ @Test
+ void write_listOfCookieSettings_joinsWithSemicolon() {
+ List settings =
+ Arrays.asList(new CookieSetting("a", "1"), new CookieSetting("b", "2"));
+ String result = CookieSettingWriter.write(settings);
+ assertTrue(result.contains("a=1"));
+ assertTrue(result.contains("b=2"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/CookieWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/CookieWriterTestCase.java
new file mode 100644
index 0000000000..eeac3bfa06
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/CookieWriterTestCase.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Cookie;
+
+class CookieWriterTestCase {
+
+ @Test
+ void write_simpleCookie_writesNameEqualsValue() {
+ assertEquals("name=value", CookieWriter.write(new Cookie("name", "value")));
+ }
+
+ @Test
+ void write_versionedCookie_quotesValueAndAppendsPathAndDomain() {
+ Cookie cookie = new Cookie(1, "name", "value", "/path", "example.com");
+ String result = CookieWriter.write(cookie);
+ assertEquals("name=\"value\"; $Path=\"/path\"; $Domain=\"example.com\"", result);
+ }
+
+ @Test
+ void write_missingName_throwsIllegalArgumentException() {
+ Cookie cookie = new Cookie(0, "", "value");
+ assertThrows(IllegalArgumentException.class, () -> CookieWriter.write(cookie));
+ }
+
+ @Test
+ void write_listOfCookies_prefixesVersionAndJoinsWithSemicolon() {
+ List cookies = Arrays.asList(new Cookie(1, "a", "1"), new Cookie(1, "b", "2"));
+ String result = CookieWriter.write(cookies);
+ assertEquals("$Version=\"1\"; a=\"1\"; b=\"2\"", result);
+ }
+
+ @Test
+ void write_listWithVersionZero_omitsVersionPrefix() {
+ List cookies = Arrays.asList(new Cookie("a", "1"), new Cookie("b", "2"));
+ assertEquals("a=1; b=2", CookieWriter.write(cookies));
+ }
+
+ @Test
+ void write_emptyList_returnsEmptyString() {
+ assertEquals("", CookieWriter.write(List.of()));
+ }
+
+ @Test
+ void appendValue_versionZero_appendsUnquoted() {
+ assertEquals("value", new CookieWriter().appendValue("value", 0).toString());
+ }
+
+ @Test
+ void appendValue_nonZeroVersion_appendsQuoted() {
+ assertEquals("\"value\"", new CookieWriter().appendValue("value", 1).toString());
+ }
+
+ @Test
+ void getCookies_matchingNameInDestinationMap_isUpdated() {
+ Cookie cookie = new Cookie("name", "value");
+ Map destination = new HashMap<>();
+ destination.put("name", null);
+
+ CookieWriter.getCookies(List.of(cookie), destination);
+
+ assertEquals(cookie, destination.get("name"));
+ }
+
+ @Test
+ void getCookies_nonMatchingName_isIgnored() {
+ Cookie cookie = new Cookie("other", "value");
+ Map destination = new HashMap<>();
+ destination.put("name", null);
+
+ CookieWriter.getCookies(List.of(cookie), destination);
+
+ assertTrue(destination.containsKey("name"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/DateWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/DateWriterTestCase.java
new file mode 100644
index 0000000000..a6f35cc4c6
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/DateWriterTestCase.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.Test;
+import org.restlet.engine.util.DateUtils;
+
+class DateWriterTestCase {
+
+ private static Date date(int year, int month, int day, int hour, int minute, int second) {
+ Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+ cal.clear();
+ cal.set(year, month, day, hour, minute, second);
+ return cal.getTime();
+ }
+
+ @Test
+ void write_defaultFormat_matchesDateUtilsDefault() {
+ Date date = date(2024, Calendar.MARCH, 5, 13, 7, 9);
+ assertEquals(DateUtils.format(date), DateWriter.write(date));
+ }
+
+ @Test
+ void write_cookieFalse_matchesDefaultFormat() {
+ Date date = date(2024, Calendar.MARCH, 5, 13, 7, 9);
+ assertEquals(DateWriter.write(date), DateWriter.write(date, false));
+ }
+
+ @Test
+ void write_cookieTrue_matchesRfc1036Format() {
+ Date date = date(2024, Calendar.MARCH, 5, 13, 7, 9);
+ assertEquals(
+ DateUtils.format(date, DateUtils.FORMAT_RFC_1036.getFirst()),
+ DateWriter.write(date, true));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/DimensionReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/DimensionReaderTestCase.java
new file mode 100644
index 0000000000..7a366a1229
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/DimensionReaderTestCase.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Dimension;
+import org.restlet.data.Header;
+
+class DimensionReaderTestCase {
+
+ @Test
+ void readValue_accept_returnsMediaType() throws IOException {
+ assertEquals(
+ Dimension.MEDIA_TYPE,
+ new DimensionReader(HeaderConstants.HEADER_ACCEPT).readValue());
+ }
+
+ @Test
+ void readValue_acceptCharset_returnsCharacterSet() throws IOException {
+ assertEquals(
+ Dimension.CHARACTER_SET,
+ new DimensionReader(HeaderConstants.HEADER_ACCEPT_CHARSET).readValue());
+ }
+
+ @Test
+ void readValue_acceptEncoding_returnsEncoding() throws IOException {
+ assertEquals(
+ Dimension.ENCODING,
+ new DimensionReader(HeaderConstants.HEADER_ACCEPT_ENCODING).readValue());
+ }
+
+ @Test
+ void readValue_acceptLanguage_returnsLanguage() throws IOException {
+ assertEquals(
+ Dimension.LANGUAGE,
+ new DimensionReader(HeaderConstants.HEADER_ACCEPT_LANGUAGE).readValue());
+ }
+
+ @Test
+ void readValue_authorization_returnsAuthorization() throws IOException {
+ assertEquals(
+ Dimension.AUTHORIZATION,
+ new DimensionReader(HeaderConstants.HEADER_AUTHORIZATION).readValue());
+ }
+
+ @Test
+ void readValue_userAgent_returnsClientAgent() throws IOException {
+ assertEquals(
+ Dimension.CLIENT_AGENT,
+ new DimensionReader(HeaderConstants.HEADER_USER_AGENT).readValue());
+ }
+
+ @Test
+ void readValue_origin_returnsOrigin() throws IOException {
+ assertEquals(
+ Dimension.ORIGIN, new DimensionReader(HeaderConstants.HEADER_ORIGIN).readValue());
+ }
+
+ @Test
+ void readValue_star_returnsUnspecified() throws IOException {
+ assertEquals(Dimension.UNSPECIFIED, new DimensionReader("*").readValue());
+ }
+
+ @Test
+ void readValue_unknownValue_returnsNull() throws IOException {
+ assertNull(new DimensionReader("something-else").readValue());
+ }
+
+ @Test
+ void addValuesStatic_fromHeader_populatesCollection() {
+ List dimensions = new ArrayList<>();
+ Header header = new Header("Vary", HeaderConstants.HEADER_ACCEPT);
+ DimensionReader.addValues(header, dimensions);
+ assertEquals(1, dimensions.size());
+ assertEquals(Dimension.MEDIA_TYPE, dimensions.get(0));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/DimensionWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/DimensionWriterTestCase.java
new file mode 100644
index 0000000000..1ac1868264
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/DimensionWriterTestCase.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Dimension;
+
+class DimensionWriterTestCase {
+
+ @Test
+ void write_nullOrEmpty_returnsEmptyString() {
+ assertEquals("", DimensionWriter.write((Collection) null));
+ assertEquals("", DimensionWriter.write(Collections.emptyList()));
+ }
+
+ @Test
+ void write_clientAddress_returnsStar() {
+ assertEquals("*", DimensionWriter.write(Collections.singleton(Dimension.CLIENT_ADDRESS)));
+ }
+
+ @Test
+ void write_time_returnsStar() {
+ assertEquals("*", DimensionWriter.write(Collections.singleton(Dimension.TIME)));
+ }
+
+ @Test
+ void write_unspecified_returnsStar() {
+ assertEquals("*", DimensionWriter.write(Collections.singleton(Dimension.UNSPECIFIED)));
+ }
+
+ @Test
+ void write_characterSet_writesAcceptCharsetHeaderName() {
+ assertEquals(
+ HeaderConstants.HEADER_ACCEPT_CHARSET,
+ DimensionWriter.write(Collections.singleton(Dimension.CHARACTER_SET)));
+ }
+
+ @Test
+ void write_clientAgent_writesUserAgentHeaderName() {
+ assertEquals(
+ HeaderConstants.HEADER_USER_AGENT,
+ DimensionWriter.write(Collections.singleton(Dimension.CLIENT_AGENT)));
+ }
+
+ @Test
+ void write_encoding_writesAcceptEncodingHeaderName() {
+ assertEquals(
+ HeaderConstants.HEADER_ACCEPT_ENCODING,
+ DimensionWriter.write(Collections.singleton(Dimension.ENCODING)));
+ }
+
+ @Test
+ void write_language_writesAcceptLanguageHeaderName() {
+ assertEquals(
+ HeaderConstants.HEADER_ACCEPT_LANGUAGE,
+ DimensionWriter.write(Collections.singleton(Dimension.LANGUAGE)));
+ }
+
+ @Test
+ void write_mediaType_writesAcceptHeaderName() {
+ assertEquals(
+ HeaderConstants.HEADER_ACCEPT,
+ DimensionWriter.write(Collections.singleton(Dimension.MEDIA_TYPE)));
+ }
+
+ @Test
+ void write_authorization_writesAuthorizationHeaderName() {
+ assertEquals(
+ HeaderConstants.HEADER_AUTHORIZATION,
+ DimensionWriter.write(Collections.singleton(Dimension.AUTHORIZATION)));
+ }
+
+ @Test
+ void write_origin_writesOriginHeaderName() {
+ assertEquals(
+ HeaderConstants.HEADER_ORIGIN,
+ DimensionWriter.write(Collections.singleton(Dimension.ORIGIN)));
+ }
+
+ @Test
+ void write_multipleDimensions_joinsWithSeparator() {
+ LinkedHashSet dimensions =
+ new LinkedHashSet<>(Arrays.asList(Dimension.MEDIA_TYPE, Dimension.ENCODING));
+ assertEquals(
+ HeaderConstants.HEADER_ACCEPT + ", " + HeaderConstants.HEADER_ACCEPT_ENCODING,
+ DimensionWriter.write(dimensions));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/EncodingReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/EncodingReaderTestCase.java
new file mode 100644
index 0000000000..9d5017062c
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/EncodingReaderTestCase.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Encoding;
+
+class EncodingReaderTestCase {
+
+ @Test
+ void readValue_gzip_parsesEncoding() throws IOException {
+ assertEquals(Encoding.GZIP, new EncodingReader("gzip").readValue());
+ }
+
+ @Test
+ void addValues_excludesIdentityEncoding() {
+ List encodings = new ArrayList<>();
+ new EncodingReader("identity, gzip").addValues(encodings);
+ assertEquals(1, encodings.size());
+ assertTrue(encodings.contains(Encoding.GZIP));
+ assertFalse(encodings.contains(Encoding.IDENTITY));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/EncodingWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/EncodingWriterTestCase.java
new file mode 100644
index 0000000000..a21c6be8c7
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/EncodingWriterTestCase.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Encoding;
+
+class EncodingWriterTestCase {
+
+ @Test
+ void write_gzip_writesEncodingName() {
+ assertEquals(Encoding.GZIP.getName(), EncodingWriter.write(Arrays.asList(Encoding.GZIP)));
+ }
+
+ @Test
+ void canWrite_identity_returnsFalse() {
+ EncodingWriter writer = new EncodingWriter();
+ assertFalse(writer.canWrite(Encoding.IDENTITY));
+ }
+
+ @Test
+ void write_identityIsSkipped() {
+ String result = EncodingWriter.write(Arrays.asList(Encoding.IDENTITY, Encoding.GZIP));
+ assertEquals(Encoding.GZIP.getName(), result);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/ExpectationReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/ExpectationReaderTestCase.java
new file mode 100644
index 0000000000..ffef5c061b
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/ExpectationReaderTestCase.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.ClientInfo;
+import org.restlet.data.Expectation;
+
+class ExpectationReaderTestCase {
+
+ @Test
+ void readValue_nameOnly_parsesName() throws IOException {
+ ExpectationReader reader = new ExpectationReader("100-continue");
+ Expectation expectation = reader.readValue();
+ assertEquals("100-continue", expectation.getName());
+ assertTrue(expectation.getParameters().isEmpty());
+ }
+
+ @Test
+ void readValue_withParameter_addsParameter() throws IOException {
+ ExpectationReader reader = new ExpectationReader("foo;bar=baz");
+ Expectation expectation = reader.readValue();
+ assertEquals("foo", expectation.getName());
+ assertEquals(1, expectation.getParameters().size());
+ assertEquals("bar", expectation.getParameters().get(0).getName());
+ assertEquals("baz", expectation.getParameters().get(0).getValue());
+ }
+
+ @Test
+ void addValues_populatesClientInfoExpectations() {
+ ClientInfo clientInfo = new ClientInfo();
+ ExpectationReader.addValues("100-continue", clientInfo);
+ assertEquals(1, clientInfo.getExpectations().size());
+ assertEquals("100-continue", clientInfo.getExpectations().get(0).getName());
+ }
+
+ @Test
+ void addValues_nullHeader_leavesExpectationsUnchanged() {
+ ClientInfo clientInfo = new ClientInfo();
+ ExpectationReader.addValues(null, clientInfo);
+ assertTrue(clientInfo.getExpectations().isEmpty());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/HeaderReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/HeaderReaderTestCase.java
new file mode 100644
index 0000000000..8e674db447
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/HeaderReaderTestCase.java
@@ -0,0 +1,348 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Header;
+import org.restlet.data.Parameter;
+
+class HeaderReaderTestCase {
+
+ @Test
+ void readDate_notCookieFormat_parsesRfc1123() {
+ Date date = HeaderReader.readDate("Sun, 06 Nov 1994 08:49:37 GMT", false);
+ assertEquals(784111777000L, date.getTime());
+ }
+
+ @Test
+ void readDate_cookieFormat_parsesRfc1036() {
+ Date date = HeaderReader.readDate("Sunday, 06-Nov-94 08:49:37 GMT", true);
+ assertEquals(784111777000L, date.getTime());
+ }
+
+ @Test
+ void readHeaderFromCharSequence_endOfHeaders_returnsNull() throws IOException {
+ assertNull(HeaderReader.readHeader(""));
+ }
+
+ @Test
+ void readHeaderFromCharSequence_crlfOnly_returnsNull() throws IOException {
+ assertNull(HeaderReader.readHeader("\r\n"));
+ }
+
+ @Test
+ void readHeaderFromCharSequence_crWithoutLf_throws() {
+ assertThrows(IOException.class, () -> HeaderReader.readHeader("\rX"));
+ }
+
+ @Test
+ void readHeaderFromCharSequence_missingColon_throws() {
+ assertThrows(IOException.class, () -> HeaderReader.readHeader("NameOnly"));
+ }
+
+ @Test
+ void readHeaderFromCharSequence_nameOnly_returnsHeaderWithNullValue() throws IOException {
+ Header header = HeaderReader.readHeader("Name:");
+ assertEquals("Name", header.getName());
+ }
+
+ @Test
+ void readHeaderFromCharSequence_nameAndValue_parsesBoth() throws IOException {
+ Header header = HeaderReader.readHeader("Content-Type: text/plain");
+ assertEquals("Content-Type", header.getName());
+ assertEquals("text/plain", header.getValue());
+ }
+
+ @Test
+ void readHeaderFromInputStream_nameAndValue_parsesBoth() throws IOException {
+ ByteArrayInputStream is =
+ new ByteArrayInputStream("Content-Type: text/plain\r\n".getBytes());
+ Header header = HeaderReader.readHeader(is, new StringBuilder());
+ assertEquals("Content-Type", header.getName());
+ assertEquals("text/plain", header.getValue());
+ }
+
+ @Test
+ void readHeaderFromInputStream_endOfHeaders_returnsNull() throws IOException {
+ ByteArrayInputStream is = new ByteArrayInputStream("\r\n".getBytes());
+ assertNull(HeaderReader.readHeader(is, new StringBuilder()));
+ }
+
+ @Test
+ void readHeaderFromInputStream_crWithoutLf_throws() {
+ ByteArrayInputStream is = new ByteArrayInputStream("\rX".getBytes());
+ assertThrows(IOException.class, () -> HeaderReader.readHeader(is, new StringBuilder()));
+ }
+
+ @Test
+ void readHeaderFromInputStream_missingColon_throws() {
+ ByteArrayInputStream is = new ByteArrayInputStream("NameOnly".getBytes());
+ assertThrows(IOException.class, () -> HeaderReader.readHeader(is, new StringBuilder()));
+ }
+
+ @Test
+ void readHeaderFromInputStream_missingTrailingLineFeed_throws() {
+ ByteArrayInputStream is = new ByteArrayInputStream("Name: value\r".getBytes());
+ assertThrows(IOException.class, () -> HeaderReader.readHeader(is, new StringBuilder()));
+ }
+
+ @Test
+ void peek_atStart_returnsFirstCharacterWithoutAdvancing() {
+ HeaderReader reader = new HeaderReader<>("abc");
+ assertEquals('a', reader.peek());
+ assertEquals('a', reader.peek());
+ }
+
+ @Test
+ void peek_emptyHeader_returnsMinusOne() {
+ HeaderReader reader = new HeaderReader<>("");
+ assertEquals(-1, reader.peek());
+ }
+
+ @Test
+ void read_advancesIndexUntilEnd() {
+ HeaderReader reader = new HeaderReader<>("ab");
+ assertEquals('a', reader.read());
+ assertEquals('b', reader.read());
+ assertEquals(-1, reader.read());
+ }
+
+ @Test
+ void unread_movesIndexBackOneCharacter() {
+ HeaderReader reader = new HeaderReader<>("abc");
+ reader.read();
+ reader.read();
+ reader.unread();
+ assertEquals('b', reader.read());
+ }
+
+ @Test
+ void markAndReset_repositionsToMarkedIndex() {
+ HeaderReader reader = new HeaderReader<>("abc");
+ reader.read();
+ reader.mark();
+ reader.read();
+ reader.reset();
+ assertEquals('b', reader.read());
+ }
+
+ @Test
+ void readToken_stopsAtNonTokenCharacter() {
+ HeaderReader reader = new HeaderReader<>("abc=def");
+ assertEquals("abc", reader.readToken());
+ assertEquals('=', reader.read());
+ }
+
+ @Test
+ void readDigits_stopsAtNonTokenCharacter() {
+ HeaderReader reader = new HeaderReader<>("123;abc");
+ assertEquals("123", reader.readDigits());
+ assertEquals(';', reader.read());
+ }
+
+ @Test
+ void readQuotedString_simpleValue_returnsUnquoted() throws IOException {
+ HeaderReader reader = new HeaderReader<>("\"hello\"");
+ assertEquals("hello", reader.readQuotedString());
+ }
+
+ @Test
+ void readQuotedString_withEscapedCharacter_unescapesIt() throws IOException {
+ HeaderReader reader = new HeaderReader<>("\"a\\\"b\"");
+ assertEquals("a\"b", reader.readQuotedString());
+ }
+
+ @Test
+ void readQuotedString_withoutLeadingQuote_throws() {
+ HeaderReader reader = new HeaderReader<>("hello\"");
+ assertThrows(IOException.class, reader::readQuotedString);
+ }
+
+ @Test
+ void readQuotedString_unterminated_throws() {
+ HeaderReader reader = new HeaderReader<>("\"hello");
+ assertThrows(IOException.class, reader::readQuotedString);
+ }
+
+ @Test
+ void readActualNamedValue_quotedString_returnsUnquoted() throws IOException {
+ HeaderReader reader = new HeaderReader<>("\"quoted value\"");
+ assertEquals("quoted value", reader.readActualNamedValue());
+ }
+
+ @Test
+ void readActualNamedValue_token_returnsToken() throws IOException {
+ HeaderReader reader = new HeaderReader<>("token123");
+ assertEquals("token123", reader.readActualNamedValue());
+ }
+
+ @Test
+ void readComment_simpleComment_returnsContent() throws IOException {
+ HeaderReader reader = new HeaderReader<>("(a comment)");
+ assertEquals("a comment", reader.readComment());
+ }
+
+ @Test
+ void readComment_nestedComment_returnsFullContent() throws IOException {
+ HeaderReader reader = new HeaderReader<>("(outer (inner) end)");
+ assertEquals("outer (inner) end", reader.readComment());
+ }
+
+ @Test
+ void readComment_withoutLeadingParenthesis_throws() {
+ HeaderReader reader = new HeaderReader<>("no parenthesis)");
+ assertThrows(IOException.class, reader::readComment);
+ }
+
+ @Test
+ void readComment_unterminated_throws() {
+ HeaderReader reader = new HeaderReader<>("(unterminated");
+ assertThrows(IOException.class, reader::readComment);
+ }
+
+ @Test
+ void readRawText_stopsAtSpace() {
+ HeaderReader reader = new HeaderReader<>("abc def");
+ assertEquals("abc", reader.readRawText());
+ }
+
+ @Test
+ void readRawText_emptyInput_returnsNull() {
+ HeaderReader reader = new HeaderReader<>("");
+ assertNull(reader.readRawText());
+ }
+
+ @Test
+ void readRawValue_trimsTrailingSpacesAndStopsAtComma() {
+ HeaderReader reader = new HeaderReader<>(" value with spaces , next");
+ assertEquals("value with spaces", reader.readRawValue());
+ }
+
+ @Test
+ void readRawValue_emptyInput_returnsNull() {
+ HeaderReader reader = new HeaderReader<>("");
+ assertNull(reader.readRawValue());
+ }
+
+ @Test
+ void readParameter_nameOnly_returnsNullValueParameter() throws IOException {
+ HeaderReader reader = new HeaderReader<>("flag");
+ Parameter parameter = reader.readParameter();
+ assertEquals("flag", parameter.getName());
+ assertNull(parameter.getValue());
+ }
+
+ @Test
+ void readParameter_nameAndValue_parsesBoth() throws IOException {
+ HeaderReader reader = new HeaderReader<>("name=value");
+ Parameter parameter = reader.readParameter();
+ assertEquals("name", parameter.getName());
+ assertEquals("value", parameter.getValue());
+ }
+
+ @Test
+ void readParameter_emptyName_throws() {
+ HeaderReader reader = new HeaderReader<>("=value");
+ assertThrows(IOException.class, reader::readParameter);
+ }
+
+ @Test
+ void skipParameterSeparator_semicolonPresent_returnsTrueAndSkipsSpaces() {
+ HeaderReader reader = new HeaderReader<>(" ; rest");
+ assertTrue(reader.skipParameterSeparator());
+ assertEquals('r', reader.read());
+ }
+
+ @Test
+ void skipParameterSeparator_noSeparator_returnsFalseAndDoesNotConsume() {
+ HeaderReader reader = new HeaderReader<>("rest");
+ assertFalse(reader.skipParameterSeparator());
+ assertEquals('r', reader.read());
+ }
+
+ @Test
+ void skipValueSeparator_commaPresent_returnsTrue() {
+ HeaderReader reader = new HeaderReader<>(" , rest");
+ assertTrue(reader.skipValueSeparator());
+ assertEquals('r', reader.read());
+ }
+
+ @Test
+ void skipValueSeparator_noSeparator_returnsFalse() {
+ HeaderReader reader = new HeaderReader<>("rest");
+ assertFalse(reader.skipValueSeparator());
+ }
+
+ @Test
+ void skipSpaces_leadingSpaces_returnsTrueAndSkipsThem() {
+ HeaderReader reader = new HeaderReader<>(" rest");
+ assertTrue(reader.skipSpaces());
+ assertEquals('r', reader.read());
+ }
+
+ @Test
+ void skipSpaces_noSpaces_returnsFalse() {
+ HeaderReader reader = new HeaderReader<>("rest");
+ assertFalse(reader.skipSpaces());
+ }
+
+ @Test
+ void readValue_defaultImplementation_returnsNull() throws IOException {
+ HeaderReader reader = new HeaderReader<>("anything");
+ assertNull(reader.readValue());
+ }
+
+ @Test
+ void readValues_defaultImplementation_returnsEmptyList() {
+ HeaderReader reader = new HeaderReader<>("anything");
+ List values = reader.readValues();
+ assertTrue(values.isEmpty());
+ }
+
+ @Test
+ void addValues_customReaderWithTokenValues_addsEachNonNullValue() {
+ HeaderReader reader =
+ new HeaderReader<>("a, b, a") {
+ @Override
+ public String readValue() {
+ return readToken();
+ }
+ };
+ List values = new ArrayList<>();
+ reader.addValues(values);
+ assertEquals(List.of("a", "b"), values);
+ }
+
+ @Test
+ void canAdd_duplicateValue_isExcludedByDefault() {
+ HeaderReader reader = new HeaderReader<>("");
+ List existing = new ArrayList<>(List.of("a"));
+ assertFalse(reader.canAdd("a", existing));
+ assertTrue(reader.canAdd("b", existing));
+ assertFalse(reader.canAdd(null, existing));
+ }
+
+ @Test
+ void constructor_nullHeader_behavesAsEmpty() {
+ HeaderReader reader = new HeaderReader<>(null);
+ assertEquals(-1, reader.peek());
+ assertEquals(-1, reader.read());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/HeaderWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/HeaderWriterTestCase.java
new file mode 100644
index 0000000000..3ff56e29eb
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/HeaderWriterTestCase.java
@@ -0,0 +1,224 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+
+class HeaderWriterTestCase {
+
+ private static HeaderWriter newWriter() {
+ return new HeaderWriter<>() {
+ @Override
+ public HeaderWriter append(String value) {
+ return append((CharSequence) value);
+ }
+ };
+ }
+
+ @Test
+ void appendChar_appendsSingleCharacter() {
+ assertEquals("a", newWriter().append('a').toString());
+ }
+
+ @Test
+ void appendCharArray_appendsAllCharacters() {
+ assertEquals("abc", newWriter().append(new char[] {'a', 'b', 'c'}).toString());
+ }
+
+ @Test
+ void appendCharArray_null_appendsNothing() {
+ assertEquals("", newWriter().append((char[]) null).toString());
+ }
+
+ @Test
+ void appendCharSequence_appendsText() {
+ assertEquals("hello", newWriter().append((CharSequence) "hello").toString());
+ }
+
+ @Test
+ void appendCollection_nullOrEmpty_appendsNothing() {
+ assertEquals("", newWriter().append((java.util.Collection) null).toString());
+ assertEquals("", newWriter().append(java.util.List.of()).toString());
+ }
+
+ @Test
+ void appendCollection_multipleValues_joinsWithValueSeparator() {
+ assertEquals("a, b", newWriter().append(Arrays.asList("a", "b")).toString());
+ }
+
+ @Test
+ void appendCollection_skipsValuesRejectedByCanWrite() {
+ HeaderWriter writer =
+ new HeaderWriter<>() {
+ @Override
+ public HeaderWriter append(String value) {
+ return append((CharSequence) value);
+ }
+
+ @Override
+ protected boolean canWrite(String value) {
+ return !"skip".equals(value);
+ }
+ };
+ writer.append(Arrays.asList("a", "skip", "b"));
+ assertEquals("a, b", writer.toString());
+ }
+
+ @Test
+ void appendInt_appendsDecimalRepresentation() {
+ assertEquals("42", newWriter().append(42).toString());
+ }
+
+ @Test
+ void appendLong_appendsDecimalRepresentation() {
+ assertEquals("123456789012", newWriter().append(123456789012L).toString());
+ }
+
+ @Test
+ void appendComment_plainText_isWrappedInParentheses() {
+ assertEquals("(hello)", newWriter().appendComment("hello").toString());
+ }
+
+ @Test
+ void appendComment_withSpecialCharacter_isQuotedPair() {
+ assertEquals("(a\\)b)", newWriter().appendComment("a)b").toString());
+ }
+
+ @Test
+ void appendExtension_namedValueObject_delegatesToNameValueOverload() {
+ org.restlet.data.Parameter parameter = new org.restlet.data.Parameter("name", "value");
+ assertEquals("name=value", newWriter().appendExtension(parameter).toString());
+ }
+
+ @Test
+ void appendExtension_nullNamedValue_appendsNothing() {
+ assertEquals(
+ "",
+ newWriter().appendExtension((org.restlet.util.NamedValue) null).toString());
+ }
+
+ @Test
+ void appendExtension_nameOnly_appendsNameOnly() {
+ assertEquals("name", newWriter().appendExtension("name", null).toString());
+ }
+
+ @Test
+ void appendExtension_tokenValue_appendsUnquoted() {
+ assertEquals("name=value", newWriter().appendExtension("name", "value").toString());
+ }
+
+ @Test
+ void appendExtension_nonTokenValue_appendsQuoted() {
+ assertEquals(
+ "name=\"has space\"", newWriter().appendExtension("name", "has space").toString());
+ }
+
+ @Test
+ void appendExtension_emptyName_appendsNothing() {
+ assertEquals("", newWriter().appendExtension("", "value").toString());
+ }
+
+ @Test
+ void appendParameterSeparator_appendsSemicolon() {
+ assertEquals(";", newWriter().appendParameterSeparator().toString());
+ }
+
+ @Test
+ void appendProduct_nameOnly_appendsNameOnly() {
+ assertEquals("Restlet", newWriter().appendProduct("Restlet", null).toString());
+ }
+
+ @Test
+ void appendProduct_nameAndVersion_joinsWithSlash() {
+ assertEquals("Restlet/2.7", newWriter().appendProduct("Restlet", "2.7").toString());
+ }
+
+ @Test
+ void appendQuotedPair_prefixesWithBackslash() {
+ assertEquals("\\\"", newWriter().appendQuotedPair('"').toString());
+ }
+
+ @Test
+ void appendQuotedString_nullOrEmpty_appendsNothing() {
+ assertEquals("", newWriter().appendQuotedString(null).toString());
+ assertEquals("", newWriter().appendQuotedString("").toString());
+ }
+
+ @Test
+ void appendQuotedString_plainText_isWrappedInQuotes() {
+ assertEquals("\"hello\"", newWriter().appendQuotedString("hello").toString());
+ }
+
+ @Test
+ void appendQuotedString_withEmbeddedQuote_isEscaped() {
+ assertEquals("\"a\\\"b\"", newWriter().appendQuotedString("a\"b").toString());
+ }
+
+ @Test
+ void appendSpace_appendsSingleSpace() {
+ assertEquals(" ", newWriter().appendSpace().toString());
+ }
+
+ @Test
+ void appendToken_validToken_isAppended() {
+ assertEquals("abc123", newWriter().appendToken("abc123").toString());
+ }
+
+ @Test
+ void appendToken_invalidToken_throwsIllegalArgumentException() {
+ assertThrows(IllegalArgumentException.class, () -> newWriter().appendToken("has space"));
+ }
+
+ @Test
+ void appendUriEncoded_encodesReservedCharacters() {
+ String result = newWriter().appendUriEncoded("a b", CharacterSet.UTF_8).toString();
+ assertEquals("a%20b", result);
+ }
+
+ @Test
+ void appendValueSeparator_appendsCommaAndSpace() {
+ assertEquals(", ", newWriter().appendValueSeparator().toString());
+ }
+
+ @Test
+ void canWrite_nonNullValue_returnsTrueByDefault() {
+ assertTrue(newWriter().append(java.util.List.of("value")).toString().equals("value"));
+ }
+
+ @Test
+ void append_chaining_returnsSameWriterInstance() {
+ HeaderWriter writer = newWriter();
+ assertEquals(writer, writer.append('a'));
+ }
+
+ @Test
+ void appendCollection_allValuesRejected_appendsNothing() {
+ HeaderWriter writer =
+ new HeaderWriter<>() {
+ @Override
+ public HeaderWriter append(String value) {
+ return append((CharSequence) value);
+ }
+
+ @Override
+ protected boolean canWrite(String value) {
+ return false;
+ }
+ };
+ writer.append(Arrays.asList("a", "b"));
+ assertFalse(writer.toString().length() > 0);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/LanguageReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/LanguageReaderTestCase.java
new file mode 100644
index 0000000000..df28e231fe
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/LanguageReaderTestCase.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Language;
+
+class LanguageReaderTestCase {
+
+ @Test
+ void readValue_english_parsesLanguage() throws IOException {
+ assertEquals(Language.ENGLISH, new LanguageReader("en").readValue());
+ }
+
+ @Test
+ void readValue_frenchFrance_parsesLanguage() throws IOException {
+ assertEquals(Language.FRENCH_FRANCE, new LanguageReader("fr-fr").readValue());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/LanguageWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/LanguageWriterTestCase.java
new file mode 100644
index 0000000000..dab8c115a2
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/LanguageWriterTestCase.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Language;
+
+class LanguageWriterTestCase {
+
+ @Test
+ void write_singleLanguage_writesLanguageName() {
+ assertEquals(
+ Language.ENGLISH.getName(), LanguageWriter.write(Arrays.asList(Language.ENGLISH)));
+ }
+
+ @Test
+ void write_multipleLanguages_joinsWithSeparator() {
+ String result =
+ LanguageWriter.write(Arrays.asList(Language.ENGLISH, Language.FRENCH_FRANCE));
+ assertEquals(Language.ENGLISH.getName() + ", " + Language.FRENCH_FRANCE.getName(), result);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/MetadataWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/MetadataWriterTestCase.java
new file mode 100644
index 0000000000..0e89726912
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/MetadataWriterTestCase.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Language;
+
+class MetadataWriterTestCase {
+
+ @Test
+ void append_metadata_writesItsName() {
+ MetadataWriter writer = new MetadataWriter<>();
+ writer.append(Language.ENGLISH);
+ assertEquals(Language.ENGLISH.getName(), writer.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/MethodWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/MethodWriterTestCase.java
new file mode 100644
index 0000000000..03d970bef6
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/MethodWriterTestCase.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Method;
+
+class MethodWriterTestCase {
+
+ @Test
+ void write_singleMethod_writesMethodName() {
+ Set methods = new LinkedHashSet<>();
+ methods.add(Method.GET);
+ assertEquals("GET", MethodWriter.write(methods));
+ }
+
+ @Test
+ void write_multipleMethods_joinsWithSeparator() {
+ Set methods = new LinkedHashSet<>();
+ methods.add(Method.GET);
+ methods.add(Method.POST);
+ assertEquals("GET, POST", MethodWriter.write(methods));
+ }
+
+ @Test
+ void append_method_writesTokenizedName() {
+ MethodWriter writer = new MethodWriter();
+ writer.append(Method.PUT);
+ assertTrue(writer.toString().contains("PUT"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/PreferenceWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/PreferenceWriterTestCase.java
new file mode 100644
index 0000000000..ffa098e77c
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/PreferenceWriterTestCase.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.data.Parameter;
+import org.restlet.data.Preference;
+import org.restlet.util.Series;
+
+class PreferenceWriterTestCase {
+
+ @Test
+ void isValidQuality_withinRange_returnsTrue() {
+ assertTrue(PreferenceWriter.isValidQuality(0F));
+ assertTrue(PreferenceWriter.isValidQuality(1F));
+ assertTrue(PreferenceWriter.isValidQuality(0.5F));
+ }
+
+ @Test
+ void isValidQuality_outsideRange_returnsFalse() {
+ assertFalse(PreferenceWriter.isValidQuality(-0.1F));
+ assertFalse(PreferenceWriter.isValidQuality(1.1F));
+ }
+
+ @Test
+ void write_fullQuality_omitsQualityParameter() {
+ Preference pref = new Preference<>(MediaType.TEXT_PLAIN, 1F);
+ assertEquals("text/plain", PreferenceWriter.write(Arrays.asList(pref)));
+ }
+
+ @Test
+ void write_partialQuality_appendsQParameter() {
+ Preference pref = new Preference<>(MediaType.TEXT_PLAIN, 0.5F);
+ assertEquals("text/plain;q=0.5", PreferenceWriter.write(Arrays.asList(pref)));
+ }
+
+ @Test
+ void write_withParameters_appendsEachOne() {
+ Series parameters = new Series<>(Parameter.class);
+ parameters.add(new Parameter("level", "1"));
+ Preference pref = new Preference<>(MediaType.TEXT_PLAIN, 1F, parameters);
+ assertEquals("text/plain;level=1", PreferenceWriter.write(Arrays.asList(pref)));
+ }
+
+ @Test
+ void appendQuality_invalidValue_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class, () -> new PreferenceWriter().appendQuality(2F));
+ }
+
+ @Test
+ void appendQuality_formatsWithMaxTwoDecimals() {
+ String result = new PreferenceWriter().appendQuality(0.333F).toString();
+ assertEquals("0.33", result);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/ProductReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/ProductReaderTestCase.java
new file mode 100644
index 0000000000..147295af2b
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/ProductReaderTestCase.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Product;
+
+class ProductReaderTestCase {
+
+ @Test
+ void read_nullInput_returnsEmptyList() {
+ assertTrue(ProductReader.read(null).isEmpty());
+ }
+
+ @Test
+ void read_tokenOnly_parsesName() {
+ List products = ProductReader.read("Restlet");
+ assertEquals(1, products.size());
+ assertEquals("Restlet", products.get(0).getName());
+ assertNull(products.get(0).getVersion());
+ }
+
+ @Test
+ void read_tokenAndVersion_parsesBoth() {
+ List products = ProductReader.read("Restlet/2.7");
+ assertEquals(1, products.size());
+ assertEquals("Restlet", products.get(0).getName());
+ assertEquals("2.7", products.get(0).getVersion());
+ }
+
+ @Test
+ void read_tokenVersionAndComment_parsesAll() {
+ List products = ProductReader.read("Restlet/2.7 (Java)");
+ assertEquals(1, products.size());
+ assertEquals("Restlet", products.get(0).getName());
+ assertEquals("2.7", products.get(0).getVersion());
+ assertEquals("Java", products.get(0).getComment());
+ }
+
+ @Test
+ void read_multipleProducts_parsesEachOne() {
+ List products = ProductReader.read("Restlet/2.7 (Java) Mozilla/5.0");
+ assertEquals(2, products.size());
+ assertEquals("Restlet", products.get(0).getName());
+ assertEquals("Mozilla", products.get(1).getName());
+ assertEquals("5.0", products.get(1).getVersion());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/ProductWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/ProductWriterTestCase.java
new file mode 100644
index 0000000000..49de548f1f
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/ProductWriterTestCase.java
@@ -0,0 +1,59 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Product;
+
+class ProductWriterTestCase {
+
+ @Test
+ void write_nameOnly_writesNameOnly() {
+ assertEquals(
+ "Restlet", ProductWriter.write(Arrays.asList(new Product("Restlet", null, null))));
+ }
+
+ @Test
+ void write_nameAndVersion_joinsWithSlash() {
+ assertEquals(
+ "Restlet/2.7",
+ ProductWriter.write(Arrays.asList(new Product("Restlet", "2.7", null))));
+ }
+
+ @Test
+ void write_nameVersionAndComment_appendsParenthesizedComment() {
+ assertEquals(
+ "Restlet/2.7 (Java)",
+ ProductWriter.write(Arrays.asList(new Product("Restlet", "2.7", "Java"))));
+ }
+
+ @Test
+ void write_multipleProducts_joinsWithSpace() {
+ String result =
+ ProductWriter.write(
+ Arrays.asList(
+ new Product("Restlet", "2.7", null),
+ new Product("Mozilla", "5.0", null)));
+ assertEquals("Restlet/2.7 Mozilla/5.0", result);
+ }
+
+ @Test
+ void write_nullOrEmptyName_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ProductWriter.write(Arrays.asList(new Product(null, "2.7", null))));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> ProductWriter.write(Arrays.asList(new Product("", "2.7", null))));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/RecipientInfoReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/RecipientInfoReaderTestCase.java
new file mode 100644
index 0000000000..ae9a2cdc97
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/RecipientInfoReaderTestCase.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Header;
+import org.restlet.data.RecipientInfo;
+
+class RecipientInfoReaderTestCase {
+
+ @Test
+ void readValue_implicitHttpProtocol_defaultsProtocolNameToHttp() throws IOException {
+ RecipientInfoReader reader = new RecipientInfoReader("1.1 proxy.example.com");
+ RecipientInfo info = reader.readValue();
+ assertEquals("HTTP", info.getProtocol().getName());
+ assertEquals("1.1", info.getProtocol().getVersion());
+ assertEquals("proxy.example.com", info.getName());
+ }
+
+ @Test
+ void readValue_explicitProtocol_parsesNameAndVersion() throws IOException {
+ RecipientInfoReader reader = new RecipientInfoReader("FSTR/2.1 proxy.example.com");
+ RecipientInfo info = reader.readValue();
+ assertEquals("FSTR", info.getProtocol().getName());
+ assertEquals("2.1", info.getProtocol().getVersion());
+ }
+
+ @Test
+ void readValue_withComment_parsesComment() throws IOException {
+ RecipientInfoReader reader = new RecipientInfoReader("1.1 proxy.example.com (Apache)");
+ RecipientInfo info = reader.readValue();
+ assertEquals("proxy.example.com", info.getName());
+ assertEquals("Apache", info.getComment());
+ }
+
+ @Test
+ void readValue_emptyProtocolToken_throws() {
+ RecipientInfoReader reader = new RecipientInfoReader("");
+ assertThrows(IOException.class, reader::readValue);
+ }
+
+ @Test
+ void addValuesStatic_fromHeader_populatesCollection() {
+ List infos = new ArrayList<>();
+ Header header = new Header("Via", "1.1 proxy.example.com");
+ RecipientInfoReader.addValues(header, infos);
+ assertEquals(1, infos.size());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/RecipientInfoWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/RecipientInfoWriterTestCase.java
new file mode 100644
index 0000000000..d74554b156
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/RecipientInfoWriterTestCase.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Protocol;
+import org.restlet.data.RecipientInfo;
+
+class RecipientInfoWriterTestCase {
+
+ @Test
+ void write_nameOnly_writesProtocolAndName() {
+ RecipientInfo info =
+ new RecipientInfo(new Protocol("HTTP", "HTTP", null, -1, "1.1"), "proxy", null);
+ assertEquals("HTTP/1.1 proxy", RecipientInfoWriter.write(Arrays.asList(info)));
+ }
+
+ @Test
+ void write_withComment_appendsCommentInParentheses() {
+ RecipientInfo info =
+ new RecipientInfo(new Protocol("HTTP", "HTTP", null, -1, "1.1"), "proxy", null);
+ info.setComment("Apache");
+ assertEquals("HTTP/1.1 proxy (Apache)", RecipientInfoWriter.write(Arrays.asList(info)));
+ }
+
+ @Test
+ void append_nullProtocol_throwsIllegalArgumentException() {
+ RecipientInfo info = new RecipientInfo();
+ assertThrows(IllegalArgumentException.class, () -> new RecipientInfoWriter().append(info));
+ }
+
+ @Test
+ void append_nullName_throwsIllegalArgumentException() {
+ RecipientInfo info =
+ new RecipientInfo(new Protocol("HTTP", "HTTP", null, -1, "1.1"), null, null);
+ assertThrows(IllegalArgumentException.class, () -> new RecipientInfoWriter().append(info));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/StringWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/StringWriterTestCase.java
new file mode 100644
index 0000000000..0703453151
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/StringWriterTestCase.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class StringWriterTestCase {
+
+ @Test
+ void write_singleValue_writesValue() {
+ Set values = new LinkedHashSet<>();
+ values.add("a");
+ assertEquals("a", StringWriter.write(values));
+ }
+
+ @Test
+ void write_multipleValues_joinsWithSeparator() {
+ Set values = new LinkedHashSet<>();
+ values.add("a");
+ values.add("b");
+ assertEquals("a, b", StringWriter.write(values));
+ }
+
+ @Test
+ void append_value_writesTokenizedValue() {
+ StringWriter writer = new StringWriter();
+ writer.append("token");
+ assertEquals("token", writer.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/TagReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/TagReaderTestCase.java
new file mode 100644
index 0000000000..4c925d2b81
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/TagReaderTestCase.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Header;
+import org.restlet.data.Tag;
+
+class TagReaderTestCase {
+
+ @Test
+ void readValue_singleTag_parsesTag() throws IOException {
+ TagReader reader = new TagReader("\"abc\"");
+ Tag tag = reader.readValue();
+ assertEquals(new Tag("abc"), tag);
+ }
+
+ @Test
+ void readValue_weakTag_parsesWeakFlag() throws IOException {
+ TagReader reader = new TagReader("W/\"abc\"");
+ Tag tag = reader.readValue();
+ assertTrue(tag.isWeak());
+ }
+
+ @Test
+ void addValues_multipleTags_populatesCollection() {
+ List tags = new ArrayList<>();
+ new TagReader("\"abc\", \"def\"").addValues(tags);
+ assertEquals(2, tags.size());
+ assertEquals(new Tag("abc"), tags.get(0));
+ assertEquals(new Tag("def"), tags.get(1));
+ }
+
+ @Test
+ void addValuesStatic_fromHeader_populatesCollection() {
+ List tags = new ArrayList<>();
+ Header header = new Header("ETag", "\"abc\"");
+ TagReader.addValues(header, tags);
+ assertEquals(1, tags.size());
+ assertEquals(new Tag("abc"), tags.get(0));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/TagWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/TagWriterTestCase.java
new file mode 100644
index 0000000000..876c67b0ac
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/TagWriterTestCase.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Tag;
+
+class TagWriterTestCase {
+
+ @Test
+ void write_singleTag_delegatesToTagFormat() {
+ Tag tag = new Tag("abc");
+ assertEquals(tag.format(), TagWriter.write(tag));
+ }
+
+ @Test
+ void write_listOfTags_joinsWithSeparator() {
+ Tag tag1 = new Tag("abc");
+ Tag tag2 = new Tag("def");
+ String result = TagWriter.write(Arrays.asList(tag1, tag2));
+ assertEquals(tag1.format() + ", " + tag2.format(), result);
+ }
+
+ @Test
+ void append_singleTag_writesFormattedValue() {
+ Tag tag = new Tag("abc");
+ TagWriter writer = new TagWriter();
+ writer.append(tag);
+ assertEquals(tag.format(), writer.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/TokenReaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/TokenReaderTestCase.java
new file mode 100644
index 0000000000..4d7db3ce64
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/TokenReaderTestCase.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class TokenReaderTestCase {
+
+ @Test
+ void readValue_singleToken_returnsToken() throws IOException {
+ TokenReader reader = new TokenReader("gzip");
+ assertEquals("gzip", reader.readValue());
+ }
+
+ @Test
+ void addValues_multipleTokens_populatesCollection() {
+ List tokens = new ArrayList<>();
+ new TokenReader("gzip, deflate").addValues(tokens);
+ assertEquals(List.of("gzip", "deflate"), tokens);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/header/WarningWriterTestCase.java b/org.restlet/src/test/java/org/restlet/engine/header/WarningWriterTestCase.java
new file mode 100644
index 0000000000..2d9f9119ff
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/header/WarningWriterTestCase.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.header;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Status;
+import org.restlet.data.Warning;
+
+class WarningWriterTestCase {
+
+ private static Warning newWarning() {
+ Warning warning = new Warning();
+ warning.setStatus(Status.valueOf(110));
+ warning.setAgent("restlet/2.7");
+ warning.setText("Response is stale");
+ return warning;
+ }
+
+ @Test
+ void write_validWarning_formatsCodeAgentAndQuotedText() {
+ String result = WarningWriter.write(Arrays.asList(newWarning()));
+ assertEquals("110 restlet/2.7 \"Response is stale\"", result);
+ }
+
+ @Test
+ void write_withDate_appendsQuotedDate() {
+ Warning warning = newWarning();
+ warning.setDate(new java.util.Date(0));
+ String result = WarningWriter.write(Arrays.asList(warning));
+ assertTrue(result.startsWith("110 restlet/2.7 \"Response is stale\" \""));
+ }
+
+ @Test
+ void append_nullStatus_throwsIllegalArgumentException() {
+ Warning warning = newWarning();
+ warning.setStatus(null);
+ assertThrows(IllegalArgumentException.class, () -> new WarningWriter().append(warning));
+ }
+
+ @Test
+ void append_missingAgent_throwsIllegalArgumentException() {
+ Warning warning = newWarning();
+ warning.setAgent(null);
+ assertThrows(IllegalArgumentException.class, () -> new WarningWriter().append(warning));
+ }
+
+ @Test
+ void append_missingText_throwsIllegalArgumentException() {
+ Warning warning = newWarning();
+ warning.setText("");
+ assertThrows(IllegalArgumentException.class, () -> new WarningWriter().append(warning));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/io/PipeStreamTestCase.java b/org.restlet/src/test/java/org/restlet/engine/io/PipeStreamTestCase.java
new file mode 100644
index 0000000000..59e518daf1
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/io/PipeStreamTestCase.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.io;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import org.junit.jupiter.api.Test;
+
+class PipeStreamTestCase {
+
+ @Test
+ void writeThenReadThenClose_roundTripsBytesAndSignalsEndOfStream() throws IOException {
+ PipeStream pipe = new PipeStream();
+ OutputStream out = pipe.getOutputStream();
+ InputStream in = pipe.getInputStream();
+
+ out.write('a');
+ out.write('b');
+ out.close();
+
+ assertEquals('a', in.read());
+ assertEquals('b', in.read());
+ assertEquals(-1, in.read());
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ void write_masksToUnsignedByte() throws IOException {
+ PipeStream pipe = new PipeStream();
+ OutputStream out = pipe.getOutputStream();
+ InputStream in = pipe.getInputStream();
+
+ out.write(0xFF);
+ out.close();
+
+ assertEquals(0xFF, in.read());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/io/RangeInputStreamTestCase.java b/org.restlet/src/test/java/org/restlet/engine/io/RangeInputStreamTestCase.java
new file mode 100644
index 0000000000..9f1f20252e
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/io/RangeInputStreamTestCase.java
@@ -0,0 +1,167 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.io;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Range;
+
+class RangeInputStreamTestCase {
+
+ private static byte[] source() {
+ return "0123456789".getBytes(StandardCharsets.US_ASCII);
+ }
+
+ @Test
+ void readByteArray_boundedRange_returnsOnlyThatSlice() throws IOException {
+ RangeInputStream in =
+ new RangeInputStream(new ByteArrayInputStream(source()), 10, new Range(2, 5));
+
+ byte[] buffer = new byte[10];
+ int read = in.read(buffer, 0, buffer.length);
+
+ assertEquals("23456", new String(buffer, 0, read, StandardCharsets.US_ASCII));
+ }
+
+ @Test
+ void readByteArray_unboundedRangeFromIndex_readsToEndOfStream() throws IOException {
+ RangeInputStream in =
+ new RangeInputStream(
+ new ByteArrayInputStream(source()), 10, new Range(5, Range.SIZE_MAX));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4];
+ int read;
+ while ((read = in.read(buffer, 0, buffer.length)) != -1) {
+ out.write(buffer, 0, read);
+ }
+
+ assertEquals("56789", out.toString(StandardCharsets.US_ASCII));
+ }
+
+ @Test
+ void readByteArray_lastRangeWithKnownSize_readsFromComputedStart() throws IOException {
+ RangeInputStream in =
+ new RangeInputStream(
+ new ByteArrayInputStream(source()), 10, new Range(Range.INDEX_LAST, 3));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[10];
+ int read;
+ while ((read = in.read(buffer, 0, buffer.length)) != -1) {
+ out.write(buffer, 0, read);
+ }
+
+ assertEquals("789", out.toString(StandardCharsets.US_ASCII));
+ }
+
+ @Test
+ void readSingleByte_skipsBytesOutsideRange() throws IOException {
+ RangeInputStream in =
+ new RangeInputStream(new ByteArrayInputStream(source()), 10, new Range(2, 3));
+
+ assertEquals('2', (char) in.read());
+ assertEquals('3', (char) in.read());
+ assertEquals('4', (char) in.read());
+ assertEquals(-1, in.read());
+ }
+
+ @Test
+ void available_returnsConfiguredRangeSize() throws IOException {
+ RangeInputStream in =
+ new RangeInputStream(new ByteArrayInputStream(source()), 10, new Range(2, 5));
+ assertEquals(5, in.available());
+ }
+
+ @Test
+ void mark_withRegularIndex_doesNotThrow() {
+ RangeInputStream in =
+ new RangeInputStream(new ByteArrayInputStream(source()), 10, new Range(2, 5));
+ in.mark(10);
+ }
+
+ @Test
+ void mark_withLastIndex_doesNotThrow() {
+ RangeInputStream in =
+ new RangeInputStream(
+ new ByteArrayInputStream(source()), 10, new Range(Range.INDEX_LAST, 3));
+ in.mark(10);
+ }
+
+ @Test
+ void constructor_unknownSizeWithLastIndexAndNonMaxSize_throws() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ new RangeInputStream(
+ new ByteArrayInputStream(source()),
+ org.restlet.representation.Representation.UNKNOWN_SIZE,
+ new Range(Range.INDEX_LAST, 3)));
+ }
+
+ @Test
+ void constructor_unknownSizeWithLastIndexAndMaxSize_readsWholeStream() throws IOException {
+ RangeInputStream in =
+ new RangeInputStream(
+ new ByteArrayInputStream(source()),
+ org.restlet.representation.Representation.UNKNOWN_SIZE,
+ new Range(Range.INDEX_LAST, Range.SIZE_MAX));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[10];
+ int read;
+ while ((read = in.read(buffer, 0, buffer.length)) != -1) {
+ out.write(buffer, 0, read);
+ }
+
+ assertEquals("0123456789", out.toString(StandardCharsets.US_ASCII));
+ }
+
+ @Test
+ void constructor_unknownSizeWithIndexAndMaxSize_readsFromIndexToEnd() throws IOException {
+ RangeInputStream in =
+ new RangeInputStream(
+ new ByteArrayInputStream(source()),
+ org.restlet.representation.Representation.UNKNOWN_SIZE,
+ new Range(3, Range.SIZE_MAX));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[10];
+ int read;
+ while ((read = in.read(buffer, 0, buffer.length)) != -1) {
+ out.write(buffer, 0, read);
+ }
+
+ assertEquals("3456789", out.toString(StandardCharsets.US_ASCII));
+ }
+
+ @Test
+ void constructor_unknownSizeWithIndexAndBoundedSize_readsExactSlice() throws IOException {
+ RangeInputStream in =
+ new RangeInputStream(
+ new ByteArrayInputStream(source()),
+ org.restlet.representation.Representation.UNKNOWN_SIZE,
+ new Range(3, 4));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ byte[] buffer = new byte[10];
+ int read;
+ while ((read = in.read(buffer, 0, buffer.length)) != -1) {
+ out.write(buffer, 0, read);
+ }
+
+ assertEquals("3456", out.toString(StandardCharsets.US_ASCII));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/io/WriterOutputStreamTestCase.java b/org.restlet/src/test/java/org/restlet/engine/io/WriterOutputStreamTestCase.java
new file mode 100644
index 0000000000..15a085237e
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/io/WriterOutputStreamTestCase.java
@@ -0,0 +1,100 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.io;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+
+class WriterOutputStreamTestCase {
+
+ @Test
+ void writeByteArray_decodesUsingGivenCharacterSet() throws IOException {
+ StringWriter writer = new StringWriter();
+ WriterOutputStream out = new WriterOutputStream(writer, CharacterSet.UTF_8);
+
+ out.write("hello".getBytes(StandardCharsets.UTF_8));
+
+ assertEquals("hello", writer.toString());
+ }
+
+ @Test
+ void writeByteArray_nullCharacterSet_defaultsToIso88591() throws IOException {
+ StringWriter writer = new StringWriter();
+ WriterOutputStream out = new WriterOutputStream(writer, null);
+
+ out.write("abc".getBytes(StandardCharsets.ISO_8859_1));
+
+ assertEquals("abc", writer.toString());
+ }
+
+ @Test
+ void writeSingleByte_delegatesToWriter() throws IOException {
+ StringWriter writer = new StringWriter();
+ WriterOutputStream out = new WriterOutputStream(writer, CharacterSet.UTF_8);
+
+ out.write('a');
+
+ assertEquals("a", writer.toString());
+ }
+
+ @Test
+ void writeByteArrayWithOffsetAndLength_writesOnlyThatSlice() throws IOException {
+ StringWriter writer = new StringWriter();
+ WriterOutputStream out = new WriterOutputStream(writer, CharacterSet.UTF_8);
+ byte[] data = "xxhelloxx".getBytes(StandardCharsets.UTF_8);
+
+ out.write(data, 2, 5);
+
+ assertEquals("hello", writer.toString());
+ }
+
+ @Test
+ void flush_flushesUnderlyingWriter() throws IOException {
+ java.util.concurrent.atomic.AtomicBoolean flushed =
+ new java.util.concurrent.atomic.AtomicBoolean();
+ java.io.Writer tracker =
+ new java.io.FilterWriter(new StringWriter()) {
+ @Override
+ public void flush() throws IOException {
+ flushed.set(true);
+ super.flush();
+ }
+ };
+ WriterOutputStream out = new WriterOutputStream(tracker, CharacterSet.UTF_8);
+
+ out.flush();
+
+ assertTrue(flushed.get());
+ }
+
+ @Test
+ void close_closesUnderlyingWriter() throws IOException {
+ java.util.concurrent.atomic.AtomicBoolean closed =
+ new java.util.concurrent.atomic.AtomicBoolean();
+ java.io.Writer tracker =
+ new java.io.FilterWriter(new StringWriter()) {
+ @Override
+ public void close() throws IOException {
+ closed.set(true);
+ super.close();
+ }
+ };
+ WriterOutputStream out = new WriterOutputStream(tracker, CharacterSet.UTF_8);
+
+ out.close();
+
+ assertTrue(closed.get());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/resource/ThrowableAnnotationInfoTestCase.java b/org.restlet/src/test/java/org/restlet/engine/resource/ThrowableAnnotationInfoTestCase.java
new file mode 100644
index 0000000000..464d7bbe52
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/resource/ThrowableAnnotationInfoTestCase.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.resource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Status;
+
+class ThrowableAnnotationInfoTestCase {
+
+ @Test
+ void constructor_parsesStatusFromAnnotationValue() {
+ ThrowableAnnotationInfo info =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, true);
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, info.getStatus());
+ assertEquals("404", info.getAnnotationValue());
+ assertEquals(RuntimeException.class, info.getJavaClass());
+ assertNull(info.getJavaMethod());
+ assertTrue(info.isSerializable());
+ }
+
+ @Test
+ void equals_sameClassAndStatus_returnsTrue() {
+ ThrowableAnnotationInfo info1 =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, true);
+ ThrowableAnnotationInfo info2 =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, false);
+ assertTrue(info1.equals(info2));
+ }
+
+ @Test
+ void equals_differentStatus_returnsFalse() {
+ ThrowableAnnotationInfo info1 =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, true);
+ ThrowableAnnotationInfo info2 =
+ new ThrowableAnnotationInfo(RuntimeException.class, 500, true);
+ assertFalse(info1.equals(info2));
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ ThrowableAnnotationInfo info =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, true);
+ assertFalse(info.equals("not an info"));
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ ThrowableAnnotationInfo info =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, true);
+ assertTrue(info.equals(info));
+ }
+
+ @Test
+ void hashCode_matchesForEqualInstances() {
+ ThrowableAnnotationInfo info1 =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, true);
+ ThrowableAnnotationInfo info2 =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, false);
+ assertEquals(info1.hashCode(), info2.hashCode());
+ }
+
+ @Test
+ void toString_containsStatusAndSerializable() {
+ ThrowableAnnotationInfo info =
+ new ThrowableAnnotationInfo(RuntimeException.class, 404, true);
+ String result = info.toString();
+ assertTrue(result.contains("404"));
+ assertTrue(result.contains("serializable=true"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/resource/VariantInfoTestCase.java b/org.restlet/src/test/java/org/restlet/engine/resource/VariantInfoTestCase.java
new file mode 100644
index 0000000000..4ee9ab0f73
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/resource/VariantInfoTestCase.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.resource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.MediaType;
+import org.restlet.representation.Variant;
+
+class VariantInfoTestCase {
+
+ @Test
+ void constructor_mediaTypeOnly_setsDefaultInputScore() {
+ VariantInfo variant = new VariantInfo(MediaType.APPLICATION_JSON);
+ assertEquals(MediaType.APPLICATION_JSON, variant.getMediaType());
+ assertNull(variant.getAnnotationInfo());
+ assertEquals(1.0f, variant.getInputScore());
+ }
+
+ @Test
+ void constructor_fromVariant_copiesCharacterSetEncodingsAndLanguages() {
+ Variant source = new Variant(MediaType.TEXT_PLAIN);
+ source.setCharacterSet(CharacterSet.UTF_8);
+ source.setLanguages(Collections.singletonList(org.restlet.data.Language.ENGLISH));
+
+ VariantInfo variant = new VariantInfo(source, null);
+
+ assertEquals(MediaType.TEXT_PLAIN, variant.getMediaType());
+ assertEquals(CharacterSet.UTF_8, variant.getCharacterSet());
+ assertEquals(1, variant.getLanguages().size());
+ }
+
+ @Test
+ void setInputScore_updatesValue() {
+ VariantInfo variant = new VariantInfo(MediaType.APPLICATION_JSON);
+ variant.setInputScore(0.5f);
+ assertEquals(0.5f, variant.getInputScore());
+ }
+
+ @Test
+ void equals_sameMediaTypeAndAnnotationInfo_returnsTrue() {
+ VariantInfo variant1 = new VariantInfo(MediaType.APPLICATION_JSON, null);
+ VariantInfo variant2 = new VariantInfo(MediaType.APPLICATION_JSON, null);
+ assertTrue(variant1.equals(variant2));
+ assertEquals(variant1.hashCode(), variant2.hashCode());
+ }
+
+ @Test
+ void equals_differentMediaType_returnsFalse() {
+ VariantInfo variant1 = new VariantInfo(MediaType.APPLICATION_JSON, null);
+ VariantInfo variant2 = new VariantInfo(MediaType.TEXT_PLAIN, null);
+ assertFalse(variant1.equals(variant2));
+ }
+
+ @Test
+ void equals_differentType_returnsFalse() {
+ VariantInfo variant = new VariantInfo(MediaType.APPLICATION_JSON, null);
+ assertFalse(variant.equals("not a variant"));
+ }
+
+ @Test
+ void equals_sameInstance_returnsTrue() {
+ VariantInfo variant = new VariantInfo(MediaType.APPLICATION_JSON, null);
+ assertTrue(variant.equals(variant));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/security/AuthenticatorUtilsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/security/AuthenticatorUtilsTestCase.java
new file mode 100644
index 0000000000..99a005aac4
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/security/AuthenticatorUtilsTestCase.java
@@ -0,0 +1,197 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.AuthenticationInfo;
+import org.restlet.data.ChallengeRequest;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Method;
+
+class AuthenticatorUtilsTestCase {
+
+ @Test
+ void anyNull_noNulls_returnsFalse() {
+ assertFalse(AuthenticatorUtils.anyNull("a", "b", 1));
+ }
+
+ @Test
+ void anyNull_someNull_returnsTrue() {
+ assertTrue(AuthenticatorUtils.anyNull("a", null, 1));
+ }
+
+ @Test
+ void formatNonceCount_padsToEightHexCharacters() {
+ assertEquals("00000001", AuthenticatorUtils.formatNonceCount(1));
+ assertEquals("000000ff", AuthenticatorUtils.formatNonceCount(255));
+ }
+
+ @Test
+ void formatAuthenticationInfo_null_returnsEmptyString() {
+ assertEquals("", AuthenticatorUtils.formatAuthenticationInfo(null));
+ }
+
+ @Test
+ void formatAuthenticationInfo_allFieldsSet_formatsAllParameters() {
+ AuthenticationInfo info = new AuthenticationInfo("next", 5, "client", "auth", "digest");
+ String result = AuthenticatorUtils.formatAuthenticationInfo(info);
+ assertTrue(result.contains("nextnonce=\"next\""));
+ assertTrue(result.contains("qop=auth"));
+ assertTrue(result.contains("nc=00000005"));
+ assertTrue(result.contains("rspauth=\"digest\""));
+ assertTrue(result.contains("cnonce=client"));
+ }
+
+ @Test
+ void formatAuthenticationInfo_emptyFields_areOmitted() {
+ AuthenticationInfo info = new AuthenticationInfo("", 0, "", "", "");
+ assertEquals("", AuthenticatorUtils.formatAuthenticationInfo(info));
+ }
+
+ @Test
+ void formatRequest_nullChallenge_returnsNull() {
+ assertNull(AuthenticatorUtils.formatRequest(null, new Response(new Request()), null));
+ }
+
+ @Test
+ void formatRequest_nullScheme_returnsNull() {
+ ChallengeRequest challenge = new ChallengeRequest((ChallengeScheme) null);
+ assertNull(AuthenticatorUtils.formatRequest(challenge, new Response(new Request()), null));
+ }
+
+ @Test
+ void formatRequest_rawValuePresent_isUsedDirectly() {
+ ChallengeRequest challenge = new ChallengeRequest(ChallengeScheme.HTTP_BASIC);
+ challenge.setRawValue("realm=\"test\"");
+ String result =
+ AuthenticatorUtils.formatRequest(challenge, new Response(new Request()), null);
+ assertEquals("Basic realm=\"test\"", result);
+ }
+
+ @Test
+ void formatResponse_nullChallenge_returnsNull() {
+ assertNull(AuthenticatorUtils.formatResponse(null, new Request(), null));
+ }
+
+ @Test
+ void formatResponse_nullScheme_returnsNull() {
+ ChallengeResponse challenge = new ChallengeResponse((ChallengeScheme) null);
+ assertNull(AuthenticatorUtils.formatResponse(challenge, new Request(), null));
+ }
+
+ @Test
+ void formatResponse_rawValuePresent_isUsedDirectly() {
+ ChallengeResponse challenge = new ChallengeResponse(ChallengeScheme.HTTP_BASIC);
+ challenge.setRawValue("dXNlcjpwYXNz");
+ String result = AuthenticatorUtils.formatResponse(challenge, new Request(), null);
+ assertEquals("Basic dXNlcjpwYXNz", result);
+ }
+
+ @Test
+ void parseAuthenticationInfo_allFields_parsesEachParameter() {
+ AuthenticationInfo info =
+ AuthenticatorUtils.parseAuthenticationInfo(
+ "nextnonce=\"next\", qop=auth, nc=00000005, rspauth=\"digest\", cnonce=client");
+ assertEquals("next", info.getNextServerNonce());
+ assertEquals("auth", info.getQuality());
+ assertEquals(5, info.getNonceCount());
+ assertEquals("digest", info.getResponseDigest());
+ assertEquals("client", info.getClientNonce());
+ }
+
+ @Test
+ void parseAuthenticationInfo_invalidNonceCount_defaultsToZero() {
+ AuthenticationInfo info = AuthenticatorUtils.parseAuthenticationInfo("nc=notahexnumber");
+ assertEquals(0, info.getNonceCount());
+ }
+
+ @Test
+ void parseAuthenticationInfo_unparseableHeader_returnsNull() {
+ assertNull(AuthenticatorUtils.parseAuthenticationInfo(""));
+ }
+
+ @Test
+ void parseRequest_nullHeader_returnsEmptyList() {
+ assertTrue(
+ AuthenticatorUtils.parseRequest(new Response(new Request()), null, null).isEmpty());
+ }
+
+ @Test
+ void parseResponse_nullHeader_returnsNull() {
+ assertNull(AuthenticatorUtils.parseResponse(new Request(), null, null));
+ }
+
+ @Test
+ void parseResponse_headerWithoutSpace_returnsNull() {
+ assertNull(AuthenticatorUtils.parseResponse(new Request(), "NoSpaceHeader", null));
+ }
+
+ @Test
+ void parseResponse_validHeader_parsesSchemeAndRawValue() {
+ ChallengeResponse result =
+ AuthenticatorUtils.parseResponse(new Request(), "Basic dXNlcjpwYXNz", null);
+ assertEquals("Basic", result.getScheme().getTechnicalName());
+ assertEquals("dXNlcjpwYXNz", result.getRawValue());
+ }
+
+ @Test
+ void update_noMatchingChallengeRequest_leavesRealmAndNonceNull() {
+ Request request = new Request(Method.GET, "http://example.com/path");
+ Response response = new Response(request);
+ ChallengeResponse challengeResponse = new ChallengeResponse(ChallengeScheme.HTTP_BASIC);
+
+ AuthenticatorUtils.update(challengeResponse, request, response);
+
+ assertNull(challengeResponse.getRealm());
+ assertNull(challengeResponse.getServerNonce());
+ }
+
+ @Test
+ void update_matchingChallengeRequest_copiesRealmAndNonce() {
+ Request request = new Request(Method.GET, "http://example.com/path");
+ Response response = new Response(request);
+ ChallengeRequest challengeRequest = new ChallengeRequest(ChallengeScheme.HTTP_DIGEST);
+ challengeRequest.setRealm("test-realm");
+ challengeRequest.setServerNonce("test-nonce");
+ response.getChallengeRequests().add(challengeRequest);
+
+ ChallengeResponse challengeResponse = new ChallengeResponse(ChallengeScheme.HTTP_DIGEST);
+ AuthenticatorUtils.update(challengeResponse, request, response);
+
+ assertEquals("test-realm", challengeResponse.getRealm());
+ assertEquals("test-nonce", challengeResponse.getServerNonce());
+ }
+
+ @Test
+ void updateReference_noChallengeResponse_returnsUnchangedReference() {
+ Request request = new Request(Method.GET, "http://example.com/path");
+ assertEquals(
+ request.getResourceRef(),
+ AuthenticatorUtils.updateReference(request.getResourceRef(), null, request));
+ }
+
+ @Test
+ void updateReference_rawValuePresent_returnsUnchangedReference() {
+ Request request = new Request(Method.GET, "http://example.com/path");
+ ChallengeResponse challengeResponse = new ChallengeResponse(ChallengeScheme.HTTP_BASIC);
+ challengeResponse.setRawValue("dXNlcjpwYXNz");
+ assertEquals(
+ request.getResourceRef(),
+ AuthenticatorUtils.updateReference(
+ request.getResourceRef(), challengeResponse, request));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/security/RoleMappingTestCase.java b/org.restlet/src/test/java/org/restlet/engine/security/RoleMappingTestCase.java
new file mode 100644
index 0000000000..fe2758f430
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/security/RoleMappingTestCase.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.security.Role;
+import org.restlet.security.User;
+
+class RoleMappingTestCase {
+
+ @Test
+ void defaultConstructor_hasNullSourceAndTarget() {
+ RoleMapping mapping = new RoleMapping();
+ assertNull(mapping.getSource());
+ assertNull(mapping.getTarget());
+ }
+
+ @Test
+ void constructor_setsSourceAndTarget() {
+ User user = new User("alice");
+ Role role = new Role(null, "admin", "Administrator role");
+ RoleMapping mapping = new RoleMapping(user, role);
+ assertEquals(user, mapping.getSource());
+ assertEquals(role, mapping.getTarget());
+ }
+
+ @Test
+ void settersUpdateState() {
+ RoleMapping mapping = new RoleMapping();
+ User user = new User("bob");
+ Role role = new Role(null, "editor", "Editor role");
+ mapping.setSource(user);
+ mapping.setTarget(role);
+ assertEquals(user, mapping.getSource());
+ assertEquals(role, mapping.getTarget());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/AlphabeticalComparatorTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/AlphabeticalComparatorTestCase.java
new file mode 100644
index 0000000000..664b563620
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/AlphabeticalComparatorTestCase.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Reference;
+
+class AlphabeticalComparatorTestCase {
+
+ private final AlphabeticalComparator comparator = new AlphabeticalComparator();
+
+ @Test
+ void compare_bothNullIdentifiers_returnsZero() {
+ Reference ref0 = new Reference((String) null);
+ Reference ref1 = new Reference((String) null);
+ assertEquals(0, comparator.compare(ref0, ref1));
+ }
+
+ @Test
+ void compare_firstNullIdentifier_returnsNegative() {
+ Reference ref0 = new Reference((String) null);
+ Reference ref1 = new Reference("http://example.com/a");
+ assertTrue(comparator.compare(ref0, ref1) < 0);
+ }
+
+ @Test
+ void compare_secondNullIdentifier_returnsPositive() {
+ Reference ref0 = new Reference("http://example.com/a");
+ Reference ref1 = new Reference((String) null);
+ assertTrue(comparator.compare(ref0, ref1) > 0);
+ }
+
+ @Test
+ void compare_bothNonNull_delegatesToStringComparison() {
+ Reference ref0 = new Reference("http://example.com/a");
+ Reference ref1 = new Reference("http://example.com/b");
+ assertTrue(comparator.compare(ref0, ref1) < 0);
+ assertTrue(comparator.compare(ref1, ref0) > 0);
+ }
+
+ @Test
+ void compareStrings_usesNaturalOrdering() {
+ assertTrue(comparator.compare("abc", "abd") < 0);
+ assertEquals(0, comparator.compare("abc", "abc"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/CaseInsensitiveHashSetTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/CaseInsensitiveHashSetTestCase.java
new file mode 100644
index 0000000000..226b569ec9
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/CaseInsensitiveHashSetTestCase.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+class CaseInsensitiveHashSetTestCase {
+
+ @Test
+ void constructor_lowercasesInitialElements() {
+ CaseInsensitiveHashSet set = new CaseInsensitiveHashSet(Arrays.asList("Foo", "BAR"));
+ assertTrue(set.contains("foo"));
+ assertTrue(set.contains("FOO"));
+ assertTrue(set.contains("bar"));
+ }
+
+ @Test
+ void add_lowercasesElement() {
+ CaseInsensitiveHashSet set = new CaseInsensitiveHashSet(Collections.emptyList());
+ set.add("Hello");
+ assertTrue(set.contains("hello"));
+ assertTrue(set.contains("HELLO"));
+ }
+
+ @Test
+ void contains_stringOverload_isCaseInsensitive() {
+ CaseInsensitiveHashSet set = new CaseInsensitiveHashSet(Collections.singletonList("value"));
+ assertTrue(set.contains("VALUE"));
+ assertFalse(set.contains("other"));
+ }
+
+ @Test
+ void contains_objectOverload_delegatesToStringOverload() {
+ CaseInsensitiveHashSet set = new CaseInsensitiveHashSet(Collections.singletonList("value"));
+ Object key = "VALUE";
+ assertTrue(set.contains(key));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/ChildClientDispatcherTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/ChildClientDispatcherTestCase.java
new file mode 100644
index 0000000000..d984e381d2
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/ChildClientDispatcherTestCase.java
@@ -0,0 +1,150 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.jupiter.api.Test;
+import org.restlet.Application;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.LocalReference;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.routing.Filter;
+
+class ChildClientDispatcherTestCase {
+
+ @Test
+ void doHandle_riapApplication_dispatchesToChildApplicationInboundRoot() throws Exception {
+ Context parentContext = new Context();
+ ChildContext childContext = new ChildContext(parentContext);
+ AtomicBoolean handled = new AtomicBoolean();
+ Application application =
+ new Application(childContext) {
+ @Override
+ public Restlet createInboundRoot() {
+ return new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ handled.set(true);
+ response.setStatus(Status.SUCCESS_OK);
+ }
+ };
+ }
+ };
+ application.start();
+ childContext.setChild(application);
+
+ ChildClientDispatcher dispatcher = new ChildClientDispatcher(childContext);
+ Request request =
+ new Request(
+ Method.GET,
+ LocalReference.createRiapReference(LocalReference.RIAP_APPLICATION, "/path")
+ .toString());
+ Response response = new Response(request);
+
+ int result = dispatcher.doHandle(request, response);
+
+ assertTrue(handled.get());
+ assertEquals(Filter.CONTINUE, result);
+ }
+
+ @Test
+ void doHandle_riapComponent_delegatesToParentClientDispatcher() {
+ Context parentContext = new Context();
+ AtomicBoolean handled = new AtomicBoolean();
+ parentContext.setClientDispatcher(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ handled.set(true);
+ }
+ });
+ ChildContext childContext = new ChildContext(parentContext);
+ ChildClientDispatcher dispatcher = new ChildClientDispatcher(childContext);
+
+ Request request =
+ new Request(
+ Method.GET,
+ LocalReference.createRiapReference(LocalReference.RIAP_COMPONENT, "/path")
+ .toString());
+ Response response = new Response(request);
+
+ dispatcher.doHandle(request, response);
+
+ assertTrue(handled.get());
+ }
+
+ @Test
+ void doHandle_riapHost_delegatesToParentClientDispatcher() {
+ Context parentContext = new Context();
+ AtomicBoolean handled = new AtomicBoolean();
+ parentContext.setClientDispatcher(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ handled.set(true);
+ }
+ });
+ ChildContext childContext = new ChildContext(parentContext);
+ ChildClientDispatcher dispatcher = new ChildClientDispatcher(childContext);
+
+ Request request =
+ new Request(
+ Method.GET,
+ LocalReference.createRiapReference(LocalReference.RIAP_HOST, "/path")
+ .toString());
+ Response response = new Response(request);
+
+ dispatcher.doHandle(request, response);
+
+ assertTrue(handled.get());
+ }
+
+ @Test
+ void doHandle_nonRiapProtocol_delegatesToParentClientDispatcher() {
+ Context parentContext = new Context();
+ AtomicBoolean handled = new AtomicBoolean();
+ parentContext.setClientDispatcher(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ handled.set(true);
+ }
+ });
+ ChildContext childContext = new ChildContext(parentContext);
+ ChildClientDispatcher dispatcher = new ChildClientDispatcher(childContext);
+
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ int result = dispatcher.doHandle(request, response);
+
+ assertTrue(handled.get());
+ assertEquals(Filter.CONTINUE, result);
+ }
+
+ @Test
+ void doHandle_noParentContext_logsWarningAndDoesNotThrow() {
+ ChildContext childContext = new ChildContext(null);
+ ChildClientDispatcher dispatcher = new ChildClientDispatcher(childContext);
+
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ int result = dispatcher.doHandle(request, response);
+
+ assertEquals(Filter.CONTINUE, result);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/ChildContextTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/ChildContextTestCase.java
new file mode 100644
index 0000000000..974c2855f5
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/ChildContextTestCase.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Restlet;
+
+class ChildContextTestCase {
+
+ @Test
+ void constructor_withParentContext_copiesServerDispatcher() {
+ Context parent = new Context();
+ ChildContext child = new ChildContext(parent);
+ assertEquals(parent.getServerDispatcher(), child.getServerDispatcher());
+ assertEquals(parent, child.getParentContext());
+ }
+
+ @Test
+ void constructor_setsChildClientDispatcher() {
+ Context parent = new Context();
+ ChildContext child = new ChildContext(parent);
+ assertNotNull(child.getClientDispatcher());
+ }
+
+ @Test
+ void constructor_withNullParentContext_leavesServerDispatcherNull() {
+ ChildContext child = new ChildContext(null);
+ assertNull(child.getServerDispatcher());
+ assertNull(child.getExecutorService());
+ }
+
+ @Test
+ void getChildAndSetChild_roundTrip() {
+ Context parent = new Context();
+ ChildContext child = new ChildContext(parent);
+ Restlet restlet = new Restlet() {};
+
+ child.setChild(restlet);
+
+ assertEquals(restlet, child.getChild());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/ContextualRunnableTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/ContextualRunnableTestCase.java
new file mode 100644
index 0000000000..3c0fea0c25
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/ContextualRunnableTestCase.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class ContextualRunnableTestCase {
+
+ private static class TestContextualRunnable extends ContextualRunnable {
+ volatile boolean ran;
+
+ @Override
+ public void run() {
+ ran = true;
+ }
+ }
+
+ @Test
+ void constructor_capturesCurrentThreadContextClassLoader() {
+ TestContextualRunnable runnable = new TestContextualRunnable();
+ assertEquals(
+ Thread.currentThread().getContextClassLoader(), runnable.getContextClassLoader());
+ }
+
+ @Test
+ void setContextClassLoader_updatesValue() {
+ TestContextualRunnable runnable = new TestContextualRunnable();
+ ClassLoader custom = ContextualRunnableTestCase.class.getClassLoader();
+ runnable.setContextClassLoader(custom);
+ assertEquals(custom, runnable.getContextClassLoader());
+ }
+
+ @Test
+ void run_invokesSubclassImplementation() {
+ TestContextualRunnable runnable = new TestContextualRunnable();
+ runnable.run();
+ assertTrue(runnable.ran);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/EngineClassLoaderTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/EngineClassLoaderTestCase.java
new file mode 100644
index 0000000000..a4d49f2882
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/EngineClassLoaderTestCase.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.net.URL;
+import java.util.Enumeration;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.engine.Engine;
+
+class EngineClassLoaderTestCase {
+
+ private Engine engine;
+
+ private ClassLoader previousUserClassLoader;
+
+ @BeforeEach
+ void setUpEach() {
+ engine = Engine.getInstance();
+ previousUserClassLoader = engine.getUserClassLoader();
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ engine.setUserClassLoader(previousUserClassLoader);
+ }
+
+ @Test
+ void loadClass_existingClass_delegatesToUserClassLoader() throws ClassNotFoundException {
+ engine.setUserClassLoader(EngineClassLoaderTestCase.class.getClassLoader());
+ EngineClassLoader loader = new EngineClassLoader(engine);
+
+ Class> result = loader.loadClass("java.lang.String");
+
+ assertEquals(String.class, result);
+ }
+
+ @Test
+ void loadClass_withoutUserClassLoader_fallsBackToContextClassLoader()
+ throws ClassNotFoundException {
+ engine.setUserClassLoader(null);
+ EngineClassLoader loader = new EngineClassLoader(engine);
+
+ Class> result = loader.loadClass("java.lang.String");
+
+ assertEquals(String.class, result);
+ }
+
+ @Test
+ void loadClass_unknownClass_throwsClassNotFoundException() {
+ EngineClassLoader loader = new EngineClassLoader(engine);
+
+ assertThrows(
+ ClassNotFoundException.class, () -> loader.loadClass("com.example.NoSuchClass"));
+ }
+
+ @Test
+ void getResource_delegatesToUserClassLoader() {
+ engine.setUserClassLoader(EngineClassLoaderTestCase.class.getClassLoader());
+ EngineClassLoader loader = new EngineClassLoader(engine);
+
+ URL result = loader.getResource("java/lang/String.class");
+
+ assertNotNull(result);
+ }
+
+ @Test
+ void getResources_returnsDeduplicatedEnumeration() throws Exception {
+ engine.setUserClassLoader(EngineClassLoaderTestCase.class.getClassLoader());
+ EngineClassLoader loader = new EngineClassLoader(engine);
+
+ Enumeration resources = loader.getResources("java/lang/String.class");
+
+ assertNotNull(resources);
+ }
+
+ @Test
+ void getEngine_returnsConstructorArgument() {
+ EngineClassLoader loader = new EngineClassLoader(engine);
+ assertEquals(engine, loader.getEngine());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/FormUtilsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/FormUtilsTestCase.java
new file mode 100644
index 0000000000..1f6946aa1d
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/FormUtilsTestCase.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.Form;
+import org.restlet.data.MediaType;
+import org.restlet.data.Parameter;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+class FormUtilsTestCase {
+
+ @Test
+ void create_nullName_returnsNull() {
+ assertNull(FormUtils.create(null, "value", false, CharacterSet.UTF_8));
+ }
+
+ @Test
+ void create_notDecoded_usesRawValues() {
+ Parameter param = FormUtils.create("a%20b", "c%20d", false, CharacterSet.UTF_8);
+ assertEquals("a%20b", param.getName());
+ assertEquals("c%20d", param.getValue());
+ }
+
+ @Test
+ void create_decoded_decodesNameAndValue() {
+ Parameter param = FormUtils.create("a%20b", "c%20d", true, CharacterSet.UTF_8);
+ assertEquals("a b", param.getName());
+ assertEquals("c d", param.getValue());
+ }
+
+ @Test
+ void create_nullValue_producesNullValuedParameter() {
+ Parameter param = FormUtils.create("name", null, false, CharacterSet.UTF_8);
+ assertEquals("name", param.getName());
+ assertNull(param.getValue());
+ }
+
+ @Test
+ void getFirstParameter_fromQueryString_returnsMatch() throws IOException {
+ Parameter param =
+ FormUtils.getFirstParameter("a=1&b=2", "b", CharacterSet.UTF_8, '&', true);
+ assertEquals("2", param.getValue());
+ }
+
+ @Test
+ void getFirstParameter_fromRepresentation_returnsMatch() throws IOException {
+ Representation post = new StringRepresentation("a=1&b=2", MediaType.APPLICATION_WWW_FORM);
+ Parameter param = FormUtils.getFirstParameter(post, "a");
+ assertEquals("1", param.getValue());
+ }
+
+ @Test
+ void getParameter_singleMatch_returnsParameterValue() throws IOException {
+ Object result = FormUtils.getParameter("a=1&b=2", "a", CharacterSet.UTF_8, '&', true);
+ assertEquals("1", result);
+ }
+
+ @Test
+ void getParameter_multipleMatches_returnsListOfValues() throws IOException {
+ Object result = FormUtils.getParameter("a=1&a=2", "a", CharacterSet.UTF_8, '&', true);
+ assertTrue(result instanceof java.util.List>);
+ assertEquals(2, ((java.util.List>) result).size());
+ }
+
+ @Test
+ void getParameters_fromQueryString_populatesMap() throws IOException {
+ Map params = new HashMap<>();
+ params.put("a", null);
+ params.put("b", null);
+ FormUtils.getParameters("a=1&b=2", params, CharacterSet.UTF_8, '&', true);
+ assertEquals("1", params.get("a"));
+ assertEquals("2", params.get("b"));
+ }
+
+ @Test
+ void getParameters_fromRepresentation_populatesMap() throws IOException {
+ Representation post = new StringRepresentation("a=1", MediaType.APPLICATION_WWW_FORM);
+ Map params = new HashMap<>();
+ params.put("a", null);
+ FormUtils.getParameters(post, params);
+ assertEquals("1", params.get("a"));
+ }
+
+ @Test
+ void isParameterFound_matchingParameter_returnsTrue() {
+ Parameter searched = new Parameter("charset", "UTF-8");
+ MediaType typeWithParam = new MediaType("application/json;charset=UTF-8");
+ assertTrue(FormUtils.isParameterFound(searched, typeWithParam));
+ }
+
+ @Test
+ void isParameterFound_nonMatchingParameter_returnsFalse() {
+ Parameter searched = new Parameter("charset", "ISO-8859-1");
+ MediaType typeWithParam = new MediaType("application/json;charset=UTF-8");
+ assertFalse(FormUtils.isParameterFound(searched, typeWithParam));
+ }
+
+ @Test
+ void parse_representationOverload_addsParametersToForm() {
+ Form form = new Form();
+ Representation post = new StringRepresentation("a=1", MediaType.APPLICATION_WWW_FORM);
+ FormUtils.parse(form, post, true);
+ assertEquals("1", form.getFirstValue("a"));
+ }
+
+ @Test
+ void parse_nullRepresentation_leavesFormUnchanged() {
+ Form form = new Form();
+ FormUtils.parse(form, (Representation) null, true);
+ assertTrue(form.isEmpty());
+ }
+
+ @Test
+ void parse_stringOverload_addsParametersToForm() {
+ Form form = new Form();
+ FormUtils.parse(form, "a=1&b=2", CharacterSet.UTF_8, true, '&');
+ assertEquals("1", form.getFirstValue("a"));
+ assertEquals("2", form.getFirstValue("b"));
+ }
+
+ @Test
+ void parse_emptyString_leavesFormUnchanged() {
+ Form form = new Form();
+ FormUtils.parse(form, "", CharacterSet.UTF_8, true, '&');
+ assertTrue(form.isEmpty());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/ImmutableDateTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/ImmutableDateTestCase.java
new file mode 100644
index 0000000000..1147cc49a8
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/ImmutableDateTestCase.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Date;
+import org.junit.jupiter.api.Test;
+
+class ImmutableDateTestCase {
+
+ @Test
+ void constructor_copiesTimeFromSource() {
+ Date source = new Date(123456789L);
+ ImmutableDate immutable = new ImmutableDate(source);
+ assertEquals(source.getTime(), immutable.getTime());
+ }
+
+ @Test
+ void clone_throwsUnsupportedOperationException() {
+ ImmutableDate immutable = new ImmutableDate(new Date());
+ assertThrows(UnsupportedOperationException.class, immutable::clone);
+ }
+
+ @Test
+ void setDate_throwsUnsupportedOperationException() {
+ ImmutableDate immutable = new ImmutableDate(new Date());
+ assertThrows(UnsupportedOperationException.class, () -> immutable.setDate(1));
+ }
+
+ @Test
+ void setHours_throwsUnsupportedOperationException() {
+ ImmutableDate immutable = new ImmutableDate(new Date());
+ assertThrows(UnsupportedOperationException.class, () -> immutable.setHours(1));
+ }
+
+ @Test
+ void setMinutes_throwsUnsupportedOperationException() {
+ ImmutableDate immutable = new ImmutableDate(new Date());
+ assertThrows(UnsupportedOperationException.class, () -> immutable.setMinutes(1));
+ }
+
+ @Test
+ void setMonth_throwsUnsupportedOperationException() {
+ ImmutableDate immutable = new ImmutableDate(new Date());
+ assertThrows(UnsupportedOperationException.class, () -> immutable.setMonth(1));
+ }
+
+ @Test
+ void setSeconds_throwsUnsupportedOperationException() {
+ ImmutableDate immutable = new ImmutableDate(new Date());
+ assertThrows(UnsupportedOperationException.class, () -> immutable.setSeconds(1));
+ }
+
+ @Test
+ void setTime_throwsUnsupportedOperationException() {
+ ImmutableDate immutable = new ImmutableDate(new Date());
+ assertThrows(UnsupportedOperationException.class, () -> immutable.setTime(1));
+ }
+
+ @Test
+ void setYear_throwsUnsupportedOperationException() {
+ ImmutableDate immutable = new ImmutableDate(new Date());
+ assertThrows(UnsupportedOperationException.class, () -> immutable.setYear(1));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/InternetDateFormatTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/InternetDateFormatTestCase.java
new file mode 100644
index 0000000000..fd3a0e7743
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/InternetDateFormatTestCase.java
@@ -0,0 +1,212 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.text.ParsePosition;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+import org.junit.jupiter.api.Test;
+
+class InternetDateFormatTestCase {
+
+ @Test
+ void toString_calendarUtc_formatsWithZSuffix() {
+ Calendar cal = new GregorianCalendar(InternetDateFormat.UTC);
+ cal.clear();
+ cal.set(2024, Calendar.MARCH, 5, 13, 7, 9);
+ assertEquals("2024-03-05T13:07:09Z", InternetDateFormat.toString(cal));
+ }
+
+ @Test
+ void toString_calendarWithMilliseconds_includesFraction() {
+ Calendar cal = new GregorianCalendar(InternetDateFormat.UTC);
+ cal.clear();
+ cal.set(2024, Calendar.MARCH, 5, 13, 7, 9);
+ cal.set(Calendar.MILLISECOND, 500);
+ assertEquals("2024-03-05T13:07:09.50Z", InternetDateFormat.toString(cal));
+ }
+
+ @Test
+ void toString_calendarWithPositiveOffset_formatsSignedOffset() {
+ TimeZone plusTwo = TimeZone.getTimeZone("GMT+02:00");
+ Calendar cal = new GregorianCalendar(plusTwo);
+ cal.clear();
+ cal.set(2024, Calendar.MARCH, 5, 13, 7, 9);
+ assertEquals("2024-03-05T13:07:09+02:00", InternetDateFormat.toString(cal));
+ }
+
+ @Test
+ void toString_calendarWithNegativeOffset_formatsSignedOffset() {
+ TimeZone minusFive = TimeZone.getTimeZone("GMT-05:00");
+ Calendar cal = new GregorianCalendar(minusFive);
+ cal.clear();
+ cal.set(2024, Calendar.MARCH, 5, 13, 7, 9);
+ assertEquals("2024-03-05T13:07:09-05:00", InternetDateFormat.toString(cal));
+ }
+
+ @Test
+ void toString_date_usesUtcByDefault() {
+ Date date = InternetDateFormat.parseDate("2024-03-05T13:07:09Z");
+ assertEquals("2024-03-05T13:07:09Z", InternetDateFormat.toString(date));
+ }
+
+ @Test
+ void toString_time_usesUtcByDefault() {
+ long time = InternetDateFormat.parseTime("2024-03-05T13:07:09Z");
+ assertEquals("2024-03-05T13:07:09Z", InternetDateFormat.toString(time));
+ }
+
+ @Test
+ void now_returnsParseableString() {
+ String now = InternetDateFormat.now();
+ assertNotNull(InternetDateFormat.parseDate(now));
+ }
+
+ @Test
+ void now_withZone_returnsParseableString() {
+ String now = InternetDateFormat.now(TimeZone.getTimeZone("GMT+01:00"));
+ assertNotNull(InternetDateFormat.parseDate(now));
+ }
+
+ @Test
+ void parse_zuluTimeZone_parsesAsUtc() {
+ Calendar cal = InternetDateFormat.parseCalendar("2024-03-05T13:07:09z");
+ assertEquals(2024, cal.get(Calendar.YEAR));
+ assertEquals(Calendar.MARCH, cal.get(Calendar.MONTH));
+ assertEquals(5, cal.get(Calendar.DAY_OF_MONTH));
+ assertEquals(0, cal.getTimeZone().getRawOffset());
+ }
+
+ @Test
+ void parse_explicitOffset_parsesTimeZone() {
+ Calendar cal = InternetDateFormat.parseCalendar("2024-03-05T13:07:09+02:30");
+ assertEquals(2 * 60 * 60000 + 30 * 60000, cal.getTimeZone().getRawOffset());
+ }
+
+ @Test
+ void parse_negativeOffset_parsesTimeZone() {
+ Calendar cal = InternetDateFormat.parseCalendar("2024-03-05T13:07:09-02:30");
+ assertEquals(-(2 * 60 * 60000 + 30 * 60000), cal.getTimeZone().getRawOffset());
+ }
+
+ @Test
+ void parse_withFractionalSeconds_setsMilliseconds() {
+ Calendar cal = InternetDateFormat.parseCalendar("2024-03-05T13:07:09.5Z");
+ assertEquals(500, cal.get(Calendar.MILLISECOND));
+ }
+
+ @Test
+ void parse_spaceSeparator_isAccepted() {
+ Calendar cal = InternetDateFormat.parseCalendar("2024-03-05 13:07:09Z");
+ assertEquals(2024, cal.get(Calendar.YEAR));
+ }
+
+ @Test
+ void parse_invalidString_throwsIllegalArgumentException() {
+ assertThrows(
+ IllegalArgumentException.class, () -> InternetDateFormat.parseDate("not-a-date"));
+ }
+
+ @Test
+ void valueOf_date_roundTripsToSameInstant() {
+ Date date = new Date(1_700_000_000_000L);
+ InternetDateFormat format = InternetDateFormat.valueOf(date);
+ assertEquals(date, format.getDate());
+ assertEquals(date.getTime(), format.getTime());
+ }
+
+ @Test
+ void valueOf_dateWithZone_roundTripsToSameInstant() {
+ Date date = new Date(1_700_000_000_000L);
+ InternetDateFormat format =
+ InternetDateFormat.valueOf(date, TimeZone.getTimeZone("GMT+01:00"));
+ assertEquals(date, format.getDate());
+ }
+
+ @Test
+ void valueOf_time_roundTripsToSameInstant() {
+ long time = 1_700_000_000_000L;
+ InternetDateFormat format = InternetDateFormat.valueOf(time);
+ assertEquals(time, format.getTime());
+ }
+
+ @Test
+ void valueOf_timeWithZone_roundTripsToSameInstant() {
+ long time = 1_700_000_000_000L;
+ InternetDateFormat format =
+ InternetDateFormat.valueOf(time, TimeZone.getTimeZone("GMT+01:00"));
+ assertEquals(time, format.getTime());
+ }
+
+ @Test
+ void valueOf_string_parsesToMatchingCalendar() {
+ InternetDateFormat format = InternetDateFormat.valueOf("2024-03-05T13:07:09Z");
+ assertEquals("2024-03-05T13:07:09Z", format.toString());
+ }
+
+ @Test
+ void constructor_fromCalendar_clonesInput() {
+ Calendar cal = new GregorianCalendar(InternetDateFormat.UTC);
+ cal.clear();
+ cal.set(2024, Calendar.MARCH, 5, 13, 7, 9);
+ InternetDateFormat format = new InternetDateFormat(cal);
+
+ cal.set(Calendar.YEAR, 1999);
+
+ assertEquals(2024, format.getCalendar().get(Calendar.YEAR));
+ }
+
+ @Test
+ void constructor_defaultZone_usesUtc() {
+ InternetDateFormat format = new InternetDateFormat();
+ assertEquals(0, format.getCalendar().getTimeZone().getRawOffset());
+ }
+
+ @Test
+ void constructor_withZoneOnly_setsCurrentTimeInThatZone() {
+ InternetDateFormat format = new InternetDateFormat(TimeZone.getTimeZone("GMT+01:00"));
+ assertEquals(60 * 60000, format.getCalendar().getTimeZone().getRawOffset());
+ }
+
+ @Test
+ void format_appendsRfc3339RepresentationToBuffer() {
+ Date date = InternetDateFormat.parseDate("2024-03-05T13:07:09Z");
+ StringBuffer buffer = new StringBuffer("prefix-");
+ InternetDateFormat format = new InternetDateFormat();
+ format.format(date, buffer, null);
+ assertEquals("prefix-2024-03-05T13:07:09Z", buffer.toString());
+ }
+
+ @Test
+ void parse_instanceMethod_delegatesToStaticParseDate() throws Exception {
+ InternetDateFormat format = new InternetDateFormat();
+ Date date = format.parse("2024-03-05T13:07:09Z");
+ assertEquals(InternetDateFormat.parseDate("2024-03-05T13:07:09Z"), date);
+ }
+
+ @Test
+ void parse_instanceMethodWithPosition_delegatesToStaticParseDate() {
+ InternetDateFormat format = new InternetDateFormat();
+ Date date = format.parse("2024-03-05T13:07:09Z", new ParsePosition(0));
+ assertEquals(InternetDateFormat.parseDate("2024-03-05T13:07:09Z"), date);
+ }
+
+ @Test
+ void utc_hasZeroOffsetAndNoDst() {
+ assertTrue(InternetDateFormat.UTC.getRawOffset() == 0);
+ assertTrue(InternetDateFormat.UTC.getDSTSavings() == 0);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/ReferenceUtilsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/ReferenceUtilsTestCase.java
new file mode 100644
index 0000000000..8632aca638
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/ReferenceUtilsTestCase.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.data.Header;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.engine.header.HeaderConstants;
+import org.restlet.util.Series;
+
+class ReferenceUtilsTestCase {
+
+ @Test
+ void update_withoutChallengeResponse_returnsAbsoluteReferenceUnchanged() {
+ Request request = new Request(Method.GET, "http://example.com/path");
+ Reference result = ReferenceUtils.update(request.getResourceRef(), request);
+ assertEquals("http://example.com/path", result.toString());
+ }
+
+ @Test
+ void format_notProxied_returnsPathAndQuery() {
+ Request request = new Request(Method.GET, "http://example.com/path?a=b");
+ String result = ReferenceUtils.format(request.getResourceRef(), false, request);
+ assertEquals("/path?a=b", result);
+ }
+
+ @Test
+ void format_notProxiedWithoutQuery_returnsPathOnly() {
+ Request request = new Request(Method.GET, "http://example.com/path");
+ String result = ReferenceUtils.format(request.getResourceRef(), false, request);
+ assertEquals("/path", result);
+ }
+
+ @Test
+ void format_notProxiedWithEmptyPath_returnsRootSlash() {
+ Request request = new Request(Method.GET, "http://example.com");
+ String result = ReferenceUtils.format(request.getResourceRef(), false, request);
+ assertEquals("/", result);
+ }
+
+ @Test
+ void format_proxied_returnsFullIdentifier() {
+ Request request = new Request(Method.GET, "http://example.com/path?a=b");
+ String result = ReferenceUtils.format(request.getResourceRef(), true, request);
+ assertEquals("http://example.com/path?a=b", result);
+ }
+
+ @Test
+ void getOriginalRef_withNullHeaders_returnsTargetRef() {
+ Reference resourceRef = new Reference("http://example.com/path");
+ Reference result = ReferenceUtils.getOriginalRef(resourceRef, null);
+ assertEquals("http://example.com/path", result.toString());
+ }
+
+ @Test
+ void getOriginalRef_withForwardedHeaders_updatesPortAndScheme() {
+ Reference resourceRef = new Reference("http://example.com/path");
+ Series headers = new Series<>(Header.class);
+ headers.add(HeaderConstants.HEADER_X_FORWARDED_PORT, "8443");
+ headers.add(HeaderConstants.HEADER_X_FORWARDED_PROTO, "https");
+
+ Reference result = ReferenceUtils.getOriginalRef(resourceRef, headers);
+
+ assertEquals(8443, result.getHostPort());
+ assertEquals("https", result.getScheme());
+ }
+
+ @Test
+ void getOriginalRef_withoutForwardedHeaders_leavesReferenceUnchanged() {
+ Reference resourceRef = new Reference("http://example.com/path");
+ Series headers = new Series<>(Header.class);
+
+ Reference result = ReferenceUtils.getOriginalRef(resourceRef, headers);
+
+ assertEquals("http", result.getScheme());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/SetUtilsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/SetUtilsTestCase.java
new file mode 100644
index 0000000000..dd491262ea
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/SetUtilsTestCase.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class SetUtilsTestCase {
+
+ @Test
+ void newHashSet_withElements_containsAllElements() {
+ Set set = SetUtils.newHashSet("a", "b", "c");
+ assertEquals(3, set.size());
+ assertTrue(set.contains("a"));
+ assertTrue(set.contains("b"));
+ assertTrue(set.contains("c"));
+ }
+
+ @Test
+ void newHashSet_withNoElements_isEmpty() {
+ Set set = SetUtils.newHashSet();
+ assertTrue(set.isEmpty());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/StringUtilsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/StringUtilsTestCase.java
new file mode 100644
index 0000000000..fa056f28a2
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/StringUtilsTestCase.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+
+class StringUtilsTestCase {
+
+ @Test
+ void firstLower_lowercasesFirstCharacterOnly() {
+ assertEquals("hELLO", StringUtils.firstLower("HELLO"));
+ }
+
+ @Test
+ void firstLower_nullOrEmpty_returnsUnchanged() {
+ assertNull(StringUtils.firstLower(null));
+ assertEquals("", StringUtils.firstLower(""));
+ }
+
+ @Test
+ void firstUpper_uppercasesFirstCharacterOnly() {
+ assertEquals("Hello", StringUtils.firstUpper("hello"));
+ }
+
+ @Test
+ void firstUpper_nullOrEmpty_returnsUnchanged() {
+ assertNull(StringUtils.firstUpper(null));
+ assertEquals("", StringUtils.firstUpper(""));
+ }
+
+ @Test
+ void getAsciiBytes_encodesString() {
+ assertArrayEquals(
+ "abc".getBytes(StandardCharsets.US_ASCII), StringUtils.getAsciiBytes("abc"));
+ }
+
+ @Test
+ void getAsciiBytes_null_returnsEmptyArray() {
+ assertArrayEquals(new byte[0], StringUtils.getAsciiBytes(null));
+ }
+
+ @Test
+ void getLatin1Bytes_encodesString() {
+ assertArrayEquals(
+ "abc".getBytes(StandardCharsets.ISO_8859_1), StringUtils.getLatin1Bytes("abc"));
+ }
+
+ @Test
+ void getLatin1Bytes_null_returnsEmptyArray() {
+ assertArrayEquals(new byte[0], StringUtils.getLatin1Bytes(null));
+ }
+
+ @Test
+ void htmlEscape_nullOrEmpty_returnsUnchanged() {
+ assertNull(StringUtils.htmlEscape(null));
+ assertEquals("", StringUtils.htmlEscape(""));
+ }
+
+ @Test
+ void htmlEscape_asciiWithoutEntities_isUnchanged() {
+ assertEquals("hello world", StringUtils.htmlEscape("hello world"));
+ }
+
+ @Test
+ void htmlEscape_knownEntities_areEscapedByName() {
+ assertEquals(""&<>", StringUtils.htmlEscape("\"&<>"));
+ }
+
+ @Test
+ void htmlEscape_nonAsciiKnownEntity_isEscapedByName() {
+ // U+00FF (yuml) and U+20AC (euro) are both known HTML 4.0 entities.
+ assertEquals("ÿ", StringUtils.htmlEscape("ÿ"));
+ assertEquals("€", StringUtils.htmlEscape("€"));
+ }
+
+ @Test
+ void htmlEscape_nonAsciiWithoutEntity_isEscapedNumerically() {
+ // U+00FE (thorn) is a known entity, but codepoints beyond the 10000-entry table
+ // (e.g. U+2603 SNOWMAN) fall back to numeric escaping.
+ assertEquals("☃", StringUtils.htmlEscape("☃"));
+ }
+
+ @Test
+ void htmlUnescape_nullOrEmpty_returnsUnchanged() {
+ assertNull(StringUtils.htmlUnescape(null));
+ assertEquals("", StringUtils.htmlUnescape(""));
+ }
+
+ @Test
+ void htmlUnescape_namedEntity_isDecoded() {
+ assertEquals("\"&<>", StringUtils.htmlUnescape(""&<>"));
+ }
+
+ @Test
+ void htmlUnescape_decimalNumericEntity_isDecoded() {
+ assertEquals("€", StringUtils.htmlUnescape("€"));
+ }
+
+ @Test
+ void htmlUnescape_hexNumericEntityLowerX_isDecoded() {
+ assertEquals("€", StringUtils.htmlUnescape("€"));
+ }
+
+ @Test
+ void htmlUnescape_hexNumericEntityUpperX_isDecoded() {
+ assertEquals("€", StringUtils.htmlUnescape("€"));
+ }
+
+ @Test
+ void htmlUnescape_invalidNumericEntity_isLeftAsIs() {
+ assertEquals("notanumber;", StringUtils.htmlUnescape("notanumber;"));
+ }
+
+ @Test
+ void htmlUnescape_unknownEntityName_isLeftAsIs() {
+ assertEquals("&unknownentity;", StringUtils.htmlUnescape("&unknownentity;"));
+ }
+
+ @Test
+ void htmlUnescape_emptyEntity_isReplacedWithAmpersandSemicolon() {
+ assertEquals("&;", StringUtils.htmlUnescape("&;"));
+ }
+
+ @Test
+ void htmlUnescape_ampersandWithoutSemicolon_isLeftAsIs() {
+ assertEquals("a & b", StringUtils.htmlUnescape("a & b"));
+ }
+
+ @Test
+ void htmlUnescape_consecutiveAmpersands_areHandled() {
+ assertEquals("&&", StringUtils.htmlUnescape("&&"));
+ }
+
+ @Test
+ void htmlUnescape_trailingAmpersand_isLeftAsIs() {
+ assertEquals("value&", StringUtils.htmlUnescape("value&"));
+ }
+
+ @Test
+ void isNullOrEmpty_nullOrEmptyString_returnsTrue() {
+ assertTrue(StringUtils.isNullOrEmpty(null));
+ assertTrue(StringUtils.isNullOrEmpty(""));
+ }
+
+ @Test
+ void isNullOrEmpty_nonEmptyString_returnsFalse() {
+ assertFalse(StringUtils.isNullOrEmpty("x"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/SystemUtilsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/SystemUtilsTestCase.java
new file mode 100644
index 0000000000..a2d9cdc80e
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/SystemUtilsTestCase.java
@@ -0,0 +1,74 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import org.junit.jupiter.api.Test;
+
+class SystemUtilsTestCase {
+
+ @Test
+ void getJavaMajorVersion_parsesJavaVersionSystemProperty() {
+ String javaVersion = System.getProperty("java.version");
+ int dotIndex = javaVersion.indexOf('.');
+ int expected = dotIndex < 0 ? 0 : Integer.parseInt(javaVersion.substring(0, dotIndex));
+ assertEquals(expected, SystemUtils.getJavaMajorVersion());
+ }
+
+ @Test
+ void getJavaMinorVersion_returnsZero_whenVersionHasNoMinorComponent() {
+ // Modern JDKs (e.g. "21") have no "major.minor" form, exercising the catch branch.
+ assertEquals(0, SystemUtils.getJavaMinorVersion());
+ }
+
+ @Test
+ void getJavaUpdateVersion_returnsZero_whenVersionHasNoUpdateComponent() {
+ // Modern JDKs have no "_update" suffix, exercising the catch branch.
+ assertEquals(0, SystemUtils.getJavaUpdateVersion());
+ }
+
+ @Test
+ void hashCode_nullVarargs_returnsBaseValue() {
+ assertEquals(17, SystemUtils.hashCode((Object[]) null));
+ }
+
+ @Test
+ void hashCode_noArguments_returnsBaseValue() {
+ assertEquals(17, SystemUtils.hashCode(new Object[0]));
+ }
+
+ @Test
+ void hashCode_withNullElement_treatsAsZero() {
+ assertEquals(17 * 31, SystemUtils.hashCode((Object) null));
+ }
+
+ @Test
+ void hashCode_isConsistentForEqualInputs() {
+ assertEquals(SystemUtils.hashCode("a", "b"), SystemUtils.hashCode("a", "b"));
+ }
+
+ @Test
+ void hashCode_differsForDifferentInputs() {
+ assertFalse(SystemUtils.hashCode("a", "b") == SystemUtils.hashCode("a", "c"));
+ }
+
+ @Test
+ void isWindows_matchesOsNameProperty() {
+ boolean expected = System.getProperty("os.name").toLowerCase().contains("win");
+ assertEquals(expected, SystemUtils.isWindows());
+ }
+
+ @Test
+ void shouldApplyBug6331920Patch_returnsFalseOnModernNonSunJvm() {
+ // This JVM is neither an old Sun JVM nor an old Java version, so no patch is needed.
+ assertFalse(SystemUtils.shouldApplyBug6331920Patch());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/TemplateDispatcherTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/TemplateDispatcherTestCase.java
new file mode 100644
index 0000000000..0d65c264e0
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/TemplateDispatcherTestCase.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.representation.StringRepresentation;
+
+class TemplateDispatcherTestCase {
+
+ private final TemplateDispatcher dispatcher = new TemplateDispatcher();
+
+ @Test
+ void afterHandle_entityWithoutLocationRef_setsItFromResourceRef() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ response.setEntity(new StringRepresentation("body"));
+
+ dispatcher.afterHandle(request, response);
+
+ assertEquals(
+ request.getResourceRef().getTargetRef().toString(),
+ response.getEntity().getLocationRef().toString());
+ }
+
+ @Test
+ void afterHandle_entityWithExistingLocationRef_leavesItUnchanged() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ StringRepresentation entity = new StringRepresentation("body");
+ entity.setLocationRef("http://localhost/other");
+ response.setEntity(entity);
+
+ dispatcher.afterHandle(request, response);
+
+ assertEquals("http://localhost/other", response.getEntity().getLocationRef().toString());
+ }
+
+ @Test
+ void afterHandle_nullEntity_doesNothing() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ dispatcher.afterHandle(request, response);
+
+ assertNull(response.getEntity());
+ }
+
+ @Test
+ void beforeHandle_nullProtocol_throwsUnsupportedOperationException() {
+ Request request = new Request(Method.GET, "relative/path");
+ Response response = new Response(request);
+
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> dispatcher.beforeHandle(request, response));
+ }
+
+ @Test
+ void beforeHandle_uriWithoutTemplate_leavesResourceRefUnchanged() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Reference original = request.getResourceRef();
+ Response response = new Response(request);
+
+ int result = dispatcher.beforeHandle(request, response);
+
+ assertEquals(original.toString(), request.getResourceRef().toString());
+ assertEquals(org.restlet.routing.Filter.CONTINUE, result);
+ }
+
+ @Test
+ void beforeHandle_uriWithTemplate_resolvesPlaceholder() {
+ Request request = new Request(Method.GET, "http://localhost/{name}");
+ request.getAttributes().put("name", "world");
+ Response response = new Response(request);
+
+ dispatcher.beforeHandle(request, response);
+
+ assertEquals("http://localhost/world", request.getResourceRef().toString());
+ }
+
+ @Test
+ void beforeHandle_setsOriginalRef() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ dispatcher.beforeHandle(request, response);
+
+ assertEquals(request.getResourceRef().getTargetRef(), request.getOriginalRef());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/engine/util/WrapperScheduledExecutorServiceTestCase.java b/org.restlet/src/test/java/org/restlet/engine/util/WrapperScheduledExecutorServiceTestCase.java
new file mode 100644
index 0000000000..0e81b8ea4c
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/util/WrapperScheduledExecutorServiceTestCase.java
@@ -0,0 +1,136 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class WrapperScheduledExecutorServiceTestCase {
+
+ private ScheduledExecutorService delegate;
+
+ private WrapperScheduledExecutorService wrapper;
+
+ @BeforeEach
+ void setUpEach() {
+ delegate = Executors.newSingleThreadScheduledExecutor();
+ wrapper = new WrapperScheduledExecutorService(delegate);
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ delegate.shutdownNow();
+ }
+
+ @Test
+ void getWrapped_returnsConstructorArgument() {
+ assertEquals(delegate, wrapper.getWrapped());
+ }
+
+ @Test
+ void submitCallable_executesAndReturnsResult() throws Exception {
+ Future future = wrapper.submit(() -> "done");
+ assertEquals("done", future.get());
+ }
+
+ @Test
+ void submitRunnable_executesTask() throws Exception {
+ java.util.concurrent.atomic.AtomicBoolean ran =
+ new java.util.concurrent.atomic.AtomicBoolean();
+ Future> future = wrapper.submit(() -> ran.set(true));
+ future.get();
+ assertTrue(ran.get());
+ }
+
+ @Test
+ void submitRunnableWithResult_returnsGivenResult() throws Exception {
+ Future future = wrapper.submit(() -> {}, "result");
+ assertEquals("result", future.get());
+ }
+
+ @Test
+ void execute_runsCommand() throws Exception {
+ java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
+ wrapper.execute(latch::countDown);
+ assertTrue(latch.await(2, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void schedule_callable_executesAfterDelay() throws Exception {
+ ScheduledFuture future =
+ wrapper.schedule(() -> "scheduled", 1, TimeUnit.MILLISECONDS);
+ assertEquals("scheduled", future.get());
+ }
+
+ @Test
+ void schedule_runnable_executesAfterDelay() throws Exception {
+ ScheduledFuture> future = wrapper.schedule(() -> {}, 1, TimeUnit.MILLISECONDS);
+ future.get();
+ assertNotNull(future);
+ }
+
+ @Test
+ void invokeAll_executesAllTasks() throws Exception {
+ List> tasks = List.of(() -> "a", () -> "b");
+ List> results = wrapper.invokeAll(tasks);
+ assertEquals(2, results.size());
+ }
+
+ @Test
+ void invokeAllWithTimeout_executesAllTasks() throws Exception {
+ List> tasks = List.of(() -> "a");
+ List> results = wrapper.invokeAll(tasks, 2, TimeUnit.SECONDS);
+ assertEquals(1, results.size());
+ }
+
+ @Test
+ void invokeAny_returnsResultOfOneTask() throws Exception {
+ List> tasks = List.of(() -> "a");
+ assertEquals("a", wrapper.invokeAny(tasks));
+ }
+
+ @Test
+ void invokeAnyWithTimeout_returnsResultOfOneTask() throws Exception {
+ List> tasks = List.of(() -> "a");
+ assertEquals("a", wrapper.invokeAny(tasks, 2, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void isShutdownAndIsTerminated_reflectDelegateState() {
+ assertFalse(wrapper.isShutdown());
+ assertFalse(wrapper.isTerminated());
+ }
+
+ @Test
+ void shutdownAndAwaitTermination_stopsAcceptingTasks() throws Exception {
+ wrapper.shutdown();
+ boolean terminated = wrapper.awaitTermination(2, TimeUnit.SECONDS);
+ assertTrue(terminated);
+ assertTrue(wrapper.isShutdown());
+ }
+
+ @Test
+ void shutdownNow_returnsPendingTasks() {
+ List pending = wrapper.shutdownNow();
+ assertNotNull(pending);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/CharacterRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/representation/CharacterRepresentationTestCase.java
new file mode 100644
index 0000000000..a0d0aab924
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/CharacterRepresentationTestCase.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.MediaType;
+import org.restlet.engine.io.IoUtils;
+
+/**
+ * Unit tests for the {@link CharacterRepresentation} abstract class, exercised through a minimal
+ * local subclass.
+ *
+ * @author Jerome Louvel
+ */
+class CharacterRepresentationTestCase {
+
+ /** Minimal concrete subclass backed by an in-memory string. */
+ private static class SimpleCharacterRepresentation extends CharacterRepresentation {
+
+ private final String content;
+
+ SimpleCharacterRepresentation(MediaType mediaType, String content) {
+ super(mediaType);
+ this.content = content;
+ }
+
+ @Override
+ public Reader getReader() {
+ return new StringReader(content);
+ }
+
+ @Override
+ public void write(Writer writer) throws IOException {
+ writer.write(content);
+ }
+ }
+
+ @Test
+ void constructor_setsMediaTypeAndDefaultsToUtf8CharacterSet() {
+ SimpleCharacterRepresentation representation =
+ new SimpleCharacterRepresentation(MediaType.TEXT_PLAIN, "hello");
+
+ assertEquals(MediaType.TEXT_PLAIN, representation.getMediaType());
+ assertEquals(CharacterSet.UTF_8, representation.getCharacterSet());
+ }
+
+ @Test
+ void getStream_producesBytesMatchingReaderContent() throws IOException {
+ SimpleCharacterRepresentation representation =
+ new SimpleCharacterRepresentation(MediaType.TEXT_PLAIN, "some text");
+
+ try (InputStream stream = representation.getStream()) {
+ assertEquals("some text", IoUtils.toString(stream, representation.getCharacterSet()));
+ }
+ }
+
+ @Test
+ void writeToOutputStream_encodesUsingConfiguredCharacterSet() throws IOException {
+ SimpleCharacterRepresentation representation =
+ new SimpleCharacterRepresentation(MediaType.TEXT_PLAIN, "café");
+ representation.setCharacterSet(CharacterSet.UTF_8);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ representation.write(out);
+
+ assertEquals("café", new String(out.toByteArray(), CharacterSet.UTF_8.getName()));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/EmptyRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/representation/EmptyRepresentationTestCase.java
new file mode 100644
index 0000000000..dd03c0ccd1
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/EmptyRepresentationTestCase.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.StringWriter;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the {@link EmptyRepresentation} class.
+ *
+ * @author Jerome Louvel
+ */
+class EmptyRepresentationTestCase {
+
+ @Test
+ void constructor_setsUnavailableTransientAndZeroSize() {
+ EmptyRepresentation representation = new EmptyRepresentation();
+
+ assertFalse(representation.isAvailable());
+ assertTrue(representation.isTransient());
+ assertEquals(0, representation.getSize());
+ assertTrue(representation.isEmpty());
+ }
+
+ @Test
+ void getReaderGetStreamAndGetText_returnNull() throws Exception {
+ EmptyRepresentation representation = new EmptyRepresentation();
+
+ assertNull(representation.getReader());
+ assertNull(representation.getStream());
+ assertNull(representation.getText());
+ }
+
+ @Test
+ void writeToWriter_doesNothingAndDoesNotThrow() throws Exception {
+ EmptyRepresentation representation = new EmptyRepresentation();
+ StringWriter writer = new StringWriter();
+
+ representation.write(writer);
+
+ assertEquals("", writer.toString());
+ }
+
+ @Test
+ void writeToOutputStream_doesNothingAndDoesNotThrow() throws Exception {
+ EmptyRepresentation representation = new EmptyRepresentation();
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ representation.write(out);
+
+ assertEquals(0, out.size());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/InputRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/representation/InputRepresentationTestCase.java
new file mode 100644
index 0000000000..ac41ea36ba
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/InputRepresentationTestCase.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+
+/**
+ * Unit tests for the {@link InputRepresentation} class.
+ *
+ * @author Jerome Louvel
+ */
+class InputRepresentationTestCase {
+
+ private static InputStream stream(String content) {
+ return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Test
+ void constructor_withStreamOnly_isTransientAndAvailable() {
+ InputRepresentation representation = new InputRepresentation(stream("content"));
+
+ assertTrue(representation.isTransient());
+ assertTrue(representation.isAvailable());
+ assertEquals(Representation.UNKNOWN_SIZE, representation.getSize());
+ }
+
+ @Test
+ void constructor_withMediaType_setsMediaType() {
+ InputRepresentation representation =
+ new InputRepresentation(stream("content"), MediaType.TEXT_PLAIN);
+
+ assertEquals(MediaType.TEXT_PLAIN, representation.getMediaType());
+ }
+
+ @Test
+ void constructor_withExpectedSize_setsSize() {
+ InputRepresentation representation =
+ new InputRepresentation(stream("content"), MediaType.TEXT_PLAIN, 7L);
+
+ assertEquals(7L, representation.getSize());
+ }
+
+ @Test
+ void getStream_returnsStreamOnceThenNull() throws IOException {
+ InputRepresentation representation = new InputRepresentation(stream("content"));
+
+ InputStream first = representation.getStream();
+ assertTrue(first != null);
+
+ InputStream second = representation.getStream();
+ assertNull(second);
+ }
+
+ @Test
+ void getStream_marksRepresentationUnavailableAfterConsumption() throws IOException {
+ InputRepresentation representation = new InputRepresentation(stream("content"));
+
+ representation.getStream();
+
+ assertFalse(representation.isAvailable());
+ }
+
+ @Test
+ void getText_readsFullStreamContent() throws IOException {
+ InputRepresentation representation = new InputRepresentation(stream("hello world"));
+
+ assertEquals("hello world", representation.getText());
+ }
+
+ @Test
+ void write_copiesStreamContentToOutputStream() throws IOException {
+ InputRepresentation representation = new InputRepresentation(stream("copied content"));
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ representation.write(out);
+
+ assertEquals("copied content", out.toString(StandardCharsets.UTF_8.name()));
+ }
+
+ @Test
+ void release_closesStreamAndMarksUnavailable() {
+ InputRepresentation representation = new InputRepresentation(stream("content"));
+
+ representation.release();
+
+ assertFalse(representation.isAvailable());
+ }
+
+ @Test
+ void setStream_withNull_marksRepresentationUnavailable() {
+ InputRepresentation representation = new InputRepresentation(stream("content"));
+
+ representation.setStream(null);
+
+ assertFalse(representation.isAvailable());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/ObjectRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/representation/ObjectRepresentationTestCase.java
new file mode 100644
index 0000000000..fc40542abc
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/ObjectRepresentationTestCase.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.Serializable;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+
+/**
+ * Unit tests for the {@link ObjectRepresentation} class.
+ *
+ * @author Jerome Louvel
+ */
+class ObjectRepresentationTestCase {
+
+ private boolean initialBinarySupported;
+
+ private boolean initialXmlSupported;
+
+ @BeforeEach
+ void setUpEach() {
+ initialBinarySupported = ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED;
+ initialXmlSupported = ObjectRepresentation.VARIANT_OBJECT_XML_SUPPORTED;
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED = initialBinarySupported;
+ ObjectRepresentation.VARIANT_OBJECT_XML_SUPPORTED = initialXmlSupported;
+ }
+
+ @Test
+ void constructor_withObjectOnly_usesJavaObjectMediaType() throws IOException {
+ ObjectRepresentation representation = new ObjectRepresentation<>("payload");
+
+ assertEquals(MediaType.APPLICATION_JAVA_OBJECT, representation.getMediaType());
+ assertEquals("payload", representation.getObject());
+ }
+
+ @Test
+ void constructor_withNullMediaType_defaultsToJavaObject() {
+ ObjectRepresentation representation = new ObjectRepresentation<>("payload", null);
+
+ assertEquals(MediaType.APPLICATION_JAVA_OBJECT, representation.getMediaType());
+ }
+
+ @Test
+ void setObjectThenGetObject_roundTrips() throws IOException {
+ ObjectRepresentation representation = new ObjectRepresentation<>("initial");
+
+ representation.setObject("updated");
+
+ assertEquals("updated", representation.getObject());
+ }
+
+ @Test
+ void release_clearsObject() throws IOException {
+ ObjectRepresentation representation = new ObjectRepresentation<>("payload");
+
+ representation.release();
+
+ assertNull(representation.getObject());
+ }
+
+ @Test
+ void writeThenReadBack_roundTripsBinarySerializedObject() throws Exception {
+ ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED = true;
+ ObjectRepresentation original = new ObjectRepresentation<>("round-trip-value");
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ original.write(out);
+
+ Representation serialized =
+ new ByteArrayRepresentation(out.toByteArray(), MediaType.APPLICATION_JAVA_OBJECT);
+ ObjectRepresentation deserialized = new ObjectRepresentation<>(serialized);
+
+ assertEquals("round-trip-value", deserialized.getObject());
+ }
+
+ @Test
+ void constructor_fromBinaryRepresentation_throwsWhenBinaryNotSupported() throws Exception {
+ ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED = true;
+ ObjectRepresentation original = new ObjectRepresentation<>("value");
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ original.write(out);
+ Representation serialized =
+ new ByteArrayRepresentation(out.toByteArray(), MediaType.APPLICATION_JAVA_OBJECT);
+
+ ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED = false;
+
+ assertThrows(IllegalArgumentException.class, () -> new ObjectRepresentation<>(serialized));
+ }
+
+ @Test
+ void constructor_fromRepresentationWithUnsupportedMediaType_throwsIllegalArgumentException() {
+ Representation notSerialized = new StringRepresentation("plain text");
+
+ assertThrows(
+ IllegalArgumentException.class, () -> new ObjectRepresentation<>(notSerialized));
+ }
+
+ @Test
+ void write_withXmlMediaType_producesXmlEncodedOutput() throws IOException {
+ ObjectRepresentation representation =
+ new ObjectRepresentation<>("xml-value", MediaType.APPLICATION_JAVA_OBJECT_XML);
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ representation.write(out);
+
+ String xml = out.toString("UTF-8");
+ assertTrue(xml.contains("xml-value"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/OutputRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/representation/OutputRepresentationTestCase.java
new file mode 100644
index 0000000000..1b1115d96e
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/OutputRepresentationTestCase.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.engine.io.IoUtils;
+
+/**
+ * Unit tests for the {@link OutputRepresentation} abstract class, exercised through a minimal local
+ * subclass.
+ *
+ * @author Jerome Louvel
+ */
+class OutputRepresentationTestCase {
+
+ /** Minimal concrete subclass writing a fixed piece of content. */
+ private static class SimpleOutputRepresentation extends OutputRepresentation {
+
+ private final String content;
+
+ SimpleOutputRepresentation(MediaType mediaType, String content) {
+ super(mediaType);
+ this.content = content;
+ }
+
+ SimpleOutputRepresentation(MediaType mediaType, long expectedSize, String content) {
+ super(mediaType, expectedSize);
+ this.content = content;
+ }
+
+ @Override
+ public void write(OutputStream outputStream) throws IOException {
+ outputStream.write(content.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ @Test
+ void constructor_withMediaTypeOnly_hasUnknownSize() {
+ SimpleOutputRepresentation representation =
+ new SimpleOutputRepresentation(MediaType.TEXT_PLAIN, "data");
+
+ assertEquals(MediaType.TEXT_PLAIN, representation.getMediaType());
+ assertEquals(Representation.UNKNOWN_SIZE, representation.getSize());
+ }
+
+ @Test
+ void constructor_withExpectedSize_setsSize() {
+ SimpleOutputRepresentation representation =
+ new SimpleOutputRepresentation(MediaType.TEXT_PLAIN, 4L, "data");
+
+ assertEquals(4L, representation.getSize());
+ }
+
+ @Test
+ void getStream_producesContentWrittenByWrite() throws IOException {
+ SimpleOutputRepresentation representation =
+ new SimpleOutputRepresentation(MediaType.TEXT_PLAIN, "streamed content");
+
+ try (InputStream stream = representation.getStream()) {
+ assertEquals(
+ "streamed content", IoUtils.toString(stream, representation.getCharacterSet()));
+ }
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/RepresentationInfoTestCase.java b/org.restlet/src/test/java/org/restlet/representation/RepresentationInfoTestCase.java
new file mode 100644
index 0000000000..ca501315b8
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/RepresentationInfoTestCase.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.Date;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.MediaType;
+import org.restlet.data.Tag;
+
+/**
+ * Unit tests for the {@link RepresentationInfo} class.
+ *
+ * @author Jerome Louvel
+ */
+class RepresentationInfoTestCase {
+
+ @Test
+ void defaultConstructor_hasNoMediaTypeOrTag() {
+ RepresentationInfo info = new RepresentationInfo();
+
+ assertNull(info.getMediaType());
+ assertNull(info.getTag());
+ assertNull(info.getModificationDate());
+ }
+
+ @Test
+ void constructor_withMediaType_setsMediaType() {
+ RepresentationInfo info = new RepresentationInfo(MediaType.APPLICATION_JSON);
+
+ assertEquals(MediaType.APPLICATION_JSON, info.getMediaType());
+ }
+
+ @Test
+ void constructor_withMediaTypeAndModificationDate_setsBoth() {
+ Date date = new Date(0);
+ RepresentationInfo info = new RepresentationInfo(MediaType.TEXT_PLAIN, date);
+
+ assertEquals(MediaType.TEXT_PLAIN, info.getMediaType());
+ assertEquals(date, info.getModificationDate());
+ }
+
+ @Test
+ void constructor_withMediaTypeModificationDateAndTag_setsAllFields() {
+ Date date = new Date(0);
+ Tag tag = new Tag("etag-value");
+ RepresentationInfo info = new RepresentationInfo(MediaType.TEXT_PLAIN, date, tag);
+
+ assertEquals(MediaType.TEXT_PLAIN, info.getMediaType());
+ assertEquals(date, info.getModificationDate());
+ assertEquals(tag, info.getTag());
+ }
+
+ @Test
+ void constructor_withMediaTypeAndTag_setsBoth() {
+ Tag tag = new Tag("etag-value");
+ RepresentationInfo info = new RepresentationInfo(MediaType.TEXT_PLAIN, tag);
+
+ assertEquals(MediaType.TEXT_PLAIN, info.getMediaType());
+ assertEquals(tag, info.getTag());
+ assertNull(info.getModificationDate());
+ }
+
+ @Test
+ void constructor_fromVariant_copiesVariantMetadata() {
+ Variant variant = new Variant(MediaType.APPLICATION_XML);
+ variant.setCharacterSet(CharacterSet.UTF_8);
+ Date date = new Date(0);
+ Tag tag = new Tag("copied-tag");
+
+ RepresentationInfo info = new RepresentationInfo(variant, date, tag);
+
+ assertEquals(MediaType.APPLICATION_XML, info.getMediaType());
+ assertEquals(CharacterSet.UTF_8, info.getCharacterSet());
+ assertEquals(date, info.getModificationDate());
+ assertEquals(tag, info.getTag());
+ }
+
+ @Test
+ void constructor_fromVariantAndTag_copiesVariantMetadataWithoutDate() {
+ Variant variant = new Variant(MediaType.APPLICATION_XML);
+ Tag tag = new Tag("copied-tag");
+
+ RepresentationInfo info = new RepresentationInfo(variant, tag);
+
+ assertEquals(MediaType.APPLICATION_XML, info.getMediaType());
+ assertEquals(tag, info.getTag());
+ assertNull(info.getModificationDate());
+ }
+
+ @Test
+ void setModificationDateThenGetModificationDate_roundTrips() {
+ RepresentationInfo info = new RepresentationInfo();
+ Date date = new Date(123456789L);
+
+ info.setModificationDate(date);
+
+ assertEquals(date, info.getModificationDate());
+ }
+
+ @Test
+ void setTagThenGetTag_roundTrips() {
+ RepresentationInfo info = new RepresentationInfo();
+ Tag tag = new Tag("my-tag");
+
+ info.setTag(tag);
+
+ assertEquals(tag, info.getTag());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/RepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/representation/RepresentationTestCase.java
new file mode 100644
index 0000000000..393ebed89f
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/RepresentationTestCase.java
@@ -0,0 +1,235 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Digest;
+import org.restlet.data.Disposition;
+import org.restlet.data.MediaType;
+import org.restlet.data.Range;
+import org.restlet.data.Tag;
+
+/**
+ * Unit tests for the {@link Representation} abstract class, exercised through a minimal local
+ * subclass.
+ *
+ * @author Jerome Louvel
+ */
+class RepresentationTestCase {
+
+ /** Minimal concrete subclass backed by an in-memory string. */
+ private static class SimpleRepresentation extends Representation {
+
+ private final String content;
+
+ SimpleRepresentation(String content) {
+ super(MediaType.TEXT_PLAIN);
+ this.content = content;
+ setSize(content.getBytes(StandardCharsets.UTF_8).length);
+ }
+
+ @Override
+ public Reader getReader() {
+ return new StringReader(content);
+ }
+
+ @Override
+ public InputStream getStream() {
+ return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public void write(Writer writer) throws IOException {
+ writer.write(content);
+ }
+
+ @Override
+ public void write(OutputStream outputStream) throws IOException {
+ outputStream.write(content.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ @Test
+ void constructor_withMediaTypeOnly_setsSensibleDefaults() {
+ SimpleRepresentation representation = new SimpleRepresentation("");
+ representation.setSize(Representation.UNKNOWN_SIZE);
+
+ assertTrue(representation.isAvailable());
+ assertFalse(representation.isTransient());
+ assertNull(representation.getDisposition());
+ assertNull(representation.getDigest());
+ assertNull(representation.getExpirationDate());
+ assertNull(representation.getRange());
+ }
+
+ @Test
+ void isAvailable_isFalseWhenSizeIsZero() {
+ SimpleRepresentation representation = new SimpleRepresentation("");
+
+ assertEquals(0, representation.getSize());
+ assertTrue(representation.isEmpty());
+ assertFalse(representation.isAvailable());
+ }
+
+ @Test
+ void hasKnownSize_isTrueWhenSizeIsNonNegative() {
+ SimpleRepresentation representation = new SimpleRepresentation("content");
+
+ assertTrue(representation.hasKnownSize());
+
+ representation.setSize(Representation.UNKNOWN_SIZE);
+ assertFalse(representation.hasKnownSize());
+ }
+
+ @Test
+ void exhaust_consumesStreamAndReturnsByteCount() throws IOException {
+ SimpleRepresentation representation = new SimpleRepresentation("0123456789");
+
+ long consumed = representation.exhaust();
+
+ assertEquals(10L, consumed);
+ }
+
+ @Test
+ void getText_usesWriteWhenAvailable() throws IOException {
+ SimpleRepresentation representation = new SimpleRepresentation("some text");
+
+ assertEquals("some text", representation.getText());
+ }
+
+ @Test
+ void getText_returnsEmptyStringWhenEmpty() throws IOException {
+ SimpleRepresentation representation = new SimpleRepresentation("");
+
+ assertEquals("", representation.getText());
+ }
+
+ @Test
+ void append_writesTextToAppendable() throws IOException {
+ SimpleRepresentation representation = new SimpleRepresentation("appended");
+ StringBuilder builder = new StringBuilder();
+
+ representation.append(builder);
+
+ assertEquals("appended", builder.toString());
+ }
+
+ @Test
+ void setDigestThenGetDigest_roundTrips() {
+ SimpleRepresentation representation = new SimpleRepresentation("data");
+ Digest digest = new Digest(Digest.ALGORITHM_MD5, new byte[] {1, 2, 3});
+
+ representation.setDigest(digest);
+
+ assertEquals(digest, representation.getDigest());
+ }
+
+ @Test
+ void setDispositionThenGetDisposition_roundTrips() {
+ SimpleRepresentation representation = new SimpleRepresentation("data");
+ Disposition disposition = new Disposition(Disposition.TYPE_ATTACHMENT);
+
+ representation.setDisposition(disposition);
+
+ assertEquals(disposition, representation.getDisposition());
+ }
+
+ @Test
+ void setExpirationDateThenGetExpirationDate_roundTrips() {
+ SimpleRepresentation representation = new SimpleRepresentation("data");
+ Date date = new Date(0);
+
+ representation.setExpirationDate(date);
+
+ assertEquals(date, representation.getExpirationDate());
+ }
+
+ @Test
+ void setRangeThenGetRange_roundTrips() {
+ SimpleRepresentation representation = new SimpleRepresentation("data");
+ Range range = new Range(0, 10);
+
+ representation.setRange(range);
+
+ assertEquals(range, representation.getRange());
+ }
+
+ @Test
+ void setTransientThenIsTransient_roundTrips() {
+ SimpleRepresentation representation = new SimpleRepresentation("data");
+
+ representation.setTransient(true);
+
+ assertTrue(representation.isTransient());
+ }
+
+ @Test
+ void release_marksRepresentationUnavailable() {
+ SimpleRepresentation representation = new SimpleRepresentation("data");
+
+ representation.release();
+
+ assertFalse(representation.isAvailable());
+ }
+
+ @Test
+ void constructorFromVariant_copiesMetadata() {
+ Variant variant = new Variant(MediaType.APPLICATION_JSON);
+ variant.setCharacterSet(org.restlet.data.CharacterSet.UTF_8);
+
+ // Use a dedicated subclass constructed from the variant to verify copy semantics.
+ Representation fromVariant = new StreamRepresentationFromVariant(variant, new Tag("abc"));
+
+ assertEquals(MediaType.APPLICATION_JSON, fromVariant.getMediaType());
+ assertEquals(org.restlet.data.CharacterSet.UTF_8, fromVariant.getCharacterSet());
+ assertEquals(new Tag("abc"), fromVariant.getTag());
+ }
+
+ /** Helper subclass used solely to exercise the Representation(Variant, Tag) constructor. */
+ private static class StreamRepresentationFromVariant extends Representation {
+
+ StreamRepresentationFromVariant(Variant variant, Tag tag) {
+ super(variant, tag);
+ }
+
+ @Override
+ public Reader getReader() {
+ return new StringReader("");
+ }
+
+ @Override
+ public InputStream getStream() {
+ return new ByteArrayInputStream(new byte[0]);
+ }
+
+ @Override
+ public void write(Writer writer) {
+ // no-op
+ }
+
+ @Override
+ public void write(OutputStream outputStream) {
+ // no-op
+ }
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/StreamRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/representation/StreamRepresentationTestCase.java
new file mode 100644
index 0000000000..88f0bc40e1
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/StreamRepresentationTestCase.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.MediaType;
+
+/**
+ * Unit tests for the {@link StreamRepresentation} abstract class, exercised through a minimal local
+ * subclass.
+ *
+ * @author Jerome Louvel
+ */
+class StreamRepresentationTestCase {
+
+ /** Minimal concrete subclass backed by an in-memory byte array. */
+ private static class SimpleStreamRepresentation extends StreamRepresentation {
+
+ private final String content;
+
+ SimpleStreamRepresentation(MediaType mediaType, String content) {
+ super(mediaType);
+ this.content = content;
+ }
+
+ @Override
+ public InputStream getStream() {
+ return new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public void write(OutputStream outputStream) throws IOException {
+ outputStream.write(content.getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ @Test
+ void constructor_setsMediaType() {
+ SimpleStreamRepresentation representation =
+ new SimpleStreamRepresentation(MediaType.TEXT_PLAIN, "content");
+
+ assertEquals(MediaType.TEXT_PLAIN, representation.getMediaType());
+ }
+
+ @Test
+ void getReader_delegatesToStreamUsingConfiguredCharacterSet() throws IOException {
+ SimpleStreamRepresentation representation =
+ new SimpleStreamRepresentation(MediaType.TEXT_PLAIN, "héllo");
+ representation.setCharacterSet(CharacterSet.UTF_8);
+
+ try (Reader reader = representation.getReader()) {
+ char[] buffer = new char[5];
+ int read = reader.read(buffer);
+ assertEquals("héllo", new String(buffer, 0, read));
+ }
+ }
+
+ @Test
+ void writeToWriter_delegatesToWriteOutputStream() throws IOException {
+ SimpleStreamRepresentation representation =
+ new SimpleStreamRepresentation(MediaType.TEXT_PLAIN, "writer content");
+ representation.setCharacterSet(CharacterSet.UTF_8);
+ StringWriter writer = new StringWriter();
+
+ representation.write(writer);
+
+ assertEquals("writer content", writer.toString());
+ }
+
+ @Test
+ void writeToOutputStream_writesRawContent() throws IOException {
+ SimpleStreamRepresentation representation =
+ new SimpleStreamRepresentation(MediaType.TEXT_PLAIN, "raw");
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ representation.write(out);
+
+ assertEquals("raw", out.toString(StandardCharsets.UTF_8.name()));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/StringRepresentationTestCase.java b/org.restlet/src/test/java/org/restlet/representation/StringRepresentationTestCase.java
new file mode 100644
index 0000000000..5b1413d6f3
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/StringRepresentationTestCase.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.Language;
+import org.restlet.data.MediaType;
+
+/**
+ * Unit tests for the {@link StringRepresentation} class.
+ *
+ * @author Jerome Louvel
+ */
+class StringRepresentationTestCase {
+
+ @Test
+ void constructor_withCharSequence_usesTextPlainAndUtf8() {
+ StringRepresentation representation = new StringRepresentation("hello");
+
+ assertEquals("hello", representation.getText());
+ assertEquals(MediaType.TEXT_PLAIN, representation.getMediaType());
+ assertEquals(CharacterSet.UTF_8, representation.getCharacterSet());
+ }
+
+ @Test
+ void constructor_withCharArray_convertsToString() {
+ StringRepresentation representation = new StringRepresentation("hello".toCharArray());
+
+ assertEquals("hello", representation.getText());
+ }
+
+ @Test
+ void constructor_withLanguage_addsLanguageToList() {
+ StringRepresentation representation = new StringRepresentation("hello", Language.ENGLISH);
+
+ assertTrue(representation.getLanguages().contains(Language.ENGLISH));
+ }
+
+ @Test
+ void constructor_withMediaType_setsMediaType() {
+ StringRepresentation representation =
+ new StringRepresentation("{}", MediaType.APPLICATION_JSON);
+
+ assertEquals(MediaType.APPLICATION_JSON, representation.getMediaType());
+ }
+
+ @Test
+ void constructor_withFullSignature_setsAllMetadata() {
+ StringRepresentation representation =
+ new StringRepresentation(
+ "text", MediaType.TEXT_PLAIN, Language.FRENCH, CharacterSet.ISO_8859_1);
+
+ assertEquals(MediaType.TEXT_PLAIN, representation.getMediaType());
+ assertTrue(representation.getLanguages().contains(Language.FRENCH));
+ assertEquals(CharacterSet.ISO_8859_1, representation.getCharacterSet());
+ }
+
+ @Test
+ void getSize_reflectsUtf8ByteLength() {
+ StringRepresentation representation = new StringRepresentation("café");
+
+ assertEquals(
+ "café".getBytes(CharacterSet.UTF_8.toCharset()).length, representation.getSize());
+ }
+
+ @Test
+ void setText_updatesSize() {
+ StringRepresentation representation = new StringRepresentation("abc");
+
+ representation.setText("abcdef");
+
+ assertEquals(6, representation.getSize());
+ assertEquals("abcdef", representation.getText());
+ }
+
+ @Test
+ void setText_withNull_setsUnknownSize() {
+ StringRepresentation representation = new StringRepresentation("abc");
+
+ representation.setText((String) null);
+
+ assertNull(representation.getText());
+ assertEquals(Representation.UNKNOWN_SIZE, representation.getSize());
+ }
+
+ @Test
+ void getReader_returnsFreshReaderWithFullContent() throws IOException {
+ StringRepresentation representation = new StringRepresentation("reader content");
+
+ try (Reader reader = representation.getReader()) {
+ char[] buffer = new char[32];
+ int read = reader.read(buffer);
+ assertEquals("reader content", new String(buffer, 0, read));
+ }
+ }
+
+ @Test
+ void getStream_encodesTextUsingCharacterSet() throws IOException {
+ StringRepresentation representation = new StringRepresentation("stream content");
+
+ String result =
+ new String(
+ representation.getStream().readAllBytes(), CharacterSet.UTF_8.toCharset());
+
+ assertEquals("stream content", result);
+ }
+
+ @Test
+ void write_writesTextToWriter() throws IOException {
+ StringRepresentation representation = new StringRepresentation("written content");
+ StringWriter writer = new StringWriter();
+
+ representation.write(writer);
+
+ assertEquals("written content", writer.toString());
+ }
+
+ @Test
+ void toString_returnsText() {
+ StringRepresentation representation = new StringRepresentation("as string");
+
+ assertEquals("as string", representation.toString());
+ }
+
+ @Test
+ void release_clearsTextAndMarksUnavailable() {
+ StringRepresentation representation = new StringRepresentation("to release");
+
+ representation.release();
+
+ assertNull(representation.getText());
+ assertFalse(representation.isAvailable());
+ }
+
+ @Test
+ void setCharacterSet_updatesSize() {
+ StringRepresentation representation = new StringRepresentation("café");
+
+ representation.setCharacterSet(CharacterSet.ISO_8859_1);
+
+ assertEquals(
+ "café".getBytes(CharacterSet.ISO_8859_1.toCharset()).length,
+ representation.getSize());
+ }
+
+ @Test
+ void write_withNullText_writesNothing() throws IOException {
+ StringRepresentation representation = new StringRepresentation("abc");
+ representation.setText((String) null);
+ StringWriter writer = new StringWriter();
+
+ representation.write(writer);
+
+ assertEquals("", writer.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/representation/VariantTestCase.java b/org.restlet/src/test/java/org/restlet/representation/VariantTestCase.java
new file mode 100644
index 0000000000..8b10638b10
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/representation/VariantTestCase.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.representation;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.data.CharacterSet;
+import org.restlet.data.ClientInfo;
+import org.restlet.data.Encoding;
+import org.restlet.data.Language;
+import org.restlet.data.MediaType;
+import org.restlet.data.Reference;
+
+/**
+ * Unit tests for the {@link Variant} class.
+ *
+ * @author Jerome Louvel
+ */
+class VariantTestCase {
+
+ @Test
+ void defaultConstructor_hasNoMediaType() {
+ Variant variant = new Variant();
+
+ assertEquals(null, variant.getMediaType());
+ assertTrue(variant.getLanguages().isEmpty());
+ assertTrue(variant.getEncodings().isEmpty());
+ }
+
+ @Test
+ void constructor_withMediaType_setsMediaType() {
+ Variant variant = new Variant(MediaType.TEXT_PLAIN);
+
+ assertEquals(MediaType.TEXT_PLAIN, variant.getMediaType());
+ }
+
+ @Test
+ void constructor_withMediaTypeAndLanguage_addsLanguage() {
+ Variant variant = new Variant(MediaType.TEXT_PLAIN, Language.ENGLISH);
+
+ assertTrue(variant.getLanguages().contains(Language.ENGLISH));
+ }
+
+ @Test
+ void settersAndGetters_roundTrip() {
+ Variant variant = new Variant();
+
+ variant.setCharacterSet(CharacterSet.UTF_8);
+ variant.setMediaType(MediaType.APPLICATION_JSON);
+ variant.setLocationRef(new Reference("http://localhost/resource"));
+
+ assertEquals(CharacterSet.UTF_8, variant.getCharacterSet());
+ assertEquals(MediaType.APPLICATION_JSON, variant.getMediaType());
+ assertEquals(new Reference("http://localhost/resource"), variant.getLocationRef());
+ }
+
+ @Test
+ void setLocationRefFromString_parsesUri() {
+ Variant variant = new Variant();
+
+ variant.setLocationRef("http://localhost/other");
+
+ assertEquals(new Reference("http://localhost/other"), variant.getLocationRef());
+ }
+
+ @Test
+ void getEncodings_rejectsNullElement() {
+ Variant variant = new Variant();
+
+ assertThrows(IllegalArgumentException.class, () -> variant.getEncodings().add(null));
+ }
+
+ @Test
+ void getLanguages_rejectsNullElement() {
+ Variant variant = new Variant();
+
+ assertThrows(IllegalArgumentException.class, () -> variant.getLanguages().add(null));
+ }
+
+ @Test
+ void equals_isTrueForEquivalentVariants() {
+ Variant v1 = new Variant(MediaType.TEXT_PLAIN);
+ v1.setCharacterSet(CharacterSet.UTF_8);
+ Variant v2 = new Variant(MediaType.TEXT_PLAIN);
+ v2.setCharacterSet(CharacterSet.UTF_8);
+
+ assertEquals(v1, v2);
+ assertEquals(v1.hashCode(), v2.hashCode());
+ }
+
+ @Test
+ void equals_isFalseForDifferentMediaType() {
+ Variant v1 = new Variant(MediaType.TEXT_PLAIN);
+ Variant v2 = new Variant(MediaType.APPLICATION_JSON);
+
+ assertNotEquals(v1, v2);
+ }
+
+ @Test
+ void equals_isFalseForNonVariantObject() {
+ Variant variant = new Variant(MediaType.TEXT_PLAIN);
+
+ assertNotEquals(variant, "not a variant");
+ }
+
+ @Test
+ void equals_isTrueForSameInstance() {
+ Variant variant = new Variant(MediaType.TEXT_PLAIN);
+
+ assertEquals(variant, variant);
+ }
+
+ @Test
+ void includes_isTrueWhenMediaTypeIsBroader() {
+ Variant textAll = new Variant(MediaType.TEXT_ALL);
+ Variant textPlain = new Variant(MediaType.TEXT_PLAIN);
+
+ assertTrue(textAll.includes(textPlain));
+ assertFalse(textPlain.includes(textAll));
+ }
+
+ @Test
+ void includes_isFalseForNullOther() {
+ Variant variant = new Variant(MediaType.TEXT_PLAIN);
+
+ assertFalse(variant.includes(null));
+ }
+
+ @Test
+ void isCompatible_isTrueWhenEitherIncludesTheOther() {
+ Variant textAll = new Variant(MediaType.TEXT_ALL);
+ Variant textPlain = new Variant(MediaType.TEXT_PLAIN);
+
+ assertTrue(textAll.isCompatible(textPlain));
+ assertTrue(textPlain.isCompatible(textAll));
+ }
+
+ @Test
+ void isCompatible_isFalseForNullOther() {
+ Variant variant = new Variant(MediaType.TEXT_PLAIN);
+
+ assertFalse(variant.isCompatible(null));
+ }
+
+ @Test
+ void createClientInfo_reflectsVariantPreferences() {
+ Variant variant = new Variant(MediaType.APPLICATION_JSON, Language.ENGLISH);
+ variant.setCharacterSet(CharacterSet.UTF_8);
+ variant.getEncodings().add(Encoding.GZIP);
+
+ ClientInfo clientInfo = variant.createClientInfo();
+
+ assertTrue(
+ clientInfo.getAcceptedMediaTypes().stream()
+ .anyMatch(
+ preference ->
+ MediaType.APPLICATION_JSON.equals(
+ preference.getMetadata())));
+ assertTrue(
+ clientInfo.getAcceptedCharacterSets().stream()
+ .anyMatch(
+ preference -> CharacterSet.UTF_8.equals(preference.getMetadata())));
+ assertTrue(
+ clientInfo.getAcceptedLanguages().stream()
+ .anyMatch(preference -> Language.ENGLISH.equals(preference.getMetadata())));
+ assertTrue(
+ clientInfo.getAcceptedEncodings().stream()
+ .anyMatch(preference -> Encoding.GZIP.equals(preference.getMetadata())));
+ }
+
+ @Test
+ void toString_includesMediaTypeAndCharacterSet() {
+ Variant variant = new Variant(MediaType.TEXT_PLAIN);
+ variant.setCharacterSet(CharacterSet.UTF_8);
+
+ String result = variant.toString();
+
+ assertTrue(result.contains("text/plain"));
+ assertTrue(result.contains("UTF-8"));
+ }
+
+ @Test
+ void toString_isEmptyBracketsWhenNoMetadataSet() {
+ Variant variant = new Variant();
+
+ assertEquals("[]", variant.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/resource/ClientResourceTestCase.java b/org.restlet/src/test/java/org/restlet/resource/ClientResourceTestCase.java
new file mode 100644
index 0000000000..8798a24215
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/resource/ClientResourceTestCase.java
@@ -0,0 +1,472 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.resource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+import org.restlet.engine.Engine;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+/**
+ * Unit tests for the {@link ClientResource} class.
+ *
+ * @author Jerome Louvel
+ */
+class ClientResourceTestCase {
+
+ @BeforeEach
+ void setUpEach() {
+ Engine.clearThreadLocalVariables();
+ Engine.register(false);
+ Engine.getInstance().registerDefaultConverters();
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ Engine.clearThreadLocalVariables();
+ }
+
+ @Test
+ void constructorWithUri_setsReferenceAndDefaultGetMethod() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+
+ assertEquals(new Reference("http://localhost/test"), cr.getReference());
+ assertEquals(Method.GET, cr.getMethod());
+ }
+
+ @Test
+ void constructorWithMethodAndUri_setsMethod() {
+ ClientResource cr = new ClientResource(Method.POST, "http://localhost/test");
+
+ assertEquals(Method.POST, cr.getMethod());
+ }
+
+ @Test
+ void defaultSettings_matchDocumentedDefaults() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+
+ assertEquals(10, cr.getMaxRedirects());
+ assertTrue(cr.isFollowingRedirects());
+ assertTrue(cr.isRetryOnError());
+ assertEquals(2000L, cr.getRetryDelay());
+ assertEquals(2, cr.getRetryAttempts());
+ assertFalse(cr.isRequestEntityBuffering());
+ assertFalse(cr.isResponseEntityBuffering());
+ }
+
+ @Test
+ void copyConstructor_copiesConfiguration() {
+ ClientResource original = new ClientResource("http://localhost/test");
+ original.setMaxRedirects(5);
+ original.setRetryAttempts(1);
+
+ ClientResource copy = new ClientResource(original);
+
+ assertEquals(5, copy.getMaxRedirects());
+ assertEquals(1, copy.getRetryAttempts());
+ }
+
+ @Test
+ void setNextThenGetNext_roundTrips() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ Restlet next =
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setStatus(Status.SUCCESS_OK);
+ }
+ };
+
+ cr.setNext(next);
+
+ assertEquals(next, cr.getNext());
+ assertTrue(cr.hasNext());
+ }
+
+ @Test
+ void get_returnsEntityFromNextRestlet() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ cr.setNext(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setStatus(Status.SUCCESS_OK);
+ response.setEntity(new StringRepresentation("hello"));
+ }
+ });
+
+ Representation result = cr.get();
+
+ assertEquals("hello", getText(result));
+ }
+
+ @Test
+ void put_sendsPutMethodToNextRestlet() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ final AtomicInteger methodSeen = new AtomicInteger();
+ cr.setNext(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (Method.PUT.equals(request.getMethod())) {
+ methodSeen.incrementAndGet();
+ }
+ response.setStatus(Status.SUCCESS_OK);
+ response.setEntity(new StringRepresentation("put-ack"));
+ }
+ });
+
+ Representation result = cr.put(new StringRepresentation("payload"));
+
+ assertEquals(1, methodSeen.get());
+ assertEquals("put-ack", getText(result));
+ }
+
+ @Test
+ void post_sendsPostMethodToNextRestlet() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ cr.setNext(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ assertEquals(Method.POST, request.getMethod());
+ response.setStatus(Status.SUCCESS_OK);
+ response.setEntity(new StringRepresentation("post-ack"));
+ }
+ });
+
+ Representation result = cr.post(new StringRepresentation("payload"));
+
+ assertEquals("post-ack", getText(result));
+ }
+
+ @Test
+ void delete_sendsDeleteMethodToNextRestlet() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ cr.setNext(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ assertEquals(Method.DELETE, request.getMethod());
+ response.setStatus(Status.SUCCESS_OK);
+ }
+ });
+
+ cr.delete();
+
+ assertEquals(Status.SUCCESS_OK, cr.getStatus());
+ }
+
+ @Test
+ void handle_withErrorStatus_throwsResourceException() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ cr.setNext(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ }
+ });
+
+ ResourceException exception = assertThrows(ResourceException.class, cr::get);
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, exception.getStatus());
+ }
+
+ @Test
+ void handle_retriesRecoverableErrorsUntilSuccess() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ cr.setRetryDelay(0);
+ final AtomicInteger callCount = new AtomicInteger();
+ cr.setNext(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (callCount.incrementAndGet() <= 2) {
+ response.setStatus(Status.SERVER_ERROR_SERVICE_UNAVAILABLE);
+ } else {
+ response.setStatus(Status.SUCCESS_OK);
+ response.setEntity(new StringRepresentation("retried-result"));
+ }
+ }
+ });
+
+ Representation result = cr.get();
+
+ assertEquals(3, callCount.get());
+ assertEquals("retried-result", getText(result));
+ }
+
+ @Test
+ void handle_followsSeeOtherRedirectForSafeMethod() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ final AtomicInteger callCount = new AtomicInteger();
+ cr.setNext(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ if (callCount.incrementAndGet() == 1) {
+ response.setStatus(Status.REDIRECTION_SEE_OTHER);
+ response.setLocationRef(new Reference("http://localhost/other"));
+ } else {
+ response.setStatus(Status.SUCCESS_OK);
+ response.setEntity(new StringRepresentation("redirected-result"));
+ }
+ }
+ });
+
+ Representation result = cr.get();
+
+ assertEquals(2, callCount.get());
+ assertEquals("redirected-result", getText(result));
+ }
+
+ @Test
+ void getChild_withRelativeReference_returnsChildResource() {
+ ClientResource cr = new ClientResource("http://localhost/parent/");
+
+ ClientResource child = cr.getChild("child");
+
+ assertEquals(new Reference("http://localhost/parent/child"), child.getReference());
+ }
+
+ @Test
+ void getChild_withAbsoluteReference_throwsResourceException() {
+ ClientResource cr = new ClientResource("http://localhost/parent/");
+
+ ResourceException exception =
+ assertThrows(
+ ResourceException.class,
+ () -> cr.getChild(new Reference("http://otherhost/child")));
+
+ assertEquals(Status.CLIENT_ERROR_BAD_REQUEST, exception.getStatus());
+ }
+
+ @Test
+ void getParent_withHierarchicalReference_returnsParentResource() {
+ ClientResource cr = new ClientResource("http://localhost/parent/child");
+
+ ClientResource parent = cr.getParent();
+
+ assertEquals(new Reference("http://localhost/parent/"), parent.getReference());
+ }
+
+ @Test
+ void getParent_withNonHierarchicalReference_throwsResourceException() {
+ ClientResource cr = new ClientResource("mailto:someone@example.com");
+
+ ResourceException exception = assertThrows(ResourceException.class, cr::getParent);
+
+ assertEquals(Status.CLIENT_ERROR_BAD_REQUEST, exception.getStatus());
+ }
+
+ @Test
+ void wrap_proxiesAnnotatedInterfaceThroughFinder() {
+ boolean previous =
+ org.restlet.representation.ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED;
+ org.restlet.representation.ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED = true;
+ try {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ Finder finder = new Finder(new Context());
+ finder.setTargetClass(MyServerResource01.class);
+ cr.setNext(finder);
+
+ MyResource01 proxy = cr.wrap(MyResource01.class);
+
+ assertNotNull(proxy.represent());
+ assertEquals("myName", proxy.represent().getName());
+ } finally {
+ org.restlet.representation.ObjectRepresentation.VARIANT_OBJECT_BINARY_SUPPORTED =
+ previous;
+ }
+ }
+
+ @Test
+ void setEntityBuffering_setsBothRequestAndResponseBuffering() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+
+ cr.setEntityBuffering(true);
+
+ assertTrue(cr.isRequestEntityBuffering());
+ assertTrue(cr.isResponseEntityBuffering());
+ }
+
+ private static ClientResource echoResource() {
+ ClientResource cr = new ClientResource("http://localhost/test");
+ cr.setNext(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setStatus(Status.SUCCESS_OK);
+ response.setEntity(
+ new StringRepresentation("echo-" + request.getMethod().getName()));
+ }
+ });
+ return cr;
+ }
+
+ @Test
+ void get_withResultClass_convertsEntity() {
+ ClientResource cr = echoResource();
+ String result = cr.get(String.class);
+ assertEquals("echo-GET", result);
+ }
+
+ @Test
+ void get_withMediaType_returnsRepresentation() {
+ ClientResource cr = echoResource();
+ Representation result = cr.get(org.restlet.data.MediaType.TEXT_PLAIN);
+ assertEquals("echo-GET", getText(result));
+ }
+
+ @Test
+ void head_returnsRepresentation() {
+ ClientResource cr = echoResource();
+ Representation result = cr.head();
+ assertEquals("echo-HEAD", getText(result));
+ assertNotNull(result);
+ }
+
+ @Test
+ void head_withMediaType_setsAcceptedMediaType() {
+ ClientResource cr = echoResource();
+ Representation result = cr.head(org.restlet.data.MediaType.TEXT_PLAIN);
+ assertEquals("echo-HEAD", getText(result));
+ }
+
+ @Test
+ void options_returnsRepresentation() {
+ ClientResource cr = echoResource();
+ Representation result = cr.options();
+ assertEquals("echo-OPTIONS", getText(result));
+ }
+
+ @Test
+ void options_withResultClass_convertsEntity() {
+ ClientResource cr = echoResource();
+ String result = cr.options(String.class);
+ assertEquals("echo-OPTIONS", result);
+ }
+
+ @Test
+ void options_withMediaType_returnsRepresentation() {
+ ClientResource cr = echoResource();
+ Representation result = cr.options(org.restlet.data.MediaType.TEXT_PLAIN);
+ assertEquals("echo-OPTIONS", getText(result));
+ }
+
+ @Test
+ void patch_withObjectEntity_sendsPatchMethod() {
+ ClientResource cr = echoResource();
+ Representation result = cr.patch("payload");
+ assertEquals("echo-PATCH", getText(result));
+ }
+
+ @Test
+ void patch_withObjectEntityAndResultClass_convertsEntity() {
+ ClientResource cr = echoResource();
+ String result = cr.patch("payload", String.class);
+ assertEquals("echo-PATCH", result);
+ }
+
+ @Test
+ void patch_withObjectEntityAndMediaType_returnsRepresentation() {
+ ClientResource cr = echoResource();
+ Representation result = cr.patch("payload", org.restlet.data.MediaType.TEXT_PLAIN);
+ assertEquals("echo-PATCH", getText(result));
+ }
+
+ @Test
+ void patch_withRepresentationEntity_sendsPatchMethod() {
+ ClientResource cr = echoResource();
+ Representation result = cr.patch(new StringRepresentation("payload"));
+ assertEquals("echo-PATCH", getText(result));
+ }
+
+ @Test
+ void post_withObjectEntityAndResultClass_convertsEntity() {
+ ClientResource cr = echoResource();
+ String result = cr.post("payload", String.class);
+ assertEquals("echo-POST", result);
+ }
+
+ @Test
+ void post_withObjectEntityAndMediaType_returnsRepresentation() {
+ ClientResource cr = echoResource();
+ Representation result = cr.post("payload", org.restlet.data.MediaType.TEXT_PLAIN);
+ assertEquals("echo-POST", getText(result));
+ }
+
+ @Test
+ void post_withObjectEntity_sendsPostMethod() {
+ ClientResource cr = echoResource();
+ Representation result = cr.post("payload");
+ assertEquals("echo-POST", getText(result));
+ }
+
+ @Test
+ void put_withObjectEntityAndResultClass_convertsEntity() {
+ ClientResource cr = echoResource();
+ String result = cr.put("payload", String.class);
+ assertEquals("echo-PUT", result);
+ }
+
+ @Test
+ void put_withObjectEntityAndMediaType_returnsRepresentation() {
+ ClientResource cr = echoResource();
+ Representation result = cr.put("payload", org.restlet.data.MediaType.TEXT_PLAIN);
+ assertEquals("echo-PUT", getText(result));
+ }
+
+ @Test
+ void put_withObjectEntity_sendsPutMethod() {
+ ClientResource cr = echoResource();
+ Representation result = cr.put("payload");
+ assertEquals("echo-PUT", getText(result));
+ }
+
+ @Test
+ void delete_withResultClass_convertsEntity() {
+ ClientResource cr = echoResource();
+ String result = cr.delete(String.class);
+ assertEquals("echo-DELETE", result);
+ }
+
+ @Test
+ void delete_withMediaType_returnsRepresentation() {
+ ClientResource cr = echoResource();
+ Representation result = cr.delete(org.restlet.data.MediaType.TEXT_PLAIN);
+ assertEquals("echo-DELETE", getText(result));
+ }
+
+ private static String getText(Representation representation) {
+ try {
+ return representation == null ? null : representation.getText();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/resource/FinderTestCase.java b/org.restlet/src/test/java/org/restlet/resource/FinderTestCase.java
new file mode 100644
index 0000000000..100c38c5ba
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/resource/FinderTestCase.java
@@ -0,0 +1,170 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.resource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.engine.Engine;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+/**
+ * Unit tests for the {@link Finder} class.
+ *
+ * @author Jerome Louvel
+ */
+class FinderTestCase {
+
+ /** Simple server resource used as a target for the finder. */
+ public static class SampleServerResource extends ServerResource {
+
+ @Override
+ protected Representation get() {
+ return new StringRepresentation("sample-content");
+ }
+ }
+
+ @BeforeEach
+ void setUpEach() {
+ Engine.clearThreadLocalVariables();
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ Engine.clearThreadLocalVariables();
+ }
+
+ @Test
+ void defaultConstructor_hasNoTargetClass() {
+ Finder finder = new Finder();
+
+ assertNull(finder.getTargetClass());
+ }
+
+ @Test
+ void constructorWithTargetClass_setsTargetClass() {
+ Finder finder = new Finder(new Context(), SampleServerResource.class);
+
+ assertEquals(SampleServerResource.class, finder.getTargetClass());
+ }
+
+ @Test
+ void setTargetClassThenGetTargetClass_roundTrips() {
+ Finder finder = new Finder();
+
+ finder.setTargetClass(SampleServerResource.class);
+
+ assertEquals(SampleServerResource.class, finder.getTargetClass());
+ }
+
+ @Test
+ void create_withoutTargetClass_returnsNull() {
+ Finder finder = new Finder();
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ assertNull(finder.create(request, response));
+ }
+
+ @Test
+ void create_withTargetClass_instantiatesDefaultConstructor() {
+ Finder finder = new Finder(null, SampleServerResource.class);
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ ServerResource result = finder.create(request, response);
+
+ assertNotNull(result);
+ assertTrue(result instanceof SampleServerResource);
+ }
+
+ @Test
+ void find_delegatesToCreate() {
+ Finder finder = new Finder(null, SampleServerResource.class);
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ assertNotNull(finder.find(request, response));
+ }
+
+ @Test
+ void handle_withoutTargetClass_setsNotFoundStatus() {
+ Finder finder = new Finder();
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ finder.handle(request, response);
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, response.getStatus());
+ }
+
+ @Test
+ void handle_withTargetClass_dispatchesToServerResource() {
+ Finder finder = new Finder(new Context(), SampleServerResource.class);
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ finder.handle(request, response);
+
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ void toString_withoutTargetClass_indicatesNoTarget() {
+ Finder finder = new Finder();
+
+ assertEquals("Finder with no target class", finder.toString());
+ }
+
+ @Test
+ void toString_withTargetClass_includesSimpleName() {
+ Finder finder = new Finder(null, SampleServerResource.class);
+
+ assertTrue(finder.toString().contains("SampleServerResource"));
+ }
+
+ @Test
+ void createFinder_withoutFinderClass_createsPlainFinder() {
+ Finder finder = Finder.createFinder(SampleServerResource.class, null, new Context(), null);
+
+ assertNotNull(finder);
+ assertEquals(Finder.class, finder.getClass());
+ assertEquals(SampleServerResource.class, finder.getTargetClass());
+ }
+
+ /** Custom finder subclass exposing the expected (Context, Class) constructor. */
+ public static class CustomFinder extends Finder {
+ public CustomFinder(Context context, Class extends ServerResource> targetClass) {
+ super(context, targetClass);
+ }
+ }
+
+ @Test
+ void createFinder_withFinderClass_instantiatesCustomFinder() {
+ Finder finder =
+ Finder.createFinder(
+ SampleServerResource.class, CustomFinder.class, new Context(), null);
+
+ assertNotNull(finder);
+ assertTrue(finder instanceof CustomFinder);
+ assertEquals(SampleServerResource.class, finder.getTargetClass());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/resource/ResourceExceptionTestCase.java b/org.restlet/src/test/java/org/restlet/resource/ResourceExceptionTestCase.java
new file mode 100644
index 0000000000..6db5ac072b
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/resource/ResourceExceptionTestCase.java
@@ -0,0 +1,120 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.resource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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 org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+
+/**
+ * Unit tests for the {@link ResourceException} class.
+ *
+ * @author Jerome Louvel
+ */
+class ResourceExceptionTestCase {
+
+ @Test
+ void constructor_withCodeOnly_setsMatchingStatus() {
+ ResourceException re = new ResourceException(404);
+
+ assertEquals(404, re.getStatus().getCode());
+ assertNull(re.getRequest());
+ assertNull(re.getResponse());
+ }
+
+ @Test
+ void constructor_withCodeAndReasonPhrase_setsReasonPhrase() {
+ ResourceException re = new ResourceException(500, "Custom Reason");
+
+ assertEquals(500, re.getStatus().getCode());
+ assertEquals("Custom Reason", re.getStatus().getReasonPhrase());
+ }
+
+ @Test
+ void constructor_withCodeReasonPhraseAndDescription_setsDescription() {
+ ResourceException re = new ResourceException(400, "Bad Request", "Missing field");
+
+ assertEquals(400, re.getStatus().getCode());
+ assertEquals("Bad Request", re.getStatus().getReasonPhrase());
+ assertEquals("Missing field", re.getStatus().getDescription());
+ }
+
+ @Test
+ void constructor_withStatus_copiesStatus() {
+ ResourceException re = new ResourceException(Status.CLIENT_ERROR_FORBIDDEN);
+
+ assertEquals(Status.CLIENT_ERROR_FORBIDDEN, re.getStatus());
+ }
+
+ @Test
+ void constructor_withStatusAndCause_setsCauseAndStatus() {
+ Exception cause = new IllegalStateException("boom");
+ ResourceException re = new ResourceException(Status.SERVER_ERROR_INTERNAL, cause);
+
+ assertEquals(Status.SERVER_ERROR_INTERNAL, re.getStatus());
+ assertSame(cause, re.getCause());
+ }
+
+ @Test
+ void constructor_withThrowableOnly_defaultsToInternalServerError() {
+ Exception cause = new RuntimeException("failure");
+ ResourceException re = new ResourceException(cause);
+
+ assertEquals(Status.SERVER_ERROR_INTERNAL.getCode(), re.getStatus().getCode());
+ assertSame(cause, re.getCause());
+ }
+
+ @Test
+ void constructor_withStatusRequestAndResponse_exposesRequestAndResponse() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ ResourceException re =
+ new ResourceException(Status.CLIENT_ERROR_NOT_FOUND, request, response);
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, re.getStatus());
+ assertSame(request, re.getRequest());
+ assertSame(response, re.getResponse());
+ }
+
+ @Test
+ void constructor_withStatusAndDescription_appliesDescriptionToCopiedStatus() {
+ ResourceException re =
+ new ResourceException(Status.CLIENT_ERROR_BAD_REQUEST, "Invalid input");
+
+ assertEquals(Status.CLIENT_ERROR_BAD_REQUEST.getCode(), re.getStatus().getCode());
+ assertEquals("Invalid input", re.getStatus().getDescription());
+ }
+
+ @Test
+ void getMessage_returnsStatusToString() {
+ ResourceException re = new ResourceException(Status.CLIENT_ERROR_NOT_FOUND);
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND.toString(), re.getMessage());
+ }
+
+ @Test
+ void constructor_withCodeNameDescriptionAndUri_setsAllStatusFields() {
+ ResourceException re =
+ new ResourceException(
+ 422, "Unprocessable", "Bad payload", "http://example.com/spec");
+
+ assertEquals(422, re.getStatus().getCode());
+ assertEquals("Unprocessable", re.getStatus().getReasonPhrase());
+ assertEquals("Bad payload", re.getStatus().getDescription());
+ assertNotNull(re.getStatus().getUri());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/resource/ResourceTestCase.java b/org.restlet/src/test/java/org/restlet/resource/ResourceTestCase.java
new file mode 100644
index 0000000000..99da92f37b
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/resource/ResourceTestCase.java
@@ -0,0 +1,241 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.resource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+import org.restlet.engine.Engine;
+import org.restlet.representation.Representation;
+
+/**
+ * Unit tests for the {@link Resource} abstract class, exercised through a minimal local subclass.
+ *
+ * @author Jerome Louvel
+ */
+class ResourceTestCase {
+
+ /** Minimal concrete subclass used to exercise the abstract base class. */
+ private static class ConcreteResource extends Resource {
+
+ private final Map attributes = new HashMap<>();
+
+ private volatile boolean released;
+
+ private volatile boolean failInit;
+
+ @Override
+ public String getAttribute(String name) {
+ Object value = attributes.get(name);
+ return (value == null) ? null : value.toString();
+ }
+
+ @Override
+ public void setAttribute(String name, Object value) {
+ attributes.put(name, value);
+ }
+
+ @Override
+ public Representation handle() {
+ return null;
+ }
+
+ @Override
+ protected void doInit() throws ResourceException {
+ if (failInit) {
+ throw new ResourceException(Status.SERVER_ERROR_INTERNAL);
+ }
+ }
+
+ @Override
+ protected void doRelease() {
+ released = true;
+ }
+ }
+
+ private ConcreteResource resource;
+
+ @BeforeEach
+ void setUpEach() {
+ Engine.clearThreadLocalVariables();
+ resource = new ConcreteResource();
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ Engine.clearThreadLocalVariables();
+ resource = null;
+ }
+
+ @Test
+ void toBoolean_convertsValidStringAndReturnsNullForNull() {
+ assertEquals(Boolean.TRUE, Resource.toBoolean("true"));
+ assertNull(Resource.toBoolean(null));
+ }
+
+ @Test
+ void toByte_convertsValidStringAndReturnsNullForNull() {
+ assertEquals(Byte.valueOf((byte) 5), Resource.toByte("5"));
+ assertNull(Resource.toByte(null));
+ }
+
+ @Test
+ void toDouble_convertsValidStringAndReturnsNullForNull() {
+ assertEquals(Double.valueOf(3.14), Resource.toDouble("3.14"));
+ assertNull(Resource.toDouble(null));
+ }
+
+ @Test
+ void toFloat_convertsValidStringAndReturnsNullForNull() {
+ assertEquals(Float.valueOf(1.5f), Resource.toFloat("1.5"));
+ assertNull(Resource.toFloat(null));
+ }
+
+ @Test
+ void toInteger_convertsValidStringAndReturnsNullForNull() {
+ assertEquals(Integer.valueOf(42), Resource.toInteger("42"));
+ assertNull(Resource.toInteger(null));
+ }
+
+ @Test
+ void toLong_convertsValidStringAndReturnsNullForNull() {
+ assertEquals(Long.valueOf(42L), Resource.toLong("42"));
+ assertNull(Resource.toLong(null));
+ }
+
+ @Test
+ void toShort_convertsValidStringAndReturnsNullForNull() {
+ assertEquals(Short.valueOf((short) 7), Resource.toShort("7"));
+ assertNull(Resource.toShort(null));
+ }
+
+ @Test
+ void getRequestAndGetResponse_returnNullBeforeInit() {
+ assertNull(resource.getRequest());
+ assertNull(resource.getResponse());
+ assertNull(resource.getContext());
+ }
+
+ @Test
+ void init_setsContextRequestAndResponse() {
+ Context context = new Context();
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+
+ resource.init(context, request, response);
+
+ assertEquals(context, resource.getContext());
+ assertEquals(request, resource.getRequest());
+ assertEquals(response, resource.getResponse());
+ }
+
+ @Test
+ void init_catchesExceptionThrownByDoInit() {
+ Context context = new Context();
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ resource.failInit = true;
+
+ // Should not propagate the ResourceException thrown by doInit().
+ resource.init(context, request, response);
+
+ assertEquals(request, resource.getRequest());
+ }
+
+ @Test
+ void getMethod_delegatesToRequest() {
+ Request request = new Request(Method.PUT, "http://localhost/test");
+ resource.setRequest(request);
+
+ assertEquals(Method.PUT, resource.getMethod());
+ }
+
+ @Test
+ void getStatus_delegatesToResponse() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ response.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
+ resource.setResponse(response);
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, resource.getStatus());
+ }
+
+ @Test
+ void getReference_delegatesToRequest() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ resource.setRequest(request);
+
+ assertEquals(new Reference("http://localhost/test"), resource.getReference());
+ }
+
+ @Test
+ void getApplication_createsNewInstanceWhenNoneSet() {
+ assertNotNull(resource.getApplication());
+ }
+
+ @Test
+ void setApplication_returnsConfiguredInstance() {
+ org.restlet.Application application = new org.restlet.Application();
+ resource.setApplication(application);
+
+ assertEquals(application, resource.getApplication());
+ }
+
+ @Test
+ void setQueryValueThenGetQueryValue_roundTrips() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ resource.setRequest(request);
+
+ resource.setQueryValue("name", "value");
+
+ assertEquals("value", resource.getQueryValue("name"));
+ }
+
+ @Test
+ void getQueryValue_returnsNullWhenNoRequest() {
+ assertNull(resource.getQueryValue("name"));
+ }
+
+ @Test
+ void release_invokesDoRelease() {
+ assertFalse(resource.released);
+ resource.release();
+ assertTrue(resource.released);
+ }
+
+ @Test
+ void toString_combinesRequestAndResponse() {
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ resource.init(new Context(), request, response);
+
+ String result = resource.toString();
+
+ assertTrue(result.contains(request.toString()));
+ }
+
+ @Test
+ void toString_isEmptyWhenNoRequestOrResponse() {
+ assertEquals("", resource.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/resource/ServerResourceTestCase.java b/org.restlet/src/test/java/org/restlet/resource/ServerResourceTestCase.java
new file mode 100644
index 0000000000..f425267f75
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/resource/ServerResourceTestCase.java
@@ -0,0 +1,470 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.resource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.engine.Engine;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+/**
+ * Unit tests for the {@link ServerResource} abstract class, exercised through minimal local
+ * subclasses.
+ *
+ * @author Jerome Louvel
+ */
+class ServerResourceTestCase {
+
+ /** Server resource with no overridden methods: exercises default behaviors. */
+ private static class PlainServerResource extends ServerResource {}
+
+ /** Server resource overriding the standard uniform interface methods. */
+ private static class EchoServerResource extends ServerResource {
+
+ volatile boolean putInvoked;
+
+ volatile boolean deleteInvoked;
+
+ @Override
+ protected Representation get() {
+ return new StringRepresentation("get-result");
+ }
+
+ @Override
+ protected Representation put(Representation entity) {
+ putInvoked = true;
+ return new StringRepresentation("put-result");
+ }
+
+ @Override
+ protected Representation delete() {
+ deleteInvoked = true;
+ return new StringRepresentation("delete-result");
+ }
+ }
+
+ @BeforeEach
+ void setUpEach() {
+ Engine.clearThreadLocalVariables();
+ Engine.register(false);
+ }
+
+ @AfterEach
+ void tearDownEach() {
+ Engine.clearThreadLocalVariables();
+ }
+
+ private static EchoServerResource createInitializedResource(Method method) {
+ EchoServerResource resource = new EchoServerResource();
+ Context context = new Context();
+ Request request = new Request(method, "http://localhost/test");
+ Response response = new Response(request);
+ resource.init(context, request, response);
+ return resource;
+ }
+
+ @Test
+ void defaultFlags_areTrueByDefault() {
+ PlainServerResource resource = new PlainServerResource();
+
+ assertTrue(resource.isAnnotated());
+ assertTrue(resource.isConditional());
+ assertTrue(resource.isExisting());
+ assertTrue(resource.isNegotiated());
+ }
+
+ @Test
+ void nameAndDescription_roundTrip() {
+ PlainServerResource resource = new PlainServerResource();
+
+ resource.setName("myResource");
+ resource.setDescription("myDescription");
+
+ assertEquals("myResource", resource.getName());
+ assertEquals("myDescription", resource.getDescription());
+ }
+
+ @Test
+ void setAnnotated_updatesFlag() {
+ PlainServerResource resource = new PlainServerResource();
+
+ resource.setAnnotated(false);
+
+ assertFalse(resource.isAnnotated());
+ }
+
+ @Test
+ void setConditional_updatesFlag() {
+ PlainServerResource resource = new PlainServerResource();
+
+ resource.setConditional(false);
+
+ assertFalse(resource.isConditional());
+ }
+
+ @Test
+ void setExisting_updatesFlag() {
+ PlainServerResource resource = new PlainServerResource();
+
+ resource.setExisting(false);
+
+ assertFalse(resource.isExisting());
+ }
+
+ @Test
+ void setNegotiated_updatesFlag() {
+ PlainServerResource resource = new PlainServerResource();
+
+ resource.setNegotiated(false);
+
+ assertFalse(resource.isNegotiated());
+ }
+
+ @Test
+ void getAttribute_readsFromRequestAttributes() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ resource.getRequestAttributes().put("key", "value");
+
+ assertEquals("value", resource.getAttribute("key"));
+ }
+
+ @Test
+ void getAttribute_returnsNullWhenMissing() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ assertNull(resource.getAttribute("missing"));
+ }
+
+ @Test
+ void setAttribute_writesToResponseAttributes() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.setAttribute("key", "value");
+
+ assertEquals("value", resource.getResponseAttributes().get("key"));
+ }
+
+ @Test
+ void handle_withGetMethod_dispatchesToGetAndSetsEntity() throws Exception {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.handle();
+
+ assertEquals(Status.SUCCESS_OK, resource.getStatus());
+ assertEquals("get-result", resource.getResponseEntity().getText());
+ }
+
+ @Test
+ void handle_withPutMethod_dispatchesToPutRegardlessOfExisting() throws Exception {
+ EchoServerResource resource = createInitializedResource(Method.PUT);
+ resource.setExisting(false);
+
+ resource.handle();
+
+ assertTrue(resource.putInvoked);
+ assertEquals("put-result", resource.getResponseEntity().getText());
+ }
+
+ @Test
+ void handle_withDeleteMethod_dispatchesToDelete() throws Exception {
+ EchoServerResource resource = createInitializedResource(Method.DELETE);
+
+ resource.handle();
+
+ assertTrue(resource.deleteInvoked);
+ assertEquals("delete-result", resource.getResponseEntity().getText());
+ }
+
+ @Test
+ void handle_withUnsupportedMethodOnPlainResource_setsMethodNotAllowed() {
+ PlainServerResource resource = new PlainServerResource();
+ Context context = new Context();
+ Request request = new Request(Method.GET, "http://localhost/test");
+ Response response = new Response(request);
+ resource.init(context, request, response);
+
+ resource.handle();
+
+ assertEquals(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED, resource.getStatus());
+ }
+
+ @Test
+ void handle_whenNotExistingAndMethodSafe_setsNotFound() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ resource.setExisting(false);
+
+ resource.handle();
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, resource.getStatus());
+ }
+
+ @Test
+ void doError_setsResponseStatus() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.doError(Status.CLIENT_ERROR_CONFLICT);
+
+ assertEquals(Status.CLIENT_ERROR_CONFLICT, resource.getStatus());
+ }
+
+ @Test
+ void isInRole_returnsFalseWhenClientHasNoRoles() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ assertFalse(resource.isInRole("admin"));
+ }
+
+ @Test
+ void getRole_createsRoleWithGivenName() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ assertEquals("admin", resource.getRole("admin").getName());
+ }
+
+ @Test
+ void redirectPermanent_withStringUri_setsLocationAndStatus() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.redirectPermanent("http://localhost/target");
+
+ assertEquals(Status.REDIRECTION_PERMANENT, resource.getStatus());
+ assertEquals("http://localhost/target", resource.getLocationRef().toString());
+ }
+
+ @Test
+ void redirectPermanent_withReference_setsLocationAndStatus() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.redirectPermanent(new org.restlet.data.Reference("http://localhost/target"));
+
+ assertEquals(Status.REDIRECTION_PERMANENT, resource.getStatus());
+ }
+
+ @Test
+ void redirectSeeOther_withStringUri_setsLocationAndStatus() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.redirectSeeOther("http://localhost/target");
+
+ assertEquals(Status.REDIRECTION_SEE_OTHER, resource.getStatus());
+ }
+
+ @Test
+ void redirectSeeOther_withReference_setsLocationAndStatus() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.redirectSeeOther(new org.restlet.data.Reference("http://localhost/target"));
+
+ assertEquals(Status.REDIRECTION_SEE_OTHER, resource.getStatus());
+ }
+
+ @Test
+ void redirectTemporary_withStringUri_setsLocationAndStatus() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.redirectTemporary("http://localhost/target");
+
+ assertEquals(Status.REDIRECTION_TEMPORARY, resource.getStatus());
+ }
+
+ @Test
+ void redirectTemporary_withReference_setsLocationAndStatus() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.redirectTemporary(new org.restlet.data.Reference("http://localhost/target"));
+
+ assertEquals(Status.REDIRECTION_TEMPORARY, resource.getStatus());
+ }
+
+ @Test
+ void setLocationRef_withString_setsLocation() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.setLocationRef("http://localhost/other");
+
+ assertEquals("http://localhost/other", resource.getLocationRef().toString());
+ }
+
+ @Test
+ void setStatus_statusOnly_updatesResponseStatus() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.setStatus(Status.CLIENT_ERROR_FORBIDDEN);
+
+ assertEquals(Status.CLIENT_ERROR_FORBIDDEN, resource.getStatus());
+ }
+
+ @Test
+ void setStatus_statusAndMessage_setsReasonPhrase() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.setStatus(Status.CLIENT_ERROR_FORBIDDEN, "custom message");
+
+ assertEquals("custom message", resource.getStatus().getDescription());
+ }
+
+ @Test
+ void setStatus_statusAndThrowable_attachesThrowable() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ Exception cause = new IllegalStateException("boom");
+
+ resource.setStatus(Status.SERVER_ERROR_INTERNAL, cause);
+
+ assertEquals(cause, resource.getStatus().getThrowable());
+ }
+
+ @Test
+ void setStatus_statusThrowableAndMessage_setsBoth() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ Exception cause = new IllegalStateException("boom");
+
+ resource.setStatus(Status.SERVER_ERROR_INTERNAL, cause, "custom message");
+
+ assertEquals(cause, resource.getStatus().getThrowable());
+ assertEquals("custom message", resource.getStatus().getReasonPhrase());
+ }
+
+ @Test
+ void commit_delegatesToResponseWithoutThrowing() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ assertFalse(resource.isCommitted());
+
+ resource.commit();
+
+ assertFalse(resource.isCommitted());
+ }
+
+ @Test
+ void setCommitted_updatesFlag() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.setCommitted(true);
+
+ assertTrue(resource.isCommitted());
+ }
+
+ @Test
+ void isAutoCommitting_defaultsToTrue() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ assertTrue(resource.isAutoCommitting());
+ }
+
+ @Test
+ void setAutoCommitting_updatesFlag() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.setAutoCommitting(false);
+
+ assertFalse(resource.isAutoCommitting());
+ }
+
+ @Test
+ void abort_delegatesToResponseWithoutThrowing() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.abort();
+
+ assertNotNull(resource.getResponse());
+ }
+
+ @Test
+ void setAllowedMethods_updatesAllowedMethodsSet() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.setAllowedMethods(java.util.Set.of(Method.GET, Method.POST));
+
+ assertTrue(resource.getResponse().getAllowedMethods().contains(Method.GET));
+ assertTrue(resource.getResponse().getAllowedMethods().contains(Method.POST));
+ }
+
+ @Test
+ void setServerInfo_updatesResponseServerInfo() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ org.restlet.data.ServerInfo serverInfo = new org.restlet.data.ServerInfo();
+ serverInfo.setAgent("test-agent");
+
+ resource.setServerInfo(serverInfo);
+
+ assertEquals("test-agent", resource.getResponse().getServerInfo().getAgent());
+ }
+
+ @Test
+ void setCookieSettings_updatesResponseCookieSettings() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ org.restlet.util.Series cookieSettings =
+ new org.restlet.util.Series<>(org.restlet.data.CookieSetting.class);
+ cookieSettings.add(new org.restlet.data.CookieSetting(0, "name", "value"));
+
+ resource.setCookieSettings(cookieSettings);
+
+ assertEquals(1, resource.getResponse().getCookieSettings().size());
+ }
+
+ @Test
+ void setDimensions_updatesResponseDimensions() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+
+ resource.setDimensions(java.util.Set.of(org.restlet.data.Dimension.CHARACTER_SET));
+
+ assertTrue(
+ resource.getResponse()
+ .getDimensions()
+ .contains(org.restlet.data.Dimension.CHARACTER_SET));
+ }
+
+ @Test
+ void setChallengeRequests_updatesResponseChallengeRequests() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ java.util.List requests =
+ java.util.List.of(
+ new org.restlet.data.ChallengeRequest(
+ org.restlet.data.ChallengeScheme.HTTP_BASIC));
+
+ resource.setChallengeRequests(requests);
+
+ assertEquals(1, resource.getResponse().getChallengeRequests().size());
+ }
+
+ @Test
+ void setProxyChallengeRequests_updatesResponseProxyChallengeRequests() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ java.util.List requests =
+ java.util.List.of(
+ new org.restlet.data.ChallengeRequest(
+ org.restlet.data.ChallengeScheme.HTTP_BASIC));
+
+ resource.setProxyChallengeRequests(requests);
+
+ assertEquals(1, resource.getResponse().getProxyChallengeRequests().size());
+ }
+
+ @Test
+ void getOnSentAndSetOnSent_roundTrip() {
+ EchoServerResource resource = createInitializedResource(Method.GET);
+ org.restlet.Uniform callback = (request, response) -> {};
+
+ resource.setOnSent(callback);
+
+ assertEquals(callback, resource.getOnSent());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/routing/RedirectorTestCase.java b/org.restlet/src/test/java/org/restlet/routing/RedirectorTestCase.java
new file mode 100644
index 0000000000..0e774d2877
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/routing/RedirectorTestCase.java
@@ -0,0 +1,279 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.routing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Header;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+import org.restlet.engine.header.HeaderConstants;
+import org.restlet.representation.StringRepresentation;
+
+/** Unit tests for {@link Redirector}. */
+class RedirectorTestCase {
+
+ @Test
+ void twoArgConstructor_defaultsToServerOutboundMode() {
+ Redirector redirector = new Redirector(new Context(), "/target");
+
+ assertEquals(Redirector.MODE_SERVER_OUTBOUND, redirector.getMode());
+ assertEquals("/target", redirector.getTargetTemplate());
+ assertTrue(redirector.isHeadersCleaning());
+ }
+
+ @Test
+ void threeArgConstructor_setsGivenMode() {
+ Redirector redirector =
+ new Redirector(new Context(), "/target", Redirector.MODE_CLIENT_FOUND);
+
+ assertEquals(Redirector.MODE_CLIENT_FOUND, redirector.getMode());
+ }
+
+ @Test
+ void settersRoundTrip() {
+ Redirector redirector = new Redirector(new Context(), "/target");
+
+ redirector.setMode(Redirector.MODE_CLIENT_TEMPORARY);
+ assertEquals(Redirector.MODE_CLIENT_TEMPORARY, redirector.getMode());
+
+ redirector.setTargetTemplate("/other");
+ assertEquals("/other", redirector.getTargetTemplate());
+
+ redirector.setHeadersCleaning(false);
+ assertFalse(redirector.isHeadersCleaning());
+ }
+
+ @Test
+ void getTargetRef_withRelativeTemplate_isResolvedAgainstResourceRef() {
+ Redirector redirector = new Redirector(new Context(), "/target");
+ Request request = new Request(Method.GET, "http://localhost/foo/bar");
+ Response response = new Response(request);
+
+ Reference targetRef = redirector.getTargetRef(request, response);
+
+ // getTargetRef() returns a reference that is relative to the request's
+ // resource reference; resolving it yields the absolute target URI.
+ assertEquals("/target", targetRef.toString());
+ assertEquals("http://localhost/target", targetRef.getTargetRef().toString());
+ }
+
+ @Test
+ void getTargetRef_withAbsoluteTemplate_isUsedAsIs() {
+ Redirector redirector = new Redirector(new Context(), "http://example.com/target");
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+
+ Reference targetRef = redirector.getTargetRef(request, response);
+
+ assertEquals("http://example.com/target", targetRef.toString());
+ }
+
+ @Test
+ void getTargetRef_withTemplateVariable_isResolvedFromRequest() {
+ Redirector redirector =
+ new Redirector(
+ new Context(), "http://example.com/{a}", Redirector.MODE_CLIENT_FOUND);
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ request.getAttributes().put("a", "resolved");
+ Response response = new Response(request);
+
+ Reference targetRef = redirector.getTargetRef(request, response);
+
+ assertEquals("http://example.com/resolved", targetRef.toString());
+ }
+
+ @Test
+ void handle_clientPermanentMode_redirectsPermanently() {
+ Redirector redirector =
+ new Redirector(new Context(), "/new", Redirector.MODE_CLIENT_PERMANENT);
+ Request request = new Request(Method.GET, "http://localhost/old");
+ Response response = new Response(request);
+
+ redirector.handle(request, response);
+
+ assertEquals(Status.REDIRECTION_PERMANENT, response.getStatus());
+ assertEquals("http://localhost/new", response.getLocationRef().getTargetRef().toString());
+ }
+
+ @Test
+ void handle_clientFoundMode_redirectsWithFoundStatus() {
+ Redirector redirector = new Redirector(new Context(), "/new", Redirector.MODE_CLIENT_FOUND);
+ Request request = new Request(Method.GET, "http://localhost/old");
+ Response response = new Response(request);
+
+ redirector.handle(request, response);
+
+ assertEquals(Status.REDIRECTION_FOUND, response.getStatus());
+ assertEquals("http://localhost/new", response.getLocationRef().getTargetRef().toString());
+ }
+
+ @Test
+ void handle_clientSeeOtherMode_redirectsWithSeeOtherStatus() {
+ Redirector redirector =
+ new Redirector(new Context(), "/new", Redirector.MODE_CLIENT_SEE_OTHER);
+ Request request = new Request(Method.GET, "http://localhost/old");
+ Response response = new Response(request);
+
+ redirector.handle(request, response);
+
+ assertEquals(Status.REDIRECTION_SEE_OTHER, response.getStatus());
+ }
+
+ @Test
+ void handle_clientTemporaryMode_redirectsWithTemporaryStatus() {
+ Redirector redirector =
+ new Redirector(new Context(), "/new", Redirector.MODE_CLIENT_TEMPORARY);
+ Request request = new Request(Method.GET, "http://localhost/old");
+ Response response = new Response(request);
+
+ redirector.handle(request, response);
+
+ assertEquals(Status.REDIRECTION_TEMPORARY, response.getStatus());
+ }
+
+ @Test
+ void handle_serverOutboundModeWithoutDispatcher_logsWarningAndDoesNotThrow() {
+ Redirector redirector =
+ new Redirector(
+ new Context(), "http://backend/target", Redirector.MODE_SERVER_OUTBOUND);
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+
+ redirector.handle(request, response);
+ }
+
+ @Test
+ void handle_serverInboundModeWithoutDispatcher_logsWarningAndDoesNotThrow() {
+ Redirector redirector =
+ new Redirector(
+ new Context(), "http://backend/target", Redirector.MODE_SERVER_INBOUND);
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+
+ redirector.handle(request, response);
+ }
+
+ @Test
+ void serverRedirect_withNext_dispatchesRequestAndRestoresResourceRef() {
+ Redirector redirector = new Redirector(new Context(), "http://backend/target");
+ Reference originalResourceRef = new Reference("http://localhost/foo");
+ Request request = new Request(Method.GET, originalResourceRef);
+ Response response = new Response(request);
+ Reference targetRef = new Reference("http://backend/target");
+
+ Restlet next =
+ new Restlet(new Context()) {
+ @Override
+ public void handle(Request req, Response resp) {
+ super.handle(req, resp);
+ assertEquals(targetRef.toString(), req.getResourceRef().toString());
+ resp.setEntity(new StringRepresentation("payload"));
+ }
+ };
+
+ redirector.serverRedirect(next, targetRef, request, response);
+
+ assertEquals(originalResourceRef.toString(), request.getResourceRef().toString());
+ assertNotNull(response.getEntity());
+ }
+
+ @Test
+ void serverRedirect_withNullNext_logsWarningAndDoesNotThrow() {
+ Redirector redirector = new Redirector(new Context(), "/target");
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+
+ redirector.serverRedirect(null, new Reference("http://backend/target"), request, response);
+ }
+
+ @Test
+ void rewriteRequest_withHeadersCleaningEnabled_removesHeadersAttribute() {
+ Redirector redirector = new Redirector(new Context(), "/target");
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ request.getHeaders().add(new Header("X-Custom", "value"));
+
+ redirector.rewrite(request);
+
+ assertNull(request.getAttributes().get(HeaderConstants.ATTRIBUTE_HEADERS));
+ }
+
+ @Test
+ void rewriteRequest_withHeadersCleaningDisabled_keepsOnlyExtensionHeaders() {
+ Redirector redirector = new Redirector(new Context(), "/target");
+ redirector.setHeadersCleaning(false);
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ request.getHeaders().add(new Header("X-Custom", "value"));
+ request.getHeaders().add(new Header(HeaderConstants.HEADER_CONTENT_TYPE, "text/plain"));
+
+ redirector.rewrite(request);
+
+ org.restlet.util.Series remaining = request.getHeaders();
+ assertEquals(1, remaining.size());
+ assertEquals("X-Custom", remaining.get(0).getName());
+ }
+
+ @Test
+ void rewriteResponse_withHeadersCleaningEnabled_removesHeadersAttribute() {
+ Redirector redirector = new Redirector(new Context(), "/target");
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+ response.getHeaders().add(new Header("X-Custom", "value"));
+
+ redirector.rewrite(response);
+
+ assertNull(response.getAttributes().get(HeaderConstants.ATTRIBUTE_HEADERS));
+ }
+
+ @Test
+ void rewriteRepresentation_returnsSameInstanceByDefault() {
+ Redirector redirector = new Redirector(new Context(), "/target");
+ StringRepresentation representation = new StringRepresentation("body");
+
+ assertEquals(
+ representation,
+ redirector.rewrite((org.restlet.representation.Representation) representation));
+ }
+
+ @Test
+ void rewriteLocation_rewritesResponseLocationUsingTemplate() {
+ Redirector redirector =
+ new Redirector(
+ new Context(), "http://backend{rr}", Redirector.MODE_SERVER_OUTBOUND);
+ Request request = new Request(Method.GET, "http://myproxy/foo");
+ request.getResourceRef().setBaseRef(new Reference("http://myproxy"));
+ Response response = new Response(request);
+ response.setLocationRef("http://backend/foo");
+
+ redirector.rewriteLocation(request, response);
+
+ assertEquals("http://myproxy/foo", response.getLocationRef().toString());
+ }
+
+ @Test
+ void rewriteLocation_withoutLocationRef_isNoOp() {
+ Redirector redirector = new Redirector(new Context(), "http://backend{rr}");
+ Request request = new Request(Method.GET, "http://myproxy/foo");
+ Response response = new Response(request);
+
+ redirector.rewriteLocation(request, response);
+
+ assertNull(response.getLocationRef());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/routing/RouteTestCase.java b/org.restlet/src/test/java/org/restlet/routing/RouteTestCase.java
new file mode 100644
index 0000000000..d35bf7b662
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/routing/RouteTestCase.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.routing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+
+/** Unit tests for {@link Route}. */
+class RouteTestCase {
+
+ /** Minimal concrete fixture, since {@link Route} is abstract. */
+ private static class FixedScoreRoute extends Route {
+ private final float score;
+
+ FixedScoreRoute(Router router, Restlet next, float score) {
+ super(router, next);
+ this.score = score;
+ }
+
+ @Override
+ public float score(Request request, Response response) {
+ return score;
+ }
+ }
+
+ @Test
+ void constructorWithNext_hasNoRouterAndUsesNextContext() {
+ Restlet next = new MockRestlet(new Context());
+
+ Route route = new FixedScoreRoute(null, next, 1.0F);
+
+ assertNull(route.getRouter());
+ assertSame(next.getContext(), route.getContext());
+ }
+
+ @Test
+ void constructorWithRouter_usesRouterContext() {
+ Router router = new Router(new Context());
+ Restlet next = new MockRestlet(null);
+
+ Route route = new FixedScoreRoute(router, next, 1.0F);
+
+ assertSame(router, route.getRouter());
+ assertSame(router.getContext(), route.getContext());
+ }
+
+ @Test
+ void setRouter_roundTrip() {
+ Router router = new Router();
+ Route route = new FixedScoreRoute(null, new MockRestlet(null), 1.0F);
+
+ route.setRouter(router);
+
+ assertSame(router, route.getRouter());
+ }
+
+ @Test
+ void score_returnsValueFromSubclass() {
+ Route route = new FixedScoreRoute(null, new MockRestlet(null), 0.75F);
+
+ float score = route.score(new Request(), new Response(new Request()));
+
+ assertEquals(0.75F, score);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/routing/RouterTestCase.java b/org.restlet/src/test/java/org/restlet/routing/RouterTestCase.java
new file mode 100644
index 0000000000..b93718f1be
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/routing/RouterTestCase.java
@@ -0,0 +1,366 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.routing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+import org.restlet.resource.Directory;
+import org.restlet.resource.ServerResource;
+
+/** Unit tests for {@link Router}. */
+class RouterTestCase {
+
+ /** Minimal concrete fixture. */
+ static class MyServerResource extends ServerResource {}
+
+ private Restlet targetRestlet() {
+ return new Restlet(new Context()) {
+ @Override
+ public void handle(Request request, Response response) {
+ super.handle(request, response);
+ response.setStatus(Status.SUCCESS_OK);
+ }
+ };
+ }
+
+ @Test
+ void defaultConstructor_hasExpectedDefaults() {
+ Router router = new Router();
+
+ assertEquals(Template.MODE_EQUALS, router.getDefaultMatchingMode());
+ assertFalse(router.getDefaultMatchingQuery());
+ assertNull(router.getDefaultRoute());
+ assertEquals(Router.MODE_FIRST_MATCH, router.getRoutingMode());
+ assertEquals(0.5F, router.getRequiredScore());
+ assertEquals(1, router.getMaxAttempts());
+ assertEquals(500L, router.getRetryDelay());
+ assertNotNull(router.getRoutes());
+ assertTrue(router.getRoutes().isEmpty());
+ }
+
+ @Test
+ void attach_withPlainRestlet_usesDefaultMatchingMode() {
+ Router router = new Router(new Context());
+
+ TemplateRoute route = router.attach(targetRestlet());
+
+ assertEquals(1, router.getRoutes().size());
+ assertEquals(Template.MODE_EQUALS, route.getMatchingMode());
+ }
+
+ @Test
+ void attach_withDirectoryTarget_usesStartsWithMatchingMode() {
+ Router router = new Router(new Context());
+ Directory directory = new Directory(router.getContext(), "clap://class/");
+
+ TemplateRoute route = router.attach(directory);
+
+ assertEquals(Template.MODE_STARTS_WITH, route.getMatchingMode());
+ }
+
+ @Test
+ void attach_withRouterTarget_usesStartsWithMatchingMode() {
+ Router router = new Router(new Context());
+ Router subRouter = new Router(router.getContext());
+
+ TemplateRoute route = router.attach(subRouter);
+
+ assertEquals(Template.MODE_STARTS_WITH, route.getMatchingMode());
+ }
+
+ @Test
+ void attach_withFilterTarget_delegatesMatchingModeToNext() {
+ Router router = new Router(new Context());
+ Directory directory = new Directory(router.getContext(), "clap://class/");
+ Extractor filter = new Extractor(router.getContext(), directory);
+
+ TemplateRoute route = router.attach(filter);
+
+ assertEquals(Template.MODE_STARTS_WITH, route.getMatchingMode());
+ }
+
+ @Test
+ void attach_withPathTemplateAndExplicitMode_setsGivenMode() {
+ Router router = new Router(new Context());
+
+ TemplateRoute route = router.attach("/foo", targetRestlet(), Template.MODE_STARTS_WITH);
+
+ assertEquals(Template.MODE_STARTS_WITH, route.getMatchingMode());
+ assertEquals(1, router.getRoutes().size());
+ }
+
+ @Test
+ void attach_withResourceClass_createsFinderRoute() {
+ Router router = new Router(new Context());
+
+ TemplateRoute route = router.attach("/foo", MyServerResource.class);
+
+ assertNotNull(route);
+ assertEquals(1, router.getRoutes().size());
+ }
+
+ @Test
+ void attachDefault_setsDefaultRouteWithStartsWithMode() {
+ Router router = new Router(new Context());
+ Restlet target = targetRestlet();
+
+ TemplateRoute route = router.attachDefault(target);
+
+ assertSame(route, router.getDefaultRoute());
+ assertEquals(Template.MODE_STARTS_WITH, route.getMatchingMode());
+ }
+
+ @Test
+ void attachDefault_withResourceClass_setsDefaultRoute() {
+ Router router = new Router(new Context());
+
+ TemplateRoute route = router.attachDefault(MyServerResource.class);
+
+ assertSame(route, router.getDefaultRoute());
+ }
+
+ @Test
+ void detach_byRestlet_removesMatchingRoutesAndDefaultRoute() {
+ Router router = new Router(new Context());
+ Restlet target = targetRestlet();
+ router.attach("/foo", target);
+ router.attachDefault(target);
+
+ router.detach(target);
+
+ assertTrue(router.getRoutes().isEmpty());
+ assertNull(router.getDefaultRoute());
+ }
+
+ @Test
+ void detach_byClass_removesFinderRoutesAndDefaultRoute() {
+ Router router = new Router(new Context());
+ router.attach("/foo", MyServerResource.class);
+ router.attachDefault(MyServerResource.class);
+
+ router.detach(MyServerResource.class);
+
+ assertTrue(router.getRoutes().isEmpty());
+ assertNull(router.getDefaultRoute());
+ }
+
+ @Test
+ void getNext_firstMatch_returnsMatchingRoute() {
+ Router router = new Router(new Context());
+ Route route = router.attach("/foo", targetRestlet());
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ request.getResourceRef().setBaseRef(new Reference("http://localhost"));
+ Response response = new Response(request);
+
+ Restlet next = router.getNext(request, response);
+
+ assertNotNull(next);
+ assertSame(route, next);
+ }
+
+ @Test
+ void getNext_noMatchAndNoDefaultRoute_setsNotFoundStatus() {
+ Router router = new Router(new Context());
+ router.attach("/foo", targetRestlet(), Template.MODE_EQUALS);
+ Request request = new Request(Method.GET, "http://localhost/bar");
+ Response response = new Response(request);
+
+ Restlet next = router.getNext(request, response);
+
+ assertNull(next);
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, response.getStatus());
+ }
+
+ @Test
+ void getNext_noMatch_fallsBackToDefaultRoute() {
+ Router router = new Router(new Context());
+ router.attach("/foo", targetRestlet(), Template.MODE_EQUALS);
+ Restlet defaultTarget = targetRestlet();
+ TemplateRoute defaultRoute = router.attachDefault(defaultTarget);
+ Request request = new Request(Method.GET, "http://localhost/bar");
+ Response response = new Response(request);
+
+ Restlet next = router.getNext(request, response);
+
+ assertSame(defaultRoute, next);
+ }
+
+ @Test
+ void getNext_bestMatchMode_selectsHighestScoringRoute() {
+ Router router = new Router(new Context());
+ router.setRoutingMode(Router.MODE_BEST_MATCH);
+ Restlet shortTarget = targetRestlet();
+ Restlet longTarget = targetRestlet();
+ router.attach("/foo", shortTarget, Template.MODE_STARTS_WITH);
+ Route longRoute = router.attach("/foo/bar", longTarget, Template.MODE_STARTS_WITH);
+ Request request = new Request(Method.GET, "http://localhost/foo/bar");
+ request.getResourceRef().setBaseRef(new Reference("http://localhost"));
+ Response response = new Response(request);
+
+ Restlet next = router.getNext(request, response);
+
+ assertSame(longRoute, next);
+ }
+
+ @Test
+ void getNext_customMode_usesGetCustomOverride() {
+ Restlet target = targetRestlet();
+ Router router =
+ new Router(new Context()) {
+ @Override
+ protected Route getCustom(Request request, Response response) {
+ return getRoutes().isEmpty() ? null : getRoutes().get(0);
+ }
+ };
+ router.setRoutingMode(Router.MODE_CUSTOM);
+ Route route = router.attach("/foo", target);
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+
+ Restlet next = router.getNext(request, response);
+
+ assertSame(route, next);
+ }
+
+ @Test
+ void getNext_retriesUpToMaxAttempts() {
+ java.util.concurrent.atomic.AtomicInteger attempts =
+ new java.util.concurrent.atomic.AtomicInteger();
+ Restlet target = targetRestlet();
+ Router router =
+ new Router(new Context()) {
+ @Override
+ protected Route getCustom(Request request, Response response) {
+ return attempts.incrementAndGet() < 2 ? null : getRoutes().get(0);
+ }
+ };
+ router.setRoutingMode(Router.MODE_CUSTOM);
+ router.setMaxAttempts(3);
+ router.setRetryDelay(1L);
+ Route route = router.attach("/foo", target);
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+
+ Restlet next = router.getNext(request, response);
+
+ assertSame(route, next);
+ assertEquals(2, attempts.get());
+ }
+
+ @Test
+ void handle_dispatchesToMatchingRoute() {
+ Router router = new Router(new Context());
+ router.attach("/foo", targetRestlet());
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ request.getResourceRef().setBaseRef(new Reference("http://localhost"));
+ Response response = new Response(request);
+
+ router.handle(request, response);
+
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ }
+
+ @Test
+ void handle_withNoMatch_setsNotFoundStatus() {
+ Router router = new Router(new Context());
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+
+ router.handle(request, response);
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, response.getStatus());
+ }
+
+ @Test
+ void redirectPermanent_attachesRedirectorWithPermanentMode() {
+ Router router = new Router(new Context());
+
+ TemplateRoute route = router.redirectPermanent("/old", "/new");
+
+ assertTrue(route.getNext() instanceof Redirector);
+ assertEquals(Redirector.MODE_CLIENT_PERMANENT, ((Redirector) route.getNext()).getMode());
+ }
+
+ @Test
+ void redirectSeeOther_attachesRedirectorWithSeeOtherMode() {
+ Router router = new Router(new Context());
+
+ TemplateRoute route = router.redirectSeeOther("/old", "/new");
+
+ assertEquals(Redirector.MODE_CLIENT_SEE_OTHER, ((Redirector) route.getNext()).getMode());
+ }
+
+ @Test
+ void redirectTemporary_attachesRedirectorWithTemporaryMode() {
+ Router router = new Router(new Context());
+
+ TemplateRoute route = router.redirectTemporary("/old", "/new");
+
+ assertEquals(Redirector.MODE_CLIENT_TEMPORARY, ((Redirector) route.getNext()).getMode());
+ }
+
+ @Test
+ void startAndStop_propagateToRoutesAndDefaultRoute() throws Exception {
+ Router router = new Router(new Context());
+ TemplateRoute route = router.attach("/foo", targetRestlet());
+ TemplateRoute defaultRoute = router.attachDefault(targetRestlet());
+
+ router.start();
+
+ assertTrue(router.isStarted());
+ assertTrue(route.isStarted());
+ assertTrue(defaultRoute.isStarted());
+
+ router.stop();
+
+ assertFalse(router.isStarted());
+ assertFalse(route.isStarted());
+ assertFalse(defaultRoute.isStarted());
+ }
+
+ @Test
+ void settersRoundTrip() {
+ Router router = new Router();
+
+ router.setDefaultMatchingMode(Template.MODE_STARTS_WITH);
+ assertEquals(Template.MODE_STARTS_WITH, router.getDefaultMatchingMode());
+
+ router.setDefaultMatchingQuery(true);
+ assertTrue(router.getDefaultMatchingQuery());
+
+ router.setMaxAttempts(5);
+ assertEquals(5, router.getMaxAttempts());
+
+ router.setRequiredScore(0.9F);
+ assertEquals(0.9F, router.getRequiredScore());
+
+ router.setRetryDelay(100L);
+ assertEquals(100L, router.getRetryDelay());
+
+ router.setRoutingMode(Router.MODE_LAST_MATCH);
+ assertEquals(Router.MODE_LAST_MATCH, router.getRoutingMode());
+
+ org.restlet.util.RouteList routes = new org.restlet.util.RouteList();
+ router.setRoutes(routes);
+ assertSame(routes, router.getRoutes());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/routing/TemplateRouteTestCase.java b/org.restlet/src/test/java/org/restlet/routing/TemplateRouteTestCase.java
new file mode 100644
index 0000000000..d1792da258
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/routing/TemplateRouteTestCase.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.routing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+
+/** Unit tests for {@link TemplateRoute}. */
+class TemplateRouteTestCase {
+
+ @Test
+ void constructorWithNext_hasNoTemplate() {
+ Restlet next = new MockRestlet(null);
+
+ TemplateRoute route = new TemplateRoute(next);
+
+ assertNull(route.getTemplate());
+ assertNull(route.getRouter());
+ }
+
+ @Test
+ void constructorWithStringPattern_createsStartsWithTemplate() {
+ Router router = new Router(new Context());
+ Restlet next = new MockRestlet(null);
+
+ TemplateRoute route = new TemplateRoute(router, "/foo", next);
+
+ assertNotNull(route.getTemplate());
+ assertEquals(Template.MODE_STARTS_WITH, route.getMatchingMode());
+ }
+
+ @Test
+ void constructorWithTemplate_defaultsMatchingQueryFromRouter() {
+ Router router = new Router();
+ router.setDefaultMatchingQuery(true);
+ Template template = new Template("/foo");
+
+ TemplateRoute route = new TemplateRoute(router, template, new MockRestlet(null));
+
+ assertTrue(route.isMatchingQuery());
+ }
+
+ @Test
+ void constructorWithoutRouter_matchingQueryDefaultsTrue() {
+ TemplateRoute route = new TemplateRoute(null, new Template("/foo"), new MockRestlet(null));
+
+ assertTrue(route.isMatchingQuery());
+ }
+
+ @Test
+ void score_withoutRouter_returnsZero() {
+ TemplateRoute route = new TemplateRoute(null, new Template("/foo"), new MockRestlet(null));
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+
+ assertEquals(0F, route.score(request, response));
+ }
+
+ @Test
+ void score_withMatchingUri_returnsPositiveScore() {
+ Router router = new Router(new Context());
+ TemplateRoute route = new TemplateRoute(router, "/foo", new MockRestlet(null));
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ request.getResourceRef().setBaseRef(new org.restlet.data.Reference("http://localhost"));
+ Response response = new Response(request);
+
+ float score = route.score(request, response);
+
+ assertTrue(score > 0F);
+ }
+
+ @Test
+ void score_withNonMatchingUri_returnsZero() {
+ Router router = new Router(new Context());
+ TemplateRoute route = new TemplateRoute(router, "/foo", new MockRestlet(null));
+ Request request = new Request(Method.GET, "http://localhost/bar");
+ Response response = new Response(request);
+
+ assertEquals(0F, route.score(request, response));
+ }
+
+ @Test
+ void beforeHandle_onMatch_updatesBaseReferenceAndContinues() {
+ Router router = new Router(new Context());
+ Restlet next =
+ new Restlet(new Context()) {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setStatus(Status.SUCCESS_OK);
+ }
+ };
+ TemplateRoute route = new TemplateRoute(router, "/foo", next);
+ Request request = new Request(Method.GET, "http://localhost/foo/bar");
+ request.getResourceRef().setBaseRef(new org.restlet.data.Reference("http://localhost"));
+ Response response = new Response(request);
+
+ route.handle(request, response);
+
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ assertEquals("/foo", request.getResourceRef().getBaseRef().getPath());
+ }
+
+ @Test
+ void beforeHandle_onNoMatch_setsNotFoundStatus() {
+ Router router = new Router(new Context());
+ Restlet next = new MockRestlet(new Context());
+ TemplateRoute route =
+ new TemplateRoute(router, new Template("/foo", Template.MODE_EQUALS), next);
+ Request request = new Request(Method.GET, "http://localhost/other");
+ Response response = new Response(request);
+
+ route.handle(request, response);
+
+ assertEquals(Status.CLIENT_ERROR_NOT_FOUND, response.getStatus());
+ }
+
+ @Test
+ void setMatchingMode_updatesTemplate() {
+ TemplateRoute route = new TemplateRoute(null, new Template("/foo"), new MockRestlet(null));
+
+ route.setMatchingMode(Template.MODE_STARTS_WITH);
+
+ assertEquals(Template.MODE_STARTS_WITH, route.getMatchingMode());
+ }
+
+ @Test
+ void setMatchingQuery_roundTrip() {
+ TemplateRoute route = new TemplateRoute(null, new Template("/foo"), new MockRestlet(null));
+
+ route.setMatchingQuery(false);
+
+ assertEquals(false, route.isMatchingQuery());
+ }
+
+ @Test
+ void setTemplate_roundTrip() {
+ TemplateRoute route = new TemplateRoute(new MockRestlet(null));
+ Template template = new Template("/bar");
+
+ route.setTemplate(template);
+
+ assertEquals(template, route.getTemplate());
+ }
+
+ @Test
+ void toString_withTemplate_includesPatternAndNext() {
+ Restlet next = new MockRestlet(null);
+ TemplateRoute route = new TemplateRoute(null, new Template("/foo"), next);
+
+ String result = route.toString();
+
+ assertTrue(result.contains("/foo"));
+ }
+
+ @Test
+ void toString_withoutTemplate_fallsBackToSuper() {
+ TemplateRoute route = new TemplateRoute(new MockRestlet(null));
+
+ String result = route.toString();
+
+ assertNotNull(result);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/routing/VariableTestCase.java b/org.restlet/src/test/java/org/restlet/routing/VariableTestCase.java
new file mode 100644
index 0000000000..d77cbf5095
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/routing/VariableTestCase.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.routing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link Variable}. */
+class VariableTestCase {
+
+ @Test
+ void defaultConstructor_hasExpectedDefaults() {
+ Variable variable = new Variable();
+
+ assertEquals(Variable.TYPE_ALL, variable.getType());
+ assertEquals("", variable.getDefaultValue());
+ assertTrue(variable.isRequired());
+ assertFalse(variable.isFixed());
+ assertFalse(variable.isDecodingOnParse());
+ assertFalse(variable.isEncodingOnFormat());
+ }
+
+ @Test
+ void typeConstructor_setsTypeAndDefaults() {
+ Variable variable = new Variable(Variable.TYPE_ALPHA);
+
+ assertEquals(Variable.TYPE_ALPHA, variable.getType());
+ assertEquals("", variable.getDefaultValue());
+ assertTrue(variable.isRequired());
+ assertFalse(variable.isFixed());
+ }
+
+ @Test
+ void fourArgConstructor_setsAllFields() {
+ Variable variable = new Variable(Variable.TYPE_DIGIT, "42", false, true);
+
+ assertEquals(Variable.TYPE_DIGIT, variable.getType());
+ assertEquals("42", variable.getDefaultValue());
+ assertFalse(variable.isRequired());
+ assertTrue(variable.isFixed());
+ }
+
+ @Test
+ void sixArgConstructor_setsEncodingFlags() {
+ Variable variable = new Variable(Variable.TYPE_URI_ALL, "x", true, false, true, true);
+
+ assertTrue(variable.isDecodingOnParse());
+ assertTrue(variable.isEncodingOnFormat());
+ }
+
+ @Test
+ void gettersAndSetters_roundTrip() {
+ Variable variable = new Variable();
+
+ variable.setType(Variable.TYPE_WORD);
+ assertEquals(Variable.TYPE_WORD, variable.getType());
+
+ variable.setDefaultValue("abc");
+ assertEquals("abc", variable.getDefaultValue());
+
+ variable.setRequired(false);
+ assertFalse(variable.isRequired());
+
+ variable.setFixed(true);
+ assertTrue(variable.isFixed());
+
+ variable.setDecodingOnParse(true);
+ assertTrue(variable.isDecodingOnParse());
+
+ variable.setEncodingOnFormat(true);
+ assertTrue(variable.isEncodingOnFormat());
+ }
+
+ @Test
+ void encode_withUriPathType_encodesReservedCharacters() {
+ Variable variable = new Variable(Variable.TYPE_URI_PATH);
+
+ String encoded = variable.encode("a b/c");
+
+ assertEquals(org.restlet.data.Reference.encode("a b/c"), encoded);
+ }
+
+ @Test
+ void encode_withNonUriType_returnsValueUnchanged() {
+ Variable variable = new Variable(Variable.TYPE_ALPHA);
+
+ String encoded = variable.encode("a b/c");
+
+ assertEquals("a b/c", encoded);
+ }
+
+ @Test
+ void encode_withUriAllType_encodesValue() {
+ Variable variable = new Variable(Variable.TYPE_URI_ALL);
+
+ assertEquals(
+ org.restlet.data.Reference.encode("hello world"), variable.encode("hello world"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/routing/VirtualHostTestCase.java b/org.restlet/src/test/java/org/restlet/routing/VirtualHostTestCase.java
new file mode 100644
index 0000000000..f4a4d1c551
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/routing/VirtualHostTestCase.java
@@ -0,0 +1,213 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.routing;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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 org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+
+/** Unit tests for {@link VirtualHost}. */
+class VirtualHostTestCase {
+
+ @Test
+ void defaultConstructor_matchesEverything() {
+ VirtualHost host = new VirtualHost();
+
+ assertEquals(".*", host.getHostDomain());
+ assertEquals(".*", host.getHostPort());
+ assertEquals(".*", host.getHostScheme());
+ assertEquals(".*", host.getResourceDomain());
+ assertEquals(".*", host.getResourcePort());
+ assertEquals(".*", host.getResourceScheme());
+ assertEquals(".*", host.getServerAddress());
+ assertEquals(".*", host.getServerPort());
+ assertEquals(Template.MODE_STARTS_WITH, host.getDefaultMatchingMode());
+ assertEquals(Router.MODE_BEST_MATCH, host.getRoutingMode());
+ }
+
+ @Test
+ void constructorWithContext_createsChildContext() {
+ Context parent = new Context();
+
+ VirtualHost host = new VirtualHost(parent);
+
+ assertNotNull(host.getContext());
+ }
+
+ @Test
+ void fullConstructor_setsAllPatterns() {
+ Context parent = new Context();
+
+ VirtualHost host =
+ new VirtualHost(
+ parent,
+ "hostDomain",
+ "hostPort",
+ "hostScheme",
+ "resourceDomain",
+ "resourcePort",
+ "resourceScheme",
+ "serverAddress",
+ "serverPort");
+
+ assertEquals("hostDomain", host.getHostDomain());
+ assertEquals("hostPort", host.getHostPort());
+ assertEquals("hostScheme", host.getHostScheme());
+ assertEquals("resourceDomain", host.getResourceDomain());
+ assertEquals("resourcePort", host.getResourcePort());
+ assertEquals("resourceScheme", host.getResourceScheme());
+ assertEquals("serverAddress", host.getServerAddress());
+ assertEquals("serverPort", host.getServerPort());
+ }
+
+ @Test
+ void settersRoundTrip() {
+ VirtualHost host = new VirtualHost();
+
+ host.setHostDomain("a");
+ assertEquals("a", host.getHostDomain());
+
+ host.setHostPort("b");
+ assertEquals("b", host.getHostPort());
+
+ host.setHostScheme("c");
+ assertEquals("c", host.getHostScheme());
+
+ host.setResourceDomain("d");
+ assertEquals("d", host.getResourceDomain());
+
+ host.setResourcePort("e");
+ assertEquals("e", host.getResourcePort());
+
+ host.setResourceScheme("f");
+ assertEquals("f", host.getResourceScheme());
+
+ host.setServerAddress("g");
+ assertEquals("g", host.getServerAddress());
+
+ host.setServerPort("h");
+ assertEquals("h", host.getServerPort());
+ }
+
+ @Test
+ void attach_withoutContext_createsChildContextForTarget() {
+ Context parent = new Context();
+ VirtualHost host = new VirtualHost(parent);
+ Restlet target = new MockRestlet(null);
+
+ host.attach(target);
+
+ assertNotNull(target.getContext());
+ }
+
+ @Test
+ void attach_withPathAndTargetHavingContext_keepsExistingContext() {
+ VirtualHost host = new VirtualHost(new Context());
+ Context existing = new Context();
+ Restlet target = new MockRestlet(existing);
+
+ host.attach("/foo", target);
+
+ assertSame(existing, target.getContext());
+ }
+
+ @Test
+ void attachDefault_withoutContext_createsChildContextForTarget() {
+ Context parent = new Context();
+ VirtualHost host = new VirtualHost(parent);
+ Restlet target = new MockRestlet(null);
+
+ host.attachDefault(target);
+
+ assertNotNull(target.getContext());
+ assertSame(host.getDefaultRoute().getNext(), target);
+ }
+
+ @Test
+ void handle_dispatchesAndSetsCurrentHost() {
+ VirtualHost host = new VirtualHost(new Context());
+ Restlet target =
+ new Restlet(new Context()) {
+ @Override
+ public void handle(Request request, Response response) {
+ super.handle(request, response);
+ response.setStatus(Status.SUCCESS_OK);
+ }
+ };
+ host.attach("/foo", target);
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ request.getResourceRef().setBaseRef(new Reference("http://localhost"));
+ Response response = new Response(request);
+
+ host.handle(request, response);
+
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ assertEquals(Integer.valueOf(host.hashCode()), VirtualHost.getCurrent());
+ assertNotNull(request.getRootRef());
+ }
+
+ @Test
+ void setContext_recreatesChildContext() {
+ VirtualHost host = new VirtualHost();
+ Context parent = new Context();
+
+ host.setContext(parent);
+
+ assertNotNull(host.getContext());
+ }
+
+ @Test
+ void setContext_withNull_clearsContext() {
+ VirtualHost host = new VirtualHost(new Context());
+
+ host.setContext(null);
+
+ assertNull(host.getContext());
+ }
+
+ @Test
+ void staticCurrentThreadLocal_roundTrip() {
+ VirtualHost.setCurrent(42);
+
+ assertEquals(Integer.valueOf(42), VirtualHost.getCurrent());
+
+ VirtualHost.setCurrent(null);
+ }
+
+ @Test
+ void getLocalHostNameAndAddress_returnNonNullValues() {
+ assertNotNull(VirtualHost.getLocalHostName());
+ assertNotNull(VirtualHost.getLocalHostAddress());
+ }
+
+ @Test
+ void getIpAddress_forLocalhost_returnsLoopbackAddress() {
+ String ip = VirtualHost.getIpAddress("localhost");
+
+ assertNotNull(ip);
+ }
+
+ @Test
+ void getIpAddress_forUnknownHost_doesNotThrow() {
+ // Depending on the local DNS/network configuration, this may resolve to
+ // null (UnknownHostException) or to some address (e.g. search domain
+ // wildcards), so we only assert that no exception propagates.
+ VirtualHost.getIpAddress("this.host.does.not.exist.invalid");
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/AuthenticatorTestCase.java b/org.restlet/src/test/java/org/restlet/security/AuthenticatorTestCase.java
new file mode 100644
index 0000000000..c11076a520
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/AuthenticatorTestCase.java
@@ -0,0 +1,212 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Status;
+import org.restlet.routing.Filter;
+
+/** Unit tests for {@link Authenticator}. */
+class AuthenticatorTestCase {
+
+ /** Minimal concrete fixture, since {@link Authenticator} is abstract. */
+ private static class TestAuthenticator extends Authenticator {
+
+ private boolean authenticateResult;
+
+ private int authenticateCallCount;
+
+ TestAuthenticator(Context context) {
+ super(context);
+ }
+
+ TestAuthenticator(Context context, boolean optional) {
+ super(context, optional);
+ }
+
+ TestAuthenticator(
+ Context context, boolean multiAuthenticating, boolean optional, Enroler enroler) {
+ super(context, multiAuthenticating, optional, enroler);
+ }
+
+ void setAuthenticateResult(boolean authenticateResult) {
+ this.authenticateResult = authenticateResult;
+ }
+
+ int getAuthenticateCallCount() {
+ return authenticateCallCount;
+ }
+
+ @Override
+ protected boolean authenticate(Request request, Response response) {
+ authenticateCallCount++;
+ return authenticateResult;
+ }
+ }
+
+ @Test
+ void constructorWithContext_defaultsToRequiredAndMultiAuthenticating() {
+ TestAuthenticator authenticator = new TestAuthenticator(new Context());
+ assertFalse(authenticator.isOptional());
+ assertTrue(authenticator.isMultiAuthenticating());
+ }
+
+ @Test
+ void constructorWithOptional_setsOptionalFlag() {
+ TestAuthenticator authenticator = new TestAuthenticator(new Context(), true);
+ assertTrue(authenticator.isOptional());
+ assertTrue(authenticator.isMultiAuthenticating());
+ }
+
+ @Test
+ void constructorWithContext_usesContextDefaultEnroler() {
+ Enroler enroler = clientInfo -> {};
+ Context context = new Context();
+ context.setDefaultEnroler(enroler);
+
+ TestAuthenticator authenticator = new TestAuthenticator(context);
+
+ assertSame(enroler, authenticator.getEnroler());
+ }
+
+ @Test
+ void getEnrolerSetEnroler_roundTrip() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, false, false, null);
+ assertNull(authenticator.getEnroler());
+
+ Enroler enroler = clientInfo -> {};
+ authenticator.setEnroler(enroler);
+ assertSame(enroler, authenticator.getEnroler());
+ }
+
+ @Test
+ void isOptionalSetOptional_roundTrip() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, false, false, null);
+ assertFalse(authenticator.isOptional());
+ authenticator.setOptional(true);
+ assertTrue(authenticator.isOptional());
+ }
+
+ @Test
+ void isMultiAuthenticatingSetMultiAuthenticating_roundTrip() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, false, false, null);
+ assertFalse(authenticator.isMultiAuthenticating());
+ authenticator.setMultiAuthenticating(true);
+ assertTrue(authenticator.isMultiAuthenticating());
+ }
+
+ @Test
+ void beforeHandle_whenAuthenticateSucceeds_returnsContinueAndMarksAuthenticated() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, true, false, null);
+ authenticator.setAuthenticateResult(true);
+ Request request = new Request();
+ Response response = new Response(request);
+
+ int result = authenticator.beforeHandle(request, response);
+
+ assertEquals(Filter.CONTINUE, result);
+ assertTrue(request.getClientInfo().isAuthenticated());
+ }
+
+ @Test
+ void beforeHandle_whenAuthenticateFailsAndNotOptional_returnsStopAndMarksUnauthenticated() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, true, false, null);
+ authenticator.setAuthenticateResult(false);
+ Request request = new Request();
+ Response response = new Response(request);
+
+ int result = authenticator.beforeHandle(request, response);
+
+ assertEquals(Filter.STOP, result);
+ assertFalse(request.getClientInfo().isAuthenticated());
+ }
+
+ @Test
+ void beforeHandle_whenAuthenticateFailsAndOptional_returnsContinueAndSetsOkStatus() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, true, true, null);
+ authenticator.setAuthenticateResult(false);
+ Request request = new Request();
+ Response response = new Response(request);
+
+ int result = authenticator.beforeHandle(request, response);
+
+ assertEquals(Filter.CONTINUE, result);
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ }
+
+ @Test
+ void beforeHandle_whenAlreadyAuthenticatedAndNotMultiAuthenticating_skipsAuthenticate() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, false, false, null);
+ authenticator.setAuthenticateResult(true);
+ Request request = new Request();
+ request.getClientInfo().setAuthenticated(true);
+ Response response = new Response(request);
+
+ int result = authenticator.beforeHandle(request, response);
+
+ assertEquals(Filter.CONTINUE, result);
+ assertEquals(0, authenticator.getAuthenticateCallCount());
+ }
+
+ @Test
+ void beforeHandle_whenAlreadyAuthenticatedAndMultiAuthenticating_reAuthenticates() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, true, false, null);
+ authenticator.setAuthenticateResult(true);
+ Request request = new Request();
+ request.getClientInfo().setAuthenticated(true);
+ Response response = new Response(request);
+
+ authenticator.beforeHandle(request, response);
+
+ assertEquals(1, authenticator.getAuthenticateCallCount());
+ }
+
+ @Test
+ void authenticated_clearsChallengeRequestsAndInvokesEnroler() {
+ java.util.concurrent.atomic.AtomicBoolean enroled =
+ new java.util.concurrent.atomic.AtomicBoolean(false);
+ Enroler enroler = clientInfo -> enroled.set(true);
+ TestAuthenticator authenticator = new TestAuthenticator(null, true, false, enroler);
+ Request request = new Request();
+ Response response = new Response(request);
+ response.getChallengeRequests()
+ .add(
+ new org.restlet.data.ChallengeRequest(
+ org.restlet.data.ChallengeScheme.HTTP_BASIC, "realm"));
+
+ int result = authenticator.authenticated(request, response);
+
+ assertEquals(Filter.CONTINUE, result);
+ assertTrue(request.getClientInfo().isAuthenticated());
+ assertTrue(response.getChallengeRequests().isEmpty());
+ assertTrue(enroled.get());
+ }
+
+ @Test
+ void unauthenticated_marksUnauthenticatedAndReturnsStop() {
+ TestAuthenticator authenticator = new TestAuthenticator(null, true, false, null);
+ Request request = new Request();
+ request.getClientInfo().setAuthenticated(true);
+ Response response = new Response(request);
+
+ int result = authenticator.unauthenticated(request, response);
+
+ assertEquals(Filter.STOP, result);
+ assertFalse(request.getClientInfo().isAuthenticated());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/AuthorizerTestCase.java b/org.restlet/src/test/java/org/restlet/security/AuthorizerTestCase.java
new file mode 100644
index 0000000000..8805fe78aa
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/AuthorizerTestCase.java
@@ -0,0 +1,158 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Status;
+import org.restlet.routing.Filter;
+
+/** Unit tests for {@link Authorizer}. */
+class AuthorizerTestCase {
+
+ /** Minimal concrete fixture, since {@link Authorizer} is abstract. */
+ private static class TestAuthorizer extends Authorizer {
+
+ private boolean authorizeResult;
+
+ TestAuthorizer() {
+ super();
+ }
+
+ TestAuthorizer(String identifier) {
+ super(identifier);
+ }
+
+ void setAuthorizeResult(boolean authorizeResult) {
+ this.authorizeResult = authorizeResult;
+ }
+
+ @Override
+ protected boolean authorize(Request request, Response response) {
+ return authorizeResult;
+ }
+ }
+
+ @Test
+ void defaultConstructor_hasNullIdentifier() {
+ TestAuthorizer authorizer = new TestAuthorizer();
+ assertNull(authorizer.getIdentifier());
+ }
+
+ @Test
+ void constructorWithIdentifier_setsIdentifier() {
+ TestAuthorizer authorizer = new TestAuthorizer("my-id");
+ assertEquals("my-id", authorizer.getIdentifier());
+ }
+
+ @Test
+ void getIdentifierSetIdentifier_roundTrip() {
+ TestAuthorizer authorizer = new TestAuthorizer();
+ authorizer.setIdentifier("another-id");
+ assertEquals("another-id", authorizer.getIdentifier());
+ }
+
+ @Test
+ void authorized_default_returnsContinue() {
+ TestAuthorizer authorizer = new TestAuthorizer();
+ Request request = new Request();
+ Response response = new Response(request);
+
+ assertEquals(Filter.CONTINUE, authorizer.authorized(request, response));
+ }
+
+ @Test
+ void unauthorized_default_setsForbiddenStatusAndReturnsStop() {
+ TestAuthorizer authorizer = new TestAuthorizer();
+ Request request = new Request();
+ Response response = new Response(request);
+
+ int result = authorizer.unauthorized(request, response);
+
+ assertEquals(Filter.STOP, result);
+ assertEquals(Status.CLIENT_ERROR_FORBIDDEN, response.getStatus());
+ }
+
+ @Test
+ void beforeHandle_whenAuthorizeSucceeds_returnsContinue() {
+ TestAuthorizer authorizer = new TestAuthorizer();
+ authorizer.setAuthorizeResult(true);
+ Request request = new Request();
+ Response response = new Response(request);
+
+ assertEquals(Filter.CONTINUE, authorizer.beforeHandle(request, response));
+ }
+
+ @Test
+ void beforeHandle_whenAuthorizeFails_setsForbiddenAndReturnsStop() {
+ TestAuthorizer authorizer = new TestAuthorizer();
+ authorizer.setAuthorizeResult(false);
+ Request request = new Request();
+ Response response = new Response(request);
+
+ int result = authorizer.beforeHandle(request, response);
+
+ assertEquals(Filter.STOP, result);
+ assertEquals(Status.CLIENT_ERROR_FORBIDDEN, response.getStatus());
+ }
+
+ @Test
+ void always_authorize_returnsTrue() {
+ Request request = new Request();
+ Response response = new Response(request);
+
+ assertTrue(Authorizer.ALWAYS.authorize(request, response));
+ }
+
+ @Test
+ void never_authorize_returnsFalse() {
+ Request request = new Request();
+ Response response = new Response(request);
+
+ assertFalse(Authorizer.NEVER.authorize(request, response));
+ }
+
+ @Test
+ void authenticated_authorize_returnsTrueOnlyWhenClientAuthenticated() {
+ Request request = new Request();
+ Response response = new Response(request);
+ assertFalse(Authorizer.AUTHENTICATED.authorize(request, response));
+
+ request.getClientInfo().setAuthenticated(true);
+ assertTrue(Authorizer.AUTHENTICATED.authorize(request, response));
+ }
+
+ @Test
+ void authenticated_beforeHandle_whenNotAuthenticated_setsUnauthorizedStatus() {
+ Request request = new Request();
+ Response response = new Response(request);
+
+ int result = Authorizer.AUTHENTICATED.beforeHandle(request, response);
+
+ assertEquals(Filter.STOP, result);
+ assertEquals(Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus());
+ }
+
+ @Test
+ void authenticated_beforeHandle_whenAuthenticated_returnsContinue() {
+ Request request = new Request();
+ request.getClientInfo().setAuthenticated(true);
+ Response response = new Response(request);
+
+ int result = Authorizer.AUTHENTICATED.beforeHandle(request, response);
+
+ assertEquals(Filter.CONTINUE, result);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/ChallengeAuthenticatorTestCase.java b/org.restlet/src/test/java/org/restlet/security/ChallengeAuthenticatorTestCase.java
new file mode 100644
index 0000000000..e5fe96323e
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/ChallengeAuthenticatorTestCase.java
@@ -0,0 +1,308 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeRequest;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Status;
+
+/** Unit tests for {@link ChallengeAuthenticator}. */
+class ChallengeAuthenticatorTestCase {
+
+ /** Verifier returning a fixed result, for exercising each authenticate() branch. */
+ private static class FixedResultVerifier implements Verifier {
+ private final int result;
+
+ FixedResultVerifier(int result) {
+ this.result = result;
+ }
+
+ @Override
+ public int verify(Request request, Response response) {
+ return result;
+ }
+ }
+
+ @Test
+ void constructorWithRealmAndVerifier_setsFields() {
+ Verifier verifier = new FixedResultVerifier(Verifier.RESULT_VALID);
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null, true, ChallengeScheme.HTTP_BASIC, "realm", verifier);
+
+ assertTrue(authenticator.isOptional());
+ assertEquals(ChallengeScheme.HTTP_BASIC, authenticator.getScheme());
+ assertEquals("realm", authenticator.getRealm());
+ assertSame(verifier, authenticator.getVerifier());
+ assertTrue(authenticator.isRechallenging());
+ }
+
+ @Test
+ void constructorWithoutOptional_defaultsToRequired() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(null, ChallengeScheme.HTTP_BASIC, "realm");
+ assertFalse(authenticator.isOptional());
+ }
+
+ @Test
+ void constructorWithContext_usesContextDefaultVerifier() {
+ Verifier verifier = new FixedResultVerifier(Verifier.RESULT_VALID);
+ Context context = new Context();
+ context.setDefaultVerifier(verifier);
+
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(context, true, ChallengeScheme.HTTP_BASIC, "realm");
+
+ assertSame(verifier, authenticator.getVerifier());
+ }
+
+ @Test
+ void authenticate_validCredentials_returnsTrue() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null,
+ false,
+ ChallengeScheme.HTTP_BASIC,
+ "realm",
+ new FixedResultVerifier(Verifier.RESULT_VALID));
+ Request request = new Request();
+ Response response = new Response(request);
+
+ assertTrue(authenticator.authenticate(request, response));
+ }
+
+ @Test
+ void authenticate_missingCredentialsNotOptional_challengesAndReturnsFalse() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null,
+ false,
+ ChallengeScheme.HTTP_BASIC,
+ "realm",
+ new FixedResultVerifier(Verifier.RESULT_MISSING));
+ Request request = new Request();
+ Response response = new Response(request);
+
+ boolean result = authenticator.authenticate(request, response);
+
+ assertFalse(result);
+ assertEquals(Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus());
+ assertEquals(1, response.getChallengeRequests().size());
+ }
+
+ @Test
+ void authenticate_missingCredentialsOptional_doesNotChallenge() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null,
+ true,
+ ChallengeScheme.HTTP_BASIC,
+ "realm",
+ new FixedResultVerifier(Verifier.RESULT_MISSING));
+ Request request = new Request();
+ Response response = new Response(request);
+
+ boolean result = authenticator.authenticate(request, response);
+
+ assertFalse(result);
+ assertTrue(response.getChallengeRequests().isEmpty());
+ }
+
+ @Test
+ void authenticate_invalidCredentialsRechallenging_callsChallenge() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null,
+ false,
+ ChallengeScheme.HTTP_BASIC,
+ "realm",
+ new FixedResultVerifier(Verifier.RESULT_INVALID));
+ Request request = new Request();
+ Response response = new Response(request);
+
+ boolean result = authenticator.authenticate(request, response);
+
+ assertFalse(result);
+ assertEquals(Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus());
+ assertEquals(1, response.getChallengeRequests().size());
+ }
+
+ @Test
+ void authenticate_invalidCredentialsNotRechallenging_callsForbid() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null,
+ false,
+ ChallengeScheme.HTTP_BASIC,
+ "realm",
+ new FixedResultVerifier(Verifier.RESULT_INVALID));
+ authenticator.setRechallenging(false);
+ Request request = new Request();
+ Response response = new Response(request);
+
+ boolean result = authenticator.authenticate(request, response);
+
+ assertFalse(result);
+ assertEquals(Status.CLIENT_ERROR_FORBIDDEN, response.getStatus());
+ assertTrue(response.getChallengeRequests().isEmpty());
+ }
+
+ @Test
+ void authenticate_staleCredentials_challengesWithStaleFlag() {
+ final boolean[] staleFlag = new boolean[1];
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null,
+ false,
+ ChallengeScheme.HTTP_BASIC,
+ "realm",
+ new FixedResultVerifier(Verifier.RESULT_STALE)) {
+ @Override
+ protected ChallengeRequest createChallengeRequest(boolean stale) {
+ staleFlag[0] = stale;
+ return super.createChallengeRequest(stale);
+ }
+ };
+ Request request = new Request();
+ Response response = new Response(request);
+
+ boolean result = authenticator.authenticate(request, response);
+
+ assertFalse(result);
+ assertTrue(staleFlag[0]);
+ assertEquals(1, response.getChallengeRequests().size());
+ }
+
+ @Test
+ void authenticate_unknownIdentifierRechallenging_callsChallenge() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null,
+ false,
+ ChallengeScheme.HTTP_BASIC,
+ "realm",
+ new FixedResultVerifier(Verifier.RESULT_UNKNOWN));
+ Request request = new Request();
+ Response response = new Response(request);
+
+ boolean result = authenticator.authenticate(request, response);
+
+ assertFalse(result);
+ assertEquals(1, response.getChallengeRequests().size());
+ }
+
+ @Test
+ void authenticate_unknownIdentifierNotRechallenging_callsForbid() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(
+ null,
+ false,
+ ChallengeScheme.HTTP_BASIC,
+ "realm",
+ new FixedResultVerifier(Verifier.RESULT_UNKNOWN));
+ authenticator.setRechallenging(false);
+ Request request = new Request();
+ Response response = new Response(request);
+
+ boolean result = authenticator.authenticate(request, response);
+
+ assertFalse(result);
+ assertEquals(Status.CLIENT_ERROR_FORBIDDEN, response.getStatus());
+ }
+
+ @Test
+ void authenticate_withoutVerifier_setsInternalServerError() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(null, false, ChallengeScheme.HTTP_BASIC, "realm", null);
+ Request request = new Request();
+ Response response = new Response(request);
+
+ boolean result = authenticator.authenticate(request, response);
+
+ assertFalse(result);
+ assertEquals(Status.SERVER_ERROR_INTERNAL, response.getStatus());
+ }
+
+ @Test
+ void challenge_addsChallengeRequestAndSetsUnauthorizedStatus() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(null, ChallengeScheme.HTTP_BASIC, "realm");
+ Request request = new Request();
+ Response response = new Response(request);
+
+ authenticator.challenge(response, false);
+
+ assertEquals(Status.CLIENT_ERROR_UNAUTHORIZED, response.getStatus());
+ assertEquals(1, response.getChallengeRequests().size());
+ assertEquals(
+ ChallengeScheme.HTTP_BASIC, response.getChallengeRequests().get(0).getScheme());
+ assertEquals("realm", response.getChallengeRequests().get(0).getRealm());
+ }
+
+ @Test
+ void forbid_setsForbiddenStatus() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(null, ChallengeScheme.HTTP_BASIC, "realm");
+ Request request = new Request();
+ Response response = new Response(request);
+
+ authenticator.forbid(response);
+
+ assertEquals(Status.CLIENT_ERROR_FORBIDDEN, response.getStatus());
+ }
+
+ @Test
+ void createChallengeRequest_returnsRequestWithSchemeAndRealm() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(null, ChallengeScheme.HTTP_BASIC, "realm");
+
+ ChallengeRequest challengeRequest = authenticator.createChallengeRequest(true);
+
+ assertEquals(ChallengeScheme.HTTP_BASIC, challengeRequest.getScheme());
+ assertEquals("realm", challengeRequest.getRealm());
+ }
+
+ @Test
+ void getRealmSetRealm_roundTrip() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(null, ChallengeScheme.HTTP_BASIC, "realm");
+ authenticator.setRealm("other-realm");
+ assertEquals("other-realm", authenticator.getRealm());
+ }
+
+ @Test
+ void getVerifierSetVerifier_roundTrip() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(null, ChallengeScheme.HTTP_BASIC, "realm");
+ assertNull(authenticator.getVerifier());
+
+ Verifier verifier = new FixedResultVerifier(Verifier.RESULT_VALID);
+ authenticator.setVerifier(verifier);
+ assertSame(verifier, authenticator.getVerifier());
+ }
+
+ @Test
+ void isRechallengingSetRechallenging_roundTrip() {
+ ChallengeAuthenticator authenticator =
+ new ChallengeAuthenticator(null, ChallengeScheme.HTTP_BASIC, "realm");
+ assertTrue(authenticator.isRechallenging());
+ authenticator.setRechallenging(false);
+ assertFalse(authenticator.isRechallenging());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/GroupTestCase.java b/org.restlet/src/test/java/org/restlet/security/GroupTestCase.java
new file mode 100644
index 0000000000..2eb9c1e5c8
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/GroupTestCase.java
@@ -0,0 +1,161 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 java.util.List;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link Group}. */
+class GroupTestCase {
+
+ @Test
+ void defaultConstructor_hasNullNameAndInheritsRoles() {
+ Group group = new Group();
+
+ assertNull(group.getName());
+ assertNull(group.getDescription());
+ assertTrue(group.isInheritingRoles());
+ assertTrue(group.getMemberGroups().isEmpty());
+ assertTrue(group.getMemberUsers().isEmpty());
+ }
+
+ @Test
+ void constructorWithNameAndDescription_setsFieldsAndInheritsRoles() {
+ Group group = new Group("admins", "Administrators group");
+
+ assertEquals("admins", group.getName());
+ assertEquals("Administrators group", group.getDescription());
+ assertTrue(group.isInheritingRoles());
+ }
+
+ @Test
+ void constructorWithInheritingRolesFalse_setsFieldFalse() {
+ Group group = new Group("admins", "Administrators group", false);
+ assertFalse(group.isInheritingRoles());
+ }
+
+ @Test
+ void getNameSetName_roundTrip() {
+ Group group = new Group();
+ group.setName("users");
+ assertEquals("users", group.getName());
+ }
+
+ @Test
+ void getDescriptionSetDescription_roundTrip() {
+ Group group = new Group();
+ group.setDescription("desc");
+ assertEquals("desc", group.getDescription());
+ }
+
+ @Test
+ void isInheritingRolesSetInheritingRoles_roundTrip() {
+ Group group = new Group();
+ group.setInheritingRoles(false);
+ assertFalse(group.isInheritingRoles());
+ }
+
+ @Test
+ void getMemberGroups_isModifiableAndTracksMembership() {
+ Group group = new Group();
+ Group child = new Group("child", null);
+
+ group.getMemberGroups().add(child);
+
+ assertEquals(1, group.getMemberGroups().size());
+ assertSame(child, group.getMemberGroups().get(0));
+ }
+
+ @Test
+ void getMemberUsers_isModifiableAndTracksMembership() {
+ Group group = new Group();
+ User user = new User("bob");
+
+ group.getMemberUsers().add(user);
+
+ assertEquals(1, group.getMemberUsers().size());
+ assertSame(user, group.getMemberUsers().get(0));
+ }
+
+ @Test
+ void setMemberGroups_withNull_clearsList() {
+ Group group = new Group();
+ group.getMemberGroups().add(new Group("child", null));
+
+ group.setMemberGroups(null);
+
+ assertTrue(group.getMemberGroups().isEmpty());
+ }
+
+ @Test
+ void setMemberGroups_withNewList_replacesContents() {
+ Group group = new Group();
+ group.getMemberGroups().add(new Group("old", null));
+ Group newChild = new Group("new", null);
+
+ group.setMemberGroups(List.of(newChild));
+
+ assertEquals(1, group.getMemberGroups().size());
+ assertSame(newChild, group.getMemberGroups().get(0));
+ }
+
+ @Test
+ void setMemberGroups_withSameListInstance_isNoOp() {
+ Group group = new Group();
+ group.getMemberGroups().add(new Group("child", null));
+
+ group.setMemberGroups(group.getMemberGroups());
+
+ assertEquals(1, group.getMemberGroups().size());
+ }
+
+ @Test
+ void setMemberUsers_withNull_clearsList() {
+ Group group = new Group();
+ group.getMemberUsers().add(new User("bob"));
+
+ group.setMemberUsers(null);
+
+ assertTrue(group.getMemberUsers().isEmpty());
+ }
+
+ @Test
+ void setMemberUsers_withNewList_replacesContents() {
+ Group group = new Group();
+ group.getMemberUsers().add(new User("old"));
+ User newUser = new User("new");
+
+ group.setMemberUsers(List.of(newUser));
+
+ assertEquals(1, group.getMemberUsers().size());
+ assertSame(newUser, group.getMemberUsers().get(0));
+ }
+
+ @Test
+ void setMemberUsers_withSameListInstance_isNoOp() {
+ Group group = new Group();
+ group.getMemberUsers().add(new User("bob"));
+
+ group.setMemberUsers(group.getMemberUsers());
+
+ assertEquals(1, group.getMemberUsers().size());
+ }
+
+ @Test
+ void toString_returnsName() {
+ Group group = new Group("admins", null);
+ assertEquals("admins", group.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/LocalVerifierTestCase.java b/org.restlet/src/test/java/org/restlet/security/LocalVerifierTestCase.java
new file mode 100644
index 0000000000..cd608d10d6
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/LocalVerifierTestCase.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link LocalVerifier}. */
+class LocalVerifierTestCase {
+
+ /** Minimal concrete fixture, since {@link LocalVerifier} is abstract. */
+ private static class MapBackedLocalVerifier extends LocalVerifier {
+
+ private final Map secrets = new HashMap<>();
+
+ void putSecret(String identifier, char[] secret) {
+ secrets.put(identifier, secret);
+ }
+
+ @Override
+ public char[] getLocalSecret(String identifier) {
+ return secrets.get(identifier);
+ }
+ }
+
+ @Test
+ void verify_withMatchingSecret_returnsValid() {
+ MapBackedLocalVerifier verifier = new MapBackedLocalVerifier();
+ verifier.putSecret("alice", "secret".toCharArray());
+
+ assertEquals(Verifier.RESULT_VALID, verifier.verify("alice", "secret".toCharArray()));
+ }
+
+ @Test
+ void verify_withNonMatchingSecret_returnsInvalid() {
+ MapBackedLocalVerifier verifier = new MapBackedLocalVerifier();
+ verifier.putSecret("alice", "secret".toCharArray());
+
+ assertEquals(Verifier.RESULT_INVALID, verifier.verify("alice", "wrong".toCharArray()));
+ }
+
+ @Test
+ void verify_withUnknownIdentifier_returnsInvalid() {
+ MapBackedLocalVerifier verifier = new MapBackedLocalVerifier();
+
+ assertEquals(Verifier.RESULT_INVALID, verifier.verify("unknown", "secret".toCharArray()));
+ }
+
+ @Test
+ void getLocalSecret_returnsConfiguredValue() {
+ MapBackedLocalVerifier verifier = new MapBackedLocalVerifier();
+ char[] secret = "secret".toCharArray();
+ verifier.putSecret("alice", secret);
+
+ assertSame(secret, verifier.getLocalSecret("alice"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/MapVerifierTestCase.java b/org.restlet/src/test/java/org/restlet/security/MapVerifierTestCase.java
new file mode 100644
index 0000000000..ac4c2f55ca
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/MapVerifierTestCase.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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 java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+
+/** Unit tests for {@link MapVerifier}. */
+class MapVerifierTestCase {
+
+ @Test
+ void defaultConstructor_hasEmptyLocalSecrets() {
+ MapVerifier verifier = new MapVerifier();
+ assertTrue(verifier.getLocalSecrets().isEmpty());
+ }
+
+ @Test
+ void constructorWithMap_usesProvidedMap() {
+ ConcurrentMap secrets = new ConcurrentHashMap<>();
+ secrets.put("alice", "secret".toCharArray());
+
+ MapVerifier verifier = new MapVerifier(secrets);
+
+ assertSame(secrets, verifier.getLocalSecrets());
+ }
+
+ @Test
+ void getLocalSecret_withNullIdentifier_returnsNull() {
+ MapVerifier verifier = new MapVerifier();
+ assertNull(verifier.getLocalSecret(null));
+ }
+
+ @Test
+ void getLocalSecret_withKnownIdentifier_returnsSecret() {
+ MapVerifier verifier = new MapVerifier();
+ verifier.getLocalSecrets().put("alice", "secret".toCharArray());
+
+ assertEquals("secret", new String(verifier.getLocalSecret("alice")));
+ }
+
+ @Test
+ void getLocalSecret_withUnknownIdentifier_returnsNull() {
+ MapVerifier verifier = new MapVerifier();
+ assertNull(verifier.getLocalSecret("unknown"));
+ }
+
+ @Test
+ void setLocalSecrets_withNull_clearsMap() {
+ MapVerifier verifier = new MapVerifier();
+ verifier.getLocalSecrets().put("alice", "secret".toCharArray());
+
+ verifier.setLocalSecrets(null);
+
+ assertTrue(verifier.getLocalSecrets().isEmpty());
+ }
+
+ @Test
+ void setLocalSecrets_withNewMap_replacesContents() {
+ MapVerifier verifier = new MapVerifier();
+ verifier.getLocalSecrets().put("old", "secret".toCharArray());
+
+ verifier.setLocalSecrets(Map.of("new", "other".toCharArray()));
+
+ assertEquals(1, verifier.getLocalSecrets().size());
+ assertTrue(verifier.getLocalSecrets().containsKey("new"));
+ }
+
+ @Test
+ void setLocalSecrets_withSameMapInstance_isNoOp() {
+ MapVerifier verifier = new MapVerifier();
+ verifier.getLocalSecrets().put("alice", "secret".toCharArray());
+
+ verifier.setLocalSecrets(verifier.getLocalSecrets());
+
+ assertEquals(1, verifier.getLocalSecrets().size());
+ }
+
+ @Test
+ void verifyIdentifierSecret_withMatchingSecret_returnsValid() {
+ MapVerifier verifier = new MapVerifier();
+ verifier.getLocalSecrets().put("alice", "secret".toCharArray());
+
+ assertEquals(Verifier.RESULT_VALID, verifier.verify("alice", "secret".toCharArray()));
+ }
+
+ @Test
+ void verifyIdentifierSecret_withWrongSecret_returnsInvalid() {
+ MapVerifier verifier = new MapVerifier();
+ verifier.getLocalSecrets().put("alice", "secret".toCharArray());
+
+ assertEquals(Verifier.RESULT_INVALID, verifier.verify("alice", "wrong".toCharArray()));
+ }
+
+ @Test
+ void verifyRequestResponse_withMissingChallengeResponse_returnsMissing() {
+ MapVerifier verifier = new MapVerifier();
+ Request request = new Request();
+ Response response = new Response(request);
+
+ assertEquals(Verifier.RESULT_MISSING, verifier.verify(request, response));
+ }
+
+ @Test
+ void verifyRequestResponse_withValidCredentials_setsUserOnClientInfo() {
+ MapVerifier verifier = new MapVerifier();
+ verifier.getLocalSecrets().put("alice", "secret".toCharArray());
+ Request request = new Request();
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "alice", "secret"));
+ Response response = new Response(request);
+
+ int result = verifier.verify(request, response);
+
+ assertEquals(Verifier.RESULT_VALID, result);
+ assertEquals("alice", request.getClientInfo().getUser().getIdentifier());
+ }
+
+ @Test
+ void verifyRequestResponse_withInvalidCredentials_doesNotSetUser() {
+ MapVerifier verifier = new MapVerifier();
+ verifier.getLocalSecrets().put("alice", "secret".toCharArray());
+ Request request = new Request();
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "alice", "wrong"));
+ Response response = new Response(request);
+
+ int result = verifier.verify(request, response);
+
+ assertEquals(Verifier.RESULT_INVALID, result);
+ assertNull(request.getClientInfo().getUser());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/RealmTestCase.java b/org.restlet/src/test/java/org/restlet/security/RealmTestCase.java
new file mode 100644
index 0000000000..b9ddf49b77
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/RealmTestCase.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 java.util.concurrent.CopyOnWriteArrayList;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Parameter;
+import org.restlet.util.Series;
+
+/** Unit tests for {@link Realm}. */
+class RealmTestCase {
+
+ /** Minimal concrete fixture, since {@link Realm} is abstract. */
+ private static class TestRealm extends Realm {
+
+ TestRealm() {
+ super();
+ }
+
+ TestRealm(Verifier verifier, Enroler enroler) {
+ super(verifier, enroler);
+ }
+ }
+
+ private static final Verifier NO_OP_VERIFIER = (request, response) -> Verifier.RESULT_VALID;
+
+ private static final Enroler NO_OP_ENROLER = clientInfo -> {};
+
+ @Test
+ void defaultConstructor_hasNullNameVerifierAndEnroler() {
+ TestRealm realm = new TestRealm();
+
+ assertNull(realm.getName());
+ assertNull(realm.getVerifier());
+ assertNull(realm.getEnroler());
+ assertTrue(realm.getParameters().isEmpty());
+ assertFalse(realm.isStarted());
+ assertTrue(realm.isStopped());
+ }
+
+ @Test
+ void constructorWithVerifierAndEnroler_setsFields() {
+ TestRealm realm = new TestRealm(NO_OP_VERIFIER, NO_OP_ENROLER);
+
+ assertSame(NO_OP_VERIFIER, realm.getVerifier());
+ assertSame(NO_OP_ENROLER, realm.getEnroler());
+ }
+
+ @Test
+ void getNameSetName_roundTrip() {
+ TestRealm realm = new TestRealm();
+ realm.setName("my-realm");
+ assertEquals("my-realm", realm.getName());
+ }
+
+ @Test
+ void getVerifierSetVerifier_roundTrip() {
+ TestRealm realm = new TestRealm();
+ realm.setVerifier(NO_OP_VERIFIER);
+ assertSame(NO_OP_VERIFIER, realm.getVerifier());
+ }
+
+ @Test
+ void getEnrolerSetEnroler_roundTrip() {
+ TestRealm realm = new TestRealm();
+ realm.setEnroler(NO_OP_ENROLER);
+ assertSame(NO_OP_ENROLER, realm.getEnroler());
+ }
+
+ @Test
+ void getParameters_isModifiable() {
+ TestRealm realm = new TestRealm();
+ realm.getParameters().add("name", "value");
+ assertEquals(1, realm.getParameters().size());
+ }
+
+ @Test
+ void setParameters_withNull_clearsSeries() {
+ TestRealm realm = new TestRealm();
+ realm.getParameters().add("name", "value");
+
+ realm.setParameters(null);
+
+ assertTrue(realm.getParameters().isEmpty());
+ }
+
+ @Test
+ void setParameters_withNewSeries_replacesContents() {
+ TestRealm realm = new TestRealm();
+ realm.getParameters().add("old", "value");
+
+ Series newParameters =
+ new Series<>(Parameter.class, new CopyOnWriteArrayList());
+ newParameters.add("new", "value");
+ realm.setParameters(newParameters);
+
+ assertEquals(1, realm.getParameters().size());
+ assertEquals("new", realm.getParameters().get(0).getName());
+ }
+
+ @Test
+ void setParameters_withSameSeriesInstance_isNoOp() {
+ TestRealm realm = new TestRealm();
+ realm.getParameters().add("name", "value");
+
+ realm.setParameters(realm.getParameters());
+
+ assertEquals(1, realm.getParameters().size());
+ }
+
+ @Test
+ void startStop_updateStartedAndStoppedState() throws Exception {
+ TestRealm realm = new TestRealm();
+
+ realm.start();
+ assertTrue(realm.isStarted());
+ assertFalse(realm.isStopped());
+
+ realm.stop();
+ assertFalse(realm.isStarted());
+ assertTrue(realm.isStopped());
+ }
+
+ @Test
+ void toString_returnsName() {
+ TestRealm realm = new TestRealm();
+ realm.setName("my-realm");
+ assertEquals("my-realm", realm.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/RoleAuthorizerTestCase.java b/org.restlet/src/test/java/org/restlet/security/RoleAuthorizerTestCase.java
new file mode 100644
index 0000000000..b8d8e2336c
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/RoleAuthorizerTestCase.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+
+/** Unit tests for {@link RoleAuthorizer}. */
+class RoleAuthorizerTestCase {
+
+ private Request requestWithRoles(Role... roles) {
+ Request request = new Request();
+ request.getClientInfo().getRoles().addAll(List.of(roles));
+ return request;
+ }
+
+ @Test
+ void defaultConstructor_hasEmptyRoleListsAndNullIdentifier() {
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+
+ assertNull(authorizer.getIdentifier());
+ assertTrue(authorizer.getAuthorizedRoles().isEmpty());
+ assertTrue(authorizer.getForbiddenRoles().isEmpty());
+ }
+
+ @Test
+ void constructorWithIdentifier_setsIdentifierAndEmptyLists() {
+ RoleAuthorizer authorizer = new RoleAuthorizer("my-id");
+
+ assertEquals("my-id", authorizer.getIdentifier());
+ assertTrue(authorizer.getAuthorizedRoles().isEmpty());
+ assertTrue(authorizer.getForbiddenRoles().isEmpty());
+ }
+
+ @Test
+ void authorize_withNoAuthorizedRolesConfigured_returnsTrue() {
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ Request request = requestWithRoles();
+
+ assertTrue(authorizer.authorize(request, new Response(request)));
+ }
+
+ @Test
+ void authorize_withMatchingAuthorizedRole_returnsTrue() {
+ Role adminRole = new Role(null, "admin", null);
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ authorizer.getAuthorizedRoles().add(adminRole);
+
+ Request request = requestWithRoles(adminRole);
+
+ assertTrue(authorizer.authorize(request, new Response(request)));
+ }
+
+ @Test
+ void authorize_withoutMatchingAuthorizedRole_returnsFalse() {
+ Role adminRole = new Role(null, "admin", null);
+ Role userRole = new Role(null, "user", null);
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ authorizer.getAuthorizedRoles().add(adminRole);
+
+ Request request = requestWithRoles(userRole);
+
+ assertFalse(authorizer.authorize(request, new Response(request)));
+ }
+
+ @Test
+ void authorize_withMatchingForbiddenRole_returnsFalseEvenWithoutAuthorizedRoles() {
+ Role bannedRole = new Role(null, "banned", null);
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ authorizer.getForbiddenRoles().add(bannedRole);
+
+ Request request = requestWithRoles(bannedRole);
+
+ assertFalse(authorizer.authorize(request, new Response(request)));
+ }
+
+ @Test
+ void authorize_withAuthorizedAndForbiddenRole_returnsFalse() {
+ Role adminRole = new Role(null, "admin", null);
+ Role bannedRole = new Role(null, "banned", null);
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ authorizer.getAuthorizedRoles().add(adminRole);
+ authorizer.getForbiddenRoles().add(bannedRole);
+
+ Request request = requestWithRoles(adminRole, bannedRole);
+
+ assertFalse(authorizer.authorize(request, new Response(request)));
+ }
+
+ @Test
+ void setAuthorizedRoles_withNull_clearsList() {
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ authorizer.getAuthorizedRoles().add(new Role(null, "admin", null));
+
+ authorizer.setAuthorizedRoles(null);
+
+ assertTrue(authorizer.getAuthorizedRoles().isEmpty());
+ }
+
+ @Test
+ void setAuthorizedRoles_withSameListInstance_isNoOp() {
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ authorizer.getAuthorizedRoles().add(new Role(null, "admin", null));
+
+ authorizer.setAuthorizedRoles(authorizer.getAuthorizedRoles());
+
+ assertEquals(1, authorizer.getAuthorizedRoles().size());
+ }
+
+ @Test
+ void setForbiddenRoles_withNull_clearsList() {
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ authorizer.getForbiddenRoles().add(new Role(null, "banned", null));
+
+ authorizer.setForbiddenRoles(null);
+
+ assertTrue(authorizer.getForbiddenRoles().isEmpty());
+ }
+
+ @Test
+ void setForbiddenRoles_withSameListInstance_isNoOp() {
+ RoleAuthorizer authorizer = new RoleAuthorizer();
+ authorizer.getForbiddenRoles().add(new Role(null, "banned", null));
+
+ authorizer.setForbiddenRoles(authorizer.getForbiddenRoles());
+
+ assertEquals(1, authorizer.getForbiddenRoles().size());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/SecretVerifierTestCase.java b/org.restlet/src/test/java/org/restlet/security/SecretVerifierTestCase.java
new file mode 100644
index 0000000000..c4b6f1c03b
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/SecretVerifierTestCase.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+
+/** Unit tests for {@link SecretVerifier}. */
+class SecretVerifierTestCase {
+
+ /** Minimal concrete fixture, since {@link SecretVerifier} is abstract. */
+ private static class TestSecretVerifier extends SecretVerifier {
+
+ @Override
+ public int verify(String identifier, char[] secret) {
+ return "alice".equals(identifier) && compare(secret, "secret".toCharArray())
+ ? RESULT_VALID
+ : RESULT_INVALID;
+ }
+ }
+
+ @Test
+ void compare_withEqualSecrets_returnsTrue() {
+ assertTrue(SecretVerifier.compare("secret".toCharArray(), "secret".toCharArray()));
+ }
+
+ @Test
+ void compare_withDifferentSecrets_returnsFalse() {
+ assertFalse(SecretVerifier.compare("secret".toCharArray(), "other!".toCharArray()));
+ }
+
+ @Test
+ void compare_withDifferentLengths_returnsFalse() {
+ assertFalse(SecretVerifier.compare("secret".toCharArray(), "short".toCharArray()));
+ }
+
+ @Test
+ void compare_withOneNullSecret_returnsFalse() {
+ assertFalse(SecretVerifier.compare(null, "secret".toCharArray()));
+ assertFalse(SecretVerifier.compare("secret".toCharArray(), null));
+ }
+
+ @Test
+ void compare_withBothNullSecrets_returnsFalse() {
+ assertFalse(SecretVerifier.compare(null, null));
+ }
+
+ @Test
+ void createUser_returnsUserWithGivenIdentifier() {
+ TestSecretVerifier verifier = new TestSecretVerifier();
+ Request request = new Request();
+ Response response = new Response(request);
+
+ User user = verifier.createUser("alice", request, response);
+
+ assertEquals("alice", user.getIdentifier());
+ }
+
+ @Test
+ void getIdentifier_extractsFromChallengeResponse() {
+ TestSecretVerifier verifier = new TestSecretVerifier();
+ Request request = new Request();
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "alice", "secret"));
+ Response response = new Response(request);
+
+ assertEquals("alice", verifier.getIdentifier(request, response));
+ }
+
+ @Test
+ void getSecret_extractsFromChallengeResponse() {
+ TestSecretVerifier verifier = new TestSecretVerifier();
+ Request request = new Request();
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "alice", "secret"));
+ Response response = new Response(request);
+
+ assertEquals("secret", new String(verifier.getSecret(request, response)));
+ }
+
+ @Test
+ void verify_withoutChallengeResponse_returnsMissing() {
+ TestSecretVerifier verifier = new TestSecretVerifier();
+ Request request = new Request();
+ Response response = new Response(request);
+
+ assertEquals(Verifier.RESULT_MISSING, verifier.verify(request, response));
+ }
+
+ @Test
+ void verify_withValidCredentials_setsUserOnClientInfo() {
+ TestSecretVerifier verifier = new TestSecretVerifier();
+ Request request = new Request();
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "alice", "secret"));
+ Response response = new Response(request);
+
+ int result = verifier.verify(request, response);
+
+ assertEquals(Verifier.RESULT_VALID, result);
+ assertEquals("alice", request.getClientInfo().getUser().getIdentifier());
+ }
+
+ @Test
+ void verify_withInvalidCredentials_doesNotSetUser() {
+ TestSecretVerifier verifier = new TestSecretVerifier();
+ Request request = new Request();
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "alice", "wrong"));
+ Response response = new Response(request);
+
+ int result = verifier.verify(request, response);
+
+ assertEquals(Verifier.RESULT_INVALID, result);
+ assertNull(request.getClientInfo().getUser());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/security/UserTestCase.java b/org.restlet/src/test/java/org/restlet/security/UserTestCase.java
new file mode 100644
index 0000000000..4cf6164610
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/security/UserTestCase.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.security;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link User}. */
+class UserTestCase {
+
+ @Test
+ void defaultConstructor_hasNullFields() {
+ User user = new User();
+
+ assertNull(user.getIdentifier());
+ assertNull(user.getSecret());
+ assertNull(user.getFirstName());
+ assertNull(user.getLastName());
+ assertNull(user.getEmail());
+ }
+
+ @Test
+ void constructorWithIdentifier_setsIdentifierOnly() {
+ User user = new User("alice");
+
+ assertEquals("alice", user.getIdentifier());
+ assertNull(user.getSecret());
+ }
+
+ @Test
+ void constructorWithIdentifierAndCharSecret_setsBoth() {
+ char[] secret = "secret".toCharArray();
+ User user = new User("alice", secret);
+
+ assertEquals("alice", user.getIdentifier());
+ assertArrayEquals(secret, user.getSecret());
+ }
+
+ @Test
+ void constructorWithIdentifierAndStringSecret_convertsToCharArray() {
+ User user = new User("alice", "secret");
+
+ assertEquals("alice", user.getIdentifier());
+ assertArrayEquals("secret".toCharArray(), user.getSecret());
+ }
+
+ @Test
+ void constructorWithAllFieldsAndCharSecret_setsAllFields() {
+ char[] secret = "secret".toCharArray();
+ User user = new User("alice", secret, "Alice", "Wonderland", "alice@example.com");
+
+ assertEquals("alice", user.getIdentifier());
+ assertArrayEquals(secret, user.getSecret());
+ assertEquals("Alice", user.getFirstName());
+ assertEquals("Wonderland", user.getLastName());
+ assertEquals("alice@example.com", user.getEmail());
+ }
+
+ @Test
+ void constructorWithAllFieldsAndStringSecret_setsAllFields() {
+ User user = new User("alice", "secret", "Alice", "Wonderland", "alice@example.com");
+
+ assertEquals("alice", user.getIdentifier());
+ assertArrayEquals("secret".toCharArray(), user.getSecret());
+ assertEquals("Alice", user.getFirstName());
+ assertEquals("Wonderland", user.getLastName());
+ assertEquals("alice@example.com", user.getEmail());
+ }
+
+ @Test
+ void getIdentifierSetIdentifier_roundTrip() {
+ User user = new User();
+ user.setIdentifier("bob");
+ assertEquals("bob", user.getIdentifier());
+ }
+
+ @Test
+ void getSecretSetSecret_roundTrip() {
+ User user = new User();
+ char[] secret = "secret".toCharArray();
+ user.setSecret(secret);
+ assertArrayEquals(secret, user.getSecret());
+ }
+
+ @Test
+ void getFirstNameSetFirstName_roundTrip() {
+ User user = new User();
+ user.setFirstName("Bob");
+ assertEquals("Bob", user.getFirstName());
+ }
+
+ @Test
+ void getLastNameSetLastName_roundTrip() {
+ User user = new User();
+ user.setLastName("Builder");
+ assertEquals("Builder", user.getLastName());
+ }
+
+ @Test
+ void getEmailSetEmail_roundTrip() {
+ User user = new User();
+ user.setEmail("bob@example.com");
+ assertEquals("bob@example.com", user.getEmail());
+ }
+
+ @Test
+ void getName_returnsIdentifier() {
+ User user = new User("bob");
+ assertEquals("bob", user.getName());
+ }
+
+ @Test
+ void toString_returnsIdentifier() {
+ User user = new User("bob");
+ assertEquals("bob", user.toString());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/ConnectorServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/ConnectorServiceTestCase.java
new file mode 100644
index 0000000000..e08094046f
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/ConnectorServiceTestCase.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Protocol;
+import org.restlet.representation.StringRepresentation;
+
+/** Unit tests for {@link ConnectorService}. */
+class ConnectorServiceTestCase {
+
+ @Test
+ void defaultConstructor_hasEmptyProtocolLists() {
+ ConnectorService service = new ConnectorService();
+
+ assertTrue(service.getClientProtocols().isEmpty());
+ assertTrue(service.getServerProtocols().isEmpty());
+ }
+
+ @Test
+ void setClientProtocols_replacesListContents() {
+ ConnectorService service = new ConnectorService();
+ service.getClientProtocols().add(Protocol.FTP);
+
+ service.setClientProtocols(Arrays.asList(Protocol.HTTP, Protocol.HTTPS));
+
+ assertEquals(2, service.getClientProtocols().size());
+ assertTrue(service.getClientProtocols().contains(Protocol.HTTP));
+ assertTrue(service.getClientProtocols().contains(Protocol.HTTPS));
+ }
+
+ @Test
+ void setClientProtocols_withNull_clearsList() {
+ ConnectorService service = new ConnectorService();
+ service.getClientProtocols().add(Protocol.HTTP);
+
+ service.setClientProtocols(null);
+
+ assertTrue(service.getClientProtocols().isEmpty());
+ }
+
+ @Test
+ void setServerProtocols_replacesListContents() {
+ ConnectorService service = new ConnectorService();
+ service.getServerProtocols().add(Protocol.FTP);
+
+ service.setServerProtocols(Arrays.asList(Protocol.HTTP));
+
+ assertEquals(1, service.getServerProtocols().size());
+ assertTrue(service.getServerProtocols().contains(Protocol.HTTP));
+ }
+
+ @Test
+ void setServerProtocols_withNull_clearsList() {
+ ConnectorService service = new ConnectorService();
+ service.getServerProtocols().add(Protocol.HTTP);
+
+ service.setServerProtocols(null);
+
+ assertTrue(service.getServerProtocols().isEmpty());
+ }
+
+ @Test
+ void setServerProtocols_withSameListReference_isNoOp() {
+ ConnectorService service = new ConnectorService();
+ service.getServerProtocols().add(Protocol.HTTP);
+ List same = service.getServerProtocols();
+
+ service.setServerProtocols(same);
+
+ assertEquals(1, service.getServerProtocols().size());
+ }
+
+ @Test
+ void beforeSendAndAfterSend_doNotThrow() {
+ ConnectorService service = new ConnectorService();
+
+ service.beforeSend(new StringRepresentation("test"));
+ service.afterSend(new StringRepresentation("test"));
+ service.beforeSend(null);
+ service.afterSend(null);
+ }
+
+ @Test
+ void getClientProtocols_returnsModifiableList() {
+ ConnectorService service = new ConnectorService();
+
+ service.getClientProtocols().addAll(new ArrayList<>(List.of(Protocol.HTTP)));
+
+ assertEquals(1, service.getClientProtocols().size());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/ConverterServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/ConverterServiceTestCase.java
new file mode 100644
index 0000000000..62308c87ca
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/ConverterServiceTestCase.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.data.Preference;
+import org.restlet.engine.Engine;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.Variant;
+
+/** Unit tests for {@link ConverterService}. */
+class ConverterServiceTestCase {
+
+ @BeforeEach
+ void setUp() {
+ Engine.register();
+ }
+
+ @AfterEach
+ void tearDown() {
+ Engine.clearThreadLocalVariables();
+ }
+
+ @Test
+ void constructors_createUsableInstances() {
+ assertNotNull(new ConverterService());
+ ConverterService disabled = new ConverterService(false);
+ assertEquals(false, disabled.isEnabled());
+ }
+
+ @Test
+ void toRepresentation_convertsStringToStringRepresentation() throws Exception {
+ ConverterService service = new ConverterService();
+
+ Representation representation = service.toRepresentation("hello");
+
+ assertNotNull(representation);
+ assertEquals("hello", representation.getText());
+ assertEquals(MediaType.TEXT_PLAIN, representation.getMediaType());
+ }
+
+ @Test
+ void toRepresentation_withTargetMediaType_usesRequestedType() throws Exception {
+ ConverterService service = new ConverterService();
+
+ Representation representation = service.toRepresentation("hello", MediaType.TEXT_HTML);
+
+ assertNotNull(representation);
+ assertEquals(MediaType.TEXT_HTML, representation.getMediaType());
+ }
+
+ @Test
+ void toObject_convertsRepresentationToString() throws Exception {
+ ConverterService service = new ConverterService();
+ Representation source = new StringRepresentation("test content", MediaType.TEXT_PLAIN);
+
+ Object result = service.toObject(source, String.class, null);
+
+ assertEquals("test content", result);
+ }
+
+ @Test
+ void toObject_withEmptyRepresentation_returnsNull() throws Exception {
+ ConverterService service = new ConverterService();
+
+ Object result = service.toObject(null);
+
+ assertNull(result);
+ }
+
+ @Test
+ void getObjectClasses_forTextVariant_includesString() {
+ ConverterService service = new ConverterService();
+ Variant variant = new Variant(MediaType.TEXT_PLAIN);
+
+ List> classes = service.getObjectClasses(variant);
+
+ assertNotNull(classes);
+ assertEquals(true, classes.contains(String.class));
+ }
+
+ @Test
+ void getVariants_forStringSource_returnsNonEmptyList() throws Exception {
+ ConverterService service = new ConverterService();
+
+ List extends Variant> variants =
+ service.getVariants(String.class, new Variant(MediaType.TEXT_PLAIN));
+
+ assertNotNull(variants);
+ }
+
+ @Test
+ void applyPatchCreatePatchRevertPatch_returnNullByDefault() throws Exception {
+ ConverterService service = new ConverterService();
+ Representation initial = new StringRepresentation("initial");
+ Representation modified = new StringRepresentation("modified");
+
+ assertNull(service.applyPatch(initial, modified));
+ assertNull(service.createPatch(initial, modified));
+ assertNull(service.revertPatch(modified, initial));
+ }
+
+ @Test
+ void getPatchTypes_returnsNullByDefault() {
+ ConverterService service = new ConverterService();
+
+ assertNull(service.getPatchTypes(MediaType.TEXT_PLAIN));
+ }
+
+ @Test
+ void updatePreferences_doesNotThrowForStringEntity() {
+ ConverterService service = new ConverterService();
+ List> preferences = new ArrayList<>();
+
+ service.updatePreferences(preferences, String.class);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/DecoderServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/DecoderServiceTestCase.java
new file mode 100644
index 0000000000..87119835dd
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/DecoderServiceTestCase.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.engine.application.Decoder;
+import org.restlet.routing.Filter;
+
+/** Unit tests for {@link DecoderService}. */
+class DecoderServiceTestCase {
+
+ @Test
+ void defaultConstructor_isEnabled() {
+ DecoderService service = new DecoderService();
+ assertTrue(service.isEnabled());
+ }
+
+ @Test
+ void constructorWithFlag_setsEnabled() {
+ DecoderService service = new DecoderService(false);
+ assertTrue(!service.isEnabled());
+ }
+
+ @Test
+ void createInboundFilter_returnsDecoder() {
+ DecoderService service = new DecoderService();
+
+ Filter filter = service.createInboundFilter(new Context());
+
+ assertNotNull(filter);
+ assertTrue(filter instanceof Decoder);
+ }
+
+ @Test
+ void createOutboundFilter_returnsDecoder() {
+ DecoderService service = new DecoderService();
+
+ Filter filter = service.createOutboundFilter(new Context());
+
+ assertNotNull(filter);
+ assertTrue(filter instanceof Decoder);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/EncoderServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/EncoderServiceTestCase.java
new file mode 100644
index 0000000000..51a40f1252
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/EncoderServiceTestCase.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.data.MediaType;
+import org.restlet.engine.application.Encoder;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.routing.Filter;
+
+/** Unit tests for {@link EncoderService}. */
+class EncoderServiceTestCase {
+
+ @Test
+ void defaultConstructor_isEnabledWithDefaultMinimumSize() {
+ EncoderService service = new EncoderService();
+
+ assertTrue(service.isEnabled());
+ assertEquals(EncoderService.DEFAULT_MINIMUM_SIZE, service.getMinimumSize());
+ assertFalse(service.getAcceptedMediaTypes().isEmpty());
+ assertFalse(service.getIgnoredMediaTypes().isEmpty());
+ }
+
+ @Test
+ void constructorWithFlag_setsEnabled() {
+ EncoderService service = new EncoderService(false);
+ assertFalse(service.isEnabled());
+ }
+
+ @Test
+ void canEncode_withLargeAcceptedRepresentation_returnsTrue() {
+ EncoderService service = new EncoderService();
+ service.setMinimumSize(EncoderService.ANY_SIZE);
+ StringRepresentation representation =
+ new StringRepresentation("some text", MediaType.TEXT_PLAIN);
+
+ assertTrue(service.canEncode(representation));
+ }
+
+ @Test
+ void canEncode_withTooSmallRepresentation_returnsFalse() {
+ EncoderService service = new EncoderService();
+ service.setMinimumSize(1_000_000);
+ StringRepresentation representation =
+ new StringRepresentation("small", MediaType.TEXT_PLAIN);
+
+ assertFalse(service.canEncode(representation));
+ }
+
+ @Test
+ void canEncode_withIgnoredMediaType_returnsFalse() {
+ EncoderService service = new EncoderService();
+ service.setMinimumSize(EncoderService.ANY_SIZE);
+ StringRepresentation representation = new StringRepresentation("data", MediaType.IMAGE_PNG);
+
+ assertFalse(service.canEncode(representation));
+ }
+
+ @Test
+ void canEncode_withNullRepresentation_returnsFalse() {
+ EncoderService service = new EncoderService();
+
+ assertFalse(service.canEncode(null));
+ }
+
+ @Test
+ void getMinimumSizeAndSetMinimumSize_roundTrip() {
+ EncoderService service = new EncoderService();
+
+ service.setMinimumSize(2048);
+
+ assertEquals(2048, service.getMinimumSize());
+ }
+
+ @Test
+ void createInboundFilter_returnsEncoder() {
+ EncoderService service = new EncoderService();
+
+ Filter filter = service.createInboundFilter(new Context());
+
+ assertNotNull(filter);
+ assertTrue(filter instanceof Encoder);
+ }
+
+ @Test
+ void createOutboundFilter_returnsEncoder() {
+ EncoderService service = new EncoderService();
+
+ Filter filter = service.createOutboundFilter(new Context());
+
+ assertNotNull(filter);
+ assertTrue(filter instanceof Encoder);
+ }
+
+ @Test
+ void getDefaultAcceptedMediaTypes_containsAll() {
+ assertTrue(EncoderService.getDefaultAcceptedMediaTypes().contains(MediaType.ALL));
+ }
+
+ @Test
+ void getDefaultIgnoredMediaTypes_containsImages() {
+ assertTrue(EncoderService.getDefaultIgnoredMediaTypes().contains(MediaType.IMAGE_ALL));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/LogServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/LogServiceTestCase.java
new file mode 100644
index 0000000000..a5df7f85e8
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/LogServiceTestCase.java
@@ -0,0 +1,159 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.Method;
+import org.restlet.data.Status;
+import org.restlet.engine.log.LogFilter;
+import org.restlet.routing.Filter;
+import org.restlet.routing.Template;
+
+/** Unit tests for {@link LogService}. */
+class LogServiceTestCase {
+
+ @Test
+ void defaultConstructor_isEnabledWithNullDefaults() {
+ LogService service = new LogService();
+
+ assertTrue(service.isEnabled());
+ assertNull(service.getLoggableTemplate());
+ assertNull(service.getLoggerName());
+ assertNull(service.getResponseLogFormat());
+ assertNull(service.getLogPropertiesRef());
+ assertFalse(service.isIdentityCheck());
+ }
+
+ @Test
+ void constructorWithFlag_setsEnabled() {
+ LogService service = new LogService(false);
+ assertFalse(service.isEnabled());
+ }
+
+ @Test
+ void createInboundFilter_returnsLogFilter() {
+ LogService service = new LogService();
+
+ Filter filter = service.createInboundFilter(new Context());
+
+ assertNotNull(filter);
+ assertTrue(filter instanceof LogFilter);
+ }
+
+ @Test
+ void isLoggable_withoutTemplate_returnsTrue() {
+ LogService service = new LogService();
+ Request request = new Request(Method.GET, "http://localhost/foo");
+
+ assertTrue(service.isLoggable(request));
+ }
+
+ @Test
+ void isLoggable_withMatchingTemplate_returnsTrue() {
+ LogService service = new LogService();
+ service.setLoggableTemplate("http://localhost/foo");
+ Request request = new Request(Method.GET, "http://localhost/foo");
+
+ assertTrue(service.isLoggable(request));
+ }
+
+ @Test
+ void isLoggable_withNonMatchingTemplate_returnsFalse() {
+ LogService service = new LogService();
+ service.setLoggableTemplate("/bar");
+ Request request = new Request(Method.GET, "http://localhost/foo");
+
+ assertFalse(service.isLoggable(request));
+ }
+
+ @Test
+ void setLoggableTemplate_withNullString_clearsTemplate() {
+ LogService service = new LogService();
+ service.setLoggableTemplate("/foo");
+
+ service.setLoggableTemplate((String) null);
+
+ assertNull(service.getLoggableTemplate());
+ }
+
+ @Test
+ void setLoggableTemplate_withTemplateInstance_roundTrips() {
+ LogService service = new LogService();
+ Template template = new Template("/foo");
+
+ service.setLoggableTemplate(template);
+
+ assertEquals(template, service.getLoggableTemplate());
+ }
+
+ @Test
+ void gettersAndSetters_roundTrip() {
+ LogService service = new LogService();
+
+ service.setIdentityCheck(true);
+ assertTrue(service.isIdentityCheck());
+
+ service.setLoggerName("my.logger");
+ assertEquals("my.logger", service.getLoggerName());
+
+ service.setLogPropertiesRef("file:///tmp/log.properties");
+ assertEquals("file:///tmp/log.properties", service.getLogPropertiesRef().toString());
+
+ service.setResponseLogFormat("{m}");
+ assertEquals("{m}", service.getResponseLogFormat());
+ }
+
+ @Test
+ void getResponseLogMessage_withoutFormat_usesDefaultFormat() {
+ LogService service = new LogService();
+ Request request = new Request(Method.GET, "http://localhost/foo?bar=baz");
+ Response response = new Response(request);
+ response.setStatus(Status.SUCCESS_OK);
+
+ String message = service.getResponseLogMessage(response, 42);
+
+ assertNotNull(message);
+ assertTrue(message.contains("GET"));
+ assertTrue(message.contains("200"));
+ assertTrue(message.contains("42"));
+ }
+
+ @Test
+ void getResponseLogMessage_afterStartWithCustomFormat_usesTemplate() throws Exception {
+ LogService service = new LogService();
+ service.setResponseLogFormat("{m}");
+ service.start();
+
+ Request request = new Request(Method.GET, "http://localhost/foo");
+ Response response = new Response(request);
+ response.setStatus(Status.SUCCESS_OK);
+
+ String message = service.getResponseLogMessage(response, 10);
+
+ assertEquals("GET", message);
+ }
+
+ @Test
+ void start_withoutLogPropertiesRef_startsNormally() throws Exception {
+ LogService service = new LogService();
+
+ service.start();
+
+ assertTrue(service.isStarted());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/RangeServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/RangeServiceTestCase.java
new file mode 100644
index 0000000000..1631bc8157
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/RangeServiceTestCase.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.engine.application.RangeFilter;
+import org.restlet.routing.Filter;
+
+/** Unit tests for {@link RangeService}. */
+class RangeServiceTestCase {
+
+ @Test
+ void defaultConstructor_isEnabled() {
+ RangeService service = new RangeService();
+ assertTrue(service.isEnabled());
+ }
+
+ @Test
+ void constructorWithFlag_setsEnabled() {
+ RangeService service = new RangeService(false);
+ assertFalse(service.isEnabled());
+ }
+
+ @Test
+ void createInboundFilter_returnsRangeFilter() {
+ RangeService service = new RangeService();
+
+ Filter filter = service.createInboundFilter(new Context());
+
+ assertNotNull(filter);
+ assertTrue(filter instanceof RangeFilter);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/ServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/ServiceTestCase.java
new file mode 100644
index 0000000000..506bad5f71
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/ServiceTestCase.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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 org.junit.jupiter.api.Test;
+import org.restlet.Context;
+
+/** Unit tests for {@link Service}. */
+class ServiceTestCase {
+
+ /** Minimal concrete fixture, since {@link Service} is abstract. */
+ private static class TestService extends Service {
+ TestService() {
+ super();
+ }
+
+ TestService(boolean enabled) {
+ super(enabled);
+ }
+ }
+
+ @Test
+ void defaultConstructor_isEnabledAndNotStarted() {
+ TestService service = new TestService();
+
+ assertTrue(service.isEnabled());
+ assertFalse(service.isStarted());
+ assertTrue(service.isStopped());
+ assertNull(service.getContext());
+ }
+
+ @Test
+ void constructorWithFlag_setsEnabled() {
+ TestService disabled = new TestService(false);
+ assertFalse(disabled.isEnabled());
+ }
+
+ @Test
+ void setContext_roundTrip() {
+ TestService service = new TestService();
+ Context context = new Context();
+
+ service.setContext(context);
+
+ assertSame(context, service.getContext());
+ }
+
+ @Test
+ void setEnabled_roundTrip() {
+ TestService service = new TestService();
+
+ service.setEnabled(false);
+
+ assertFalse(service.isEnabled());
+ }
+
+ @Test
+ void start_whenEnabled_setsStarted() throws Exception {
+ TestService service = new TestService();
+
+ service.start();
+
+ assertTrue(service.isStarted());
+ assertFalse(service.isStopped());
+ }
+
+ @Test
+ void start_whenDisabled_staysStopped() throws Exception {
+ TestService service = new TestService(false);
+
+ service.start();
+
+ assertFalse(service.isStarted());
+ assertTrue(service.isStopped());
+ }
+
+ @Test
+ void stop_afterStart_setsStopped() throws Exception {
+ TestService service = new TestService();
+ service.start();
+
+ service.stop();
+
+ assertFalse(service.isStarted());
+ assertTrue(service.isStopped());
+ }
+
+ @Test
+ void createInboundFilter_returnsNullByDefault() {
+ TestService service = new TestService();
+
+ assertNull(service.createInboundFilter(new Context()));
+ }
+
+ @Test
+ void createOutboundFilter_returnsNullByDefault() {
+ TestService service = new TestService();
+
+ assertNull(service.createOutboundFilter(new Context()));
+ }
+
+ @Test
+ void concreteSubclass_logService_isAlsoAService() {
+ LogService logService = new LogService();
+ assertTrue(logService.isEnabled());
+ }
+
+ @Test
+ void concreteSubclass_encoderService_isAlsoAService() {
+ EncoderService encoderService = new EncoderService();
+ assertTrue(encoderService.isEnabled());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/TaskServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/TaskServiceTestCase.java
new file mode 100644
index 0000000000..5d00021ca4
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/TaskServiceTestCase.java
@@ -0,0 +1,281 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link TaskService}. */
+class TaskServiceTestCase {
+
+ private TaskService taskService;
+
+ @AfterEach
+ void tearDown() throws Exception {
+ if (taskService != null) {
+ taskService.setShutdownAllowed(true);
+ taskService.shutdownNow();
+ taskService.awaitTermination(2, TimeUnit.SECONDS);
+ }
+ }
+
+ @Test
+ void defaultConstructor_isEnabledWithFourThreads() {
+ taskService = new TaskService();
+
+ assertTrue(taskService.isEnabled());
+ assertEquals(4, taskService.getCorePoolSize());
+ assertTrue(taskService.isDaemon());
+ assertFalse(taskService.isShutdownAllowed());
+ }
+
+ @Test
+ void constructorWithCorePoolSize_setsSize() {
+ taskService = new TaskService(2);
+
+ assertEquals(2, taskService.getCorePoolSize());
+ }
+
+ @Test
+ void constructorWithEnabledAndDaemon_setsDaemonFlag() {
+ taskService = new TaskService(true, false);
+
+ assertFalse(taskService.isDaemon());
+ }
+
+ @Test
+ void setCorePoolSizeAndSetDaemon_roundTrip() {
+ taskService = new TaskService();
+
+ taskService.setCorePoolSize(8);
+ taskService.setDaemon(false);
+
+ assertEquals(8, taskService.getCorePoolSize());
+ assertFalse(taskService.isDaemon());
+ }
+
+ @Test
+ void submitRunnable_executesTheTask() throws Exception {
+ taskService = new TaskService();
+ AtomicBoolean ran = new AtomicBoolean(false);
+
+ Future> future = taskService.submit(() -> ran.set(true));
+ future.get(5, TimeUnit.SECONDS);
+
+ assertTrue(ran.get());
+ }
+
+ @Test
+ void submitCallable_returnsComputedResult() throws Exception {
+ taskService = new TaskService();
+
+ Future future = taskService.submit(() -> "done");
+
+ assertEquals("done", future.get(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void submitRunnableWithResult_returnsGivenResult() throws Exception {
+ taskService = new TaskService();
+ AtomicBoolean ran = new AtomicBoolean(false);
+
+ Future future = taskService.submit(() -> ran.set(true), "the-result");
+
+ assertEquals("the-result", future.get(5, TimeUnit.SECONDS));
+ assertTrue(ran.get());
+ }
+
+ @Test
+ void execute_runsCommandAsynchronously() throws Exception {
+ taskService = new TaskService();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ taskService.execute(latch::countDown);
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void schedule_runsCallableAfterDelay() throws Exception {
+ taskService = new TaskService();
+
+ ScheduledFuture future =
+ taskService.schedule(
+ (Callable) () -> "scheduled", 10, TimeUnit.MILLISECONDS);
+
+ assertEquals("scheduled", future.get(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void schedule_runsRunnableAfterDelay() throws Exception {
+ taskService = new TaskService();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ ScheduledFuture> future =
+ taskService.schedule((Runnable) latch::countDown, 10, TimeUnit.MILLISECONDS);
+ future.get(5, TimeUnit.SECONDS);
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ @Test
+ void scheduleAtFixedRate_runsMultipleTimes() throws Exception {
+ taskService = new TaskService();
+ CountDownLatch latch = new CountDownLatch(3);
+
+ ScheduledFuture> future =
+ taskService.scheduleAtFixedRate(latch::countDown, 0, 10, TimeUnit.MILLISECONDS);
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ future.cancel(true);
+ }
+
+ @Test
+ void scheduleWithFixedDelay_runsMultipleTimes() throws Exception {
+ taskService = new TaskService();
+ CountDownLatch latch = new CountDownLatch(3);
+
+ ScheduledFuture> future =
+ taskService.scheduleWithFixedDelay(latch::countDown, 0, 10, TimeUnit.MILLISECONDS);
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ future.cancel(true);
+ }
+
+ @Test
+ void invokeAll_executesAllTasks() throws Exception {
+ taskService = new TaskService();
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ List> futures =
+ taskService.invokeAll(
+ List.of((Callable) () -> "a", (Callable) () -> "b"));
+
+ assertEquals(2, futures.size());
+ assertEquals("a", futures.get(0).get());
+ assertEquals("b", futures.get(1).get());
+ }
+
+ @Test
+ void invokeAny_returnsOneResult() throws Exception {
+ taskService = new TaskService();
+
+ @SuppressWarnings("unchecked")
+ Object result = taskService.invokeAny(List.of((Callable) () -> "only"));
+
+ assertEquals("only", result);
+ }
+
+ @Test
+ void isShutdown_beforeStart_returnsTrue() {
+ taskService = new TaskService();
+
+ assertTrue(taskService.isShutdown());
+ }
+
+ @Test
+ void isShutdown_afterStart_returnsFalse() throws Exception {
+ taskService = new TaskService();
+
+ taskService.start();
+
+ assertFalse(taskService.isShutdown());
+ }
+
+ @Test
+ void shutdown_withoutAllowedFlag_isNoOp() throws Exception {
+ taskService = new TaskService();
+ taskService.start();
+
+ taskService.shutdown();
+
+ assertFalse(taskService.isShutdown());
+ }
+
+ @Test
+ void shutdown_withAllowedFlag_shutsDownExecutor() throws Exception {
+ taskService = new TaskService();
+ taskService.start();
+ taskService.setShutdownAllowed(true);
+
+ taskService.shutdown();
+
+ assertTrue(taskService.awaitTermination(5, TimeUnit.SECONDS));
+ assertTrue(taskService.isShutdown());
+ }
+
+ @Test
+ void shutdownNow_withoutAllowedFlag_returnsEmptyList() throws Exception {
+ taskService = new TaskService();
+ taskService.start();
+
+ List pending = taskService.shutdownNow();
+
+ assertTrue(pending.isEmpty());
+ assertFalse(taskService.isShutdown());
+ }
+
+ @Test
+ void shutdownNow_withAllowedFlag_stopsExecutor() throws Exception {
+ taskService = new TaskService();
+ taskService.start();
+ taskService.setShutdownAllowed(true);
+
+ taskService.shutdownNow();
+
+ assertTrue(taskService.awaitTermination(5, TimeUnit.SECONDS));
+ assertTrue(taskService.isTerminated());
+ }
+
+ @Test
+ void stop_shutsDownWrappedExecutorRegardlessOfAllowedFlag() throws Exception {
+ taskService = new TaskService();
+ taskService.start();
+
+ taskService.stop();
+
+ // Note: isShutdown() is checked directly (rather than awaitTermination(),
+ // which calls startIfNeeded() and would transparently restart the
+ // service since stop() resets the started flag).
+ assertTrue(taskService.isShutdown());
+ assertFalse(taskService.isStarted());
+ }
+
+ @Test
+ void wrap_copiesThreadLocalsAndDelegatesToWrappedExecutor() throws Exception {
+ java.util.concurrent.ScheduledExecutorService delegate =
+ java.util.concurrent.Executors.newSingleThreadScheduledExecutor();
+ try {
+ ScheduledExecutorService wrapped = TaskService.wrap(delegate);
+ AtomicInteger counter = new AtomicInteger();
+
+ Future> future = wrapped.submit(counter::incrementAndGet);
+ future.get(5, TimeUnit.SECONDS);
+
+ assertEquals(1, counter.get());
+ assertFalse(wrapped.isShutdown());
+ assertFalse(wrapped.isTerminated());
+ } finally {
+ delegate.shutdownNow();
+ }
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/service/TunnelServiceTestCase.java b/org.restlet/src/test/java/org/restlet/service/TunnelServiceTestCase.java
new file mode 100644
index 0000000000..f5e4d565c8
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/service/TunnelServiceTestCase.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Context;
+import org.restlet.data.ClientInfo;
+import org.restlet.engine.application.TunnelFilter;
+import org.restlet.engine.header.HeaderConstants;
+import org.restlet.routing.Filter;
+
+/** Unit tests for {@link TunnelService}. */
+class TunnelServiceTestCase {
+
+ @Test
+ void twoArgConstructor_enablesQueryTunnelOnly() {
+ TunnelService service = new TunnelService(true, true);
+
+ assertTrue(service.isEnabled());
+ assertTrue(service.isMethodTunnel());
+ assertTrue(service.isPreferencesTunnel());
+ assertTrue(service.isQueryTunnel());
+ assertFalse(service.isExtensionsTunnel());
+ assertFalse(service.isUserAgentTunnel());
+ assertTrue(service.isHeadersTunnel());
+ }
+
+ @Test
+ void threeArgConstructor_setsEnabledFlag() {
+ TunnelService service = new TunnelService(false, true, false);
+
+ assertFalse(service.isEnabled());
+ assertTrue(service.isMethodTunnel());
+ assertFalse(service.isPreferencesTunnel());
+ }
+
+ @Test
+ void fiveArgConstructor_setsQueryAndExtensionsTunnel() {
+ TunnelService service = new TunnelService(true, true, true, false, true);
+
+ assertFalse(service.isQueryTunnel());
+ assertTrue(service.isExtensionsTunnel());
+ assertFalse(service.isUserAgentTunnel());
+ }
+
+ @Test
+ void sixArgConstructor_setsUserAgentTunnel() {
+ TunnelService service = new TunnelService(true, true, true, true, false, true);
+
+ assertTrue(service.isUserAgentTunnel());
+ assertTrue(service.isHeadersTunnel());
+ }
+
+ @Test
+ void sevenArgConstructor_setsHeadersTunnel() {
+ TunnelService service = new TunnelService(true, true, true, true, false, true, false);
+
+ assertFalse(service.isHeadersTunnel());
+ }
+
+ @Test
+ void defaultParameterNames_areSet() {
+ TunnelService service = new TunnelService(true, true);
+
+ assertEquals("charset", service.getCharacterSetParameter());
+ assertEquals("encoding", service.getEncodingParameter());
+ assertEquals("language", service.getLanguageParameter());
+ assertEquals("media", service.getMediaTypeParameter());
+ assertEquals("method", service.getMethodParameter());
+ assertEquals(HeaderConstants.HEADER_X_HTTP_METHOD_OVERRIDE, service.getMethodHeader());
+ }
+
+ @Test
+ void gettersAndSetters_roundTrip() {
+ TunnelService service = new TunnelService(true, true);
+
+ service.setCharacterSetParameter("cs");
+ assertEquals("cs", service.getCharacterSetParameter());
+
+ service.setEncodingParameter("enc");
+ assertEquals("enc", service.getEncodingParameter());
+
+ service.setLanguageParameter("lang");
+ assertEquals("lang", service.getLanguageParameter());
+
+ service.setMediaTypeParameter("mt");
+ assertEquals("mt", service.getMediaTypeParameter());
+
+ service.setMethodParameter("mth");
+ assertEquals("mth", service.getMethodParameter());
+
+ service.setMethodHeader("X-Method");
+ assertEquals("X-Method", service.getMethodHeader());
+
+ service.setMethodTunnel(false);
+ assertFalse(service.isMethodTunnel());
+
+ service.setPreferencesTunnel(false);
+ assertFalse(service.isPreferencesTunnel());
+
+ service.setQueryTunnel(false);
+ assertFalse(service.isQueryTunnel());
+
+ service.setExtensionsTunnel(true);
+ assertTrue(service.isExtensionsTunnel());
+
+ service.setUserAgentTunnel(true);
+ assertTrue(service.isUserAgentTunnel());
+
+ service.setHeadersTunnel(false);
+ assertFalse(service.isHeadersTunnel());
+ }
+
+ @Test
+ void allowClient_alwaysReturnsTrue() {
+ TunnelService service = new TunnelService(true, true);
+
+ assertTrue(service.allowClient(new ClientInfo()));
+ assertTrue(service.allowClient(null));
+ }
+
+ @Test
+ void createInboundFilter_returnsTunnelFilter() {
+ TunnelService service = new TunnelService(true, true);
+
+ Filter filter = service.createInboundFilter(new Context());
+
+ assertNotNull(filter);
+ assertTrue(filter instanceof TunnelFilter);
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/util/ClientListTestCase.java b/org.restlet/src/test/java/org/restlet/util/ClientListTestCase.java
new file mode 100644
index 0000000000..4bf9a84ff2
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/util/ClientListTestCase.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+import org.restlet.Client;
+import org.restlet.Context;
+import org.restlet.data.Protocol;
+
+class ClientListTestCase {
+
+ @Test
+ void add_clientWithoutContext_assignsChildContext() {
+ Context context = new Context();
+ ClientList list = new ClientList(context);
+ Client client = new Client(Protocol.HTTP);
+
+ list.add(client);
+
+ assertNotNull(client.getContext());
+ }
+
+ @Test
+ void add_clientWithExistingContext_leavesItUnchanged() {
+ Context context = new Context();
+ ClientList list = new ClientList(context);
+ Client client = new Client(Protocol.HTTP);
+ Context existing = new Context();
+ client.setContext(existing);
+
+ list.add(client);
+
+ assertEquals(existing, client.getContext());
+ }
+
+ @Test
+ void addProtocol_createsAndRegistersClient() {
+ Context context = new Context();
+ ClientList list = new ClientList(context);
+
+ Client client = list.add(Protocol.HTTP);
+
+ assertEquals(1, list.size());
+ assertNotNull(client.getContext());
+ assertEquals(client, list.get(0));
+ }
+
+ @Test
+ void getContextAndSetContext_roundTrip() {
+ Context context = new Context();
+ ClientList list = new ClientList(context);
+ assertEquals(context, list.getContext());
+
+ Context other = new Context();
+ list.setContext(other);
+ assertEquals(other, list.getContext());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/util/NamedValuesCollectorTestCase.java b/org.restlet/src/test/java/org/restlet/util/NamedValuesCollectorTestCase.java
new file mode 100644
index 0000000000..21578c0cc0
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/util/NamedValuesCollectorTestCase.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.Parameter;
+
+class NamedValuesCollectorTestCase {
+
+ @Test
+ void constructorWithNames_prePopulatesKeysWithNullValues() {
+ NamedValuesCollector collector = new NamedValuesCollector("a", "b");
+ assertTrue(collector.getCollectedValues().containsKey("a"));
+ assertNull(collector.getCollectedValues().get("a"));
+ }
+
+ @Test
+ void constructorWithMap_usesProvidedMap() {
+ Map map = new HashMap<>();
+ map.put("a", null);
+ NamedValuesCollector collector = new NamedValuesCollector(map);
+ assertEquals(map, collector.getCollectedValues());
+ }
+
+ @Test
+ void collect_unknownName_isIgnored() {
+ NamedValuesCollector collector = new NamedValuesCollector("a");
+ collector.collect(new Parameter("unknown", "value"));
+ assertFalse(collector.getCollectedValues().containsKey("unknown"));
+ }
+
+ @Test
+ void collect_firstValueForKnownName_setsValue() {
+ NamedValuesCollector collector = new NamedValuesCollector("a");
+ collector.collect(new Parameter("a", "1"));
+ assertEquals("1", collector.getCollectedValues().get("a"));
+ }
+
+ @Test
+ void collect_nullValueForKnownName_usesEmptyValueMarker() {
+ NamedValuesCollector collector = new NamedValuesCollector("a");
+ collector.collect(new Parameter("a", null));
+ assertEquals(Series.EMPTY_VALUE, collector.getCollectedValues().get("a"));
+ }
+
+ @Test
+ void collect_secondValueForSameName_createsListOfValues() {
+ NamedValuesCollector collector = new NamedValuesCollector("a");
+ collector.collect(new Parameter("a", "1"));
+ collector.collect(new Parameter("a", "2"));
+
+ Object result = collector.getCollectedValues().get("a");
+ assertTrue(result instanceof List>);
+ assertEquals(List.of("1", "2"), result);
+ }
+
+ @Test
+ void collect_thirdValueForSameName_appendsToExistingList() {
+ NamedValuesCollector collector = new NamedValuesCollector("a");
+ collector.collect(new Parameter("a", "1"));
+ collector.collect(new Parameter("a", "2"));
+ collector.collect(new Parameter("a", "3"));
+
+ assertEquals(List.of("1", "2", "3"), collector.getCollectedValues().get("a"));
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/util/NonNullItemsListTestCase.java b/org.restlet/src/test/java/org/restlet/util/NonNullItemsListTestCase.java
new file mode 100644
index 0000000000..13ff0093c5
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/util/NonNullItemsListTestCase.java
@@ -0,0 +1,140 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+class NonNullItemsListTestCase {
+
+ @Test
+ void add_nonNullElement_succeeds() {
+ NonNullItemsList list = new NonNullItemsList<>("no nulls allowed");
+ assertTrue(list.add("a"));
+ assertEquals("a", list.get(0));
+ }
+
+ @Test
+ void add_nullElement_throwsWithConfiguredMessage() {
+ NonNullItemsList list = new NonNullItemsList<>("no nulls allowed");
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () -> list.add(null));
+ assertEquals("no nulls allowed", exception.getMessage());
+ }
+
+ @Test
+ void addAtIndex_nullElement_throws() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ assertThrows(IllegalArgumentException.class, () -> list.add(0, null));
+ }
+
+ @Test
+ void addFirst_nullElement_throws() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ assertThrows(IllegalArgumentException.class, () -> list.addFirst(null));
+ }
+
+ @Test
+ void addFirst_nonNullElement_succeeds() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ list.add("b");
+ list.addFirst("a");
+ assertEquals("a", list.get(0));
+ }
+
+ @Test
+ void addLast_nullElement_throws() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ assertThrows(IllegalArgumentException.class, () -> list.addLast(null));
+ }
+
+ @Test
+ void addLast_nonNullElement_succeeds() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ list.add("a");
+ list.addLast("b");
+ assertEquals("b", list.get(1));
+ }
+
+ @Test
+ void addAll_collectionWithNull_throws() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ assertThrows(
+ IllegalArgumentException.class, () -> list.addAll(Arrays.asList("a", null, "b")));
+ }
+
+ @Test
+ void addAll_nullCollection_throws() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ assertThrows(IllegalArgumentException.class, () -> list.addAll(null));
+ }
+
+ @Test
+ void addAll_validCollection_succeeds() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ assertTrue(list.addAll(Arrays.asList("a", "b")));
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ void addAllAtIndex_collectionWithNull_throws() {
+ NonNullItemsList list = new NonNullItemsList<>("msg");
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> list.addAll(0, Collections.singletonList(null)));
+ }
+
+ @Test
+ void addAllAtIndex_validCollection_succeeds() {
+ NonNullItemsList