rumtk-web 0.9.2

Web framework part of the RUMTK framework that attempts to simplify and expedite dashboard development in Healthcare.
Documentation
/*
 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
 * This toolkit aims to be reliable, simple, performant, and standards compliant.
 * Copyright (C) 2025  Luis M. Santos, M.D. <lsantos@medicalmasses.com>
 * Copyright (C) 2025  Ethan Dixon
 * Copyright (C) 2025  MedicalMasses L.L.C. <contact@medicalmasses.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
#![feature(once_cell_get_mut)]
#![feature(macro_metavar_expr)]
#![feature(str_as_str)]
#![feature(random)]
extern crate core;

pub mod api;
pub mod components;
pub mod css;
pub mod pages;
pub mod utils;
pub mod js;

pub use app::*;
pub use utils::*;

///
/// Add utils unit tests here to ensure internal functions work.
///
#[cfg(test)]
mod tests {
    use crate::components::app::css::css;
    use crate::components::sanitize::sanitized;
    use crate::components::title::title;
    use crate::defaults::{PARAMS_ID, PARAMS_TITLE};
    use crate::jobs::JobResult;
    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};
    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};
    use crate::{RUMWebResponse, RUMWebTemplate};
    use rand::random;
    use rumtk_core::buffers::buffer_to_str;
    use rumtk_core::strings::{rumtk_format, RUMString};
    use rumtk_core::{rumtk_new_lock, rumtk_sleep};
    use std::thread::spawn;


    const EXPECTED_RESPONSE_SIZE: usize = 2012;

    ///////////////////////////////////FormData/////////////////////////////////////////////////
    #[test]
    fn test_compile_form() {
        let expected_form = TESTDATA_EXPECTED_FORMDATA();
        let form_data = create_test_form(TESTDATA_FORMDATA_REQUEST).expect("Form");

        assert_eq!(form_data, expected_form, "Form results mismatch!");
    }

    #[test]
    fn test_compile_empty_form() {
        let expected_form = TESTDATA_EXPECTED_FORMDATA_EMPTY();
        let form_data = create_test_form(TESTDATA_FORMDATA_EMPTY_REQUEST).expect("Form");

        assert_eq!(form_data, expected_form, "Form results mismatch!");
    }

    #[test]
    fn test_compile_empty_form_with_boundaries() {
        let expected_form = TESTDATA_EXPECTED_FORMDATA_EMPTY();
        let form_data = create_test_form(TESTDATA_FORMDATA_EMPTY_REQUEST_WITH_BOUNDARIES).expect("Form");

        assert_eq!(form_data, expected_form, "Form results mismatch!");
    }

    ///////////////////////////////////Response/////////////////////////////////////////////////
    #[test]
    fn test_render_redirect_response() {
        let url = "http://localhost/redirected";
        let redirect =
            rumtk_web_render_redirect!(RUMWebRedirect::Redirect(url.to_string())).unwrap();
        let redirect_code = redirect.get_code();
        let redirect_url = redirect.get_url();
        assert_eq!(redirect_url, url, "Redirect url mismatch!");
        assert_eq!(redirect_code, 303, "Wrong redirect code!");
    }

    #[test]
    fn test_render_redirect_response_temporary() {
        let url = "http://localhost/redirected";
        let redirect =
            rumtk_web_render_redirect!(RUMWebRedirect::RedirectTemporary(url.to_string()))
                .unwrap();
        let redirect_code = redirect.get_code();
        let redirect_url = redirect.get_url();
        assert_eq!(redirect_url, url, "Redirect url mismatch!");
        assert_eq!(redirect_code, 307, "Wrong redirect code!");
    }

    #[test]
    fn test_render_redirect_response_permanent() {
        let url = "http://localhost/redirected";
        let redirect =
            rumtk_web_render_redirect!(RUMWebRedirect::RedirectPermanent(url.to_string()))
                .unwrap();
        let redirect_code = redirect.get_code();
        let redirect_url = redirect.get_url();
        assert_eq!(redirect_url, url, "Redirect url mismatch!");
        assert_eq!(redirect_code, 308, "Wrong redirect code!");
    }

    #[test]
    fn test_render_standard_web_component() {
        let params = rumtk_web_params_map!([(PARAMS_TITLE, "Hello World!")]);
        let state = SharedAppState::default();
        let rendered = title(&[], params.get_inner(), state).unwrap().to_string();
        let rendered_trimmed = rumtk_web_post_process(rendered, RUMWebRedirect::None).unwrap().to_string();

        assert_eq!(
            rendered_trimmed, TRIMMED_HTML_TITLE_RENDER,
            "Commponent rendered improperly!"
        );
    }

    #[test]
    fn test_render() {
        #[derive(RUMWebTemplate)]
        #[template(source = "<div></div>", ext = "html")]
        struct Div {}

        let result = rumtk_web_render(Div {}, RUMWebRedirect::None).unwrap();
        let expected = RUMWebResponse::into_get_response("<div></div>");

        assert_eq!(result, expected, "Test Div template rendered improperly!");
    }

    #[test]
    fn test_render_static_component() {
        let rendered = rumtk_web_render_component!(css);
        let expected = TRIMMED_HTML_RENDER_CSS;

        assert_eq!(rendered, expected, "Commponent rendered improperly!");
    }

    #[test]
    fn test_trim_preformatted_component() {
        let result = rumtk_web_trim_rendered_html(RAW_HTML_PREFORMATTED.to_string()).unwrap();

        assert_eq!(result, TRIMMED_HTML_PREFORMATTED.to_string(), "Preformatted html string was filtered inappropriately.!");
    }

    ///////////////////////////////////Jobs/////////////////////////////////////////////////
    #[test]
    fn test_job_run() {
        const HELLO_STR: &str = "Hello World";

        let workers: usize = 5;
        rumtk_web_init_job_manager!(&workers);

        async fn basic_processor() -> JobResult {
            let result = RUMString::from(HELLO_STR);
            let rendered = sanitized(result);
            Ok(Some(rendered?))
        }

        let app_state = rumtk_new_lock!(AppState::default());
        let mut params = RUMWebData::new();
        let job_id = rumtk_web_get_job_manager!().unwrap().spawn_task(basic_processor()).unwrap();
        params.insert(RUMString::from(PARAMS_ID), job_id.to_string());

        let result = rumtk_web_get_job_manager!().unwrap().wait_on(&job_id).unwrap().unwrap().unwrap().unwrap();

        rumtk_sleep!(1);
        let rendered = result.to_string();

        assert_eq!(&rendered, HELLO_STR, "Job returned the wrong result!");
    }

    ///////////////////////////////////Endpoint Tests/////////////////////////////////////////////////
    #[test]
    fn test_run_app() {
        pub fn index(app_state: SharedAppState) -> RenderedPageComponentsResult {
            let title_params = rumtk_web_params_map!([("title", "Hello World!")]);
            let title = title(&[], title_params.get_inner(), app_state.clone())?.to_string();
            Ok(vec![title])
        }

        let app_components = rumtk_web_register_app_components!(
            vec![
                ("index", index),
            ]
        );
        let app_switches = rumtk_web_register_app_switches!(
            true,
            true,
            true
        );
        rumtk_web_run_app!(
            app_components,
            app_switches
        );
    }
    #[test]
    fn test_request_get() {
        let port = random::<u16>();
        let port_copy = port.clone();
        spawn(move || {
            pub fn index(app_state: SharedAppState) -> RenderedPageComponentsResult {
                let title_params = rumtk_web_params_map!([("title", "Hello World!")]);
                let title = title(&[], title_params.get_inner(), app_state.clone())?.to_string();
                Ok(vec![title])
            }

            let app_components = rumtk_web_register_app_components!(
                vec![
                    ("index", index),
                ]
            );
            let app_switches = rumtk_web_register_app_switches!(
                false,
                true,
                true
            );
            rumtk_web_run_app!(
                app_components,
                app_switches,
                Some(port)
            );
        });
        let (code, output) = rumtk_web_sync_get(&rumtk_format!("http://127.0.0.1:{port_copy}/")).unwrap();
        let output_str = buffer_to_str(&output).unwrap();
        println!("Output: {}", output_str);
        //%Abz
        assert!(output_str.contains(">Hello World!</h1>"), "Applet responded improperly with wrong response content!");
    }
    #[test]
    fn test_fuzzed_request_get() {
        let port = random::<u16>();
        let port_copy = port.clone();
        spawn(move || {
            pub fn index(app_state: SharedAppState) -> RenderedPageComponentsResult {
                let title_params = rumtk_web_params_map!([("title", "Hello World!")]);
                let title = title(&[], title_params.get_inner(), app_state.clone())?.to_string();
                Ok(vec![title])
            }

            let app_components = rumtk_web_register_app_components!(
                vec![
                    ("index", index),
                ]
            );
            let app_switches = rumtk_web_register_app_switches!(
                false,
                true,
                true
            );
            rumtk_web_run_app!(
                app_components,
                app_switches,
                Some(port)
            );
        });

        let fuzz_get_tests = vec![
            "%Abz"
        ];

        for fuzz_itm in fuzz_get_tests {
            let (code, output) = rumtk_web_sync_get(&rumtk_format!("http://127.0.0.1:{port_copy}/{fuzz_itm}")).unwrap();
            if output.len() != EXPECTED_RESPONSE_SIZE {
                match code {
                    200 => panic!("Applet responded with wrong response size! => {}", output.len()),
                    _ => {
                        println!("Applet responded with handled error message!");
                        continue;
                    }
                }
            }
            let output_str = buffer_to_str(&output).unwrap();
            println!("Output: {}", output_str);
            assert!(output_str.contains(">Hello World!</h1>"), "Applet responded improperly with wrong response content!");
        }
    }
    #[test]
    fn test_fuzzed_request_get_kills_axum() {
        let port = random::<u16>();
        let port_copy = port.clone();
        spawn(move || {
            pub fn index(app_state: SharedAppState) -> RenderedPageComponentsResult {
                let title_params = rumtk_web_params_map!([("title", "Hello World!")]);
                let title = title(&[], title_params.get_inner(), app_state.clone())?.to_string();
                Ok(vec![title])
            }

            let app_components = rumtk_web_register_app_components!(
                vec![
                    ("index", index),
                ]
            );
            let app_switches = rumtk_web_register_app_switches!(
                false,
                true,
                true
            );
            rumtk_web_run_app!(
                app_components,
                app_switches,
                Some(port)
            );
        });

        let fuzz_get_tests = vec![
            "%Abz"
        ];

        for fuzz_itm in fuzz_get_tests {
            let request = rumtk_format!("http://127.0.0.1:{port_copy}/{fuzz_itm}");
            let (code, output) = rumtk_web_sync_get(&request).unwrap();
            if output.len() != EXPECTED_RESPONSE_SIZE {
                match rumtk_web_sync_get(&request) {
                    Ok((c,r)) => {
                        println!("Request[{}] Does not kill axum listener or tokio runtime so allowing... Output: {}", &request,buffer_to_str(&r).unwrap());
                    }
                    Err(e) => {
                        println!("Request[{}] killed axum listener or tokio runtime... Error: {:?}", &request, &e);
                    }
                }
            }
        }
    }

    ///////////////////////////////////Sanitizer Tests///////////////////////////////////////////////
    #[test]
    fn test_svg_sanitizer() {
        let input = "<svg><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\" /></svg>";
        let output = sanitize_html(input, false);
        assert!(output.to_string().contains("<svg><circle"), "SVG and circle tags filtered");
    }
    #[test]
    fn test_svg_sanitizer_full() {
        let input = "<svg><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\" /></svg>";
        let expected: &str = "<svg><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\"></circle></svg>";
        let output = sanitize_html(input, false);
        assert_eq!(&output.to_string(), expected, "Circle tag attributes filtered");
    }

    #[test]
    fn test_sanitize_invalid_tag() {
        let input = "<xml><circle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\" /></xml>";
        let output = sanitize_html(input, true);
        assert!(output.to_string().is_empty(), "XML and circle tags filtered");
    }

    #[test]
    fn test_sanitize_script_tag() {
        let input = r#"<script type="text/javascript">alert('hi');</script>"#;;
        let output = sanitize_html(input, false);
        assert!(output.is_empty(), "Script tag was not filtered as expected!");
    }

    #[test]
    fn test_sanitize_script_tag_relaxed() {
        let input = r#"<script type="text/javascript">alert('hi');</script>"#;;
        let output = sanitize_html(input, true);
        assert_eq!(input, &output, "Script tag was filtered when it shouldn't have or attributes were stripped!");
    }

    #[test]
    fn test_sanitize_consecutive_script_tag_relaxed() {
        let input = r#"<script type="text/javascript">alert('hi');</script><script type="text/javascript">alert('hi2');</script>"#;;
        let output = sanitize_html(input, true);
        assert_eq!(input, &output, "Script tag was filtered when it shouldn't have or attributes were stripped!");
    }

    #[test]
    fn test_sanitize_module_script_tag_relaxed() {
        let input = r#"<script type="module">alert('hi');</script><script type="text/javascript">alert('hi2');</script>"#;;
        let output = sanitize_html(input, true);
        assert_eq!(input, &output, "Script tag was filtered when it shouldn't have or attributes were stripped!");
    }

    #[test]
    fn test_sanitize_consecutive_module_script_tag_relaxed() {
        let input = r#"<script type="module">alert('hi');</script><script type="module">alert('hi2');</script>"#;;
        let output = sanitize_html(input, true);
        assert_eq!(input, &output, "Script tag was filtered when it shouldn't have or attributes were stripped!");
    }
}