ProxyTest.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
 *******************************************************************************/
package org.eclipse.rdf4j.http.client;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockserver.model.Header.header;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.rdf4j.http.protocol.Protocol;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.impl.SimpleDataset;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockserver.client.MockServerClient;
import org.mockserver.junit.jupiter.MockServerExtension;
import org.mockserver.matchers.Times;
import org.mockserver.model.MediaType;
import org.mockserver.model.NottableString;
import org.mockserver.verify.VerificationTimes;

/**
 * Unit tests for {@link SPARQLProtocolSession} using standard Java properties for proxy configuration.
 *
 * @author Manuel Fiorelli
 */
@ExtendWith(MockServerExtension.class)
public class ProxyTest {

	// the hostname is guaranteed not to exist (https://datatracker.ietf.org/doc/html/rfc6761#section-6.4)
	String serverURL = "http://rdf4j.invalid/rdf4j-server";
	String repositoryID = "test";

	String proxyUser = "proxyUser";
	String proxyPassword = "proxyPassword";

	/* @Nullable */ String proxyHostOld;
	/* @Nullable */ String proxyPortOld;
	/* @Nullable */ String proxyUserOld;
	/* @Nullable */ String proxyPasswordOld;

	RDF4JProtocolSession sparqlSession;

	@BeforeEach
	public void setUp(MockServerClient client) {
		// Set the system properties related to (non-secured) HTTP proxy.
		// Keep a copy of the old value, if any, to restore it after the execution of the test.
		proxyHostOld = System.setProperty("http.proxyHost", "localhost");
		proxyPortOld = System.setProperty("http.proxyPort", String.valueOf(client.getPort()));
		proxyUserOld = System.setProperty("http.proxyUser", proxyUser);
		proxyPasswordOld = System.setProperty("http.proxyPassword", proxyPassword);

		// Instantiate an RDF4JProtocolSession
		sparqlSession = new SharedHttpClientSessionManager().createRDF4JProtocolSession(serverURL);
		sparqlSession.setQueryURL(Protocol.getRepositoryLocation(serverURL, repositoryID));
		sparqlSession.setUpdateURL(
				Protocol.getStatementsLocation(Protocol.getRepositoryLocation(serverURL, repositoryID)));
	}

	@AfterEach
	public void tearDown() {
		// Restore previous value of the system properties, if any
		restoreSystemProperty("http.proxyHost", proxyHostOld);
		restoreSystemProperty("http.proxyPort", proxyPortOld);
		restoreSystemProperty("http.proxyUser", proxyUserOld);
		restoreSystemProperty("http.proxyPassword", proxyPasswordOld);
	}

	void restoreSystemProperty(String key, /* @Nullable */ String value) {
		if (StringUtils.isNotBlank(value)) {
			System.setProperty(key, value);
		} else {
			System.clearProperty(key);
		}
	}

	@Test
	public void testUserNameAndPassword(MockServerClient client) throws Exception {
		String serverUser = "serverUser";
		String serverPassword = "serverPassword";

		String proxyCredentialsEncoded = Base64.getEncoder()
				.encodeToString((proxyUser + ":" + proxyPassword).getBytes(StandardCharsets.US_ASCII));
		String serverCredentialsEncoded = Base64.getEncoder()
				.encodeToString((serverUser + ":" + serverPassword).getBytes(StandardCharsets.US_ASCII));

		// Mock requests to request proxy and server authentication

		client.when(
				request()
						.withMethod("POST")
						.withPath("/rdf4j-server/repositories/test")
						.withHeader(header(NottableString.not("Proxy-Authorization")))
		)
				.respond(
						response()
								.withStatusCode(407)
								.withHeader("Proxy-Authenticate", "Basic realm=\"rdf4j\"")
				);

		client.when(
				request()
						.withMethod("POST")
						.withPath("/rdf4j-server/repositories/test")
						.withHeader("Proxy-Authorization", "Basic " + proxyCredentialsEncoded)
						.withHeader(header(NottableString.not("Authorization")))
		)
				.respond(
						response()
								.withStatusCode(401)
								.withHeader("WWW-Authenticate", "Basic realm=\"rdf4j\"")
				);

		client.when(
				request()
						.withMethod("POST")
						.withPath("/rdf4j-server/repositories/test")
						.withHeader("Proxy-Authorization", "Basic " + proxyCredentialsEncoded)
						.withHeader("Authorization", "Basic " + serverCredentialsEncoded),
				Times.once()
		)
				.respond(
						response()
								.withStatusCode(200)
								.withContentType(MediaType.parse("application/sparql-results+xml;charset=UTF-8"))
								.withBody("<?xml version='1.0' encoding='UTF-8'?>\n" +
										"<sparql xmlns='http://www.w3.org/2005/sparql-results#'>\n" +
										"    <head>\n" +
										"    </head>\n" +
										"    <boolean>true</boolean>\n" +
										"</sparql>")
				);

		// Set server the credentials for the server
		sparqlSession.setUsernameAndPassword(serverUser, serverPassword);

		// Invoke the test
		boolean response = sparqlSession.sendBooleanQuery(QueryLanguage.SPARQL, "ASK {}", new SimpleDataset(), false);

		// Verifications
		assertThat(response).isTrue();
		client.verify(
				request()
						.withMethod("POST")
						.withPath("/rdf4j-server/repositories/test")
						.withHeader("Proxy-Authorization", "Basic " + proxyCredentialsEncoded)
						.withHeader("Authorization", "Basic " + serverCredentialsEncoded),
				VerificationTimes.once()
		);

	}

}