#![allow(clippy::needless_pass_by_value)]
use http::{Response, header};
use crate::app_state::AppState;
use crate::router::{Body, form_field, parse_form_body, redirect};
use crate::routes::lang::return_path;
use crate::routes::layout::{THEME_COOKIE, ThemeChoice};
const COOKIE_MAX_AGE: u32 = 31_536_000;
pub async fn set_theme(
_state: AppState,
parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let mut response = redirect(&return_path(&parts));
if let Some(choice) = chosen_theme(&parts)
&& let Ok(value) = header::HeaderValue::from_str(&theme_cookie(choice))
{
response.headers_mut().insert(header::SET_COOKIE, value);
}
response
}
fn chosen_theme(parts: &http::request::Parts) -> Option<ThemeChoice> {
let form = parse_form_body(parts).ok()?;
ThemeChoice::from_value(form_field(&form, "theme")?)
}
fn theme_cookie(choice: ThemeChoice) -> String {
format!(
"{THEME_COOKIE}={}; Path=/; Max-Age={COOKIE_MAX_AGE}; SameSite=Lax; Secure",
choice.as_str()
)
}
#[cfg(test)]
mod tests {
#![allow(clippy::missing_panics_doc)]
use bytes::Bytes;
use http::Request;
use super::{chosen_theme, theme_cookie};
use crate::router::RequestBody;
use crate::routes::layout::ThemeChoice;
fn parts_with_form(form: &str) -> http::request::Parts {
let (mut parts, ()) = Request::builder()
.method("POST")
.uri("/preferences/theme")
.body(())
.expect("request builds")
.into_parts();
parts
.extensions
.insert(RequestBody(Bytes::copy_from_slice(form.as_bytes())));
parts
}
#[test]
fn each_valid_choice_is_accepted() {
for (value, want) in [
("system", ThemeChoice::System),
("light", ThemeChoice::Light),
("dark", ThemeChoice::Dark),
] {
let parts = parts_with_form(&format!("theme={value}"));
assert_eq!(chosen_theme(&parts), Some(want), "theme={value}");
}
}
#[test]
fn unknown_or_missing_values_are_rejected() {
for form in ["theme=purple", "theme=", "theme=dark%3Bx", "other=dark", ""] {
assert_eq!(chosen_theme(&parts_with_form(form)), None, "form={form:?}");
}
}
#[test]
fn a_request_without_a_body_is_rejected() {
let (parts, ()) = Request::builder()
.method("POST")
.uri("/preferences/theme")
.body(())
.expect("request builds")
.into_parts();
assert_eq!(chosen_theme(&parts), None);
}
#[test]
fn cookie_is_site_wide_secure_samesite_lax_and_script_writable() {
let cookie = theme_cookie(ThemeChoice::Dark);
assert!(cookie.starts_with("csaf_theme=dark;"), "{cookie}");
for attr in ["Path=/", "Max-Age=31536000", "SameSite=Lax", "Secure"] {
assert!(cookie.contains(attr), "{attr} missing from {cookie}");
}
assert!(!cookie.contains("HttpOnly"), "{cookie}");
}
}