konnektoren_core/challenges/package/
package_reader.rs

1use crate::asset_loader::AssetLoader;
2use crate::challenges::{Package, PackageMetadata};
3use std::collections::HashMap;
4use std::io::{Cursor, Read};
5use zip::ZipArchive;
6
7pub struct PackageReader;
8
9impl PackageReader {
10    pub async fn download(url: &str) -> Result<Vec<u8>, String> {
11        let loader = AssetLoader::default();
12        loader.load_binary(url).await
13    }
14
15    pub fn read(package_data: &[u8]) -> Result<Package, String> {
16        let reader = Cursor::new(package_data);
17        let mut archive =
18            ZipArchive::new(reader).map_err(|e| format!("Failed to create ZIP archive: {}", e))?;
19
20        let mut files = HashMap::new();
21        let mut config = None;
22        let mut custom = None;
23
24        for i in 0..archive.len() {
25            let mut file = archive
26                .by_index(i)
27                .map_err(|e| format!("Failed to read file from archive: {}", e))?;
28            let file_name = file.name().to_string();
29
30            let mut contents = Vec::new();
31            file.read_to_end(&mut contents)
32                .map_err(|e| format!("Failed to read file contents: {}", e))?;
33
34            match file_name.as_str() {
35                "config.yml" => {
36                    config = Some(
37                        serde_yaml::from_slice(&contents)
38                            .map_err(|e| format!("Failed to parse config.yml: {}", e))?,
39                    );
40                }
41                "challenge.yml" => {
42                    custom = Some(
43                        serde_yaml::from_slice(&contents)
44                            .map_err(|e| format!("Failed to parse challenge.yml: {}", e))?,
45                    );
46                }
47                _ => {
48                    files.insert(file_name, contents);
49                }
50            }
51        }
52
53        let config = config.ok_or_else(|| "Missing config.yml in package".to_string())?;
54        let custom = custom.unwrap_or_default();
55        let metadata = PackageMetadata { config, custom };
56
57        Ok(Package { metadata, files })
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use env_logger;
65    #[cfg(feature = "ssr")]
66    use std::env;
67
68    #[test]
69    fn test_package_reader() {
70        env_logger::init();
71        let package_data = include_bytes!("../../../assets/articles-pkg.zip");
72        let package = PackageReader::read(package_data).unwrap();
73
74        assert_eq!(package.files.len(), 5);
75        log::debug!("files {:?}", package.files.keys());
76        assert!(package.get_html_file().is_some());
77        assert!(package.get_css_file().is_some());
78        assert!(package.get_js_file().is_some());
79        assert!(package.get_results_file().is_some());
80    }
81
82    #[cfg(feature = "ssr")]
83    #[tokio::test]
84    async fn test_package_reader_ssr() {
85        use std::fs;
86        use tempfile::TempDir;
87
88        // 1. Create a temporary directory for BUILD_DIR
89        let temp_dir = TempDir::new().unwrap();
90        let build_dir_path = temp_dir.path().to_str().unwrap().to_string();
91
92        // 2. Create a test zip file within the temporary directory
93        let test_zip_content = include_bytes!("../../../assets/articles-pkg.zip");
94        let test_zip_path = format!("{}/test_package.zip", build_dir_path);
95        fs::write(&test_zip_path, test_zip_content).unwrap();
96
97        // 3. Set the BUILD_DIR environment variable
98        let _guard = SetEnvVariableGuard::new("BUILD_DIR", Some(build_dir_path.clone()));
99
100        // 4. Instead of using the file:// protocol, read the file directly
101        // since we're testing the read functionality more than the download part
102        let package_data = fs::read(&test_zip_path).unwrap();
103        let package = PackageReader::read(&package_data).unwrap();
104
105        // 5. Verify the package contents
106        assert_eq!(package.files.len(), 5);
107        assert!(package.get_html_file().is_some());
108        assert!(package.get_css_file().is_some());
109        assert!(package.get_js_file().is_some());
110        assert!(package.get_results_file().is_some());
111    }
112
113    #[cfg(feature = "ssr")]
114    struct SetEnvVariableGuard {
115        name: String,
116        original_value: Option<String>,
117    }
118
119    #[cfg(feature = "ssr")]
120    impl SetEnvVariableGuard {
121        fn new(name: impl Into<String>, value: Option<String>) -> Self {
122            let name = name.into();
123            let original_value = env::var(&name).ok();
124
125            match &value {
126                Some(value) => unsafe { env::set_var(&name, value) },
127                None => unsafe { env::remove_var(&name) },
128            }
129
130            Self {
131                name,
132                original_value,
133            }
134        }
135    }
136
137    #[cfg(feature = "ssr")]
138    impl Drop for SetEnvVariableGuard {
139        fn drop(&mut self) {
140            match &self.original_value {
141                Some(value) => unsafe { env::set_var(&self.name, value) },
142                None => unsafe { env::remove_var(&self.name) },
143            }
144        }
145    }
146}