socket_patch_core/package_json/
update.rs1use std::path::Path;
2use tokio::fs;
3
4use super::detect::{is_setup_configured_str, update_package_json_content, PackageManager};
5
6#[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
25pub 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 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}