Compare commits

...

4 commits

Author SHA1 Message Date
9403e36dce tests: add tests for page endpoint
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-03-04 03:09:17 +01:00
b4529662c0 tests: add file_to_response tests 2023-03-04 02:34:52 +01:00
949159a9eb feat: add x-content-type-options=nosniff header 2023-03-03 20:13:54 +01:00
0b35d88244 feat: make html compression configurable 2023-03-03 20:01:04 +01:00
13 changed files with 541 additions and 152 deletions

109
Cargo.lock generated
View file

@ -90,6 +90,28 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "async-stream"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad445822218ce64be7a341abfb0b1ea43b5c23aa83902542a4542e78309d8e5e"
dependencies = [
"async-stream-impl",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-stream-impl"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4655ae1a7b0cdf149156f780c5bf3f1352bc53cbd9e0a361a7ef7b22947e965"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.64" version = "0.1.64"
@ -173,6 +195,12 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.4.0" version = "1.4.0"
@ -648,6 +676,18 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "futures_codec"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce54d63f8b0c75023ed920d46fd71d0cbbb830b0ee012726b5b4f506fb6dea5b"
dependencies = [
"bytes 0.5.6",
"futures",
"memchr",
"pin-project",
]
[[package]] [[package]]
name = "fxhash" name = "fxhash"
version = "0.2.1" version = "0.2.1"
@ -694,7 +734,7 @@ version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
dependencies = [ dependencies = [
"bytes", "bytes 1.4.0",
"fnv", "fnv",
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@ -721,7 +761,7 @@ checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"bitflags", "bitflags",
"bytes", "bytes 1.4.0",
"headers-core", "headers-core",
"http", "http",
"httpdate", "httpdate",
@ -786,7 +826,7 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [ dependencies = [
"bytes", "bytes 1.4.0",
"fnv", "fnv",
"itoa", "itoa",
] ]
@ -797,7 +837,7 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [ dependencies = [
"bytes", "bytes 1.4.0",
"http", "http",
"pin-project-lite", "pin-project-lite",
] ]
@ -820,7 +860,7 @@ version = "0.14.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
dependencies = [ dependencies = [
"bytes", "bytes 1.4.0",
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
@ -1055,7 +1095,7 @@ version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed4198ce7a4cbd2a57af78d28c6fbb57d81ac5f1d6ad79ac6c5587419cbdf22" checksum = "6ed4198ce7a4cbd2a57af78d28c6fbb57d81ac5f1d6ad79ac6c5587419cbdf22"
dependencies = [ dependencies = [
"bytes", "bytes 1.4.0",
"encoding_rs", "encoding_rs",
"futures-util", "futures-util",
"http", "http",
@ -1200,6 +1240,26 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "pin-project"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.9" version = "0.2.9"
@ -1225,7 +1285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0608069d4999c3c02d49dff261663f2e73a8f7b00b7cd364fb5e93e419dafa1" checksum = "c0608069d4999c3c02d49dff261663f2e73a8f7b00b7cd364fb5e93e419dafa1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes 1.4.0",
"chrono", "chrono",
"cookie", "cookie",
"futures-util", "futures-util",
@ -1246,6 +1306,7 @@ dependencies = [
"serde_urlencoded", "serde_urlencoded",
"serde_yaml", "serde_yaml",
"smallvec", "smallvec",
"sse-codec",
"tempfile", "tempfile",
"thiserror", "thiserror",
"time", "time",
@ -1274,7 +1335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1077defedfd8ff15990bb42993970ac75bc46dd8a5b3c0b452ab4e2041b825a4" checksum = "1077defedfd8ff15990bb42993970ac75bc46dd8a5b3c0b452ab4e2041b825a4"
dependencies = [ dependencies = [
"base64 0.21.0", "base64 0.21.0",
"bytes", "bytes 1.4.0",
"derive_more", "derive_more",
"futures-util", "futures-util",
"mime", "mime",
@ -1686,6 +1747,18 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc"
[[package]]
name = "sse-codec"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a59f811350c44b4a037aabeb72dc6a9591fc22aa95a036db9a96297c58085a"
dependencies = [
"bytes 0.5.6",
"futures-io",
"futures_codec",
"memchr",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -1719,7 +1792,6 @@ dependencies = [
"flate2", "flate2",
"hex", "hex",
"hex-literal", "hex-literal",
"httpdate",
"insta", "insta",
"log", "log",
"mime_guess", "mime_guess",
@ -1739,6 +1811,7 @@ dependencies = [
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
"tokio-test",
"toml", "toml",
"tracing-subscriber", "tracing-subscriber",
"zip", "zip",
@ -1853,7 +1926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes 1.4.0",
"libc", "libc",
"memchr", "memchr",
"mio", "mio",
@ -1886,14 +1959,28 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-test"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3"
dependencies = [
"async-stream",
"bytes 1.4.0",
"futures-core",
"tokio",
"tokio-stream",
]
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.7" version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
dependencies = [ dependencies = [
"bytes", "bytes 1.4.0",
"futures-core", "futures-core",
"futures-io",
"futures-sink", "futures-sink",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",

View file

@ -37,7 +37,6 @@ mime_guess = { version = "2.0.4", default-features = false }
compressible = "0.2.0" compressible = "0.2.0"
regex = "1.7.1" regex = "1.7.1"
log = "0.4.17" log = "0.4.17"
httpdate = "1.0.2"
tracing-subscriber = "0.3.16" tracing-subscriber = "0.3.16"
async-compression = { version = "0.3.15", features = [ async-compression = { version = "0.3.15", features = [
"tokio", "tokio",
@ -47,6 +46,8 @@ async-compression = { version = "0.3.15", features = [
[dev-dependencies] [dev-dependencies]
rstest = "0.16.0" rstest = "0.16.0"
poem = { version = "1.3.55", features = ["test"] }
tokio-test = "0.4.2"
temp_testdir = "0.2.3" temp_testdir = "0.2.3"
insta = { version = "1.17.1", features = ["ron"] } insta = { version = "1.17.1", features = ["ron"] }
hex-literal = "0.3.4" hex-literal = "0.3.4"

View file

@ -3,6 +3,8 @@ use std::{collections::BTreeMap, ops::Deref, path::Path, sync::Arc};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::storage::CompressionAlg;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct Config { pub struct Config {
i: Arc<ConfigInner>, i: Arc<ConfigInner>,
@ -88,9 +90,9 @@ impl Default for ServerCfg {
fn default() -> Self { fn default() -> Self {
Self { Self {
address: "0.0.0.0:3000".to_owned(), address: "0.0.0.0:3000".to_owned(),
root_domain: "localhost".to_owned(), root_domain: "localhost:3000".to_owned(),
internal_subdomain: "talon".to_owned(), internal_subdomain: "talon".to_owned(),
internal_url: "http://talon.localhost".to_owned(), internal_url: "http://talon.localhost:3000".to_owned(),
} }
} }
} }
@ -111,9 +113,9 @@ pub struct CompressionCfg {
impl Default for CompressionCfg { impl Default for CompressionCfg {
fn default() -> Self { fn default() -> Self {
Self { Self {
gzip_en: false, gzip_en: true,
gzip_level: 6, gzip_level: 6,
brotli_en: false, brotli_en: true,
brotli_level: 7, brotli_level: 7,
} }
} }
@ -123,6 +125,17 @@ impl CompressionCfg {
pub fn enabled(&self) -> bool { pub fn enabled(&self) -> bool {
self.gzip_en || self.brotli_en self.gzip_en || self.brotli_en
} }
pub fn algs(&self) -> Vec<CompressionAlg> {
let mut res = vec![CompressionAlg::None];
if self.gzip_en {
res.push(CompressionAlg::Gzip)
}
if self.brotli_en {
res.push(CompressionAlg::Brotli)
}
res
}
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]

View file

@ -36,10 +36,9 @@ pub async fn page(request: &Request, talon: Data<&Talon>) -> Result<Response> {
let ws = talon.db.get_website(subdomain)?; let ws = talon.db.get_website(subdomain)?;
let vid = ws.latest_version.ok_or(PageError::NoVersion)?; let vid = ws.latest_version.ok_or(PageError::NoVersion)?;
let (file, ok) = let (file, ok) = match talon
match talon
.storage .storage
.get_file(vid, request.original_uri().path(), request.headers()) .get_file(vid, request.uri().path(), request.headers())
{ {
Ok(file) => (file, true), Ok(file) => (file, true),
Err(StorageError::NotFound(f)) => { Err(StorageError::NotFound(f)) => {
@ -58,8 +57,9 @@ pub async fn page(request: &Request, talon: Data<&Talon>) -> Result<Response> {
Ok(match file.rd_path { Ok(match file.rd_path {
Some(rd_path) => Redirect::moved_permanent(rd_path).into_response(), Some(rd_path) => Redirect::moved_permanent(rd_path).into_response(),
None => file None => talon
.to_response(request.headers(), ok) .storage
.file_to_response(file, request.headers(), ok)
.await? .await?
.into_response(), .into_response(),
}) })

View file

@ -2,7 +2,10 @@ use std::{ops::Deref, path::Path, sync::Arc};
use crate::{api::TalonApi, config::Config, db::Db, page::page, storage::Storage, util}; use crate::{api::TalonApi, config::Config, db::Db, page::page, storage::Storage, util};
use path_macro::path; use path_macro::path;
use poem::{listener::TcpListener, EndpointExt, Route, RouteDomain, Server}; use poem::{
http::header, listener::TcpListener, middleware, Endpoint, EndpointExt, Route, RouteDomain,
Server,
};
use poem_openapi::OpenApiService; use poem_openapi::OpenApiService;
#[derive(Clone)] #[derive(Clone)]
@ -54,7 +57,7 @@ impl Talon {
}) })
} }
pub async fn launch(&self) -> Result<()> { pub fn endpoint(&self) -> impl Endpoint {
let api_service = OpenApiService::new(TalonApi, "Talon", "0.1.0") let api_service = OpenApiService::new(TalonApi, "Talon", "0.1.0")
.server(format!("{}/api", self.i.cfg.server.internal_url)); .server(format!("{}/api", self.i.cfg.server.internal_url));
let swagger_ui = api_service.swagger_ui(); let swagger_ui = api_service.swagger_ui();
@ -71,7 +74,7 @@ impl Talon {
"/api/spec", "/api/spec",
poem::endpoint::make_sync(move |_| spec.clone()), poem::endpoint::make_sync(move |_| spec.clone()),
) )
.with(poem::middleware::Cors::new()); .with(middleware::Cors::new());
let internal_domain = format!( let internal_domain = format!(
"{}.{}", "{}.{}",
@ -79,15 +82,20 @@ impl Talon {
); );
let site_domains = format!("+.{}", self.i.cfg.server.root_domain); let site_domains = format!("+.{}", self.i.cfg.server.root_domain);
let route = RouteDomain::new() RouteDomain::new()
.at(&internal_domain, route_internal) .at(internal_domain, route_internal)
.at(&site_domains, page) .at(site_domains, page)
.at(&self.i.cfg.server.root_domain, page) .at(&self.i.cfg.server.root_domain, page)
.with(poem::middleware::Tracing) .with(middleware::Tracing)
.data(self.clone()); .with(
middleware::SetHeader::new().overriding(header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
)
.data(self.clone())
}
pub async fn launch(&self) -> Result<()> {
Server::new(TcpListener::bind(&self.i.cfg.server.address)) Server::new(TcpListener::bind(&self.i.cfg.server.address))
.run(route) .run(self.endpoint())
.await?; .await?;
Ok(()) Ok(())
} }

View file

@ -9,7 +9,6 @@ use std::{
use flate2::{read::GzDecoder, write::GzEncoder}; use flate2::{read::GzDecoder, write::GzEncoder};
use hex::ToHex; use hex::ToHex;
use httpdate::HttpDate;
use mime_guess::Mime; use mime_guess::Mime;
use poem::{ use poem::{
error::{ResponseError, StaticFileError}, error::{ResponseError, StaticFileError},
@ -52,7 +51,7 @@ impl CompressionAlg {
} }
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct GotFile { pub struct GotFile {
/// File hash /// File hash
pub hash: String, pub hash: String,
@ -363,26 +362,25 @@ impl Storage {
)), )),
} }
} }
}
impl GotFile {
/// Convert the retrieved file to a HTTP response /// Convert the retrieved file to a HTTP response
/// ///
/// Adapted from: <https://github.com/poem-web/poem/blob/049215cf02c5d4b1ab76f290b4708f3142d6d61b/poem/src/web/static_file.rs#L175> /// Adapted from: <https://github.com/poem-web/poem/blob/049215cf02c5d4b1ab76f290b4708f3142d6d61b/poem/src/web/static_file.rs#L175>
pub async fn to_response( pub async fn file_to_response(
self, &self,
gf: GotFile,
headers: &HeaderMap, headers: &HeaderMap,
ok: bool, ok: bool,
) -> std::result::Result<Response, StaticFileError> { ) -> std::result::Result<Response, StaticFileError> {
let mut file = File::open(&self.file_path)?; let mut file = File::open(&gf.file_path)?;
let metadata = file.metadata()?; let metadata = file.metadata()?;
// etag and last modified // etag and last modified
let etag = headers::ETag::from_str(&format!("\"{}\"", self.hash)).unwrap(); let etag = headers::ETag::from_str(&format!("\"{}\"", gf.hash)).unwrap();
let mut last_modified_str = String::new(); let mut last_modified = None;
if ok { if ok {
// handle if-match and if-(un)modified queries // handle if-(none)-match and if-(un)modified queries
let if_match = headers.typed_get::<headers::IfMatch>(); let if_match = headers.typed_get::<headers::IfMatch>();
let if_unmodified_since = headers.typed_get::<headers::IfUnmodifiedSince>(); let if_unmodified_since = headers.typed_get::<headers::IfUnmodifiedSince>();
let if_none_match = headers.typed_get::<headers::IfNoneMatch>(); let if_none_match = headers.typed_get::<headers::IfNoneMatch>();
@ -411,11 +409,11 @@ impl GotFile {
} }
} }
last_modified_str = HttpDate::from(modified).to_string(); last_modified = Some(headers::LastModified::from(modified));
} }
} }
if self if gf
.mime .mime
.as_ref() .as_ref()
.map(|m| m.essence_str() == "text/html") .map(|m| m.essence_str() == "text/html")
@ -434,28 +432,21 @@ impl GotFile {
} }
// Compress response if possible // Compress response if possible
let alg = util::parse_accept_encoding( let alg = util::parse_accept_encoding(headers, &self.cfg.compression.algs())
headers,
&[
CompressionAlg::Brotli,
CompressionAlg::Gzip,
CompressionAlg::None,
],
)
.unwrap_or_default(); .unwrap_or_default();
let body = match alg { let body = match alg {
CompressionAlg::None => Body::from(html), CompressionAlg::None => Body::from(html),
CompressionAlg::Gzip => { CompressionAlg::Gzip => {
let enc = async_compression::tokio::bufread::GzipEncoder::with_quality( let enc = async_compression::tokio::bufread::GzipEncoder::with_quality(
tokio::io::BufReader::new(Body::from(html).into_async_read()), tokio::io::BufReader::new(Body::from(html).into_async_read()),
async_compression::Level::Precise(6), async_compression::Level::Precise(self.cfg.compression.gzip_level.into()),
); );
Body::from_async_read(enc) Body::from_async_read(enc)
} }
CompressionAlg::Brotli => { CompressionAlg::Brotli => {
let enc = async_compression::tokio::bufread::BrotliEncoder::with_quality( let enc = async_compression::tokio::bufread::BrotliEncoder::with_quality(
tokio::io::BufReader::new(Body::from(html).into_async_read()), tokio::io::BufReader::new(Body::from(html).into_async_read()),
async_compression::Level::Precise(7), async_compression::Level::Precise(self.cfg.compression.brotli_level.into()),
); );
Body::from_async_read(enc) Body::from_async_read(enc)
} }
@ -469,8 +460,8 @@ impl GotFile {
if let Some(encoding) = alg.encoding() { if let Some(encoding) = alg.encoding() {
response = response.header(header::CONTENT_ENCODING, encoding) response = response.header(header::CONTENT_ENCODING, encoding)
} }
if !last_modified_str.is_empty() { if let Some(last_modified) = last_modified {
response = response.header(header::LAST_MODIFIED, last_modified_str); response = response.typed_header(last_modified);
} }
Ok(response.body(body)) Ok(response.body(body))
@ -517,13 +508,13 @@ impl GotFile {
} else { } else {
response = response.status(StatusCode::NOT_FOUND); response = response.status(StatusCode::NOT_FOUND);
} }
if !last_modified_str.is_empty() { if let Some(last_modified) = last_modified {
response = response.header(header::LAST_MODIFIED, last_modified_str); response = response.typed_header(last_modified);
} }
if let Some(encoding) = self.encoding { if let Some(encoding) = gf.encoding {
response = response.header(header::CONTENT_ENCODING, encoding); response = response.header(header::CONTENT_ENCODING, encoding);
} }
if let Some(mime) = &self.mime { if let Some(mime) = &gf.mime {
response = response.header(header::CONTENT_TYPE, mime.essence_str()); response = response.header(header::CONTENT_TYPE, mime.essence_str());
} }
if let Some((range, size)) = content_range { if let Some((range, size)) = content_range {

143
tests/fixtures/mod.rs vendored
View file

@ -1,4 +1,4 @@
use std::{collections::BTreeMap, ops::Deref}; use std::{collections::BTreeMap, fs::File, ops::Deref};
use hex_literal::hex; use hex_literal::hex;
use path_macro::path; use path_macro::path;
@ -7,27 +7,47 @@ use temp_testdir::TempDir;
use time::macros::datetime; use time::macros::datetime;
use talon::{ use talon::{
config::{CompressionCfg, Config, ConfigInner},
db::{ db::{
model::{Version, Website}, model::{Version, Website},
Db, Db,
}, },
storage::Storage, Talon,
}; };
pub const SUBDOMAIN_1: &str = "-"; pub const SUBDOMAIN_1: &str = "-";
pub const SUBDOMAIN_2: &str = "spotify-gender-ex"; pub const SUBDOMAIN_2: &str = "spotify-gender-ex";
pub const SUBDOMAIN_3: &str = "rustypipe"; pub const SUBDOMAIN_3: &str = "rustypipe";
pub const SUBDOMAIN_4: &str = "spa";
pub const VERSION_1_1: u32 = 1; pub const VERSION_1_1: u32 = 1;
pub const VERSION_1_2: u32 = 2; pub const VERSION_1_2: u32 = 2;
pub const VERSION_2_1: u32 = 3; pub const VERSION_2_1: u32 = 3;
pub const VERSION_3_1: u32 = 4; pub const VERSION_3_1: u32 = 4;
pub const VERSION_4_1: u32 = 5;
pub const HASH_1_1_INDEX: [u8; 32] = pub const HASH_1_1_INDEX: [u8; 32] =
hex!("3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68"); hex!("3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68");
pub const HASH_1_1_STYLE: [u8; 32] = pub const HASH_1_1_STYLE: [u8; 32] =
hex!("356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"); hex!("356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026");
pub const HASH_1_2_INDEX: [u8; 32] =
hex!("a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb");
pub const HASH_1_2_STYLE: [u8; 32] =
hex!("356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026");
pub const HASH_2_1_INDEX: [u8; 32] =
hex!("6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2");
pub const HASH_2_1_STYLE: [u8; 32] =
hex!("fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541");
pub const HASH_3_1_INDEX: [u8; 32] =
hex!("cc31423924cf1f124750825861ab1ccc675e755921fc2fa111c0a98e8c346a5e");
pub const HASH_3_1_STYLE: [u8; 32] =
hex!("ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993");
pub const HASH_3_1_PAGE2: [u8; 32] =
hex!("be4f409ca0adcb21cdc7130cde63031718406726f889ef97ac8870c90b330a75");
pub const HASH_SPA_INDEX: [u8; 32] =
hex!("90d454dd999b52486902e845f748ce7510a1fc8404421c8f44dadccc5b4b8e1d");
pub const HASH_SPA_FALLBACK: [u8; 32] =
hex!("4ee0d3f7522f620a2a69b39b7443f8fe65029e1324cefaf797b8cad2b223cf7b");
pub struct DbTest { pub struct DbTest {
db: Db, db: Db,
@ -91,6 +111,18 @@ fn insert_websites(db: &Db) {
}, },
) )
.unwrap(); .unwrap();
db.insert_website(
SUBDOMAIN_4,
&Website {
name: "SvelteKit SPA".to_owned(),
created_at: datetime!(2023-03-03 22:00 +0),
latest_version: Some(VERSION_4_1),
color: Some(16727552),
visibility: talon::model::Visibility::Hidden,
..Default::default()
},
)
.unwrap();
let mut v1_data = BTreeMap::new(); let mut v1_data = BTreeMap::new();
v1_data.insert("Version".to_owned(), "v0.1.0".to_owned()); v1_data.insert("Version".to_owned(), "v0.1.0".to_owned());
@ -134,7 +166,6 @@ fn insert_websites(db: &Db) {
VERSION_2_1, VERSION_2_1,
&Version { &Version {
created_at: datetime!(2023-02-18 16:30 +0), created_at: datetime!(2023-02-18 16:30 +0),
data: BTreeMap::new(),
..Default::default() ..Default::default()
}, },
) )
@ -145,7 +176,19 @@ fn insert_websites(db: &Db) {
VERSION_3_1, VERSION_3_1,
&Version { &Version {
created_at: datetime!(2023-02-20 18:30 +0), created_at: datetime!(2023-02-20 18:30 +0),
data: BTreeMap::new(), ..Default::default()
},
)
.unwrap();
assert_eq!(db.new_version_id().unwrap(), VERSION_4_1);
db.insert_version(
SUBDOMAIN_4,
VERSION_4_1,
&Version {
created_at: datetime!(2023-03-03 22:00 +0),
fallback: Some("200.html".to_owned()),
spa: true,
..Default::default() ..Default::default()
}, },
) )
@ -163,17 +206,9 @@ pub fn db() -> DbTest {
db.insert_file(VERSION_1_1, "style.css", &HASH_1_1_STYLE) db.insert_file(VERSION_1_1, "style.css", &HASH_1_1_STYLE)
.unwrap(); .unwrap();
db.insert_file( db.insert_file(VERSION_1_2, "index.html", &HASH_1_2_INDEX)
VERSION_1_2,
"index.html",
&hex!("a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb"),
)
.unwrap(); .unwrap();
db.insert_file( db.insert_file(VERSION_1_2, "assets/style.css", &HASH_1_2_STYLE)
VERSION_1_2,
"assets/style.css",
&hex!("356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"),
)
.unwrap(); .unwrap();
db.insert_file( db.insert_file(
VERSION_1_2, VERSION_1_2,
@ -194,80 +229,64 @@ pub fn db() -> DbTest {
) )
.unwrap(); .unwrap();
db.insert_file( db.insert_file(VERSION_2_1, "index.html", &HASH_2_1_INDEX)
VERSION_2_1,
"index.html",
&hex!("6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"),
)
.unwrap(); .unwrap();
db.insert_file( db.insert_file(VERSION_2_1, "gex_style.css", &HASH_2_1_STYLE)
VERSION_2_1,
"gex_style.css",
&hex!("fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"),
)
.unwrap(); .unwrap();
db.insert_file( db.insert_file(VERSION_3_1, "index.html", &HASH_3_1_INDEX)
VERSION_3_1,
"index.html",
&hex!("94a67cf13d752a9c1875ad999eb2be5a1b0f9746c66bca2631820b8186028811"),
)
.unwrap(); .unwrap();
db.insert_file( db.insert_file(VERSION_3_1, "rp_style.css", &HASH_3_1_STYLE)
VERSION_3_1, .unwrap();
"rp_style.css", db.insert_file(VERSION_3_1, "page2/index.html", &HASH_3_1_PAGE2)
&hex!("ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"),
)
.unwrap(); .unwrap();
DbTest { db, _temp: temp } DbTest { db, _temp: temp }
} }
pub struct StorageTest { pub struct TalonTest {
store: Storage, talon: Talon,
_temp: TempDir, pub temp: TempDir,
} }
impl Deref for StorageTest { impl Deref for TalonTest {
type Target = Storage; type Target = Talon;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.store &self.talon
} }
} }
#[fixture] #[fixture]
pub fn store() -> StorageTest { pub fn tln() -> TalonTest {
let temp = temp_testdir::TempDir::default(); let temp = temp_testdir::TempDir::default();
let db_path = path!(temp / "db"); let talon = Talon::new(&temp).unwrap();
std::fs::create_dir(&db_path).unwrap();
let cfg = Config::new(ConfigInner { insert_websites(&talon.db);
compression: CompressionCfg {
gzip_en: true,
brotli_en: true,
..Default::default()
},
..Default::default()
});
let db = Db::new(&db_path).unwrap(); talon
insert_websites(&db); .storage
let store = Storage::new(temp.to_path_buf(), db, cfg);
store
.insert_dir(path!("tests" / "testfiles" / "ThetaDev0"), VERSION_1_1) .insert_dir(path!("tests" / "testfiles" / "ThetaDev0"), VERSION_1_1)
.unwrap(); .unwrap();
store talon
.storage
.insert_dir(path!("tests" / "testfiles" / "ThetaDev1"), VERSION_1_2) .insert_dir(path!("tests" / "testfiles" / "ThetaDev1"), VERSION_1_2)
.unwrap(); .unwrap();
store talon
.storage
.insert_dir(path!("tests" / "testfiles" / "GenderEx"), VERSION_2_1) .insert_dir(path!("tests" / "testfiles" / "GenderEx"), VERSION_2_1)
.unwrap(); .unwrap();
store talon
.storage
.insert_dir(path!("tests" / "testfiles" / "RustyPipe"), VERSION_3_1) .insert_dir(path!("tests" / "testfiles" / "RustyPipe"), VERSION_3_1)
.unwrap(); .unwrap();
talon
.storage
.insert_tgz_archive(
File::open(path!("tests" / "testfiles" / "archive" / "spa.tar.gz")).unwrap(),
VERSION_4_1,
)
.unwrap();
StorageTest { store, _temp: temp } TalonTest { talon, temp }
} }

View file

@ -5,14 +5,14 @@ expression: "&cfg"
ConfigInner( ConfigInner(
server: ServerCfg( server: ServerCfg(
address: "0.0.0.0:3000", address: "0.0.0.0:3000",
root_domain: "localhost", root_domain: "localhost:3000",
internal_subdomain: "talon", internal_subdomain: "talon",
internal_url: "http://talon.localhost", internal_url: "http://talon.localhost:3000",
), ),
compression: CompressionCfg( compression: CompressionCfg(
gzip_en: true, gzip_en: true,
gzip_level: 6, gzip_level: 6,
brotli_en: false, brotli_en: true,
brotli_level: 7, brotli_level: 7,
), ),
keys: { keys: {

View file

@ -3,10 +3,13 @@ source: tests/tests.rs
expression: data expression: data
--- ---
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}} {"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}}
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"latest_version":5,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null}}
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}} {"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}}
{"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}} {"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"version","key":"spa:5","value":{"created_at":[2023,62,22,0,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"version","key":"spotify-gender-ex:3","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}} {"type":"version","key":"spotify-gender-ex:3","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"} {"type":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}
{"type":"file","key":"3:index.html","value":"6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"} {"type":"file","key":"3:index.html","value":"6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"}
{"type":"file","key":"4:index.html","value":"94a67cf13d752a9c1875ad999eb2be5a1b0f9746c66bca2631820b8186028811"} {"type":"file","key":"4:index.html","value":"cc31423924cf1f124750825861ab1ccc675e755921fc2fa111c0a98e8c346a5e"}
{"type":"file","key":"4:page2/index.html","value":"be4f409ca0adcb21cdc7130cde63031718406726f889ef97ac8870c90b330a75"}
{"type":"file","key":"4:rp_style.css","value":"ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"} {"type":"file","key":"4:rp_style.css","value":"ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"}

View file

@ -4,10 +4,12 @@ expression: data
--- ---
{"type":"website","key":"-","value":{"name":"ThetaDev","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":2,"color":2068974,"visibility":"featured","source_url":null,"source_icon":null}} {"type":"website","key":"-","value":{"name":"ThetaDev","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":2,"color":2068974,"visibility":"featured","source_url":null,"source_icon":null}}
{"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}} {"type":"website","key":"rustypipe","value":{"name":"RustyPipe","created_at":[2023,51,18,30,0,0,0,0,0],"latest_version":4,"color":7943647,"visibility":"featured","source_url":"https://code.thetadev.de/ThetaDev/rustypipe","source_icon":"gitea"}}
{"type":"website","key":"spa","value":{"name":"SvelteKit SPA","created_at":[2023,62,22,0,0,0,0,0,0],"latest_version":5,"color":16727552,"visibility":"hidden","source_url":null,"source_icon":null}}
{"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}} {"type":"website","key":"spotify-gender-ex","value":{"name":"Spotify-Gender-Ex","created_at":[2023,49,16,30,0,0,0,0,0],"latest_version":3,"color":1947988,"visibility":"featured","source_url":"https://github.com/Theta-Dev/Spotify-Gender-Ex","source_icon":"github"}}
{"type":"version","key":"-:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1352014628","Version":"v0.1.0"},"fallback":null,"spa":false}} {"type":"version","key":"-:1","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1352014628","Version":"v0.1.0"},"fallback":null,"spa":false}}
{"type":"version","key":"-:2","value":{"created_at":[2023,49,16,52,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1354755231","Version":"v0.1.1"},"fallback":null,"spa":false}} {"type":"version","key":"-:2","value":{"created_at":[2023,49,16,52,0,0,0,0,0],"data":{"Deployed by":"https://github.com/Theta-Dev/Talon/actions/runs/1354755231","Version":"v0.1.1"},"fallback":null,"spa":false}}
{"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}} {"type":"version","key":"rustypipe:4","value":{"created_at":[2023,51,18,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"version","key":"spa:5","value":{"created_at":[2023,62,22,0,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"version","key":"spotify-gender-ex:3","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}} {"type":"version","key":"spotify-gender-ex:3","value":{"created_at":[2023,49,16,30,0,0,0,0,0],"data":{},"fallback":null,"spa":false}}
{"type":"file","key":"1:index.html","value":"3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68"} {"type":"file","key":"1:index.html","value":"3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68"}
{"type":"file","key":"1:style.css","value":"356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"} {"type":"file","key":"1:style.css","value":"356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"}
@ -18,5 +20,6 @@ expression: data
{"type":"file","key":"2:index.html","value":"a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb"} {"type":"file","key":"2:index.html","value":"a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb"}
{"type":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"} {"type":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}
{"type":"file","key":"3:index.html","value":"6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"} {"type":"file","key":"3:index.html","value":"6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"}
{"type":"file","key":"4:index.html","value":"94a67cf13d752a9c1875ad999eb2be5a1b0f9746c66bca2631820b8186028811"} {"type":"file","key":"4:index.html","value":"cc31423924cf1f124750825861ab1ccc675e755921fc2fa111c0a98e8c346a5e"}
{"type":"file","key":"4:page2/index.html","value":"be4f409ca0adcb21cdc7130cde63031718406726f889ef97ac8870c90b330a75"}
{"type":"file","key":"4:rp_style.css","value":"ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"} {"type":"file","key":"4:rp_style.css","value":"ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"}

View file

@ -21,6 +21,15 @@ expression: websites
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"), source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"),
source_icon: Some(gitea), source_icon: Some(gitea),
)), )),
("spa", Website(
name: "SvelteKit SPA",
created_at: (2023, 62, 22, 0, 0, 0, 0, 0, 0),
latest_version: Some(5),
color: Some(16727552),
visibility: hidden,
source_url: None,
source_icon: None,
)),
("spotify-gender-ex", Website( ("spotify-gender-ex", Website(
name: "Spotify-Gender-Ex", name: "Spotify-Gender-Ex",
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0), created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),

Binary file not shown.

View file

@ -176,15 +176,22 @@ mod database {
#[rstest] #[rstest]
fn get_file_hashes(db: DbTest) { fn get_file_hashes(db: DbTest) {
let hashes = db.get_file_hashes().unwrap(); let hashes = db.get_file_hashes().unwrap();
assert_eq!(hashes.len(), 10) assert_eq!(hashes.len(), 11)
} }
} }
mod storage { mod storage {
use std::{str::FromStr, time::SystemTime};
use hex::ToHex; use hex::ToHex;
use poem::http::{header, HeaderMap}; use poem::{
error::StaticFileError,
http::{header, HeaderMap, StatusCode},
web::headers::{self, HeaderMapExt},
};
use talon::config::{CompressionCfg, Config, ConfigInner}; use talon::config::{CompressionCfg, Config, ConfigInner};
use talon::storage::Storage; use talon::storage::{GotFile, Storage};
use time::OffsetDateTime;
use super::*; use super::*;
@ -297,7 +304,7 @@ mod storage {
#[case::image("br", VERSION_1_2, "assets/image.jpg", false, "image/jpeg", None)] #[case::image("br", VERSION_1_2, "assets/image.jpg", false, "image/jpeg", None)]
#[case::subdir("br", VERSION_3_1, "page2", false, "text/html", Some("/page2/"))] #[case::subdir("br", VERSION_3_1, "page2", false, "text/html", Some("/page2/"))]
fn get_file( fn get_file(
store: StorageTest, tln: TalonTest,
#[case] encoding: &str, #[case] encoding: &str,
#[case] version: u32, #[case] version: u32,
#[case] path: &str, #[case] path: &str,
@ -318,7 +325,8 @@ mod storage {
None None
}; };
let index_file = store.get_file(version, path, &headers).unwrap(); let index_file = tln.storage.get_file(version, path, &headers).unwrap();
dbg!(&index_file);
assert!(index_file.file_path.is_file()); assert!(index_file.file_path.is_file());
assert_eq!( assert_eq!(
index_file index_file
@ -334,6 +342,213 @@ mod storage {
assert_eq!(index_file.mime.unwrap().essence_str(), mime); assert_eq!(index_file.mime.unwrap().essence_str(), mime);
assert_eq!(index_file.rd_path.as_deref(), rd_path); assert_eq!(index_file.rd_path.as_deref(), rd_path);
} }
#[rstest]
#[case::index(&HASH_1_2_INDEX, "text/html", "")]
#[case::index_gz(&HASH_1_2_INDEX, "text/html", "gzip")]
#[case::index_br(&HASH_1_2_INDEX, "text/html", "br")]
#[case::style(&HASH_1_2_STYLE, "text/css", "")]
fn file_to_response(
tln: TalonTest,
#[case] hash: &[u8],
#[case] mime: &str,
#[case] encoding: &str,
) {
let gf = got_file(&tln, hash, mime);
let file_date = std::fs::metadata(&gf.file_path)
.unwrap()
.modified()
.unwrap();
let mut headers = HeaderMap::new();
if !encoding.is_empty() {
headers.insert(header::ACCEPT_ENCODING, encoding.parse().unwrap());
}
let resp =
tokio_test::block_on(tln.storage.file_to_response(gf.clone(), &headers, true)).unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.header(header::CONTENT_TYPE).unwrap(), mime);
assert_eq!(
resp.header(header::ETAG).unwrap(),
format!("\"{}\"", gf.hash)
);
let date = OffsetDateTime::from(SystemTime::from(
resp.headers().typed_get::<headers::LastModified>().unwrap(),
));
assert!(date - file_date < time::Duration::SECOND);
// HTML files should get dynamically compressed
if mime == "text/html" && !encoding.is_empty() {
assert_eq!(resp.header(header::CONTENT_ENCODING).unwrap(), encoding);
} else {
assert!(resp.header(header::CONTENT_ENCODING).is_none())
}
}
fn got_file(tln: &TalonTest, hash: &[u8], mime: &str) -> GotFile {
let hash = hash.encode_hex::<String>();
let file_path = path!(tln.temp / "storage" / &hash[..2] / &hash);
GotFile {
hash: hash.clone(),
file_path,
encoding: None,
mime: Some(mime_guess::Mime::from_str(mime).unwrap()),
rd_path: None,
}
}
fn got_file_html(tln: &TalonTest) -> GotFile {
got_file(tln, &HASH_1_2_INDEX, "text/html")
}
#[rstest]
fn file_to_response_inject(tln: TalonTest) {
let gf = got_file_html(&tln);
let resp = tokio_test::block_on(tln.storage.file_to_response(gf, &HeaderMap::new(), true))
.unwrap();
let body = tokio_test::block_on(resp.into_body().into_string()).unwrap();
assert!(body.contains("<!-- Hello World -->\n"));
}
#[rstest]
#[case::unmodified(true)]
#[case::modified(true)]
fn file_to_response_if_modified(tln: TalonTest, #[case] modified: bool) {
let gf = got_file_html(&tln);
let mut file_date = std::fs::metadata(&gf.file_path)
.unwrap()
.modified()
.unwrap();
if modified {
file_date -= std::time::Duration::from_secs(1);
}
let mut headers = HeaderMap::new();
headers.typed_insert(headers::IfModifiedSince::from(file_date));
let resp = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true)).unwrap();
assert_eq!(
resp.status(),
if modified {
StatusCode::OK
} else {
StatusCode::NOT_MODIFIED
}
);
assert_eq!(resp.into_body().is_empty(), !modified);
}
#[rstest]
#[case::unmodified(true)]
#[case::modified(true)]
fn file_to_response_if_unmodified(tln: TalonTest, #[case] modified: bool) {
let gf = got_file_html(&tln);
let mut file_date = std::fs::metadata(&gf.file_path)
.unwrap()
.modified()
.unwrap();
if modified {
file_date -= std::time::Duration::from_secs(1);
}
let mut headers = HeaderMap::new();
headers.typed_insert(headers::IfModifiedSince::from(file_date));
let res = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true));
if modified {
let resp = res.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(!resp.into_body().is_empty());
} else {
assert!(matches!(
res.unwrap_err(),
StaticFileError::PreconditionFailed
));
}
}
#[rstest]
#[case::matched(true)]
#[case::no_match(false)]
fn file_to_response_if_match(tln: TalonTest, #[case] matched: bool) {
let gf = got_file_html(&tln);
let etag = format!(
"\"{}\"",
if matched {
gf.hash.clone()
} else {
HASH_2_1_INDEX.encode_hex()
}
);
let mut headers = HeaderMap::new();
headers.typed_insert(headers::IfMatch::from(
headers::ETag::from_str(&etag).unwrap(),
));
let res = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true));
if matched {
let resp = res.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(!resp.into_body().is_empty());
} else {
assert!(matches!(
res.unwrap_err(),
StaticFileError::PreconditionFailed
));
}
}
#[rstest]
#[case::matched(true)]
#[case::no_match(false)]
fn file_to_response_if_none_match(tln: TalonTest, #[case] matched: bool) {
let gf = got_file_html(&tln);
let etag = format!(
"\"{}\"",
if matched {
gf.hash.clone()
} else {
HASH_2_1_INDEX.encode_hex()
}
);
let mut headers = HeaderMap::new();
headers.typed_insert(headers::IfNoneMatch::from(
headers::ETag::from_str(&etag).unwrap(),
));
let resp = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true)).unwrap();
assert_eq!(
resp.status(),
if matched {
StatusCode::NOT_MODIFIED
} else {
StatusCode::OK
}
);
assert_eq!(resp.into_body().is_empty(), matched);
}
#[rstest]
fn file_to_response_range(tln: TalonTest) {
let gf = got_file(&tln, &HASH_2_1_STYLE, "text/css");
let mut headers = HeaderMap::new();
headers.typed_insert(headers::Range::bytes(0..100).unwrap());
let resp = tokio_test::block_on(tln.storage.file_to_response(gf, &headers, true)).unwrap();
assert_eq!(resp.status(), StatusCode::PARTIAL_CONTENT);
let body = tokio_test::block_on(resp.into_body().into_bytes()).unwrap();
assert_eq!(body.len(), 100);
}
} }
mod config { mod config {
@ -351,3 +566,43 @@ mod config {
insta::assert_ron_snapshot!(name, &cfg); insta::assert_ron_snapshot!(name, &cfg);
} }
} }
mod page {
use hex::ToHex;
use poem::{http::header, test::TestClient};
use super::*;
#[rstest]
#[case::index("", "/", &HASH_1_2_INDEX, "text/html")]
#[case::style("", "/assets/style.css", &HASH_1_2_STYLE, "text/css")]
#[case::rustypipe("rustypipe", "/", &HASH_3_1_INDEX, "text/html")]
#[case::rustypipe2("rustypipe", "/page2/index.html", &HASH_3_1_PAGE2, "text/html")]
#[case::spa_index("spa", "/", &HASH_SPA_INDEX, "text/html")]
#[case::spa_fallback("spa", "/user/2", &HASH_SPA_FALLBACK, "text/html")]
fn page(
tln: TalonTest,
#[case] subdomain: &str,
#[case] path: &str,
#[case] hash: &[u8],
#[case] mime: &str,
) {
let host = if subdomain.is_empty() {
"localhost:3000".to_owned()
} else {
format!("{subdomain}.localhost:3000")
};
let resp = tokio_test::block_on(
TestClient::new(tln.endpoint())
.get(format!("http://{host}{path}"))
.header(header::HOST, host)
.data(tln.clone())
.send(),
);
resp.assert_status_is_ok();
resp.assert_content_type(mime);
resp.assert_header(header::ETAG, format!("\"{}\"", hash.encode_hex::<String>()))
}
}