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 new file mode 100644 index 0000000000..e7c56f1cfa --- /dev/null +++ b/org.restlet.ext.freemarker/src/test/java/org/restlet/ext/freemarker/ContextTemplateLoaderTestCase.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.ext.freemarker;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+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.Date;
+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.Reference;
+import org.restlet.representation.StringRepresentation;
+
+class ContextTemplateLoaderTestCase {
+
+ /** Context whose dispatcher records the last URI it received. */
+ private static class CapturingContext extends Context {
+ String lastRequestedUri;
+
+ CapturingContext() {
+ setClientDispatcher(
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ lastRequestedUri = request.getResourceRef().toString();
+ response.setEntity(new StringRepresentation("template content"));
+ }
+ });
+ }
+ }
+
+ /** Representation that tracks whether release() was called. */
+ private static class TrackingRepresentation extends StringRepresentation {
+ boolean released = false;
+
+ TrackingRepresentation() {
+ super("content");
+ }
+
+ @Override
+ public void release() {
+ released = true;
+ super.release();
+ }
+ }
+
+ @Test
+ void findTemplateSource_nullContext_returnsNull() throws IOException {
+ ContextTemplateLoader loader = new ContextTemplateLoader(null, "clap://test");
+ assertNull(loader.findTemplateSource("test.ftl"));
+ }
+
+ @Test
+ void findTemplateSource_baseUriWithTrailingSlash_buildsCorrectUri() throws IOException {
+ CapturingContext ctx = new CapturingContext();
+ ContextTemplateLoader loader = new ContextTemplateLoader(ctx, "clap://test/");
+ loader.findTemplateSource("hello.ftl");
+ assertEquals("clap://test/hello.ftl", ctx.lastRequestedUri);
+ }
+
+ @Test
+ void findTemplateSource_baseUriWithoutTrailingSlash_buildsCorrectUri() throws IOException {
+ CapturingContext ctx = new CapturingContext();
+ ContextTemplateLoader loader = new ContextTemplateLoader(ctx, "clap://test");
+ loader.findTemplateSource("hello.ftl");
+ assertEquals("clap://test/hello.ftl", ctx.lastRequestedUri);
+ }
+
+ @Test
+ void constructor_withReference_usesReferenceToString() throws IOException {
+ CapturingContext ctx = new CapturingContext();
+ Reference ref = new Reference("clap://test/");
+ ContextTemplateLoader loader = new ContextTemplateLoader(ctx, ref);
+ loader.findTemplateSource("hello.ftl");
+ assertEquals("clap://test/hello.ftl", ctx.lastRequestedUri);
+ }
+
+ @Test
+ void closeTemplateSource_withRepresentation_callsRelease() {
+ ContextTemplateLoader loader = new ContextTemplateLoader(null, "clap://test");
+ TrackingRepresentation rep = new TrackingRepresentation();
+ loader.closeTemplateSource(rep);
+ assertTrue(rep.released);
+ }
+
+ @Test
+ void closeTemplateSource_withNonRepresentation_doesNotThrow() {
+ ContextTemplateLoader loader = new ContextTemplateLoader(null, "clap://test");
+ assertDoesNotThrow(() -> loader.closeTemplateSource("notARepresentation"));
+ }
+
+ @Test
+ void getLastModified_withModificationDate_returnsTimestamp() {
+ ContextTemplateLoader loader = new ContextTemplateLoader(null, "clap://test");
+ Date now = new Date();
+ StringRepresentation rep = new StringRepresentation("content");
+ rep.setModificationDate(now);
+ assertEquals(now.getTime(), loader.getLastModified(rep));
+ }
+
+ @Test
+ void getLastModified_withNullModificationDate_returnsMinusOne() {
+ ContextTemplateLoader loader = new ContextTemplateLoader(null, "clap://test");
+ StringRepresentation rep = new StringRepresentation("content");
+ assertEquals(-1L, loader.getLastModified(rep));
+ }
+}
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 7afafb2b70..8c7cb39f44 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
@@ -9,8 +9,12 @@
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.assertNull;
+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.nio.file.Files;
@@ -18,6 +22,8 @@
import org.junit.jupiter.api.Test;
import org.restlet.data.MediaType;
import org.restlet.engine.io.IoUtils;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.Variant;
/**
* Unit test for the FreeMarker extension.
@@ -37,7 +43,7 @@ void testTemplate() throws Exception {
fw.write("Value=${value}");
fw.close();
- final Configuration fmc = new Configuration();
+ final Configuration fmc = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
fmc.setDirectoryForTemplateLoading(testDir);
final Map
+ * 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.jaas;
+
+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 static org.junit.jupiter.api.Assertions.assertThrows;
+
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ChallengeScheme;
+import org.restlet.data.Method;
+import org.restlet.data.Reference;
+
+/** Unit tests for {@link ChallengeCallbackHandler}. */
+class ChallengeCallbackHandlerTestCase {
+
+ private Request request;
+ private Response response;
+ private ChallengeCallbackHandler handler;
+
+ @BeforeEach
+ void setUp() {
+ request = new Request(Method.GET, new Reference("http://localhost/test"));
+ response = new Response(request);
+ handler = new ChallengeCallbackHandler(request, response);
+ }
+
+ @Test
+ void testConstructor() {
+ assertEquals(request, handler.getRequest());
+ assertEquals(response, handler.getResponse());
+ }
+
+ @Test
+ void testSetRequest() {
+ Request newRequest = new Request(Method.POST, new Reference("http://localhost/other"));
+ handler.setRequest(newRequest);
+ assertEquals(newRequest, handler.getRequest());
+ }
+
+ @Test
+ void testSetResponse() {
+ Response newResponse = new Response(request);
+ handler.setResponse(newResponse);
+ assertEquals(newResponse, handler.getResponse());
+ }
+
+ @Test
+ void testHandleNameCallback() throws UnsupportedCallbackException {
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "john", "secret"));
+
+ NameCallback nameCallback = new NameCallback("Username:");
+ handler.handle(new javax.security.auth.callback.Callback[] {nameCallback});
+
+ assertEquals("john", nameCallback.getName());
+ }
+
+ @Test
+ void testHandlePasswordCallback() throws UnsupportedCallbackException {
+ request.setChallengeResponse(
+ new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "john", "secret"));
+
+ PasswordCallback passwordCallback = new PasswordCallback("Password:", false);
+ handler.handle(new javax.security.auth.callback.Callback[] {passwordCallback});
+
+ assertArrayEquals("secret".toCharArray(), passwordCallback.getPassword());
+ }
+
+ @Test
+ void testHandleNullCallbacks() throws UnsupportedCallbackException {
+ handler.handle((javax.security.auth.callback.Callback[]) null);
+ }
+
+ @Test
+ void testHandleEmptyCallbacks() throws UnsupportedCallbackException {
+ handler.handle(new javax.security.auth.callback.Callback[] {});
+ }
+
+ @Test
+ void testHandleUnsupportedCallback() {
+ javax.security.auth.callback.Callback unsupported =
+ new javax.security.auth.callback.Callback() {};
+ assertThrows(
+ UnsupportedCallbackException.class,
+ () -> handler.handle(new javax.security.auth.callback.Callback[] {unsupported}));
+ }
+
+ @Test
+ void testHandleNameCallbackWithoutChallengeResponse() {
+ assertNull(request.getChallengeResponse());
+ NameCallback nameCallback = new NameCallback("Username:");
+ assertThrows(
+ UnsupportedCallbackException.class,
+ () -> handler.handle(new javax.security.auth.callback.Callback[] {nameCallback}));
+ }
+}
diff --git a/org.restlet.ext.jaas/src/test/java/org/restlet/ext/jaas/JaasUtilsTestCase.java b/org.restlet.ext.jaas/src/test/java/org/restlet/ext/jaas/JaasUtilsTestCase.java
new file mode 100644
index 0000000000..f3584248b5
--- /dev/null
+++ b/org.restlet.ext.jaas/src/test/java/org/restlet/ext/jaas/JaasUtilsTestCase.java
@@ -0,0 +1,106 @@
+/**
+ * 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.jaas;
+
+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 java.security.Principal;
+import javax.security.auth.Subject;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.ClientInfo;
+import org.restlet.security.Role;
+import org.restlet.security.User;
+
+/** Unit tests for {@link JaasUtils}. */
+class JaasUtilsTestCase {
+
+ @Test
+ void testCreateSubjectWithNullClientInfo() {
+ Subject subject = JaasUtils.createSubject(null);
+
+ assertNotNull(subject);
+ assertTrue(subject.getPrincipals().isEmpty());
+ }
+
+ @Test
+ void testCreateSubjectWithEmptyClientInfo() {
+ ClientInfo clientInfo = new ClientInfo();
+ Subject subject = JaasUtils.createSubject(clientInfo);
+
+ assertNotNull(subject);
+ assertTrue(subject.getPrincipals().isEmpty());
+ }
+
+ @Test
+ void testCreateSubjectWithUser() {
+ ClientInfo clientInfo = new ClientInfo();
+ User user = new User("alice");
+ clientInfo.setUser(user);
+
+ Subject subject = JaasUtils.createSubject(clientInfo);
+
+ assertEquals(1, subject.getPrincipals().size());
+ assertTrue(subject.getPrincipals().contains(user));
+ }
+
+ @Test
+ void testCreateSubjectWithRole() {
+ ClientInfo clientInfo = new ClientInfo();
+ Role role = new Role(null, "admin");
+ clientInfo.getRoles().add(role);
+
+ Subject subject = JaasUtils.createSubject(clientInfo);
+
+ assertEquals(1, subject.getPrincipals().size());
+ assertTrue(subject.getPrincipals().contains(role));
+ }
+
+ @Test
+ void testCreateSubjectWithPrincipal() {
+ ClientInfo clientInfo = new ClientInfo();
+ Principal principal = () -> "custom-principal";
+ clientInfo.getPrincipals().add(principal);
+
+ Subject subject = JaasUtils.createSubject(clientInfo);
+
+ assertEquals(1, subject.getPrincipals().size());
+ assertTrue(subject.getPrincipals().contains(principal));
+ }
+
+ @Test
+ void testCreateSubjectWithAllTypes() {
+ ClientInfo clientInfo = new ClientInfo();
+ User user = new User("bob");
+ Role role = new Role(null, "editor");
+ Principal principal = () -> "extra";
+
+ clientInfo.setUser(user);
+ clientInfo.getRoles().add(role);
+ clientInfo.getPrincipals().add(principal);
+
+ Subject subject = JaasUtils.createSubject(clientInfo);
+
+ assertEquals(3, subject.getPrincipals().size());
+ assertTrue(subject.getPrincipals().contains(user));
+ assertTrue(subject.getPrincipals().contains(role));
+ assertTrue(subject.getPrincipals().contains(principal));
+ }
+
+ @Test
+ void testDoAsPrivileged() {
+ ClientInfo clientInfo = new ClientInfo();
+ clientInfo.setUser(new User("eve"));
+
+ String result = JaasUtils.doAsPriviledged(clientInfo, () -> "executed");
+
+ assertEquals("executed", result);
+ }
+}
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
new file mode 100644
index 0000000000..1590d5639f
--- /dev/null
+++ b/org.restlet.ext.jaas/src/test/java/org/restlet/ext/jaas/JaasVerifierTestCase.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.jaas;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import 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.Verifier;
+
+/** Unit tests for {@link JaasVerifier}. */
+class JaasVerifierTestCase {
+
+ @Test
+ void testConstructor() {
+ JaasVerifier verifier = new JaasVerifier("myApp");
+ assertEquals("myApp", verifier.getName());
+ assertNull(verifier.getConfiguration());
+ assertNull(verifier.getUserPrincipalClassName());
+ }
+
+ @Test
+ void testSetName() {
+ JaasVerifier verifier = new JaasVerifier("initial");
+ verifier.setName("updated");
+ assertEquals("updated", verifier.getName());
+ }
+
+ @Test
+ void testSetUserPrincipalClassName() {
+ JaasVerifier verifier = new JaasVerifier("myApp");
+ verifier.setUserPrincipalClassName("com.example.MyPrincipal");
+ assertEquals("com.example.MyPrincipal", verifier.getUserPrincipalClassName());
+ }
+
+ @Test
+ void testCreateCallbackHandler() {
+ JaasVerifier verifier = new JaasVerifier("myApp");
+ Request request = new Request(Method.GET, new Reference("http://localhost/test"));
+ Response response = new Response(request);
+
+ var handler = verifier.createCallbackHandler(request, response);
+
+ assertNotNull(handler);
+ assertInstanceOf(ChallengeCallbackHandler.class, handler);
+ }
+
+ @Test
+ void testVerifyReturnsInvalidWhenLoginFails() {
+ JaasVerifier verifier = new JaasVerifier("nonExistentLoginModule");
+ Request request = new Request(Method.GET, new Reference("http://localhost/test"));
+ Response response = new Response(request);
+
+ int result = verifier.verify(request, response);
+
+ assertEquals(Verifier.RESULT_INVALID, result);
+ }
+}
diff --git a/org.restlet.ext.json/src/test/java/org/restlet/ext/json/JsonConverterTestCase.java b/org.restlet.ext.json/src/test/java/org/restlet/ext/json/JsonConverterTestCase.java
new file mode 100644
index 0000000000..f4e5284692
--- /dev/null
+++ b/org.restlet.ext.json/src/test/java/org/restlet/ext/json/JsonConverterTestCase.java
@@ -0,0 +1,240 @@
+/**
+ * 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.json;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.data.Preference;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.Variant;
+
+/** Unit tests for {@link JsonConverter}. */
+class JsonConverterTestCase {
+
+ @Test
+ void getObjectClasses_withJsonVariant_returnsThreeClasses() {
+ List
+ * 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.json;
+
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Map;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONStringer;
+import org.json.JSONTokener;
+import org.junit.jupiter.api.Test;
+import org.restlet.representation.Representation;
+import org.restlet.representation.StringRepresentation;
+
+/** Unit tests for {@link JsonRepresentation}. */
+class JsonRepresentationTestCase {
+
+ @Test
+ void write_fromJsonObject_producesCorrectJson() throws Exception {
+ JSONObject obj = new JSONObject("{\"key\":\"value\"}");
+ String result = new JsonRepresentation(obj).getText();
+ assertEquals("value", new JSONObject(result).getString("key"));
+ }
+
+ @Test
+ void write_fromJsonArray_producesCorrectJson() throws Exception {
+ JSONArray arr = new JSONArray("[1,2,3]");
+ String result = new JsonRepresentation(arr).getText();
+ assertEquals(3, new JSONArray(result).length());
+ }
+
+ @Test
+ void write_fromJsonStringer_producesCorrectJson() throws Exception {
+ JSONStringer stringer = new JSONStringer();
+ stringer.object().key("key").value("value").endObject();
+ String result = new JsonRepresentation(stringer).getText();
+ assertEquals("{\"key\":\"value\"}", result);
+ }
+
+ @Test
+ void write_fromString_producesOriginalText() throws Exception {
+ String json = "{\"key\":\"value\"}";
+ assertEquals(json, new JsonRepresentation(json).getText());
+ }
+
+ @Test
+ void write_fromRepresentation_producesWrappedText() throws Exception {
+ String json = "{\"key\":\"value\"}";
+ assertEquals(json, new JsonRepresentation(new StringRepresentation(json)).getText());
+ }
+
+ @Test
+ void write_fromMap_containsExpectedKey() throws Exception {
+ String result = new JsonRepresentation(Map.of("key", "value")).getText();
+ assertEquals("value", new JSONObject(result).getString("key"));
+ }
+
+ @Test
+ void getJsonObject_fromJsonObjectValue_returnsValue() {
+ JSONObject obj = new JSONObject("{\"key\":\"value\"}");
+ assertEquals("value", new JsonRepresentation(obj).getJsonObject().getString("key"));
+ }
+
+ @Test
+ void getJsonArray_fromJsonArrayValue_returnsValue() {
+ JSONArray arr = new JSONArray("[1,2,3]");
+ assertEquals(3, new JsonRepresentation(arr).getJsonArray().length());
+ }
+
+ @Test
+ void getJsonObject_fromString_parsesCorrectly() {
+ JSONObject result = new JsonRepresentation("{\"key\":\"value\"}").getJsonObject();
+ assertEquals("value", result.getString("key"));
+ }
+
+ @Test
+ void getJsonArray_fromString_parsesCorrectly() {
+ JSONArray result = new JsonRepresentation("[1,2,3]").getJsonArray();
+ assertEquals(3, result.length());
+ }
+
+ @Test
+ void getJsonTokener_fromString_returnsUsableTokener() {
+ JSONTokener tokener = new JsonRepresentation("{\"key\":\"value\"}").getJsonTokener();
+ assertNotNull(tokener);
+ assertEquals("value", new JSONObject(tokener).getString("key"));
+ }
+
+ @Test
+ void indenting_defaultIsFalse() {
+ assertFalse(new JsonRepresentation(new JSONObject()).isIndenting());
+ }
+
+ @Test
+ void indentingSize_defaultIsThree() {
+ assertEquals(3, new JsonRepresentation(new JSONObject()).getIndentingSize());
+ }
+
+ @Test
+ void write_withIndentingOnJsonObject_producesFormattedOutput() throws Exception {
+ // json.org adds a space after ":" when indenting — output differs from compact form
+ String compact = new JsonRepresentation(new JSONObject("{\"key\":\"value\"}")).getText();
+ JsonRepresentation jr = new JsonRepresentation(new JSONObject("{\"key\":\"value\"}"));
+ jr.setIndenting(true);
+ assertNotEquals(compact, jr.getText());
+ }
+
+ @Test
+ void write_withIndentingOnJsonArray_producesFormattedOutput() throws Exception {
+ JsonRepresentation jr = new JsonRepresentation(new JSONArray("[1,2,3]"));
+ jr.setIndenting(true);
+ assertTrue(jr.getText().contains("\n"));
+ }
+
+ @Test
+ void setIndentingSize_changesIndentationWidth() throws Exception {
+ // Arrays produce multi-line indented output; use one to verify the indent width
+ JsonRepresentation jr = new JsonRepresentation(new JSONArray("[1,2,3]"));
+ jr.setIndenting(true);
+ jr.setIndentingSize(2);
+ assertEquals(2, jr.getIndentingSize());
+ String result = jr.getText();
+ assertTrue(result.contains("\n"));
+ assertTrue(result.contains(" ")); // 2-space indent present
+ assertFalse(result.contains(" ")); // 3-space indent absent
+ }
+
+ @Test
+ void getSize_fromString_delegatesToWrappedRepresentation() {
+ String json = "{\"key\":\"value\"}";
+ assertEquals(json.length(), new JsonRepresentation(json).getSize());
+ }
+
+ @Test
+ void getSize_fromJsonValue_returnsUnknownSize() {
+ assertEquals(
+ Representation.UNKNOWN_SIZE, new JsonRepresentation(new JSONObject()).getSize());
+ }
+}
diff --git a/org.restlet.ext.slf4j/pom.xml b/org.restlet.ext.slf4j/pom.xml
index 5b7df3f144..9a36ecc440 100644
--- a/org.restlet.ext.slf4j/pom.xml
+++ b/org.restlet.ext.slf4j/pom.xml
@@ -25,6 +25,18 @@
+ * 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.slf4j;
+
+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.logging.Logger;
+import org.junit.jupiter.api.Test;
+
+/** Unit tests for {@link Slf4jLoggerFacade}. */
+class Slf4jLoggerFacadeTestCase {
+
+ @Test
+ void testGetAnonymousLogger() {
+ Slf4jLoggerFacade facade = new Slf4jLoggerFacade();
+ Logger logger = facade.getAnonymousLogger();
+
+ assertNotNull(logger);
+ assertInstanceOf(Slf4jLogger.class, logger);
+ assertEquals("", logger.getName());
+ }
+
+ @Test
+ void testGetLogger() {
+ Slf4jLoggerFacade facade = new Slf4jLoggerFacade();
+ String loggerName = "org.restlet.test";
+ Logger logger = facade.getLogger(loggerName);
+
+ assertNotNull(logger);
+ assertInstanceOf(Slf4jLogger.class, logger);
+ assertEquals(loggerName, logger.getName());
+ }
+}
diff --git a/org.restlet.ext.slf4j/src/test/java/org/restlet/ext/slf4j/Slf4jLoggerTestCase.java b/org.restlet.ext.slf4j/src/test/java/org/restlet/ext/slf4j/Slf4jLoggerTestCase.java
new file mode 100644
index 0000000000..77a503f94b
--- /dev/null
+++ b/org.restlet.ext.slf4j/src/test/java/org/restlet/ext/slf4j/Slf4jLoggerTestCase.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.ext.slf4j;
+
+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.assertTrue;
+
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+
+/** Unit tests for {@link Slf4jLogger}. */
+class Slf4jLoggerTestCase {
+
+ private Slf4jLogger logger;
+
+ @BeforeEach
+ void setUp() {
+ logger = new Slf4jLogger(LoggerFactory.getLogger(Slf4jLoggerTestCase.class.getName()));
+ }
+
+ @Test
+ void testConstructor() {
+ assertNotNull(logger.getSlf4jLogger());
+ assertEquals(Slf4jLoggerTestCase.class.getName(), logger.getName());
+ }
+
+ @Test
+ void testSetSlf4jLogger() {
+ var newLogger = LoggerFactory.getLogger("other");
+ logger.setSlf4jLogger(newLogger);
+ assertEquals(newLogger, logger.getSlf4jLogger());
+ }
+
+ @Test
+ void testLoggingMethods() {
+ assertDoesNotThrow(
+ () -> {
+ logger.config("config message");
+ logger.fine("fine message");
+ logger.finer("finer message");
+ logger.finest("finest message");
+ logger.info("info message");
+ logger.severe("severe message");
+ logger.warning("warning message");
+ });
+ }
+
+ @Test
+ void testIsLoggableFixedLevels() {
+ assertTrue(logger.isLoggable(Level.ALL));
+ assertFalse(logger.isLoggable(Level.OFF));
+ }
+
+ @Test
+ void testIsLoggableAllLevels() {
+ assertDoesNotThrow(
+ () -> {
+ logger.isLoggable(Level.CONFIG);
+ logger.isLoggable(Level.FINE);
+ logger.isLoggable(Level.FINER);
+ logger.isLoggable(Level.FINEST);
+ logger.isLoggable(Level.INFO);
+ logger.isLoggable(Level.SEVERE);
+ logger.isLoggable(Level.WARNING);
+ });
+ }
+
+ @Test
+ void testIsLoggableUnknownLevel() {
+ Level unknownLevel = new Level("CUSTOM", 450) {};
+ assertFalse(logger.isLoggable(unknownLevel));
+ }
+
+ @Test
+ void testLogLevelMessage() {
+ assertDoesNotThrow(
+ () -> {
+ logger.log(Level.CONFIG, "config");
+ logger.log(Level.FINE, "fine");
+ logger.log(Level.FINER, "finer");
+ logger.log(Level.FINEST, "finest");
+ logger.log(Level.INFO, "info");
+ logger.log(Level.SEVERE, "severe");
+ logger.log(Level.WARNING, "warning");
+ logger.log(Level.ALL, "unmapped");
+ });
+ }
+
+ @Test
+ void testLogLevelMessageObject() {
+ assertDoesNotThrow(
+ () -> {
+ logger.log(Level.CONFIG, "msg {}", "p");
+ logger.log(Level.FINE, "msg {}", "p");
+ logger.log(Level.FINER, "msg {}", "p");
+ logger.log(Level.FINEST, "msg {}", "p");
+ logger.log(Level.INFO, "msg {}", "p");
+ logger.log(Level.SEVERE, "msg {}", "p");
+ logger.log(Level.WARNING, "msg {}", "p");
+ logger.log(Level.ALL, "msg {}", "p");
+ });
+ }
+
+ @Test
+ void testLogLevelMessageObjects() {
+ Object[] params = {"p1", "p2"};
+ assertDoesNotThrow(
+ () -> {
+ logger.log(Level.CONFIG, "msg {} {}", params);
+ logger.log(Level.FINE, "msg {} {}", params);
+ logger.log(Level.FINER, "msg {} {}", params);
+ logger.log(Level.FINEST, "msg {} {}", params);
+ logger.log(Level.INFO, "msg {} {}", params);
+ logger.log(Level.SEVERE, "msg {} {}", params);
+ logger.log(Level.WARNING, "msg {} {}", params);
+ logger.log(Level.ALL, "msg {} {}", params);
+ });
+ }
+
+ @Test
+ void testLogLevelMessageThrowable() {
+ Throwable t = new RuntimeException("test");
+ assertDoesNotThrow(
+ () -> {
+ logger.log(Level.CONFIG, "msg", t);
+ logger.log(Level.FINE, "msg", t);
+ logger.log(Level.FINER, "msg", t);
+ logger.log(Level.FINEST, "msg", t);
+ logger.log(Level.INFO, "msg", t);
+ logger.log(Level.SEVERE, "msg", t);
+ logger.log(Level.WARNING, "msg", t);
+ logger.log(Level.ALL, "msg", t);
+ });
+ }
+
+ @Test
+ void testLogRecord_withThrowable() {
+ LogRecord logRecord = new LogRecord(Level.SEVERE, "test");
+ logRecord.setThrown(new RuntimeException("test"));
+ assertDoesNotThrow(() -> logger.log(logRecord));
+ }
+
+ @Test
+ void testLogRecord_withParams() {
+ LogRecord logRecord = new LogRecord(Level.INFO, "test {} {}");
+ logRecord.setParameters(new Object[] {"p1", "p2"});
+ assertDoesNotThrow(() -> logger.log(logRecord));
+ }
+
+ @Test
+ void testLogRecord_simple() {
+ LogRecord logRecord = new LogRecord(Level.WARNING, "simple message");
+ assertDoesNotThrow(() -> logger.log(logRecord));
+ }
+}
diff --git a/org.restlet.ext.thymeleaf/src/test/java/org/restlet/ext/thymeleaf/TemplateRepresentationTestCase.java b/org.restlet.ext.thymeleaf/src/test/java/org/restlet/ext/thymeleaf/TemplateRepresentationTestCase.java
new file mode 100644
index 0000000000..c8723c3d9f
--- /dev/null
+++ b/org.restlet.ext.thymeleaf/src/test/java/org/restlet/ext/thymeleaf/TemplateRepresentationTestCase.java
@@ -0,0 +1,138 @@
+/**
+ * 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.thymeleaf;
+
+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.Locale;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.data.MediaType;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.util.Resolver;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
+import org.thymeleaf.templateresolver.ITemplateResolver;
+
+/** Unit tests for {@link TemplateRepresentation}. */
+class TemplateRepresentationTestCase {
+
+ private static TemplateEngine buildEngine() {
+ ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
+ resolver.setPrefix("org/restlet/ext/thymeleaf/");
+ resolver.setSuffix(".html");
+ return TemplateRepresentation.createTemplateEngine(resolver);
+ }
+
+ @Test
+ void getLocale_returnsLocaleFromConstructor() {
+ TemplateRepresentation tr =
+ new TemplateRepresentation(
+ "test", buildEngine(), Locale.FRENCH, MediaType.TEXT_PLAIN);
+ assertEquals(Locale.FRENCH, tr.getLocale());
+ }
+
+ @Test
+ void getTemplateName_returnsNameFromConstructor() {
+ TemplateRepresentation tr =
+ new TemplateRepresentation(
+ "myTemplate", buildEngine(), Locale.getDefault(), MediaType.TEXT_PLAIN);
+ assertEquals("myTemplate", tr.getTemplateName());
+ }
+
+ @Test
+ void setTemplateName_changesTemplateName() {
+ TemplateRepresentation tr =
+ new TemplateRepresentation(
+ "original", buildEngine(), Locale.getDefault(), MediaType.TEXT_PLAIN);
+ tr.setTemplateName("changed");
+ assertEquals("changed", tr.getTemplateName());
+ }
+
+ @Test
+ void createTemplateEngine_noArg_returnsNonNull() {
+ assertNotNull(TemplateRepresentation.createTemplateEngine());
+ }
+
+ @Test
+ void createTemplateResolver_returnsNonNull() {
+ ITemplateResolver resolver = TemplateRepresentation.createTemplateResolver();
+ assertNotNull(resolver);
+ }
+
+ @Test
+ void wrapConstructor_copiesTemplateName() {
+ TemplateRepresentation original =
+ new TemplateRepresentation(
+ "test", buildEngine(), Locale.getDefault(), MediaType.TEXT_PLAIN);
+ TemplateRepresentation wrapped =
+ new TemplateRepresentation(
+ original, buildEngine(), Locale.ENGLISH, MediaType.TEXT_HTML);
+ assertEquals("test", wrapped.getTemplateName());
+ }
+
+ @Test
+ void setDataModel_withResolver_rendersTemplateWithoutThrowing() throws Exception {
+ // ResolverContext.getVariableNames() returns an empty set, so Thymeleaf's OGNL evaluator
+ // cannot see resolver variables — ${welcome} resolves to null and the renders empty.
+ TemplateRepresentation tr =
+ new TemplateRepresentation(
+ "test", buildEngine(), Locale.getDefault(), Map.of(), MediaType.TEXT_PLAIN);
+ tr.setDataModel(
+ new Resolver<>() {
+ @Override
+ public Object resolve(String name) {
+ return "welcome".equals(name) ? "Hello, resolver" : null;
+ }
+ });
+ String result = tr.getText();
+ assertNotNull(result);
+ assertFalse(result.contains("Hello, resolver"));
+ }
+
+ @Test
+ void write_whenTemplateNotFound_throwsIoException() {
+ // createTemplateEngine() uses /WEB-INF/templates/ prefix; "nonexistent" won't resolve
+ TemplateRepresentation tr =
+ new TemplateRepresentation(
+ "nonexistent", Locale.getDefault(), MediaType.TEXT_PLAIN);
+ assertThrows(IOException.class, tr::getText);
+ }
+
+ @Test
+ void setDataModel_withFormRequest_rendersFormValues() throws Exception {
+ TemplateRepresentation tr =
+ new TemplateRepresentation(
+ "test", buildEngine(), Locale.getDefault(), MediaType.TEXT_PLAIN);
+ Request request = new Request();
+ request.setEntity(
+ new StringRepresentation(
+ "welcome=Hello+from+form", MediaType.APPLICATION_WWW_FORM));
+ tr.setDataModel(request, new Response(request));
+ assertTrue(tr.getText().contains("Hello from form"));
+ }
+
+ @Test
+ void setDataModel_withEmptyFormRequest_rendersTemplateSuccessfully() throws Exception {
+ TemplateRepresentation tr =
+ new TemplateRepresentation(
+ "test", buildEngine(), Locale.getDefault(), MediaType.TEXT_PLAIN);
+ Request request = new Request();
+ request.setEntity(new StringRepresentation("", MediaType.APPLICATION_WWW_FORM));
+ tr.setDataModel(request, new Response(request));
+ assertNotNull(tr.getText());
+ }
+}
diff --git a/org.restlet.ext.thymeleaf/src/test/java/org/restlet/ext/thymeleaf/ThymeleafConverterTestCase.java b/org.restlet.ext.thymeleaf/src/test/java/org/restlet/ext/thymeleaf/ThymeleafConverterTestCase.java
new file mode 100644
index 0000000000..910bc62d2a
--- /dev/null
+++ b/org.restlet.ext.thymeleaf/src/test/java/org/restlet/ext/thymeleaf/ThymeleafConverterTestCase.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.thymeleaf;
+
+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.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.data.Preference;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.Variant;
+import org.thymeleaf.templateresource.ITemplateResource;
+
+/** Unit tests for {@link ThymeleafConverter}. */
+class ThymeleafConverterTestCase {
+
+ private static ITemplateResource dummyResource() {
+ return new ITemplateResource() {
+ @Override
+ public String getDescription() {
+ return "dummy";
+ }
+
+ @Override
+ public String getBaseName() {
+ return "test";
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ @Override
+ public Reader reader() {
+ return new StringReader("");
+ }
+
+ @Override
+ public ITemplateResource relative(String relativeLocation) {
+ return this;
+ }
+ };
+ }
+
+ @Test
+ void getObjectClasses_alwaysReturnsEmptyList() {
+ assertTrue(
+ new ThymeleafConverter()
+ .getObjectClasses(new Variant(MediaType.TEXT_HTML))
+ .isEmpty());
+ }
+
+ @Test
+ void getVariants_forITemplateResource_returnsMediaTypeAll() {
+ List> variants = new ThymeleafConverter().getVariants(ITemplateResource.class);
+ assertEquals(1, variants.size());
+ assertTrue(MediaType.ALL.isCompatible(((Variant) variants.getFirst()).getMediaType()));
+ }
+
+ @Test
+ void getVariants_forOtherClass_returnsEmptyList() {
+ assertTrue(new ThymeleafConverter().getVariants(String.class).isEmpty());
+ }
+
+ @Test
+ void score_iTemplateResource_returnsOne() {
+ assertEquals(
+ 1.0f,
+ new ThymeleafConverter()
+ .score(dummyResource(), new Variant(MediaType.TEXT_HTML), null));
+ }
+
+ @Test
+ void score_nonITemplateResource_returnsMinusOne() {
+ assertEquals(
+ -1.0f,
+ new ThymeleafConverter()
+ .score("not a template", new Variant(MediaType.TEXT_HTML), null));
+ }
+
+ @Test
+ void score_repr_alwaysReturnsMinusOne() {
+ assertEquals(
+ -1.0f,
+ new ThymeleafConverter()
+ .score(new StringRepresentation("text"), String.class, null));
+ }
+
+ @Test
+ void toObject_alwaysReturnsNull() {
+ assertNull(
+ new ThymeleafConverter()
+ .toObject(new StringRepresentation("text"), String.class, null));
+ }
+
+ @Test
+ void toRepresentation_nonITemplateResource_returnsNull() {
+ assertNull(
+ new ThymeleafConverter()
+ .toRepresentation("string", new Variant(MediaType.TEXT_HTML), null));
+ }
+
+ @Test
+ void updatePreferences_forITemplateResource_addsMediaTypeAll() {
+ List
+ * 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.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.dom.DOMSource;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.representation.StringRepresentation;
+import org.w3c.dom.Document;
+
+/** Unit tests for {@link DomRepresentation}. */
+class DomRepresentationTestCase {
+
+ private static final String XML = "
+ * 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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+import javax.xml.transform.sax.SAXSource;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.representation.StringRepresentation;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.helpers.DefaultHandler;
+
+/** Unit tests for {@link SaxRepresentation}. */
+class SaxRepresentationTestCase {
+
+ private static final String XML = "
+ * 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.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 java.util.ArrayList;
+import java.util.List;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.data.Preference;
+import org.restlet.representation.StringRepresentation;
+import org.restlet.representation.Variant;
+import org.w3c.dom.Document;
+
+/** Unit tests for {@link XmlConverter}. */
+class XmlConverterTestCase {
+
+ private static final String XML = "
+ * 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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Iterator;
+import javax.xml.transform.stream.StreamSource;
+import org.junit.jupiter.api.Test;
+import org.restlet.data.MediaType;
+import org.restlet.representation.StringRepresentation;
+import org.w3c.dom.Node;
+
+/**
+ * Unit tests for the XPath API of {@link XmlRepresentation} (exercised via {@link
+ * DomRepresentation}) and for {@link NodeList}.
+ */
+class XmlRepresentationTestCase {
+
+ private static final String XML =
+ ""
+ + "
+ * 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 java.io.ByteArrayOutputStream;
+import java.io.StringWriter;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.AttributesImpl;
+
+/** Unit tests for {@link XmlWriter}. */
+class XmlWriterTestCase {
+
+ private static StringWriter sw() {
+ return new StringWriter();
+ }
+
+ @Test
+ void basicDocument_containsXmlDeclarationAndElement() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.startDocument();
+ w.startElement("root");
+ w.characters("hello");
+ w.endElement("root");
+ w.endDocument();
+ String result = out.toString();
+ assertTrue(result.startsWith(""));
+ assertTrue(result.contains("
"));
+ }
+
+ @Test
+ void emptyElement_uriAndLocalName_writesEmptyTag() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.startDocument();
+ w.startElement("root");
+ w.emptyElement("", "hr");
+ w.endElement("root");
+ w.endDocument();
+ assertTrue(out.toString().contains("
"));
+ }
+
+ @Test
+ void startElement_withAttributes_writesAttributeValue() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ AttributesImpl atts = new AttributesImpl();
+ atts.addAttribute("", "id", "id", "CDATA", "42");
+ w.startDocument();
+ w.startElement("", "item", "", atts);
+ w.endElement("item");
+ w.endDocument();
+ assertTrue(out.toString().contains("id=\"42\""));
+ }
+
+ @Test
+ void characters_specialChars_areEscaped() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.startDocument();
+ w.startElement("root");
+ w.characters("a&c>d");
+ w.endElement("root");
+ w.endDocument();
+ String result = out.toString();
+ assertTrue(result.contains("<"));
+ assertTrue(result.contains("&"));
+ assertTrue(result.contains(">"));
+ }
+
+ @Test
+ void attributeValue_withQuote_isEscaped() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ AttributesImpl atts = new AttributesImpl();
+ atts.addAttribute("", "val", "val", "CDATA", "say \"hello\"");
+ w.startDocument();
+ w.startElement("", "e", "", atts);
+ w.endElement("e");
+ w.endDocument();
+ assertTrue(out.toString().contains("""));
+ }
+
+ @Test
+ void characters_highUnicodeChar_isNumericEscaped() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.startDocument();
+ w.startElement("root");
+ w.characters("café");
+ w.endElement("root");
+ w.endDocument();
+ assertTrue(out.toString().contains("é"));
+ }
+
+ @Test
+ void setDataFormat_isDataFormat_roundTrip() {
+ XmlWriter w = new XmlWriter(sw());
+ assertFalse(w.isDataFormat());
+ w.setDataFormat(true);
+ assertTrue(w.isDataFormat());
+ }
+
+ @Test
+ void setIndentStep_getIndentStep_roundTrip() {
+ XmlWriter w = new XmlWriter(sw());
+ assertEquals(0, w.getIndentStep());
+ w.setIndentStep(4);
+ assertEquals(4, w.getIndentStep());
+ }
+
+ @Test
+ void setPrefix_getPrefix_roundTrip() {
+ XmlWriter w = new XmlWriter(sw());
+ w.setPrefix("http://example.com/", "ex");
+ assertEquals("ex", w.getPrefix("http://example.com/"));
+ }
+
+ @Test
+ void forceNSDecl_withPrefix_declaresOnRootElement() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.forceNSDecl("http://example.com/ns", "ex");
+ w.startDocument();
+ w.startElement("root");
+ w.endElement("root");
+ w.endDocument();
+ assertTrue(out.toString().contains("xmlns:ex=\"http://example.com/ns\""));
+ }
+
+ @Test
+ void processingInstruction_writesPI() throws Exception {
+ StringWriter out = sw();
+ XmlWriter w = new XmlWriter(out);
+ w.startDocument();
+ w.processingInstruction("xml-stylesheet", "type=\"text/css\" href=\"style.css\"");
+ w.startElement("root");
+ w.endElement("root");
+ w.endDocument();
+ assertTrue(out.toString().contains("ok"));
+ }
+
+ @Test
+ void reset_inDataFormatMode_allowsNewDocument() throws Exception {
+ StringWriter first = sw();
+ XmlWriter w = new XmlWriter(first);
+ w.setDataFormat(true);
+ w.setIndentStep(2);
+ w.startDocument();
+ w.startElement("root");
+ w.reset();
+ StringWriter second = sw();
+ w.setOutput(second);
+ w.startDocument();
+ w.dataElement("fresh", "yes");
+ w.endDocument();
+ assertTrue(second.toString().contains("