1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-27 17:22:57 +01:00

refactor and enable some tests for staticfiles

This commit is contained in:
Nikolay Kim 2019-03-05 22:10:08 -08:00
parent 1a80b70868
commit 6efc3438b8
18 changed files with 2198 additions and 2063 deletions

View File

@ -35,7 +35,7 @@ before_script:
script:
- cargo clean
- cargo test -- --nocapture
- cargo test --all -- --nocapture
# Upload docs
after_success:
@ -49,7 +49,7 @@ after_success:
fi
- |
if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-03-02" ]]; then
cargo tarpaulin --out Xml
cargo tarpaulin --out Xml --all
bash <(curl -s https://codecov.io/bash)
echo "Uploaded code coverage"
fi

View File

@ -28,7 +28,7 @@ path = "src/lib.rs"
members = [
".",
"actix-session",
"staticfiles",
"actix-staticfiles",
]
[package.metadata.docs.rs]

View File

@ -20,7 +20,8 @@ path = "src/lib.rs"
[dependencies]
actix-web = { path=".." }
actix-http = { git = "https://github.com/actix/actix-http.git" }
actix-service = "0.3.0"
actix-service = { git = "https://github.com/actix/actix-net.git" }
#actix-service = "0.3.0"
bytes = "0.4"
futures = "0.1"

View File

@ -0,0 +1,70 @@
use actix_http::http::header::DispositionType;
use actix_web::http::Method;
use mime;
/// Describes `StaticFiles` configiration
///
/// To configure actix's static resources you need
/// to define own configiration type and implement any method
/// you wish to customize.
/// As trait implements reasonable defaults for Actix.
///
/// ## Example
///
/// ```rust,ignore
/// extern crate mime;
/// extern crate actix_web;
/// use actix_web::http::header::DispositionType;
/// use actix_web::fs::{StaticFileConfig, NamedFile};
///
/// #[derive(Default)]
/// struct MyConfig;
///
/// impl StaticFileConfig for MyConfig {
/// fn content_disposition_map(typ: mime::Name) -> DispositionType {
/// DispositionType::Attachment
/// }
/// }
///
/// let file = NamedFile::open_with_config("foo.txt", MyConfig);
/// ```
pub trait StaticFileConfig: Default {
///Describes mapping for mime type to content disposition header
///
///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline.
///Others are mapped to Attachment
fn content_disposition_map(typ: mime::Name) -> DispositionType {
match typ {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment,
}
}
///Describes whether Actix should attempt to calculate `ETag`
///
///Defaults to `true`
fn is_use_etag() -> bool {
true
}
///Describes whether Actix should use last modified date of file.
///
///Defaults to `true`
fn is_use_last_modifier() -> bool {
true
}
///Describes allowed methods to access static resources.
///
///By default all methods are allowed
fn is_method_allowed(_method: &Method) -> bool {
true
}
}
///Default content disposition as described in
///[StaticFileConfig](trait.StaticFileConfig.html)
#[derive(Default)]
pub struct DefaultConfig;
impl StaticFileConfig for DefaultConfig {}

View File

@ -0,0 +1,41 @@
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use derive_more::Display;
/// Errors which can occur when serving static files.
#[derive(Display, Debug, PartialEq)]
pub enum StaticFilesError {
/// Path is not a directory
#[display(fmt = "Path is not a directory. Unable to serve static files")]
IsNotDirectory,
/// Cannot render directory
#[display(fmt = "Unable to render directory without index file")]
IsDirectory,
}
/// Return `NotFound` for `StaticFilesError`
impl ResponseError for StaticFilesError {
fn error_response(&self) -> HttpResponse {
HttpResponse::new(StatusCode::NOT_FOUND)
}
}
#[derive(Display, Debug, PartialEq)]
pub enum UriSegmentError {
/// The segment started with the wrapped invalid character.
#[display(fmt = "The segment started with the wrapped invalid character")]
BadStart(char),
/// The segment contained the wrapped invalid character.
#[display(fmt = "The segment contained the wrapped invalid character")]
BadChar(char),
/// The segment ended with the wrapped invalid character.
#[display(fmt = "The segment ended with the wrapped invalid character")]
BadEnd(char),
}
/// Return `BadRequest` for `UriSegmentError`
impl ResponseError for UriSegmentError {
fn error_response(&self) -> HttpResponse {
HttpResponse::new(StatusCode::BAD_REQUEST)
}
}

1482
actix-staticfiles/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,438 @@
use std::fs::{File, Metadata};
use std::io;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use mime;
use mime_guess::guess_mime_type;
use actix_http::error::Error;
use actix_http::http::header::{self, ContentDisposition, DispositionParam};
use actix_web::http::{ContentEncoding, Method, StatusCode};
use actix_web::{HttpMessage, HttpRequest, HttpResponse, Responder};
use crate::config::{DefaultConfig, StaticFileConfig};
use crate::{ChunkedReadFile, HttpRange};
/// A file with an associated name.
#[derive(Debug)]
pub struct NamedFile<C = DefaultConfig> {
path: PathBuf,
file: File,
pub(crate) content_type: mime::Mime,
pub(crate) content_disposition: header::ContentDisposition,
pub(crate) md: Metadata,
modified: Option<SystemTime>,
encoding: Option<ContentEncoding>,
pub(crate) status_code: StatusCode,
_cd_map: PhantomData<C>,
}
impl NamedFile {
/// Creates an instance from a previously opened file.
///
/// The given `path` need not exist and is only used to determine the `ContentType` and
/// `ContentDisposition` headers.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate actix_web;
///
/// use actix_web::fs::NamedFile;
/// use std::io::{self, Write};
/// use std::env;
/// use std::fs::File;
///
/// fn main() -> io::Result<()> {
/// let mut file = File::create("foo.txt")?;
/// file.write_all(b"Hello, world!")?;
/// let named_file = NamedFile::from_file(file, "bar.txt")?;
/// Ok(())
/// }
/// ```
pub fn from_file<P: AsRef<Path>>(file: File, path: P) -> io::Result<NamedFile> {
Self::from_file_with_config(file, path, DefaultConfig)
}
/// Attempts to open a file in read-only mode.
///
/// # Examples
///
/// ```rust,ignore
/// use actix_web::fs::NamedFile;
///
/// let file = NamedFile::open("foo.txt");
/// ```
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
Self::open_with_config(path, DefaultConfig)
}
}
impl<C: StaticFileConfig> NamedFile<C> {
/// Creates an instance from a previously opened file using the provided configuration.
///
/// The given `path` need not exist and is only used to determine the `ContentType` and
/// `ContentDisposition` headers.
///
/// # Examples
///
/// ```rust,ignore
/// extern crate actix_web;
///
/// use actix_web::fs::{DefaultConfig, NamedFile};
/// use std::io::{self, Write};
/// use std::env;
/// use std::fs::File;
///
/// fn main() -> io::Result<()> {
/// let mut file = File::create("foo.txt")?;
/// file.write_all(b"Hello, world!")?;
/// let named_file = NamedFile::from_file_with_config(file, "bar.txt", DefaultConfig)?;
/// Ok(())
/// }
/// ```
pub fn from_file_with_config<P: AsRef<Path>>(
file: File,
path: P,
_: C,
) -> io::Result<NamedFile<C>> {
let path = path.as_ref().to_path_buf();
// Get the name of the file and use it to construct default Content-Type
// and Content-Disposition values
let (content_type, content_disposition) = {
let filename = match path.file_name() {
Some(name) => name.to_string_lossy(),
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Provided path has no filename",
));
}
};
let ct = guess_mime_type(&path);
let disposition_type = C::content_disposition_map(ct.type_());
let cd = ContentDisposition {
disposition: disposition_type,
parameters: vec![DispositionParam::Filename(filename.into_owned())],
};
(ct, cd)
};
let md = file.metadata()?;
let modified = md.modified().ok();
let encoding = None;
Ok(NamedFile {
path,
file,
content_type,
content_disposition,
md,
modified,
encoding,
status_code: StatusCode::OK,
_cd_map: PhantomData,
})
}
/// Attempts to open a file in read-only mode using provided configuration.
///
/// # Examples
///
/// ```rust,ignore
/// use actix_web::fs::{DefaultConfig, NamedFile};
///
/// let file = NamedFile::open_with_config("foo.txt", DefaultConfig);
/// ```
pub fn open_with_config<P: AsRef<Path>>(
path: P,
config: C,
) -> io::Result<NamedFile<C>> {
Self::from_file_with_config(File::open(&path)?, path, config)
}
/// Returns reference to the underlying `File` object.
#[inline]
pub fn file(&self) -> &File {
&self.file
}
/// Retrieve the path of this file.
///
/// # Examples
///
/// ```rust,ignore
/// # use std::io;
/// use actix_web::fs::NamedFile;
///
/// # fn path() -> io::Result<()> {
/// let file = NamedFile::open("test.txt")?;
/// assert_eq!(file.path().as_os_str(), "foo.txt");
/// # Ok(())
/// # }
/// ```
#[inline]
pub fn path(&self) -> &Path {
self.path.as_path()
}
/// Set response **Status Code**
pub fn set_status_code(mut self, status: StatusCode) -> Self {
self.status_code = status;
self
}
/// Set the MIME Content-Type for serving this file. By default
/// the Content-Type is inferred from the filename extension.
#[inline]
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
self.content_type = mime_type;
self
}
/// Set the Content-Disposition for serving this file. This allows
/// changing the inline/attachment disposition as well as the filename
/// sent to the peer. By default the disposition is `inline` for text,
/// image, and video content types, and `attachment` otherwise, and
/// the filename is taken from the path provided in the `open` method
/// after converting it to UTF-8 using
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy).
#[inline]
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
self.content_disposition = cd;
self
}
/// Set content encoding for serving this file
#[inline]
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
self.encoding = Some(enc);
self
}
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
// This etag format is similar to Apache's.
self.modified.as_ref().map(|mtime| {
let ino = {
#[cfg(unix)]
{
self.md.ino()
}
#[cfg(not(unix))]
{
0
}
};
let dur = mtime
.duration_since(UNIX_EPOCH)
.expect("modification time must be after epoch");
header::EntityTag::strong(format!(
"{:x}:{:x}:{:x}:{:x}",
ino,
self.md.len(),
dur.as_secs(),
dur.subsec_nanos()
))
})
}
pub(crate) fn last_modified(&self) -> Option<header::HttpDate> {
self.modified.map(|mtime| mtime.into())
}
}
impl<C> Deref for NamedFile<C> {
type Target = File;
fn deref(&self) -> &File {
&self.file
}
}
impl<C> DerefMut for NamedFile<C> {
fn deref_mut(&mut self) -> &mut File {
&mut self.file
}
}
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfMatch>() {
None | Some(header::IfMatch::Any) => true,
Some(header::IfMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
if item.strong_eq(some_etag) {
return true;
}
}
}
false
}
}
}
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfNoneMatch>() {
Some(header::IfNoneMatch::Any) => false,
Some(header::IfNoneMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
if item.weak_eq(some_etag) {
return false;
}
}
}
true
}
None => true,
}
}
impl<C: StaticFileConfig> Responder for NamedFile<C> {
type Error = Error;
type Future = Result<HttpResponse, Error>;
fn respond_to(self, req: &HttpRequest) -> Self::Future {
if self.status_code != StatusCode::OK {
let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
// TODO blocking by compressing
// if let Some(current_encoding) = self.encoding {
// resp.content_encoding(current_encoding);
// }
let reader = ChunkedReadFile {
size: self.md.len(),
offset: 0,
file: Some(self.file),
fut: None,
counter: 0,
};
return Ok(resp.streaming(reader));
}
if !C::is_method_allowed(req.method()) {
return Ok(HttpResponse::MethodNotAllowed()
.header(header::CONTENT_TYPE, "text/plain")
.header(header::ALLOW, "GET, HEAD")
.body("This resource only supports GET and HEAD."));
}
let etag = if C::is_use_etag() { self.etag() } else { None };
let last_modified = if C::is_use_last_modifier() {
self.last_modified()
} else {
None
};
// check preconditions
let precondition_failed = if !any_match(etag.as_ref(), req) {
true
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
(last_modified, req.get_header())
{
m > since
} else {
false
};
// check last modified
let not_modified = if !none_match(etag.as_ref(), req) {
true
} else if req.headers().contains_key(header::IF_NONE_MATCH) {
false
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
(last_modified, req.get_header())
{
m <= since
} else {
false
};
let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone()))
.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
);
// TODO blocking by compressing
// if let Some(current_encoding) = self.encoding {
// resp.content_encoding(current_encoding);
// }
resp.if_some(last_modified, |lm, resp| {
resp.set(header::LastModified(lm));
})
.if_some(etag, |etag, resp| {
resp.set(header::ETag(etag));
});
resp.header(header::ACCEPT_RANGES, "bytes");
let mut length = self.md.len();
let mut offset = 0;
// check for range header
if let Some(ranges) = req.headers().get(header::RANGE) {
if let Ok(rangesheader) = ranges.to_str() {
if let Ok(rangesvec) = HttpRange::parse(rangesheader, length) {
length = rangesvec[0].length;
offset = rangesvec[0].start;
// TODO blocking by compressing
// resp.content_encoding(ContentEncoding::Identity);
resp.header(
header::CONTENT_RANGE,
format!(
"bytes {}-{}/{}",
offset,
offset + length - 1,
self.md.len()
),
);
} else {
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
};
} else {
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
};
};
resp.header(header::CONTENT_LENGTH, format!("{}", length));
if precondition_failed {
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
} else if not_modified {
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
}
if *req.method() == Method::HEAD {
Ok(resp.finish())
} else {
let reader = ChunkedReadFile {
offset,
size: length,
file: Some(self.file),
fut: None,
counter: 0,
};
if offset != 0 || length != self.md.len() {
return Ok(resp.status(StatusCode::PARTIAL_CONTENT).streaming(reader));
};
Ok(resp.streaming(reader))
}
}
}

