use csaf_models::settings::{
CLASSIFICATION_STORAGE_MODES, NATO_LABELS, Settings, TLP_LABELS, VERSCHLUSSSACHE_LABELS,
is_valid_classification_storage_mode, is_valid_nato, is_valid_publisher_category,
is_valid_storage_path, is_valid_tlp, is_valid_verschlusssache,
};
use http::{Response, StatusCode};
use crate::app_state::AppState;
use crate::i18n::{self, Lang};
use crate::router::{Body, form_checkbox, form_field, html_response, parse_form_body, redirect};
use crate::routes::layout::{Nav, PageContext, html_escape, wrap_page};
pub async fn settings_form(
state: AppState,
parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let settings = state.settings();
render_settings_form(&settings, i18n::from_parts(&parts), None, None)
}
pub async fn settings_update(
state: AppState,
parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let lang = i18n::from_parts(&parts);
let form = match parse_form_body(&parts) {
Ok(f) => f,
Err(err) => {
let current = state.settings();
return render_settings_form(
¤t,
lang,
Some(&format!("Invalid form data: {err}")),
None,
);
},
};
let csaf_mode = form_field(&form, "csaf_mode").unwrap_or("2.1").to_owned();
let theme = form_field(&form, "theme").unwrap_or("light").to_owned();
let import_directory = form_field(&form, "import_directory")
.unwrap_or("./data_import")
.to_owned();
let export_directory = form_field(&form, "export_directory")
.unwrap_or("./data_export")
.to_owned();
let dump_directory = form_field(&form, "dump_directory")
.unwrap_or("./data_dump")
.to_owned();
let log_directory = form_field(&form, "log_directory")
.unwrap_or("./data_log")
.to_owned();
let naming_convention = form_field(&form, "naming_convention")
.unwrap_or("ndaal-sa-")
.to_owned();
let sidecar_sha256 = form_checkbox(&form, "sidecar_sha256");
let sidecar_sha512 = form_checkbox(&form, "sidecar_sha512");
let sidecar_sha3_512 = form_checkbox(&form, "sidecar_sha3_512");
let sidecar_blake3_512 = form_checkbox(&form, "sidecar_blake3_512");
let sidecar_shake256_512 = form_checkbox(&form, "sidecar_shake256_512");
let publisher_name = form_field(&form, "publisher_name")
.unwrap_or("")
.trim()
.to_owned();
let publisher_namespace = form_field(&form, "publisher_namespace")
.unwrap_or("")
.trim()
.to_owned();
let publisher_category = form_field(&form, "publisher_category")
.unwrap_or("vendor")
.trim()
.to_owned();
let publisher_contact_details = form_field(&form, "publisher_contact_details")
.unwrap_or("")
.trim()
.to_owned();
let tlp_default = form_field(&form, "tlp_default")
.unwrap_or("AMBER")
.trim()
.to_owned();
let verschlusssache_enabled = form_checkbox(&form, "verschlusssache_enabled");
let verschlusssache_default = form_field(&form, "verschlusssache_default")
.unwrap_or("VS-NfD (VS-NUR FÜR DEN DIENSTGEBRAUCH)")
.trim()
.to_owned();
let nato_enabled = form_checkbox(&form, "nato_enabled");
let nato_default = form_field(&form, "nato_default")
.unwrap_or("NR (NATO RESTRICTED)")
.trim()
.to_owned();
let classification_storage_mode = form_field(&form, "classification_storage_mode")
.unwrap_or("both")
.trim()
.to_owned();
if let Err(msg) = validate_settings_form(
&csaf_mode,
&theme,
&import_directory,
&export_directory,
&dump_directory,
&log_directory,
&publisher_name,
&publisher_namespace,
&publisher_category,
&tlp_default,
&verschlusssache_default,
&nato_default,
&classification_storage_mode,
lang,
) {
let current = state.settings();
return render_settings_form(¤t, lang, Some(msg), None);
}
let new_settings = Settings {
csaf_mode,
theme,
import_directory,
export_directory,
dump_directory,
log_directory,
sidecar_sha256,
sidecar_sha512,
sidecar_sha3_512,
sidecar_blake3_512,
sidecar_shake256_512,
naming_convention,
publisher_name,
publisher_namespace,
publisher_category,
publisher_contact_details,
tlp_default,
verschlusssache_enabled,
verschlusssache_default,
nato_enabled,
nato_default,
classification_storage_mode,
};
match state.update_settings(new_settings.clone()) {
Ok(()) => render_settings_form(
&new_settings,
lang,
None,
Some("Settings saved successfully."),
),
Err(err) => {
let current = state.settings();
render_settings_form(
¤t,
lang,
Some(&format!("Failed to save settings: {err}")),
None,
)
},
}
}
#[allow(
clippy::too_many_arguments,
reason = "renders the settings form; each argument is an independent field, \
and a struct would only relocate the same list"
)]
fn validate_settings_form(
csaf_mode: &str,
theme: &str,
import_directory: &str,
export_directory: &str,
dump_directory: &str,
log_directory: &str,
publisher_name: &str,
publisher_namespace: &str,
publisher_category: &str,
tlp_default: &str,
verschlusssache_default: &str,
nato_default: &str,
classification_storage_mode: &str,
lang: Lang,
) -> Result<(), &'static str> {
if csaf_mode != "2.0" && csaf_mode != "2.1" {
return Err(i18n::t(lang, "settings.err_csaf_mode"));
}
if theme != "light" && theme != "dark" {
return Err(i18n::t(lang, "settings.err_theme"));
}
if !is_valid_storage_path(import_directory) {
return Err(i18n::t(lang, "settings.err_import_directory"));
}
if !is_valid_storage_path(export_directory) {
return Err(i18n::t(lang, "settings.err_export_directory"));
}
if !is_valid_storage_path(dump_directory) {
return Err(i18n::t(lang, "settings.err_dump_directory"));
}
if !is_valid_storage_path(log_directory) {
return Err(i18n::t(lang, "settings.err_log_directory"));
}
if publisher_name.is_empty() {
return Err(i18n::t(lang, "settings.err_publisher_name"));
}
if publisher_namespace.is_empty() {
return Err(i18n::t(lang, "settings.err_publisher_namespace"));
}
if !is_valid_publisher_category(publisher_category) {
return Err(i18n::t(lang, "settings.err_publisher_category"));
}
if !is_valid_tlp(tlp_default) {
return Err(i18n::t(lang, "settings.err_tlp_default"));
}
if !is_valid_verschlusssache(verschlusssache_default) {
return Err(i18n::t(lang, "settings.err_verschlusssache_default"));
}
if !is_valid_nato(nato_default) {
return Err(i18n::t(lang, "settings.err_nato_default"));
}
if !is_valid_classification_storage_mode(classification_storage_mode) {
return Err(i18n::t(lang, "settings.err_classification_storage_mode"));
}
Ok(())
}
pub async fn settings_reset(
state: AppState,
parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let lang = i18n::from_parts(&parts);
let defaults = Settings::default();
match state.update_settings(defaults.clone()) {
Ok(()) => {
let _ = state.db_pool().with_conn(|conn| {
csaf_models::audit_log::record(
conn,
"settings_reset",
"all",
None,
Some("Settings restored to factory defaults."),
)
});
render_settings_form(
&defaults,
lang,
None,
Some("All settings were restored to defaults."),
)
},
Err(err) => {
let current = state.settings();
render_settings_form(
¤t,
lang,
Some(&format!("Failed to reset settings: {err}")),
None,
)
},
}
}
pub async fn toggle_theme(
state: AppState,
parts: http::request::Parts,
_params: Vec<(String, String)>,
) -> Response<Body> {
let mut new_settings = state.settings();
new_settings.theme = if new_settings.theme == "dark" {
"light".to_owned()
} else {
"dark".to_owned()
};
let _ = state.update_settings(new_settings);
let location = parts
.headers
.get("referer")
.and_then(|v| v.to_str().ok())
.map(str::to_owned)
.unwrap_or_else(|| "/".to_owned());
redirect(&location)
}
fn render_settings_form(
settings: &Settings,
lang: Lang,
error: Option<&str>,
success: Option<&str>,
) -> Response<Body> {
let html = wrap_page(
i18n::t(lang, "nav.settings"),
PageContext {
theme: &settings.theme,
lang,
},
Nav::Settings,
&render_settings_content(settings, lang, error, success),
);
html_response(StatusCode::OK, html)
}
fn options_from_labels(labels: &[&str], current: &str, display: impl Fn(&str) -> String) -> String {
use std::fmt::Write as _;
labels.iter().fold(String::new(), |mut out, label| {
let _ = write!(
out,
r#"<option value="{value}" {sel}>{text}</option>"#,
value = html_escape(label),
sel = if current == *label { "selected" } else { "" },
text = display(label),
);
out
})
}
fn render_settings_content(
settings: &Settings,
lang: Lang,
error: Option<&str>,
success: Option<&str>,
) -> String {
let error_html = error
.map(|e| format!(r#"<div class="flash error">{}</div>"#, html_escape(e)))
.unwrap_or_default();
let success_html = success
.map(|s| format!(r#"<div class="flash success">{}</div>"#, html_escape(s)))
.unwrap_or_default();
let sel = |v: &str, c: &str| if v == c { "selected" } else { "" };
let csaf_20_selected = sel(&settings.csaf_mode, "2.0");
let csaf_21_selected = sel(&settings.csaf_mode, "2.1");
let theme_light_selected = sel(&settings.theme, "light");
let theme_dark_selected = sel(&settings.theme, "dark");
let sha256_checked = if settings.sidecar_sha256 {
"checked"
} else {
""
};
let sha512_checked = if settings.sidecar_sha512 {
"checked"
} else {
""
};
let sha3_checked = if settings.sidecar_sha3_512 {
"checked"
} else {
""
};
let blake3_checked = if settings.sidecar_blake3_512 {
"checked"
} else {
""
};
let shake256_checked = if settings.sidecar_shake256_512 {
"checked"
} else {
""
};
let pc_vendor = sel(&settings.publisher_category, "vendor");
let pc_disc = sel(&settings.publisher_category, "discoverer");
let pc_coord = sel(&settings.publisher_category, "coordinator");
let pc_user = sel(&settings.publisher_category, "user");
let pc_trans = sel(&settings.publisher_category, "translator");
let pc_other = sel(&settings.publisher_category, "other");
let tlp_options = options_from_labels(TLP_LABELS, &settings.tlp_default, |l| {
format!("TLP:{}", html_escape(l))
});
let vs_options = options_from_labels(
VERSCHLUSSSACHE_LABELS,
&settings.verschlusssache_default,
html_escape,
);
let nato_options = options_from_labels(NATO_LABELS, &settings.nato_default, html_escape);
let storage_options = options_from_labels(
CLASSIFICATION_STORAGE_MODES,
&settings.classification_storage_mode,
|mode| {
match mode {
"distribution_text" => {
i18n::t(lang, "settings.storage_mode_option_distribution_text")
},
"notes" => i18n::t(lang, "settings.storage_mode_option_notes"),
_ => i18n::t(lang, "settings.storage_mode_option_both"),
}
.to_owned()
},
);
let vs_checked = if settings.verschlusssache_enabled {
"checked"
} else {
""
};
let nato_checked = if settings.nato_enabled { "checked" } else { "" };
let content = format!(
r#"<h1>{page_title}</h1>
{error_html}
{success_html}
<div class="card">
<form method="POST" action="/settings">
<h2>{section_general}</h2>
<div class="form-row">
<div>
<label for="csaf_mode">{csaf_mode_label}</label>
<select name="csaf_mode" id="csaf_mode">
<option value="2.0" {csaf_20_selected}>CSAF 2.0</option>
<option value="2.1" {csaf_21_selected}>CSAF 2.1</option>
</select>
<div class="muted">{csaf_mode_help}</div>
</div>
<div>
<label for="theme">{theme_label}</label>
<select name="theme" id="theme">
<option value="light" {theme_light_selected}>{theme_light}</option>
<option value="dark" {theme_dark_selected}>{theme_dark}</option>
</select>
<div class="muted">{theme_help}</div>
</div>
</div>
<div class="form-row">
<div>
<label for="import_directory">{import_directory_label}</label>
<input type="text" name="import_directory" id="import_directory" value="{import_dir}">
<div class="muted">{import_directory_help}</div>
</div>
<div>
<label for="export_directory">{export_directory_label}</label>
<input type="text" name="export_directory" id="export_directory" value="{export_dir}">
<div class="muted">{export_directory_help}</div>
</div>
</div>
<div class="form-row">
<div>
<label for="dump_directory">{dump_directory_label}</label>
<input type="text" name="dump_directory" id="dump_directory" value="{dump_dir}">
<div class="muted">{dump_directory_help}</div>
</div>
<div>
<label for="log_directory">{log_directory_label}</label>
<input type="text" name="log_directory" id="log_directory" value="{log_dir}">
<div class="muted">{log_directory_help}</div>
</div>
</div>
<div class="form-row single">
<div>
<label for="naming_convention">{naming_convention_label}</label>
<input type="text" name="naming_convention" id="naming_convention" value="{naming}">
<div class="muted">{naming_convention_help}</div>
</div>
</div>
<div class="form-row single">
<div>
<label>{sidecar_hash_files_label}</label>
<div><label style="font-weight:normal;"><input type="checkbox" name="sidecar_sha256" {sha256_checked}> {sidecar_sha256_label}</label></div>
<div><label style="font-weight:normal;"><input type="checkbox" name="sidecar_sha512" {sha512_checked}> {sidecar_sha512_label}</label></div>
<div><label style="font-weight:normal;"><input type="checkbox" name="sidecar_sha3_512" {sha3_checked}> {sidecar_sha3_512_label}</label></div>
<div><label style="font-weight:normal;"><input type="checkbox" name="sidecar_blake3_512" {blake3_checked}> {sidecar_blake3_512_label}</label></div>
<div><label style="font-weight:normal;"><input type="checkbox" name="sidecar_shake256_512" {shake256_checked}> {sidecar_shake256_512_label}</label></div>
</div>
</div>
<h2>{section_classification}</h2>
<p class="muted">{classification_intro}</p>
<div class="form-row">
<div>
<label for="tlp_default">{tlp_default_label}</label>
<select name="tlp_default" id="tlp_default" class="tlp-select" required>
{tlp_options}
</select>
<div class="muted">{tlp_default_help}</div>
</div>
<div>
<label for="classification_storage_mode">{storage_mode_label}</label>
<select name="classification_storage_mode" id="classification_storage_mode" required>
{storage_options}
</select>
<div class="muted">{storage_mode_help}</div>
</div>
</div>
<div class="form-row">
<div>
<label style="font-weight:normal;">
<input type="checkbox" name="verschlusssache_enabled" {vs_checked}>
{verschlusssache_enable_label}
</label>
<label for="verschlusssache_default">{verschlusssache_default_label}</label>
<select name="verschlusssache_default" id="verschlusssache_default" required>
{vs_options}
</select>
<div class="muted">{verschlusssache_default_help}</div>
</div>
<div>
<label style="font-weight:normal;">
<input type="checkbox" name="nato_enabled" {nato_checked}>
{nato_enable_label}
</label>
<label for="nato_default">{nato_default_label}</label>
<select name="nato_default" id="nato_default" required>
{nato_options}
</select>
<div class="muted">{nato_default_help}</div>
</div>
</div>
<h2>{section_publisher}</h2>
<p class="muted">{publisher_intro}</p>
<div class="form-row">
<div>
<label for="publisher_name">{publisher_name_label}</label>
<input type="text" name="publisher_name" id="publisher_name" required value="{publisher_name}">
</div>
<div>
<label for="publisher_namespace">{publisher_namespace_label}</label>
<input type="url" name="publisher_namespace" id="publisher_namespace" required value="{publisher_namespace}">
</div>
</div>
<div class="form-row">
<div>
<label for="publisher_category">{publisher_category_label}</label>
<select name="publisher_category" id="publisher_category" required>
<option value="vendor" {pc_vendor}>vendor</option>
<option value="discoverer" {pc_disc}>discoverer</option>
<option value="coordinator" {pc_coord}>coordinator</option>
<option value="user" {pc_user}>user</option>
<option value="translator" {pc_trans}>translator</option>
<option value="other" {pc_other}>other</option>
</select>
<div class="muted">{publisher_category_help}</div>
</div>
<div>
<label for="publisher_contact_details">{publisher_contact_label}</label>
<input type="text" name="publisher_contact_details" id="publisher_contact_details" value="{publisher_contact}">
<div class="muted">{publisher_contact_help}</div>
</div>
</div>
<button type="submit" class="btn">{save_button}</button>
</form>
<hr style="margin:1.5rem 0;">
<form method="POST" action="/settings/reset"
onsubmit="return confirm('{reset_confirm}');">
<button type="submit" class="btn secondary" id="reset-settings-btn">{reset_button}</button>
<div class="muted" style="margin-top:0.25rem;">{reset_help}</div>
</form>
</div>"#,
page_title = i18n::t(lang, "nav.settings"),
section_general = i18n::t(lang, "settings.section_general"),
csaf_mode_label = i18n::t(lang, "settings.csaf_mode_label"),
csaf_mode_help = i18n::t(lang, "settings.csaf_mode_help"),
theme_label = i18n::t(lang, "settings.theme_label"),
theme_light = i18n::t(lang, "nav.theme_light"),
theme_dark = i18n::t(lang, "nav.theme_dark"),
theme_help = i18n::t(lang, "settings.theme_help"),
import_directory_label = i18n::t(lang, "settings.import_directory_label"),
import_dir = html_escape(&settings.import_directory),
import_directory_help = i18n::t(lang, "settings.import_directory_help"),
export_directory_label = i18n::t(lang, "settings.export_directory_label"),
export_dir = html_escape(&settings.export_directory),
export_directory_help = i18n::t(lang, "settings.export_directory_help"),
dump_directory_label = i18n::t(lang, "settings.dump_directory_label"),
dump_dir = html_escape(&settings.dump_directory),
dump_directory_help = i18n::t(lang, "settings.dump_directory_help"),
log_directory_label = i18n::t(lang, "settings.log_directory_label"),
log_dir = html_escape(&settings.log_directory),
log_directory_help = i18n::t(lang, "settings.log_directory_help"),
naming_convention_label = i18n::t(lang, "settings.naming_convention_label"),
naming = html_escape(&settings.naming_convention),
naming_convention_help = i18n::t(lang, "settings.naming_convention_help"),
sidecar_hash_files_label = i18n::t(lang, "settings.sidecar_hash_files_label"),
sidecar_sha256_label = i18n::t(lang, "settings.sidecar_sha256_label"),
sidecar_sha512_label = i18n::t(lang, "settings.sidecar_sha512_label"),
sidecar_sha3_512_label = i18n::t(lang, "settings.sidecar_sha3_512_label"),
sidecar_blake3_512_label = i18n::t(lang, "settings.sidecar_blake3_512_label"),
sidecar_shake256_512_label = i18n::t(lang, "settings.sidecar_shake256_512_label"),
section_classification = i18n::t(lang, "settings.section_classification"),
classification_intro = i18n::t(lang, "settings.classification_intro"),
tlp_default_label = i18n::t(lang, "settings.tlp_default_label"),
tlp_default_help = i18n::t(lang, "settings.tlp_default_help"),
storage_mode_label = i18n::t(lang, "settings.storage_mode_label"),
storage_mode_help = i18n::t(lang, "settings.storage_mode_help"),
verschlusssache_enable_label = i18n::t(lang, "settings.verschlusssache_enable_label"),
verschlusssache_default_label = i18n::t(lang, "settings.verschlusssache_default_label"),
verschlusssache_default_help = i18n::t(lang, "settings.verschlusssache_default_help"),
nato_enable_label = i18n::t(lang, "settings.nato_enable_label"),
nato_default_label = i18n::t(lang, "settings.nato_default_label"),
nato_default_help = i18n::t(lang, "settings.nato_default_help"),
section_publisher = i18n::t(lang, "settings.section_publisher"),
publisher_intro = i18n::t(lang, "settings.publisher_intro"),
publisher_name_label = i18n::t(lang, "settings.publisher_name_label"),
publisher_name = html_escape(&settings.publisher_name),
publisher_namespace_label = i18n::t(lang, "settings.publisher_namespace_label"),
publisher_namespace = html_escape(&settings.publisher_namespace),
publisher_category_label = i18n::t(lang, "settings.publisher_category_label"),
publisher_category_help = i18n::t(lang, "settings.publisher_category_help"),
publisher_contact_label = i18n::t(lang, "settings.publisher_contact_label"),
publisher_contact = html_escape(&settings.publisher_contact_details),
publisher_contact_help = i18n::t(lang, "settings.publisher_contact_help"),
save_button = i18n::t(lang, "settings.save_button"),
reset_confirm = i18n::t(lang, "settings.reset_confirm"),
reset_button = i18n::t(lang, "settings.reset_button"),
reset_help = i18n::t(lang, "settings.reset_help"),
);
content
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_settings_form_default() {
let settings = Settings::default();
let resp = render_settings_form(&settings, Lang::En, None, None);
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_render_settings_form_with_error() {
let settings = Settings::default();
let resp = render_settings_form(&settings, Lang::En, Some("test error"), None);
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_render_settings_form_with_success() {
let settings = Settings::default();
let resp = render_settings_form(&settings, Lang::En, None, Some("saved"));
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_render_settings_content_includes_publisher_fields() {
let settings = Settings::default();
let html = render_settings_content(&settings, Lang::En, None, None);
assert!(html.contains(r#"name="publisher_name""#));
assert!(html.contains(r#"name="publisher_namespace""#));
assert!(html.contains(r#"name="publisher_category""#));
assert!(html.contains(r#"name="publisher_contact_details""#));
assert!(html.contains("Publisher Defaults"));
assert!(html.contains(r#"<option value="vendor" selected>"#));
}
#[test]
fn test_render_settings_content_labels_include_hashes() {
let settings = Settings::default();
let html = render_settings_content(&settings, Lang::En, None, None);
assert!(
html.contains("Generate SHA-256 hashes sidecars"),
"expected SHA-256 label in HTML"
);
assert!(
html.contains("Generate SHA-512 hashes sidecars"),
"expected SHA-512 label in HTML"
);
assert!(
html.contains("Generate SHA3-512 hashes sidecars"),
"expected SHA3-512 label in HTML"
);
assert!(!html.contains(">Generate SHA-256 sidecars<"));
assert!(!html.contains(">Generate SHA3-512 sidecars<"));
}
#[test]
fn test_render_settings_content_shows_sha512_checkbox() {
let settings = Settings::default();
let html = render_settings_content(&settings, Lang::En, None, None);
assert!(
html.contains(r#"name="sidecar_sha512""#),
"expected SHA-512 checkbox input in HTML"
);
assert!(
html.contains("<code>.sha-512</code>"),
"expected SHA-512 extension help text"
);
}
#[test]
fn test_render_settings_content_shows_log_directory() {
let settings = Settings::default();
let html = render_settings_content(&settings, Lang::En, None, None);
assert!(
html.contains(r#"name="log_directory""#),
"expected log_directory input in HTML"
);
assert!(
html.contains(r#"value="./data_log""#),
"expected default log directory value"
);
assert!(
html.contains(
"Filesystem path where the application log files and sidecars are written."
)
);
}
#[test]
fn test_render_settings_content_shows_reset_button() {
let settings = Settings::default();
let html = render_settings_content(&settings, Lang::En, None, None);
assert!(
html.contains("/settings/reset"),
"expected reset form action in HTML"
);
assert!(
html.contains("Reset to default settings"),
"expected reset button label"
);
assert!(
html.contains("onsubmit=\"return confirm(")
|| html.contains("onsubmit='return confirm("),
"expected native confirm() guard on reset form"
);
}
#[test]
fn test_render_settings_content_shows_dump_directory() {
let settings = Settings::default();
let html = render_settings_content(&settings, Lang::En, None, None);
assert!(
html.contains(r#"name="dump_directory""#),
"expected dump_directory input in HTML"
);
assert!(
html.contains(r#"value="./data_dump""#),
"expected default dump directory value"
);
assert!(
html.contains(
"Filesystem path where the database dump files and sidecars are written."
)
);
}
#[test]
fn test_render_settings_content_prefills_non_default_dump_directory() {
let settings = Settings {
dump_directory: "/backup/csaf".to_owned(),
..Settings::default()
};
let html = render_settings_content(&settings, Lang::En, None, None);
assert!(html.contains(r#"value="/backup/csaf""#));
}
#[test]
fn test_render_settings_content_prefills_non_default_publisher() {
let settings = Settings {
publisher_name: "Acme Security".to_owned(),
publisher_namespace: "https://acme.example/csaf".to_owned(),
publisher_category: "coordinator".to_owned(),
publisher_contact_details: "psirt@acme.example".to_owned(),
..Settings::default()
};
let html = render_settings_content(&settings, Lang::En, None, None);
assert!(html.contains("Acme Security"));
assert!(html.contains("acme.example/csaf"));
assert!(html.contains("psirt@acme.example"));
assert!(html.contains(r#"<option value="coordinator" selected>"#));
assert!(!html.contains(r#"<option value="vendor" selected>"#));
}
}