feat: reimplements the captcha and related options
This commit is contained in:
parent
726fe55eb8
commit
75f1644cc1
5 changed files with 242 additions and 30 deletions
25
.env.example
25
.env.example
|
|
@ -25,16 +25,31 @@ BOOK_ENABLE_WEBSITE_LINKS=true
|
||||||
|
|
||||||
# Allow raw HTML/JS in entry names and message bodies. When false, HTML is escaped.
|
# Allow raw HTML/JS in entry names and message bodies. When false, HTML is escaped.
|
||||||
# Website URLs are always escaped regardless of this setting.
|
# Website URLs are always escaped regardless of this setting.
|
||||||
BOOK_ENABLE_HTML_INJECTION=true
|
BOOK_ENABLE_HTML_INJECTION=false
|
||||||
|
|
||||||
|
# Enable captcha on submission form.
|
||||||
|
BOOK_ENABLE_CAPTCHA=false
|
||||||
|
|
||||||
|
# Captcha question displayed as a label.
|
||||||
|
# BOOK_CAPTCHA_QUESTION=What is my name?
|
||||||
|
|
||||||
|
# Captcha answer to validate against.
|
||||||
|
# BOOK_CAPTCHA_ANSWER=lew
|
||||||
|
|
||||||
|
# Require exact match (true) or just "contains" (false).
|
||||||
|
BOOK_CAPTCHA_EXACT=false
|
||||||
|
|
||||||
|
# Require case-sensitive match.
|
||||||
|
BOOK_CAPTCHA_CASESENSITIVE=false
|
||||||
|
|
||||||
# Maximum length for names. 0 for unlimited.
|
# Maximum length for names. 0 for unlimited.
|
||||||
BOOK_MAX_NAME_LENGTH=50
|
BOOK_MAX_NAME_LENGTH=0
|
||||||
|
|
||||||
# Maximum length for messages. 0 for unlimited.
|
# Maximum length for messages. 0 for unlimited.
|
||||||
BOOK_MAX_MESSAGE_LENGTH=1000
|
BOOK_MAX_MESSAGE_LENGTH=0
|
||||||
|
|
||||||
# Maximum length for website URLs. 0 for unlimited.
|
# Maximum length for website URLs. 0 for unlimited.
|
||||||
BOOK_MAX_WEBSITE_LENGTH=100
|
BOOK_MAX_WEBSITE_LENGTH=0
|
||||||
|
|
||||||
# Separator between guestbook entries.
|
# Separator between guestbook entries.
|
||||||
BOOK_SEPARATOR=------------------------------------------------------------
|
BOOK_SEPARATOR=------------------------------------------------------------
|
||||||
|
|
@ -49,7 +64,7 @@ BOOK_SEPARATOR=------------------------------------------------------------
|
||||||
# BOOK_STYLE=
|
# BOOK_STYLE=
|
||||||
|
|
||||||
# Text shown above the form.
|
# Text shown above the form.
|
||||||
BOOK_FORM_PROMPT=If you visited my site, please sign my guestbook!
|
BOOK_FORM_PROMPT=Thanks for visiting. Sign the guestbook!
|
||||||
|
|
||||||
# Submit button text.
|
# Submit button text.
|
||||||
BOOK_BUTTON_TEXT=sign
|
BOOK_BUTTON_TEXT=sign
|
||||||
|
|
|
||||||
43
module.nix
43
module.nix
|
|
@ -60,7 +60,7 @@ in
|
||||||
|
|
||||||
enableHtmlInjection = mkOption {
|
enableHtmlInjection = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = false;
|
||||||
description = "Allow raw HTML/JS in entry names and message bodies. When false, HTML is escaped. Website URLs are always escaped.";
|
description = "Allow raw HTML/JS in entry names and message bodies. When false, HTML is escaped. Website URLs are always escaped.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -75,6 +75,34 @@ in
|
||||||
default = true;
|
default = true;
|
||||||
description = "Enable honeypot field for spam prevention.";
|
description = "Enable honeypot field for spam prevention.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
captcha = {
|
||||||
|
enable = mkEnableOption "captcha on submission form";
|
||||||
|
|
||||||
|
question = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = "Captcha question displayed as a label.";
|
||||||
|
};
|
||||||
|
|
||||||
|
answer = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = "Captcha answer to validate against.";
|
||||||
|
};
|
||||||
|
|
||||||
|
exact = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Require exact match. When false, the answer just needs to be contained in the response.";
|
||||||
|
};
|
||||||
|
|
||||||
|
caseSensitive = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Require case-sensitive match.";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
telegram = {
|
telegram = {
|
||||||
|
|
@ -92,19 +120,19 @@ in
|
||||||
limits = {
|
limits = {
|
||||||
name = mkOption {
|
name = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 50;
|
default = 0;
|
||||||
description = "Maximum length for names. 0 for unlimited.";
|
description = "Maximum length for names. 0 for unlimited.";
|
||||||
};
|
};
|
||||||
|
|
||||||
message = mkOption {
|
message = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 1000;
|
default = 0;
|
||||||
description = "Maximum length for messages. 0 for unlimited.";
|
description = "Maximum length for messages. 0 for unlimited.";
|
||||||
};
|
};
|
||||||
|
|
||||||
website = mkOption {
|
website = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 100;
|
default = 0;
|
||||||
description = "Maximum length for website URLs. 0 for unlimited.";
|
description = "Maximum length for website URLs. 0 for unlimited.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -136,7 +164,7 @@ in
|
||||||
|
|
||||||
greeting = mkOption {
|
greeting = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "If you visited my site, please sign my guestbook!";
|
default = "Thanks for visiting. Sign the guestbook!";
|
||||||
description = "Text shown above the form.";
|
description = "Text shown above the form.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -199,6 +227,11 @@ in
|
||||||
BOOK_ENABLE_SUBMISSIONS = if cfg.security.enableSubmissions then "true" else "false";
|
BOOK_ENABLE_SUBMISSIONS = if cfg.security.enableSubmissions then "true" else "false";
|
||||||
BOOK_ENABLE_HTML_INJECTION = if cfg.security.enableHtmlInjection then "true" else "false";
|
BOOK_ENABLE_HTML_INJECTION = if cfg.security.enableHtmlInjection then "true" else "false";
|
||||||
BOOK_ENABLE_WEBSITE_LINKS = if cfg.security.enableWebsiteLinks then "true" else "false";
|
BOOK_ENABLE_WEBSITE_LINKS = if cfg.security.enableWebsiteLinks then "true" else "false";
|
||||||
|
BOOK_ENABLE_CAPTCHA = if cfg.security.captcha.enable then "true" else "false";
|
||||||
|
BOOK_CAPTCHA_QUESTION = cfg.security.captcha.question;
|
||||||
|
BOOK_CAPTCHA_ANSWER = cfg.security.captcha.answer;
|
||||||
|
BOOK_CAPTCHA_EXACT = if cfg.security.captcha.exact then "true" else "false";
|
||||||
|
BOOK_CAPTCHA_CASESENSITIVE = if cfg.security.captcha.caseSensitive then "true" else "false";
|
||||||
BOOK_MAX_NAME_LENGTH = toString cfg.limits.name;
|
BOOK_MAX_NAME_LENGTH = toString cfg.limits.name;
|
||||||
BOOK_MAX_MESSAGE_LENGTH = toString cfg.limits.message;
|
BOOK_MAX_MESSAGE_LENGTH = toString cfg.limits.message;
|
||||||
BOOK_MAX_WEBSITE_LENGTH = toString cfg.limits.website;
|
BOOK_MAX_WEBSITE_LENGTH = toString cfg.limits.website;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,11 @@ pub struct Config {
|
||||||
pub enable_submissions: bool,
|
pub enable_submissions: bool,
|
||||||
pub enable_website_links: bool,
|
pub enable_website_links: bool,
|
||||||
pub enable_html_injection: bool,
|
pub enable_html_injection: bool,
|
||||||
|
pub enable_captcha: bool,
|
||||||
|
pub captcha_question: String,
|
||||||
|
pub captcha_answer: String,
|
||||||
|
pub captcha_exact: bool,
|
||||||
|
pub captcha_casesensitive: bool,
|
||||||
pub template: Option<String>,
|
pub template: Option<String>,
|
||||||
pub separator: String,
|
pub separator: String,
|
||||||
pub style: String,
|
pub style: String,
|
||||||
|
|
@ -54,15 +59,15 @@ impl Config {
|
||||||
.map(|v| v != "false")
|
.map(|v| v != "false")
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
max_name_length: env::var("BOOK_MAX_NAME_LENGTH")
|
max_name_length: env::var("BOOK_MAX_NAME_LENGTH")
|
||||||
.unwrap_or_else(|_| "50".into())
|
.unwrap_or_else(|_| "0".into())
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| "BOOK_MAX_NAME_LENGTH must be a number")?,
|
.map_err(|_| "BOOK_MAX_NAME_LENGTH must be a number")?,
|
||||||
max_message_length: env::var("BOOK_MAX_MESSAGE_LENGTH")
|
max_message_length: env::var("BOOK_MAX_MESSAGE_LENGTH")
|
||||||
.unwrap_or_else(|_| "1000".into())
|
.unwrap_or_else(|_| "0".into())
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| "BOOK_MAX_MESSAGE_LENGTH must be a number")?,
|
.map_err(|_| "BOOK_MAX_MESSAGE_LENGTH must be a number")?,
|
||||||
max_website_length: env::var("BOOK_MAX_WEBSITE_LENGTH")
|
max_website_length: env::var("BOOK_MAX_WEBSITE_LENGTH")
|
||||||
.unwrap_or_else(|_| "100".into())
|
.unwrap_or_else(|_| "0".into())
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| "BOOK_MAX_WEBSITE_LENGTH must be a number")?,
|
.map_err(|_| "BOOK_MAX_WEBSITE_LENGTH must be a number")?,
|
||||||
enable_submissions: env::var("BOOK_ENABLE_SUBMISSIONS")
|
enable_submissions: env::var("BOOK_ENABLE_SUBMISSIONS")
|
||||||
|
|
@ -73,7 +78,20 @@ impl Config {
|
||||||
.unwrap_or(true),
|
.unwrap_or(true),
|
||||||
enable_html_injection: env::var("BOOK_ENABLE_HTML_INJECTION")
|
enable_html_injection: env::var("BOOK_ENABLE_HTML_INJECTION")
|
||||||
.map(|v| v != "false")
|
.map(|v| v != "false")
|
||||||
.unwrap_or(true),
|
.unwrap_or(false),
|
||||||
|
enable_captcha: env::var("BOOK_ENABLE_CAPTCHA")
|
||||||
|
.map(|v| v != "false")
|
||||||
|
.unwrap_or(false),
|
||||||
|
captcha_question: env::var("BOOK_CAPTCHA_QUESTION")
|
||||||
|
.unwrap_or_default(),
|
||||||
|
captcha_answer: env::var("BOOK_CAPTCHA_ANSWER")
|
||||||
|
.unwrap_or_default(),
|
||||||
|
captcha_exact: env::var("BOOK_CAPTCHA_EXACT")
|
||||||
|
.map(|v| v != "false")
|
||||||
|
.unwrap_or(false),
|
||||||
|
captcha_casesensitive: env::var("BOOK_CAPTCHA_CASESENSITIVE")
|
||||||
|
.map(|v| v != "false")
|
||||||
|
.unwrap_or(false),
|
||||||
separator: env::var("BOOK_SEPARATOR")
|
separator: env::var("BOOK_SEPARATOR")
|
||||||
.unwrap_or_else(|_| "------------------------------------------------------------".into()),
|
.unwrap_or_else(|_| "------------------------------------------------------------".into()),
|
||||||
template: env::var("BOOK_TEMPLATE").ok().map(|path| {
|
template: env::var("BOOK_TEMPLATE").ok().map(|path| {
|
||||||
|
|
@ -89,7 +107,7 @@ impl Config {
|
||||||
.or_else(|| env::var("BOOK_STYLE").ok())
|
.or_else(|| env::var("BOOK_STYLE").ok())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
form_prompt: env::var("BOOK_FORM_PROMPT")
|
form_prompt: env::var("BOOK_FORM_PROMPT")
|
||||||
.unwrap_or_else(|_| "If you visited my site, please sign my guestbook!".into()),
|
.unwrap_or_else(|_| "Thanks for visiting. Sign the guestbook!".into()),
|
||||||
button_text: env::var("BOOK_BUTTON_TEXT")
|
button_text: env::var("BOOK_BUTTON_TEXT")
|
||||||
.unwrap_or_else(|_| "sign".into()),
|
.unwrap_or_else(|_| "sign".into()),
|
||||||
label_name: env::var("BOOK_LABEL_NAME")
|
label_name: env::var("BOOK_LABEL_NAME")
|
||||||
|
|
@ -203,21 +221,21 @@ mod tests {
|
||||||
env::remove_var("BOOK_ENABLE_HTML_INJECTION");
|
env::remove_var("BOOK_ENABLE_HTML_INJECTION");
|
||||||
|
|
||||||
let config = Config::from_env().unwrap();
|
let config = Config::from_env().unwrap();
|
||||||
assert!(config.enable_html_injection);
|
assert!(!config.enable_html_injection);
|
||||||
|
|
||||||
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
|
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
|
||||||
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
|
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_enable_html_injection_false() {
|
fn test_enable_html_injection_true() {
|
||||||
let _lock = ENV_LOCK.lock().unwrap();
|
let _lock = ENV_LOCK.lock().unwrap();
|
||||||
env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC");
|
env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC");
|
||||||
env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345");
|
env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345");
|
||||||
env::set_var("BOOK_ENABLE_HTML_INJECTION", "false");
|
env::set_var("BOOK_ENABLE_HTML_INJECTION", "true");
|
||||||
|
|
||||||
let config = Config::from_env().unwrap();
|
let config = Config::from_env().unwrap();
|
||||||
assert!(!config.enable_html_injection);
|
assert!(config.enable_html_injection);
|
||||||
|
|
||||||
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
|
env::remove_var("BOOK_TELEGRAM_BOT_TOKEN");
|
||||||
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
|
env::remove_var("BOOK_TELEGRAM_CHAT_ID");
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,15 @@ pub fn render_form(config: &Config) -> String {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let captcha_section = if config.enable_captcha {
|
||||||
|
format!(
|
||||||
|
"\n<label class=\"guestbook-label\">{}</label>\n<input class=\"guestbook-input\" name=\"captcha\" required>\n",
|
||||||
|
config.captcha_question
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
r#"<span class="guestbook-prompt">{prompt}</span>
|
r#"<span class="guestbook-prompt">{prompt}</span>
|
||||||
<form class="guestbook-form" method="post" action="/submit" accept-charset="UTF-8">
|
<form class="guestbook-form" method="post" action="/submit" accept-charset="UTF-8">
|
||||||
|
|
@ -37,6 +46,7 @@ pub fn render_form(config: &Config) -> String {
|
||||||
{website_section}
|
{website_section}
|
||||||
<label class="guestbook-label">{label_message}</label>
|
<label class="guestbook-label">{label_message}</label>
|
||||||
<textarea class="guestbook-textarea" name="message" rows="{rows}" cols="{cols}" required></textarea>
|
<textarea class="guestbook-textarea" name="message" rows="{rows}" cols="{cols}" required></textarea>
|
||||||
|
{captcha_section}
|
||||||
<input name="url" style="display:none" tabindex="-1" autocomplete="off">
|
<input name="url" style="display:none" tabindex="-1" autocomplete="off">
|
||||||
<button class="guestbook-button" type="submit">{button}</button>
|
<button class="guestbook-button" type="submit">{button}</button>
|
||||||
</form>"#,
|
</form>"#,
|
||||||
|
|
@ -46,6 +56,7 @@ pub fn render_form(config: &Config) -> String {
|
||||||
label_message = config.label_message,
|
label_message = config.label_message,
|
||||||
rows = config.textarea_rows,
|
rows = config.textarea_rows,
|
||||||
cols = config.textarea_cols,
|
cols = config.textarea_cols,
|
||||||
|
captcha_section = captcha_section,
|
||||||
button = config.button_text,
|
button = config.button_text,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -110,16 +121,21 @@ mod tests {
|
||||||
telegram_bot_token: "fake".into(),
|
telegram_bot_token: "fake".into(),
|
||||||
telegram_chat_id: 0,
|
telegram_chat_id: 0,
|
||||||
enable_honeypot: true,
|
enable_honeypot: true,
|
||||||
max_name_length: 50,
|
max_name_length: 0,
|
||||||
max_message_length: 1000,
|
max_message_length: 0,
|
||||||
max_website_length: 100,
|
max_website_length: 0,
|
||||||
enable_submissions: true,
|
enable_submissions: true,
|
||||||
enable_website_links: true,
|
enable_website_links: true,
|
||||||
enable_html_injection: true,
|
enable_html_injection: false,
|
||||||
|
enable_captcha: false,
|
||||||
|
captcha_question: String::new(),
|
||||||
|
captcha_answer: String::new(),
|
||||||
|
captcha_exact: false,
|
||||||
|
captcha_casesensitive: false,
|
||||||
template: None,
|
template: None,
|
||||||
separator: "---".into(),
|
separator: "---".into(),
|
||||||
style: String::new(),
|
style: String::new(),
|
||||||
form_prompt: "Sign my guestbook!".into(),
|
form_prompt: "Thanks for visiting. Sign the guestbook!".into(),
|
||||||
button_text: "sign".into(),
|
button_text: "sign".into(),
|
||||||
label_name: "Your name:".into(),
|
label_name: "Your name:".into(),
|
||||||
label_website: "Your website (optional):".into(),
|
label_website: "Your website (optional):".into(),
|
||||||
|
|
@ -186,7 +202,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_render_preserves_html_in_body() {
|
fn test_render_preserves_html_in_body() {
|
||||||
let config = test_config();
|
let mut config = test_config();
|
||||||
|
config.enable_html_injection = true;
|
||||||
let entry = make_entry("carol", "2026-04-09", "<b>Bold</b>");
|
let entry = make_entry("carol", "2026-04-09", "<b>Bold</b>");
|
||||||
let form = render_form(&config);
|
let form = render_form(&config);
|
||||||
let html = render_page(DEFAULT_TEMPLATE, &config, &[entry], &form);
|
let html = render_page(DEFAULT_TEMPLATE, &config, &[entry], &form);
|
||||||
|
|
|
||||||
139
src/web.rs
139
src/web.rs
|
|
@ -25,6 +25,8 @@ pub struct SubmitForm {
|
||||||
message: String,
|
message: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
url: String, // honeypot
|
url: String, // honeypot
|
||||||
|
#[serde(default)]
|
||||||
|
captcha: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router(state: Arc<AppState>) -> Router {
|
pub fn router(state: Arc<AppState>) -> Router {
|
||||||
|
|
@ -74,6 +76,30 @@ async fn submit(
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Captcha check
|
||||||
|
if state.config.enable_captcha {
|
||||||
|
let input = form.captcha.trim();
|
||||||
|
let answer = &state.config.captcha_answer;
|
||||||
|
let ok = if state.config.captcha_casesensitive {
|
||||||
|
if state.config.captcha_exact {
|
||||||
|
input == answer
|
||||||
|
} else {
|
||||||
|
input.contains(answer.as_str())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let input_lower = input.to_lowercase();
|
||||||
|
let answer_lower = answer.to_lowercase();
|
||||||
|
if state.config.captcha_exact {
|
||||||
|
input_lower == answer_lower
|
||||||
|
} else {
|
||||||
|
input_lower.contains(&answer_lower)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !ok {
|
||||||
|
return Html("Wrong answer.".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if name.is_empty() || message.is_empty() {
|
if name.is_empty() || message.is_empty() {
|
||||||
return Html("Name and message are required.".to_string());
|
return Html("Name and message are required.".to_string());
|
||||||
}
|
}
|
||||||
|
|
@ -137,16 +163,21 @@ mod tests {
|
||||||
telegram_bot_token: "fake".into(),
|
telegram_bot_token: "fake".into(),
|
||||||
telegram_chat_id: 0,
|
telegram_chat_id: 0,
|
||||||
enable_honeypot: true,
|
enable_honeypot: true,
|
||||||
max_name_length: 50,
|
max_name_length: 0,
|
||||||
max_message_length: 1000,
|
max_message_length: 0,
|
||||||
max_website_length: 100,
|
max_website_length: 0,
|
||||||
enable_submissions: true,
|
enable_submissions: true,
|
||||||
enable_website_links: true,
|
enable_website_links: true,
|
||||||
enable_html_injection: true,
|
enable_html_injection: false,
|
||||||
|
enable_captcha: false,
|
||||||
|
captcha_question: String::new(),
|
||||||
|
captcha_answer: String::new(),
|
||||||
|
captcha_exact: false,
|
||||||
|
captcha_casesensitive: false,
|
||||||
template: None,
|
template: None,
|
||||||
separator: "---".into(),
|
separator: "---".into(),
|
||||||
style: String::new(),
|
style: String::new(),
|
||||||
form_prompt: "Sign my guestbook!".into(),
|
form_prompt: "Thanks for visiting. Sign the guestbook!".into(),
|
||||||
button_text: "sign".into(),
|
button_text: "sign".into(),
|
||||||
label_name: "Your name:".into(),
|
label_name: "Your name:".into(),
|
||||||
label_website: "Your website (optional):".into(),
|
label_website: "Your website (optional):".into(),
|
||||||
|
|
@ -333,4 +364,102 @@ mod tests {
|
||||||
let html = get_index(&app).await;
|
let html = get_index(&app).await;
|
||||||
assert!(!html.contains("name=\"website\""));
|
assert!(!html.contains("name=\"website\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_captcha_rejects_wrong_answer() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut config = test_config(dir.path());
|
||||||
|
config.enable_captcha = true;
|
||||||
|
config.captcha_question = "What is my name?".into();
|
||||||
|
config.captcha_answer = "lew".into();
|
||||||
|
let (app, _rx) = test_app(config);
|
||||||
|
let (_, body) = post_form(&app, "name=alice&message=hello&captcha=wrong").await;
|
||||||
|
assert!(body.contains("Wrong answer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_captcha_accepts_correct_answer() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut config = test_config(dir.path());
|
||||||
|
config.enable_captcha = true;
|
||||||
|
config.captcha_question = "What is my name?".into();
|
||||||
|
config.captcha_answer = "lew".into();
|
||||||
|
let (app, _rx) = test_app(config);
|
||||||
|
let (_, body) = post_form(&app, "name=alice&message=hello&captcha=lew").await;
|
||||||
|
assert!(body.contains("pending approval"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_captcha_inexact_contains() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut config = test_config(dir.path());
|
||||||
|
config.enable_captcha = true;
|
||||||
|
config.captcha_exact = false;
|
||||||
|
config.captcha_question = "What is my name?".into();
|
||||||
|
config.captcha_answer = "lew".into();
|
||||||
|
let (app, _rx) = test_app(config);
|
||||||
|
let (_, body) = post_form(&app, "name=alice&message=hello&captcha=lewis").await;
|
||||||
|
assert!(body.contains("pending approval"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_captcha_inexact_rejects_no_match() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut config = test_config(dir.path());
|
||||||
|
config.enable_captcha = true;
|
||||||
|
config.captcha_exact = false;
|
||||||
|
config.captcha_question = "What is my name?".into();
|
||||||
|
config.captcha_answer = "lew".into();
|
||||||
|
let (app, _rx) = test_app(config);
|
||||||
|
let (_, body) = post_form(&app, "name=alice&message=hello&captcha=bob").await;
|
||||||
|
assert!(body.contains("Wrong answer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_captcha_casesensitive() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut config = test_config(dir.path());
|
||||||
|
config.enable_captcha = true;
|
||||||
|
config.captcha_question = "What is my name?".into();
|
||||||
|
config.captcha_answer = "lew".into();
|
||||||
|
config.captcha_casesensitive = true;
|
||||||
|
let (app, _rx) = test_app(config);
|
||||||
|
let (_, body) = post_form(&app, "name=alice&message=hello&captcha=Lew").await;
|
||||||
|
assert!(body.contains("Wrong answer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_captcha_case_insensitive() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut config = test_config(dir.path());
|
||||||
|
config.enable_captcha = true;
|
||||||
|
config.captcha_question = "What is my name?".into();
|
||||||
|
config.captcha_answer = "lew".into();
|
||||||
|
config.captcha_casesensitive = false;
|
||||||
|
let (app, _rx) = test_app(config);
|
||||||
|
let (_, body) = post_form(&app, "name=alice&message=hello&captcha=LEW").await;
|
||||||
|
assert!(body.contains("pending approval"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_captcha_disabled_skips_check() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let config = test_config(dir.path());
|
||||||
|
let (app, _rx) = test_app(config);
|
||||||
|
let (_, body) = post_form(&app, "name=alice&message=hello").await;
|
||||||
|
assert!(body.contains("pending approval"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_captcha_shows_in_form() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut config = test_config(dir.path());
|
||||||
|
config.enable_captcha = true;
|
||||||
|
config.captcha_question = "What is 2+2?".into();
|
||||||
|
config.captcha_answer = "4".into();
|
||||||
|
let (app, _rx) = test_app(config);
|
||||||
|
let html = get_index(&app).await;
|
||||||
|
assert!(html.contains("What is 2+2?"));
|
||||||
|
assert!(html.contains("name=\"captcha\""));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue