AddServletTest.java

/*******************************************************************************
 * Copyright (c) 2025 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
// Some portions generated by Codex
package org.eclipse.rdf4j.workbench.commands;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.workbench.util.WorkbenchRequest;
import org.junit.jupiter.api.Test;

class AddServletTest {

	private static final Path ADD_XSL = Paths.get("src", "main", "webapp", "transformations", "add.xsl");

	@Test
	void addPageRendersIsolationOptionsFromResults() throws Exception {
		TransformerFactory factory = TransformerFactory.newInstance();
		StreamSource stylesheet = new StreamSource(ADD_XSL.toFile());
		stylesheet.setSystemId(ADD_XSL.toUri().toString());
		Templates templates = factory.newTemplates(stylesheet);
		Transformer transformer = templates.newTransformer();

		String sparqlResults = ""
				+ "<?xml version=\"1.0\"?>\n"
				+ "<sparql xmlns=\"http://www.w3.org/2005/sparql-results#\">\n"
				+ "  <head />\n"
				+ "  <results>\n"
				+ "    <result>\n"
				+ "      <binding name=\"isolation-level-option\">\n"
				+ "        <literal>NONE</literal>\n"
				+ "      </binding>\n"
				+ "      <binding name=\"isolation-level-option-label\">\n"
				+ "        <literal>None</literal>\n"
				+ "      </binding>\n"
				+ "    </result>\n"
				+ "    <result>\n"
				+ "      <binding name=\"isolation-level-option\">\n"
				+ "        <literal>READ_COMMITTED</literal>\n"
				+ "      </binding>\n"
				+ "      <binding name=\"isolation-level-option-label\">\n"
				+ "        <literal>Read Committed</literal>\n"
				+ "      </binding>\n"
				+ "    </result>\n"
				+ "  </results>\n"
				+ "</sparql>\n";

		StringWriter html = new StringWriter();
		transformer.transform(new StreamSource(new StringReader(sparqlResults)), new StreamResult(html));
		String output = html.toString();

		assertThat(output).contains("value=\"NONE\"")
				.contains(">None<")
				.contains("value=\"READ_COMMITTED\"")
				.contains(">Read Committed<")
				.doesNotContain("value=\"SNAPSHOT\"");
	}

	@Test
	void addPageUsesTransactionSettingParam() throws Exception {
		TransformerFactory factory = TransformerFactory.newInstance();
		StreamSource stylesheet = new StreamSource(ADD_XSL.toFile());
		stylesheet.setSystemId(ADD_XSL.toUri().toString());
		Templates templates = factory.newTemplates(stylesheet);
		Transformer transformer = templates.newTransformer();

		String sparqlResults = ""
				+ "<?xml version=\"1.0\"?>\n"
				+ "<sparql xmlns=\"http://www.w3.org/2005/sparql-results#\">\n"
				+ "  <head />\n"
				+ "  <results />\n"
				+ "</sparql>\n";

		StringWriter html = new StringWriter();
		transformer.transform(new StreamSource(new StringReader(sparqlResults)), new StreamResult(html));
		String output = html.toString();

		assertThat(output)
				.contains("name=\"transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel\"");
	}

	@Test
	void doPostReadsTransactionSettingParameter() throws Exception {
		AddServlet servlet = new AddServlet();
		Repository repository = mock(Repository.class);
		RepositoryConnection connection = mock(RepositoryConnection.class);
		when(repository.getConnection()).thenReturn(connection);
		when(connection.isActive()).thenReturn(true);
		servlet.setRepository(repository);

		WorkbenchRequest request = mock(WorkbenchRequest.class);
		when(request.getParameter("Content-Type")).thenReturn("text/turtle");
		when(request.getParameter("baseURI")).thenReturn("http://example/base");
		when(request.isParameterPresent("context")).thenReturn(false);
		when(request.isParameterPresent("url")).thenReturn(false);
		when(request.getContentParameter()).thenReturn(
				new ByteArrayInputStream("<a> <b> <c> .".getBytes(StandardCharsets.UTF_8)));
		when(request.getContentFileName()).thenReturn("data.ttl");
		when(request.getParameter("transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel"))
				.thenReturn("READ_COMMITTED");

		HttpServletResponse response = mock(HttpServletResponse.class);
		when(response.getOutputStream()).thenReturn(mock(ServletOutputStream.class));

		servlet.doPost(request, response, "");

		verify(connection).commit();
		verify(request).getParameter("transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel");
	}

	@Test
	void serviceUsesTwoColumnsForIsolationLevelOptions() throws Exception {
		AddServlet servlet = new TestAddServlet();

		WorkbenchRequest request = mock(WorkbenchRequest.class);
		when(request.getParameter("transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel"))
				.thenReturn("READ_COMMITTED");

		HttpServletResponse response = mock(HttpServletResponse.class);
		when(response.getOutputStream()).thenReturn(mock(ServletOutputStream.class));

		servlet.service(request, response, "");
	}

	@Test
	void doPostIncludesIsolationLevelBindingInErrorResponse() throws Exception {
		AddServlet servlet = new AddServlet();

		WorkbenchRequest request = mock(WorkbenchRequest.class);
		when(request.getParameter("baseURI")).thenReturn("http://example/base");
		when(request.getParameter("Content-Type")).thenReturn(null);
		when(request.isParameterPresent("context")).thenReturn(false);
		when(request.isParameterPresent("url")).thenReturn(false);
		when(request.getContentParameter()).thenReturn(new ByteArrayInputStream(new byte[0]));
		when(request.getContentFileName()).thenReturn("data.ttl");
		when(request.getParameter("transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel"))
				.thenReturn("READ_COMMITTED");

		HttpServletResponse response = mock(HttpServletResponse.class);
		RecordingServletOutputStream outputStream = new RecordingServletOutputStream();
		when(response.getOutputStream()).thenReturn(outputStream);

		assertThatCode(() -> servlet.doPost(request, response, "transformations")).doesNotThrowAnyException();

		assertThat(outputStream.asString())
				.contains("<binding name='transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel'>")
				.contains(">READ_COMMITTED<");
	}

	@Test
	void doPostErrorIncludesIsolationLevelOptions() throws Exception {
		AddServlet servlet = new RecordingAddServlet();

		WorkbenchRequest request = mock(WorkbenchRequest.class);
		when(request.getParameter("baseURI")).thenReturn("http://example/base");
		when(request.getParameter("Content-Type")).thenReturn(null);
		when(request.isParameterPresent("context")).thenReturn(false);
		when(request.isParameterPresent("url")).thenReturn(false);
		when(request.getContentParameter()).thenReturn(new ByteArrayInputStream(new byte[0]));
		when(request.getContentFileName()).thenReturn("data.ttl");
		when(request.getParameter("transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel"))
				.thenReturn("SNAPSHOT");

		HttpServletResponse response = mock(HttpServletResponse.class);
		RecordingServletOutputStream outputStream = new RecordingServletOutputStream();
		when(response.getOutputStream()).thenReturn(outputStream);

		assertThatCode(() -> servlet.doPost(request, response, "transformations")).doesNotThrowAnyException();

		String output = outputStream.asString();
		assertThat(output)
				.contains("<binding name='isolation-level-option'>")
				.contains("<binding name='isolation-level-option-label'>")
				.contains(">READ_COMMITTED<")
				.contains(">SNAPSHOT<")
				.contains(">Read Committed<")
				.contains(">Snapshot<");
	}

	@Test
	void serviceEmitsSelectedIsolationLevelBinding() throws Exception {
		AddServlet servlet = new RecordingAddServlet();

		WorkbenchRequest request = mock(WorkbenchRequest.class);
		when(request.getParameter("transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel"))
				.thenReturn("SNAPSHOT");

		HttpServletResponse response = mock(HttpServletResponse.class);
		RecordingServletOutputStream outputStream = new RecordingServletOutputStream();
		when(response.getOutputStream()).thenReturn(outputStream);

		servlet.service(request, response, "transformations");

		assertThat(outputStream.asString())
				.contains("<binding name='transaction-setting__org.eclipse.rdf4j.common.transaction.IsolationLevel'>")
				.contains(">SNAPSHOT<");
	}

	private static class TestAddServlet extends AddServlet {

		@Override
		List<String> determineIsolationLevels() {
			return List.of("READ_COMMITTED");
		}
	}

	private static class RecordingAddServlet extends AddServlet {

		@Override
		List<String> determineIsolationLevels() {
			return List.of("READ_COMMITTED", "SNAPSHOT");
		}
	}

	private static class RecordingServletOutputStream extends ServletOutputStream {

		private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

		@Override
		public void write(int b) {
			buffer.write(b);
		}

		@Override
		public boolean isReady() {
			return true;
		}

		@Override
		public void setWriteListener(WriteListener writeListener) {
			// no-op
		}

		String asString() {
			return buffer.toString(StandardCharsets.UTF_8);
		}
	}
}