Skip to content
This repository has been archived by the owner on Nov 6, 2020. It is now read-only.

Additional fetch tests #3983

Merged
merged 2 commits into from
Dec 28, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added dapps/res/gavcoin.zip
Binary file not shown.
191 changes: 186 additions & 5 deletions dapps/src/tests/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
use devtools::http_client;
use rustc_serialize::hex::FromHex;
use tests::helpers::{
serve_with_registrar, serve_with_registrar_and_sync, serve_with_registrar_and_fetch, serve_with_fetch,
serve_with_registrar, serve_with_registrar_and_sync, serve_with_fetch,
serve_with_registrar_and_fetch, serve_with_registrar_and_fetch_and_threads,
request, assert_security_headers_for_embed,
};

Expand Down Expand Up @@ -128,6 +129,70 @@ fn should_return_error_for_invalid_dapp_zip() {
assert_security_headers_for_embed(&response.headers);
}

#[test]
fn should_return_fetched_dapp_content() {
// given
let (server, fetch, registrar) = serve_with_registrar_and_fetch();
let gavcoin = GAVCOIN_DAPP.from_hex().unwrap();
registrar.set_result(
"9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a".parse().unwrap(),
Ok(gavcoin.clone())
);
fetch.set_response(include_bytes!("../../res/gavcoin.zip"));

// when
let response1 = http_client::request(server.addr(),
"\
GET /index.html HTTP/1.1\r\n\
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\
Connection: close\r\n\
\r\n\
"
);
let response2 = http_client::request(server.addr(),
"\
GET /manifest.json HTTP/1.1\r\n\
Host: 9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a.parity\r\n\
Connection: close\r\n\
\r\n\
"
);

// then
assert_eq!(registrar.calls.lock().len(), 4);

fetch.assert_requested("https://codeload.github.com/gavofyork/gavcoin/zip/9faf32e1e3845e237cc6efd27187cee13b3b99db");
fetch.assert_no_more_requests();

response1.assert_status("HTTP/1.1 200 OK");
assert_security_headers_for_embed(&response1.headers);
assert_eq!(
response1.body,
r#"18
<h1>Hello Gavcoin!</h1>

"#
);

response2.assert_status("HTTP/1.1 200 OK");
assert_security_headers_for_embed(&response2.headers);
assert_eq!(
response2.body,
r#"BE
{
"id": "9c94e154dab8acf859b30ee80fc828fb1d38359d938751b65db71d460588d82a",
"name": "Gavcoin",
"description": "Gavcoin",
"version": "1.0.0",
"author": "",
"iconUrl": "icon.png"
}
0

"#
);
}

