mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
chore(repo): prune dead code and duplicate abstractions (#3413)
This commit is contained in:
1 parent
c17091768c
commit
c9fdf8fead
2937 files changed
+42233
-176793
No files matched your search
@@ -386,32 +386,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_environment_favicon_falls_back_to_the_icon() {
|
||||
let favicon = |favicon: Option<&str>, icon: Option<&str>| {
|
||||
snapshot_from_environment(|name| match name {
|
||||
"FLUXER_APP_FAVICON_URL" => favicon.map(str::to_owned),
|
||||
"FLUXER_APP_ICON_URL" => icon.map(str::to_owned),
|
||||
_ => None,
|
||||
})
|
||||
.favicon_url
|
||||
.map(|url| url.to_string())
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
favicon(
|
||||
Some("https://example.test/favicon.png"),
|
||||
Some("https://example.test/icon.png")
|
||||
),
|
||||
Some("https://example.test/favicon.png".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
favicon(Some(" "), Some("https://example.test/icon.png")),
|
||||
Some("https://example.test/icon.png".to_owned())
|
||||
);
|
||||
assert_eq!(favicon(None, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn environment_branding_images_yield_their_distinct_origins() {
|
||||
let snapshot = snapshot_from_environment(|name| match name {
|
||||
@@ -429,57 +403,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovery_fills_every_head_field_from_the_branding_object() {
|
||||
let snapshot = snapshot_from_discovery(&serde_json::json!({
|
||||
"app_public": {
|
||||
"branding": {
|
||||
"product_name": " Example Chat ",
|
||||
"icon_url": "https://example.test/icon.png",
|
||||
"favicon_url": "https://example.test/fav icon.png",
|
||||
"theme_color": "#7b2ce4"
|
||||
}
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(snapshot.product_name.as_deref(), Some("Example Chat"));
|
||||
assert_eq!(
|
||||
snapshot.icon_url.as_deref(),
|
||||
Some("https://example.test/icon.png")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.favicon_url.as_deref(),
|
||||
Some("https://example.test/fav%20icon.png")
|
||||
);
|
||||
assert_eq!(snapshot.theme_color.as_deref(), Some("#7b2ce4"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_environment_fills_every_head_field() {
|
||||
let snapshot = snapshot_from_environment(|name| match name {
|
||||
"FLUXER_APP_PRODUCT_NAME" => Some("Example".to_owned()),
|
||||
"FLUXER_APP_ICON_URL" => Some("https://example.test/icon.png".to_owned()),
|
||||
"FLUXER_APP_THEME_COLOR" => Some("7B2CE4".to_owned()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
assert_eq!(snapshot.product_name.as_deref(), Some("Example"));
|
||||
assert_eq!(
|
||||
snapshot.icon_url.as_deref(),
|
||||
Some("https://example.test/icon.png")
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.favicon_url.as_deref(),
|
||||
Some("https://example.test/icon.png")
|
||||
);
|
||||
assert_eq!(snapshot.theme_color.as_deref(), Some("#7b2ce4"));
|
||||
assert_eq!(
|
||||
snapshot_from_environment(|_| Some(" ".to_owned())),
|
||||
BrandingSnapshot::default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_theme_colour_is_normalised_like_the_api() {
|
||||
assert_eq!(theme_color("#7B2CE4").as_deref(), Some("#7b2ce4"));
|
||||
|
||||
@@ -148,10 +148,6 @@ impl HttpEndpoint {
|
||||
Ok(Self { url, csp_origin })
|
||||
}
|
||||
|
||||
pub fn as_url(&self) -> &Url {
|
||||
&self.url
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.url.as_str().trim_end_matches('/')
|
||||
}
|
||||
@@ -362,7 +358,6 @@ pub struct AppProxyConfig {
|
||||
pub branding: crate::branding_images::BrandingSnapshot,
|
||||
pub discovery_upstream_url: String,
|
||||
pub discovery_refresh_interval_ms: u64,
|
||||
pub release_channel: ReleaseChannel,
|
||||
pub build_version: String,
|
||||
pub csp: CspConfig,
|
||||
pub same_origin_hosts: Vec<String>,
|
||||
@@ -370,29 +365,6 @@ pub struct AppProxyConfig {
|
||||
pub self_hosted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ReleaseChannel {
|
||||
Stable,
|
||||
Canary,
|
||||
}
|
||||
|
||||
impl ReleaseChannel {
|
||||
fn from_env_value(value: &str) -> Self {
|
||||
if value.eq_ignore_ascii_case("canary") {
|
||||
Self::Canary
|
||||
} else {
|
||||
Self::Stable
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Stable => "stable",
|
||||
Self::Canary => "canary",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CspConfig {
|
||||
pub extra_default_src: Vec<CspSource>,
|
||||
@@ -448,9 +420,6 @@ fn read_csp_report_uri(name: &'static str) -> Option<CspReportUri> {
|
||||
|
||||
impl AppProxyConfig {
|
||||
pub fn from_env() -> Self {
|
||||
let release_channel =
|
||||
ReleaseChannel::from_env_value(&cfg::read_env("RELEASE_CHANNEL", "stable"));
|
||||
|
||||
let s3_public_endpoint = parse_optional_http_endpoint(
|
||||
"FLUXER_S3_PUBLIC_ENDPOINT",
|
||||
cfg::env_value("FLUXER_S3_PUBLIC_ENDPOINT"),
|
||||
@@ -495,7 +464,6 @@ impl AppProxyConfig {
|
||||
&cfg::read_env("DISCOVERY_REFRESH_INTERVAL_MS", "60000"),
|
||||
60_000u64,
|
||||
),
|
||||
release_channel,
|
||||
build_version: cfg::read_first_env(
|
||||
&["BUILD_VERSION", "FLUXER_BUILD_VERSION"],
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
|
||||
@@ -53,7 +53,7 @@ fn main() -> anyhow::Result<()> {
|
||||
tracing::info!(%addr, "starting Fluxer app proxy");
|
||||
|
||||
axum::serve(listener, router)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.with_graceful_shutdown(fluxer_common::shutdown::wait_for_shutdown())
|
||||
.await
|
||||
.context("app proxy server exited unexpectedly")?;
|
||||
|
||||
@@ -63,27 +63,3 @@ fn main() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install Ctrl+C handler");
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install SIGTERM handler")
|
||||
.recv()
|
||||
.await;
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let terminate = std::future::pending::<()>();
|
||||
|
||||
tokio::select! {
|
||||
_ = ctrl_c => {},
|
||||
_ = terminate => {},
|
||||
}
|
||||
}
|
||||
@@ -330,19 +330,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upstream_failure_is_never_stamped_with_an_asset_lifetime() {
|
||||
let response = proxied_asset(
|
||||
StatusCode::NOT_FOUND,
|
||||
"no-store",
|
||||
"2d715e4730758083.worker.js",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(cache_control_of(&response), Some("no-store"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_not_found_with_a_long_upstream_lifetime_is_rewritten_to_no_store() {
|
||||
let response = proxied_asset(
|
||||
@@ -360,38 +347,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_bad_gateway_with_a_long_upstream_lifetime_is_rewritten_to_no_store() {
|
||||
let response = proxied_asset(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"public, max-age=604800",
|
||||
"2d715e4730758083.worker.js",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
|
||||
assert_eq!(
|
||||
cache_control_of(&response),
|
||||
Some(UPSTREAM_FAILURE_CACHE_CONTROL)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_server_error_with_a_long_upstream_lifetime_is_rewritten_to_no_store() {
|
||||
let response = proxied_asset(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"public, max-age=86400, immutable",
|
||||
"voice_engine_bg.wasm",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(
|
||||
cache_control_of(&response),
|
||||
Some(UPSTREAM_FAILURE_CACHE_CONTROL)
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_test_asset_policy(headers: &mut HeaderMap, path: &str, status: StatusCode) {
|
||||
apply_asset_response_policy(
|
||||
headers,
|
||||
@@ -1087,24 +1042,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_wasm_asset_overrides_upstream_octet_stream() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/octet-stream"),
|
||||
);
|
||||
|
||||
apply_test_asset_policy(&mut headers, "voice_engine_bg.wasm", StatusCode::OK);
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("application/wasm")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxied_font_gains_cors_when_upstream_omits_it() {
|
||||
let mut headers = HeaderMap::new();
|
||||
@@ -1176,24 +1113,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_asset_preserves_upstream_content_type() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/octet-stream"),
|
||||
);
|
||||
|
||||
apply_test_asset_policy(&mut headers, "artifact.unknown-extension", StatusCode::OK);
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("application/octet-stream")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_local_asset_is_refused_once_the_read_slots_are_gone() {
|
||||
let fixture = LocalAssetDir::with_asset("356aaade04a117b1.js", b"console.log(1)");
|
||||
|
||||
@@ -5,25 +5,13 @@ use axum::{
|
||||
http::{HeaderMap, HeaderValue, StatusCode, header},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use fluxer_common::http_range::{RangeSelection, parse_range};
|
||||
use std::path::Path;
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
const STREAM_CHUNK_BYTES: usize = 64 * 1024;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
struct ByteRange {
|
||||
start: u64,
|
||||
end: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum RequestedRange {
|
||||
Whole,
|
||||
Partial(ByteRange),
|
||||
Unsatisfiable,
|
||||
}
|
||||
|
||||
pub(crate) async fn stream_file(
|
||||
path: &Path,
|
||||
request_headers: &HeaderMap,
|
||||
@@ -37,13 +25,13 @@ pub(crate) async fn stream_file(
|
||||
let file_size = metadata.len();
|
||||
|
||||
let (status, start, length) = match requested_range(request_headers, entity_tag, file_size) {
|
||||
RequestedRange::Whole => (StatusCode::OK, 0, file_size),
|
||||
RequestedRange::Partial(range) => (
|
||||
RangeSelection::Full => (StatusCode::OK, 0, file_size),
|
||||
RangeSelection::Partial(range) => (
|
||||
StatusCode::PARTIAL_CONTENT,
|
||||
range.start,
|
||||
range.end - range.start + 1,
|
||||
range.start as u64,
|
||||
(range.end - range.start + 1) as u64,
|
||||
),
|
||||
RequestedRange::Unsatisfiable => return Ok(range_not_satisfiable(file_size)),
|
||||
RangeSelection::Unsatisfiable => return Ok(range_not_satisfiable(file_size)),
|
||||
};
|
||||
|
||||
if start > 0 {
|
||||
@@ -79,17 +67,20 @@ fn requested_range(
|
||||
headers: &HeaderMap,
|
||||
entity_tag: Option<&str>,
|
||||
file_size: u64,
|
||||
) -> RequestedRange {
|
||||
) -> RangeSelection {
|
||||
let Some(raw) = headers
|
||||
.get(header::RANGE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
else {
|
||||
return RequestedRange::Whole;
|
||||
return RangeSelection::Full;
|
||||
};
|
||||
if !if_range_matches(headers, entity_tag) {
|
||||
return RequestedRange::Whole;
|
||||
return RangeSelection::Full;
|
||||
}
|
||||
parse_byte_range(raw, file_size)
|
||||
let Ok(file_size) = usize::try_from(file_size) else {
|
||||
return RangeSelection::Full;
|
||||
};
|
||||
parse_range(Some(raw), file_size)
|
||||
}
|
||||
|
||||
fn if_range_matches(headers: &HeaderMap, entity_tag: Option<&str>) -> bool {
|
||||
@@ -102,57 +93,6 @@ fn if_range_matches(headers: &HeaderMap, entity_tag: Option<&str>) -> bool {
|
||||
entity_tag.is_some_and(|tag| raw.trim() == tag)
|
||||
}
|
||||
|
||||
fn parse_byte_range(raw: &str, file_size: u64) -> RequestedRange {
|
||||
let Some(spec) = raw.trim_matches([' ', '\t']).strip_prefix("bytes=") else {
|
||||
return RequestedRange::Whole;
|
||||
};
|
||||
if spec.contains(',') {
|
||||
return RequestedRange::Whole;
|
||||
}
|
||||
let Some((start_part, end_part)) = spec.split_once('-') else {
|
||||
return RequestedRange::Whole;
|
||||
};
|
||||
let start_part = start_part.trim_matches([' ', '\t']);
|
||||
let end_part = end_part.trim_matches([' ', '\t']);
|
||||
if start_part.is_empty() && end_part.is_empty() {
|
||||
return RequestedRange::Whole;
|
||||
}
|
||||
if file_size == 0 {
|
||||
return RequestedRange::Unsatisfiable;
|
||||
}
|
||||
|
||||
if start_part.is_empty() {
|
||||
let Ok(suffix_len) = end_part.parse::<u64>() else {
|
||||
return RequestedRange::Whole;
|
||||
};
|
||||
if suffix_len == 0 {
|
||||
return RequestedRange::Unsatisfiable;
|
||||
}
|
||||
return RequestedRange::Partial(ByteRange {
|
||||
start: file_size - suffix_len.min(file_size),
|
||||
end: file_size - 1,
|
||||
});
|
||||
}
|
||||
|
||||
let Ok(start) = start_part.parse::<u64>() else {
|
||||
return RequestedRange::Whole;
|
||||
};
|
||||
let requested_end = if end_part.is_empty() {
|
||||
file_size - 1
|
||||
} else if let Ok(end) = end_part.parse::<u64>() {
|
||||
end
|
||||
} else {
|
||||
return RequestedRange::Whole;
|
||||
};
|
||||
if start >= file_size || requested_end < start {
|
||||
return RequestedRange::Unsatisfiable;
|
||||
}
|
||||
RequestedRange::Partial(ByteRange {
|
||||
start,
|
||||
end: requested_end.min(file_size - 1),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -364,58 +304,4 @@ mod tests {
|
||||
|
||||
assert_ne!(err.kind(), std::io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_malformed_range_is_ignored() {
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=abc-def", 100),
|
||||
RequestedRange::Whole
|
||||
);
|
||||
assert_eq!(parse_byte_range("bytes=", 100), RequestedRange::Whole);
|
||||
assert_eq!(parse_byte_range("items=0-9", 100), RequestedRange::Whole);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_file_satisfies_no_range() {
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=0-9", 0),
|
||||
RequestedRange::Unsatisfiable
|
||||
);
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=-5", 0),
|
||||
RequestedRange::Unsatisfiable
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zero_length_suffix_is_unsatisfiable() {
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=-0", 100),
|
||||
RequestedRange::Unsatisfiable
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_suffix_longer_than_the_file_is_clamped_to_the_whole_file() {
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=-9999", 100),
|
||||
RequestedRange::Partial(ByteRange { start: 0, end: 99 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_end_past_the_last_byte_is_clamped() {
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=90-9999", 100),
|
||||
RequestedRange::Partial(ByteRange { start: 90, end: 99 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_inverted_range_is_unsatisfiable() {
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=10-5", 100),
|
||||
RequestedRange::Unsatisfiable
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,6 @@ use tower_http::{
|
||||
trace::TraceLayer,
|
||||
};
|
||||
|
||||
const STRICT_TRANSPORT_SECURITY_VALUE: &str = "max-age=31536000; includeSubDomains; preload";
|
||||
const REFERRER_POLICY_VALUE: &str = "strict-origin-when-cross-origin";
|
||||
const X_FRAME_OPTIONS_VALUE: &str = "DENY";
|
||||
const PERMISSIONS_POLICY_VALUE: &str = "accelerometer=(), camera=(self), ch-dpr=(self), ch-save-data=(self), ch-viewport-width=(self), ch-width=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(self), payment=(), usb=()";
|
||||
|
||||
pub fn build_router(state: AppState) -> Router {
|
||||
@@ -75,19 +72,7 @@ async fn security_headers_middleware(request: Request, next: Next) -> Response {
|
||||
}
|
||||
|
||||
fn set_security_headers(headers: &mut axum::http::HeaderMap) {
|
||||
set_static_header(
|
||||
headers,
|
||||
header::STRICT_TRANSPORT_SECURITY,
|
||||
STRICT_TRANSPORT_SECURITY_VALUE,
|
||||
);
|
||||
set_static_header(headers, header::X_CONTENT_TYPE_OPTIONS, "nosniff");
|
||||
set_static_header(headers, header::REFERRER_POLICY, REFERRER_POLICY_VALUE);
|
||||
set_static_header(headers, header::X_FRAME_OPTIONS, X_FRAME_OPTIONS_VALUE);
|
||||
set_static_header(
|
||||
headers,
|
||||
HeaderName::from_static("permissions-policy"),
|
||||
PERMISSIONS_POLICY_VALUE,
|
||||
);
|
||||
fluxer_common::server::set_baseline_security_headers(headers, PERMISSIONS_POLICY_VALUE);
|
||||
}
|
||||
|
||||
async fn cache_headers_middleware(request: Request, next: Next) -> Response {
|
||||
@@ -131,9 +116,3 @@ pub(crate) fn capacity_refused_response() -> Response {
|
||||
headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
|
||||
response
|
||||
}
|
||||
|
||||
fn set_static_header(headers: &mut axum::http::HeaderMap, name: HeaderName, value: &'static str) {
|
||||
headers
|
||||
.entry(name)
|
||||
.or_insert(HeaderValue::from_static(value));
|
||||
}
|
||||
@@ -700,7 +700,7 @@ mod tests {
|
||||
}
|
||||
|
||||
use crate::branding_images::InstanceBranding;
|
||||
use crate::config::{AppProxyConfig, ReleaseChannel};
|
||||
use crate::config::AppProxyConfig;
|
||||
use crate::state::LOCAL_FILE_READS_IN_FLIGHT_MAX;
|
||||
use axum::Router;
|
||||
use axum::body::Body;
|
||||
@@ -1046,27 +1046,6 @@ mod tests {
|
||||
(read_document(response).await, policy)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_self_hosted_instance_serves_its_branded_favicon() {
|
||||
let (served, _) = served_self_hosted_head(BrandingSnapshot {
|
||||
image_origins: Arc::from([branding_origin()]),
|
||||
favicon_url: Some(Arc::from("https://example.test/favicon.png?v=1&size=32")),
|
||||
..BrandingSnapshot::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(served.matches(r#"<link rel="icon""#).count(), 1);
|
||||
assert!(served.contains(
|
||||
r#"<link rel="icon" href="https://example.test/favicon.png?v=1&size=32">"#
|
||||
));
|
||||
assert!(!served.contains("/web/favicon-32x32.png"));
|
||||
assert_eq!(served.matches(r#"<link rel="apple-touch-icon""#).count(), 1);
|
||||
assert!(served.contains(
|
||||
r#"<link rel="apple-touch-icon" href="https://example.test/favicon.png?v=1&size=32">"#
|
||||
));
|
||||
assert!(!served.contains("/web/apple-touch-icon.png"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_self_hosted_instance_serves_its_branded_head() {
|
||||
let (served, policy) = served_self_hosted_head(full_branding()).await;
|
||||
@@ -1096,18 +1075,6 @@ mod tests {
|
||||
assert_every_inline_script_is_granted(&served, &policy);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_self_hosted_instance_without_branding_keeps_the_default_head() {
|
||||
let (served, _) = served_self_hosted_head(BrandingSnapshot::default()).await;
|
||||
|
||||
assert_eq!(
|
||||
served,
|
||||
strip_link_preview_metadata(
|
||||
&render_spa_document(SHIPPED_APP_SHELL, "", "", None).unwrap()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branded_head_values_cannot_break_out_of_their_markup() {
|
||||
let html = r#"<head><title>Fluxer</title><meta name="apple-mobile-web-app-title" content="Fluxer"><link rel="icon" type="image/png" href="/web/favicon-32x32.png"><link rel="apple-touch-icon" href="/web/apple-touch-icon.png"></head>"#;
|
||||
@@ -1318,7 +1285,6 @@ mod tests {
|
||||
self_hosted: bool,
|
||||
) -> AppState {
|
||||
let mut config = AppProxyConfig::from_env();
|
||||
config.release_channel = ReleaseChannel::Stable;
|
||||
config.index_upstream_url = match &spa_index_source {
|
||||
SpaIndexSource::Bundled { .. } => None,
|
||||
SpaIndexSource::Upstream(url) => Some((**url).clone()),
|
||||
|
||||
@@ -345,45 +345,6 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_self_hosted_manifest_takes_the_instance_branding() {
|
||||
let manifest = branded_manifest(&BrandingSnapshot {
|
||||
product_name: Some("Example Chat".into()),
|
||||
icon_url: Some("https://example.test/icon.png".into()),
|
||||
favicon_url: Some("https://example.test/favicon.png".into()),
|
||||
theme_color: Some("#7b2ce4".into()),
|
||||
..BrandingSnapshot::default()
|
||||
});
|
||||
|
||||
assert_eq!(manifest["name"], "Example Chat");
|
||||
assert_eq!(manifest["short_name"], "Example Chat");
|
||||
assert_eq!(manifest["theme_color"], "#7b2ce4");
|
||||
assert_eq!(
|
||||
manifest["icons"],
|
||||
serde_json::json!([{
|
||||
"src": "https://example.test/icon.png",
|
||||
"sizes": "any",
|
||||
"purpose": "any"
|
||||
}])
|
||||
);
|
||||
assert!(manifest.get("description").is_none());
|
||||
assert_eq!(manifest["background_color"], "#2b2d31");
|
||||
assert_eq!(manifest["start_url"], "/app");
|
||||
assert_installable_shape(&manifest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unbranded_self_hosted_manifest_only_loses_its_description() {
|
||||
let manifest = branded_manifest(&BrandingSnapshot::default());
|
||||
|
||||
let mut expected: serde_json::Value =
|
||||
serde_json::from_str(&GENERATED_MANIFEST.replace("{{STATIC_CDN_ENDPOINT}}", ""))
|
||||
.unwrap();
|
||||
expected.as_object_mut().unwrap().remove("description");
|
||||
assert_eq!(manifest, expected);
|
||||
assert_installable_shape(&manifest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_branded_product_name_stays_a_json_string() {
|
||||
let product_name = "A \"quoted\" \\ name</script>\n\u{2028}";
|
||||
@@ -488,51 +449,6 @@ mod tests {
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
fn location_of(response: &Response) -> &str {
|
||||
assert_eq!(response.status(), StatusCode::FOUND);
|
||||
response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
.expect("the favicon redirect has no location")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_favicon_ico_redirects_to_the_bundled_icon_on_the_official_instance() {
|
||||
let root = static_dir_with_generated_files();
|
||||
let mut state = state_serving(&root, false);
|
||||
|
||||
let response = fetch(state.clone(), "/favicon.ico").await;
|
||||
assert_eq!(location_of(&response), "/web/favicon.ico");
|
||||
|
||||
std::sync::Arc::make_mut(&mut state.config).static_cdn_endpoint = Some(
|
||||
HttpEndpoint::parse("TEST_STATIC_CDN_ENDPOINT", "https://fluxerstatic.com").unwrap(),
|
||||
);
|
||||
let response = fetch(state, "/favicon.ico").await;
|
||||
assert_eq!(
|
||||
location_of(&response),
|
||||
"https://fluxerstatic.com/web/favicon.ico"
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_favicon_ico_redirects_to_the_instance_favicon_when_self_hosted() {
|
||||
let root = static_dir_with_generated_files();
|
||||
let mut state = state_serving(&root, true);
|
||||
|
||||
let response = fetch(state.clone(), "/favicon.ico").await;
|
||||
assert_eq!(location_of(&response), "https://example.test/favicon.png");
|
||||
|
||||
state.branding = crate::branding_images::InstanceBranding::new(BrandingSnapshot::default());
|
||||
let response = fetch(state, "/favicon.ico").await;
|
||||
assert_eq!(location_of(&response), "/web/favicon.ico");
|
||||
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
const GENERATED_BROWSERCONFIG: &str = r#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
|
||||
@@ -252,47 +252,6 @@ pub(crate) fn is_content_hash(value: &str) -> bool {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mime_html() {
|
||||
assert_eq!(guess_mime("i.html"), "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_js() {
|
||||
assert_eq!(guess_mime("a.js"), "application/javascript; charset=utf-8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_css() {
|
||||
assert_eq!(guess_mime("s.css"), "text/css; charset=utf-8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_wasm() {
|
||||
assert_eq!(guess_mime("m.wasm"), "application/wasm");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_woff2() {
|
||||
assert_eq!(guess_mime("f.woff2"), "font/woff2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_unknown() {
|
||||
assert_eq!(guess_mime("f.xyz"), "application/octet-stream");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_no_ext() {
|
||||
assert_eq!(guess_mime("LICENSE"), "application/octet-stream");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_case_insensitive() {
|
||||
assert_eq!(guess_mime("F.HTML"), "text/html; charset=utf-8");
|
||||
assert_eq!(guess_mime("F.JS"), "application/javascript; charset=utf-8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashed_asset_positive() {
|
||||
assert!(is_hashed_asset("app.a1b2c3d4.js"));
|
||||
|
||||
Reference in new issue
Block a user