View File

@ -0,0 +1 @@
ÂTÇÉVù2þvI ª\ÇRË™ˆæeÞ<04>vDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ. <0C>û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI

View File

@ -0,0 +1 @@
ÂTÇÉVù2þvI ª\ÇRË™ˆæeÞ<04>vDØ:è—½¬RVÖYpíÿ;ÍÏGñùp!2÷CŒ. <0C>û®õpA !ûߦÙx j+Uc÷±©X”c%Û;ï"yì­AI

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

View File

@ -2,7 +2,6 @@ use futures::IntoFuture;
use actix_web::{
http::Method, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer,
Resource, Route,
};
fn index(req: HttpRequest) -> &'static str {
@ -29,19 +28,17 @@ fn main() -> std::io::Result<()> {
.middleware(middleware::DefaultHeaders::new().header("X-Version", "0.2"))
.middleware(middleware::Compress::default())
.resource("/resource1/index.html", |r| r.route(web::get().to(index)))
.service(
"/resource2/index.html",
Resource::new()
.middleware(
middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"),
)
.default_resource(|r| {
r.route(Route::new().to(|| HttpResponse::MethodNotAllowed()))
})
.route(web::method(Method::GET).to_async(index_async)),
)
.service("/test1.html", Resource::new().to(|| "Test\r\n"))
.service("/", Resource::new().to(no_params))
.resource("/resource2/index.html", |r| {
r.middleware(
middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"),
)
.default_resource(|r| {
r.route(web::route().to(|| HttpResponse::MethodNotAllowed()))
})
.route(web::method(Method::GET).to_async(index_async))
})
.resource("/test1.html", |r| r.to(|| "Test\r\n"))
.resource("/", |r| r.to(no_params))
})
.bind("127.0.0.1:8080")?
.workers(1)

