Skip to main content

socket_patch_core/package_json/
update.rs

1use std::path::Path;
2use tokio::fs;
3
4use super::detect::{is_setup_configured_str, update_package_json_content, PackageManager};
5
6/// Result of updating a single package.json.
7#[derive(Debug, Clone)]
8pub struct UpdateResult {
9    pub path: String,
10    pub status: UpdateStatus,
11    pub old_script: String,
12    pub new_script: String,
13    pub old_dependencies_script: String,
14    pub new_dependencies_script: String,
15    pub error: Option<String>,
16}
17
18#[derive(Debug, Clone, PartialEq)]
19pub enum UpdateStatus {
20    Updated,
21    AlreadyConfigured,
22    Error,
23}
24
25/// Update a single package.json file with socket-patch lifecycle scripts.
26pub async fn update_package_json(
27    package_json_path: &Path,
28    dry_run: bool,
29    pm: PackageManager,
30) -> UpdateResult {
31    let path_str = package_json_path.display().to_string();
32
33    let content = match fs::read_to_string(package_json_path).await {
34        Ok(c) => c,
35        Err(e) => {
36            return UpdateResult {
37                path: path_str,
38                status: UpdateStatus::Error,
39                old_script: String::new(),
40                new_script: String::new(),
41                old_dependencies_script: String::new(),
42                new_dependencies_script: String::new(),
43                error: Some(e.to_string()),
44            };
45        }
46    };
47
48    let status = is_setup_configured_str(&content);
49    if !status.needs_update {
50        return UpdateResult {
51            path: path_str,
52            status: UpdateStatus::AlreadyConfigured,
53            old_script: status.postinstall_script.clone(),
54            new_script: status.postinstall_script,
55            old_dependencies_script: status.dependencies_script.clone(),
56            new_dependencies_script: status.dependencies_script,
57            error: None,
58        };
59    }
60
61    match update_package_json_content(&content, pm) {
62        Ok((modified, new_content, old_pi, new_pi, old_dep, new_dep)) => {
63            if !modified {
64                return UpdateResult {
65                    path: path_str,
66                    status: UpdateStatus::AlreadyConfigured,
67                    old_script: old_pi,
68                    new_script: new_pi,
69                    old_dependencies_script: old_dep,
70                    new_dependencies_script: new_dep,
71                    error: None,
72                };
73            }
74
75            if !dry_run {
76                if let Err(e) = fs::write(package_json_path, &new_content).await {
77                    return UpdateResult {
78                        path: path_str,
79                        status: UpdateStatus::Error,
80                        old_script: old_pi,
81                        new_script: new_pi,
82                        old_dependencies_script: old_dep,
83                        new_dependencies_script: new_dep,
84                        error: Some(e.to_string()),
85                    };
86                }
87            }
88
89            UpdateResult {
90                path: path_str,
91                status: UpdateStatus::Updated,
92                old_script: old_pi,
93                new_script: new_pi,
94                old_dependencies_script: old_dep,
95                new_dependencies_script: new_dep,
96                error: None,
97            }
98        }
99        Err(e) => UpdateResult {
100            path: path_str,
101            status: UpdateStatus::Error,
102            old_script: String::new(),
103            new_script: String::new(),
104            old_dependencies_script: String::new(),
105            new_dependencies_script: String::new(),
106            error: Some(e),
107        },
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[tokio::test]
116    async fn test_update_file_not_found() {
117        let dir = tempfile::tempdir().unwrap();
118        let missing = dir.path().join("nonexistent.json");
119        let result = update_package_json(&missing, false, PackageManager::Npm).await;
120        assert_eq!(result.status, UpdateStatus::Error);
121        assert!(result.error.is_some());
122    }
123
124    #[tokio::test]
125    async fn test_update_already_configured() {
126        let dir = tempfile::tempdir().unwrap();
127        let pkg = dir.path().join("package.json");
128        fs::write(
129            &pkg,
130            r#"{"name":"test","scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent --ecosystems npm","dependencies":"npx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#,
131        )
132        .await
133        .unwrap();
134        let result = update_package_json(&pkg, false, PackageManager::Npm).await;
135        assert_eq!(result.status, UpdateStatus::AlreadyConfigured);
136    }
137
138    #[tokio::test]
139    async fn test_update_dry_run_does_not_write() {
140        let dir = tempfile::tempdir().unwrap();
141        let pkg = dir.path().join("package.json");
142        let original = r#"{"name":"test","scripts":{"build":"tsc"}}"#;
143        fs::write(&pkg, original).await.unwrap();
144        let result = update_package_json(&pkg, true, PackageManager::Npm).await;
145        assert_eq!(result.status, UpdateStatus::Updated);
146        // File should remain unchanged
147        let content = fs::read_to_string(&pkg).await.unwrap();
148        assert_eq!(content, original);
149    }
150
151    #[tokio::test]
152    async fn test_update_writes_file() {
153        let dir = tempfile::tempdir().unwrap();
154        let pkg = dir.path().join("package.json");
155        fs::write(&pkg, r#"{"name":"test","scripts":{"build":"tsc"}}"#)
156            .await
157            .unwrap();
158        let result = update_package_json(&pkg, false, PackageManager::Npm).await;
159        assert_eq!(result.status, UpdateStatus::Updated);
160        let content = fs::read_to_string(&pkg).await.unwrap();
161        assert!(content.contains("npx @socketsecurity/socket-patch apply"));
162        assert!(content.contains("postinstall"));
163        assert!(content.contains("dependencies"));
164    }
165
166    #[tokio::test]
167    async fn test_update_invalid_json() {
168        let dir = tempfile::tempdir().unwrap();
169        let pkg = dir.path().join("package.json");
170        fs::write(&pkg, "not json!!!").await.unwrap();
171        let result = update_package_json(&pkg, false, PackageManager::Npm).await;
172        assert_eq!(result.status, UpdateStatus::Error);
173        assert!(result.error.is_some());
174    }
175
176    #[tokio::test]
177    async fn test_update_no_scripts_key() {
178        let dir = tempfile::tempdir().unwrap();
179        let pkg = dir.path().join("package.json");
180        fs::write(&pkg, r#"{"name":"x"}"#).await.unwrap();
181        let result = update_package_json(&pkg, false, PackageManager::Npm).await;
182        assert_eq!(result.status, UpdateStatus::Updated);
183        let content = fs::read_to_string(&pkg).await.unwrap();
184        assert!(content.contains("postinstall"));
185        assert!(content.contains("dependencies"));
186        assert!(content.contains("npx @socketsecurity/socket-patch apply"));
187    }
188
189    #[tokio::test]
190    async fn test_update_pnpm() {
191        let dir = tempfile::tempdir().unwrap();
192        let pkg = dir.path().join("package.json");
193        fs::write(&pkg, r#"{"name":"x"}"#).await.unwrap();
194        let result = update_package_json(&pkg, false, PackageManager::Pnpm).await;
195        assert_eq!(result.status, UpdateStatus::Updated);
196        let content = fs::read_to_string(&pkg).await.unwrap();
197        assert!(content.contains("pnpm dlx @socketsecurity/socket-patch apply"));
198    }
199
200    #[tokio::test]
201    async fn test_update_adds_dependencies_when_postinstall_exists() {
202        let dir = tempfile::tempdir().unwrap();
203        let pkg = dir.path().join("package.json");
204        fs::write(
205            &pkg,
206            r#"{"name":"test","scripts":{"postinstall":"npx @socketsecurity/socket-patch apply --silent --ecosystems npm"}}"#,
207        )
208        .await
209        .unwrap();
210        let result = update_package_json(&pkg, false, PackageManager::Npm).await;
211        assert_eq!(result.status, UpdateStatus::Updated);
212        let content = fs::read_to_string(&pkg).await.unwrap();
213        assert!(content.contains("dependencies"));
214    }
215
216}