summaryrefslogtreecommitdiffstats
path: root/vespaclient-container-plugin/src/test
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /vespaclient-container-plugin/src/test
Publish
Diffstat (limited to 'vespaclient-container-plugin/src/test')
-rw-r--r--vespaclient-container-plugin/src/test/application/services.xml17
-rw-r--r--vespaclient-container-plugin/src/test/files/feedhandler/documentmanager.cfg113
-rw-r--r--vespaclient-container-plugin/src/test/files/feedhandler/test10.xml91
-rw-r--r--vespaclient-container-plugin/src/test/files/feedhandler/test10b.xml53
-rw-r--r--vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_docid.xml48
-rwxr-xr-xvespaclient-container-plugin/src/test/files/feedhandler/test_bogus_docid_first.xml43
-rwxr-xr-xvespaclient-container-plugin/src/test/files/feedhandler/test_bogus_xml.xml44
-rwxr-xr-xvespaclient-container-plugin/src/test/files/feedhandler/test_removes2
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/OperationHandlerImplTest.java75
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/RestUriTest.java109
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/feed-document1.json0
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/MockedOperationHandler.java59
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiMaxThreadTest.java54
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiTest.java298
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiWithTestDocumentHandler.java36
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/externalfeeding/server/.gitignore0
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/FeedHandlerTest.java103
-rwxr-xr-xvespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/VespaFeedHandlerTestCase.java1015
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/v3/FeedTesterV3.java134
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/ContinuationHitTest.java103
-rwxr-xr-xvespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/DocumentSessionFactory.java129
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/DummyVisitorSession.java98
-rwxr-xr-xvespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/GetSearcherTestCase.java1090
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/ResultRenderingUtil.java25
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/VisitorSearcherTestCase.java248
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/DummyMetric.java31
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/FeedHandlerCompressionTest.java70
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MetaStream.java39
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MockNetwork.java69
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MockReply.java35
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2ErrorsInResultTestCase.java236
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2ExternalFeedTestCase.java535
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2FailingMessagebusTestCase.java224
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2NoXmlReaderTestCase.java162
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V3CongestionTestCase.java160
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/VersionsTestCase.java104
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/util/ByteLimitedInputStreamTestCase.java91
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespaxmlparser/MockFeedReaderFactory.java28
-rw-r--r--vespaclient-container-plugin/src/test/java/com/yahoo/vespaxmlparser/MockReader.java75
39 files changed, 5846 insertions, 0 deletions
diff --git a/vespaclient-container-plugin/src/test/application/services.xml b/vespaclient-container-plugin/src/test/application/services.xml
new file mode 100644
index 00000000000..df178e109c3
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/application/services.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<jdisc version="1.0" jetty="true">
+
+ <handler id="com.yahoo.document.restapi.resource.RestApiWithTestDocumentHandler" bundle="integration-test">
+ <binding>http://*/document/v1/*</binding>
+ </handler>
+
+ <component id="injected" class="com.yahoo.document.restapi.resource.MockedOperationHandler" bundle="integration-test">
+ </component>
+
+
+ <http>
+ <!-- This indicates that we want JDisc to allocate a port for us -->
+ <server id="mainServer" port="0" />
+ </http>
+</jdisc>
diff --git a/vespaclient-container-plugin/src/test/files/feedhandler/documentmanager.cfg b/vespaclient-container-plugin/src/test/files/feedhandler/documentmanager.cfg
new file mode 100644
index 00000000000..6878ddffb79
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/files/feedhandler/documentmanager.cfg
@@ -0,0 +1,113 @@
+enablecompression false
+datatype[10]
+datatype[0].id 1002
+datatype[0].arraytype[1]
+datatype[0].arraytype[0].datatype 2
+datatype[0].weightedsettype[0]
+datatype[0].structtype[0]
+datatype[0].documenttype[0]
+datatype[1].id 1000
+datatype[1].arraytype[1]
+datatype[1].arraytype[0].datatype 0
+datatype[1].weightedsettype[0]
+datatype[1].structtype[0]
+datatype[1].documenttype[0]
+datatype[2].id 1004
+datatype[2].arraytype[1]
+datatype[2].arraytype[0].datatype 4
+datatype[2].weightedsettype[0]
+datatype[2].structtype[0]
+datatype[2].documenttype[0]
+datatype[3].id 1016
+datatype[3].arraytype[1]
+datatype[3].arraytype[0].datatype 16
+datatype[3].weightedsettype[0]
+datatype[3].structtype[0]
+datatype[3].documenttype[0]
+datatype[4].id 1001
+datatype[4].arraytype[1]
+datatype[4].arraytype[0].datatype 1
+datatype[4].weightedsettype[0]
+datatype[4].structtype[0]
+datatype[4].documenttype[0]
+datatype[5].id 2001
+datatype[5].arraytype[0]
+datatype[5].weightedsettype[1]
+datatype[5].weightedsettype[0].datatype 0
+datatype[5].weightedsettype[0].createifnonexistant false
+datatype[5].weightedsettype[0].removeifzero false
+datatype[5].structtype[0]
+datatype[5].documenttype[0]
+datatype[6].id 2002
+datatype[6].arraytype[0]
+datatype[6].weightedsettype[1]
+datatype[6].weightedsettype[0].datatype 2
+datatype[6].weightedsettype[0].createifnonexistant false
+datatype[6].weightedsettype[0].removeifzero false
+datatype[6].structtype[0]
+datatype[6].documenttype[0]
+datatype[7].id -628990518
+datatype[7].arraytype[0]
+datatype[7].weightedsettype[0]
+datatype[7].structtype[1]
+datatype[7].structtype[0].name news.header
+datatype[7].structtype[0].version 0
+datatype[7].structtype[0].field[6]
+datatype[7].structtype[0].field[0].name url
+datatype[7].structtype[0].field[0].id[0]
+datatype[7].structtype[0].field[0].datatype 10
+datatype[7].structtype[0].field[1].name title
+datatype[7].structtype[0].field[1].id[0]
+datatype[7].structtype[0].field[1].datatype 2
+datatype[7].structtype[0].field[2].name last_downloaded
+datatype[7].structtype[0].field[2].id[0]
+datatype[7].structtype[0].field[2].datatype 0
+datatype[7].structtype[0].field[3].name value_long
+datatype[7].structtype[0].field[3].id[0]
+datatype[7].structtype[0].field[3].datatype 4
+datatype[7].structtype[0].field[4].name value_content
+datatype[7].structtype[0].field[4].id[0]
+datatype[7].structtype[0].field[4].datatype 3
+datatype[7].structtype[0].field[5].name stringarr
+datatype[7].structtype[0].field[5].id[0]
+datatype[7].structtype[0].field[5].datatype 1002
+datatype[7].documenttype[0]
+datatype[8].id 538588767
+datatype[8].arraytype[0]
+datatype[8].weightedsettype[0]
+datatype[8].structtype[1]
+datatype[8].structtype[0].name news.body
+datatype[8].structtype[0].version 0
+datatype[8].structtype[0].field[7]
+datatype[8].structtype[0].field[0].name intarr
+datatype[8].structtype[0].field[0].id[0]
+datatype[8].structtype[0].field[0].datatype 1000
+datatype[8].structtype[0].field[1].name longarr
+datatype[8].structtype[0].field[1].id[0]
+datatype[8].structtype[0].field[1].datatype 1004
+datatype[8].structtype[0].field[2].name bytearr
+datatype[8].structtype[0].field[2].id[0]
+datatype[8].structtype[0].field[2].datatype 1016
+datatype[8].structtype[0].field[3].name floatarr
+datatype[8].structtype[0].field[3].id[0]
+datatype[8].structtype[0].field[3].datatype 1001
+datatype[8].structtype[0].field[4].name weightedsetint
+datatype[8].structtype[0].field[4].id[0]
+datatype[8].structtype[0].field[4].datatype 2001
+datatype[8].structtype[0].field[5].name weightedsetstring
+datatype[8].structtype[0].field[5].id[0]
+datatype[8].structtype[0].field[5].datatype 2002
+datatype[8].structtype[0].field[6].name content
+datatype[8].structtype[0].field[6].id[0]
+datatype[8].structtype[0].field[6].datatype 3
+datatype[8].documenttype[0]
+datatype[9].id -1048827947
+datatype[9].arraytype[0]
+datatype[9].weightedsettype[0]
+datatype[9].structtype[0]
+datatype[9].documenttype[1]
+datatype[9].documenttype[0].name news
+datatype[9].documenttype[0].version 0
+datatype[9].documenttype[0].inherits[0]
+datatype[9].documenttype[0].headerstruct -628990518
+datatype[9].documenttype[0].bodystruct 538588767
diff --git a/vespaclient-container-plugin/src/test/files/feedhandler/test10.xml b/vespaclient-container-plugin/src/test/files/feedhandler/test10.xml
new file mode 100644
index 00000000000..52739246b1f
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/files/feedhandler/test10.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+
+<!--
+ Document : test10.xml
+ Created on : July 27, 2007, 11:37 AM
+ Author : alimf
+ Description:
+ this feed contains both documents, updates and removes.
+-->
+
+<vespafeed>
+
+ <document documenttype="news" documentid="doc:news:http://news10a">
+ <url>testUrl</url>
+ <title>testTitle</title>
+ <last_downloaded>1</last_downloaded>
+ <value_long>2</value_long>
+ <value_content>testValueContent</value_content>
+ <stringarr>
+ <item>stringarrItem1</item>
+ <item>stringarrItem2</item>
+ </stringarr>
+ <intarr>
+ <item>3</item>
+ <item>4</item>
+ </intarr>
+ <longarr>
+ <item>5</item>
+ <item>6</item>
+ </longarr>
+ <bytearr>
+ <item>7</item>
+ <item>8</item>
+ </bytearr>
+ <floatarr>
+ <item>9</item>
+ <item>10</item>
+ </floatarr>
+ <weightedsetint>
+ <item weight="11">11</item>
+ <item weight="12">12</item>
+ </weightedsetint>
+ <weightedsetstring>
+ <item weight="13">string13</item>
+ <item weight="14">string14</item>
+ </weightedsetstring>
+ </document>
+
+ <document documenttype="news" documentid="doc:news:http://news10b">
+ <url>testUrl2</url>
+ </document>
+
+ <update documenttype="news" documentid="doc:news:http://news10c">
+ <add field="stringarr">
+ <item>addString1</item>
+ <item>addString2</item>
+ </add>
+ <add field="longarr">
+ <item>5</item>
+ </add>
+ <add field="longarr">6</add>
+ <add field="weightedsetint">
+ <item weight="11">11</item>
+ <item weight="12">12</item>
+ </add>
+ <add field="weightedsetstring">
+ <item>add13</item>
+ </add>
+ <add field="weightedsetstring">add14</add>
+ </update>
+
+ <update documenttype="news" documentid="doc:news:http://news10d">
+ <assign field="url">assignUrl</assign>
+ <assign field="value_long">2</assign>
+ <assign field="stringarr">
+ <item>assignString1</item>
+ <item>assignString2</item>
+ </assign>
+ <assign field="intarr">
+ <item>3</item>
+ <item>4</item>
+ </assign>
+ <assign field="weightedsetint">
+ <item weight="11">11</item>
+ <item weight="12">12</item>
+ </assign>
+ </update>
+
+ <remove documentid="doc:news:http://news10e"/>
+</vespafeed>
diff --git a/vespaclient-container-plugin/src/test/files/feedhandler/test10b.xml b/vespaclient-container-plugin/src/test/files/feedhandler/test10b.xml
new file mode 100644
index 00000000000..44762f594e0
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/files/feedhandler/test10b.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+
+<!--
+ Document : test10.xml
+ Created on : July 27, 2007, 11:37 AM
+ Author : alimf
+ Description:
+ this feed contains both documents, updates and removes.
+-->
+
+<vespafeed>
+
+ <document documenttype="news" documentid="doc:news:http://news10a">
+ <url>testUrl</url>
+ <title>testTitle</title>
+ <last_downloaded>1</last_downloaded>
+ <value_long>2</value_long>
+ <value_content>testValueContent</value_content>
+ <stringarr>
+ <item>stringarrItem1</item>
+ <item>stringarrItem2</item>
+ </stringarr>
+ <intarr>
+ <item>3</item>
+ <item>4</item>
+ </intarr>
+ <longarr>
+ <item>5</item>
+ <item>6</item>
+ </longarr>
+ <bytearr>
+ <item>7</item>
+ <item>8</item>
+ </bytearr>
+ <floatarr>
+ <item>9</item>
+ <item>10</item>
+ </floatarr>
+ <weightedsetint>
+ <item weight="11">11</item>
+ <item weight="12">12</item>
+ </weightedsetint>
+ <weightedsetstring>
+ <item weight="13">string13</item>
+ <item weight="14">string14</item>
+ </weightedsetstring>
+ </document>
+
+ <document documenttype="news" documentid="doc:news:http://news10b">
+ <url>testUrl2</url>
+ </document>
+</vespafeed>
diff --git a/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_docid.xml b/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_docid.xml
new file mode 100644
index 00000000000..a62df0c645a
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_docid.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<vespafeed>
+
+ <document documenttype="news" documentid="doc:news:http://news10a">
+ <url>testUrl</url>
+ <title>testTitle</title>
+ <last_downloaded>1</last_downloaded>
+ <value_long>2</value_long>
+ <value_content>testValueContent</value_content>
+ <stringarr>
+ <item>stringarrItem1</item>
+ <item>stringarrItem2</item>
+ </stringarr>
+ <intarr>
+ <item>3</item>
+ <item>4</item>
+ </intarr>
+ <longarr>
+ <item>5</item>
+ <item>6</item>
+ </longarr>
+ <bytearr>
+ <item>7</item>
+ <item>8</item>
+ </bytearr>
+ <floatarr>
+ <item>9</item>
+ <item>10</item>
+ </floatarr>
+ <weightedsetint>
+ <item weight="11">11</item>
+ <item weight="12">12</item>
+ </weightedsetint>
+ <weightedsetstring>
+ <item weight="13">string13</item>
+ <item weight="14">string14</item>
+ </weightedsetstring>
+ </document>
+
+ <document documenttype="news" documentid="foobar:news:http://news12">
+ <url>testUrl2</url>
+ </document>
+
+ <document documenttype="news" documentid="doc:news:ok">
+ <url>testUrl2</url>
+ </document>
+</vespafeed>
diff --git a/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_docid_first.xml b/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_docid_first.xml
new file mode 100755
index 00000000000..434eaa7789c
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_docid_first.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<vespafeed>
+ <document documenttype="news" documentid="foobar:news:http://news12">
+ <url>testUrl2</url>
+ </document>
+
+ <document documenttype="news" documentid="doc:news:http://news10a">
+ <url>testUrl</url>
+ <title>testTitle</title>
+ <last_downloaded>1</last_downloaded>
+ <value_long>2</value_long>
+ <value_content>testValueContent</value_content>
+ <stringarr>
+ <item>stringarrItem1</item>
+ <item>stringarrItem2</item>
+ </stringarr>
+ <intarr>
+ <item>3</item>
+ <item>4</item>
+ </intarr>
+ <longarr>
+ <item>5</item>
+ <item>6</item>
+ </longarr>
+ <bytearr>
+ <item>7</item>
+ <item>8</item>
+ </bytearr>
+ <floatarr>
+ <item>9</item>
+ <item>10</item>
+ </floatarr>
+ <weightedsetint>
+ <item weight="11">11</item>
+ <item weight="12">12</item>
+ </weightedsetint>
+ <weightedsetstring>
+ <item weight="13">string13</item>
+ <item weight="14">string14</item>
+ </weightedsetstring>
+ </document>
+</vespafeed>
diff --git a/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_xml.xml b/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_xml.xml
new file mode 100755
index 00000000000..f0b36ea3bc9
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/files/feedhandler/test_bogus_xml.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<vespafeed>
+
+ <document documenttype="news" documentid="doc:news:http://news10a">
+ <url>testUrl</url>
+ <title>testTitle</title>
+ <last_downloaded>1</last_downloaded>
+ <value_long>2</value_long>
+ <value_content>testValueContent</value_content>
+ <stringarr>
+ <item>stringarrItem1</item>
+ <item>stringarrItem2</item>
+ </stringarr>
+ <intarr>
+ <item>3</item>
+ <item>4</item>
+ </intarr>
+ <longarr>
+ <item>5</item>
+ <item>6</item>
+ </longarr>
+ <bytearr>
+ <item>7</item>
+ <item>8</item>
+ </bytearr>
+ <floatarr>
+ <item>9</item>
+ <item>10</item>
+ </floatarr>
+ <weightedsetint>
+ <item weight="11">11</item>
+ <item weight="12">12</item>
+ </weightedsetint>
+ <weightedsetstring>
+ <item weight="13">string13</item>
+ <item weight="14">string14</item>
+ </weightedsetstring>
+ </document>
+
+ <document documenttype=news documentid=foobar:news:http://news12
+ <url>testUrl2</url>
+ </document>
+</vespafed>
diff --git a/vespaclient-container-plugin/src/test/files/feedhandler/test_removes b/vespaclient-container-plugin/src/test/files/feedhandler/test_removes
new file mode 100755
index 00000000000..0232970ed4f
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/files/feedhandler/test_removes
@@ -0,0 +1,2 @@
+doc:test:remove1
+doc:test:remove2
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/OperationHandlerImplTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/OperationHandlerImplTest.java
new file mode 100644
index 00000000000..87360fcf998
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/OperationHandlerImplTest.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.restapi;
+
+import com.yahoo.vespaclient.ClusterDef;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+
+public class OperationHandlerImplTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void missingClusterDef() throws RestApiException {
+ List<ClusterDef> clusterDef = new ArrayList<>();
+ OperationHandlerImpl.resolveClusterRoute(Optional.empty(), clusterDef);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void missingClusterDefSpecifiedCluster() throws RestApiException {
+ List<ClusterDef> clusterDef = new ArrayList<>();
+ OperationHandlerImpl.resolveClusterRoute(Optional.of("cluster"), clusterDef);
+ }
+
+ @Test(expected = RestApiException.class)
+ public void oneClusterPresentNotMatching() throws RestApiException {
+ List<ClusterDef> clusterDef = new ArrayList<>();
+ clusterDef.add(new ClusterDef("foo", "configId"));
+ OperationHandlerImpl.resolveClusterRoute(Optional.of("cluster"), clusterDef);
+ }
+
+ @Test()
+ public void oneClusterMatching() throws RestApiException {
+ List<ClusterDef> clusterDef = new ArrayList<>();
+ clusterDef.add(new ClusterDef("foo", "configId"));
+ assertThat(OperationHandlerImpl.resolveClusterRoute(Optional.of("foo"), clusterDef),
+ is("[Storage:cluster=foo;clusterconfigid=configId]"));
+ }
+
+ @Test()
+ public void oneClusterMatchingManyAvailable() throws RestApiException {
+ List<ClusterDef> clusterDef = new ArrayList<>();
+ clusterDef.add(new ClusterDef("foo2", "configId2"));
+ clusterDef.add(new ClusterDef("foo", "configId"));
+ clusterDef.add(new ClusterDef("foo3", "configId2"));
+ assertThat(OperationHandlerImpl.resolveClusterRoute(Optional.of("foo"), clusterDef),
+ is("[Storage:cluster=foo;clusterconfigid=configId]"));
+ }
+
+ @Test()
+ public void checkErrorMessage() throws RestApiException, IOException {
+ List<ClusterDef> clusterDef = new ArrayList<>();
+ clusterDef.add(new ClusterDef("foo2", "configId2"));
+ clusterDef.add(new ClusterDef("foo", "configId"));
+ clusterDef.add(new ClusterDef("foo3", "configId2"));
+ try {
+ OperationHandlerImpl.resolveClusterRoute(Optional.of("wrong"), clusterDef);
+ } catch(RestApiException e) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ e.getResponse().render(stream);
+ String errorMsg = new String( stream.toByteArray());
+ assertThat(errorMsg, is("{\"errors\":[\"Your vespa cluster contains the content clusters foo2 " +
+ "(configId2), foo (configId), foo3 (configId2), not wrong. Please select a valid vespa cluster.\"]}"));
+ return;
+ }
+ fail("Expected exception");
+ }
+} \ No newline at end of file
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/RestUriTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/RestUriTest.java
new file mode 100644
index 00000000000..4bb7a264aba
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/RestUriTest.java
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.restapi;
+
+import org.apache.http.client.utils.URIBuilder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public class RestUriTest {
+
+ URI createUri(String path, String query) throws URISyntaxException {
+ return new URIBuilder()
+ .addParameter("foo", "bar")
+ .setHost("host")
+ .setScheme("http")
+ .setPort(666)
+ .setPath(path)
+ .setCustomQuery(query)
+ .setFragment("fargment").build();
+ }
+
+ @Rule
+ public ExpectedException thrown= ExpectedException.none();
+
+ @Test
+ public void testBasic() throws Exception {
+ RestUri restUri = new RestUri(createUri("/document/v1/namespace/doctype/docid/myid", "query"));
+ assertThat(restUri.getDocId(), is("myid"));
+ assertThat(restUri.getDocumentType(), is("doctype"));
+ assertThat(restUri.getNamespace(), is("namespace"));
+ assertThat(restUri.getGroup(), is(Optional.<RestUri.Group>empty()));
+ assertThat(restUri.generateFullId(), is("id:namespace:doctype::myid"));
+ }
+
+ @Test
+ public void encodingSlashes() throws Exception {
+ // Try with slashes encoded.
+ final String id = " !\"øæåp/:;&,.:;'1Q";
+ String encodedId = URLEncoder.encode(id, StandardCharsets.UTF_8.name());
+ RestUri restUri = new RestUri(URI.create("/document/v1/namespace/doctype/docid/" + encodedId));
+ assertThat(restUri.getDocId(), is(id));
+ assertThat(restUri.getDocumentType(), is("doctype"));
+ assertThat(restUri.getNamespace(), is("namespace"));
+ assertThat(restUri.getGroup(), is(Optional.<RestUri.Group>empty()));
+ assertThat(restUri.generateFullId(), is("id:namespace:doctype::" + id));
+ }
+
+ @Test
+ public void encodingSlashes2() throws Exception {
+ // This will decode the slashes.
+ final String id = " !\"øæåp/:;&,.:;'1Q ";
+ RestUri restUri = new RestUri(createUri("/document/v1/namespace/doctype/docid/" + id, "query"));
+ assertThat(restUri.getDocId(), is(id));
+ assertThat(restUri.getDocumentType(), is("doctype"));
+ assertThat(restUri.getNamespace(), is("namespace"));
+ assertThat(restUri.getGroup(), is(Optional.<RestUri.Group>empty()));
+ assertThat(restUri.generateFullId(), is("id:namespace:doctype::" + id));
+ }
+
+
+ @Test
+ public void testVisit() throws Exception {
+ RestUri restUri = new RestUri(createUri("/document/v1/namespace/doctype/docid/", "query"));
+ assertThat(restUri.getDocId(), is(""));
+ assertThat(restUri.getDocumentType(), is("doctype"));
+ assertThat(restUri.getNamespace(), is("namespace"));
+ assertThat(restUri.getGroup(), is(Optional.<RestUri.Group>empty()));
+ assertThat(restUri.generateFullId(), is("id:namespace:doctype::"));
+ }
+
+ @Test
+ public void testOneSlashTooMuchWhichIsFine() throws Exception {
+ RestUri restUri = new RestUri(createUri("/document/v1/namespace/doctype/docid/myid:342:23/wrong", ""));
+ assertThat(restUri.getDocId(), is("myid:342:23/wrong"));
+ }
+
+ @Test
+ public void testGroupG() throws Exception {
+ RestUri restUri = new RestUri(createUri("/document/v1/namespace/doctype/group/group/myid", ""));
+ assertThat(restUri.getDocId(), is("myid"));
+ assertThat(restUri.getDocumentType(), is("doctype"));
+ assertThat(restUri.getGroup().get().name, is('g'));
+ assertThat(restUri.getGroup().get().value, is("group"));
+ assertThat(restUri.generateFullId(), is("id:namespace:doctype:g=group:myid"));
+ }
+
+ @Test
+ public void testGroupN() throws Exception {
+ RestUri restUri = new RestUri(createUri("/document/v1/namespace/doctype/number/group/myid", ""));
+ assertThat(restUri.getGroup().get().name, is('n'));
+ assertThat(restUri.getGroup().get().value, is("group"));
+ }
+
+ @Test
+ public void testGroupUnknown() throws Exception {
+ thrown.expect(RestApiException.class);
+ new RestUri(createUri("/document/v1/namespace/doctype/Q/myid", ""));
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/feed-document1.json b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/feed-document1.json
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/feed-document1.json
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/MockedOperationHandler.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/MockedOperationHandler.java
new file mode 100644
index 00000000000..ab5b36c74d1
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/MockedOperationHandler.java
@@ -0,0 +1,59 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.restapi.resource;
+
+import com.yahoo.document.restapi.OperationHandler;
+import com.yahoo.document.restapi.Response;
+import com.yahoo.document.restapi.RestApiException;
+import com.yahoo.document.restapi.RestUri;
+import com.yahoo.vespaxmlparser.VespaXMLFeedReader;
+
+import java.util.Optional;
+
+/**
+ * Mock that collects info about operation and returns them on second delete.
+ */
+public class MockedOperationHandler implements OperationHandler {
+
+ StringBuilder log = new StringBuilder();
+ int deleteCount = 0;
+
+ @Override
+ public VisitResult visit(RestUri restUri, String documentSelection, Optional<String> cluster, Optional<String> continuation) throws RestApiException {
+ return new VisitResult(Optional.of("token"), "List of json docs, cont token " + continuation.map(a->a).orElse("not set") + ", doc selection: '"
+ + documentSelection + "'");
+ }
+
+ @Override
+ public void put(RestUri restUri, VespaXMLFeedReader.Operation data) throws RestApiException {
+ log.append("PUT: " + data.getDocument().getId());
+ log.append(data.getDocument().getBody().toString());
+ }
+
+ @Override
+ public void update(RestUri restUri, VespaXMLFeedReader.Operation data) throws RestApiException {
+ log.append("UPDATE: " + data.getDocumentUpdate().getId());
+ log.append(data.getDocumentUpdate().getFieldUpdates().toString());
+ if (data.getDocumentUpdate().getCreateIfNonExistent()) {
+ log.append("[CREATE IF NON EXISTENT IS TRUE]");
+ }
+ }
+
+ @Override
+ public void delete(RestUri restUri, String condition) throws RestApiException {
+ deleteCount++;
+ if (deleteCount == 2) {
+ String theLog = log.toString();
+ log = new StringBuilder();
+ deleteCount = 0;
+ throw new RestApiException(Response.createErrorResponse(666, theLog));
+ }
+ log.append("DELETE: " + restUri.generateFullId());
+ }
+
+ @Override
+ public Optional<String> get(RestUri restUri) throws RestApiException {
+ log.append("GET: " + restUri.generateFullId());
+ return Optional.empty();
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiMaxThreadTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiMaxThreadTest.java
new file mode 100644
index 00000000000..17a36c142ea
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiMaxThreadTest.java
@@ -0,0 +1,54 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.restapi.resource;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.document.restapi.OperationHandler;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class RestApiMaxThreadTest {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicInteger requestsInFlight = new AtomicInteger(0);
+ private class RestApiMocked extends RestApi {
+
+ public RestApiMocked() {
+ super(mock(Executor.class), null, (OperationHandler)null);
+ }
+
+ @Override
+ protected HttpResponse handleInternal(HttpRequest request) {
+ requestsInFlight.incrementAndGet();
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }
+ }
+
+ @Test
+ public void testCallsAreThrottled() throws InterruptedException {
+ RestApiMocked restApiMocked = new RestApiMocked();
+ // Fire lots of requests.
+ for (int x = 0; x < 30; x++) {
+ new Thread(() -> restApiMocked.handle(null)).start();
+ }
+ // Wait for all threads to be used
+ while (requestsInFlight.get() != 19) {
+ Thread.sleep(1);
+ }
+ // A new request should be blocked.
+ final HttpResponse response = restApiMocked.handle(null);
+ assertThat(response.getStatus(), is(429));
+ latch.countDown();
+ }
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiTest.java
new file mode 100644
index 00000000000..036dc63ad34
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiTest.java
@@ -0,0 +1,298 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.restapi.resource;
+
+import com.yahoo.application.Application;
+import com.yahoo.application.Networking;
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.container.Container;
+import com.yahoo.jdisc.http.server.jetty.JettyHttpServer;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.hamcrest.core.StringStartsWith.startsWith;
+import static org.junit.Assert.assertThat;
+
+public class RestApiTest {
+ Application application;
+
+ @Before
+ public void setup() throws Exception {
+ application = Application.fromApplicationPackage(Paths.get("src/test/application"), Networking.enable);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ application.close();
+ }
+
+ String post_test_uri = "/document/v1/namespace/testdocument/docid/c";
+ String post_test_doc = "{\n" +
+ "\"foo\" : \"bar\"," +
+ "\"fields\": {\n" +
+ "\"title\": \"This is the title\",\n" +
+ "\"body\": \"This is the body\"" +
+ "}" +
+ "}";
+ String post_test_response = "{\"id\":\"id:namespace:testdocument::c\"," +
+ "\"pathId\":\"/document/v1/namespace/testdocument/docid/c\"}";
+
+ // Run this test to manually do request against the REST-API with backend mock.
+ @Ignore
+ @Test
+ public void blockingTest() throws Exception {
+ System.out.println("Running on port " + getFirstListenPort());
+ Thread.sleep(Integer.MAX_VALUE);
+ }
+
+ @Test
+ public void testbasicPost() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + post_test_uri);
+ HttpPost httpPost = new HttpPost(request.getUri());
+ StringEntity entity = new StringEntity(post_test_doc, ContentType.create("application/json"));
+ httpPost.setEntity(entity);
+ String x = doRest(httpPost);
+ assertThat(x, is(post_test_response));
+ }
+
+ String post_test_uri_cond = "/document/v1/namespace/testdocument/docid/c?condition=foo";
+ String post_test_doc_cond = "{\n" +
+ "\"foo\" : \"bar\"," +
+ "\"fields\": {\n" +
+ "\"title\": \"This is the title\",\n" +
+ "\"body\": \"This is the body\"" +
+ "}" +
+ "}";
+ String post_test_response_cond = "{\"id\":\"id:namespace:testdocument::c\"," +
+ "\"pathId\":\"/document/v1/namespace/testdocument/docid/c\"}";
+
+ @Test
+ public void testConditionalPost() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + post_test_uri_cond);
+ HttpPost httpPost = new HttpPost(request.getUri());
+ StringEntity entity = new StringEntity(post_test_doc_cond, ContentType.create("application/json"));
+ httpPost.setEntity(entity);
+ String x = doRest(httpPost);
+ assertThat(x, is(post_test_response_cond));
+ }
+
+ String post_test_empty_response = "{\"errors\":[\"Could not read document, no document?\"]";
+ @Test
+ public void testEmptyPost() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + post_test_uri);
+ HttpPost httpPost = new HttpPost(request.getUri());
+ StringEntity entity = new StringEntity("", ContentType.create("application/json"));
+ httpPost.setEntity(entity);
+ String x = doRest(httpPost);
+ assertThat(x, startsWith(post_test_empty_response));
+ }
+
+ String update_test_uri = "/document/v1/namespace/testdocument/docid/c";
+ String update_test_doc = "{\n" +
+ "\t\"fields\": {\n" +
+ "\"title\": {\n" +
+ "\"assign\": \"Oh lala\"\n" +
+ "}\n" +
+ "}\n" +
+ "}\n";
+
+ String update_test_response = "{\"id\":\"id:namespace:testdocument::c\"," +
+ "\"pathId\":\"/document/v1/namespace/testdocument/docid/c\"}";
+
+ @Test
+ public void testbasicUpdate() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + update_test_uri);
+ HttpPut httpPut = new HttpPut(request.getUri());
+ StringEntity entity = new StringEntity(update_test_doc, ContentType.create("application/json"));
+ httpPut.setEntity(entity);
+ assertThat(doRest(httpPut), is(update_test_response));
+ assertThat(getLog(), not(containsString("CREATE IF NON EXISTING IS TRUE")));
+ }
+
+ @Test
+ public void testbasicUpdateCreateTrue() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + update_test_uri + "?create=true");
+ HttpPut httpPut = new HttpPut(request.getUri());
+ StringEntity entity = new StringEntity(update_test_doc, ContentType.create("application/json"));
+ httpPut.setEntity(entity);
+ assertThat(doRest(httpPut), is(update_test_response));
+ assertThat(getLog(), containsString("CREATE IF NON EXISTENT IS TRUE"));
+ }
+
+ String update_test_create_if_non_existient_uri = "/document/v1/namespace/testdocument/docid/c";
+ String update_test_create_if_non_existient_doc = "{\n" +
+ "\"create\":true," +
+ "\t\"fields\": {\n" +
+ "\"title\": {\n" +
+ "\"assign\": \"Oh lala\"\n" +
+ "}\n" +
+ "}\n" +
+ "}\n";
+
+ String update_test_create_if_non_existing_response = "{\"id\":\"id:namespace:testdocument::c\"," +
+ "\"pathId\":\"/document/v1/namespace/testdocument/docid/c\"}";
+
+ @Test
+ public void testCreateIfNonExistingUpdateInDocTrue() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + update_test_create_if_non_existient_uri);
+ HttpPut httpPut = new HttpPut(request.getUri());
+ StringEntity entity = new StringEntity(update_test_create_if_non_existient_doc, ContentType.create("application/json"));
+ httpPut.setEntity(entity);
+ assertThat(doRest(httpPut), is(update_test_create_if_non_existing_response));
+ assertThat(getLog(), containsString("CREATE IF NON EXISTENT IS TRUE"));
+
+ }
+
+ @Test
+ public void testCreateIfNonExistingUpdateInDocTrueButQueryParamsFalse() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + update_test_create_if_non_existient_uri + "?create=false");
+ HttpPut httpPut = new HttpPut(request.getUri());
+ StringEntity entity = new StringEntity(update_test_create_if_non_existient_doc, ContentType.create("application/json"));
+ httpPut.setEntity(entity);
+ assertThat(doRest(httpPut), is(update_test_create_if_non_existing_response));
+ assertThat(getLog(), not(containsString("CREATE IF NON EXISTENT IS TRUE")));
+
+ }
+
+ // Get logs through some hackish fetch method. Logs is something the mocked backend write.
+ String getLog() throws IOException {
+ // The mocked backend will throw a runtime exception wtih a log if delete is called three times..
+ Request request = new Request("http://localhost:" + getFirstListenPort() + remove_test_uri);
+ HttpDelete delete = new HttpDelete(request.getUri());
+ doRest(delete);
+ return doRest(delete);
+ }
+
+
+ String remove_test_uri = "/document/v1/namespace/testdocument/docid/c";
+ String remove_test_response = "{\"id\":\"id:namespace:testdocument::c\"," +
+ "\"pathId\":\"/document/v1/namespace/testdocument/docid/c\"}";
+
+ @Test
+ public void testbasicRemove() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + remove_test_uri);
+ HttpDelete delete = new HttpDelete(request.getUri());
+ assertThat(doRest(delete), is(remove_test_response));
+ }
+
+ String get_test_uri = "/document/v1/namespace/document-type/docid/c";
+ String get_response_part1 = "\"pathId\":\"/document/v1/namespace/document-type/docid/c\"";
+ String get_response_part2 = "\"id\":\"id:namespace:document-type::c\"";
+
+
+ @Test
+ public void testbasicGet() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + get_test_uri);
+ HttpGet get = new HttpGet(request.getUri());
+ final String rest = doRest(get);
+ assertThat(rest, containsString(get_response_part1));
+ assertThat(rest, containsString(get_response_part2));
+ }
+
+ String id_test_uri = "/document/v1/namespace/document-type/docid/f/u/n/n/y/!";
+ String id_response_part1 = "\"pathId\":\"/document/v1/namespace/document-type/docid/f/u/n/n/y/!\"";
+ String id_response_part2 = "\"id\":\"id:namespace:document-type::f/u/n/n/y/!\"";
+
+ @Test
+ public void testSlashesInId() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + id_test_uri);
+ HttpGet get = new HttpGet(request.getUri());
+ final String rest = doRest(get);
+ assertThat(rest, containsString(id_response_part1));
+ assertThat(rest, containsString(id_response_part2));
+ }
+
+
+ String get_enc_id = "!\":æøå@/& Q1+";
+ // Space encoded as %20, not encoding !
+ String get_enc_id_encoded_v1 = "!%22%3A%C3%A6%C3%B8%C3%A5%40%2F%26%20Q1%2B";
+ // Space encoded as +
+ String get_enc_id_encoded_v2 = "%21%22%3A%C3%A6%C3%B8%C3%A5%40%2F%26+Q1%2B";
+ String get_enc_test_uri_v1 = "/document/v1/namespace/document-type/docid/" + get_enc_id_encoded_v1;
+ String get_enc_test_uri_v2 = "/document/v1/namespace/document-type/docid/" + get_enc_id_encoded_v2;
+ String get_enc_response_part1 = "\"pathId\":\"/document/v1/namespace/document-type/docid/" + get_enc_id_encoded_v1 + "\"";
+ String get_enc_response_part1_v2 = "\"pathId\":\"/document/v1/namespace/document-type/docid/" + get_enc_id_encoded_v2 + "\"";
+
+ // JSON encode " as \"
+ String get_enc_response_part2 = "\"id\":\"id:namespace:document-type::" + get_enc_id.replace("\"", "\\\"") + "\"";
+
+
+ @Test
+ public void testbasicEncodingV1() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + get_enc_test_uri_v1);
+ HttpGet get = new HttpGet(request.getUri());
+ final String rest = doRest(get);
+ assertThat(rest, containsString(get_enc_response_part1));
+ assertThat(rest, containsString(get_enc_response_part2));
+ }
+
+ @Test
+ public void testbasicEncodingV2() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + get_enc_test_uri_v2);
+ HttpGet get = new HttpGet(request.getUri());
+ final String rest = doRest(get);
+ assertThat(rest, containsString(get_enc_response_part1_v2));
+ assertThat(rest, containsString(get_enc_response_part2));
+ }
+
+ String visit_test_uri = "/document/v1/namespace/document-type/docid/?continuation=abc";
+ String visit_response_part1 = "\"documents\":[List of json docs, cont token abc, doc selection: '']";
+ String visit_response_part2 = "\"continuation\":\"token\"";
+ String visit_response_part3 = "\"pathId\":\"/document/v1/namespace/document-type/docid/\"";
+
+
+ @Test
+ public void testbasicVisit() throws Exception {
+ Request request = new Request("http://localhost:" + getFirstListenPort() + visit_test_uri);
+ HttpGet get = new HttpGet(request.getUri());
+ final String rest = doRest(get);
+ assertThat(rest, containsString(visit_response_part1));
+ assertThat(rest, containsString(visit_response_part2));
+ assertThat(rest, containsString(visit_response_part3));
+ }
+
+ String visit_test_bad_uri = "/document/v1/namespace/document-type/group/abc?continuation=abc";
+ String visit_test_bad_response = "Visiting does not support setting value for group/value,";
+
+
+ @Test
+ public void testBadVisit() throws Exception {
+ final Request request = new Request("http://localhost:" + getFirstListenPort() + visit_test_bad_uri);
+ HttpGet get = new HttpGet(request.getUri());
+ final String rest = doRest(get);
+ assertThat(rest, containsString(visit_test_bad_response));
+ }
+
+ private String doRest(HttpRequestBase request) throws IOException {
+ HttpClient client = HttpClientBuilder.create().build();
+ HttpResponse response = client.execute(request);
+ HttpEntity entity = response.getEntity();
+ return EntityUtils.toString(entity);
+ }
+
+ private String getFirstListenPort() {
+ JettyHttpServer serverProvider =
+ (JettyHttpServer) Container.get().getServerProviderRegistry().allComponents().get(0);
+ return Integer.toString(serverProvider.getListenPort());
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiWithTestDocumentHandler.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiWithTestDocumentHandler.java
new file mode 100644
index 00000000000..cfb120d9891
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/RestApiWithTestDocumentHandler.java
@@ -0,0 +1,36 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.document.restapi.resource;
+
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.restapi.OperationHandler;
+
+import java.util.concurrent.Executor;
+
+/**
+ * For setting up RestApi with a simple document type manager.
+ *
+ * @author dybdahl
+ */
+public class RestApiWithTestDocumentHandler extends RestApi{
+
+ private DocumentTypeManager docTypeManager = new DocumentTypeManager();
+
+ public RestApiWithTestDocumentHandler(
+ Executor executor,
+ AccessLog accessLog,
+ OperationHandler operationHandler) {
+ super(executor, accessLog, operationHandler);
+
+ DocumentType documentType = new DocumentType("testdocument");
+
+ documentType.addField("title", DataType.STRING);
+ documentType.addField("body", DataType.STRING);
+ docTypeManager.registerDocumentType(documentType);
+
+ setDocTypeManagerForTests(docTypeManager);
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/externalfeeding/server/.gitignore b/vespaclient-container-plugin/src/test/java/com/yahoo/externalfeeding/server/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/externalfeeding/server/.gitignore
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/FeedHandlerTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/FeedHandlerTest.java
new file mode 100644
index 00000000000..6d0ee59c68d
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/FeedHandlerTest.java
@@ -0,0 +1,103 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.feedhandler;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.vespa.http.client.core.Headers;
+import com.yahoo.vespa.http.client.core.OperationStatus;
+import com.yahoo.vespa.http.server.FeedHandler;
+import com.yahoo.vespa.http.server.Feeder;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit test for FeedHandler class.
+ *
+ * @author dybdahl
+ */
+public class FeedHandlerTest {
+
+ /**
+ * This class extends FeedHandler and allows to create a custom Feeder.
+ */
+ static class TestFeedHandler extends FeedHandler {
+ private final CountDownLatch countDownLatch = new CountDownLatch(1);
+
+ public TestFeedHandler() throws Exception {
+ super(Executors.newCachedThreadPool(), null, null, mock(Metric.class), mock(AccessLog.class), null);
+ }
+
+ /**
+ * Builds a feeder that blocks until countDownLatch is stepped down.
+ */
+ @Override
+ protected Feeder createFeeder(
+ com.yahoo.container.jdisc.HttpRequest request,
+ InputStream requestInputStream,
+ final BlockingQueue<OperationStatus> operations,
+ String clientId,
+ boolean sessionIdWasGeneratedJustNow,
+ int protocolVersion) throws Exception {
+ Feeder feeder = mock(Feeder.class);
+ doAnswer(invocation -> {
+ try {
+ countDownLatch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+ }).when(feeder).waitForRequestReceived();
+ return feeder;
+ }
+ }
+
+ /**
+ * nginx require that a post is finished before the server ack with a response. This behaviour is verified
+ * in this test
+ */
+ @Test
+ public void testResponseIsSentAfterWaitForRequestReceivedReturns() throws Exception {
+ HttpRequest request = mock(HttpRequest.class);
+
+ // Create a request with valid version.
+ com.yahoo.jdisc.http.HttpRequest jdiscRequest = mock(com.yahoo.jdisc.http.HttpRequest.class);
+ HeaderFields headerFields = mock(HeaderFields.class);
+ List<String> version = new ArrayList<>();
+ version.add("2");
+ when(headerFields.get(Headers.VERSION)).thenReturn(version);
+ when(jdiscRequest.headers()).thenReturn(headerFields);
+ when(request.getJDiscRequest()).thenReturn(jdiscRequest);
+
+ TestFeedHandler feedHandler = new TestFeedHandler();
+ // After a short period, make the feed finish.
+ new Thread(() -> {
+ try {
+ Thread.sleep(50);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ feedHandler.countDownLatch.countDown();
+ }).start();
+ // This should not return before countdown latch is stepped down.
+ feedHandler.handle(request);
+ // This should always returns after the countDownLatch has become zero. This might cause false positive,
+ // but not false negatives. This is fine.
+ assertThat(feedHandler.countDownLatch.getCount(), is(0L));
+
+ }
+
+} \ No newline at end of file
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/VespaFeedHandlerTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/VespaFeedHandlerTestCase.java
new file mode 100755
index 00000000000..646bcb805f6
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/VespaFeedHandlerTestCase.java
@@ -0,0 +1,1015 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.feedhandler;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.docproc.CallStack;
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.messagebus.*;
+import com.yahoo.vespa.config.content.LoadTypeConfig;
+import com.yahoo.container.Container;
+import com.yahoo.docproc.*;
+import com.yahoo.docproc.jdisc.DocumentProcessingHandler;
+import com.yahoo.docproc.jdisc.DocumentProcessingHandlerParameters;
+import com.yahoo.document.*;
+import com.yahoo.document.datatypes.IntegerFieldValue;
+import com.yahoo.documentapi.messagebus.loadtypes.LoadType;
+import com.yahoo.documentapi.messagebus.protocol.*;
+import com.yahoo.feedapi.DummySessionFactory;
+import com.yahoo.feedapi.FeedContext;
+import com.yahoo.feedapi.MessagePropertyProcessor;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.vespaclient.ClusterDef;
+import com.yahoo.vespaclient.ClusterList;
+import com.yahoo.vespaclient.config.FeederConfig;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.logging.Logger;
+import java.util.zip.GZIPOutputStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class VespaFeedHandlerTestCase {
+
+ private VespaFeedHandler feedHandler;
+ private VespaFeedHandlerRemove removeHandler;
+ private VespaFeedHandlerStatus statusHandler;
+ private VespaFeedHandlerRemoveLocation removeLocationHandler;
+ private FeedContext context;
+
+ private DummySessionFactory factory;
+ private final String xmlFilesPath = "src/test/files/feedhandler/";
+
+ public void setup(com.yahoo.messagebus.Error e, LoadTypeConfig loadTypeCfg,
+ boolean autoReply,
+ DummySessionFactory.ReplyFactory autoReplyFactory) throws Exception {
+ DocumentTypeManager docMan = new DocumentTypeManager();
+ DocumentTypeManagerConfigurer.configure(docMan, "file:" + xmlFilesPath + "documentmanager.cfg");
+
+ if (autoReply) {
+ if (autoReplyFactory != null) {
+ factory = DummySessionFactory.createWithAutoReplyFactory(autoReplyFactory);
+ } else {
+ factory = DummySessionFactory.createWithErrorAutoReply(e);
+ }
+ } else {
+ factory = DummySessionFactory.createDefault();
+ }
+
+ context = new FeedContext(new MessagePropertyProcessor(new FeederConfig(new FeederConfig.Builder()), loadTypeCfg), factory, docMan, new ClusterList(), new NullFeedMetric());
+
+ Executor threadPool = Executors.newCachedThreadPool();
+ feedHandler = new VespaFeedHandler(context, threadPool);
+ removeHandler = new VespaFeedHandlerRemove(context, threadPool);
+ statusHandler = new VespaFeedHandlerStatus(context, false, false, threadPool);
+ removeLocationHandler = new VespaFeedHandlerRemoveLocation(context, threadPool);
+
+ CallStack dpCallstack = new CallStack("bar");
+ dpCallstack.addLast(new TestDocProc());
+ dpCallstack.addLast(new TestLaterDocProc());
+
+ DocprocService myservice = new DocprocService("bar");
+ myservice.setCallStack(dpCallstack);
+ myservice.setInService(true);
+
+ ComponentRegistry<DocprocService> registry = new ComponentRegistry<DocprocService>();
+ registry.register(new ComponentId(myservice.getName()), myservice);
+
+ DocumentProcessingHandler handler = new DocumentProcessingHandler(registry,
+ new ComponentRegistry<>(), new ComponentRegistry<>(),
+ new DocumentProcessingHandlerParameters());
+
+ Container container = Container.get();
+ ComponentRegistry<RequestHandler> requestHandlerComponentRegistry = new ComponentRegistry<>();
+ requestHandlerComponentRegistry.register(new ComponentId(DocumentProcessingHandler.class.getName()), handler);
+ container.setRequestHandlerRegistry(requestHandlerComponentRegistry);
+ }
+
+ public void setup(com.yahoo.messagebus.Error e) throws Exception {
+ setup(e, new LoadTypeConfig(new LoadTypeConfig.Builder()), true, null);
+ }
+
+ public void setupWithReplyFactory(DummySessionFactory.ReplyFactory autoReplyFactory) throws Exception {
+ setup(null, new LoadTypeConfig(new LoadTypeConfig.Builder()), true, autoReplyFactory);
+ }
+
+ public void setup() throws Exception {
+ setup(null, new LoadTypeConfig(new LoadTypeConfig.Builder()), false, null);
+ }
+
+ @After
+ public void resetContainer() {
+ Container.resetInstance();
+ }
+
+
+ @Test
+ public void testLoadTypes() throws Exception {
+ List<LoadTypeConfig.Type.Builder> typeBuilder = new ArrayList<>();
+ typeBuilder.add(new LoadTypeConfig.Type.Builder().id(1234).name("foo").priority("VERY_LOW"));
+ typeBuilder.add(new LoadTypeConfig.Type.Builder().id(4567).name("bar").priority("NORMAL_3"));
+
+ setup(null, new LoadTypeConfig(new LoadTypeConfig.Builder().type(typeBuilder)), true, null);
+
+ {
+ Result res = testRequest(HttpRequest.createTestRequest("remove?id=doc:test:removeme&loadtype=foo", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(1, res.messages.size());
+
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:test:removeme", d.toString());
+ assertEquals(new LoadType(1234, "foo", DocumentProtocol.Priority.VERY_LOW), ((DocumentMessage)m).getLoadType());
+ assertEquals(DocumentProtocol.Priority.VERY_LOW, ((DocumentMessage)m).getPriority());
+ }
+ }
+
+ @Test
+ public void testPostXML() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test10b.xml", "feed?");
+
+ assertEquals(2, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10b", d.toString());
+ }
+
+ assertTrue(res.output.contains("count=\"2\""));
+ assertTrue(res.error == null);
+ }
+
+ @Test
+ public void testPostXMLAsync() throws Exception {
+ setup();
+ Result res = testFeed(xmlFilesPath + "test10b.xml", "feed?asynchronous=true");
+
+ assertEquals(2, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10b", d.toString());
+ }
+
+ // Should not have metrics at this point.
+ assertTrue(!res.output.contains("count=\"2\""));
+ assertTrue(res.error == null);
+ }
+
+
+ @Test
+ public void testPostGZIPedXML() throws Exception {
+ setup(null);
+ Result res = testFeedGZIP(xmlFilesPath + "test10b.xml", "feed?");
+
+ assertEquals(2, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10b", d.toString());
+ }
+
+ assertTrue(res.error == null);
+ }
+
+ @Test
+ public void testDocProc() throws Exception {
+ setup(null);
+
+ Result res = testFeed(xmlFilesPath + "test10b.xml", "feed?docprocchain=bar");
+
+ assertEquals(2, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ Document d = ((PutDocumentMessage)m).getDocumentPut().getDocument();
+
+ assertEquals("doc:news:http://news10a", d.getId().toString());
+ assertEquals(new IntegerFieldValue(1234), d.getFieldValue("last_downloaded"));
+ }
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ Document d = ((PutDocumentMessage)m).getDocumentPut().getDocument();
+
+ assertEquals("doc:news:http://news10b", d.getId().toString());
+ assertEquals(new IntegerFieldValue(1234), d.getFieldValue("last_downloaded"));
+ }
+ }
+
+ @Test
+ public void testPostXMLVariousTypes() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test10.xml", "feed?");
+
+ assertEquals(5, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10b", d.toString());
+ }
+
+ {
+ Message m = res.messages.get(2);
+ assertEquals(DocumentProtocol.MESSAGE_UPDATEDOCUMENT, m.getType());
+ DocumentId d = ((UpdateDocumentMessage)m).getDocumentUpdate().getId();
+ assertEquals("doc:news:http://news10c", d.toString());
+ }
+ {
+ Message m = res.messages.get(3);
+ assertEquals(DocumentProtocol.MESSAGE_UPDATEDOCUMENT, m.getType());
+ DocumentId d = ((UpdateDocumentMessage)m).getDocumentUpdate().getId();
+ assertEquals("doc:news:http://news10d", d.toString());
+ }
+ {
+ Message m = res.messages.get(4);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:news:http://news10e", d.toString());
+ }
+
+ String val = res.output.replaceAll("<([a-z]+).*count=\"([0-9]+)\".*/", "<$1 count=\"$2\"/");
+
+ assertEquals("<result>\n" +
+ "\n" +
+ " <route name=\"default\">\n" +
+ " <total>\n" +
+ " <latency count=\"5\"/>\n" +
+ " <count count=\"5\"/>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <latency count=\"2\"/>\n" +
+ " <count count=\"2\"/>\n" +
+ " </putdocument>\n" +
+ " <updatedocument>\n" +
+ " <latency count=\"2\"/>\n" +
+ " <count count=\"2\"/>\n" +
+ " </updatedocument>\n" +
+ " <removedocument>\n" +
+ " <latency count=\"1\"/>\n" +
+ " <count count=\"1\"/>\n" +
+ " </removedocument>\n" +
+ " </route>\n" +
+ "\n" +
+ "</result>\n", val);
+ }
+
+ @Test
+ public void testStatusPage() throws Exception {
+ setup(null);
+
+ testFeed(xmlFilesPath + "test10b.xml", "feed?docprocchain=bar");
+ testFeed(xmlFilesPath + "test10.xml", "feed?");
+ testFeed(xmlFilesPath + "test10.xml", "feed?route=storage");
+ testFeed(xmlFilesPath + "test_removes", "remove?");
+
+ assertEquals(2, factory.sessionsCreated());
+ Result res = testRequest(HttpRequest.createTestRequest("feedstatus?", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+
+ String val = res.output.replaceAll("<([a-z]+).*count=\"([0-9]+)\".*/", "<$1 count=\"$2\"/");
+ val = val.replaceAll("to=\"[0-9]*\"", "to=\"0\"");
+
+ assertEquals("<status>\n" +
+ "\n" +
+ " <snapshot name=\"Total metrics from start until current time\" from=\"0\" to=\"0\" period=\"0\">\n" +
+ " <routes>\n" +
+ " <route name=\"total\">\n" +
+ " <total>\n" +
+ " <latency count=\"14\"/>\n" +
+ " <count count=\"14\"/>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <latency count=\"6\"/>\n" +
+ " <count count=\"6\"/>\n" +
+ " </putdocument>\n" +
+ " <updatedocument>\n" +
+ " <latency count=\"4\"/>\n" +
+ " <count count=\"4\"/>\n" +
+ " </updatedocument>\n" +
+ " <removedocument>\n" +
+ " <latency count=\"4\"/>\n" +
+ " <count count=\"4\"/>\n" +
+ " </removedocument>\n" +
+ " </route>\n" +
+ " <route name=\"default\">\n" +
+ " <total>\n" +
+ " <latency count=\"9\"/>\n" +
+ " <count count=\"9\"/>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <latency count=\"4\"/>\n" +
+ " <count count=\"4\"/>\n" +
+ " </putdocument>\n" +
+ " <updatedocument>\n" +
+ " <latency count=\"2\"/>\n" +
+ " <count count=\"2\"/>\n" +
+ " </updatedocument>\n" +
+ " <removedocument>\n" +
+ " <latency count=\"3\"/>\n" +
+ " <count count=\"3\"/>\n" +
+ " </removedocument>\n" +
+ " </route>\n" +
+ " <route name=\"storage\">\n" +
+ " <total>\n" +
+ " <latency count=\"5\"/>\n" +
+ " <count count=\"5\"/>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <latency count=\"2\"/>\n" +
+ " <count count=\"2\"/>\n" +
+ " </putdocument>\n" +
+ " <updatedocument>\n" +
+ " <latency count=\"2\"/>\n" +
+ " <count count=\"2\"/>\n" +
+ " </updatedocument>\n" +
+ " <removedocument>\n" +
+ " <latency count=\"1\"/>\n" +
+ " <count count=\"1\"/>\n" +
+ " </removedocument>\n" +
+ " </route>\n" +
+ " </routes>\n" +
+ " </snapshot>\n" +
+ "\n" +
+ "</status>\n", val);
+ }
+
+ @Test
+ public void testStatusPage2() throws Exception {
+ setup(null);
+
+ testFeed(xmlFilesPath + "test10b.xml", "feed?docprocchain=bar");
+ testFeed(xmlFilesPath + "test10.xml", "feed?");
+ testFeed(xmlFilesPath + "test10.xml", "feed?route=storage");
+ testFeed(xmlFilesPath + "test_removes", "remove?");
+
+ assertEquals(2, factory.sessionsCreated());
+ Result res = testRequest(HttpRequest.createTestRequest("feed?status", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+
+ String val = res.output.replaceAll("<([a-z]+).*count=\"([0-9]+)\".*/", "<$1 count=\"$2\"/");
+ val = val.replaceAll("to=\"[0-9]*\"", "to=\"0\"");
+
+ assertEquals("<status>\n" +
+ "\n" +
+ " <routes>\n" +
+ " <route name=\"total\" description=\"Messages sent to all routes\">\n" +
+ " <total description=\"All kinds of messages sent to the given route\">\n" +
+ " <latency count=\"14\"/>\n" +
+ " <count count=\"14\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <latency count=\"6\"/>\n" +
+ " <count count=\"6\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </putdocument>\n" +
+ " <updatedocument>\n" +
+ " <latency count=\"4\"/>\n" +
+ " <count count=\"4\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </updatedocument>\n" +
+ " <removedocument>\n" +
+ " <latency count=\"4\"/>\n" +
+ " <count count=\"4\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </removedocument>\n" +
+ " </route>\n" +
+ " <route name=\"default\" description=\"Messages sent to the named route\">\n" +
+ " <total description=\"All kinds of messages sent to the given route\">\n" +
+ " <latency count=\"9\"/>\n" +
+ " <count count=\"9\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <latency count=\"4\"/>\n" +
+ " <count count=\"4\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </putdocument>\n" +
+ " <updatedocument>\n" +
+ " <latency count=\"2\"/>\n" +
+ " <count count=\"2\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </updatedocument>\n" +
+ " <removedocument>\n" +
+ " <latency count=\"3\"/>\n" +
+ " <count count=\"3\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </removedocument>\n" +
+ " </route>\n" +
+ " <route name=\"storage\" description=\"Messages sent to the named route\">\n" +
+ " <total description=\"All kinds of messages sent to the given route\">\n" +
+ " <latency count=\"5\"/>\n" +
+ " <count count=\"5\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <latency count=\"2\"/>\n" +
+ " <count count=\"2\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </putdocument>\n" +
+ " <updatedocument>\n" +
+ " <latency count=\"2\"/>\n" +
+ " <count count=\"2\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </updatedocument>\n" +
+ " <removedocument>\n" +
+ " <latency count=\"1\"/>\n" +
+ " <count count=\"1\"/>\n" +
+ " <ignored count=\"0\"/>\n" +
+ " </removedocument>\n" +
+ " </route>\n" +
+ " </routes>\n" +
+ "\n" +
+ "</status>\n", val);
+ }
+
+ @Test
+ public void testMetricForIgnoredDocumentsIsIncreased() throws Exception {
+ DummySessionFactory.ReplyFactory replyFactory = new DummySessionFactory.ReplyFactory() {
+ @Override
+ public Reply createReply(Message m) {
+ return new DocumentIgnoredReply();
+ }
+ };
+ setupWithReplyFactory(replyFactory);
+ Result res = testFeed(xmlFilesPath + "test10b.xml", "feed?");
+ assertEquals(2, res.messages.size());
+
+ String val = res.output.replaceAll("<([a-z]+).*count=\"([0-9]+)\".*/", "<$1 count=\"$2\"/");
+
+ assertEquals("<result>\n" +
+ "\n" +
+ " <route name=\"default\">\n" +
+ " <total>\n" +
+ " <ignored count=\"2\"/>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <ignored count=\"2\"/>\n" +
+ " </putdocument>\n" +
+ " </route>\n" +
+ "\n" +
+ "</result>\n", val);
+ }
+
+ @Test
+ public void testPostXMLWithMBusFailureAllowed() throws Exception {
+ setup(new com.yahoo.messagebus.Error(DocumentProtocol.ERROR_BUCKET_DELETED, "Hello world in <document>"));
+ Result res = testFeed(xmlFilesPath + "test10b.xml", "feed?abortonfeederror=false");
+
+ assertEquals(2, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10b", d.toString());
+ }
+
+ String val = res.output.replaceAll("average=\"[0-9]*\" last=\"[0-9]*\" min=\"[0-9]*\" max=\"[0-9]*\" ", "");
+ System.out.println(val);
+
+ assertEquals("<result>\n" +
+ "\n" +
+ " <route name=\"default\">\n" +
+ " <total>\n" +
+ " <errors>\n" +
+ " <error name=\"total\" count=\"2\"/>\n" +
+ " <error name=\"BUCKET_DELETED\" count=\"2\"/>\n" +
+ " </errors>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <errors>\n" +
+ " <error name=\"total\" count=\"2\"/>\n" +
+ " <error name=\"BUCKET_DELETED\" count=\"2\"/>\n" +
+ " </errors>\n" +
+ " </putdocument>\n" +
+ " </route>\n\n" +
+ " <errors count=\"2\">\n" +
+ " <error message=\"PUT[doc:news:http://news10a] [BUCKET_DELETED] Hello world in &lt;document&gt;\"/>\n" +
+ " <error message=\"PUT[doc:news:http://news10b] [BUCKET_DELETED] Hello world in &lt;document&gt;\"/>\n" +
+ " </errors>\n" +
+ "\n" +
+ "</result>\n", val);
+
+ assertTrue(res.error != null);
+ assertTrue(res.errorCount > 0);
+ }
+
+ @Test
+ public void testPostXMLWithMBusFailure() throws Exception {
+ setup(new com.yahoo.messagebus.Error(32, "Hello world"));
+ Result res = testFeed(xmlFilesPath + "test10b.xml", "feed?");
+
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+
+ String val = res.output.replaceAll("average=\"[0-9]*\" last=\"[0-9]*\" min=\"[0-9]*\" max=\"[0-9]*\" ", "");
+ assertEquals("<result>\n" +
+ "\n" +
+ " <route name=\"default\">\n" +
+ " <total>\n" +
+ " <errors>\n" +
+ " <error name=\"total\" count=\"1\"/>\n" +
+ " <error name=\"UNKNOWN(32)\" count=\"1\"/>\n" +
+ " </errors>\n" +
+ " </total>\n" +
+ " <putdocument>\n" +
+ " <errors>\n" +
+ " <error name=\"total\" count=\"1\"/>\n" +
+ " <error name=\"UNKNOWN(32)\" count=\"1\"/>\n" +
+ " </errors>\n" +
+ " </putdocument>\n" +
+ " </route>\n\n" +
+ " <errors count=\"1\">\n" +
+ " <error message=\"PUT[doc:news:http://news10a] [UNKNOWN(32)] Hello world\"/>\n" +
+ " </errors>\n" +
+ "\n" +
+ "</result>\n", val);
+
+ assertTrue(res.error != null);
+ assertTrue(res.errorCount > 0);
+ }
+
+ @Test
+ public void testPostXMLWithIllegalDocId() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test_bogus_docid.xml", "feed?");
+
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+ }
+
+ @Test
+ public void testPostXMLWithIllegalDocIdAllowFailure() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test_bogus_docid.xml", "feed?abortondocumenterror=false");
+
+ assertEquals(2, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:ok", d.toString());
+ }
+ }
+
+ @Test
+ public void testPostUnparseableXML() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test_bogus_xml.xml", "feed?");
+
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+ }
+
+ @Test
+ public void testOverrides() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test10b.xml", "feed?timeout=2.222&route=storage&priority=HIGH_2");
+
+ assertEquals(2, res.messages.size());
+
+ for (Message m : res.messages) {
+ assertEquals(2222, m.getTimeRemaining());
+ assertEquals(Route.parse("storage"), m.getRoute());
+ assertEquals(DocumentProtocol.Priority.HIGH_2, ((DocumentMessage)m).getPriority());
+ }
+ }
+
+ @Test
+ public void testBogusPriority() throws Exception {
+ try {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test10b.xml", "feed?timeout=2222&route=storage&priority=HIPSTER_DOOFUS");
+ assertTrue(false);
+ } catch (IllegalArgumentException e) {
+ }
+ }
+
+ @Test
+ public void testPostXMLWithIllegalDocIdFirst() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test_bogus_docid_first.xml", "feed?");
+
+ assertEquals(0, res.messages.size());
+ }
+
+ @Test
+ public void testPostXMLWithIllegalDocIdFirstNoAbort() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test_bogus_docid_first.xml", "feed?abortondocumenterror=false");
+
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_PUTDOCUMENT, m.getType());
+ DocumentId d = ((PutDocumentMessage)m).getDocumentPut().getDocument().getId();
+ assertEquals("doc:news:http://news10a", d.toString());
+ }
+ }
+
+ @Test
+ public void testSimpleRemove() throws Exception {
+ setup(null);
+ Result res = testRequest(HttpRequest.createTestRequest("remove?id=doc:test:removeme", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:test:removeme", d.toString());
+ }
+ }
+
+ @Test
+ public void testRemoveUser() throws Exception {
+ setup(null);
+
+ context.getClusterList().getStorageClusters().add(new ClusterDef("storage", "storage/cluster.storage"));
+ Result res = testRequest(HttpRequest.createTestRequest("removelocation?user=1234", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVELOCATION, m.getType());
+ String selection = ((RemoveLocationMessage)m).getDocumentSelection();
+ assertEquals("storage", m.getRoute().toString());
+ assertEquals("id.user=1234", selection);
+ }
+ }
+
+ @Test
+ public void testRemoveGroup() throws Exception {
+ setup(null);
+ context.getClusterList().getStorageClusters().add(new ClusterDef("storage", "storage/cluster.storage"));
+ Result res = testRequest(HttpRequest.createTestRequest("removelocation?group=foo", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVELOCATION, m.getType());
+ String selection = ((RemoveLocationMessage)m).getDocumentSelection();
+ assertEquals("storage", m.getRoute().toString());
+ assertEquals("id.group=\"foo\"", selection);
+ }
+ }
+
+ @Test
+ public void testRemoveBadSyntax() throws Exception {
+ setup(null);
+ context.getClusterList().getStorageClusters().add(new ClusterDef("storage", "storage/cluster.storage"));
+ Result res = testRequest(HttpRequest.createTestRequest("removelocation?group=foo&user=12345", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(0, res.messages.size());
+ assertTrue(res.error.toString().contains("Exactly one of"));
+ }
+
+ @Test
+ public void testRemoveGroupMultipleClusters() throws Exception {
+ setup(null);
+ context.getClusterList().getStorageClusters().add(new ClusterDef("storage1", "storage/cluster.storage1"));
+ context.getClusterList().getStorageClusters().add(new ClusterDef("storage2", "storage/cluster.storage2"));
+ Result res = testRequest(HttpRequest.createTestRequest("removelocation?group=foo", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(0, res.messages.size());
+ assertTrue(res.error.toString().contains("More than one"));
+ }
+
+ @Test
+ public void testRemoveGroupNoClusters() throws Exception {
+ setup(null);
+ Result res = testRequest(HttpRequest.createTestRequest("removelocation?group=foo", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(0, res.messages.size());
+ assertTrue(res.error.toString().contains("No storage clusters"));
+ }
+
+ @Test
+ public void testRemoveSelection() throws Exception {
+ setup(null);
+ context.getClusterList().getStorageClusters().add(new ClusterDef("storage", "storage/cluster.storage"));
+ Result res = testRequest(HttpRequest.createTestRequest("removelocation?selection=id.user=1234", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVELOCATION, m.getType());
+ String selection = ((RemoveLocationMessage)m).getDocumentSelection();
+ assertEquals("id.user=1234", selection);
+ }
+ }
+
+ @Test
+ public void testSimpleRemoveIndex() throws Exception {
+ setup(null);
+ Result res = testRequest(HttpRequest.createTestRequest("remove?id[0]=doc:test:removeme", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(1, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:test:removeme", d.toString());
+ }
+ }
+
+ @Test
+ public void testPostRemove() throws Exception {
+ setup(null);
+ Result res = testFeed(xmlFilesPath + "test_removes", "remove?");
+ assertEquals(2, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:test:remove1", d.toString());
+ }
+
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:test:remove2", d.toString());
+ }
+ }
+
+ @Test
+ public void testRemoveBogusId() throws Exception {
+ try {
+ setup(null);
+ Result res = testRequest(HttpRequest.createTestRequest("remove?id=unknowndoc:test:removeme", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertTrue(false);
+ } catch (Exception e) {
+ }
+ }
+
+ @Test
+ public void testMultiRemove() throws Exception {
+ setup(null);
+ Result res = testRequest(HttpRequest.createTestRequest("remove?id[0]=doc:test:removeme&id[1]=doc:test:remove2&id[2]=doc:test:remove3", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(3, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:test:removeme", d.toString());
+ }
+
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:test:remove2", d.toString());
+ }
+
+ {
+ Message m = res.messages.get(2);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ DocumentId d = ((RemoveDocumentMessage)m).getDocumentId();
+ assertEquals("doc:test:remove3", d.toString());
+ }
+ }
+
+ @Test
+ public void testMultiRemoveSameDoc() throws Exception {
+ setup(null);
+ Result res = testRequest(HttpRequest.createTestRequest("remove?id[0]=userdoc:footype:1234:foo&id[1]=userdoc:footype:1234:foo", com.yahoo.jdisc.http.HttpRequest.Method.PUT));
+ assertEquals(2, res.messages.size());
+
+ {
+ Message m = res.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ }
+
+ {
+ Message m = res.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_REMOVEDOCUMENT, m.getType());
+ }
+ }
+
+ @Test
+ public void testFeedHandlerStatusCreation() throws Exception {
+ VespaFeedHandlerStatus status = new VespaFeedHandlerStatus(
+ new FeedContext(new MessagePropertyProcessor(
+ new FeederConfig(new FeederConfig.Builder()),
+ new LoadTypeConfig(new LoadTypeConfig.Builder())),
+ factory, null, new ClusterList(), new NullFeedMetric()),
+ true, true,
+ Executors.newCachedThreadPool());
+ }
+
+ private class TestDocProc extends DocumentProcessor {
+ @Override
+ public Progress process(Processing processing) {
+ for (DocumentOperation op : processing.getDocumentOperations()) {
+ if (op instanceof DocumentPut) {
+ Document document = ((DocumentPut)op).getDocument();
+ document.setFieldValue("last_downloaded", new IntegerFieldValue(1234));
+ }
+ }
+ return Progress.DONE;
+ }
+ }
+
+ private class TestLaterDocProc extends DocumentProcessor {
+ private final Logger log = Logger.getLogger(TestLaterDocProc.class.getName());
+
+ private int counter = 0;
+ @Override
+ public Progress process(Processing processing) {
+ synchronized (this) {
+ counter++;
+ if (counter % 2 == 1) {
+ log.info("Returning LATER.");
+ return Progress.LATER;
+ }
+ log.info("Returning DONE.");
+ return Progress.DONE;
+ }
+ }
+ }
+
+ private Result testRequest(HttpRequest req) throws Exception {
+ HttpResponse response = null;
+ String feedPrefix = "feed";
+ String removePrefix = "remove";
+ String feedStatusPrefix = "feedstatus";
+ String removeLocationPrefix = "removelocation";
+
+ if (req.getUri().getPath().startsWith(feedPrefix)) {
+ response = feedHandler.handle(req);
+ }
+ if (req.getUri().getPath().startsWith(removePrefix)) {
+ response = removeHandler.handle(req);
+ }
+ if (req.getUri().getPath().startsWith(feedStatusPrefix)) {
+ response = statusHandler.handle(req);
+ }
+ if (req.getUri().getPath().startsWith(removeLocationPrefix)) {
+ response = removeLocationHandler.handle(req);
+ }
+
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ response.render(output);
+
+ Result res = new Result();
+ res.messages = factory.messages;
+ res.output = new String(output.toByteArray());
+
+ if (response instanceof FeedResponse) {
+ FeedResponse feedResponse = (FeedResponse)response;
+ res.error = feedResponse.getErrorMessageList().isEmpty() ? null : feedResponse.getErrorMessageList().get(0);
+ res.errorCount = feedResponse.getErrorMessageList().size();
+ assertTrue(feedResponse.isSuccess() == (res.errorCount == 0));
+ }
+ return res;
+ }
+
+ private Result testFeed(String xmlFile, String request) throws Exception {
+ return testRequest(new FileRequest(new File(xmlFile), request).toRequest());
+ }
+
+ private Result testFeedGZIP(String xmlFile, String request) throws Exception {
+ return testRequest(new FileRequest(new File(xmlFile), request, true).toRequest());
+ }
+
+ private class FileRequest {
+
+ private final String req;
+ private final File f;
+ private boolean gzip = false;
+
+ FileRequest(File f, String req) {
+ this.req = req;
+ this.f = f;
+ }
+
+ FileRequest(File f, String req, boolean gzip) {
+ this.f = f;
+ this.req = req;
+ this.gzip = gzip;
+ }
+
+ public InputStream getData() {
+ try {
+ InputStream fileStream = new FileInputStream(f);
+ if (gzip) {
+ // Not exactly pretty, but in lack of an elegant way of transcoding
+ ByteArrayOutputStream rawOut = new ByteArrayOutputStream();
+ GZIPOutputStream compressed = new GZIPOutputStream(rawOut);
+ byte[] buffer = new byte[1024];
+ int read = -1;
+ while (true) {
+ read = fileStream.read(buffer);
+ if (read == -1) break;
+ compressed.write(buffer, 0, read);
+ }
+ compressed.finish();
+ compressed.flush();
+ rawOut.flush();
+ return new ByteArrayInputStream(rawOut.toByteArray());
+ }
+ return fileStream;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public void addHeaders(HeaderFields headers) {
+ headers.add("Content-Type", "image/jpeg");
+ if (gzip)
+ headers.add("Content-Encoding", "gzip");
+ }
+
+ public HttpRequest toRequest() {
+ HttpRequest request = HttpRequest.createTestRequest(req, com.yahoo.jdisc.http.HttpRequest.Method.GET, getData());
+ addHeaders(request.getJDiscRequest().headers());
+ return request;
+ }
+
+ }
+
+ private class Result {
+ private List<Message> messages;
+ private String output;
+ private com.yahoo.processing.request.ErrorMessage error;
+ private int errorCount;
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/v3/FeedTesterV3.java b/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/v3/FeedTesterV3.java
new file mode 100644
index 00000000000..708a9ed7a6b
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/feedhandler/v3/FeedTesterV3.java
@@ -0,0 +1,134 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.feedhandler.v3;
+
+import com.google.common.base.Splitter;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.messagebus.SessionCache;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.documentapi.messagebus.protocol.PutDocumentMessage;
+import com.yahoo.feedhandler.NullFeedMetric;
+import com.yahoo.jdisc.ReferencedResource;
+import com.yahoo.messagebus.SourceSessionParams;
+import com.yahoo.messagebus.shared.SharedSourceSession;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.http.client.config.FeedParams;
+import com.yahoo.vespa.http.client.core.ErrorCode;
+import com.yahoo.vespa.http.client.core.Headers;
+import com.yahoo.vespa.http.client.core.OperationStatus;
+import com.yahoo.vespa.http.server.ReplyContext;
+import com.yahoo.vespa.http.server.FeedHandlerV3;
+import org.junit.Test;
+import com.yahoo.container.jdisc.HttpRequest;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import com.yahoo.messagebus.Result;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+public class FeedTesterV3 {
+
+ @Test
+ public void feedOneDocument() throws Exception {
+ final FeedHandlerV3 feedHandlerV3 = setupFeederHandler();
+ HttpResponse httpResponse = feedHandlerV3.handle(createRequest(1));
+ ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+ httpResponse.render(outStream);
+ assertThat(httpResponse.getContentType(), is("text/plain"));
+ assertThat(Utf8.toString(outStream.toByteArray()), is("1230 OK message trace\n"));
+
+ }
+
+ @Test
+ public void feedManyDocument() throws Exception {
+ final FeedHandlerV3 feedHandlerV3 = setupFeederHandler();
+ HttpResponse httpResponse = feedHandlerV3.handle(createRequest(100));
+ ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+ httpResponse.render(outStream);
+ assertThat(httpResponse.getContentType(), is("text/plain"));
+ String result = Utf8.toString(outStream.toByteArray());
+ assertThat(Splitter.on("\n").splitToList(result).size(), is(101));
+ }
+
+ DocumentTypeManager createDoctypeManager() {
+ DocumentTypeManager docTypeManager = new DocumentTypeManager();
+ DocumentType documentType = new DocumentType("testdocument");
+ documentType.addField("title", DataType.STRING);
+ documentType.addField("body", DataType.STRING);
+ docTypeManager.registerDocumentType(documentType);
+ return docTypeManager;
+ }
+
+ HttpRequest createRequest(int numberOfDocs) {
+ String clientId = "client123";
+ StringBuilder wireData = new StringBuilder();
+ for (int x = 0; x < numberOfDocs; x++) {
+ String docData = "[{\"put\": \"id:testdocument:testdocument::c\", \"fields\": { \"title\": \"fooKey\", \"body\": \"value\"}}]";
+ String operationId = "123" + x;
+ wireData.append(operationId + " " + Integer.toHexString(docData.length()) + "\n" + docData);
+ }
+ InputStream inputStream = new ByteArrayInputStream(wireData.toString().getBytes());
+ HttpRequest request = HttpRequest.createTestRequest(
+ "http://dummyhostname:19020/reserved-for-internal-use/feedapi",
+ com.yahoo.jdisc.http.HttpRequest.Method.POST,
+ inputStream);
+ request.getJDiscRequest().headers().add(Headers.VERSION, "3");
+ request.getJDiscRequest().headers().add(Headers.DATA_FORMAT, FeedParams.DataFormat.JSON_UTF8.name());
+ request.getJDiscRequest().headers().add(Headers.TIMEOUT, "1000000000");
+ request.getJDiscRequest().headers().add(Headers.CLIENT_ID, clientId);
+ request.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST");
+ request.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4");
+ request.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ return request;
+ }
+
+ FeedHandlerV3 setupFeederHandler() throws Exception {
+ Executor threadPool = Executors.newCachedThreadPool();
+ DocumentmanagerConfig docMan = new DocumentmanagerConfig(new DocumentmanagerConfig.Builder().enablecompression(true));
+ FeedHandlerV3 feedHandlerV3 = new FeedHandlerV3(
+ threadPool, docMan, null /* session cache */ , new NullFeedMetric(), AccessLog.voidAccessLog(), null) {
+ @Override
+ protected ReferencedResource<SharedSourceSession> retainSource(
+ SessionCache sessionCache, SourceSessionParams sessionParams) {
+ SharedSourceSession sharedSourceSession = mock(SharedSourceSession.class);
+
+ try {
+ Mockito.stub(sharedSourceSession.sendMessageBlocking(anyObject())).toAnswer((Answer) invocation -> {
+ Object[] args = invocation.getArguments();
+ PutDocumentMessage putDocumentMessage = (PutDocumentMessage) args[0];
+ ReplyContext replyContext = (ReplyContext)putDocumentMessage.getContext();
+ replyContext.feedReplies.add(new OperationStatus("message", replyContext.docId, ErrorCode.OK, "trace"));
+ Result result = mock(Result.class);
+ when(result.isAccepted()).thenReturn(true);
+ return result;
+ });
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ Result result = mock(Result.class);
+ when(result.isAccepted()).thenReturn(true);
+ ReferencedResource<SharedSourceSession> refSharedSessopn =
+ new ReferencedResource<>(sharedSourceSession, () -> {});
+ return refSharedSessopn;
+ }
+ };
+ feedHandlerV3.injectDocumentManangerForTests(createDoctypeManager());
+ return feedHandlerV3;
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/ContinuationHitTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/ContinuationHitTest.java
new file mode 100644
index 00000000000..8b991909cd8
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/ContinuationHitTest.java
@@ -0,0 +1,103 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.storage.searcher;
+
+import com.yahoo.document.BucketId;
+import com.yahoo.documentapi.ProgressToken;
+import com.yahoo.documentapi.VisitorIterator;
+import org.junit.Test;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import static org.junit.Assert.*;
+
+public class ContinuationHitTest {
+
+ private static final String SINGLE_BUCKET_URL_SAFE_BASE64
+ = "AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAHqNFZ4mrz-_wAAAAAAAAAA";
+ private static final String MULTI_BUCKET_URL_SAFE_BASE64
+ = "AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAPqNFZ4mrz--gAAAAAAAAAA6" +
+ "jRWeJq8_vsAAAAAAAAAAOo0VniavP7_AAAAAAAAAAA=";
+
+ @Test
+ public void continuationTokensAreUrlSafeBase64Encoded() throws Exception {
+ ContinuationHit hit = new ContinuationHit(createSingleBucketProgress());
+ // We want -_ instead of +/
+ assertEquals(SINGLE_BUCKET_URL_SAFE_BASE64, hit.getValue());
+ }
+
+ @Test
+ public void continuationTokensAreNotBrokenIntoMultipleLines() throws Exception {
+ ContinuationHit hit = new ContinuationHit(createMultiBucketProgress());
+ assertTrue(hit.getValue().length() > 76); // Ensure we exceed MIME line length limits.
+ assertFalse(hit.getValue().contains("\n"));
+ }
+
+ @Test
+ public void decodingAcceptsUrlSafeTokens() throws Exception {
+ final ProgressToken token = ContinuationHit.getToken(SINGLE_BUCKET_URL_SAFE_BASE64);
+ // Roundtrip should yield identical results.
+ assertEquals(SINGLE_BUCKET_URL_SAFE_BASE64,
+ new ContinuationHit(token).getValue());
+ }
+
+ /**
+ * Legacy Base64 encoder emitted MIME Base64. Ensure we handle tokens from that era.
+ */
+ @Test
+ public void decodingAcceptsLegacyNonUrlSafeTokens() throws Exception {
+ final String legacyBase64 = convertedToMimeBase64Chars(SINGLE_BUCKET_URL_SAFE_BASE64);
+ final ProgressToken legacyToken = ContinuationHit.getToken(legacyBase64);
+
+ assertEquals(SINGLE_BUCKET_URL_SAFE_BASE64,
+ new ContinuationHit(legacyToken).getValue());
+ }
+
+ /**
+ * Legacy Base64 encoder would happily output line breaks after each MIME line
+ * boundary. Ensure we handle these gracefully.
+ */
+ @Test
+ public void decodingAcceptsLegacyMimeLineBrokenTokens() throws Exception {
+ final String multiBucketLegacyToken =
+ "AAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAPqNFZ4mrz++gAAAAAAAAAA6jRWeJq8/vsA\r\n" +
+ "AAAAAAAAAOo0VniavP7/AAAAAAAAAAA=";
+ final ProgressToken legacyToken = ContinuationHit.getToken(multiBucketLegacyToken);
+
+ assertEquals(MULTI_BUCKET_URL_SAFE_BASE64,
+ new ContinuationHit(legacyToken).getValue());
+ }
+
+ /**
+ * Returns a ProgressToken whose base 64 representation will be _less_ than 76 bytes (MIME line limit)
+ */
+ private ProgressToken createSingleBucketProgress() {
+ ProgressToken token = new ProgressToken(16);
+ // Use explicit bucket set so we can better control the binary representation
+ // of the buckets, and thus the values written as base 64.
+ Set<BucketId> buckets = new TreeSet<>();
+ // This particular bucket ID will contain +/ chars when output as non-URL safe base 64.
+ buckets.add(new BucketId(58, 0x123456789abcfeffL));
+ VisitorIterator.createFromExplicitBucketSet(buckets, 16, token); // "Prime" the token.
+ return token;
+ }
+
+ /**
+ * Returns a ProgressToken whose base 64 representation will be _more_ than 76 bytes (MIME line limit)
+ */
+ private ProgressToken createMultiBucketProgress() {
+ ProgressToken token = new ProgressToken(16);
+ Set<BucketId> buckets = new TreeSet<>();
+ buckets.add(new BucketId(58, 0x123456789abcfeffL));
+ buckets.add(new BucketId(58, 0x123456789abcfefaL));
+ buckets.add(new BucketId(58, 0x123456789abcfefbL));
+ VisitorIterator.createFromExplicitBucketSet(buckets, 16, token); // "Prime" the token.
+ return token;
+ }
+
+ private String convertedToMimeBase64Chars(String token) {
+ // Doesn't split on MIME line boundaries, so not fully MIME compliant.
+ return token.replace('-', '+').replace('_', '/');
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/DocumentSessionFactory.java b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/DocumentSessionFactory.java
new file mode 100755
index 00000000000..ea88128aaa9
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/DocumentSessionFactory.java
@@ -0,0 +1,129 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.storage.searcher;
+
+import com.yahoo.document.Document;
+import com.yahoo.document.DocumentId;
+import com.yahoo.document.DocumentType;
+import com.yahoo.documentapi.VisitorParameters;
+import com.yahoo.documentapi.VisitorSession;
+import com.yahoo.documentapi.messagebus.protocol.GetDocumentMessage;
+import com.yahoo.documentapi.messagebus.protocol.GetDocumentReply;
+import com.yahoo.feedapi.DummySessionFactory;
+import com.yahoo.feedapi.SendSession;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.messagebus.*;
+import com.yahoo.messagebus.Error;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Used to automatically reply with a GetDocumentReply for every received GetDocumentMessage
+ */
+public class DocumentSessionFactory extends DummySessionFactory {
+
+ private DocumentType docType;
+ private Error error;
+ // Reply instances are shared between the two collections.
+ List<GetDocumentReply> autoReplies;
+ Map<DocumentId, GetDocumentReply> autoReplyLookup = new HashMap<>();
+ boolean autoReply = true;
+ boolean nullReply = false;
+ private int sessionsCreated = 0;
+
+ private class DocumentReplySession extends SendSession {
+
+ ReplyHandler handler;
+ Error e;
+ DummySessionFactory owner;
+
+ public DocumentReplySession(ReplyHandler handler, Error e, DummySessionFactory owner) {
+ this.handler = handler;
+ this.e = e;
+ this.owner = owner;
+ }
+
+ protected Result onSend(Message m, boolean blockIfQueueFull) throws InterruptedException {
+ if (!(m instanceof GetDocumentMessage)) {
+ throw new IllegalArgumentException("Expected GetDocumentMessage");
+ }
+ GetDocumentMessage gm = (GetDocumentMessage)m;
+ owner.messages.add(m);
+ if (autoReply) {
+ Document replyDoc;
+ if (!nullReply) {
+ replyDoc = new Document(docType, gm.getDocumentId());
+ } else {
+ replyDoc = null;
+ }
+ Reply r = new GetDocumentReply(replyDoc);
+ r.setMessage(m);
+ r.setContext(m.getContext());
+ if (e != null) {
+ r.addError(e);
+ }
+ handler.handleReply(r);
+ } else if (owner.messages.size() == autoReplies.size()) {
+ // Pair up all replies with their messages
+ for (Message msg : owner.messages) {
+ GetDocumentReply reply = autoReplyLookup.get(((GetDocumentMessage)msg).getDocumentId());
+ reply.setMessage(msg);
+ reply.setContext(msg.getContext());
+ if (e != null) {
+ reply.addError(e);
+ }
+ }
+ // Now send them in the correct order. Instances are shared, so source
+ // messages and contexts are properly set
+ for (Reply reply : autoReplies) {
+ handler.handleReply(reply);
+ }
+ }
+
+ return Result.ACCEPTED;
+ }
+
+ public void close() {
+ }
+ }
+
+ public DocumentSessionFactory(DocumentType docType) {
+ this.docType = docType;
+ this.error = null;
+ }
+
+ public DocumentSessionFactory(DocumentType docType, Error error, boolean autoReply, GetDocumentReply... autoReplies) {
+ this.docType = docType;
+ this.error = error;
+ this.autoReplies = Arrays.asList(autoReplies);
+ for (GetDocumentReply reply : autoReplies) {
+ this.autoReplyLookup.put(reply.getDocument().getId(), reply);
+ }
+ this.autoReply = autoReply;
+ }
+
+ public boolean isNullReply() {
+ return nullReply;
+ }
+
+ public void setNullReply(boolean nullReply) {
+ this.nullReply = nullReply;
+ }
+
+ public int getSessionsCreated() {
+ return sessionsCreated;
+ }
+
+ public SendSession createSendSession(ReplyHandler r, Metric metric) {
+ ++sessionsCreated;
+ return new DocumentReplySession(r, error, this);
+ }
+
+ @Override
+ public VisitorSession createVisitorSession(VisitorParameters p) {
+ return new DummyVisitorSession(p, docType);
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/DummyVisitorSession.java b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/DummyVisitorSession.java
new file mode 100644
index 00000000000..d1c55c0968f
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/DummyVisitorSession.java
@@ -0,0 +1,98 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.storage.searcher;
+
+import com.yahoo.document.Document;
+import com.yahoo.document.DocumentId;
+import com.yahoo.document.DocumentPut;
+import com.yahoo.document.DocumentType;
+import com.yahoo.documentapi.*;
+import com.yahoo.documentapi.messagebus.protocol.PutDocumentMessage;
+import com.yahoo.documentapi.messagebus.protocol.RemoveDocumentMessage;
+import com.yahoo.messagebus.Message;
+import com.yahoo.messagebus.Trace;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Stub to test visitors.
+ */
+public class DummyVisitorSession implements VisitorSession {
+
+ final VisitorParameters parameters;
+ final DocumentType documentType;
+ final List<Message> autoReplyMessages = new ArrayList<>();
+
+ DummyVisitorSession(VisitorParameters p, DocumentType documentType) {
+ parameters = p;
+ this.documentType = documentType;
+ p.getLocalDataHandler().setSession(this);
+ addDefaultReplyMessages();
+ }
+
+ @Override
+ public boolean isDone() {
+ return true;
+ }
+
+ @Override
+ public ProgressToken getProgress() {
+ return new ProgressToken(12);
+ }
+
+ @Override
+ public Trace getTrace() {
+ return null;
+ }
+
+ public void addDocumentReply(String docId) {
+ Document replyDoc = new Document(documentType, docId);
+ autoReplyMessages.add(new PutDocumentMessage(new DocumentPut(replyDoc)));
+ }
+
+ public void addRemoveReply(String docId) {
+ autoReplyMessages.add(new RemoveDocumentMessage(new DocumentId(docId)));
+ }
+
+ public void addDefaultReplyMessages() {
+ addDocumentReply("userdoc:foo:1234:bar");
+ if (parameters.visitRemoves()) {
+ addRemoveReply("userdoc:foo:1234:removed");
+ }
+ }
+
+ public void clearAutoReplyMessages() {
+ autoReplyMessages.clear();
+ }
+
+ @Override
+ public boolean waitUntilDone(long l) throws InterruptedException {
+ for (Message msg : autoReplyMessages) {
+ parameters.getLocalDataHandler().onMessage(msg, new AckToken(this));
+ }
+ return true;
+ }
+
+ @Override
+ public void ack(AckToken ackToken) {
+ }
+
+ @Override
+ public void abort() {
+ }
+
+ @Override
+ public VisitorResponse getNext() {
+ return null;
+ }
+
+ @Override
+ public VisitorResponse getNext(int i) throws InterruptedException {
+ return null;
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/GetSearcherTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/GetSearcherTestCase.java
new file mode 100755
index 00000000000..1e39b9766b1
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/GetSearcherTestCase.java
@@ -0,0 +1,1090 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.storage.searcher;
+
+import com.yahoo.component.chain.Chain;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.feedhandler.NullFeedMetric;
+import com.yahoo.jdisc.HeaderFields;
+import com.yahoo.vespa.config.content.LoadTypeConfig;
+import com.yahoo.document.*;
+import com.yahoo.document.datatypes.IntegerFieldValue;
+import com.yahoo.document.datatypes.Raw;
+import com.yahoo.document.datatypes.StringFieldValue;
+import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol;
+import com.yahoo.documentapi.messagebus.protocol.GetDocumentMessage;
+import com.yahoo.documentapi.messagebus.protocol.GetDocumentReply;
+import com.yahoo.feedapi.FeedContext;
+import com.yahoo.feedapi.MessagePropertyProcessor;
+import com.yahoo.java7compat.Util;
+import com.yahoo.messagebus.Message;
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.prelude.templates.SearchRendererAdaptor;
+import com.yahoo.search.Query;
+import com.yahoo.search.Result;
+import com.yahoo.search.Searcher;
+import com.yahoo.search.rendering.RendererRegistry;
+import com.yahoo.search.result.Hit;
+import com.yahoo.search.result.HitGroup;
+import com.yahoo.search.searchchain.Execution;
+import com.yahoo.vespaclient.ClusterList;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+import org.junit.Test;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.zip.GZIPOutputStream;
+
+import static org.junit.Assert.*;
+
+public class GetSearcherTestCase {
+
+ private DocumentTypeManager docMan = null;
+ private DocumentType docType;
+ private FeederConfig defFeedCfg = new FeederConfig(new FeederConfig.Builder());
+ private LoadTypeConfig defLoadTypeCfg = new LoadTypeConfig(new LoadTypeConfig.Builder());
+
+ @org.junit.Before
+ public void setUp() {
+ docMan = new DocumentTypeManager();
+ docType = new DocumentType("kittens");
+ docType.addHeaderField("name", DataType.STRING);
+ docType.addField("description", DataType.STRING);
+ docType.addField("image", DataType.STRING);
+ docType.addField("fluffiness", DataType.INT);
+ docType.addField("foo", DataType.RAW);
+ docMan.registerDocumentType(docType);
+ }
+
+ @org.junit.After
+ public void tearDown() {
+ docMan = null;
+ docType = null;
+ }
+
+ private void assertHits(HitGroup hits, String... wantedHits) {
+ assertEquals(wantedHits.length, hits.size());
+ for (int i = 0; i < wantedHits.length; ++i) {
+ assertTrue(hits.get(i) instanceof DocumentHit);
+ DocumentHit hit = (DocumentHit)hits.get(i);
+ assertEquals(wantedHits[i], hit.getDocument().getId().toString());
+ }
+ }
+
+ @Test
+ public void testGetSingleDocumentQuery() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType); // Needs auto-reply
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(newQuery("?id=userdoc:kittens:1:2"));
+ System.out.println("HTTP request is " + result.getQuery().getHttpRequest());
+
+ assertEquals(1, factory.messages.size());
+ {
+ Message m = factory.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("userdoc:kittens:1:2", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ }
+ assertEquals(1, result.hits().size());
+ assertHits(result.hits(), "userdoc:kittens:1:2");
+ // By default, document hit should not have its hit fields set
+ DocumentHit hit = (DocumentHit)result.hits().get(0);
+ assertEquals(0, hit.fieldKeys().size());
+ }
+
+ @Test
+ public void testGetMultipleDocumentsQuery() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Query query = newQuery("?id[0]=userdoc:kittens:1:2&id[1]=userdoc:kittens:3:4");
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(query);
+
+ assertEquals(2, factory.messages.size());
+ {
+ Message m = factory.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("userdoc:kittens:1:2", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ }
+
+ {
+ Message m = factory.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("userdoc:kittens:3:4", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ }
+ assertEquals(2, result.hits().size());
+ assertNull(result.hits().getErrorHit());
+ assertHits(result.hits(), "userdoc:kittens:1:2", "userdoc:kittens:3:4");
+ assertEquals(2, query.getHits());
+ }
+
+ // Test that you can use both query string and POSTed IDs
+ @Test
+ public void testGetMultipleDocumentsQueryAndPOST() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ String data = "userdoc:kittens:5:6\nuserdoc:kittens:7:8\nuserdoc:kittens:9:10";
+ MockHttpRequest request = new MockHttpRequest(data.getBytes("utf-8"), "/get/?id[0]=userdoc:kittens:1:2&id[1]=userdoc:kittens:3:4");
+ Query query = new Query(request.toRequest());
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(query);
+
+ assertEquals(5, factory.messages.size());
+ assertEquals(5, result.hits().size());
+ assertNull(result.hits().getErrorHit());
+ assertHits(result.hits(), "userdoc:kittens:1:2", "userdoc:kittens:3:4",
+ "userdoc:kittens:5:6", "userdoc:kittens:7:8", "userdoc:kittens:9:10");
+ }
+
+ @Test
+ public void testGetMultipleDocumentsQueryAndGZippedPOST() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ String data = "userdoc:kittens:5:6\nuserdoc:kittens:7:8\nuserdoc:kittens:9:10";
+
+ // Create with automatic GZIP encoding
+ MockHttpRequest request = new MockHttpRequest(data.getBytes("utf-8"), "/get/?id[0]=userdoc:kittens:1:2&id[1]=userdoc:kittens:3:4", true);
+ Query query = new Query(request.toRequest());
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(query);
+
+ assertEquals(5, factory.messages.size());
+ assertEquals(5, result.hits().size());
+ assertNull(result.hits().getErrorHit());
+ assertHits(result.hits(), "userdoc:kittens:1:2", "userdoc:kittens:3:4",
+ "userdoc:kittens:5:6", "userdoc:kittens:7:8", "userdoc:kittens:9:10");
+ }
+
+ /* Test that a query without any ids is passed through to the next chain */
+ @Test
+ public void testQueryPassThrough() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ HitGroup hits = new HitGroup("mock");
+ hits.add(new Hit("blernsball"));
+ Chain<Searcher> searchChain = new Chain<>(searcher, new MockBackend(hits));
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(newQuery("?flarn=blern"));
+
+ assertEquals(0, factory.messages.size());
+ assertEquals(1, result.hits().size());
+ assertNotNull(result.hits().get("blernsball"));
+ }
+
+ /* Test that a query will contain both document hits and hits from a searcher
+ * further down the chain, iff the searcher returns a DocumentHit.
+ */
+ @Test
+ public void testQueryPassThroughAndGet() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:1234:foo"));
+ doc1.setFieldValue("name", new StringFieldValue("megacat"));
+ doc1.setFieldValue("description", new StringFieldValue("supercat"));
+ doc1.setFieldValue("fluffiness", new IntegerFieldValue(10000));
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1)
+ };
+
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType, null, false, replies);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ DocumentHit backendHit = new DocumentHit(new Document(docType, new DocumentId("userdoc:kittens:5678:bar")), 5);
+ Chain<Searcher> searchChain = new Chain<>(searcher, new MockBackend(backendHit));
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(newQuery("?query=flarn&id=userdoc:kittens:1234:foo"));
+
+ assertEquals(1, factory.messages.size());
+ assertEquals(2, result.hits().size());
+ assertNotNull(result.hits().get("userdoc:kittens:5678:bar"));
+ assertNotNull(result.hits().get("userdoc:kittens:1234:foo"));
+ }
+
+ @Test
+ public void testQueryPassThroughAndGetUnknownBackendHit() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ HitGroup hits = new HitGroup("mock");
+ hits.add(new Hit("blernsball"));
+ Chain<Searcher> searchChain = new Chain<>(searcher, new MockBackend(hits));
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(newQuery("?flarn=blern&id=userdoc:kittens:9:aaa"));
+
+ assertEquals(0, factory.messages.size());
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"18\" message=\"Internal server error.: " +
+ "A backend searcher to com.yahoo.storage.searcher.GetSearcher returned a " +
+ "hit that was not an instance of com.yahoo.storage.searcher.DocumentHit. " +
+ "Only DocumentHit instances are supported in the backend hit result set " +
+ "when doing queries that contain document identifier sets recognised by the " +
+ "Get Searcher.\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testConfig() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(new FeederConfig(new FeederConfig.Builder().timeout(458).route("route66").retryenabled(false)), defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(newQuery("?id=doc:batman:dahnahnahnah"));
+
+ assertEquals(1, factory.messages.size());
+ {
+ Message m = factory.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("doc:batman:dahnahnahnah", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ assertEquals(Route.parse("route66"), gdm.getRoute());
+ assertFalse(gdm.getRetryEnabled());
+ assertEquals(458000, gdm.getTimeRemaining());
+ }
+ }
+
+ @Test
+ public void testConfigChanges() throws Exception {
+ String config = "raw:timeout 458\nroute \"riksveg18\"\nretryenabled true";
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(new FeederConfig(new FeederConfig.Builder().timeout(458).route("riksveg18").retryenabled(true)),
+ defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ new Execution(searchChain, Execution.Context.createContextStub()).search(newQuery("?id=doc:batman:dahnahnahnah"));
+
+ assertEquals(1, factory.messages.size());
+ assertEquals(1, factory.getSessionsCreated());
+ {
+ Message m = factory.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("doc:batman:dahnahnahnah", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ assertEquals(Route.parse("riksveg18"), gdm.getRoute());
+ assertTrue(gdm.getRetryEnabled());
+ assertEquals(458000, gdm.getTimeRemaining());
+ }
+
+ factory.messages.clear();
+
+ FeederConfig newConfig = new FeederConfig(new FeederConfig.Builder()
+ .timeout(123)
+ .route("e6")
+ .retryenabled(false)
+ );
+ searcher.getMessagePropertyProcessor().configure(newConfig, defLoadTypeCfg);
+
+ new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=doc:spiderman:does_whatever_a_spider_can"));
+
+ // riksveg18 is created again, and e6 is created as well.
+ assertEquals(3, factory.getSessionsCreated());
+
+ assertEquals(1, factory.messages.size());
+ {
+ Message m = factory.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("doc:spiderman:does_whatever_a_spider_can", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ assertEquals(Route.parse("e6"), gdm.getRoute());
+ assertFalse(gdm.getRetryEnabled());
+ assertEquals(123000, gdm.getTimeRemaining());
+ }
+ }
+
+ @Test
+ public void testQueryOverridesDefaults() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:1:2&id[1]=userdoc:kittens:3:4&priority=LOW_2&route=highwaytohell&timeout=458"));
+
+ assertEquals(2, factory.messages.size());
+ {
+ Message m = factory.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("userdoc:kittens:1:2", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ assertEquals(DocumentProtocol.Priority.LOW_2, gdm.getPriority());
+ assertEquals(Route.parse("highwaytohell"), gdm.getRoute());
+ assertEquals(458000, gdm.getTimeRemaining());
+ }
+
+ {
+ Message m = factory.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("userdoc:kittens:3:4", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ assertEquals(DocumentProtocol.Priority.LOW_2, gdm.getPriority());
+ assertEquals(Route.parse("highwaytohell"), gdm.getRoute());
+ assertEquals(458000, gdm.getTimeRemaining());
+ }
+ }
+
+ @Test
+ public void testQueryOverridesConfig() throws Exception {
+ String config = "raw:timeout 458\nroute \"route66\"";
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:1:2&id[1]=userdoc:kittens:3:4&priority=LOW_2&route=highwaytohell&timeout=123"));
+
+ assertEquals(2, factory.messages.size());
+ {
+ Message m = factory.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("userdoc:kittens:1:2", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ assertEquals(DocumentProtocol.Priority.LOW_2, gdm.getPriority());
+ assertEquals(Route.parse("highwaytohell"), gdm.getRoute());
+ assertEquals(123000, gdm.getTimeRemaining());
+ }
+
+ {
+ Message m = factory.messages.get(1);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("userdoc:kittens:3:4", d.toString());
+ assertEquals("[all]", gdm.getFieldSet());
+ assertEquals(DocumentProtocol.Priority.LOW_2, gdm.getPriority());
+ assertEquals(Route.parse("highwaytohell"), gdm.getRoute());
+ assertEquals(123000, gdm.getTimeRemaining());
+ }
+ }
+
+ @Test
+ public void testBadPriorityValue() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:1:2&priority=onkel_jubalon"));
+
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"3\" message=\"Illegal query: " +
+ "java.lang.IllegalArgumentException: No enum const" +
+ (Util.isJava7Compatible() ? "ant " : " class ") +
+ "com.yahoo.documentapi.messagebus.protocol.DocumentProtocol" +
+ (Util.isJava7Compatible() ? "." : "$") +
+ "Priority.onkel_jubalon\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testMultiIdBadArrayIndex() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ {
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[1]=userdoc:kittens:1:2"));
+
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"3\" message=\"Illegal query: " +
+ "java.lang.IllegalArgumentException: query contains document ID " +
+ "array that is not zero-based and/or linearly increasing\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ {
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:1:2&id[2]=userdoc:kittens:2:3"));
+
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"3\" message=\"Illegal query: " +
+ "java.lang.IllegalArgumentException: query contains document ID " +
+ "array that is not zero-based and/or linearly increasing\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ {
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:1:2&id[1]=userdoc:kittens:2:3"));
+
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"3\" message=\"Illegal query: " +
+ "java.lang.IllegalArgumentException: query contains document ID " +
+ "array that is not zero-based and/or linearly increasing\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ {
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0=userdoc:kittens:1:2"));
+
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"3\" message=\"Illegal query: " +
+ "java.lang.IllegalArgumentException: Malformed document ID array parameter\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+ }
+
+ @Test
+ public void testLegacyHeadersOnly() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType); // Needs auto-reply
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:1:2&headersonly=true"));
+
+ assertEquals(1, factory.messages.size());
+ {
+ Message m = factory.messages.get(0);
+ assertEquals(DocumentProtocol.MESSAGE_GETDOCUMENT, m.getType());
+ GetDocumentMessage gdm = (GetDocumentMessage)m;
+ DocumentId d = gdm.getDocumentId();
+ assertEquals("userdoc:kittens:1:2", d.toString());
+ assertEquals("[header]", gdm.getFieldSet());
+ }
+ assertEquals(1, result.hits().size());
+ assertHits(result.hits(), "userdoc:kittens:1:2");
+ }
+
+ @Test
+ public void testFieldSet() throws Exception {
+ }
+
+ @Test
+ public void testConsistentResultOrdering() throws Exception {
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(new Document(docType, new DocumentId("userdoc:kittens:1:2"))),
+ new GetDocumentReply(new Document(docType, new DocumentId("userdoc:kittens:7:8"))),
+ new GetDocumentReply(new Document(docType, new DocumentId("userdoc:kittens:555:123")))
+ };
+
+ // Use a predefined reply list to ensure messages are answered out of order
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType, null, false, replies);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:555:123&id[1]=userdoc:kittens:1:2&id[2]=userdoc:kittens:7:8"));
+
+ assertEquals(3, factory.messages.size());
+ assertEquals(3, result.hits().size());
+ // Hits must be in the same order as their document IDs in the query
+ assertHits(result.hits(), "userdoc:kittens:555:123", "userdoc:kittens:1:2", "userdoc:kittens:7:8");
+
+ assertEquals(0, ((DocumentHit)result.hits().get(0)).getIndex());
+ assertEquals(1, ((DocumentHit)result.hits().get(1)).getIndex());
+ assertEquals(2, ((DocumentHit)result.hits().get(2)).getIndex());
+ }
+
+ @Test
+ public void testResultWithSingleError() throws Exception {
+ com.yahoo.messagebus.Error err = new com.yahoo.messagebus.Error(32, "Alas, it went poorly");
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType, err, true);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:1:2&id[1]=userdoc:kittens:3:4"));
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"messagebus\" code=\"32\" message=\"Alas, it went poorly\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testResultWithMultipleErrors() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:77:88"));
+ Document doc2 = new Document(docType, new DocumentId("userdoc:kittens:99:111"));
+ GetDocumentReply errorReply1 = new GetDocumentReply(doc1);
+ errorReply1.addError(new com.yahoo.messagebus.Error(123, "userdoc:kittens:77:88 had fleas."));
+ GetDocumentReply errorReply2 = new GetDocumentReply(doc2);
+ errorReply2.addError(new com.yahoo.messagebus.Error(456, "userdoc:kittens:99:111 shredded the curtains."));
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ errorReply1,
+ errorReply2
+ };
+
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:77:88&id[1]=userdoc:kittens:99:111"));
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"messagebus\" code=\"123\" message=\"userdoc:kittens:77:88 had fleas.\"/>\n" +
+ "<error type=\"messagebus\" code=\"456\" message=\"userdoc:kittens:99:111 shredded the curtains.\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testResultWithNullDocument() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType, null, true);
+ factory.setNullReply(true);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:55:bad_document_id"));
+ // Document not found does not produce any hit at all, error or otherwise
+ assertNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testDefaultDocumentHitRendering() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:3:4"));
+ doc1.setFieldValue("name", new StringFieldValue("mittens"));
+ doc1.setFieldValue("description", new StringFieldValue("it's a cat"));
+ doc1.setFieldValue("fluffiness", new IntegerFieldValue(8));
+ Document doc2 = new Document(docType, new DocumentId("userdoc:kittens:1:2"));
+ doc2.setFieldValue("name", new StringFieldValue("garfield"));
+ doc2.setFieldValue("description",
+ new StringFieldValue("preliminary research indicates <em>hatred</em> of mondays. caution advised"));
+ doc2.setFieldValue("fluffiness", new IntegerFieldValue(2));
+ Document doc3 = new Document(docType, new DocumentId("userdoc:kittens:77:88"));
+ GetDocumentReply errorReply = new GetDocumentReply(doc3);
+ errorReply.addError(new com.yahoo.messagebus.Error(123, "userdoc:kittens:77:88 had fleas."));
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1),
+ new GetDocumentReply(doc2),
+ errorReply
+ };
+
+ // Use a predefined reply list to ensure messages are answered out of order
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result xmlResult = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:77:88&id[1]=userdoc:kittens:1:2&id[2]=userdoc:kittens:3:4"));
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"messagebus\" code=\"123\" message=\"userdoc:kittens:77:88 had fleas.\"/>\n" +
+ "</errors>\n" +
+ "<document documenttype=\"kittens\" documentid=\"userdoc:kittens:1:2\">\n" +
+ " <name>garfield</name>\n" +
+ " <description>preliminary research indicates &lt;em&gt;hatred&lt;/em&gt; of mondays. caution advised</description>\n" +
+ " <fluffiness>2</fluffiness>\n" +
+ "</document>\n" +
+ "<document documenttype=\"kittens\" documentid=\"userdoc:kittens:3:4\">\n" +
+ " <name>mittens</name>\n" +
+ " <description>it's a cat</description>\n" +
+ " <fluffiness>8</fluffiness>\n" +
+ "</document>\n" +
+ "</result>\n", xmlResult);
+ }
+
+ @Test
+ public void testDocumentFieldNoContentType() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:5:1"));
+ doc1.setFieldValue("name", "derrick");
+ doc1.setFieldValue("description", "kommisar katze");
+ doc1.setFieldValue("fluffiness", 0);
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1),
+ };
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:5:1&field=description"));
+
+ assertNull(result.hits().getErrorHit());
+ assertEquals("text/xml", result.getTemplating().getTemplates().getMimeType());
+ assertEquals("UTF-8", result.getTemplating().getTemplates().getEncoding());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>kommisar katze</result>\n", result);
+ }
+
+ @Test
+ public void testDocumentFieldEscapeXML() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:5:1"));
+ doc1.setFieldValue("name", "asfd");
+ doc1.setFieldValue("description", "<script type=\"evil/madness\">horror & screams</script>");
+ doc1.setFieldValue("fluffiness", 0);
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1),
+ };
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:5:1&field=description"));
+
+ assertNull(result.hits().getErrorHit());
+ assertEquals("text/xml", result.getTemplating().getTemplates().getMimeType());
+ assertEquals("UTF-8", result.getTemplating().getTemplates().getEncoding());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>&lt;script type=\"evil/madness\"&gt;horror &amp; screams&lt;/script&gt;</result>\n", result);
+ }
+
+ @Test
+ public void testDocumentFieldRawContent() throws Exception {
+ byte[] contentBytes = new byte[] { 0, -128, 127 };
+
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:123:456"));
+ doc1.setFieldValue("foo", new Raw(ByteBuffer.wrap(contentBytes)));
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1)
+ };
+
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:123:456&field=foo"));
+
+ assertNull(result.hits().getErrorHit());
+ assertEquals("application/octet-stream", result.getTemplating().getTemplates().getMimeType());
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ SearchRendererAdaptor.callRender(stream, result);
+ stream.flush();
+
+ byte[] resultBytes = stream.toByteArray();
+ assertEquals(contentBytes.length, resultBytes.length);
+ for (int i = 0; i < resultBytes.length; ++i) {
+ assertEquals(contentBytes[i], resultBytes[i]);
+ }
+ }
+
+ @Test
+ public void testDocumentFieldRawWithContentOverride() throws Exception {
+ byte[] contentBytes = new byte[] { 0, -128, 127 };
+
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:123:456"));
+ doc1.setFieldValue("foo", new Raw(ByteBuffer.wrap(contentBytes)));
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1)
+ };
+
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:123:456&field=foo&contenttype=text/fancy"));
+
+ assertNull(result.hits().getErrorHit());
+ assertEquals("text/fancy", result.getTemplating().getTemplates().getMimeType());
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ SearchRendererAdaptor.callRender(stream, result);
+ stream.flush();
+
+ byte[] resultBytes = stream.toByteArray();
+ assertEquals(contentBytes.length, resultBytes.length);
+ for (int i = 0; i < resultBytes.length; ++i) {
+ assertEquals(contentBytes[i], resultBytes[i]);
+ }
+ }
+
+ @Test
+ public void testDocumentFieldWithMultipleIDs() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:1:2&id[1]=userdoc:kittens:3:4&field=name"));
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"3\" message=\"Illegal query: " +
+ "java.lang.IllegalArgumentException: Field only valid for single document id query\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testDocumentFieldNotSet() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:5:1"));
+ doc1.setFieldValue("name", "asdf");
+ doc1.setFieldValue("description", "fdsafsdf");
+ doc1.setFieldValue("fluffiness", 10);
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1),
+ };
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:5:1&field=image"));
+
+ assertNotNull(result.hits().getErrorHit());
+ assertEquals(1, result.hits().size());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"16\" message=\"Resource not found.: " +
+ "Field 'image' found in document type, but had no content in userdoc:kittens:5:1\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+
+ @Test
+ public void testDocumentFieldWithDocumentNotFound() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType, null, true);
+ factory.setNullReply(true);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:1:2&field=name"));
+ assertNotNull(result.hits().getErrorHit());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"16\" message=\"Resource not found.: " +
+ "Document not found, could not return field 'name'\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testDocumentFieldNotReachableWithHeadersOnly() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:5:1"));
+ doc1.setFieldValue("name", "asdf");
+ // don't set body fields
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1),
+ };
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:5:1&field=description&headersonly=true"));
+
+ assertNotNull(result.hits().getErrorHit());
+ assertEquals(1, result.hits().size());
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"4\" message=\"Invalid query parameter: " +
+ "Field 'description' is located in document body, but headersonly " +
+ "prevents it from being retrieved in userdoc:kittens:5:1\"/>\n" +
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testVespaXMLTemplate() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:3:4"));
+ doc1.setFieldValue("name", "mittens");
+ doc1.setFieldValue("description", "it's a cat");
+ doc1.setFieldValue("fluffiness", 8);
+ Document doc2 = new Document(docType, new DocumentId("userdoc:kittens:1:2"));
+ doc2.setFieldValue("name", "garfield");
+ doc2.setFieldValue("description", "preliminary research indicates <em>hatred</em> of mondays. caution advised");
+ doc2.setFieldValue("fluffiness", 2);
+ Document doc3 = new Document(docType, new DocumentId("userdoc:kittens:77:88"));
+ GetDocumentReply errorReply = new GetDocumentReply(doc3);
+ errorReply.addError(new com.yahoo.messagebus.Error(123, "userdoc:kittens:77:88 lost in a <ni!>\"shrubbery\"</ni!>"));
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1),
+ new GetDocumentReply(doc2),
+ errorReply
+ };
+
+ // Use a predefined reply list to ensure messages are answered out of order
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id[0]=userdoc:kittens:77:88&id[1]=userdoc:kittens:1:2&id[2]=userdoc:kittens:3:4")); // TODO!
+
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"messagebus\" code=\"123\" message=\"userdoc:kittens:77:88 lost in a &lt;ni!&gt;&quot;shrubbery&quot;&lt;/ni!&gt;\"/>\n"+
+ "</errors>\n" +
+ "<document documenttype=\"kittens\" documentid=\"userdoc:kittens:1:2\">\n" +
+ " <name>garfield</name>\n" +
+ " <description>preliminary research indicates &lt;em&gt;hatred&lt;/em&gt; of mondays. caution advised</description>\n" +
+ " <fluffiness>2</fluffiness>\n" +
+ "</document>\n" +
+ "<document documenttype=\"kittens\" documentid=\"userdoc:kittens:3:4\">\n" +
+ " <name>mittens</name>\n" +
+ " <description>it's a cat</description>\n" +
+ " <fluffiness>8</fluffiness>\n" +
+ "</document>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testDocumentHitWithPopulatedHitFields() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:1234:foo"));
+ doc1.setFieldValue("name", new StringFieldValue("megacat"));
+ doc1.setFieldValue("description", new StringFieldValue("supercat"));
+ doc1.setFieldValue("fluffiness", new IntegerFieldValue(10000));
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new GetDocumentReply(doc1)
+ };
+
+ // Use a predefined reply list to ensure messages are answered out of order
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("?id=userdoc:kittens:1234:foo&populatehitfields=true"));
+ assertEquals(1, result.hits().size());
+ assertHits(result.hits(), "userdoc:kittens:1234:foo");
+
+ DocumentHit hit = (DocumentHit)result.hits().get(0);
+ Iterator<Map.Entry<String, Object>> iter = hit.fieldIterator();
+ Set<String> fieldSet = new TreeSet<>();
+ while (iter.hasNext()) {
+ Map.Entry<String, Object> kv = iter.next();
+ StringBuilder field = new StringBuilder();
+ field.append(kv.getKey()).append(" -> ").append(kv.getValue());
+ fieldSet.add(field.toString());
+ }
+ StringBuilder fields = new StringBuilder();
+ for (String s : fieldSet) {
+ fields.append(s).append("\n");
+ }
+ assertEquals(
+ "description -> supercat\n" +
+ "documentid -> userdoc:kittens:1234:foo\n" +
+ "fluffiness -> 10000\n" +
+ "name -> megacat\n",
+ fields.toString());
+ }
+
+ @Test
+ public void deserializationExceptionsAreHandledGracefully() throws Exception {
+ Document doc1 = new Document(docType, new DocumentId("userdoc:kittens:5:1"));
+ GetDocumentReply[] replies = new GetDocumentReply[] {
+ new MockFailingGetDocumentReply(doc1),
+ };
+ Chain<Searcher> searchChain = createSearcherChain(replies);
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(newQuery("?id=userdoc:kittens:5:1"));
+ assertRendered("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<errors>\n" +
+ "<error type=\"searcher\" code=\"18\" message=\"Internal server error.: " +
+ "Got exception of type java.lang.RuntimeException during document " +
+ "deserialization: epic dragon attack\"/>\n"+
+ "</errors>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void testJsonRendererSetting() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType); // Needs auto-reply
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ Chain<Searcher> searchChain = new Chain<>(searcher);
+
+ Query query = newQuery("?id=userdoc:kittens:1:2&format=json");
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(query);
+ assertFalse(result.getTemplating().getTemplates() instanceof DocumentXMLTemplate);
+ }
+
+ private Query newQuery(String queryString) {
+ return new Query(HttpRequest.createTestRequest(queryString, com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ }
+
+ private Chain<Searcher> createSearcherChain(GetDocumentReply[] replies) throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType, null, false, replies);
+ GetSearcher searcher = new GetSearcher(new FeedContext(
+ new MessagePropertyProcessor(defFeedCfg, defLoadTypeCfg),
+ factory, docMan, new ClusterList(), new NullFeedMetric()));
+ return new Chain<>(searcher);
+ }
+
+ private static class MockFailingGetDocumentReply extends GetDocumentReply {
+ private int countdown = 2;
+
+ private MockFailingGetDocumentReply(Document doc) {
+ super(doc);
+ }
+
+ @Override
+ public Document getDocument() {
+ // Reason for countdown is that the test DocumentSessionFactory calls
+ // getDocument once internally before code can ever reach handleReply.
+ if (--countdown == 0) {
+ throw new RuntimeException("epic dragon attack");
+ }
+ return super.getDocument();
+ }
+ }
+
+ private static class MockBackend extends Searcher {
+ private Hit hitToReturn;
+
+ public MockBackend(Hit hitToReturn) {
+ this.hitToReturn = hitToReturn;
+ }
+
+ public @Override Result search(Query query, Execution execution) {
+ Result result = new Result(query);
+ result.hits().add(hitToReturn);
+ return result;
+ }
+ }
+
+ private class MockHttpRequest {
+
+ private final String req;
+ private byte[] data;
+ private boolean gzip = false;
+
+ MockHttpRequest(byte[] data, String req) {
+ this.req = req;
+ this.data = data;
+ }
+
+ MockHttpRequest(byte[] data, String req, boolean gzip) {
+ this.data = data;
+ this.req = req;
+ this.gzip = gzip;
+ }
+
+ public InputStream getData() {
+ if (gzip) {
+ try {
+ ByteArrayOutputStream rawOut = new ByteArrayOutputStream();
+ GZIPOutputStream compressed = new GZIPOutputStream(rawOut);
+ compressed.write(data, 0, data.length);
+ compressed.finish();
+ compressed.flush();
+ rawOut.flush();
+ return new ByteArrayInputStream(rawOut.toByteArray());
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ return new ByteArrayInputStream(data);
+ }
+
+ public void addHeaders(HeaderFields headers) {
+ headers.add("Content-Type", "text/plain;encoding=UTF-8");
+ if (gzip)
+ headers.add("Content-Encoding", "gzip");
+ }
+
+ public com.yahoo.container.jdisc.HttpRequest toRequest() {
+ com.yahoo.container.jdisc.HttpRequest request = com.yahoo.container.jdisc.HttpRequest.createTestRequest(req, com.yahoo.jdisc.http.HttpRequest.Method.GET, getData());
+ addHeaders(request.getJDiscRequest().headers());
+ return request;
+ }
+
+ }
+
+ public static void assertRendered(String expected,Result result) throws Exception {
+ assertRendered(expected,result,true);
+ }
+
+ public static void assertRendered(String expected,Result result,boolean checkFullEquality) throws Exception {
+ if (checkFullEquality)
+ assertEquals(expected, ResultRenderingUtil.getRendered(result));
+ else
+ assertTrue(ResultRenderingUtil.getRendered(result).startsWith(expected));
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/ResultRenderingUtil.java b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/ResultRenderingUtil.java
new file mode 100644
index 00000000000..21d39089d2a
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/ResultRenderingUtil.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.storage.searcher;
+
+import com.yahoo.prelude.templates.SearchRendererAdaptor;
+import com.yahoo.processing.rendering.Renderer;
+import com.yahoo.search.Result;
+import com.yahoo.search.rendering.DefaultRenderer;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+
+public class ResultRenderingUtil {
+
+ public static String getRendered(Result result) throws Exception {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ Charset cs = Charset.forName("utf-8");
+ CharsetDecoder decoder = cs.newDecoder();
+ SearchRendererAdaptor.callRender(stream, result);
+ stream.flush();
+ return decoder.decode(ByteBuffer.wrap(stream.toByteArray())).toString();
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/VisitorSearcherTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/VisitorSearcherTestCase.java
new file mode 100644
index 00000000000..820f7f56e2f
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/storage/searcher/VisitorSearcherTestCase.java
@@ -0,0 +1,248 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.storage.searcher;
+
+import com.yahoo.component.chain.Chain;
+import com.yahoo.cloud.config.ClusterListConfig;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.protect.Error;
+import com.yahoo.documentapi.VisitorSession;
+import com.yahoo.feedhandler.NullFeedMetric;
+import com.yahoo.vespa.config.content.LoadTypeConfig;
+import com.yahoo.document.DataType;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.documentapi.VisitorParameters;
+import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol;
+import com.yahoo.feedapi.FeedContext;
+import com.yahoo.feedapi.MessagePropertyProcessor;
+import com.yahoo.messagebus.StaticThrottlePolicy;
+import com.yahoo.prelude.templates.DefaultTemplateSet;
+import com.yahoo.search.Query;
+import com.yahoo.search.Result;
+import com.yahoo.search.Searcher;
+import com.yahoo.search.rendering.RendererRegistry;
+import com.yahoo.search.searchchain.Execution;
+import com.yahoo.vdslib.VisitorOrdering;
+import com.yahoo.vespaclient.ClusterList;
+import com.yahoo.vespaclient.config.FeederConfig;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class VisitorSearcherTestCase {
+
+ private DocumentTypeManager docMan = null;
+ private DocumentType docType;
+ DocumentSessionFactory factory;
+
+ @org.junit.Before
+ public void setUp() {
+ docMan = new DocumentTypeManager();
+ docType = new DocumentType("kittens");
+ docType.addHeaderField("name", DataType.STRING);
+ docType.addField("description", DataType.STRING);
+ docType.addField("image", DataType.RAW);
+ docType.addField("fluffiness", DataType.INT);
+ docType.addField("foo", DataType.RAW);
+ docMan.registerDocumentType(docType);
+ factory = new DocumentSessionFactory(docType);
+ }
+
+ public VisitSearcher create() throws Exception {
+ ClusterListConfig.Storage.Builder storageCluster = new ClusterListConfig.Storage.Builder().configid("storage/cluster.foobar").name("foobar");
+ ClusterListConfig clusterListCfg = new ClusterListConfig(new ClusterListConfig.Builder().storage(storageCluster));
+ ClusterList clusterList = new ClusterList();
+ clusterList.configure(clusterListCfg);
+ return new VisitSearcher(new FeedContext(
+ new MessagePropertyProcessor(new FeederConfig(new FeederConfig.Builder().timeout(458).route("riksveg18").retryenabled(true)),
+ new LoadTypeConfig(new LoadTypeConfig.Builder())),
+ factory, docMan, clusterList, new NullFeedMetric()));
+ }
+
+ @Test
+ public void testQueryParameters() throws Exception {
+ VisitSearcher searcher = create();
+ VisitorParameters params = searcher.getVisitorParameters(
+ newQuery("visit?visit.selection=id.user=1234&visit.cluster=foobar" +
+ "&visit.dataHandler=othercluster&visit.fieldSet=[header]&visit.fromTimestamp=112&visit.toTimestamp=224" +
+ "&visit.maxBucketsPerVisitor=2&visit.maxPendingMessagesPerVisitor=7&visit.maxPendingVisitors=14" +
+ "&visit.ordering=ASCENDING&priority=NORMAL_1&tracelevel=7&visit.visitInconsistentBuckets&visit.visitRemoves"), null);
+
+ assertEquals("id.user=1234", params.getDocumentSelection());
+ assertEquals(7, params.getMaxPending());
+ assertEquals(2, params.getMaxBucketsPerVisitor());
+ assertEquals(14, ((StaticThrottlePolicy)params.getThrottlePolicy()).getMaxPendingCount());
+ assertEquals("[Storage:cluster=foobar;clusterconfigid=storage/cluster.foobar]", params.getRoute().toString());
+ assertEquals("othercluster", params.getRemoteDataHandler());
+ assertEquals("[header]", params.fieldSet());
+ assertEquals(112, params.getFromTimestamp());
+ assertEquals(224, params.getToTimestamp());
+ assertEquals(VisitorOrdering.ASCENDING, params.getVisitorOrdering());
+ assertEquals(DocumentProtocol.Priority.NORMAL_1, params.getPriority());
+ assertEquals(7, params.getTraceLevel());
+ assertEquals(true, params.visitInconsistentBuckets());
+ assertEquals(true, params.visitRemoves());
+ }
+
+ @Test
+ public void timestampQueryParametersAreParsedAsLongs() throws Exception {
+ VisitorParameters params = create().getVisitorParameters(
+ newQuery("visit?visit.selection=id.user=1234&" +
+ "visit.fromTimestamp=1419021596000000&" +
+ "visit.toTimestamp=1419021597000000"), null);
+ assertEquals(1419021596000000L, params.getFromTimestamp());
+ assertEquals(1419021597000000L, params.getToTimestamp());
+ }
+
+ @Test
+ public void testQueryParametersDefaults() throws Exception {
+ VisitSearcher searcher = create();
+ VisitorParameters params = searcher.getVisitorParameters(
+ newQuery("visit?visit.selection=id.user=1234&hits=100"), null);
+
+ assertEquals("id.user=1234", params.getDocumentSelection());
+ assertEquals(1, params.getMaxBucketsPerVisitor());
+ assertEquals(1, ((StaticThrottlePolicy)params.getThrottlePolicy()).getMaxPendingCount());
+ assertEquals(1, params.getMaxFirstPassHits());
+ assertEquals(1, params.getMaxTotalHits());
+ assertEquals(32, params.getMaxPending());
+ assertEquals(false, params.visitInconsistentBuckets());
+ }
+
+ @Test
+ public void testWrongCluster() throws Exception {
+ VisitSearcher searcher = create();
+
+ try {
+ searcher.getVisitorParameters(
+ newQuery("visit?visit.selection=id.user=1234&visit.cluster=unknown"), null);
+
+ assertTrue(false);
+ } catch (Exception e) {
+ // e.printStackTrace();
+ }
+ }
+
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNoClusterParamWhenSeveralClusters() throws Exception {
+ DocumentSessionFactory factory = new DocumentSessionFactory(docType);
+ ClusterListConfig.Storage.Builder storageCluster1 = new ClusterListConfig.Storage.Builder().configid("storage/cluster.foo").name("foo");
+ ClusterListConfig.Storage.Builder storageCluster2 = new ClusterListConfig.Storage.Builder().configid("storage/cluster.bar").name("bar");
+ ClusterListConfig clusterListCfg = new ClusterListConfig(new ClusterListConfig.Builder().storage(Arrays.asList(storageCluster1, storageCluster2)));
+ ClusterList clusterList = new ClusterList();
+ clusterList.configure(clusterListCfg);
+ VisitSearcher searcher = new VisitSearcher(new FeedContext(
+ new MessagePropertyProcessor(new FeederConfig(new FeederConfig.Builder().timeout(100).route("whatever").retryenabled(true)),
+ new LoadTypeConfig(new LoadTypeConfig.Builder())),
+ factory, docMan, clusterList, new NullFeedMetric()));
+
+ searcher.getVisitorParameters(
+ newQuery("visit?visit.selection=id.user=1234"), null);
+ }
+
+ @Test
+ public void testSimple() throws Exception {
+ Chain<Searcher> searchChain = new Chain<>(create());
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(newQuery("visit?visit.selection=id.user=1234&hits=100"));
+ assertEquals(1, result.hits().size());
+ assertRendered(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<document documenttype=\"kittens\" documentid=\"userdoc:foo:1234:bar\"/>\n" +
+ "</result>\n", result);
+ }
+
+ private Result invokeVisitRemovesSearchChain() throws Exception {
+ Chain<Searcher> searchChain = new Chain<>(create());
+ return new Execution(searchChain, Execution.Context.createContextStub()).search(
+ newQuery("visit?visit.selection=id.user=1234&hits=100&visit.visitRemoves=true"));
+ }
+
+ @Test
+ public void visitRemovesIncludesRemoveEntriesInResultXml() throws Exception {
+ Result result = invokeVisitRemovesSearchChain();
+ assertEquals(2, result.hits().size());
+ assertRendered(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<document documenttype=\"kittens\" documentid=\"userdoc:foo:1234:bar\"/>\n" +
+ "<remove documentid=\"userdoc:foo:1234:removed\"/>\n" +
+ "</result>\n", result);
+ }
+
+ @Test
+ public void removedDocumentIdsAreXmlEscaped() throws Exception {
+ factory = mock(DocumentSessionFactory.class);
+ when(factory.createVisitorSession(any(VisitorParameters.class))).thenAnswer((p) -> {
+ VisitorParameters params = (VisitorParameters)p.getArguments()[0];
+ DummyVisitorSession session = new DummyVisitorSession(params, docType);
+ session.clearAutoReplyMessages();
+ session.addRemoveReply("userdoc:foo:1234:<rem\"o\"ved&stuff>");
+ return session;
+ });
+ Result result = invokeVisitRemovesSearchChain();
+ assertEquals(1, result.hits().size());
+ assertRendered(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<result>\n" +
+ "<remove documentid=\"userdoc:foo:1234:&lt;rem&quot;o&quot;ved&amp;stuff&gt;\"/>\n" +
+ "</result>\n", result);
+ }
+
+ private Result invokeSearcherWithUserQuery() throws Exception {
+ Chain<Searcher> searchChain = new Chain<>(create());
+ return new Execution(searchChain, Execution.Context.createContextStub())
+ .search(new Query("visit?visit.selection=id.user=1234&hits=100"));
+ }
+
+ @Test
+ public void waitUntilDoneFailureReturnsTimeoutErrorHit() throws Exception {
+ VisitorSession session = mock(VisitorSession.class);
+ when(session.waitUntilDone(anyLong())).thenReturn(false);
+ factory = mock(DocumentSessionFactory.class);
+ when(factory.createVisitorSession(any(VisitorParameters.class))).thenReturn(session);
+
+ Result result = invokeSearcherWithUserQuery();
+ assertNotNull(result.hits().getErrorHit());
+ assertEquals(Error.TIMEOUT.code, result.hits().getErrorHit().errors().iterator().next().getCode());
+ }
+
+ @Test
+ public void testRendererWiring() throws Exception {
+ Chain<Searcher> searchChain = new Chain<>(create());
+ {
+ Query query = newQuery("visit?visit.selection=id.user=1234&hits=100&format=json");
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(query);
+ assertEquals(DefaultTemplateSet.class, result.getTemplating().getTemplates().getClass());
+ }
+ {
+ Query query = newQuery("visit?visit.selection=id.user=1234&hits=100&format=JsonRenderer");
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(query);
+ assertEquals(DefaultTemplateSet.class, result.getTemplating().getTemplates().getClass());
+ }
+ {
+ Query query = newQuery("visit?visit.selection=id.user=1234&hits=100");
+ Result result = new Execution(searchChain, Execution.Context.createContextStub()).search(query);
+ assertEquals(DocumentXMLTemplate.class, result.getTemplating().getTemplates().getClass());
+ }
+ }
+
+ public static void assertRendered(String expected, Result result) throws Exception {
+ assertEquals(expected, ResultRenderingUtil.getRendered(result));
+ }
+
+ private Query newQuery(String queryString) {
+ return new Query(HttpRequest.createTestRequest(queryString, com.yahoo.jdisc.http.HttpRequest.Method.GET));
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/DummyMetric.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/DummyMetric.java
new file mode 100644
index 00000000000..d19560ee553
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/DummyMetric.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.jdisc.Metric;
+
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.20
+ */
+class DummyMetric implements Metric {
+
+ @Override
+ public void set(String key, Number val, Context ctx) {
+ }
+
+ @Override
+ public void add(String key, Number val, Context ctx) {
+ }
+
+ @Override
+ public Context createContext(Map<String, ?> properties) {
+ return DummyContext.INSTANCE;
+ }
+
+ private static class DummyContext implements Context {
+ private static final DummyContext INSTANCE = new DummyContext();
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/FeedHandlerCompressionTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/FeedHandlerCompressionTest.java
new file mode 100644
index 00000000000..2cabed13a7b
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/FeedHandlerCompressionTest.java
@@ -0,0 +1,70 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPOutputStream;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class FeedHandlerCompressionTest {
+
+ public static byte[] compress(final String dataToBrCompressed) throws IOException {
+ final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(dataToBrCompressed.length());
+ final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
+ gzipOutputStream.write(dataToBrCompressed.getBytes());
+ gzipOutputStream.close();
+ byte[] compressedBytes = byteArrayOutputStream.toByteArray();
+ byteArrayOutputStream.close();
+ return compressedBytes;
+ }
+
+ @Test
+ public void testUnzipStreamIfNeeded() throws Exception {
+ final String testData = "foo bar";
+ InputStream inputStream = new ByteArrayInputStream(compress(testData));
+ HttpRequest httpRequest = mock(HttpRequest.class);
+ when(httpRequest.getHeader("content-encoding")).thenReturn("gzip");
+ InputStream decompressedStream = FeedHandler.unzipStreamIfNeeded(inputStream, httpRequest);
+ final StringBuilder processedInput = new StringBuilder();
+ while (true) {
+ int readValue = decompressedStream.read();
+ if (readValue < 0) {
+ break;
+ }
+ processedInput.append((char)readValue);
+ }
+ assertThat(processedInput.toString(), is(testData));
+ }
+
+ /**
+ * Test by setting encoding, but not compressing data.
+ * @throws Exception
+ */
+ @Test
+ public void testUnzipFails() throws Exception {
+ final String testData = "foo bar";
+ InputStream inputStream = new ByteArrayInputStream(testData.getBytes());
+ HttpRequest httpRequest = mock(HttpRequest.class);
+ when(httpRequest.getHeader("Content-Encoding")).thenReturn("gzip");
+ InputStream decompressedStream = FeedHandler.unzipStreamIfNeeded(inputStream, httpRequest);
+ final StringBuilder processedInput = new StringBuilder();
+ while (true) {
+ int readValue = decompressedStream.read();
+ if (readValue < 0) {
+ break;
+ }
+ processedInput.append((char)readValue);
+ }
+ assertThat(processedInput.toString(), is(testData));
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MetaStream.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MetaStream.java
new file mode 100644
index 00000000000..ae96a25fad3
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MetaStream.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.text.Utf8;
+
+import java.io.ByteArrayInputStream;
+
+/**
+ * Stream with extra data outside the actual input stream.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public final class MetaStream extends ByteArrayInputStream {
+
+ private byte[] operations;
+ int i;
+
+ public MetaStream(byte[] buf) {
+ super(createPayload(buf));
+ this.operations = buf;
+ i = 0;
+ }
+
+ private static final byte[] createPayload(byte[] buf) {
+ StringBuilder bu = new StringBuilder();
+ for (byte b : buf) {
+ bu.append("id:banana:banana::doc1 0\n");
+ }
+ return Utf8.toBytes(bu.toString());
+ }
+
+ public byte getNextOperation() {
+ if (i >= operations.length) {
+ return 0;
+ }
+ return operations[i++];
+ }
+
+} \ No newline at end of file
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MockNetwork.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MockNetwork.java
new file mode 100644
index 00000000000..1208ce334d9
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MockNetwork.java
@@ -0,0 +1,69 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import java.util.List;
+
+import com.yahoo.jrt.slobrok.api.IMirror;
+import com.yahoo.messagebus.Message;
+import com.yahoo.messagebus.network.Network;
+import com.yahoo.messagebus.network.NetworkOwner;
+import com.yahoo.messagebus.routing.RoutingNode;
+
+/**
+ * NOP-network.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+class MockNetwork implements Network {
+
+ @Override
+ public boolean waitUntilReady(double seconds) {
+ return true;
+ }
+
+ @Override
+ public void attach(NetworkOwner owner) {
+ }
+
+ @Override
+ public void registerSession(String session) {
+ }
+
+ @Override
+ public void unregisterSession(String session) {
+
+ }
+
+ @Override
+ public boolean allocServiceAddress(RoutingNode recipient) {
+ return true;
+ }
+
+ @Override
+ public void freeServiceAddress(RoutingNode recipient) {
+
+ }
+
+ @Override
+ public void send(Message msg, List<RoutingNode> recipients) {
+ }
+
+ @Override
+ public void sync() {
+ }
+
+ @Override
+ public void shutdown() {
+ }
+
+ @Override
+ public String getConnectionSpec() {
+ return null;
+ }
+
+ @Override
+ public IMirror getMirror() {
+ return null;
+ }
+
+} \ No newline at end of file
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MockReply.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MockReply.java
new file mode 100644
index 00000000000..298c925d032
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/MockReply.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.messagebus.Reply;
+import com.yahoo.text.Utf8String;
+
+/**
+ * Minimal reply simulator.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+class MockReply extends Reply {
+
+ Object context;
+
+ public MockReply(Object context) {
+ this.context = context;
+ }
+
+ @Override
+ public Utf8String getProtocol() {
+ return null;
+ }
+
+ @Override
+ public int getType() {
+ return 0;
+ }
+
+ @Override
+ public Object getContext() {
+ return context;
+ }
+
+} \ No newline at end of file
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2ErrorsInResultTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2ErrorsInResultTestCase.java
new file mode 100644
index 00000000000..2e88c440b12
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2ErrorsInResultTestCase.java
@@ -0,0 +1,236 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.messagebus.SessionCache;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.jdisc.ReferencedResource;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.messagebus.*;
+import com.yahoo.messagebus.Error;
+import com.yahoo.messagebus.shared.SharedMessageBus;
+import com.yahoo.messagebus.shared.SharedSourceSession;
+import com.yahoo.text.Utf8;
+import com.yahoo.text.Utf8String;
+import com.yahoo.vespa.http.client.core.Headers;
+import com.yahoo.vespa.http.client.core.OperationStatus;
+import com.yahoo.vespaxmlparser.MockFeedReaderFactory;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Check FeedHandler APIs.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class V2ErrorsInResultTestCase {
+
+ LessConfiguredHandler handler;
+ ExecutorService workers;
+
+ @Before
+ public void setUp() throws Exception {
+ workers = Executors.newCachedThreadPool();
+ handler = new LessConfiguredHandler(workers);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ handler.destroy();
+ workers.shutdown();
+ }
+
+ private static class LessConfiguredHandler extends FeedHandler {
+
+ public LessConfiguredHandler(Executor executor) throws Exception {
+ super(executor, null, null, new DummyMetric(), AccessLog.voidAccessLog(), null);
+ }
+
+
+ @Override
+ protected Feeder createFeeder(HttpRequest request, InputStream requestInputStream,
+ BlockingQueue<OperationStatus> operations, String clientId,
+ boolean sessionIdWasGeneratedJustNow, int protocolVersion)
+ throws Exception {
+ return new LessConfiguredFeeder(requestInputStream, operations,
+ popClient(clientId), new FeederSettings(request), clientId, sessionIdWasGeneratedJustNow,
+ sourceSessionParams(request), null, this, this.feedReplyHandler, "");
+ }
+
+ @Override
+ protected DocumentTypeManager createDocumentManager(
+ DocumentmanagerConfig documentManagerConfig) {
+ return null;
+ }
+ }
+
+ private static class MockSharedSession extends SharedSourceSession {
+ int count;
+
+ public MockSharedSession(SourceSessionParams params) {
+ super(new SharedMessageBus(new MessageBus(new MockNetwork(),
+ new MessageBusParams())), params);
+ count = 0;
+ }
+
+ @Override
+ public Result sendMessageBlocking(Message msg) throws InterruptedException {
+ return sendMessage(msg);
+ }
+
+ @Override
+ public Result sendMessage(Message msg) {
+ Result r;
+ ReplyHandler handler = msg.popHandler();
+
+ switch (count++) {
+ case 0:
+ r = new Result(ErrorCode.FATAL_ERROR,
+ "boom");
+ break;
+ case 1:
+ r = new Result(ErrorCode.TRANSIENT_ERROR,
+ "transient boom");
+ break;
+ case 2:
+ final FailedReply reply = new FailedReply(msg.getContext());
+ reply.addError(new Error(
+ ErrorCode.FATAL_ERROR,
+ "bad mojo, dude"));
+ handler.handleReply(reply);
+ r = Result.ACCEPTED;
+ break;
+ default:
+ handler.handleReply(new MockReply(msg.getContext()));
+ r = Result.ACCEPTED;
+ }
+ return r;
+ }
+
+ }
+
+ private static class FailedReply extends Reply {
+ Object context;
+
+ public FailedReply(Object context) {
+ this.context = context;
+ }
+
+ @Override
+ public Utf8String getProtocol() {
+ return null;
+ }
+
+ @Override
+ public int getType() {
+ return 0;
+ }
+
+ @Override
+ public Object getContext() {
+ return context;
+ }
+ }
+
+ private static class LessConfiguredFeeder extends Feeder {
+
+ public LessConfiguredFeeder(InputStream stream,
+ BlockingQueue<OperationStatus> operations,
+ ClientState storedState, FeederSettings settings,
+ String clientId, boolean sessionIdWasGeneratedJustNow, SourceSessionParams sessionParams,
+ SessionCache sessionCache, FeedHandler handler, ReplyHandler feedReplyHandler,
+ String localHostname) throws Exception {
+ super(stream, new MockFeedReaderFactory(), null, operations, storedState, settings, clientId, sessionIdWasGeneratedJustNow,
+ sessionParams, sessionCache, handler, new DummyMetric(), feedReplyHandler, localHostname);
+ }
+
+ protected ReferencedResource<SharedSourceSession> retainSession(
+ SourceSessionParams sessionParams, SessionCache sessionCache) {
+ final SharedSourceSession session = new MockSharedSession(sessionParams);
+ return new ReferencedResource<>(session, References.fromResource(session));
+ }
+ }
+
+ @Test
+ public final void test() throws IOException {
+ String sessionId;
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ sessionId = r.headers().getFirst(Headers.SESSION_ID);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 ERROR boom \n",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 TRANSIENT_ERROR transient{20}boom \n",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 ERROR bad{20}mojo,{20}dude \n",
+ Utf8.toString(out.toByteArray()));
+ }
+
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2ExternalFeedTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2ExternalFeedTestCase.java
new file mode 100644
index 00000000000..74ed844d69f
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2ExternalFeedTestCase.java
@@ -0,0 +1,535 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.messagebus.SessionCache;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.documentapi.messagebus.protocol.PutDocumentMessage;
+import com.yahoo.jdisc.ReferencedResource;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.messagebus.Message;
+import com.yahoo.messagebus.MessageBus;
+import com.yahoo.messagebus.MessageBusParams;
+import com.yahoo.messagebus.ReplyHandler;
+import com.yahoo.messagebus.Result;
+import com.yahoo.messagebus.SourceSessionParams;
+import com.yahoo.messagebus.network.Network;
+import com.yahoo.messagebus.shared.SharedMessageBus;
+import com.yahoo.messagebus.shared.SharedSourceSession;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.http.client.config.FeedParams.DataFormat;
+import com.yahoo.vespa.http.client.core.Headers;
+import com.yahoo.vespa.http.client.core.OperationStatus;
+import com.yahoo.vespaxmlparser.MockFeedReaderFactory;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Check FeedHandler APIs.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class V2ExternalFeedTestCase {
+
+ LessConfiguredHandler handler;
+ ExecutorService workers;
+ Level logLevel;
+ Logger logger;
+ boolean initUseParentHandlers;
+ LogBuffer logChecker;
+
+ @Before
+ public void setUp() throws Exception {
+ workers = Executors.newCachedThreadPool();
+ handler = new LessConfiguredHandler(workers);
+ logger = Logger.getLogger(Feeder.class.getName());
+ logLevel = logger.getLevel();
+ logger.setLevel(Level.ALL);
+ initUseParentHandlers = logger.getUseParentHandlers();
+ logChecker = new LogBuffer();
+ logger.setUseParentHandlers(false);
+ logger.addHandler(logChecker);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ handler.destroy();
+ workers.shutdown();
+ logger.setLevel(logLevel);
+ logger.removeHandler(logChecker);
+ logger.setUseParentHandlers(initUseParentHandlers);
+ }
+
+ private static class LogBuffer extends Handler {
+ public final BlockingQueue<LogRecord> records = new LinkedBlockingQueue<>();
+
+ @Override
+ public void publish(LogRecord record) {
+ try {
+ records.put(record);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void flush() {
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ }
+ }
+
+ private static class LessConfiguredHandler extends FeedHandler {
+ volatile DataFormat lastFormatSeen;
+
+ public LessConfiguredHandler(Executor executor) throws Exception {
+ super(executor, null, null, new DummyMetric(), AccessLog.voidAccessLog(), null);
+ }
+
+ @Override
+ protected Feeder createFeeder(HttpRequest request,
+ InputStream requestInputStream,
+ BlockingQueue<OperationStatus> operations,
+ String clientId,
+ boolean sessionIdWasGeneratedJustNow, int protocolVersion)
+ throws Exception {
+ LessConfiguredFeeder f = new LessConfiguredFeeder(requestInputStream, operations,
+ popClient(clientId), new FeederSettings(request), clientId, sessionIdWasGeneratedJustNow,
+ sourceSessionParams(request), null, this, this.feedReplyHandler, "ourHostname");
+ lastFormatSeen = f.settings.dataFormat;
+ return f;
+ }
+
+ @Override
+ protected DocumentTypeManager createDocumentManager(
+ DocumentmanagerConfig documentManagerConfig) {
+ return null;
+ }
+ }
+
+ private static class MockSharedSession extends SharedSourceSession {
+
+ public MockSharedSession(SourceSessionParams params) {
+ super(new SharedMessageBus(new MessageBus(new MockNetwork(),
+ new MessageBusParams())), params);
+ }
+
+ @Override
+ public Result sendMessageBlocking(Message msg) throws InterruptedException {
+ return sendMessage(msg);
+ }
+
+ @Override
+ public Result sendMessage(Message msg) {
+ ReplyHandler handler = msg.popHandler();
+ MockReply mockReply = new MockReply(msg.getContext());
+ if (msg instanceof Feeder.FeedErrorMessage) {
+ mockReply.addError(new com.yahoo.messagebus.Error(123, "Could not feed this"));
+ }
+ if (msg instanceof PutDocumentMessage) {
+ assert(msg.getTrace().getLevel() == 4);
+ assert(((PutDocumentMessage) msg).getPriority().name().equals("LOWEST"));
+ }
+ handler.handleReply(mockReply);
+ return Result.ACCEPTED;
+ }
+
+ }
+
+ private static class LessConfiguredFeeder extends Feeder {
+ public LessConfiguredFeeder(InputStream stream,
+ BlockingQueue<OperationStatus> operations,
+ ClientState storedState, FeederSettings settings,
+ String clientId, boolean sessionIdWasGeneratedJustNow, SourceSessionParams sessionParams,
+ SessionCache sessionCache, FeedHandler handler, ReplyHandler feedReplyHandler,
+ String localHostname) throws Exception {
+ super(stream, new MockFeedReaderFactory(), null, operations, storedState, settings, clientId, sessionIdWasGeneratedJustNow,
+ sessionParams, sessionCache, handler, new DummyMetric(), feedReplyHandler, localHostname);
+ }
+
+ protected ReferencedResource<SharedSourceSession> retainSession(
+ SourceSessionParams sessionParams, SessionCache sessionCache) {
+ final SharedSourceSession session = new MockSharedSession(sessionParams);
+ return new ReferencedResource<>(session, References.fromResource(session));
+ }
+ }
+
+ @Test
+ public final void test() throws IOException, InterruptedException {
+ String sessionId;
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ sessionId = r.headers().getFirst(Headers.SESSION_ID);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[]{1, 3, 2});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.TIMEOUT, "1000000000");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST");
+ nalle.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n",
+ Utf8.toString(out.toByteArray()));
+ assertEquals("text/plain", r.getContentType());
+ assertEquals(StandardCharsets.US_ASCII.name(), r.getCharacterEncoding());
+ assertEquals(7, logChecker.records.size());
+ String actualHandshake = logChecker.records.take().getMessage();
+ assertThat(actualHandshake, actualHandshake.matches("Handshake completed for client (-?)(.+?)-#(.*?)\\."), is(true));
+ assertEquals("Successfully deserialized document id: id:banana:banana::doc1",
+ logChecker.records.take().getMessage());
+ assertEquals("Sent message successfully, document id: id:banana:banana::doc1",
+ logChecker.records.take().getMessage());
+ }
+
+ //test session ID without #, i.e. something fishy related to VIPs is going on
+ sessionId = "something";
+
+ {
+ InputStream in = new MetaStream(new byte[]{1, 3, 2});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.TIMEOUT, "1000000000");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ nalle.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST");
+ nalle.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4");
+ nalle.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "2");
+
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ String expectedErrorMsg = "Got request from client with id 'something', but found no session for this client. " +
+ "Most probably this server is in VIP rotation, and a client session was rotated from one " +
+ "server to another. This must not happen. Configure VIP with persistence=enabled, or " +
+ "(preferably) do not use a VIP at all.";
+ assertEquals(expectedErrorMsg,
+ Utf8.toString(out.toByteArray()));
+ assertEquals("text/plain", r.getContentType());
+ assertEquals(StandardCharsets.UTF_8.name(), r.getCharacterEncoding());
+ }
+
+ //test session ID with trailing # but no hostname
+ sessionId = "something#";
+
+ {
+ InputStream in = new MetaStream(new byte[]{1, 3, 2});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.TIMEOUT, "1000000000");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ nalle.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST");
+ nalle.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ String expectedErrorMsg = "Got request from client with id 'something#', but found no session for this " +
+ "client. Possible session timeout due to inactivity, server restart or " +
+ "reconfig, or bad VIP usage.";
+ assertEquals(expectedErrorMsg,
+ Utf8.toString(out.toByteArray()));
+ assertEquals("text/plain", r.getContentType());
+ assertEquals(StandardCharsets.UTF_8.name(), r.getCharacterEncoding());
+ }
+
+ //test session ID with trailing # and some unknown hostname at the end
+ sessionId = "something#thisHostnameDoesNotExistAnywhere";
+
+ {
+ InputStream in = new MetaStream(new byte[]{1, 3, 2});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.TIMEOUT, "1000000000");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ nalle.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST");
+ nalle.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ String expectedErrorMsg = "Got request from client with id 'something#thisHostnameDoesNotExistAnywhere', " +
+ "but found no session for this client. Session was originally established " +
+ "towards host thisHostnameDoesNotExistAnywhere, but our hostname is " +
+ "ourHostname. Most probably this server is in VIP rotation, and a session " +
+ "was rotated from one server to another. This should not happen. Configure VIP " +
+ "with persistence=enabled, or (preferably) do not use a VIP at all.";
+ assertEquals(expectedErrorMsg,
+ Utf8.toString(out.toByteArray()));
+ assertEquals("text/plain", r.getContentType());
+ assertEquals(StandardCharsets.UTF_8.name(), r.getCharacterEncoding());
+ }
+
+ //test session ID with trailing # and some unknown hostname at the end
+ sessionId = "something#ourHostname";
+
+ {
+ InputStream in = new MetaStream(new byte[]{1, 3, 2});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.TIMEOUT, "1000000000");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST");
+ nalle.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 OK Document{20}processed. \n" +
+ "id:banana:banana::doc1 OK Document{20}processed. \n" +
+ "id:banana:banana::doc1 OK Document{20}processed. \n",
+ Utf8.toString(out.toByteArray()));
+ assertEquals("text/plain", r.getContentType());
+ assertEquals(StandardCharsets.US_ASCII.name(), r.getCharacterEncoding());
+ Thread.sleep(1000);
+ }
+ }
+
+ @Test
+ public final void testFailedReading() throws IOException {
+ String sessionId;
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ sessionId = r.headers().getFirst(Headers.SESSION_ID);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[] { 4 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 ERROR Could{20}not{20}feed{20}this \n",
+ Utf8.toString(out.toByteArray()));
+ }
+ }
+
+ @Test
+ public final void testCleaningDoesNotBlowUp() throws IOException {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ handler.forceRunCleanClients();
+ }
+
+ @Test
+ public final void testMockNetworkDoesNotBlowUp() {
+ Network n = new MockNetwork();
+ n.registerSession(null);
+ n.unregisterSession(null);
+ assertTrue(n.allocServiceAddress(null));
+ n.freeServiceAddress(null);
+ n.send(null, null);
+ assertNull(n.getConnectionSpec());
+ assertNull(n.getMirror());
+ }
+
+ @Test
+ public final void testMockReplyDoesNotBlowUp() {
+ MockReply r = new MockReply(null);
+ assertNull(r.getProtocol());
+ assertEquals(0, r.getType());
+ assertFalse(r.hasFatalErrors());
+ }
+
+ @Test
+ public final void testFlush() throws IOException {
+ String sessionId;
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ sessionId = r.headers().getFirst(Headers.SESSION_ID);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[] { 1, 1, 1, 1, 1, 1, 1});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST");
+ nalle.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n",
+ Utf8.toString(out.toByteArray()));
+ }
+ }
+
+ @Test
+ public final void testIllegalVersion() throws IOException {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers()
+ .add(Headers.VERSION, Integer.toString(Integer.MAX_VALUE));
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals(Headers.HTTP_NOT_ACCEPTABLE, r.getStatus());
+ }
+
+ @Test
+ public final void testSettings() {
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ nalle.getJDiscRequest().headers().add(Headers.ROUTE, "bamse brakar");
+ nalle.getJDiscRequest().headers().add(Headers.DENY_IF_BUSY, "true");
+ FeederSettings settings = new FeederSettings(nalle);
+ assertEquals(false, settings.drain);
+ assertEquals(2, settings.route.getNumHops());
+ assertEquals(true, settings.denyIfBusy);
+ }
+
+ @Test
+ public final void testJsonInputFormat() throws IOException, InterruptedException {
+ String sessionId;
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ sessionId = r.headers().getFirst(Headers.SESSION_ID);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[]{1, 3, 2});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.TIMEOUT, "1000000000");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DATA_FORMAT, DataFormat.JSON_UTF8.name());
+ nalle.getJDiscRequest().headers().add(Headers.PRIORITY, "LOWEST");
+ nalle.getJDiscRequest().headers().add(Headers.TRACE_LEVEL, "4");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n"
+ + "id:banana:banana::doc1 OK Document{20}processed. \n",
+ Utf8.toString(out.toByteArray()));
+ assertEquals("text/plain", r.getContentType());
+ assertEquals(StandardCharsets.US_ASCII.name(), r.getCharacterEncoding());
+ assertEquals(7, logChecker.records.size());
+ String actualHandshake = logChecker.records.take().getMessage();
+ assertThat(actualHandshake, actualHandshake.matches("Handshake completed for client (-?)(.+?)-#(.*?)\\."), is(true));
+ assertEquals("Successfully deserialized document id: id:banana:banana::doc1",
+ logChecker.records.take().getMessage());
+ assertEquals("Sent message successfully, document id: id:banana:banana::doc1",
+ logChecker.records.take().getMessage());
+ assertSame(DataFormat.JSON_UTF8, handler.lastFormatSeen);
+ }
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2FailingMessagebusTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2FailingMessagebusTestCase.java
new file mode 100644
index 00000000000..4e6ca17abef
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2FailingMessagebusTestCase.java
@@ -0,0 +1,224 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.messagebus.SessionCache;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.jdisc.ReferencedResource;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.messagebus.*;
+import com.yahoo.messagebus.shared.SharedMessageBus;
+import com.yahoo.messagebus.shared.SharedSourceSession;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.http.client.core.Headers;
+import com.yahoo.vespa.http.client.core.OperationStatus;
+import com.yahoo.vespaxmlparser.MockFeedReaderFactory;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Check FeedHandler APIs.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class V2FailingMessagebusTestCase {
+
+ LessConfiguredHandler handler;
+ ExecutorService workers;
+ int mbus;
+
+ @Before
+ public void setUp() throws Exception {
+ workers = Executors.newCachedThreadPool();
+ handler = new LessConfiguredHandler(workers);
+ mbus = 0;
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ handler.destroy();
+ workers.shutdown();
+ mbus = 0;
+ }
+
+ private class LessConfiguredHandler extends FeedHandler {
+
+ public LessConfiguredHandler(Executor executor) throws Exception {
+ super(executor, null, null, new DummyMetric(), AccessLog.voidAccessLog(), null);
+ }
+
+ @Override
+ protected Feeder createFeeder(HttpRequest request,
+ InputStream requestInputStream,
+ BlockingQueue<OperationStatus> operations,
+ String clientId,
+ boolean sessionIdWasGeneratedJustNow, int protocolVersion) throws Exception {
+ return new LessConfiguredFeeder(requestInputStream, operations,
+ popClient(clientId), new FeederSettings(request), clientId, sessionIdWasGeneratedJustNow,
+ sourceSessionParams(request), null, this, this.feedReplyHandler, "");
+ }
+
+ @Override
+ protected DocumentTypeManager createDocumentManager(
+ DocumentmanagerConfig documentManagerConfig) {
+ return null;
+ }
+ }
+
+ private class MockSharedSession extends SharedSourceSession {
+
+ public MockSharedSession(SourceSessionParams params) {
+ super(new SharedMessageBus(new MessageBus(new MockNetwork(),
+ new MessageBusParams())), params);
+ }
+
+ @Override
+ public Result sendMessageBlocking(Message msg) throws InterruptedException {
+ return sendMessage(msg);
+ }
+
+ @Override
+ public Result sendMessage(Message msg) {
+ ReplyHandler handler = msg.popHandler();
+
+ switch (mbus) {
+ case 0:
+ throw new RuntimeException("boom");
+ case 1:
+ Result r = new Result(ErrorCode.SEND_QUEUE_FULL, "tralala");
+ mbus = 2;
+ return r;
+ case 2:
+ handler.handleReply(new MockReply(msg.getContext()));
+ return Result.ACCEPTED;
+ default:
+ throw new IllegalStateException("WTF?!");
+ }
+ }
+ }
+
+ private class LessConfiguredFeeder extends Feeder {
+
+ public LessConfiguredFeeder(InputStream inputStream,
+ BlockingQueue<OperationStatus> operations,
+ ClientState storedState, FeederSettings settings,
+ String clientId, boolean sessionIdWasGeneratedJustNow, SourceSessionParams sessionParams,
+ SessionCache sessionCache, FeedHandler handler, ReplyHandler feedReplyHandler,
+ String localHostname) throws Exception {
+ super(inputStream, new MockFeedReaderFactory(), null, operations, storedState, settings, clientId, sessionIdWasGeneratedJustNow,
+ sessionParams, sessionCache, handler, new DummyMetric(), feedReplyHandler, localHostname);
+ }
+
+ protected ReferencedResource<SharedSourceSession> retainSession(
+ SourceSessionParams sessionParams, SessionCache sessionCache) {
+ final SharedSourceSession session = new MockSharedSession(sessionParams);
+ return new ReferencedResource<>(session, References.fromResource(session));
+ }
+ }
+
+ @Test
+ public final void testFailingMbus() throws IOException {
+ String sessionId;
+ {
+ InputStream in = new MetaStream(new byte[]{1});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ sessionId = r.headers().getFirst(Headers.SESSION_ID);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[]{1});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 ERROR boom \n",
+ Utf8.toString(out.toByteArray()));
+ }
+ }
+
+ @Test
+ public final void testBusyMbus() throws IOException {
+ String sessionId;
+ {
+ InputStream in = new MetaStream(new byte[]{1});
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ mbus = 2;
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ sessionId = r.headers().getFirst(Headers.SESSION_ID);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ mbus = 1;
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ nalle.getJDiscRequest().headers()
+ .add(Headers.DENY_IF_BUSY, "false");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 OK Document{20}processed. \n",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ mbus = 1;
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ nalle.getJDiscRequest().headers().add(Headers.DENY_IF_BUSY, "true");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ assertEquals("id:banana:banana::doc1 TRANSIENT_ERROR tralala \n",
+ Utf8.toString(out.toByteArray()));
+ }
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2NoXmlReaderTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2NoXmlReaderTestCase.java
new file mode 100644
index 00000000000..1b2d855bdf2
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V2NoXmlReaderTestCase.java
@@ -0,0 +1,162 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.messagebus.SessionCache;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.config.DocumentmanagerConfig;
+import com.yahoo.jdisc.ReferencedResource;
+import com.yahoo.jdisc.References;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.messagebus.*;
+import com.yahoo.messagebus.Error;
+import com.yahoo.messagebus.shared.SharedMessageBus;
+import com.yahoo.messagebus.shared.SharedSourceSession;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.http.client.core.Headers;
+import com.yahoo.vespa.http.client.core.OperationStatus;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Check FeedHandler APIs.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class V2NoXmlReaderTestCase {
+
+ LessConfiguredHandler handler;
+ ExecutorService workers;
+
+ @Before
+ public void setUp() throws Exception {
+ workers = Executors.newCachedThreadPool();
+ handler = new LessConfiguredHandler(workers);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ handler.destroy();
+ workers.shutdown();
+ }
+
+ private static class LessConfiguredHandler extends FeedHandler {
+
+ public LessConfiguredHandler(Executor executor) throws Exception {
+ super(executor, null, null, new DummyMetric(), AccessLog.voidAccessLog(), null);
+ }
+
+
+ @Override
+ protected Feeder createFeeder(HttpRequest request, InputStream requestInputStream,
+ BlockingQueue<OperationStatus> operations, String clientId,
+ boolean sessionIdWasGeneratedJustNow, int protocolVersion)
+ throws Exception {
+ return new LessConfiguredFeeder(requestInputStream, operations,
+ popClient(clientId), new FeederSettings(request), clientId, sessionIdWasGeneratedJustNow,
+ sourceSessionParams(request), null, this, this.feedReplyHandler, "");
+ }
+
+ @Override
+ protected DocumentTypeManager createDocumentManager(
+ DocumentmanagerConfig documentManagerConfig) {
+ return null;
+ }
+ }
+
+ private static class MockSharedSession extends SharedSourceSession {
+
+ public MockSharedSession(SourceSessionParams params) {
+ super(new SharedMessageBus(new MessageBus(new MockNetwork(),
+ new MessageBusParams())), params);
+ }
+
+ @Override
+ public Result sendMessageBlocking(Message msg) throws InterruptedException {
+ return sendMessage(msg);
+ }
+
+ @Override
+ public Result sendMessage(Message msg) {
+ ReplyHandler handler = msg.popHandler();
+ MockReply mockReply = new MockReply(msg.getContext());
+ if (msg instanceof Feeder.FeedErrorMessage) {
+ mockReply.addError(new Error(123, "Could not feed this"));
+ }
+ handler.handleReply(mockReply);
+ return Result.ACCEPTED;
+ }
+
+ }
+
+ private static class LessConfiguredFeeder extends Feeder {
+
+ public LessConfiguredFeeder(InputStream inputStream,
+ BlockingQueue<OperationStatus> operations,
+ ClientState storedState, FeederSettings settings,
+ String clientId, boolean sessionIdWasGeneratedJustNow, SourceSessionParams sessionParams,
+ SessionCache sessionCache, FeedHandler handler, ReplyHandler feedReplyHandler,
+ String localHostname) throws Exception {
+ super(inputStream, null, null, operations, storedState, settings, clientId, sessionIdWasGeneratedJustNow,
+ sessionParams, sessionCache, handler, new DummyMetric(), feedReplyHandler, localHostname);
+ }
+
+ protected ReferencedResource<SharedSourceSession> retainSession(
+ SourceSessionParams sessionParams, SessionCache sessionCache) {
+ final SharedSourceSession session = new MockSharedSession(sessionParams);
+ return new ReferencedResource<>(session, References.fromResource(session));
+ }
+ }
+
+ @Test
+ public final void test() throws IOException {
+ String sessionId;
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest
+ .createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "false");
+ HttpResponse r = handler.handle(nalle);
+ sessionId = r.headers().getFirst(Headers.SESSION_ID);
+ r.render(out);
+ assertEquals("",
+ Utf8.toString(out.toByteArray()));
+ }
+ {
+ InputStream in = new MetaStream(new byte[] { 1 });
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HttpRequest nalle = HttpRequest.createTestRequest(
+ "http://test4-steinar:19020/reserved-for-internal-use/feedapi",
+ Method.POST, in);
+ nalle.getJDiscRequest().headers().add(Headers.VERSION, "2");
+ nalle.getJDiscRequest().headers().add(Headers.SESSION_ID, sessionId);
+ nalle.getJDiscRequest().headers().add(Headers.DRAIN, "true");
+ HttpResponse r = handler.handle(nalle);
+ r.render(out);
+ //This is different from v1. If we cannot parse XML, we will still get response code 200, but with a sensible
+ //error message in the response.
+ assertEquals(200, r.getStatus());
+ assertEquals("id:banana:banana::doc1 ERROR Could{20}not{20}feed{20}this \n",
+ Utf8.toString(out.toByteArray()));
+ }
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V3CongestionTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V3CongestionTestCase.java
new file mode 100644
index 00000000000..73095a3efe9
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/V3CongestionTestCase.java
@@ -0,0 +1,160 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.jdisc.ReferencedResource;
+import com.yahoo.jdisc.References;
+import com.yahoo.messagebus.ErrorCode;
+import com.yahoo.messagebus.Message;
+import com.yahoo.messagebus.MessageBus;
+import com.yahoo.messagebus.MessageBusParams;
+import com.yahoo.messagebus.ReplyHandler;
+import com.yahoo.messagebus.Result;
+import com.yahoo.messagebus.SourceSessionParams;
+import com.yahoo.messagebus.shared.SharedMessageBus;
+import com.yahoo.messagebus.shared.SharedSourceSession;
+import com.yahoo.vespa.http.client.core.Headers;
+import com.yahoo.vespaxmlparser.MockFeedReaderFactory;
+import com.yahoo.vespaxmlparser.VespaXMLFeedReader;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertTrue;
+
+
+public class V3CongestionTestCase {
+ AtomicInteger threadsAvail = new AtomicInteger(10);
+ AtomicInteger requests = new AtomicInteger(0);
+
+
+ static class ClientFeederWithMocks extends ClientFeederV3 {
+
+ private final DocumentOperationMessageV3 docOp;
+
+ ClientFeederWithMocks(ReferencedResource<SharedSourceSession> sourceSession, FeedReaderFactory feedReaderFactory, DocumentTypeManager docTypeManager, String clientId, Metric metric, ReplyHandler feedReplyHandler, AtomicInteger threadsAvailableForFeeding) {
+ super(sourceSession, feedReaderFactory, docTypeManager, clientId, metric, feedReplyHandler, threadsAvailableForFeeding);
+ // The operation to return from the client feeder.
+ VespaXMLFeedReader.Operation op = new VespaXMLFeedReader.Operation();
+ docOp = DocumentOperationMessageV3.newRemoveMessage(op, "operation id");
+
+ }
+
+ @Override
+ protected DocumentOperationMessageV3 getNextMessage(
+ String operationId, InputStream requestInputStream, FeederSettings settings) throws Exception {
+ while (true) {
+ int data = requestInputStream.read();
+ if (data == -1 || data == (char)'\n') {
+ break;
+ }
+ }
+ return docOp;
+ }
+ }
+
+ final static int NUMBER_OF_QUEUE_FULL_RESPONSES = 5;
+
+ ClientFeederV3 clientFeederV3;
+ HttpRequest request;
+
+ @Before
+ public void setup() {
+ // Set up a request to be used from the tests.
+ InputStream in = new MetaStream(new byte[] { 1 });
+ request = HttpRequest
+ .createTestRequest(
+ "http://foo.bar:19020/reserved-for-internal-use/feedapi",
+ com.yahoo.jdisc.http.HttpRequest.Method.POST, in);
+ request.getJDiscRequest().headers().add(Headers.VERSION, "3");
+ request.getJDiscRequest().headers().add(Headers.CLIENT_ID, "clientId");
+
+
+ // Create a mock that does not parse the message, only reads the rest of the line. Makes it easier
+ // to write tests. It uses a mock for message bus.
+ clientFeederV3 = new ClientFeederWithMocks(
+ retainMockSession(new SourceSessionParams(), requests),
+ new MockFeedReaderFactory(),
+ null /*DocTypeManager*/,
+ "clientID",
+ null/*metric*/,
+ new FeedReplyReader(null/*metric*/),
+ threadsAvail);
+ }
+
+ // A mock for message bus that can simulate blocking requests.
+ private static class MockSharedSession extends SharedSourceSession {
+ boolean queuFull = true;
+ AtomicInteger requests;
+
+ public MockSharedSession(SourceSessionParams params, AtomicInteger requests) {
+ super(new SharedMessageBus(new MessageBus(new MockNetwork(),
+ new MessageBusParams())), params);
+ this.requests = requests;
+ }
+
+ @Override
+ public Result sendMessageBlocking(Message msg) throws InterruptedException {
+ return sendMessage(msg);
+ }
+
+ @Override
+ public Result sendMessage(Message msg) {
+ ReplyHandler handler = msg.popHandler();
+ if (queuFull) {
+ requests.incrementAndGet();
+ // Disable queue full after some attempts
+ if (requests.get() == NUMBER_OF_QUEUE_FULL_RESPONSES) {
+ queuFull = false;
+ }
+ Result r = new Result(ErrorCode.SEND_QUEUE_FULL, "queue full");
+ return r;
+ }
+
+ handler.handleReply(new MockReply(msg.getContext()));
+ return Result.ACCEPTED;
+ }
+ }
+
+ ReferencedResource<SharedSourceSession> retainMockSession(
+ SourceSessionParams sessionParams,
+ AtomicInteger requests) {
+ final SharedSourceSession session = new MockSharedSession(sessionParams, requests);
+ return new ReferencedResource<>(session, References.fromResource(session));
+ }
+
+ @Test
+ public void testRetriesWhenThreadsAvailable() throws IOException {
+ request.getJDiscRequest().headers().add(Headers.DENY_IF_BUSY, "true");
+ threadsAvail.set(10);
+
+ clientFeederV3.handleRequest(request);
+ assertTrue(requests.get() == NUMBER_OF_QUEUE_FULL_RESPONSES);
+ }
+
+ @Test
+ public void testNoRetriesWhenNoThreadsAvailable() throws IOException {
+ request.getJDiscRequest().headers().add(Headers.DENY_IF_BUSY, "true");
+ threadsAvail.set(0);
+
+ clientFeederV3.handleRequest(request);
+ assertTrue(requests.get() == 1);
+ }
+
+ @Test
+ public void testRetriesWhenNoThreadsAvailableButNoDenyIfBusy() throws IOException {
+ request.getJDiscRequest().headers().add(Headers.DENY_IF_BUSY, "false");
+ threadsAvail.set(0);
+
+ clientFeederV3.handleRequest(request);
+ assertTrue(requests.get() == NUMBER_OF_QUEUE_FULL_RESPONSES);
+ }
+} \ No newline at end of file
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/VersionsTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/VersionsTestCase.java
new file mode 100644
index 00000000000..df1de5d75ed
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/VersionsTestCase.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server;
+
+import com.yahoo.collections.Tuple2;
+import com.yahoo.container.jdisc.HttpResponse;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.7.0
+ */
+public class VersionsTestCase {
+
+ private static final List<String> EMPTY = Collections.emptyList();
+ private static final List<String> ONE_TWO = Arrays.asList("1", "2");
+ private static final List<String> TWO_THREE = Arrays.asList("3", "2");
+ private static final List<String> ONE_NULL_TWO = Arrays.asList("1", null, "2");
+ private static final List<String> ONE_COMMA_TWO = Collections.singletonList("1, 2");
+ private static final List<String> ONE_EMPTY_TWO = Arrays.asList("1", "", "2");
+ private static final List<String> TOO_LARGE_NUMBER = Collections.singletonList("1000000000");
+ private static final List<String> TWO_TOO_LARGE_NUMBER = Arrays.asList("2", "1000000000");
+ private static final List<String> TWO_COMMA_TOO_LARGE_NUMBER = Arrays.asList("2,1000000000");
+ private static final List<String> GARBAGE = Collections.singletonList("garbage");
+
+ @Test
+ public void testEmpty() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(EMPTY);
+ assertThat(v.first, instanceOf(ErrorHttpResponse.class));
+ assertThat(v.second, is(-1));
+ }
+
+ @Test
+ public void testOneTwo() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(ONE_TWO);
+ assertThat(v.first, nullValue());
+ assertThat(v.second, is(2));
+ }
+
+ @Test
+ public void testTwoThree() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(TWO_THREE);
+ assertThat(v.first, nullValue());
+ assertThat(v.second, is(3));
+ }
+
+ @Test
+ public void testOneNullTwo() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(ONE_NULL_TWO);
+ assertThat(v.first, nullValue());
+ assertThat(v.second, is(2));
+ }
+
+ @Test
+ public void testOneCommaTwo() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(ONE_COMMA_TWO);
+ assertThat(v.first, nullValue());
+ assertThat(v.second, is(2));
+ }
+
+ @Test
+ public void testOneEmptyTwo() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(ONE_EMPTY_TWO);
+ assertThat(v.first, nullValue());
+ assertThat(v.second, is(2));
+ }
+
+ @Test
+ public void testTooLarge() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(TOO_LARGE_NUMBER);
+ assertThat(v.first, instanceOf(ErrorHttpResponse.class));
+ assertThat(v.second, is(-1));
+ }
+
+ @Test
+ public void testTwoTooLarge() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(TWO_TOO_LARGE_NUMBER);
+ assertThat(v.first, nullValue());
+ assertThat(v.second, is(2));
+ }
+
+ @Test
+ public void testTwoCommaTooLarge() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(TWO_COMMA_TOO_LARGE_NUMBER);
+ assertThat(v.first, nullValue());
+ assertThat(v.second, is(2));
+ }
+
+ @Test
+ public void testGarbage() throws Exception {
+ Tuple2<HttpResponse, Integer> v = FeedHandler.doCheckProtocolVersion(GARBAGE);
+ assertThat(v.first, instanceOf(ErrorHttpResponse.class));
+ assertThat(v.second, is(-1));
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/util/ByteLimitedInputStreamTestCase.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/util/ByteLimitedInputStreamTestCase.java
new file mode 100644
index 00000000000..b134b5c34cd
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespa/http/server/util/ByteLimitedInputStreamTestCase.java
@@ -0,0 +1,91 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.http.server.util;
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a>
+ * @since 5.1.23
+ */
+public class ByteLimitedInputStreamTestCase {
+
+ private static ByteLimitedInputStream create(byte[] source, int limit) {
+ if (limit > source.length) {
+ throw new IllegalArgumentException("Limit is greater than length of source buffer.");
+ }
+ InputStream wrappedStream = new ByteArrayInputStream(source);
+ return new ByteLimitedInputStream(wrappedStream, limit);
+ }
+
+ @Test
+ public void requireThatBasicsWork() throws IOException {
+ ByteLimitedInputStream stream = create("abcdefghijklmnopqr".getBytes(StandardCharsets.US_ASCII), 9);
+
+ assertThat(stream.available(), is(9));
+ assertThat(stream.read(), is(97));
+ assertThat(stream.available(), is(8));
+ assertThat(stream.read(), is(98));
+ assertThat(stream.available(), is(7));
+ assertThat(stream.read(), is(99));
+ assertThat(stream.available(), is(6));
+ assertThat(stream.read(), is(100));
+ assertThat(stream.available(), is(5));
+ assertThat(stream.read(), is(101));
+ assertThat(stream.available(), is(4));
+ assertThat(stream.read(), is(102));
+ assertThat(stream.available(), is(3));
+ assertThat(stream.read(), is(103));
+ assertThat(stream.available(), is(2));
+ assertThat(stream.read(), is(104));
+ assertThat(stream.available(), is(1));
+ assertThat(stream.read(), is(105));
+ assertThat(stream.available(), is(0));
+ assertThat(stream.read(), is(-1));
+ assertThat(stream.available(), is(0));
+ assertThat(stream.read(), is(-1));
+ assertThat(stream.available(), is(0));
+ assertThat(stream.read(), is(-1));
+ assertThat(stream.available(), is(0));
+ assertThat(stream.read(), is(-1));
+ assertThat(stream.available(), is(0));
+ assertThat(stream.read(), is(-1));
+ assertThat(stream.available(), is(0));
+ }
+
+ @Test
+ public void requireThatChunkedReadWorks() throws IOException {
+ ByteLimitedInputStream stream = create("abcdefghijklmnopqr".getBytes(StandardCharsets.US_ASCII), 9);
+
+ assertThat(stream.available(), is(9));
+ byte[] toBuf = new byte[4];
+ assertThat(stream.read(toBuf), is(4));
+ assertThat(toBuf[0], is((byte) 97));
+ assertThat(toBuf[1], is((byte) 98));
+ assertThat(toBuf[2], is((byte) 99));
+ assertThat(toBuf[3], is((byte) 100));
+ assertThat(stream.available(), is(5));
+
+ assertThat(stream.read(toBuf), is(4));
+ assertThat(toBuf[0], is((byte) 101));
+ assertThat(toBuf[1], is((byte) 102));
+ assertThat(toBuf[2], is((byte) 103));
+ assertThat(toBuf[3], is((byte) 104));
+ assertThat(stream.available(), is(1));
+
+ assertThat(stream.read(toBuf), is(1));
+ assertThat(toBuf[0], is((byte) 105));
+ assertThat(stream.available(), is(0));
+
+ assertThat(stream.read(toBuf), is(-1));
+ assertThat(stream.available(), is(0));
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespaxmlparser/MockFeedReaderFactory.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespaxmlparser/MockFeedReaderFactory.java
new file mode 100644
index 00000000000..c245490b1d7
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespaxmlparser/MockFeedReaderFactory.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespaxmlparser;
+
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.vespa.http.client.config.FeedParams;
+import com.yahoo.vespa.http.server.FeedReaderFactory;
+
+import java.io.InputStream;
+
+/**
+ * For creating MockReader of innput stream.
+ * @author dybdahl
+ */
+public class MockFeedReaderFactory extends FeedReaderFactory {
+
+ @Override
+ public FeedReader createReader(
+ InputStream inputStream,
+ DocumentTypeManager docTypeManager,
+ FeedParams.DataFormat dataFormat) {
+ try {
+ return new MockReader(inputStream);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/vespaxmlparser/MockReader.java b/vespaclient-container-plugin/src/test/java/com/yahoo/vespaxmlparser/MockReader.java
new file mode 100644
index 00000000000..a4ac0f4fdaf
--- /dev/null
+++ b/vespaclient-container-plugin/src/test/java/com/yahoo/vespaxmlparser/MockReader.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespaxmlparser;
+
+import com.yahoo.document.Document;
+import com.yahoo.document.DocumentId;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.DocumentUpdate;
+import com.yahoo.vespa.http.server.MetaStream;
+import com.yahoo.vespa.http.server.util.ByteLimitedInputStream;
+import com.yahoo.vespaxmlparser.VespaXMLFeedReader.Operation;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+
+/**
+ * Mock for ExternalFeedTestCase which had to override package private methods.
+ *
+ * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a>
+ */
+public class MockReader implements FeedReader {
+
+ MetaStream stream;
+ boolean finished = false;
+
+ public MockReader(InputStream stream) throws Exception {
+ this.stream = getMetaStream(stream);
+ }
+
+ private static MetaStream getMetaStream(InputStream stream) {
+ if (stream instanceof MetaStream) {
+ return (MetaStream) stream;
+ }
+ if (!(stream instanceof ByteLimitedInputStream)) {
+ throw new IllegalStateException("Given unknown stream type.");
+ }
+ //Ooooooo this is so ugly
+ try {
+ ByteLimitedInputStream byteLimitedInputStream = (ByteLimitedInputStream) stream;
+ Field f = byteLimitedInputStream.getClass().getDeclaredField("wrappedStream"); //NoSuchFieldException
+ f.setAccessible(true);
+ return (MetaStream) f.get(byteLimitedInputStream);
+ } catch (Exception e) {
+ throw new IllegalStateException("Implementation of ByteLimitedInputStream has changed.", e);
+ }
+ }
+
+ @Override
+ public void read(Operation operation) throws Exception {
+ if (finished) {
+ return;
+ }
+
+ byte whatToDo = stream.getNextOperation();
+ DocumentId id = new DocumentId("id:banana:banana::doc1");
+ DocumentType docType = new DocumentType("banana");
+ switch (whatToDo) {
+ case 0:
+ finished = true;
+ break;
+ case 1:
+ Document doc = new Document(docType, id);
+ operation.setDocument(doc);
+ break;
+ case 2:
+ operation.setRemove(id);
+ break;
+ case 3:
+ operation.setDocumentUpdate(new DocumentUpdate(docType, id));
+ break;
+ case 4:
+ throw new RuntimeException("boom");
+ }
+ }
+
+} \ No newline at end of file