socket_patch_core/package_json/
update.rs1use std::path::Path;
2use tokio::fs;
3
4use super::detect::{is_postinstall_configured_str, update_package_json_content};
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 error: Option<String>,
14}
15
16#[derive(Debug, Clone, PartialEq)]
17pub enum UpdateStatus {
18 Updated,
19 AlreadyConfigured,
20 Error,
21}
22
23pub async fn update_package_json(
25 package_json_path: &Path,
26 dry_run: bool,
27) -> UpdateResult {
28 let path_str = package_json_path.display().to_string();
29
30 let content = match fs::read_to_string(package_json_path).await {
31 Ok(c) => c,
32 Err(e) => {
33 return UpdateResult {
34 path: path_str,
35 status: UpdateStatus::Error,
36 old_script: String::new(),
37 new_script: String::new(),
38 error: Some(e.to_string()),
39 };
40 }
41 };
42
43 let status = is_postinstall_configured_str(&content);
44 if !status.needs_update {
45 return UpdateResult {
46 path: path_str,
47 status: UpdateStatus::AlreadyConfigured,
48 old_script: status.current_script.clone(),
49 new_script: status.current_script,
50 error: None,
51 };
52 }
53
54 match update_package_json_content(&content) {
55 Ok((modified, new_content, old_script, new_script)) => {
56 if !modified {
57 return UpdateResult {
58 path: path_str,
59 status: UpdateStatus::AlreadyConfigured,
60 old_script,
61 new_script,
62 error: None,
63 };
64 }
65
66 if !dry_run {
67 if let Err(e) = fs::write(package_json_path, &new_content).await {
68 return UpdateResult {
69 path: path_str,
70 status: UpdateStatus::Error,
71 old_script,
72 new_script,
73 error: Some(e.to_string()),
74 };
75 }
76 }
77
78 UpdateResult {
79 path: path_str,
80 status: UpdateStatus::Updated,
81 old_script,
82 new_script,
83 error: None,
84 }
85 }
86 Err(e) => UpdateResult {
87 path: path_str,
88 status: UpdateStatus::Error,
89 old_script: String::new(),
90 new_script: String::new(),
91 error: Some(e),
92 },
93 }
94}
95
96pub async fn update_multiple_package_jsons(
98 paths: &[&Path],
99 dry_run: bool,
100) -> Vec<UpdateResult> {
101 let mut results = Vec::new();
102 for path in paths {
103 let result = update_package_json(path, dry_run).await;
104 results.push(result);
105 }
106 results
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[tokio::test]
114 async fn test_update_file_not_found() {
115 let dir = tempfile::tempdir().unwrap();
116 let missing = dir.path().join("nonexistent.json");
117 let result = update_package_json(&missing, false).await;
118 assert_eq!(result.status, UpdateStatus::Error);
119 assert!(result.error.is_some());
120 }
121
122 #[tokio::test]
123 async fn test_update_already_configured() {
124 let dir = tempfile::tempdir().unwrap();
125 let pkg = dir.path().join("package.json");
126 fs::write(
127 &pkg,
128 r#"{"name":"test","scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#,
129 )
130 .await
131 .unwrap();
132 let result = update_package_json(&pkg, false).await;
133 assert_eq!(result.status, UpdateStatus::AlreadyConfigured);
134 }
135
136 #[tokio::test]
137 async fn test_update_dry_run_does_not_write() {
138 let dir = tempfile::tempdir().unwrap();
139 let pkg = dir.path().join("package.json");
140 let original = r#"{"name":"test","scripts":{"build":"tsc"}}"#;
141 fs::write(&pkg, original).await.unwrap();
142 let result = update_package_json(&pkg, true).await;
143 assert_eq!(result.status, UpdateStatus::Updated);
144 let content = fs::read_to_string(&pkg).await.unwrap();
146 assert_eq!(content, original);
147 }
148
149 #[tokio::test]
150 async fn test_update_writes_file() {
151 let dir = tempfile::tempdir().unwrap();
152 let pkg = dir.path().join("package.json");
153 fs::write(&pkg, r#"{"name":"test","scripts":{"build":"tsc"}}"#)
154 .await
155 .unwrap();
156 let result = update_package_json(&pkg, false).await;
157 assert_eq!(result.status, UpdateStatus::Updated);
158 let content = fs::read_to_string(&pkg).await.unwrap();
159 assert!(content.contains("socket-patch apply"));
160 }
161
162 #[tokio::test]
163 async fn test_update_invalid_json() {
164 let dir = tempfile::tempdir().unwrap();
165 let pkg = dir.path().join("package.json");
166 fs::write(&pkg, "not json!!!").await.unwrap();
167 let result = update_package_json(&pkg, false).await;
168 assert_eq!(result.status, UpdateStatus::Error);
169 assert!(result.error.is_some());
170 }
171
172 #[tokio::test]
173 async fn test_update_no_scripts_key() {
174 let dir = tempfile::tempdir().unwrap();
175 let pkg = dir.path().join("package.json");
176 fs::write(&pkg, r#"{"name":"x"}"#).await.unwrap();
177 let result = update_package_json(&pkg, false).await;
178 assert_eq!(result.status, UpdateStatus::Updated);
179 let content = fs::read_to_string(&pkg).await.unwrap();
180 assert!(content.contains("postinstall"));
181 assert!(content.contains("socket-patch apply"));
182 }
183
184 #[tokio::test]
185 async fn test_update_multiple_mixed() {
186 let dir = tempfile::tempdir().unwrap();
187
188 let p1 = dir.path().join("a.json");
189 fs::write(&p1, r#"{"name":"a"}"#).await.unwrap();
190
191 let p2 = dir.path().join("b.json");
192 fs::write(
193 &p2,
194 r#"{"name":"b","scripts":{"postinstall":"socket patch apply --silent --ecosystems npm"}}"#,
195 )
196 .await
197 .unwrap();
198
199 let p3 = dir.path().join("c.json");
200 let paths: Vec<&Path> = vec![p1.as_path(), p2.as_path(), p3.as_path()];
203 let results = update_multiple_package_jsons(&paths, false).await;
204 assert_eq!(results.len(), 3);
205 assert_eq!(results[0].status, UpdateStatus::Updated);
206 assert_eq!(results[1].status, UpdateStatus::AlreadyConfigured);
207 assert_eq!(results[2].status, UpdateStatus::Error);
208 }
209}