Compare commits
4 commits
8fc3d79abb
...
9403e36dce
Author | SHA1 | Date | |
---|---|---|---|
9403e36dce | |||
b4529662c0 | |||
949159a9eb | |||
0b35d88244 |
13 changed files with 541 additions and 152 deletions
109
Cargo.lock
generated
109
Cargo.lock
generated
|
@ -90,6 +90,28 @@ dependencies = [
|
|||
"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]]
|
||||
name = "async-trait"
|
||||
version = "0.1.64"
|
||||
|
@ -173,6 +195,12 @@ version = "1.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.4.0"
|
||||
|
@ -648,6 +676,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
|
@ -694,7 +734,7 @@ version = "0.3.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
|
@ -721,7 +761,7 @@ checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
|
|||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"headers-core",
|
||||
"http",
|
||||
"httpdate",
|
||||
|
@ -786,7 +826,7 @@ version = "0.2.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
@ -797,7 +837,7 @@ version = "0.4.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"http",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
@ -820,7 +860,7 @@ version = "0.14.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
|
@ -1055,7 +1095,7 @@ version = "2.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed4198ce7a4cbd2a57af78d28c6fbb57d81ac5f1d6ad79ac6c5587419cbdf22"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"http",
|
||||
|
@ -1200,6 +1240,26 @@ version = "2.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
|
@ -1225,7 +1285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c0608069d4999c3c02d49dff261663f2e73a8f7b00b7cd364fb5e93e419dafa1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"chrono",
|
||||
"cookie",
|
||||
"futures-util",
|
||||
|
@ -1246,6 +1306,7 @@ dependencies = [
|
|||
"serde_urlencoded",
|
||||
"serde_yaml",
|
||||
"smallvec",
|
||||
"sse-codec",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time",
|
||||
|
@ -1274,7 +1335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1077defedfd8ff15990bb42993970ac75bc46dd8a5b3c0b452ab4e2041b825a4"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"derive_more",
|
||||
"futures-util",
|
||||
"mime",
|
||||
|
@ -1686,6 +1747,18 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
|
@ -1719,7 +1792,6 @@ dependencies = [
|
|||
"flate2",
|
||||
"hex",
|
||||
"hex-literal",
|
||||
"httpdate",
|
||||
"insta",
|
||||
"log",
|
||||
"mime_guess",
|
||||
|
@ -1739,6 +1811,7 @@ dependencies = [
|
|||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"toml",
|
||||
"tracing-subscriber",
|
||||
"zip",
|
||||
|
@ -1853,7 +1926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"libc",
|
||||
"memchr",
|
||||
"mio",
|
||||
|
@ -1886,14 +1959,28 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"bytes 1.4.0",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
|
|
|
@ -37,7 +37,6 @@ mime_guess = { version = "2.0.4", default-features = false }
|
|||
compressible = "0.2.0"
|
||||
regex = "1.7.1"
|
||||
log = "0.4.17"
|
||||
httpdate = "1.0.2"
|
||||
tracing-subscriber = "0.3.16"
|
||||
async-compression = { version = "0.3.15", features = [
|
||||
"tokio",
|
||||
|
@ -47,6 +46,8 @@ async-compression = { version = "0.3.15", features = [
|
|||
|
||||
[dev-dependencies]
|
||||
rstest = "0.16.0"
|
||||
poem = { version = "1.3.55", features = ["test"] }
|
||||
tokio-test = "0.4.2"
|
||||
temp_testdir = "0.2.3"
|
||||
insta = { version = "1.17.1", features = ["ron"] }
|
||||
hex-literal = "0.3.4"
|
||||
|
|
|
@ -3,6 +3,8 @@ use std::{collections::BTreeMap, ops::Deref, path::Path, sync::Arc};
|
|||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::storage::CompressionAlg;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Config {
|
||||
i: Arc<ConfigInner>,
|
||||
|
@ -88,9 +90,9 @@ impl Default for ServerCfg {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
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_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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
gzip_en: false,
|
||||
gzip_en: true,
|
||||
gzip_level: 6,
|
||||
brotli_en: false,
|
||||
brotli_en: true,
|
||||
brotli_level: 7,
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +125,17 @@ impl CompressionCfg {
|
|||
pub fn enabled(&self) -> bool {
|
||||
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)]
|
||||
|
|
10
src/page.rs
10
src/page.rs
|
@ -36,10 +36,9 @@ pub async fn page(request: &Request, talon: Data<&Talon>) -> Result<Response> {
|
|||
|
||||
let ws = talon.db.get_website(subdomain)?;
|
||||
let vid = ws.latest_version.ok_or(PageError::NoVersion)?;
|
||||
let (file, ok) =
|
||||
match talon
|
||||
let (file, ok) = match talon
|
||||
.storage
|
||||
.get_file(vid, request.original_uri().path(), request.headers())
|
||||
.get_file(vid, request.uri().path(), request.headers())
|
||||
{
|
||||
Ok(file) => (file, true),
|
||||
Err(StorageError::NotFound(f)) => {
|
||||
|
@ -58,8 +57,9 @@ pub async fn page(request: &Request, talon: Data<&Talon>) -> Result<Response> {
|
|||
|
||||
Ok(match file.rd_path {
|
||||
Some(rd_path) => Redirect::moved_permanent(rd_path).into_response(),
|
||||
None => file
|
||||
.to_response(request.headers(), ok)
|
||||
None => talon
|
||||
.storage
|
||||
.file_to_response(file, request.headers(), ok)
|
||||
.await?
|
||||
.into_response(),
|
||||
})
|
||||
|
|
|
@ -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 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;
|
||||
|
||||
#[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")
|
||||
.server(format!("{}/api", self.i.cfg.server.internal_url));
|
||||
let swagger_ui = api_service.swagger_ui();
|
||||
|
@ -71,7 +74,7 @@ impl Talon {
|
|||
"/api/spec",
|
||||
poem::endpoint::make_sync(move |_| spec.clone()),
|
||||
)
|
||||
.with(poem::middleware::Cors::new());
|
||||
.with(middleware::Cors::new());
|
||||
|
||||
let internal_domain = format!(
|
||||
"{}.{}",
|
||||
|
@ -79,15 +82,20 @@ impl Talon {
|
|||
);
|
||||
let site_domains = format!("+.{}", self.i.cfg.server.root_domain);
|
||||
|
||||
let route = RouteDomain::new()
|
||||
.at(&internal_domain, route_internal)
|
||||
.at(&site_domains, page)
|
||||
RouteDomain::new()
|
||||
.at(internal_domain, route_internal)
|
||||
.at(site_domains, page)
|
||||
.at(&self.i.cfg.server.root_domain, page)
|
||||
.with(poem::middleware::Tracing)
|
||||
.data(self.clone());
|
||||
.with(middleware::Tracing)
|
||||
.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))
|
||||
.run(route)
|
||||
.run(self.endpoint())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ use std::{
|
|||
|
||||
use flate2::{read::GzDecoder, write::GzEncoder};
|
||||
use hex::ToHex;
|
||||
use httpdate::HttpDate;
|
||||
use mime_guess::Mime;
|
||||
use poem::{
|
||||
error::{ResponseError, StaticFileError},
|
||||
|
@ -52,7 +51,7 @@ impl CompressionAlg {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GotFile {
|
||||
/// File hash
|
||||
pub hash: String,
|
||||
|
@ -363,26 +362,25 @@ impl Storage {
|
|||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GotFile {
|
||||
/// 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>
|
||||
pub async fn to_response(
|
||||
self,
|
||||
pub async fn file_to_response(
|
||||
&self,
|
||||
gf: GotFile,
|
||||
headers: &HeaderMap,
|
||||
ok: bool,
|
||||
) -> 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()?;
|
||||
|
||||
// etag and last modified
|
||||
let etag = headers::ETag::from_str(&format!("\"{}\"", self.hash)).unwrap();
|
||||
let mut last_modified_str = String::new();
|
||||
let etag = headers::ETag::from_str(&format!("\"{}\"", gf.hash)).unwrap();
|
||||
let mut last_modified = None;
|
||||
|
||||
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_unmodified_since = headers.typed_get::<headers::IfUnmodifiedSince>();
|
||||
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
|
||||
.as_ref()
|
||||
.map(|m| m.essence_str() == "text/html")
|
||||
|
@ -434,28 +432,21 @@ impl GotFile {
|
|||
}
|
||||
|
||||
// Compress response if possible
|
||||
let alg = util::parse_accept_encoding(
|
||||
headers,
|
||||
&[
|
||||
CompressionAlg::Brotli,
|
||||
CompressionAlg::Gzip,
|
||||
CompressionAlg::None,
|
||||
],
|
||||
)
|
||||
let alg = util::parse_accept_encoding(headers, &self.cfg.compression.algs())
|
||||
.unwrap_or_default();
|
||||
let body = match alg {
|
||||
CompressionAlg::None => Body::from(html),
|
||||
CompressionAlg::Gzip => {
|
||||
let enc = async_compression::tokio::bufread::GzipEncoder::with_quality(
|
||||
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)
|
||||
}
|
||||
CompressionAlg::Brotli => {
|
||||
let enc = async_compression::tokio::bufread::BrotliEncoder::with_quality(
|
||||
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)
|
||||
}
|
||||
|
@ -469,8 +460,8 @@ impl GotFile {
|
|||
if let Some(encoding) = alg.encoding() {
|
||||
response = response.header(header::CONTENT_ENCODING, encoding)
|
||||
}
|
||||
if !last_modified_str.is_empty() {
|
||||
response = response.header(header::LAST_MODIFIED, last_modified_str);
|
||||
if let Some(last_modified) = last_modified {
|
||||
response = response.typed_header(last_modified);
|
||||
}
|
||||
|
||||
Ok(response.body(body))
|
||||
|
@ -517,13 +508,13 @@ impl GotFile {
|
|||
} else {
|
||||
response = response.status(StatusCode::NOT_FOUND);
|
||||
}
|
||||
if !last_modified_str.is_empty() {
|
||||
response = response.header(header::LAST_MODIFIED, last_modified_str);
|
||||
if let Some(last_modified) = last_modified {
|
||||
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);
|
||||
}
|
||||
if let Some(mime) = &self.mime {
|
||||
if let Some(mime) = &gf.mime {
|
||||
response = response.header(header::CONTENT_TYPE, mime.essence_str());
|
||||
}
|
||||
if let Some((range, size)) = content_range {
|
||||
|
|
143
tests/fixtures/mod.rs
vendored
143
tests/fixtures/mod.rs
vendored
|
@ -1,4 +1,4 @@
|
|||
use std::{collections::BTreeMap, ops::Deref};
|
||||
use std::{collections::BTreeMap, fs::File, ops::Deref};
|
||||
|
||||
use hex_literal::hex;
|
||||
use path_macro::path;
|
||||
|
@ -7,27 +7,47 @@ use temp_testdir::TempDir;
|
|||
use time::macros::datetime;
|
||||
|
||||
use talon::{
|
||||
config::{CompressionCfg, Config, ConfigInner},
|
||||
db::{
|
||||
model::{Version, Website},
|
||||
Db,
|
||||
},
|
||||
storage::Storage,
|
||||
Talon,
|
||||
};
|
||||
|
||||
pub const SUBDOMAIN_1: &str = "-";
|
||||
pub const SUBDOMAIN_2: &str = "spotify-gender-ex";
|
||||
pub const SUBDOMAIN_3: &str = "rustypipe";
|
||||
pub const SUBDOMAIN_4: &str = "spa";
|
||||
|
||||
pub const VERSION_1_1: u32 = 1;
|
||||
pub const VERSION_1_2: u32 = 2;
|
||||
pub const VERSION_2_1: u32 = 3;
|
||||
pub const VERSION_3_1: u32 = 4;
|
||||
pub const VERSION_4_1: u32 = 5;
|
||||
|
||||
pub const HASH_1_1_INDEX: [u8; 32] =
|
||||
hex!("3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68");
|
||||
pub const HASH_1_1_STYLE: [u8; 32] =
|
||||
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 {
|
||||
db: Db,
|
||||
|
@ -91,6 +111,18 @@ fn insert_websites(db: &Db) {
|
|||
},
|
||||
)
|
||||
.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();
|
||||
v1_data.insert("Version".to_owned(), "v0.1.0".to_owned());
|
||||
|
@ -134,7 +166,6 @@ fn insert_websites(db: &Db) {
|
|||
VERSION_2_1,
|
||||
&Version {
|
||||
created_at: datetime!(2023-02-18 16:30 +0),
|
||||
data: BTreeMap::new(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
@ -145,7 +176,19 @@ fn insert_websites(db: &Db) {
|
|||
VERSION_3_1,
|
||||
&Version {
|
||||
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()
|
||||
},
|
||||
)
|
||||
|
@ -163,17 +206,9 @@ pub fn db() -> DbTest {
|
|||
db.insert_file(VERSION_1_1, "style.css", &HASH_1_1_STYLE)
|
||||
.unwrap();
|
||||
|
||||
db.insert_file(
|
||||
VERSION_1_2,
|
||||
"index.html",
|
||||
&hex!("a44816e6c3b650bdf88e6532659ba07ef187c2113ae311da9709e056aec8eadb"),
|
||||
)
|
||||
db.insert_file(VERSION_1_2, "index.html", &HASH_1_2_INDEX)
|
||||
.unwrap();
|
||||
db.insert_file(
|
||||
VERSION_1_2,
|
||||
"assets/style.css",
|
||||
&hex!("356f131c825fbf604797c7e9c85352549d81db8af91fee834016d075110af026"),
|
||||
)
|
||||
db.insert_file(VERSION_1_2, "assets/style.css", &HASH_1_2_STYLE)
|
||||
.unwrap();
|
||||
db.insert_file(
|
||||
VERSION_1_2,
|
||||
|
@ -194,80 +229,64 @@ pub fn db() -> DbTest {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
db.insert_file(
|
||||
VERSION_2_1,
|
||||
"index.html",
|
||||
&hex!("6c5d37546616519e8973be51515b8a90898b4675f7b6d01f2d891edb686408a2"),
|
||||
)
|
||||
db.insert_file(VERSION_2_1, "index.html", &HASH_2_1_INDEX)
|
||||
.unwrap();
|
||||
db.insert_file(
|
||||
VERSION_2_1,
|
||||
"gex_style.css",
|
||||
&hex!("fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"),
|
||||
)
|
||||
db.insert_file(VERSION_2_1, "gex_style.css", &HASH_2_1_STYLE)
|
||||
.unwrap();
|
||||
|
||||
db.insert_file(
|
||||
VERSION_3_1,
|
||||
"index.html",
|
||||
&hex!("94a67cf13d752a9c1875ad999eb2be5a1b0f9746c66bca2631820b8186028811"),
|
||||
)
|
||||
db.insert_file(VERSION_3_1, "index.html", &HASH_3_1_INDEX)
|
||||
.unwrap();
|
||||
db.insert_file(
|
||||
VERSION_3_1,
|
||||
"rp_style.css",
|
||||
&hex!("ee4fc4911a56e627c047a29ba3085131939d8d487759b9149d42aaab89ce8993"),
|
||||
)
|
||||
db.insert_file(VERSION_3_1, "rp_style.css", &HASH_3_1_STYLE)
|
||||
.unwrap();
|
||||
db.insert_file(VERSION_3_1, "page2/index.html", &HASH_3_1_PAGE2)
|
||||
.unwrap();
|
||||
|
||||
DbTest { db, _temp: temp }
|
||||
}
|
||||
|
||||
pub struct StorageTest {
|
||||
store: Storage,
|
||||
_temp: TempDir,
|
||||
pub struct TalonTest {
|
||||
talon: Talon,
|
||||
pub temp: TempDir,
|
||||
}
|
||||
|
||||
impl Deref for StorageTest {
|
||||
type Target = Storage;
|
||||
impl Deref for TalonTest {
|
||||
type Target = Talon;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.store
|
||||
&self.talon
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
pub fn store() -> StorageTest {
|
||||
pub fn tln() -> TalonTest {
|
||||
let temp = temp_testdir::TempDir::default();
|
||||
let db_path = path!(temp / "db");
|
||||
std::fs::create_dir(&db_path).unwrap();
|
||||
let talon = Talon::new(&temp).unwrap();
|
||||
|
||||
let cfg = Config::new(ConfigInner {
|
||||
compression: CompressionCfg {
|
||||
gzip_en: true,
|
||||
brotli_en: true,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
insert_websites(&talon.db);
|
||||
|
||||
let db = Db::new(&db_path).unwrap();
|
||||
insert_websites(&db);
|
||||
|
||||
let store = Storage::new(temp.to_path_buf(), db, cfg);
|
||||
|
||||
store
|
||||
talon
|
||||
.storage
|
||||
.insert_dir(path!("tests" / "testfiles" / "ThetaDev0"), VERSION_1_1)
|
||||
.unwrap();
|
||||
store
|
||||
talon
|
||||
.storage
|
||||
.insert_dir(path!("tests" / "testfiles" / "ThetaDev1"), VERSION_1_2)
|
||||
.unwrap();
|
||||
store
|
||||
talon
|
||||
.storage
|
||||
.insert_dir(path!("tests" / "testfiles" / "GenderEx"), VERSION_2_1)
|
||||
.unwrap();
|
||||
store
|
||||
talon
|
||||
.storage
|
||||
.insert_dir(path!("tests" / "testfiles" / "RustyPipe"), VERSION_3_1)
|
||||
.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 }
|
||||
}
|
||||
|
|
|
@ -5,14 +5,14 @@ expression: "&cfg"
|
|||
ConfigInner(
|
||||
server: ServerCfg(
|
||||
address: "0.0.0.0:3000",
|
||||
root_domain: "localhost",
|
||||
root_domain: "localhost:3000",
|
||||
internal_subdomain: "talon",
|
||||
internal_url: "http://talon.localhost",
|
||||
internal_url: "http://talon.localhost:3000",
|
||||
),
|
||||
compression: CompressionCfg(
|
||||
gzip_en: true,
|
||||
gzip_level: 6,
|
||||
brotli_en: false,
|
||||
brotli_en: true,
|
||||
brotli_level: 7,
|
||||
),
|
||||
keys: {
|
||||
|
|
|
@ -3,10 +3,13 @@ source: tests/tests.rs
|
|||
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":"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":"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":"file","key":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}
|
||||
{"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"}
|
||||
|
|
|
@ -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":"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":"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":"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":"file","key":"1:index.html","value":"3b5f6bad5376897435def176d0fe77e5b9b4f0deafc7491fc27262650744ad68"}
|
||||
{"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":"3:gex_style.css","value":"fc825b409a49724af8f5b3c4ad15e175e68095ea746237a7b46152d3f383f541"}
|
||||
{"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"}
|
||||
|
|
|
@ -21,6 +21,15 @@ expression: websites
|
|||
source_url: Some("https://code.thetadev.de/ThetaDev/rustypipe"),
|
||||
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(
|
||||
name: "Spotify-Gender-Ex",
|
||||
created_at: (2023, 49, 16, 30, 0, 0, 0, 0, 0),
|
||||
|
|
BIN
tests/testfiles/archive/spa.tar.gz
Normal file
BIN
tests/testfiles/archive/spa.tar.gz
Normal file
Binary file not shown.
265
tests/tests.rs
265
tests/tests.rs
|
@ -176,15 +176,22 @@ mod database {
|
|||
#[rstest]
|
||||
fn get_file_hashes(db: DbTest) {
|
||||
let hashes = db.get_file_hashes().unwrap();
|
||||
assert_eq!(hashes.len(), 10)
|
||||
assert_eq!(hashes.len(), 11)
|
||||
}
|
||||
}
|
||||
|
||||
mod storage {
|
||||
use std::{str::FromStr, time::SystemTime};
|
||||
|
||||
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::storage::Storage;
|
||||
use talon::storage::{GotFile, Storage};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -297,7 +304,7 @@ mod storage {
|
|||
#[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/"))]
|
||||
fn get_file(
|
||||
store: StorageTest,
|
||||
tln: TalonTest,
|
||||
#[case] encoding: &str,
|
||||
#[case] version: u32,
|
||||
#[case] path: &str,
|
||||
|
@ -318,7 +325,8 @@ mod storage {
|
|||
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_eq!(
|
||||
index_file
|
||||
|
@ -334,6 +342,213 @@ mod storage {
|
|||
assert_eq!(index_file.mime.unwrap().essence_str(), mime);
|
||||
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 {
|
||||
|
@ -351,3 +566,43 @@ mod config {
|
|||
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>()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue