salvo_captcha/finder/
query_finder.rs

1// Copyright (c) 2024-2025, Awiteb <a@4rs.nl>
2//     A captcha middleware for Salvo framework.
3//
4// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
5// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
6// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
7// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
8// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
9// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
10// THE SOFTWARE.
11
12use salvo_core::http::Request;
13
14use crate::CaptchaFinder;
15
16/// Find the captcha token and answer from the url query
17#[derive(Debug)]
18pub struct CaptchaQueryFinder {
19    /// The query name of the captcha token
20    ///
21    /// Default: "c_t"
22    pub token_name: String,
23
24    /// The query name of the captcha answer
25    ///
26    /// Default: "c_a"
27    pub answer_name: String,
28}
29
30impl CaptchaQueryFinder {
31    /// Create a new [`CaptchaQueryFinder`]
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Set the token query name
37    pub fn token_name(mut self, token_name: String) -> Self {
38        self.token_name = token_name;
39        self
40    }
41
42    /// Set the answer query name
43    pub fn answer_name(mut self, answer_name: String) -> Self {
44        self.answer_name = answer_name;
45        self
46    }
47}
48
49impl Default for CaptchaQueryFinder {
50    /// Create a default [`CaptchaQueryFinder`] with:
51    /// - token_name: "c_t"
52    /// - answer_name: "c_a"
53    fn default() -> Self {
54        Self {
55            token_name: "c_t".to_string(),
56            answer_name: "c_a".to_string(),
57        }
58    }
59}
60
61impl CaptchaFinder for CaptchaQueryFinder {
62    async fn find_token(&self, req: &mut Request) -> Option<Option<String>> {
63        req.queries()
64            .get(&self.token_name)
65            .map(|o| Some(o.to_owned()))
66    }
67
68    async fn find_answer(&self, req: &mut Request) -> Option<Option<String>> {
69        req.queries()
70            .get(&self.answer_name)
71            .map(|o| Some(o.to_owned()))
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[tokio::test]
80    #[rstest::rstest]
81    #[case::not_found(None, None, None, None, None, None)]
82    #[case::normal(
83        None,
84        None,
85        Some(("c_t", "token")),
86        Some(("c_a", "answer")),
87        Some(Some("token")),
88        Some(Some("answer"))
89    )]
90    #[case::custom_keys(
91        Some("cc_t"),
92        Some("cc_a"),
93        Some(("cc_t", "token")),
94        Some(("cc_a", "answer")),
95        Some(Some("token")),
96        Some(Some("answer"))
97    )]
98    #[case::only_token(
99        None,
100        None,
101        Some(("c_t", "token")),
102        None,
103        Some(Some("token")),
104        None
105    )]
106    #[case::only_answer(None, None, None, Some(("c_a", "ans")), None, Some(Some("ans")))]
107    #[case::custom_not_found(Some("cc_t"), Some("cc_a"), None, None, None, None)]
108    #[case::custom_not_found_with_query(
109        Some("cc_t"),
110        Some("cc_a"),
111        Some(("c_t", "token")),
112        Some(("c_a", "answer")),
113        None,
114        None
115    )]
116    async fn test_query_finder(
117        #[case] custom_token_key: Option<&'static str>,
118        #[case] custom_answer_key: Option<&'static str>,
119        #[case] token_key_val: Option<(&'static str, &'static str)>,
120        #[case] answer_key_val: Option<(&'static str, &'static str)>,
121        #[case] excepted_token: Option<Option<&'static str>>,
122        #[case] excepted_answer: Option<Option<&'static str>>,
123    ) {
124        let mut req = Request::default();
125        let mut finder = CaptchaQueryFinder::new();
126        if let Some(token_key) = custom_token_key {
127            finder = finder.token_name(token_key.to_string())
128        }
129        if let Some(answer_key) = custom_answer_key {
130            finder = finder.answer_name(answer_key.to_string())
131        }
132
133        let queries = req.queries_mut();
134
135        if let Some((k, v)) = token_key_val {
136            queries.insert(k.to_owned(), v.to_owned());
137        }
138        if let Some((k, v)) = answer_key_val {
139            queries.insert(k.to_owned(), v.to_owned());
140        }
141
142        assert_eq!(
143            finder.find_token(&mut req).await,
144            excepted_token.map(|o| o.map(ToOwned::to_owned))
145        );
146        assert_eq!(
147            finder.find_answer(&mut req).await,
148            excepted_answer.map(|o| o.map(ToOwned::to_owned))
149        );
150    }
151}