View File

@ -24,8 +24,13 @@ type HttpService<P> = BoxedService<ServiceRequest<P>, ServiceResponse, ()>;
type HttpNewService<P> = BoxedNewService<(), ServiceRequest<P>, ServiceResponse, (), ()>;
type BoxedResponse = Box<Future<Item = ServiceResponse, Error = ()>>;
pub trait HttpServiceFactory<Request> {
type Factory: NewService<Request>;
pub trait HttpServiceFactory<P> {
type Factory: NewService<
ServiceRequest<P>,
Response = ServiceResponse,
Error = (),
InitError = (),
>;
fn rdef(&self) -> &ResourceDef;
@ -293,6 +298,29 @@ where
}
}
/// Register resource handler service.
pub fn service<F>(self, service: F) -> AppRouter<T, P, Body, AppEntry<P>>
where
F: HttpServiceFactory<P> + 'static,
{
let fref = Rc::new(RefCell::new(None));
AppRouter {
chain: self.chain,
services: vec![(
service.rdef().clone(),
boxed::new_service(service.create().map_init_err(|_| ())),
None,
)],
default: None,
defaults: vec![],
endpoint: AppEntry::new(fref.clone()),
factory_ref: fref,
extensions: self.extensions,
state: self.state,
_t: PhantomData,
}
}
/// Set server host name.
///
/// Host name is used by application router aa a hostname for url
@ -445,16 +473,15 @@ where
}
/// Register resource handler service.
pub fn service<R, F, U>(mut self, rdef: R, factory: F) -> Self
pub fn service<F>(mut self, factory: F) -> Self
where
R: Into<ResourceDef>,
F: IntoNewService<U, ServiceRequest<P>>,
U: NewService<ServiceRequest<P>, Response = ServiceResponse, Error = ()>
+ 'static,
F: HttpServiceFactory<P> + 'static,
{
let rdef = factory.rdef().clone();
self.services.push((
rdef.into(),
boxed::new_service(factory.into_new_service().map_init_err(|_| ())),
rdef,
boxed::new_service(factory.create().map_init_err(|_| ())),
None,
));
self

View File

@ -19,9 +19,9 @@ pub mod test;
// re-export for convenience
pub use actix_http::Response as HttpResponse;
pub use actix_http::{error, http, Error, HttpMessage, ResponseError, Result};
pub use actix_http::{body, error, http, Error, HttpMessage, ResponseError, Result};
pub use crate::app::{App, AppRouter};
pub use crate::app::App;
pub use crate::extract::{FromRequest, Json};
pub use crate::request::HttpRequest;
pub use crate::resource::Resource;
@ -32,6 +32,26 @@ pub use crate::server::HttpServer;
pub use crate::service::{ServiceFromRequest, ServiceRequest, ServiceResponse};
pub use crate::state::State;
pub mod dev {
//! The `actix-web` prelude for library developers
//!
//! The purpose of this module is to alleviate imports of many common actix
//! traits by adding a glob import to the top of actix heavy modules:
//!
//! ```
//! # #![allow(unused_imports)]
//! use actix_web::dev::*;
//! ```
pub use crate::app::{AppRouter, HttpServiceFactory};
pub use actix_http::body::{Body, MessageBody, ResponseBody};
pub use actix_http::dev::ResponseBuilder as HttpResponseBuilder;
pub use actix_http::{
Extensions, Payload, PayloadStream, RequestHead, ResponseHead,
};
pub use actix_router::{Path, ResourceDef, Url};
}
pub mod web {
use actix_http::{http::Method, Error, Response};
use futures::IntoFuture;

View File

@ -1,8 +1,9 @@
use std::borrow::Cow;
use std::cell::{Ref, RefMut};
use std::fmt;
use std::rc::Rc;
use actix_http::body::{Body, ResponseBody};
use actix_http::body::{Body, MessageBody, ResponseBody};
use actix_http::http::{HeaderMap, Method, Uri, Version};
use actix_http::{
Error, Extensions, HttpMessage, Payload, PayloadStream, Request, RequestHead,
@ -123,6 +124,11 @@ impl<P> ServiceRequest<P> {
pub fn app_extensions(&self) -> &Extensions {
self.req.app_extensions()
}
/// Deconstruct request into parts
pub fn into_parts(self) -> (HttpRequest, Payload<P>) {
(self.req, self.payload)
}
}
impl<P> Resource<Url> for ServiceRequest<P> {
@ -172,6 +178,29 @@ impl<P> std::ops::DerefMut for ServiceRequest<P> {
}
}
impl<P> fmt::Debug for ServiceRequest<P> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(
f,
"\nServiceRequest {:?} {}:{}",
self.head().version,
self.head().method,
self.path()
)?;
if !self.query_string().is_empty() {
writeln!(f, " query: ?{:?}", self.query_string())?;
}
if !self.match_info().is_empty() {
writeln!(f, " params: {:?}", self.match_info())?;
}
writeln!(f, " headers:")?;
for (key, val) in self.headers().iter() {
writeln!(f, " {:?}: {:?}", key, val)?;
}
Ok(())
}
}
pub struct ServiceFromRequest<P> {
req: HttpRequest,
payload: Payload<P>,
@ -259,6 +288,16 @@ impl<B> ServiceResponse<B> {
ServiceResponse { request, response }
}
/// Create service response from the error
pub fn from_err<E: Into<Error>>(err: E, request: HttpRequest) -> Self {
let e: Error = err.into();
let res: Response = e.into();
ServiceResponse {
request,
response: res.into_body(),
}
}
/// Get reference to original request
#[inline]
pub fn request(&self) -> &HttpRequest {
@ -303,6 +342,11 @@ impl<B> ServiceResponse<B> {
}
}
}
/// Extract response body
pub fn take_body(&mut self) -> ResponseBody<B> {
self.response.take_body()
}
}
impl<B> ServiceResponse<B> {
@ -349,3 +393,21 @@ impl<B> IntoFuture for ServiceResponse<B> {
ok(self)
}
}
impl<B: MessageBody> fmt::Debug for ServiceResponse<B> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let res = writeln!(
f,
"\nServiceResponse {:?} {}{}",
self.response.head().version,
self.response.head().status,
self.response.head().reason.unwrap_or(""),
);
let _ = writeln!(f, " headers:");
for (key, val) in self.response.head().headers.iter() {
let _ = writeln!(f, " {:?}: {:?}", key, val);
}
let _ = writeln!(f, " body: {:?}", self.response.body().length());
res
}
}

View File

@ -70,6 +70,34 @@ where
block_on(app.into_new_service().new_service(&())).unwrap()
}
/// Calls service and waits for response future completion.
///
/// ```rust,ignore
/// use actix_web::{test, App, HttpResponse, http::StatusCode};
/// use actix_service::Service;
///
/// fn main() {
/// let mut app = test::init_service(
/// App::new()
/// .resource("/test", |r| r.to(|| HttpResponse::Ok()))
/// );
///
/// // Create request object
/// let req = test::TestRequest::with_uri("/test").to_request();
///
/// // Call application
/// let resp = test::call_succ_service(&mut app, req);
/// assert_eq!(resp.status(), StatusCode::OK);
/// }
/// ```
pub fn call_success<S, R, B, E>(app: &mut S, req: R) -> S::Response
where
S: Service<R, Response = ServiceResponse<B>, Error = E>,
E: std::fmt::Debug,
{
block_on(app.call(req)).unwrap()
}
/// Test `Request` builder.
///
/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern.

File diff suppressed because it is too large Load Diff