/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.solr.util; import org.apache.solr.JSONTestUtil; import org.apache.solr.SolrJettyTestBase; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.MultiMapSolrParams; import org.apache.solr.common.util.StrUtils; import org.apache.solr.servlet.SolrRequestParsers; import org.eclipse.jetty.servlet.ServletHolder; import org.junit.AfterClass; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; import javax.xml.xpath.XPathExpressionException; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.util.Map; import java.util.SortedMap; abstract public class RestTestBase extends SolrJettyTestBase { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); protected static RestTestHarness restTestHarness; @AfterClass public static void cleanUpHarness() throws IOException { if (restTestHarness != null) { restTestHarness.close(); } restTestHarness = null; } public static void createJettyAndHarness (String solrHome, String configFile, String schemaFile, String context, boolean stopAtShutdown, SortedMap<ServletHolder,String> extraServlets) throws Exception { createJetty(solrHome, configFile, schemaFile, context, stopAtShutdown, extraServlets); restTestHarness = new RestTestHarness(() -> jetty.getBaseUrl().toString() + "/" + DEFAULT_TEST_CORENAME); } /** Validates an update XML String is successful */ public static void assertU(String update) { assertU(null, update); } /** Validates an update XML String is successful */ public static void assertU(String message, String update) { checkUpdateU(message, update, true); } /** Validates an update XML String failed */ public static void assertFailedU(String update) { assertFailedU(null, update); } /** Validates an update XML String failed */ public static void assertFailedU(String message, String update) { checkUpdateU(message, update, false); } /** Checks the success or failure of an update message */ private static void checkUpdateU(String message, String update, boolean shouldSucceed) { try { String m = (null == message) ? "" : message + " "; if (shouldSucceed) { String response = restTestHarness.validateUpdate(update); if (response != null) fail(m + "update was not successful: " + response); } else { String response = restTestHarness.validateErrorUpdate(update); if (response != null) fail(m + "update succeeded, but should have failed: " + response); } } catch (SAXException e) { throw new RuntimeException("Invalid XML", e); } } /** * Validates a query matches some XPath test expressions * * @param request a URL path with optional query params, e.g. "/schema/fields?fl=id,_version_" */ public static void assertQ(String request, String... tests) { try { int queryStartPos = request.indexOf('?'); String query; String path; if (-1 == queryStartPos) { query = ""; path = request; } else { query = request.substring(queryStartPos + 1); path = request.substring(0, queryStartPos); } if ( ! query.matches(".*wt=schema\\.xml.*")) { // don't overwrite wt=schema.xml query = setParam(query, "wt", "xml"); } request = path + '?' + setParam(query, "indent", "on"); String response = restTestHarness.query(request); // TODO: should the facet handling below be converted to parse the URL? /* if (req.getParams().getBool("facet", false)) { // add a test to ensure that faceting did not throw an exception // internally, where it would be added to facet_counts/exception String[] allTests = new String[tests.length+1]; System.arraycopy(tests,0,allTests,1,tests.length); allTests[0] = "*[count(//lst[@name='facet_counts']/*[@name='exception'])=0]"; tests = allTests; } */ String results = TestHarness.validateXPath(response, tests); if (null != results) { String msg = "REQUEST FAILED: xpath=" + results + "\n\txml response was: " + response + "\n\trequest was:" + request; log.error(msg); throw new RuntimeException(msg); } } catch (XPathExpressionException e1) { throw new RuntimeException("XPath is invalid", e1); } catch (Exception e2) { SolrException.log(log, "REQUEST FAILED: " + request, e2); throw new RuntimeException("Exception during query", e2); } } /** * Makes a query request and returns the JSON string response * * @param request a URL path with optional query params, e.g. "/schema/fields?fl=id,_version_" */ public static String JQ(String request) throws Exception { int queryStartPos = request.indexOf('?'); String query; String path; if (-1 == queryStartPos) { query = ""; path = request; } else { query = request.substring(queryStartPos + 1); path = request.substring(0, queryStartPos); } query = setParam(query, "wt", "json"); request = path + '?' + setParam(query, "indent", "on"); String response; boolean failed=true; try { response = restTestHarness.query(request); failed = false; } finally { if (failed) { log.error("REQUEST FAILED: " + request); } } return response; } /** * Validates a query matches some JSON test expressions using the default double delta tolerance. * @see org.apache.solr.JSONTestUtil#DEFAULT_DELTA * @see #assertJQ(String,double,String...) */ public static void assertJQ(String request, String... tests) throws Exception { assertJQ(request, JSONTestUtil.DEFAULT_DELTA, tests); } /** * Validates a query matches some JSON test expressions and closes the * query. The text expression is of the form path:JSON. To facilitate * easy embedding in Java strings, the JSON can have double quotes * replaced with single quotes. * <p> * Please use this with care: this makes it easy to match complete * structures, but doing so can result in fragile tests if you are * matching more than what you want to test. * </p> * @param request a URL path with optional query params, e.g. "/schema/fields?fl=id,_version_" * @param delta tolerance allowed in comparing float/double values * @param tests JSON path expression + '==' + expected value */ public static void assertJQ(String request, double delta, String... tests) throws Exception { int queryStartPos = request.indexOf('?'); String query; String path; if (-1 == queryStartPos) { query = ""; path = request; } else { query = request.substring(queryStartPos + 1); path = request.substring(0, queryStartPos); } query = setParam(query, "wt", "json"); request = path + '?' + setParam(query, "indent", "on"); String response; boolean failed = true; try { response = restTestHarness.query(request); failed = false; } finally { if (failed) { log.error("REQUEST FAILED: " + request); } } for (String test : tests) { if (null == test || 0 == test.length()) continue; String testJSON = json(test); try { failed = true; String err = JSONTestUtil.match(response, testJSON, delta); failed = false; if (err != null) { log.error("query failed JSON validation. error=" + err + "\n expected =" + testJSON + "\n response = " + response + "\n request = " + request + "\n" ); throw new RuntimeException(err); } } finally { if (failed) { log.error("JSON query validation threw an exception." + "\n expected =" + testJSON + "\n response = " + response + "\n request = " + request + "\n" ); } } } } /** * Validates the response from a PUT request matches some JSON test expressions * * @see org.apache.solr.JSONTestUtil#DEFAULT_DELTA * @see #assertJQ(String,double,String...) */ public static void assertJPut(String request, String content, String... tests) throws Exception { assertJPut(request, content, JSONTestUtil.DEFAULT_DELTA, tests); } /** * Validates the response from a PUT request matches some JSON test expressions * and closes the query. The text expression is of the form path==JSON. * To facilitate easy embedding in Java strings, the JSON can have double * quotes replaced with single quotes. * <p> * Please use this with care: this makes it easy to match complete * structures, but doing so can result in fragile tests if you are * matching more than what you want to test. * </p> * @param request a URL path with optional query params, e.g. "/schema/fields?fl=id,_version_" * @param content The content to include with the PUT request * @param delta tolerance allowed in comparing float/double values * @param tests JSON path expression + '==' + expected value */ public static void assertJPut(String request, String content, double delta, String... tests) throws Exception { int queryStartPos = request.indexOf('?'); String query; String path; if (-1 == queryStartPos) { query = ""; path = request; } else { query = request.substring(queryStartPos + 1); path = request.substring(0, queryStartPos); } query = setParam(query, "wt", "json"); request = path + '?' + setParam(query, "indent", "on"); String response; boolean failed = true; try { response = restTestHarness.put(request, content); failed = false; } finally { if (failed) { log.error("REQUEST FAILED: " + request); } } for (String test : tests) { if (null == test || 0 == test.length()) continue; String testJSON = json(test); try { failed = true; String err = JSONTestUtil.match(response, testJSON, delta); failed = false; if (err != null) { log.error("query failed JSON validation. error=" + err + "\n expected =" + testJSON + "\n response = " + response + "\n request = " + request + "\n" ); throw new RuntimeException(err); } } finally { if (failed) { log.error("JSON query validation threw an exception." + "\n expected =" + testJSON + "\n response = " + response + "\n request = " + request + "\n" ); } } } } /** * Validates the response from a POST request matches some JSON test expressions * * @see org.apache.solr.JSONTestUtil#DEFAULT_DELTA * @see #assertJQ(String,double,String...) */ public static void assertJPost(String request, String content, String... tests) throws Exception { assertJPost(request, content, JSONTestUtil.DEFAULT_DELTA, tests); } /** * Validates the response from a PUT request matches some JSON test expressions * and closes the query. The text expression is of the form path==JSON. * To facilitate easy embedding in Java strings, the JSON can have double * quotes replaced with single quotes. * <p> * Please use this with care: this makes it easy to match complete * structures, but doing so can result in fragile tests if you are * matching more than what you want to test. * </p> * @param request a URL path with optional query params, e.g. "/schema/fields?fl=id,_version_" * @param content The content to include with the PUT request * @param delta tolerance allowed in comparing float/double values * @param tests JSON path expression + '==' + expected value */ public static void assertJPost(String request, String content, double delta, String... tests) throws Exception { int queryStartPos = request.indexOf('?'); String query; String path; if (-1 == queryStartPos) { query = ""; path = request; } else { query = request.substring(queryStartPos + 1); path = request.substring(0, queryStartPos); } query = setParam(query, "wt", "json"); request = path + '?' + setParam(query, "indent", "on"); String response; boolean failed = true; try { response = restTestHarness.post(request, content); failed = false; } finally { if (failed) { log.error("REQUEST FAILED: " + request); } } for (String test : tests) { if (null == test || 0 == test.length()) continue; String testJSON = json(test); try { failed = true; String err = JSONTestUtil.match(response, testJSON, delta); failed = false; if (err != null) { log.error("query failed JSON validation. error=" + err + "\n expected =" + testJSON + "\n response = " + response + "\n request = " + request + "\n" ); throw new RuntimeException(err); } } finally { if (failed) { log.error("JSON query validation threw an exception." + "\n expected =" + testJSON + "\n response = " + response + "\n request = " + request + "\n" ); } } } } /** * Deletes a resource and then matches some JSON test expressions against the * response using the default double delta tolerance. * @see org.apache.solr.JSONTestUtil#DEFAULT_DELTA * @see #assertJDelete(String,double,String...) */ public static void assertJDelete(String request, String... tests) throws Exception { assertJDelete(request, JSONTestUtil.DEFAULT_DELTA, tests); } /** * Deletes a resource and then matches some JSON test expressions against the * response using the specified double delta tolerance. */ public static void assertJDelete(String request, double delta, String... tests) throws Exception { int queryStartPos = request.indexOf('?'); String query; String path; if (-1 == queryStartPos) { query = ""; path = request; } else { query = request.substring(queryStartPos + 1); path = request.substring(0, queryStartPos); } query = setParam(query, "wt", "json"); request = path + '?' + setParam(query, "indent", "on"); String response; boolean failed = true; try { response = restTestHarness.delete(request); failed = false; } finally { if (failed) { log.error("REQUEST FAILED: " + request); } } for (String test : tests) { if (null == test || 0 == test.length()) continue; String testJSON = json(test); try { failed = true; String err = JSONTestUtil.match(response, testJSON, delta); failed = false; if (err != null) { log.error("query failed JSON validation. error=" + err + "\n expected =" + testJSON + "\n response = " + response + "\n request = " + request + "\n" ); throw new RuntimeException(err); } } finally { if (failed) { log.error("JSON query validation threw an exception." + "\n expected =" + testJSON + "\n response = " + response + "\n request = " + request + "\n" ); } } } } /** * Insures that the given param is included in the query with the given value. * * <ol> * <li>If the param is already included with the given value, the request is returned unchanged.</li> * <li>If the param is not already included, it is added with the given value.</li> * <li>If the param is already included, but with a different value, the value is replaced with the given value.</li> * <li>If the param is already included multiple times, they are replaced with a single param with given value.</li> * </ol> * * The passed-in valueToSet should NOT be URL encoded, as it will be URL encoded by this method. * * @param query The query portion of a request URL, e.g. "wt=json&indent=on&fl=id,_version_" * @param paramToSet The parameter name to insure the presence of in the returned request * @param valueToSet The parameter value to insure in the returned request * @return The query with the given param set to the given value */ private static String setParam(String query, String paramToSet, String valueToSet) { if (null == valueToSet) { valueToSet = ""; } try { StringBuilder builder = new StringBuilder(); if (null == query || query.trim().isEmpty()) { // empty query -> return "paramToSet=valueToSet" builder.append(paramToSet); builder.append('='); StrUtils.partialURLEncodeVal(builder, valueToSet); return builder.toString(); } MultiMapSolrParams requestParams = SolrRequestParsers.parseQueryString(query); String[] values = requestParams.getParams(paramToSet); if (null == values) { // paramToSet isn't present in the request -> append "¶mToSet=valueToSet" builder.append(query); builder.append('&'); builder.append(paramToSet); builder.append('='); StrUtils.partialURLEncodeVal(builder, valueToSet); return builder.toString(); } if (1 == values.length && valueToSet.equals(values[0])) { // paramToSet=valueToSet is already in the query - just return the query as-is. return query; } // More than one value for paramToSet on the request, or paramToSet's value is not valueToSet // -> rebuild the query boolean isFirst = true; for (Map.Entry<String,String[]> entry : requestParams.getMap().entrySet()) { String key = entry.getKey(); String[] valarr = entry.getValue(); if ( ! key.equals(paramToSet)) { for (String val : valarr) { builder.append(isFirst ? "" : '&'); isFirst = false; builder.append(key); builder.append('='); StrUtils.partialURLEncodeVal(builder, null == val ? "" : val); } } } builder.append(isFirst ? "" : '&'); builder.append(paramToSet); builder.append('='); StrUtils.partialURLEncodeVal(builder, valueToSet); return builder.toString(); } catch (IOException e) { throw new RuntimeException(e); } } }