package org.basex.http;
import static org.basex.core.Text.*;
import static org.basex.query.func.Function.*;
import static org.basex.util.Token.*;
import static org.basex.util.http.HttpText.*;
import static org.junit.Assert.*;
import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;
import java.util.List;
import org.basex.core.*;
import org.basex.core.cmd.*;
import org.basex.io.*;
import org.basex.io.serial.*;
import org.basex.query.QueryError.ErrType;
import org.basex.query.*;
import org.basex.query.func.fn.*;
import org.basex.query.value.*;
import org.basex.query.value.item.*;
import org.basex.query.value.node.*;
import org.basex.query.value.type.*;
import org.basex.util.*;
import org.basex.util.http.*;
import org.basex.util.http.HttpRequest.Part;
import org.junit.*;
import org.junit.Test;
/**
* This class tests the server-based HTTP Client.
*
* @author BaseX Team 2005-17, BSD License
* @author Rositsa Shadura
*/
public class FnHttpTest extends HTTPTest {
/** Example url. */
static final String RESTURL = REST_ROOT + NAME;
/** Books document. */
private static final String BOOKS = "<books>" + "<book id='1'>"
+ "<name>Sherlock Holmes</name>" + "<author>Doyle</author>" + "</book>"
+ "<book id='2'>" + "<name>Winnetou</name>" + "<author>May</author>"
+ "</book>" + "<book id='3'>" + "<name>Tom Sawyer</name>"
+ "<author>Twain</author>" + "</book>" + "</books>";
/** Carriage return/line feed. */
private static final String CRLF = "\r\n";
/** Local database context. */
static Context ctx;
/**
* Start server.
* @throws Exception exception
*/
@BeforeClass
public static void start() throws Exception {
init(RESTURL, true);
ctx = new Context();
}
/**
* Test sending of HTTP PUT requests.
* @throws Exception exception
*/
@Test
public void put() throws Exception {
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='put' status-only='true'>"
+ "<http:body media-type='text/xml'>" + BOOKS + "</http:body>"
+ "</http:request>", RESTURL), ctx)) {
checkResponse(qp.value(), 1, HttpURLConnection.HTTP_CREATED);
}
}
/**
* Test sending of HTTP POST requests.
* @throws Exception exception
*/
@Test
public void putPost() throws Exception {
// PUT - query
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='put' status-only='true'>"
+ "<http:body media-type='text/xml'>" + BOOKS + "</http:body>"
+ "</http:request>", RESTURL), ctx)) {
checkResponse(qp.value(), 1, HttpURLConnection.HTTP_CREATED);
}
// POST - query
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='post'>"
+ "<http:body media-type='application/xml'>"
+ "<query xmlns='" + Prop.URL + "/rest'>"
+ "<text><![CDATA[<x>1</x>]]></text>"
+ "</query>"
+ "</http:body>"
+ "</http:request>", RESTURL), ctx)) {
checkResponse(qp.value(), 2, HttpURLConnection.HTTP_OK);
}
// Execute the same query but with content set from $bodies
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='post'>"
+ "<http:body media-type='application/xml'/>"
+ "</http:request>",
RESTURL,
"<query xmlns='" + Prop.URL + "/rest'>"
+ "<text><![CDATA[<x>1</x>]]></text>"
+ "</query>"), ctx)) {
checkResponse(qp.value(), 2, HttpURLConnection.HTTP_OK);
}
}
/**
* Test sending of HTTP GET requests.
* @throws Exception exception
*/
@Test
public void postGet() throws Exception {
// GET1 - just send a GET request
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='get' href='" + REST_ROOT + "'/>"), ctx)) {
final Value v = qp.value();
checkResponse(v, 2, HttpURLConnection.HTTP_OK);
assertEquals(NodeType.DOC, v.itemAt(1).type);
}
// GET2 - with override-media-type='text/plain'
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='get' override-media-type='text/plain'/>", REST_ROOT), ctx)) {
final Value v = qp.value();
checkResponse(v, 2, HttpURLConnection.HTTP_OK);
assertEquals(AtomType.STR, v.itemAt(1).type);
}
// Get3 - with status-only='true'
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='get' status-only='true'/>", REST_ROOT), ctx)) {
checkResponse(qp.value(), 1, HttpURLConnection.HTTP_OK);
}
}
/**
* Test sending of HTTP DELETE requests.
* @throws Exception exception
*/
@Test
public void postDelete() throws Exception {
// add document to be deleted
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='put'>"
+ "<http:body media-type='text/xml'><ToBeDeleted/></http:body>"
+ "</http:request>", RESTURL), ctx)) {
qp.value();
}
// DELETE
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='delete' status-only='true'/>", RESTURL), ctx)) {
checkResponse(qp.value(), 1, HttpURLConnection.HTTP_OK);
}
}
/**
* Test sending of HTTP request without any attributes - error shall be thrown
* that mandatory attributes are missing.
*/
@Test
public void sendEmptyReq() {
try {
new XQuery(_HTTP_SEND_REQUEST.args("<http:request/>")).execute(ctx);
fail("Error expected");
} catch(final BaseXException ex) {
assertTrue(ex.getMessage().contains(ErrType.HC.toString()));
}
}
/**
* Tests http:send-request((),()).
*/
@Test
public void sendReqNoParams() {
final Command cmd = new XQuery(_HTTP_SEND_REQUEST.args("()"));
try {
cmd.execute(ctx);
fail("Error expected");
} catch(final BaseXException ex) {
assertTrue(ex.getMessage().contains(ErrType.HC.toString()));
}
}
/**
* Tests an erroneous query.
* @throws Exception exception
*/
@Test
public void error() throws Exception {
try(QueryProcessor qp = new QueryProcessor(_HTTP_SEND_REQUEST.args(
"<http:request method='get'/>", RESTURL + "unknown") + "[1]/@status/data()", ctx)) {
assertEquals("404", qp.value().serialize().toString());
}
}
/**
* Tests RequestParser.parse() with normal (not multipart) request.
* @throws IOException I/O Exception
* @throws QueryException query exception
*/
@Test
public void parseRequest() throws IOException, QueryException {
// Simple HTTP request with no errors
final String req = "<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "method='POST' href='" + REST_ROOT + "'>"
+ "<http:header name='hdr1' value='hdr1val'/>"
+ "<http:header name='hdr2' value='hdr2val'/>"
+ "<http:body media-type='text/xml'>" + "Test body content"
+ "</http:body>" + "</http:request>";
final DBNode dbNode = new DBNode(new IOContent(req));
final HttpRequestParser rp = new HttpRequestParser(null);
final HttpRequest r = rp.parse(dbNode.children().next());
assertEquals(2, r.attributes.size());
assertEquals(2, r.headers.size());
assertFalse(r.payload.isEmpty());
assertEquals(1, r.payloadAtts.size());
}
/**
* Tests RequestParser.parse() with multipart request.
* @throws IOException I/O Exception
* @throws QueryException query exception
*/
@Test
public void parseMultipartReq() throws IOException, QueryException {
final String multiReq = "<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "method='POST' href='" + REST_ROOT + "'>"
+ "<http:header name='hdr1' value='hdr1val'/>"
+ "<http:header name='hdr2' value='hdr2val'/>"
+ "<http:multipart media-type='multipart/mixed' boundary='xxxx'>"
+ "<http:header name='p1hdr1' value='p1hdr1val'/>"
+ "<http:header name='p1hdr2' value='p1hdr2val'/>"
+ "<http:body media-type='text/plain'>" + "Part1" + "</http:body>"
+ "<http:header name='p2hdr1' value='p2hdr1val'/>"
+ "<http:body media-type='text/plain'>" + "Part2" + "</http:body>"
+ "<http:body media-type='text/plain'>"
+ "Part3" + "</http:body>" + "</http:multipart>"
+ "</http:request>";
final DBNode dbNode1 = new DBNode(new IOContent(multiReq));
final HttpRequestParser rp = new HttpRequestParser(null);
final HttpRequest r = rp.parse(dbNode1.children().next());
assertEquals(2, r.attributes.size());
assertEquals(2, r.headers.size());
assertTrue(r.isMultipart);
assertEquals(3, r.parts.size());
// check parts
final Iterator<Part> i = r.parts.iterator();
Part part = i.next();
assertEquals(2, part.headers.size());
assertEquals(1, part.bodyContents.size());
assertEquals(1, part.bodyAtts.size());
part = i.next();
assertEquals(1, part.headers.size());
assertEquals(1, part.bodyContents.size());
assertEquals(1, part.bodyAtts.size());
part = i.next();
assertEquals(0, part.headers.size());
assertEquals(1, part.bodyContents.size());
assertEquals(1, part.bodyAtts.size());
}
/**
* Tests parsing of multipart request when the contents for each part are set
* from the $bodies parameter.
* @throws IOException I/O Exception
* @throws QueryException query exception
*/
@Test
public void parseMultipartReqBodies() throws IOException, QueryException {
final String multiReq = "<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "method='POST' href='" + REST_ROOT + "'>"
+ "<http:header name='hdr1' value='hdr1val'/>"
+ "<http:header name='hdr2' value='hdr2val'/>"
+ "<http:multipart media-type='multipart/mixed' boundary='xxxx'>"
+ "<http:header name='p1hdr1' value='p1hdr1val'/>"
+ "<http:header name='p1hdr2' value='p1hdr2val'/>"
+ "<http:body media-type='text/plain'/>"
+ "<http:header name='p2hdr1' value='p2hdr1val'/>"
+ "<http:body media-type='text/plain'/>"
+ "<http:body media-type='text/plain'/>"
+ "</http:multipart>" + "</http:request>";
final DBNode dbNode1 = new DBNode(new IOContent(multiReq));
final ValueBuilder bodies = new ValueBuilder();
bodies.add(Str.get("Part1"));
bodies.add(Str.get("Part2"));
bodies.add(Str.get("Part3"));
final HttpRequestParser rp = new HttpRequestParser(null);
final HttpRequest r = rp.parse(dbNode1.children().next(), bodies.value().iter());
assertEquals(2, r.attributes.size());
assertEquals(2, r.headers.size());
assertTrue(r.isMultipart);
assertEquals(3, r.parts.size());
// check parts
final Iterator<Part> i = r.parts.iterator();
Part part = i.next();
assertEquals(2, part.headers.size());
assertEquals(1, part.bodyContents.size());
assertEquals(1, part.bodyAtts.size());
part = i.next();
assertEquals(1, part.headers.size());
assertEquals(1, part.bodyContents.size());
assertEquals(1, part.bodyAtts.size());
part = i.next();
assertEquals(0, part.headers.size());
assertEquals(1, part.bodyContents.size());
assertEquals(1, part.bodyAtts.size());
}
/**
* Tests if errors are thrown when some mandatory attributes are missing in a
* <http:request/>, <http:body/> or <http:multipart/>.
* @throws IOException I/O Exception
*/
@Test
public void errors() throws IOException {
// Incorrect requests
final List<byte[]> falseReqs = new ArrayList<>();
// Request without method
final byte[] falseReq1 = token("<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "href='" + REST_ROOT + "'/>");
falseReqs.add(falseReq1);
// Request with send-authorization and no credentials
final byte[] falseReq2 = token("<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "method='GET' href='" + REST_ROOT + "' "
+ "send-authorization='true'/>");
falseReqs.add(falseReq2);
// Request with send-authorization and only username
final byte[] falseReq3 = token("<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "method='GET' href='" + REST_ROOT + "' "
+ "send-authorization='true' username='test'/>");
falseReqs.add(falseReq3);
// Request with body that has no media-type
final byte[] falseReq4 = token("<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "method='POST' href='" + REST_ROOT + "'>" + "<http:body>"
+ "</http:body>" + "</http:request>");
falseReqs.add(falseReq4);
// Request with multipart that has no media-type
final byte[] falseReq5 = token("<http:request method='POST' "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "href='" + REST_ROOT + "'>" + "<http:multipart boundary='xxx'>"
+ "</http:multipart>" + "</http:request>");
falseReqs.add(falseReq5);
// Request with multipart with part that has a body without media-type
final byte[] falseReq6 = token("<http:request method='POST' "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "href='" + REST_ROOT + "'>" + "<http:multipart boundary='xxx'>"
+ "<http:header name='hdr1' value-='val1'/>"
+ "<http:body media-type='text/plain'>" + "Part1" + "</http:body>"
+ "<http:header name='hdr1' value-='val1'/>"
+ "<http:body>" + "Part1" + "</http:body>"
+ "</http:multipart>" + "</http:request>");
falseReqs.add(falseReq6);
// Request with schema different from http
final byte[] falseReq7 = token("<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "href='ftp://basex.org'/>");
falseReqs.add(falseReq7);
// Request with content and method which must be empty
final byte[] falseReq8 = token("<http:request "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "method='DELETE' href='" + REST_ROOT + "'>"
+ "<http:body media-type='text/plain'>" + "</http:body>"
+ "</http:request>");
falseReqs.add(falseReq8);
for(final byte[] falseReq : falseReqs) {
final DBNode dbNode = new DBNode(new IOContent(falseReq));
try {
final HttpRequestParser rp = new HttpRequestParser(null);
rp.parse(dbNode.children().next());
fail("Exception not thrown");
} catch (final QueryException ex) {
assertTrue(ex.getMessage().contains(ErrType.HC.toString()));
}
}
}
/**
* Tests method setRequestContent of HttpClient.
* @throws IOException I/O Exception
*/
@Test
public void writeMultipartMessage() throws IOException {
final HttpRequest req = new HttpRequest();
req.isMultipart = true;
req.payloadAtts.put("media-type", "multipart/alternative");
req.payloadAtts.put("boundary", "boundary42");
final Part p1 = new Part();
p1.headers.put("Content-Type", "text/plain; charset=us-ascii");
p1.bodyAtts.put("media-type", "text/plain");
final String plain = "PLAIN\r\n";
p1.bodyContents.add(Str.get(plain));
final Part p2 = new Part();
p2.headers.put("Content-Type", "text/richtext");
p2.bodyAtts.put("media-type", "text/richtext");
final String rich = "RICH\n";
p2.bodyContents.add(Str.get(rich));
final Part p3 = new Part();
p3.headers.put("Content-Type", "text/x-whatever");
p3.bodyAtts.put("media-type", "text/x-whatever");
final String fancy = "FANCY";
p3.bodyContents.add(Str.get(fancy));
req.parts.add(p1);
req.parts.add(p2);
req.parts.add(p3);
final OutputStream out = fakeOutput();
HttpClient.writePayload(out, req);
final String expResult = "--boundary42" + CRLF
+ "Content-Type: text/plain; charset=us-ascii" + CRLF + CRLF + plain + CRLF
+ "--boundary42" + CRLF + "Content-Type: text/richtext" + CRLF + CRLF + rich + CRLF
+ "--boundary42" + CRLF + "Content-Type: text/x-whatever" + CRLF + CRLF + fancy + CRLF
+ "--boundary42--" + CRLF;
// Compare results
assertEquals(expResult, out.toString());
}
/**
* Tests method setRequestContent of HttpClient.
* @throws IOException I/O Exception
*/
@Test
public void writeMultipartBinary() throws IOException {
final HttpRequest req = new HttpRequest();
req.isMultipart = true;
req.payloadAtts.put("media-type", "multipart/mixed");
req.payloadAtts.put("boundary", "boundary");
final Part p1 = new Part();
p1.headers.put("Content-Type", "application/octet-stream");
p1.bodyAtts.put("media-type", "application/octet-stream");
p1.bodyContents.add(new B64(new byte[] { -1 }));
req.parts.add(p1);
final OutputStream out = fakeOutput();
HttpClient.writePayload(out, req);
final String expResult = "--boundary" + CRLF
+ "Content-Type: application/octet-stream" + CRLF
+ "Content-Transfer-Encoding: base64" + CRLF
+ CRLF
+ "/w==" + CRLF + CRLF
+ "--boundary--" + CRLF;
// Compare results
assertEquals(expResult, out.toString());
}
/**
* Tests writing of request content with different combinations of the body
* attributes media-type and method.
* @throws IOException IO exception
*/
@Test
public void writeMessage() throws IOException {
// Case 1: No method, media-type='text/xml'
final HttpRequest req1 = new HttpRequest();
final OutputStream out1 = fakeOutput();
req1.payloadAtts.put(SerializerOptions.MEDIA_TYPE.name(), "text/xml");
// Node child
final FElem e1 = new FElem("a").add("a");
req1.payload.add(e1);
// String item child
req1.payload.add(Str.get("<b>b</b>"));
HttpClient.writePayload(out1, req1);
assertEquals("<a>a</a><b>b</b>", out1.toString());
// Case 2: No method, media-type='text/plain'
final HttpRequest req2 = new HttpRequest();
final OutputStream out2 = fakeOutput();
req2.payloadAtts.put(SerializerOptions.MEDIA_TYPE.name(), "text/plain");
// Node child
final FElem e2 = new FElem("a").add("a");
req2.payload.add(e2);
// String item child
req2.payload.add(Str.get("<b>b</b>"));
HttpClient.writePayload(out2, req2);
assertEquals("a<b>b</b>", out2.toString());
// Case 3: method='text', media-type='text/xml'
final HttpRequest req3 = new HttpRequest();
final OutputStream out3 = fakeOutput();
req3.payloadAtts.put(SerializerOptions.MEDIA_TYPE.name(), "text/xml");
req3.payloadAtts.put("method", "text");
// Node child
final FElem e3 = new FElem("a").add("a");
req3.payload.add(e3);
// String item child
req3.payload.add(Str.get("<b>b</b>"));
HttpClient.writePayload(out3, req3);
assertEquals("a<b>b</b>", out3.toString());
}
/**
* Tests writing of body content when @method is raw and output is xs:base64Binary.
* @throws IOException I/O Exception
*/
@Test
public void writeBase64() throws IOException {
// Case 1: content is xs:base64Binary
final HttpRequest req1 = new HttpRequest();
req1.payloadAtts.put("method", SerialMethod.BASEX.toString());
req1.payload.add(new B64(token("test")));
final OutputStream out1 = fakeOutput();
HttpClient.writePayload(out1, req1);
assertEquals("test", out1.toString());
// Case 2: content is a node
final HttpRequest req2 = new HttpRequest();
req2.payloadAtts.put("method", SerialMethod.BASEX.toString());
final FElem e3 = new FElem("a").add("test");
req2.payload.add(e3);
final OutputStream out2 = fakeOutput();
HttpClient.writePayload(out2, req2);
assertEquals("<a>test</a>", out2.toString());
}
/**
* Tests writing of body content when @method is raw and output is xs:hexBinary.
* @throws IOException I/O Exception
*/
@Test
public void writeHex() throws IOException {
// Case 1: content is xs:hexBinary
final HttpRequest req1 = new HttpRequest();
req1.payloadAtts.put("method", SerialMethod.BASEX.toString());
req1.payload.add(new Hex(token("test")));
final OutputStream out1 = fakeOutput();
HttpClient.writePayload(out1, req1);
assertEquals("test", out1.toString());
// Case 2: content is a node
final HttpRequest req2 = new HttpRequest();
req2.payloadAtts.put("method", SerialMethod.BASEX.toString());
final FElem e3 = new FElem("a").add("test");
req2.payload.add(e3);
final OutputStream out2 = fakeOutput();
HttpClient.writePayload(out2, req2);
assertEquals("<a>test</a>", out2.toString());
}
/**
* Tests writing of request content when @src is set.
* @throws IOException I/O Exception
*/
@Test
public void writeFromResource() throws IOException {
// Create a file form which will be read
final IOFile file = new IOFile(Prop.TMP, Util.className(FnHttpTest.class));
file.write(token("test"));
// Request
final HttpRequest req = new HttpRequest();
req.payloadAtts.put("src", file.url());
req.payloadAtts.put("method", "binary");
// HTTP connection
final OutputStream out = fakeOutput();
HttpClient.writePayload(out, req);
// Delete file
file.delete();
assertEquals("test", out.toString());
}
/**
* Tests response handling with specified charset in the header
* 'Content-Type'.
* @throws IOException I/O Exception
* @throws QueryException query exception
*/
@Test
public void responseWithCharset() throws IOException, QueryException {
// Create fake HTTP connection
final FakeHttpConnection conn = new FakeHttpConnection();
// Set content type
conn.contentType = "text/plain; charset=CP1251";
// set content encoded in CP1251
final String test = "\u0442\u0435\u0441\u0442";
conn.content = Charset.forName("CP1251").encode(test).array();
final Value res = new HttpResponse(null, ctx.options).getResponse(conn, true, null);
// compare results
assertEquals(test, string(res.itemAt(1).string(null)));
}
/**
* Tests ResponseHandler.getResponse() with multipart response.
* @throws IOException I/O Exception
* @throws Exception exception
*/
@Test
public void multipartResponse() throws Exception {
// Create fake HTTP connection
final FakeHttpConnection conn = new FakeHttpConnection();
final Map<String, List<String>> hdrs = new HashMap<>();
final List<String> fromVal = new ArrayList<>();
fromVal.add("Nathaniel Borenstein <nsb@bellcore.com>");
// From: Nathaniel Borenstein <nsb@bellcore.com>
hdrs.put("From", fromVal);
final List<String> mimeVal = new ArrayList<>();
mimeVal.add("1.0");
// MIME-Version: 1.0
hdrs.put("MIME-version", mimeVal);
final List<String> subjVal = new ArrayList<>();
subjVal.add("Formatted text mail");
// Subject: Formatted text mail
hdrs.put("Subject", subjVal);
final List<String> contTypeVal = new ArrayList<>();
contTypeVal.add("multipart/alternative");
contTypeVal.add("boundary=\"boundary42\"");
// Content-Type: multipart/alternative; boundary=boundary42
hdrs.put("Content-Type", contTypeVal);
conn.headers = hdrs;
conn.contentType = "multipart/alternative; boundary=\"boundary42\"";
conn.content = token("--boundary42" + CRLF
+ "Content-Type: text/plain; charset=us-ascii" + CRLF + CRLF
+ "...plain text...." + CRLF + CRLF
+ "--boundary42" + CRLF + "Content-Type: text/richtext" + CRLF + CRLF
+ ".... richtext..." + CRLF
+ "--boundary42" + CRLF + "Content-Type: text/x-whatever" + CRLF + CRLF
+ ".... fanciest formatted version " + CRLF + "..." + CRLF + "--boundary42--");
final Value returned = new HttpResponse(null, ctx.options).getResponse(conn, true, null);
// Construct expected result
final ValueBuilder expected = new ValueBuilder();
final String response = "<http:response "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "status='200' message='OK'>"
+ "<http:header name='Subject' value='Formatted text mail'/>"
+ "<http:header name='Content-Type' "
+ "value='multipart/alternative;boundary="boundary42"'/>"
+ "<http:header name='MIME-version' value='1.0'/>"
+ "<http:header name='From' value='Nathaniel Borenstein "
+ "<nsb@bellcore.com>'/>"
+ "<http:multipart media-type='multipart/alternative' "
+ "boundary='boundary42'>"
+ "<http:header name='Content-Type' "
+ "value='text/plain; charset=us-ascii'/>"
+ "<http:body media-type='text/plain; charset=us-ascii'/>"
+ "<http:header name='Content-Type' value='text/richtext'/>"
+ "<http:body media-type='text/richtext'/>"
+ "<http:header name='Content-Type' value='text/x-whatever'/>"
+ "<http:body media-type='text/x-whatever'/>"
+ "</http:multipart>" + "</http:response> ";
expected.add(new DBNode(new IOContent(response)).children().next());
expected.add(Str.get("...plain text....\n"));
expected.add(Str.get(".... richtext..."));
expected.add(Str.get(".... fanciest formatted version \n..."));
compare(expected.value(), returned);
}
/**
* Tests ResponseHandler.getResponse() with multipart response having preamble and epilogue.
* @throws IOException I/O Exception
* @throws Exception exception
*/
@Test
public void multipartRespPreamble() throws Exception {
// Create fake HTTP connection
final FakeHttpConnection conn = new FakeHttpConnection();
final Map<String, List<String>> hdrs = new HashMap<>();
final List<String> fromVal = new ArrayList<>();
fromVal.add("Nathaniel Borenstein <nsb@bellcore.com>");
// From: Nathaniel Borenstein <nsb@bellcore.com>
hdrs.put("From", fromVal);
final List<String> mimeVal = new ArrayList<>();
mimeVal.add("1.0");
final List<String> toVal = new ArrayList<>();
toVal.add("Ned Freed <ned@innosoft.com>");
// To: Ned Freed <ned@innosoft.com>
hdrs.put("To", toVal);
// MIME-Version: 1.0
hdrs.put("MIME-version", mimeVal);
final List<String> subjVal = new ArrayList<>();
subjVal.add("Formatted text mail");
// Subject: Formatted text mail
hdrs.put("Subject", subjVal);
final List<String> contTypeVal = new ArrayList<>();
contTypeVal.add("multipart/mixed");
contTypeVal.add("boundary=\"simple boundary\"");
// Content-Type: multipart/alternative; boundary=boundary42
hdrs.put("Content-Type", contTypeVal);
conn.headers = hdrs;
conn.contentType = "multipart/mixed; boundary=\"simple boundary\"";
// Response to be read
conn.content = token("This is the preamble. "
+ "It is to be ignored, though it" + NL
+ "is a handy place for mail composers to include an" + CRLF
+ "explanatory note to non-MIME compliant readers." + CRLF
+ "--simple boundary" + CRLF + CRLF
+ "This is implicitly typed plain ASCII text." + CRLF
+ "It does NOT end with a linebreak."
+ CRLF + "--simple boundary" + CRLF
+ "Content-type: text/plain; charset=us-ascii" + CRLF + CRLF
+ "This is explicitly typed plain ASCII text." + CRLF
+ "It DOES end with a linebreak." + CRLF
+ CRLF + "--simple boundary--" + CRLF
+ "This is the epilogue. It is also to be ignored.");
// Get response as sequence of XQuery items
final Value returned = new HttpResponse(null, ctx.options).getResponse(conn, true, null);
// Construct expected result
final ValueBuilder expected = new ValueBuilder();
final String response = "<http:response "
+ "xmlns:http='http://expath.org/ns/http-client' "
+ "status='200' message='OK'>"
+ "<http:header name='Subject' value='Formatted text mail'/>"
+ "<http:header name='To' value='Ned "
+ "Freed <ned@innosoft.com>'/>"
+ "<http:header name='Content-Type' value='multipart/mixed;"
+ "boundary="simple boundary"'/>"
+ "<http:header name='MIME-version' value='1.0'/>"
+ "<http:header name='From' value='Nathaniel Borenstein "
+ "<nsb@bellcore.com>'/>"
+ "<http:multipart media-type='multipart/mixed' "
+ "boundary='simple boundary'>"
+ "<http:body media-type='text/plain'/>"
+ "<http:header name='Content-type' value='text/plain; "
+ "charset=us-ascii'/>"
+ "<http:body media-type='text/plain; charset=us-ascii'/>"
+ "</http:multipart>" + "</http:response>";
expected.add(new DBNode(new IOContent(response)).children().next());
expected.add(Str.get("This is implicitly typed plain ASCII text.\n"
+ "It does NOT end with a linebreak."));
expected.add(Str.get("This is explicitly typed plain ASCII text.\n"
+ "It DOES end with a linebreak.\n"));
compare(expected.value(), returned);
}
/**
* Compares results.
* @param expected expected result
* @param returned returned result
* @throws Exception exception
*/
private static void compare(final Value expected, final Value returned) throws Exception {
// Compare response with expected result
assertEquals("Different number of results", expected.size(), returned.size());
final long es = expected.size();
for(int e = 0; e < es; e++) {
Item exp = expected.itemAt(e), ret = returned.itemAt(e);
// reorder response headers
if(exp.type == NodeType.ELM) exp = reorderHeaders(exp);
if(ret.type == NodeType.ELM) ret = reorderHeaders(ret);
// compare items
if(!new DeepEqual().equal(exp, ret)) {
final TokenBuilder tb = new TokenBuilder("Result ").addLong(e).add(" differs:\nReturned: ");
tb.addExt(ret.serialize()).add("\nExpected: ").addExt(exp.serialize());
fail(tb.toString());
}
}
}
/**
* Sorts HTTP headers.
* @param xml original element
* @return element with reordered headers
* @throws QueryException query exception
*/
private static Item reorderHeaders(final Item xml) throws QueryException {
final String query = ". update {"
+ " delete nodes http:header,"
+ " for $h in http:header"
+ " order by $h/@name"
+ " return insert node $h as first into ."
+ "}";
try(QueryProcessor qp = new QueryProcessor(query, ctx).context(xml)) {
return qp.iter().next();
}
}
/**
* Tests nested multipart responses.
* @throws Exception exception
*/
@Test
public void nestedMultipart() throws Exception {
// Create fake HTTP connection
final String boundary = "batchresponse_4c4c5223-efa7-4aba-9865-fb4cb102cfd2";
final FakeHttpConnection conn = new FakeHttpConnection();
final Map<String, List<String>> hdrs = new HashMap<>();
final List<String> contTypeVal = new ArrayList<>();
contTypeVal.add("multipart/mixed");
contTypeVal.add("boundary=\"" + boundary + '"');
hdrs.put("Content-Type", contTypeVal);
conn.headers = hdrs;
conn.contentType = "multipart/alternative; boundary=\"" + boundary + '"';
conn.content = new IOFile("src/test/resources/response.txt").read();
new HttpResponse(null, ctx.options).getResponse(conn, true, null);
}
/**
* Checks the response to an HTTP request.
* @param v query result
* @param itemsCount expected number of items
* @param expStatus expected status
*/
private static void checkResponse(final Value v, final int itemsCount, final int expStatus) {
assertEquals(itemsCount, v.size());
assertTrue(v.itemAt(0) instanceof FElem);
final FElem response = (FElem) v.itemAt(0);
assertNotNull(response.attributes());
if(!eq(response.attribute(STATUS), token(expStatus))) {
fail("Expected: " + expStatus + "\nFound: " + response);
}
}
/**
* Returns the output stream of a fake connection.
* @return output stream
* @throws MalformedURLException exception
*/
private OutputStream fakeOutput() throws MalformedURLException {
return new FakeHttpConnection().getOutputStream();
}
}
/**
* Fake HTTP connection.
* @author BaseX Team 2005-17, BSD License
* @author Rositsa Shadura
*/
final class FakeHttpConnection extends HttpURLConnection {
/** Connection output stream. */
final ByteArrayOutputStream out = new ByteArrayOutputStream();
/** Request headers. */
Map<String, List<String>> headers = new HashMap<>();
/** Content-type. */
String contentType;
/** Content. */
byte[] content;
/**
* Constructor.
* @throws MalformedURLException exception
*/
FakeHttpConnection() throws MalformedURLException {
super(new URL("http://fake-test.com"));
}
@Override
public ByteArrayInputStream getInputStream() {
return new ByteArrayInputStream(content);
}
@Override
public String getContentType() {
return contentType;
}
@Override
public int getResponseCode() {
return 200;
}
@Override
public String getResponseMessage() {
return "OK";
}
@Override
public Map<String, List<String>> getHeaderFields() {
return headers;
}
@Override
public String getHeaderField(final String field) {
final List<String> values = headers.get(field);
final StringBuilder sb = new StringBuilder();
for(final String v : values) sb.append(v).append(';');
return sb.substring(0, sb.length() - 1);
}
@Override
public OutputStream getOutputStream() {
return out;
}
@Override
public void disconnect() {
}
@Override
public boolean usingProxy() {
return false;
}
@Override
public void connect() {
}
}