use susy_jsonrpc_core;
use std::str::Lines;
use std::net::TcpStream;
use std::io::{Read, Write};
use self::susy_jsonrpc_core::{IoHandler, Params, Value, Error, ErrorCode};
use self::susy_jsonrpc_core::futures::{self, Future};
use super::*;
fn serve_hosts(hosts: Vec<Host>) -> Server {
ServerBuilder::new(IoHandler::default())
.cors(DomainsValidation::AllowOnly(vec![AccessControlAllowOrigin::Value("superstring.ch".into())]))
.allowed_hosts(DomainsValidation::AllowOnly(hosts))
.start_http(&"127.0.0.1:0".parse().unwrap())
.unwrap()
}
fn id<T>(t: T) -> T { t }
fn serve<F: FnOnce(ServerBuilder) -> ServerBuilder>(alter: F) -> Server {
let builder = ServerBuilder::new(io())
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Value("superstring.ch".into()),
AccessControlAllowOrigin::Null,
]))
.cors_max_age(None)
.rest_api(RestApi::Secure)
.health_api(("/health", "hello_async"));
alter(builder)
.start_http(&"127.0.0.1:0".parse().unwrap())
.unwrap()
}
fn serve_allow_headers(cors_allow_headers: cors::AccessControlAllowHeaders) -> Server {
let mut io = IoHandler::default();
io.add_method("hello", |params: Params| {
match params.parse::<(u64, )>() {
Ok((num, )) => Ok(Value::String(format!("world: {}", num))),
_ => Ok(Value::String("world".into())),
}
});
ServerBuilder::new(io)
.cors(
DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Value("superstring.ch".into()),
AccessControlAllowOrigin::Null,
])
)
.cors_allow_headers(cors_allow_headers)
.start_http(&"127.0.0.1:0".parse().unwrap())
.unwrap()
}
fn io() -> IoHandler {
use std::{thread, time};
let mut io = IoHandler::default();
io.add_method("hello", |params: Params| {
match params.parse::<(u64, )>() {
Ok((num, )) => Ok(Value::String(format!("world: {}", num))),
_ => Ok(Value::String("world".into())),
}
});
io.add_method("fail", |_: Params| Err(Error::new(ErrorCode::ServerError(-34))));
io.add_method("hello_async", |_params: Params| {
futures::finished(Value::String("world".into()))
});
io.add_method("hello_async2", |_params: Params| {
let (c, p) = futures::oneshot();
thread::spawn(move || {
thread::sleep(time::Duration::from_millis(10));
c.send(Value::String("world".into())).unwrap();
});
p.map_err(|_| Error::invalid_request())
});
io
}
struct Response {
status: String,
headers: String,
body: String,
}
fn read_block(lines: &mut Lines) -> String {
let mut block = String::new();
loop {
let line = lines.next();
match line {
Some("") | None => break,
Some(v) => {
block.push_str(v);
block.push_str("\n");
},
}
}
block
}
fn request(server: Server, request: &str) -> Response {
let mut req = TcpStream::connect(server.address()).unwrap();
req.write_all(request.as_bytes()).unwrap();
let mut response = String::new();
req.read_to_string(&mut response).unwrap();
let mut lines = response.lines();
let status = lines.next().unwrap().to_owned();
let headers = read_block(&mut lines);
let body = read_block(&mut lines);
Response {
status,
headers,
body,
}
}
#[test]
fn should_return_method_not_allowed_for_get() {
let server = serve(id);
let response = request(server,
"\
GET / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
I shouldn't be read.\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 405 Method Not Allowed".to_owned());
assert_eq!(response.body, "Used HTTP Method is not allowed. POST or OPTIONS is required\n".to_owned());
}
#[test]
fn should_handle_health_endpoint() {
let server = serve(id);
let response = request(server,
"\
GET /health HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
I shouldn't be read.\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, "\"world\"\n");
}
#[test]
fn should_handle_health_endpoint_failure() {
let server = serve(|builder| builder.health_api(("/api/health", "fail")));
let response = request(server,
"\
GET /api/health HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
I shouldn't be read.\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 503 Service Unavailable".to_owned());
assert_eq!(response.body, "{\"code\":-34,\"message\":\"Server error\"}\n");
}
#[test]
fn should_return_unsupported_media_type_if_not_json() {
let server = serve(id);
let response = request(server,
"\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
\r\n\
{}\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 415 Unsupported Media Type".to_owned());
assert_eq!(response.body, "Supplied content type is not allowed. Content-Type: application/json is required\n".to_owned());
}
#[test]
fn should_return_error_for_malformed_request() {
let server = serve(id);
let req = r#"{"jsonrpc":"3.0","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, invalid_request());
}
#[test]
fn should_return_error_for_malformed_request2() {
let server = serve(id);
let req = r#"{"jsonrpc":"2.0","metho1d":""}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, invalid_request());
}
#[test]
fn should_return_empty_response_for_notification() {
let server = serve(id);
let req = r#"{"jsonrpc":"2.0","method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, "".to_owned());
}
#[test]
fn should_return_method_not_found() {
let server = serve(id);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_add_cors_allow_origins() {
let server = serve(id);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
assert!(response.headers.contains("access-control-allow-origin: http://superstring.ch"), "Headers missing in {}", response.headers);
}
#[test]
fn should_add_cors_max_age_headers() {
let server = serve(|builder| builder.cors_max_age(1_000));
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
assert!(response.headers.contains("access-control-allow-origin: http://superstring.ch"), "Headers missing in {}", response.headers);
assert!(response.headers.contains("access-control-max-age: 1000"), "Headers missing in {}", response.headers);
}
#[test]
fn should_not_add_cors_allow_origins() {
let server = serve(id);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: fake.io\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, cors_invalid_allow_origin());
}
#[test]
fn should_not_process_the_request_in_case_of_invalid_allow_origin() {
let server = serve(id);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"hello"}"#;
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: fake.io\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, cors_invalid_allow_origin());
}
#[test]
fn should_return_proper_headers_on_options() {
let server = serve(id);
let response = request(server,
"\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Length: 0\r\n\
\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert!(response.headers.contains("allow: OPTIONS, POST"), "Headers missing in {}", response.headers);
assert!(response.headers.contains("accept: application/json"), "Headers missing in {}", response.headers);
assert_eq!(response.body, "");
}
#[test]
fn should_add_cors_allow_origin_for_null_origin() {
let server = serve(id);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: null\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
assert!(response.headers.contains("access-control-allow-origin: null"), "Headers missing in {}", response.headers);
}
#[test]
fn should_add_cors_allow_origin_for_null_origin_when_all() {
let server = serve(|builder| builder.cors(DomainsValidation::Disabled));
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: null\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
assert!(response.headers.contains("access-control-allow-origin: null"), "Headers missing in {}", response.headers);
}
#[test]
fn should_not_allow_request_larger_than_max() {
let server = ServerBuilder::new(IoHandler::default())
.max_request_body_size(7)
.start_http(&"127.0.0.1:0".parse().unwrap())
.unwrap();
let response = request(server,
"\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Length: 8\r\n\
Content-Type: application/json\r\n\
\r\n\
12345678\r\n\
"
);
assert_eq!(response.status, "HTTP/1.1 413 Payload Too Large".to_owned());
}
#[test]
fn should_reject_invalid_hosts() {
let server = serve_hosts(vec!["superstring.ch".into()]);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, invalid_host());
}
#[test]
fn should_reject_missing_host() {
let server = serve_hosts(vec!["superstring.ch".into()]);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, invalid_host());
}
#[test]
fn should_allow_if_host_is_valid() {
let server = serve_hosts(vec!["superstring.ch".into()]);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: superstring.ch\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_respond_configured_allowed_hosts_to_options() {
let allowed = vec![
"X-Allowed".to_owned(),
"X-AlsoAllowed".to_owned(),
];
let custom = cors::AccessControlAllowHeaders::Only(allowed.clone());
let server = serve_allow_headers(custom);
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Access-Control-Request-Headers: {}\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
\r\n\
", &allowed.join(", "))
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
let expected = format!("access-control-allow-headers: {}", &allowed.join(", "));
assert!(response.headers.contains(&expected), "Headers missing in {}", response.headers);
}
#[test]
fn should_not_contain_default_cors_allow_headers() {
let server = serve(id);
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: 0\r\n\
\r\n\
")
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert!(!response.headers.contains("access-control-allow-headers:"),
"Header should not be in {}", response.headers);
}
#[test]
fn should_respond_valid_to_default_allowed_headers() {
let server = serve(id);
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
Access-Control-Request-Headers: Accept, Content-Type, Origin\r\n\
\r\n\
")
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
let expected = "access-control-allow-headers: Accept, Content-Type, Origin";
assert!(response.headers.contains(expected), "Headers missing in {}", response.headers);
}
#[test]
fn should_by_default_respond_valid_to_any_request_headers() {
let allowed = vec![
"X-Abc".to_owned(),
"X-123".to_owned(),
];
let custom = cors::AccessControlAllowHeaders::Only(allowed.clone());
let server = serve_allow_headers(custom);
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
Access-Control-Request-Headers: {}\r\n\
\r\n\
", &allowed.join(", "))
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
let expected = format!("access-control-allow-headers: {}", &allowed.join(", "));
assert!(response.headers.contains(&expected), "Headers missing in {}", response.headers);
}
#[test]
fn should_respond_valid_to_configured_allow_headers() {
let allowed = vec![
"X-Allowed".to_owned(),
"X-AlsoAllowed".to_owned(),
];
let custom = cors::AccessControlAllowHeaders::Only(allowed.clone());
let server = serve_allow_headers(custom);
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
Access-Control-Request-Headers: {}\r\n\
\r\n\
", &allowed.join(", "))
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
let expected = format!("access-control-allow-headers: {}", &allowed.join(", "));
assert!(response.headers.contains(&expected), "Headers missing in {}", response.headers);
}
#[test]
fn should_respond_invalid_if_non_allowed_header_used() {
let custom = cors::AccessControlAllowHeaders::Only(
vec![
"X-Allowed".to_owned(),
]);
let server = serve_allow_headers(custom);
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
X-Not-Allowed: not allowed\r\n\
\r\n\
")
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, cors_invalid_allow_headers());
}
#[test]
fn should_respond_valid_if_allowed_header_used() {
let custom = cors::AccessControlAllowHeaders::Only(
vec![
"X-Allowed".to_owned(),
]);
let server = serve_allow_headers(custom);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":1,"method":"hello"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
X-Allowed: Foobar\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world());
}
#[test]
fn should_respond_valid_if_case_insensitive_allowed_header_used() {
let custom = cors::AccessControlAllowHeaders::Only(
vec![
"X-Allowed".to_owned(),
]);
let server = serve_allow_headers(custom);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":1,"method":"hello"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
X-AlLoWed: Foobar\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world());
}
#[test]
fn should_respond_valid_on_case_mismatches_in_allowed_headers() {
let allowed = vec![
"X-Allowed".to_owned(),
"X-AlsoAllowed".to_owned(),
];
let custom = cors::AccessControlAllowHeaders::Only(allowed.clone());
let server = serve_allow_headers(custom);
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
Access-Control-Request-Headers: x-ALLoweD, x-alSOaLloWeD\r\n\
\r\n\
")
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
let contained = response.headers.contains(
"access-control-allow-headers: x-ALLoweD, x-alSOaLloWeD"
);
assert!(contained, "Headers missing in {}", response.headers);
}
#[test]
fn should_respond_valid_to_any_requested_header() {
let custom = cors::AccessControlAllowHeaders::Any;
let server = serve_allow_headers(custom);
let headers = "Something, Anything, Xyz, 123, _?";
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
Access-Control-Request-Headers: {}\r\n\
\r\n\
", headers)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
let expected = format!("access-control-allow-headers: {}", headers);
assert!(response.headers.contains(&expected), "Headers missing in {}", response.headers);
}
#[test]
fn should_forbid_invalid_request_headers() {
let custom = cors::AccessControlAllowHeaders::Only(
vec![
"X-Allowed".to_owned(),
]);
let server = serve_allow_headers(custom);
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
Access-Control-Request-Headers: *\r\n\
\r\n\
")
);
assert_eq!(response.status, "HTTP/1.1 403 Forbidden".to_owned());
assert_eq!(response.body, cors_invalid_allow_headers());
}
#[test]
fn should_respond_valid_to_wildcard_if_any_header_allowed() {
let server = serve_allow_headers(cors::AccessControlAllowHeaders::Any);
let response = request(server,
&format!("\
OPTIONS / HTTP/1.1\r\n\
Host: 127.0.0.1:8080\r\n\
Origin: http://superstring.ch\r\n\
Content-Length: 0\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
Access-Control-Request-Headers: *\r\n\
\r\n\
")
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert!(response.headers.contains("access-control-allow-headers: *"),
"Headers missing in {}", response.headers);
}
#[test]
fn should_allow_application_json_utf8() {
let server = serve_hosts(vec!["superstring.ch".into()]);
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: superstring.ch\r\n\
Connection: close\r\n\
Content-Type: application/json; charset=utf-8\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_always_allow_the_bind_address() {
let server = serve_hosts(vec!["superstring.ch".into()]);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: {}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr, req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_always_allow_the_bind_address_as_localhost() {
let server = serve_hosts(vec![]);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, method_not_found());
}
#[test]
fn should_handle_sync_requests_correctly() {
let server = serve(id);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":1,"method":"hello"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world());
}
#[test]
fn should_handle_async_requests_with_immediate_response_correctly() {
let server = serve(id);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":1,"method":"hello_async"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world());
}
#[test]
fn should_handle_async_requests_correctly() {
let server = serve(id);
let addr = server.address().clone();
let req = r#"{"jsonrpc":"2.0","id":1,"method":"hello_async2"}"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world());
}
#[test]
fn should_handle_sync_batch_requests_correctly() {
let server = serve(id);
let addr = server.address().clone();
let req = r#"[{"jsonrpc":"2.0","id":1,"method":"hello"}]"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world_batch());
}
#[test]
fn should_handle_rest_request_with_params() {
let server = serve(id);
let addr = server.address().clone();
let req = "";
let response = request(server,
&format!("\
POST /hello/5 HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world_5());
}
#[test]
fn should_handle_rest_request_with_case_insensitive_content_type() {
let server = serve(id);
let addr = server.address().clone();
let req = "";
let response = request(server,
&format!("\
POST /hello/5 HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: Application/JSON; charset=UTF-8\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world_5());
}
#[test]
fn should_return_error_in_case_of_unsecure_rest_and_no_method() {
let server = serve(|builder| builder.rest_api(RestApi::Unsecure));
let addr = server.address().clone();
let req = "";
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert_eq!(response.status, "HTTP/1.1 415 Unsupported Media Type".to_owned());
assert_eq!(&response.body, "Supplied content type is not allowed. Content-Type: application/json is required\n");
}
#[test]
fn should_return_connection_header() {
let server = serve(id);
let addr = server.address().clone();
let req = r#"[{"jsonrpc":"2.0","id":1,"method":"hello"}]"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: close\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert!(response.headers.contains("connection: close"),
"Headers missing in {}", response.headers);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world_batch());
}
#[test]
fn should_close_connection_without_keep_alive() {
let server = serve(|builder| builder.keep_alive(false));
let addr = server.address().clone();
let req = r#"[{"jsonrpc":"2.0","id":1,"method":"hello"}]"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert!(response.headers.contains("connection: close"),
"Header missing in {}", response.headers);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world_batch());
}
#[test]
fn should_respond_with_close_even_if_client_wants_to_keep_alive() {
let server = serve(|builder| builder.keep_alive(false));
let addr = server.address().clone();
let req = r#"[{"jsonrpc":"2.0","id":1,"method":"hello"}]"#;
let response = request(server,
&format!("\
POST / HTTP/1.1\r\n\
Host: localhost:{}\r\n\
Connection: keep-alive\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
\r\n\
{}\r\n\
", addr.port(), req.as_bytes().len(), req)
);
assert!(response.headers.contains("connection: close"),
"Headers missing in {}", response.headers);
assert_eq!(response.status, "HTTP/1.1 200 OK".to_owned());
assert_eq!(response.body, world_batch());
}
fn invalid_host() -> String {
"Provided Host header is not whitelisted.\n".into()
}
fn cors_invalid_allow_origin() -> String {
"Origin of the request is not whitelisted. CORS headers would not be sent and any side-effects were cancelled as well.\n".into()
}
fn cors_invalid_allow_headers() -> String {
"Requested headers are not allowed for CORS. CORS headers would not be sent and any side-effects were cancelled as well.\n".into()
}
fn method_not_found() -> String {
"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32601,\"message\":\"Method not found\"},\"id\":1}\n".into()
}
fn invalid_request() -> String {
"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32600,\"message\":\"Invalid request\"},\"id\":null}\n".into()
}
fn world() -> String {
"{\"jsonrpc\":\"2.0\",\"result\":\"world\",\"id\":1}\n".into()
}
fn world_5() -> String {
"{\"jsonrpc\":\"2.0\",\"result\":\"world: 5\",\"id\":1}\n".into()
}
fn world_batch() -> String {
"[{\"jsonrpc\":\"2.0\",\"result\":\"world\",\"id\":1}]\n".into()
}