Skip to main content

rumtk_web/
lib.rs

1/*
2 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 * This toolkit aims to be reliable, simple, performant, and standards compliant.
4 * Copyright (C) 2025  Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 * Copyright (C) 2025  Ethan Dixon
6 * Copyright (C) 2025  MedicalMasses L.L.C. <contact@medicalmasses.com>
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20 */
21#![feature(once_cell_get_mut)]
22#![feature(macro_metavar_expr)]
23#![feature(str_as_str)]
24#![feature(random)]
25extern crate core;
26
27pub mod api;
28pub mod components;
29pub mod css;
30pub mod pages;
31pub mod utils;
32pub mod js;
33
34pub use app::*;
35pub use utils::*;
36
37///
38/// Add utils unit tests here to ensure internal functions work.
39///
40#[cfg(test)]
41mod tests {
42    use crate::components::app::css::css;
43    use crate::components::sanitize::sanitized;
44    use crate::components::title::title;
45    use crate::defaults::{PARAMS_ID, PARAMS_TITLE};
46    use crate::jobs::JobResult;
47    use crate::testdata::data::{create_test_form, RAW_HTML_PREFORMATTED, TESTDATA_EXPECTED_FORMDATA, TESTDATA_EXPECTED_FORMDATA_EMPTY, TESTDATA_FORMDATA_EMPTY_REQUEST, TESTDATA_FORMDATA_EMPTY_REQUEST_WITH_BOUNDARIES, TESTDATA_FORMDATA_REQUEST, TRIMMED_HTML_PREFORMATTED, TRIMMED_HTML_RENDER_CSS, TRIMMED_HTML_TITLE_RENDER};
48    use crate::{rumtk_web_get_job_manager, rumtk_web_init_job_manager, rumtk_web_params_map, rumtk_web_post_process, rumtk_web_register_app_components, rumtk_web_register_app_switches, rumtk_web_render, rumtk_web_render_component, rumtk_web_render_redirect, rumtk_web_run_app, rumtk_web_sync_get, rumtk_web_trim_rendered_html, sanitize_html, AppState, RUMWebData, RUMWebRedirect, RenderedPageComponentsResult, SharedAppState};
49    use crate::{RUMWebResponse, RUMWebTemplate};
50    use rand::random;
51    use rumtk_core::buffers::buffer_to_str;
52    use rumtk_core::strings::{rumtk_format, RUMString};
53    use rumtk_core::{rumtk_new_lock, rumtk_sleep};
54    use std::thread::spawn;
55
56
57    const EXPECTED_RESPONSE_SIZE: usize = 2012;
58
59    ///////////////////////////////////FormData/////////////////////////////////////////////////
60    #[test]
61    fn test_compile_form() {
62        let expected_form = TESTDATA_EXPECTED_FORMDATA();
63        let form_data = create_test_form(TESTDATA_FORMDATA_REQUEST).expect("Form");
64
65        assert_eq!(form_data, expected_form, "Form results mismatch!");
66    }
67
68    #[test]
69    fn test_compile_empty_form() {
70        let expected_form = TESTDATA_EXPECTED_FORMDATA_EMPTY();
71        let form_data = create_test_form(TESTDATA_FORMDATA_EMPTY_REQUEST).expect("Form");
72
73        assert_eq!(form_data, expected_form, "Form results mismatch!");
74    }
75
76    #[test]
77    fn test_compile_empty_form_with_boundaries() {
78        let expected_form = TESTDATA_EXPECTED_FORMDATA_EMPTY();
79        let form_data = create_test_form(TESTDATA_FORMDATA_EMPTY_REQUEST_WITH_BOUNDARIES).expect("Form");
80
81        assert_eq!(form_data, expected_form, "Form results mismatch!");
82    }
83
84    ///////////////////////////////////Response/////////////////////////////////////////////////
85    #[test]
86    fn test_render_redirect_response() {
87        let url = "http://localhost/redirected";
88        let redirect =
89            rumtk_web_render_redirect!(RUMWebRedirect::Redirect(url.to_string())).unwrap();
90        let redirect_code = redirect.get_code();
91        let redirect_url = redirect.get_url();
92        assert_eq!(redirect_url, url, "Redirect url mismatch!");
93        assert_eq!(redirect_code, 303, "Wrong redirect code!");
94    }
95
96    #[test]
97    fn test_render_redirect_response_temporary() {
98        let url = "http://localhost/redirected";
99        let redirect =
100            rumtk_web_render_redirect!(RUMWebRedirect::RedirectTemporary(url.to_string()))
101                .unwrap();
102        let redirect_code = redirect.get_code();
103        let redirect_url = redirect.get_url();
104        assert_eq!(redirect_url, url, "Redirect url mismatch!");
105        assert_eq!(redirect_code, 307, "Wrong redirect code!");
106    }
107
108    #[test]
109    fn test_render_redirect_response_permanent() {
110        let url = "http://localhost/redirected";
111        let redirect =
112            rumtk_web_render_redirect!(RUMWebRedirect::RedirectPermanent(url.to_string()))
113                .unwrap();
114        let redirect_code = redirect.get_code();
115        let redirect_url = redirect.get_url();
116        assert_eq!(redirect_url, url, "Redirect url mismatch!");
117        assert_eq!(redirect_code, 308, "Wrong redirect code!");
118    }
119
120    #[test]
121    fn test_render_standard_web_component() {
122        let params = rumtk_web_params_map!([(PARAMS_TITLE, "Hello World!")]);
123        let state = SharedAppState::default();
124        let rendered = title(&[], params.get_inner(), state).unwrap().to_string();
125        let rendered_trimmed = rumtk_web_post_process(rendered, RUMWebRedirect::None).unwrap().to_string();
126
127        assert_eq!(
128            rendered_trimmed, TRIMMED_HTML_TITLE_RENDER,
129            "Commponent rendered improperly!"
130        );
131    }
132
133    #[test]
134    fn test_render() {
135        #[derive(RUMWebTemplate)]
136        #[template(source = "<div></div>", ext = "html")]
137        struct Div {}
138
139        let result = rumtk_web_render(Div {}, RUMWebRedirect::None).unwrap();
140        let expected = RUMWebResponse::into_get_response("<div></div>");
141
142        assert_eq!(result, expected, "Test Div template rendered improperly!");
143    }
144
145    #[test]
146    fn test_render_static_component() {
147        let rendered = rumtk_web_render_component!(css);
148        let expected = TRIMMED_HTML_RENDER_CSS;
149
150        assert_eq!(rendered, expected, "Commponent rendered improperly!");
151    }
152
153    #[test]
154    fn test_trim_preformatted_component() {
155        let result = rumtk_web_trim_rendered_html(RAW_HTML_PREFORMATTED.to_string()).unwrap();
156
157        assert_eq!(result, TRIMMED_HTML_PREFORMATTED.to_string(), "Preformatted html string was filtered inappropriately.!");
158    }
159
160    ///////////////////////////////////Jobs/////////////////////////////////////////////////
161    #[test]
162    fn test_job_run() {
163        const HELLO_STR: &str = "Hello World";
164
165        let workers: usize = 5;
166        rumtk_web_init_job_manager!(&workers);
167
168        async fn basic_processor() -> JobResult {
169            let result = RUMString::from(HELLO_STR);
170            let rendered = sanitized(result);
171            Ok(Some(rendered?))
172        }
173
174        let app_state = rumtk_new_lock!(AppState::default());
175        let mut params = RUMWebData::new();
176        let job_id = rumtk_web_get_job_manager!().unwrap().spawn_task(basic_processor()).unwrap();
177        params.insert(RUMString::from(PARAMS_ID), job_id.to_string());
178
179        let result = rumtk_web_get_job_manager!().unwrap().wait_on(&job_id).unwrap().unwrap().unwrap().unwrap();
180
181        rumtk_sleep!(1);
182        let rendered = result.to_string();
183
184        assert_eq!(&rendered, HELLO_STR, "Job returned the wrong result!");
185    }
186
187    ///////////////////////////////////Endpoint Tests/////////////////////////////////////////////////
188    #[test]
189    fn test_run_app() {
190        pub fn index(app_state: SharedAppState) -> RenderedPageComponentsResult {
191            let title_params = rumtk_web_params_map!([("title", "Hello World!")]);
192            let title = title(&[], title_params.get_inner(), app_state.clone())?.to_string();
193            Ok(vec![title])
194        }
195
196        let app_components = rumtk_web_register_app_components!(
197            vec![
198                ("index", index),
199            ]
200        );
201        let app_switches = rumtk_web_register_app_switches!(
202            true,
203            true,
204            true
205        );
206        rumtk_web_run_app!(
207            app_components,
208            app_switches
209        );
210    }
211    #[test]
212    fn test_request_get() {
213        let port = random::<u16>();
214        let port_copy = port.clone();
215        spawn(move || {
216            pub fn index(app_state: SharedAppState) -> RenderedPageComponentsResult {
217                let title_params = rumtk_web_params_map!([("title", "Hello World!")]);
218                let title = title(&[], title_params.get_inner(), app_state.clone())?.to_string();
219                Ok(vec![title])
220            }
221
222            let app_components = rumtk_web_register_app_components!(
223                vec![
224                    ("index", index),
225                ]
226            );
227            let app_switches = rumtk_web_register_app_switches!(
228                false,
229                true,
230                true
231            );
232            rumtk_web_run_app!(
233                app_components,
234                app_switches,
235                Some(port)
236            );
237        });
238        let (code, output) = rumtk_web_sync_get(&rumtk_format!("http://127.0.0.1:{port_copy}/")).unwrap();
239        let output_str = buffer_to_str(&output).unwrap();
240        println!("Output: {}", output_str);
241        //%Abz
242        assert!(output_str.contains(">Hello World!</h1>"), "Applet responded improperly with wrong response content!");
243    }
244    #[test]
245    fn test_fuzzed_request_get() {
246        let port = random::<u16>();
247        let port_copy = port.clone();
248        spawn(move || {
249            pub fn index(app_state: SharedAppState) -> RenderedPageComponentsResult {
250                let title_params = rumtk_web_params_map!([("title", "Hello World!")]);
251                let title = title(&[], title_params.get_inner(), app_state.clone())?.to_string();
252                Ok(vec![title])
253            }
254
255            let app_components = rumtk_web_register_app_components!(
256                vec![
257                    ("index", index),
258                ]
259            );
260            let app_switches = rumtk_web_register_app_switches!(
261                false,
262                true,
263                true
264            );
265            rumtk_web_run_app!(
266                app_components,
267                app_switches,
268                Some(port)
269            );
270        });
271
272        let fuzz_get_tests = vec![
273            "%Abz"
274        ];
275
276        for fuzz_itm in fuzz_get_tests {
277            let (code, output) = rumtk_web_sync_get(&rumtk_format!("http://127.0.0.1:{port_copy}/{fuzz_itm}")).unwrap();
278            if output.len() != EXPECTED_RESPONSE_SIZE {
279                match code {
280                    200 => panic!("Applet responded with wrong response size! => {}", output.len()),
281                    _ => {
282                        println!("Applet responded with handled error message!");
283                        continue;
284                    }
285                }
286            }
287            let output_str = buffer_to_str(&output).unwrap();
288            println!("Output: {}", output_str);
289            assert!(output_str.contains(">Hello World!</h1>"), "Applet responded improperly with wrong response content!");
290        }
291    }
292    #[test]
293    fn test_fuzzed_request_get_kills_axum() {
294        let port = random::<u16>();
295        let port_copy = port.clone();
296        spawn(move || {
297            pub fn index(app_state: SharedAppState) -> RenderedPageComponentsResult {
298                let title_params = rumtk_web_params_map!([("title", "Hello World!")]);
299                let title = title(&[], title_params.get_inner(), app_state.clone())?.to_string();
300                Ok(vec![title])
301            }
302
303            let app_components = rumtk_web_register_app_components!(
304                vec![
305                    ("index", index),
306                ]
307            );
308            let app_switches = rumtk_web_register_app_switches!(
309                false,
310                true,
311                true
312            );
313            rumtk_web_run_app!(
314                app_components,
315                app_switches,
316                Some(port)
317            );
318        });
319
320        let fuzz_get_tests = vec![
321            "%Abz"
322        ];
323
324        for fuzz_itm in fuzz_get_tests {
325            let request = rumtk_format!("http://127.0.0.1:{port_copy}/{fuzz_itm}");
326            let (code, output) = rumtk_web_sync_get(&request).unwrap();
327            if output.len() != EXPECTED_RESPONSE_SIZE {
328                match rumtk_web_sync_get(&request) {
329                    Ok((c,r)) => {
330                        println!("Request[{}] Does not kill axum listener or tokio runtime so allowing... Output: {}", &request,buffer_to_str(&r).unwrap());
331                    }
332                    Err(e) => {
333                        println!("Request[{}] killed axum listener or tokio runtime... Error: {:?}", &request, &e);
334                    }
335                }
336            }
337        }
338    }
339
340    ///////////////////////////////////Sanitizer Tests///////////////////////////////////////////////
341    #[test]
342    fn test_svg_sanitizer() {
343        let input = "<svg><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\" /></svg>";
344        let output = sanitize_html(input, false);
345        assert!(output.to_string().contains("<svg><circle"), "SVG and circle tags filtered");
346    }
347    #[test]
348    fn test_svg_sanitizer_full() {
349        let input = "<svg><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\" /></svg>";
350        let expected: &str = "<svg><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\"></circle></svg>";
351        let output = sanitize_html(input, false);
352        assert_eq!(&output.to_string(), expected, "Circle tag attributes filtered");
353    }
354
355    #[test]
356    fn test_sanitize_invalid_tag() {
357        let input = "<xml><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\" /></xml>";
358        let output = sanitize_html(input, true);
359        assert!(output.to_string().is_empty(), "XML and circle tags filtered");
360    }
361
362    #[test]
363    fn test_sanitize_script_tag() {
364        let input = r#"<script type="text/javascript">alert('hi');</script>"#;;
365        let output = sanitize_html(input, false);
366        assert!(output.is_empty(), "Script tag was not filtered as expected!");
367    }
368
369    #[test]
370    fn test_sanitize_script_tag_relaxed() {
371        let input = r#"<script type="text/javascript">alert('hi');</script>"#;;
372        let output = sanitize_html(input, true);
373        assert_eq!(input, &output, "Script tag was filtered when it shouldn't have or attributes were stripped!");
374    }
375
376    #[test]
377    fn test_sanitize_consecutive_script_tag_relaxed() {
378        let input = r#"<script type="text/javascript">alert('hi');</script><script type="text/javascript">alert('hi2');</script>"#;;
379        let output = sanitize_html(input, true);
380        assert_eq!(input, &output, "Script tag was filtered when it shouldn't have or attributes were stripped!");
381    }
382
383    #[test]
384    fn test_sanitize_module_script_tag_relaxed() {
385        let input = r#"<script type="module">alert('hi');</script><script type="text/javascript">alert('hi2');</script>"#;;
386        let output = sanitize_html(input, true);
387        assert_eq!(input, &output, "Script tag was filtered when it shouldn't have or attributes were stripped!");
388    }
389
390    #[test]
391    fn test_sanitize_consecutive_module_script_tag_relaxed() {
392        let input = r#"<script type="module">alert('hi');</script><script type="module">alert('hi2');</script>"#;;
393        let output = sanitize_html(input, true);
394        assert_eq!(input, &output, "Script tag was filtered when it shouldn't have or attributes were stripped!");
395    }
396}