package lux.it; import static org.junit.Assert.*; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; import java.net.MalformedURLException; import java.net.URLEncoder; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.xml.transform.sax.SAXSource; import lux.Evaluator; import lux.QueryContext; import net.sf.saxon.s9api.DocumentBuilder; import net.sf.saxon.s9api.Processor; import net.sf.saxon.s9api.SaxonApiException; import net.sf.saxon.s9api.XdmNode; import net.sf.saxon.s9api.XdmItem; import net.sf.saxon.trans.XPathException; import nu.validator.htmlparser.sax.HtmlParser; import org.apache.commons.io.IOUtils; import org.junit.BeforeClass; import org.junit.Test; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.meterware.httpunit.HttpUnitOptions; import com.meterware.httpunit.PostMethodWebRequest; import com.meterware.httpunit.WebClient; import com.meterware.httpunit.WebConversation; import com.meterware.httpunit.WebResponse; /** * basic test to make sure the app server is functioning - the app server can be passed a query by * reference to a document, rather than as an inline query, and serializes xquery results * directly as a stream, rather than wrapping them up in one of the standard solr query response * structures. * */ public class SolrIT { private final String TEST_SERVER_PATH = "http://localhost:8080/collection1/testapp"; private final String APP_SERVER_PATH = "http://localhost:8080/collection1/testapp"; private final String XQUERY_PATH = "http://localhost:8080/collection1/xquery"; private final String LUX_PATH = "http://localhost:8080/collection1/lux/"; private static WebClient httpclient; private static Evaluator eval; @BeforeClass public static void setup () { httpclient = new WebConversation(); httpclient.setExceptionsThrownOnErrorStatus(false); eval = new Evaluator(); HttpUnitOptions.setScriptingEnabled(false); } @Test public void testAppServer () throws Exception { String path = (TEST_SERVER_PATH + "?lux.xquery=lux/compiler/minus-1.xqy"); String response = httpclient.getResponse(path).getText(); assertEquals ("1", response); path = (APP_SERVER_PATH + "/lux/compiler/minus-1.xqy"); response = httpclient.getResponse(path).getText(); assertEquals ("1", response); } @Test public void testNoDirectoryListing() throws Exception { String path = (TEST_SERVER_PATH + "?lux.xquery=lux/"); WebResponse response = httpclient.getResponse(path); assertEquals (403, response.getResponseCode()); // FIXME? directory path not mapped to AppServerComponent which only filters *.xq* /* path = APP_SERVER_PATH + "/lux/"; response = httpclient.getResponse(path); assertEquals (403, response.getResponseCode()); */ } @Test public void testSyntaxError () throws Exception { String path = (TEST_SERVER_PATH + "?lux.xquery=lux/functions/transform-error.xqy"); WebResponse httpResponse = httpclient.getResponse(path); assertEquals (400, httpResponse.getResponseCode()); assertEquals ("Bad Request", httpResponse.getResponseMessage()); String response = httpResponse.getText(); assertTrue ("Unexpected error message:\n" + response, response.contains ("The supplied file does not appear to be a stylesheet")); } @Test public void testNotFound () throws Exception { String path = (LUX_PATH + "notfound.xqy"); WebResponse response = httpclient.getResponse(path); assertEquals (404, response.getResponseCode()); } @Test public void testParameterMap () throws Exception { String path = (TEST_SERVER_PATH + "?lux.xquery=lux/solr/test-params.xqy&p1=A&p2=B&p2=C"); String response = httpclient.getResponse(path).getText(); // This test depends on the order in which keys are retrieved from a java.util.HashMap String expected = "<http method=\"\"><params>" + "<parm name=\"wt\"><value>lux</value></parm>" + "<parm name=\"p2\"><value>B</value><value>C</value></parm>" + "<parm name=\"p1\"><value>A</value></parm>" + "</params></http>"; assertEquals (expected, response.replaceAll("\n\\s*","")); path = APP_SERVER_PATH + "/lux/solr/test-params.xqy?p1=A&p2=B&p2=C"; response = httpclient.getResponse(path).getText(); assertEquals (expected, response.replaceAll("\n\\s*","")); } @Test public void testExhaustResultSetMemory () throws Exception { String path = (TEST_SERVER_PATH + "?lux.xquery=lux/solr/huge-result.xqy"); WebResponse httpResponse = httpclient.getResponse(path); httpResponse.getElementsWithName("error"); // caught a ResourceExhaustedException assertTrue ("did not find expected error", httpResponse.getText().contains("Maximum result size exceeded, returned result has been truncated")); // some results were returned nonetheless assertTrue ("did not find result", httpResponse.getText().contains("abracadabra")); // abuse this code which means something vaguely similar // not right, but we can't easily influence this in Solr land assertEquals (httpResponse.getText(), 200, httpResponse.getResponseCode()); } @Test public void testResultFormat () throws Exception { verifyMultiThreadedWrites(); // load test documents String path = (XQUERY_PATH + "?q=subsequence(for $x in collection() order by xs:int($x//@id) return $x,1,2)&lux.contentType=text/xml&wt=lux"); WebResponse httpResponse = httpclient.getResponse(path); String expected = "<results><doc><title id=\"1\">100</title><test>cat</test></doc><doc><title id=\"2\">99</title><test>cat</test></doc></results>"; assertEquals (expected, httpResponse.getText()); path = APP_SERVER_PATH + "/lux/it/atomic-sequence.xqy?lux.contentType=text/xml"; httpResponse = httpclient.getResponse(path); assertEquals (expected, httpResponse.getText()); } @Test public void testAttributeEncodingInJSON () throws Exception { String xmlDoc = "<doc title=\"title with <tag> & "quotes" in it\" />"; String xmlDocEnc = URLEncoder.encode(xmlDoc, "utf-8"); String path = (XQUERY_PATH + "?q=" + xmlDocEnc + "&wt=json&lux.contentType=text/xml"); WebResponse httpResponse = httpclient.getResponse(path); String resp = httpResponse.getText(); assertEquals ("xpath-results\":[\"element\",\"<doc title=\\\"title with <tag> & "quotes" in it\\\"/>\"]}\n", resp.substring(resp.indexOf("xpath-results"))); } /* * Ensure that we can write multiple documents in parallel. */ private void verifyMultiThreadedWrites () throws Exception { get ("concat(lux:delete('lux:/'), lux:commit(), 'OK')"); ExecutorService taskExecutor = Executors.newFixedThreadPool(1); for (int i = 1; i <= 30; i++) { taskExecutor.execute(new TestDocInsert (i)); } taskExecutor.shutdown(); taskExecutor.awaitTermination(1, TimeUnit.SECONDS); get ("lux:commit()"); for (int i = 1; i <= 30; i++) { WebResponse response = get ("doc('/test/" + i + "')"); assertEquals (createTestDocument(i).replaceAll("\\s+", ""), response.getText().replaceAll("\\s+", "")); } } class TestDocInsert implements Runnable { final int id; TestDocInsert (int n) { id = n; } @Override public void run () { String insert = "let $i := lux:insert('/test/" + id + "'," + createTestDocument(id) + ") return concat('OK', $i)"; try { WebResponse response = get (insert); assertEquals ("OK", response.getText()); } catch (MalformedURLException e) { fail (e.getMessage()); } catch (IOException e) { fail (e.getMessage()); } catch (SAXException e) { fail (e.getMessage()); } } } /* Now make sure that our OutputURIResolver (which handles result documents from XSLT) * is thread-safe. */ @Test public void testMTOutputURIResolver () throws Exception { get ("concat(lux:delete('lux:/'), lux:commit(), 'OK')"); long start = System.currentTimeMillis(); ExecutorService taskExecutor = Executors.newFixedThreadPool(4); for (int i = 1; i <= 30; i++) { taskExecutor.execute(new TestDocInsertMulti (i)); } taskExecutor.shutdown(); taskExecutor.awaitTermination(5, TimeUnit.SECONDS); long elapsed = System.currentTimeMillis() - start; System.out.println ("elapsed=" + elapsed); get ("lux:commit()"); for (int i = 1; i <= 30; i++) { WebResponse response = get ("doc('/doc/" + i + "')"); assertEquals (createTestDocument(i).replaceAll("\\s+", ""), response.getText().replaceAll("\\s+", "")); get ("doc('/doc/" + i + "/0/0')"); get ("doc('/doc/" + i + "/1/0')"); get ("doc('/doc/" + i + "/1/1')"); } } class TestDocInsertMulti implements Runnable { final int id; TestDocInsertMulti (int n) { id = n; } @Override public void run () { String insert = "let $doc := " + createTestDocument(id) + " let $trans := lux:transform(<xsl:stylesheet version='2.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>" + " <xsl:template match='*'>" + " <xsl:variable name='this' select='.' />" + " <xsl:result-document href='/doc/{$doc/title/@id}/{{count($this/ancestor::*)}}/{{count($this/preceding-sibling::*)}}'><xsl:copy-of select='.'/></xsl:result-document>" + " <e><xsl:apply-templates /></e>" + " </xsl:template>" + "</xsl:stylesheet>, $doc)" + " let $i := lux:insert('/doc/" + id + "',$doc) " + " return concat('OK', $i, $trans)"; try { WebResponse response = get (insert); assertTrue (response.getText().startsWith("OK")); } catch (MalformedURLException e) { fail (e.getMessage()); } catch (IOException e) { fail (e.getMessage()); } catch (SAXException e) { fail (e.getMessage()); } } } @Test public void testHttpPost () throws Exception { WebResponse resp = post ("/lux/it/echo-params.xqy", "test", "value"); assertEquals ("value", resp.getText()); resp = post ("/lux/it/echo-request.xqy", "test", "value"); XdmNode rspDoc = parseResponseBody(resp); QueryContext context = new QueryContext (rspDoc); assertEquals ("POST", evalString("/request/@method", context)); assertEquals ("value", evalString("/request/param[@name='test']/@value", context)); } @Test public void testPostXQueryServlet () throws Exception { String query = IOUtils.toString(getClass().getResourceAsStream("/lux/it/echo-request.xqy")); WebResponse resp = postToXQuery (query, "test", "value", "wt", "lux"); XdmNode rspDoc = parseResponseBody(resp); QueryContext context = new QueryContext (rspDoc); assertEquals ("POST", evalString("/request/@method", context)); assertEquals ("value", evalString("/request/param[@name='test']/@value", context)); } private XdmNode parseResponseBody (WebResponse resp) throws IOException { String body = resp.getText(); assertTrue ("empty body", body.length() > 0); eval.getCompiler().bindNamespacePrefix ("", "http://expath.org/ns/webapp"); XdmNode rspDoc = eval.build(new StringReader(body), "/test.xml"); return rspDoc; } @Test public void testValidatorNu() throws SaxonApiException { HtmlParser parser = new HtmlParser(); Processor processor = new Processor (false); DocumentBuilder builder = processor.newDocumentBuilder(); SAXSource source = new SAXSource (parser, new InputSource(new StringReader ("<!DOCTYPE html><br>"))); builder.build(source); source = new SAXSource (parser, new InputSource(new StringReader ("<html>"))); builder.build(source); } /* * Test cases for EXPath: * * html body * * multipart */ @Test public void testEXPathRequest () throws Exception { String path = "/lux/it/echo-request.xqy"; String qs = "a=b&a=c&c=d"; String url = APP_SERVER_PATH + path + '?' + qs; WebResponse response = httpclient.getResponse(url); // response envelope assertEquals (200, response.getResponseCode()); assertEquals ("OK", response.getResponseMessage()); assertEquals ("application/xml+test", response.getContentType()); assertEquals ("utf-8", response.getCharacterSet()); XdmNode rspDoc = parseResponseBody(response); QueryContext context = new QueryContext(rspDoc); // method assertEquals ("GET", evalString("/request/@method", context)); // servlet assertEquals ("/collection1/testapp", evalString("/request/@servlet", context)); // path (attribute) assertEquals ("/collection1/testapp" + path, evalString("/request/@path", context)); // url assertEquals (url, evalString("/request/url", context)); // authority assertEquals ("http://localhost:8080", evalString("/request/authority", context)); // context assertEquals ("", evalString ("/request/context-root", context)); // path assertEquals ("/collection1/testapp" + path, evalString ("/request/path", context)); assertEquals ("/collection1/testapp" + path, evalString ("/request/path/part", context)); // params // depends on the order of hashmap keys; should be stable since String.hashCode is well-defined? assertEquals ("a", evalString ("(for $p in /request/param order by $p/@name return $p/@name)[1]", context)); assertEquals ("b", evalString ("(for $p in /request/param order by $p/@name return $p/@value)[1]", context)); assertEquals ("a", evalString ("(for $p in /request/param order by $p/@name return $p/@name)[2]", context)); assertEquals ("c", evalString ("(for $p in /request/param order by $p/@name return $p/@value)[2]", context)); assertEquals ("c", evalString ("(for $p in /request/param order by $p/@name return $p/@name)[3]", context)); assertEquals ("d", evalString ("(for $p in /request/param order by $p/@name return $p/@value)[3]", context)); // header assertEquals ("httpunit/1.5", evalString ("/request/header[@name='User-Agent']/@value", context)); assertEquals ("", evalString ("/request/body", context)); } @Test public void testPostBody () throws Exception { WebResponse resp = postMime ("/lux/it/echo-multipart.xqy", "<test>this is a test</test>", "text/xml"); XdmNode rspDoc = parseResponseBody(resp); QueryContext context = new QueryContext (rspDoc); assertEquals ("POST", evalString("/result-sequence/request/@method", context)); assertEquals ("this is a test", evalString("/result-sequence/part", context)); } @Test public void testPostIllFormedXML () throws Exception { WebResponse resp = postMime ("/lux/it/echo-multipart.xqy", "<test>this is a test", "text/xml"); XdmNode rspDoc = parseResponseBody(resp); QueryContext context = new QueryContext (rspDoc); assertEquals ("POST", evalString("/result-sequence/request/@method", context)); assertEquals ("<test>this is a test", evalString("/result-sequence/part", context)); assertEquals ("text/plain; charset=utf-8", evalString("/result-sequence/part/@content-type", context)); } @Test public void testPostHTML() throws Exception { String html = "<!DOCTYPE html>\n<html>this is a <br>test</html>"; WebResponse resp = postMime ("/lux/it/echo-multipart.xqy", html, "text/html"); XdmNode rspDoc = parseResponseBody(resp); QueryContext context = new QueryContext (rspDoc); assertEquals ("POST", evalString("/result-sequence/request/@method", context)); assertEquals ("this is a test", evalString("/result-sequence/part", context)); assertEquals ("text/html", evalString("/result-sequence/part/@content-type", context)); } private String evalString (String query, QueryContext context) throws XPathException { StringBuilder buf = new StringBuilder(); for (XdmItem item : eval.evaluate (query, context).getXdmValue()) { buf.append (item.getStringValue()); } return buf.toString(); } /** * test cases: * * redirect (302) * not found (404) * normal (200) * * control mime-type (html, text) binary? * * @throws Exception */ @Test public void testEXPathResponse () throws Exception { } @Test public void testRedirect () throws Exception { WebResponse rsp = httpclient.getResponse(APP_SERVER_PATH + "/lux/it/302.xqy"); // http client follows the redirection: assertEquals (200, rsp.getResponseCode()); XdmNode rspDoc = parseResponseBody(rsp); QueryContext context = new QueryContext (rspDoc); assertEquals ("/collection1/testapp/lux/it/echo-request.xqy" , evalString("/request/@path", context)); } @Test public void test404 () throws Exception { WebResponse rsp = httpclient.getResponse(APP_SERVER_PATH + "/lux/it/404.xqy"); assertEquals (404, rsp.getResponseCode()); assertEquals ("text/html", rsp.getContentType()); // can't create a custom 404 page *in xquery*. //assertEquals ("Sorry, not here, not now.", rsp.getText()); } private WebResponse get (String xquery) throws MalformedURLException, IOException, SAXException { WebResponse response = httpclient.getResponse(XQUERY_PATH + "?wt=lux&q=" + xquery); assertEquals (200, response.getResponseCode()); return response; } private WebResponse post (String xquery, String ... params) throws MalformedURLException, IOException, SAXException { PostMethodWebRequest req = new PostMethodWebRequest(APP_SERVER_PATH + xquery); for (int i = 0; i < params.length; i+= 2) { req.setParameter(params[i], params[i+1]); } assertEquals ("", req.getQueryString()); WebResponse response = httpclient.sendRequest(req); assertEquals (200, response.getResponseCode()); return response; } private WebResponse postToXQuery (String xquery, String ... params) throws MalformedURLException, IOException, SAXException { PostMethodWebRequest req = new PostMethodWebRequest(XQUERY_PATH); req.setParameter ("q", xquery); for (int i = 0; i < params.length; i+= 2) { req.setParameter(params[i], params[i+1]); } assertEquals ("", req.getQueryString()); WebResponse response = httpclient.sendRequest(req); assertEquals (200, response.getResponseCode()); return response; } private WebResponse postMime (String xquery, String body, String contentType) throws MalformedURLException, IOException, SAXException { return postMime (xquery, body.getBytes("utf-8"), contentType); } private WebResponse postMime (String xquery, byte[] body, String contentType) throws MalformedURLException, IOException, SAXException { PostMethodWebRequest req = new PostMethodWebRequest(APP_SERVER_PATH + xquery, new ByteArrayInputStream(body), contentType); assertEquals ("", req.getQueryString()); WebResponse response = httpclient.sendRequest(req); assertEquals (200, response.getResponseCode()); return response; } private String createTestDocument(int i) { return "<doc><title id=\"" + i + "\">" + (101-i) + "</title><test>cat</test></doc>"; } }