#[test]
fn should_return_fetched_content() {
// given
Expand Down Expand Up @@ -187,6 +252,52 @@ fn should_cache_content() {
response.assert_status("HTTP/1.1 200 OK");
}

#[test]
fn should_not_request_content_twice() {
use std::thread;

// given
let (server, fetch, registrar) = serve_with_registrar_and_fetch_and_threads(true);
let gavcoin = GAVCOIN_ICON.from_hex().unwrap();
registrar.set_result(
"2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e".parse().unwrap(),
Ok(gavcoin.clone())
);
let request_str = "\
GET / HTTP/1.1\r\n\
Host: 2be00befcf008bc0e7d9cdefc194db9c75352e8632f48498b5a6bfce9f02c88e.parity\r\n\
Connection: close\r\n\
\r\n\
";
let fire_request = || {
let addr = server.addr().to_owned();
let req = request_str.to_owned();
thread::spawn(move || {
http_client::request(&addr, &req)
})
};
let control = fetch.manual();

// when

// Fire two requests at the same time
let r1 = fire_request();
let r2 = fire_request();

// wait for single request in fetch, the second one should go into waiting state.
control.wait_for_requests(1);
control.respond();

let response1 = r1.join().unwrap();
let response2 = r2.join().unwrap();

// then
fetch.assert_requested("https://raw.githubusercontent.com/ethcore/dapp-assets/b88e983abaa1a6a6345b8d9448c15b117ddb540e/tokens/gavcoin-64x64.png");
fetch.assert_no_more_requests();
response1.assert_status("HTTP/1.1 200 OK");
response2.assert_status("HTTP/1.1 200 OK");
}

#[test]
fn should_stream_web_content() {
// given
Expand All @@ -195,7 +306,7 @@ fn should_stream_web_content() {
// when
let response = request(server,
"\
GET /web/token/https/ethcore.io/ HTTP/1.1\r\n\
GET /web/token/https/parity.io/ HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
\r\n\
Expand All @@ -206,7 +317,7 @@ fn should_stream_web_content() {
response.assert_status("HTTP/1.1 200 OK");
assert_security_headers_for_embed(&response.headers);

fetch.assert_requested("https://ethcore.io/");
fetch.assert_requested("https://parity.io/");
fetch.assert_no_more_requests();
}

Expand All @@ -218,7 +329,7 @@ fn should_return_error_on_invalid_token() {
// when
let response = request(server,
"\
GET /web/invalidtoken/https/ethcore.io/ HTTP/1.1\r\n\
GET /web/invalidtoken/https/parity.io/ HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
\r\n\
Expand All @@ -240,7 +351,7 @@ fn should_return_error_on_invalid_protocol() {
// when
let response = request(server,
"\
GET /web/token/ftp/ethcore.io/ HTTP/1.1\r\n\
GET /web/token/ftp/parity.io/ HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
\r\n\
Expand All @@ -253,3 +364,73 @@ fn should_return_error_on_invalid_protocol() {

fetch.assert_no_more_requests();
}

#[test]
fn should_redirect_if_trailing_slash_is_missing() {
// given
let (server, fetch) = serve_with_fetch("token");

// when
let response = request(server,
"\
GET /web/token/https/parity.io HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
\r\n\
"
);

// then
response.assert_status("HTTP/1.1 302 Found");
response.assert_header("Location", "/web/token/https/parity.io/");

fetch.assert_no_more_requests();
}

#[test]
fn should_disallow_non_get_requests() {
// given
let (server, fetch) = serve_with_fetch("token");

// when
let response = request(server,
"\
POST /token/https/parity.io/ HTTP/1.1\r\n\
Host: web.parity\r\n\
Content-Type: application/json\r\n\
Connection: close\r\n\
\r\n\
123\r\n\
\r\n\
"
);

// then
response.assert_status("HTTP/1.1 405 Method Not Allowed");
assert_security_headers_for_embed(&response.headers);

fetch.assert_no_more_requests();
}

#[test]
fn should_fix_absolute_requests_based_on_referer() {
// given
let (server, fetch) = serve_with_fetch("token");

// when
let response = request(server,
"\
GET /styles.css HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Connection: close\r\n\
Referer: http://localhost:8080/web/token/https/parity.io/\r\n\
\r\n\
"
);

// then
response.assert_status("HTTP/1.1 302 Found");
response.assert_header("Location", "/web/token/https/parity.io/styles.css");

fetch.assert_no_more_requests();
}
68 changes: 63 additions & 5 deletions dapps/src/tests/helpers/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,71 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.

use std::{io, thread};
use std::sync::Arc;
use std::sync::atomic::{self, AtomicUsize};
use std::{io, thread, time};
use std::sync::{atomic, mpsc, Arc};
use util::Mutex;

use futures::{self, Future};
use fetch::{self, Fetch};

pub struct FetchControl {
sender: mpsc::Sender<()>,
fetch: FakeFetch,
}

impl FetchControl {
pub fn respond(self) {
self.sender.send(())
.expect("Fetch cannot be finished without sending a response at least once.");
}

pub fn wait_for_requests(&self, len: usize) {
const MAX_TIMEOUT_MS: u64 = 5000;
const ATTEMPTS: u64 = 10;
let mut attempts_left = ATTEMPTS;
loop {
let current = self.fetch.requested.lock().len();

if current == len {
break;
} else if attempts_left == 0 {
panic!(
"Timeout reached when waiting for pending requests. Expected: {}, current: {}",
len, current
);
} else {
attempts_left -= 1;
// Should we handle spurious timeouts better?
thread::park_timeout(time::Duration::from_millis(MAX_TIMEOUT_MS / ATTEMPTS));
}
}
}
}

#[derive(Clone, Default)]
pub struct FakeFetch {
asserted: Arc<AtomicUsize>,
manual: Arc<Mutex<Option<mpsc::Receiver<()>>>>,
response: Arc<Mutex<Option<&'static [u8]>>>,
asserted: Arc<atomic::AtomicUsize>,
requested: Arc<Mutex<Vec<String>>>,
}

impl FakeFetch {
pub fn set_response(&self, data: &'static [u8]) {
*self.response.lock() = Some(data);
}

pub fn manual(&self) -> FetchControl {
assert!(self.manual.lock().is_none(), "Only one manual control may be active.");
let (tx, rx) = mpsc::channel();
*self.manual.lock() = Some(rx);

FetchControl {
sender: tx,
fetch: self.clone(),
}
}

pub fn assert_requested(&self, url: &str) {
let requests = self.requested.lock();
let idx = self.asserted.fetch_add(1, atomic::Ordering::SeqCst);
Expand All @@ -52,10 +102,18 @@ impl Fetch for FakeFetch {

fn fetch_with_abort(&self, url: &str, _abort: fetch::Abort) -> Self::Result {
self.requested.lock().push(url.into());
let manual = self.manual.clone();
let response = self.response.clone();

let (tx, rx) = futures::oneshot();
thread::spawn(move || {
let cursor = io::Cursor::new(b"Some content");
if let Some(rx) = manual.lock().take() {
// wait for manual resume
let _ = rx.recv();
}

let data = response.lock().take().unwrap_or(b"Some content");
let cursor = io::Cursor::new(data);
tx.complete(fetch::Response::from_reader(cursor));
});

Expand Down
Loading