/ tests / references_rename_test.rs
references_rename_test.rs
  1  mod common;
  2  
  3  use common::TestFixture;
  4  use ecolog_lsp::server::handlers::{handle_prepare_rename, handle_references, handle_rename};
  5  use tower_lsp::lsp_types::{
  6      Position, ReferenceContext, ReferenceParams, RenameParams, TextDocumentIdentifier,
  7      TextDocumentPositionParams,
  8  };
  9  
 10  #[tokio::test]
 11  async fn test_find_references_direct_reference() {
 12      let fixture = TestFixture::new().await;
 13  
 14      let uri = fixture.create_file(
 15          "test.js",
 16          "const url = process.env.DB_URL;\nconsole.log(process.env.DB_URL);",
 17      );
 18  
 19      fixture.index_workspace().await;
 20  
 21      fixture
 22          .state
 23          .document_manager
 24          .open(
 25              uri.clone(),
 26              "javascript".to_string(),
 27              "const url = process.env.DB_URL;\nconsole.log(process.env.DB_URL);".to_string(),
 28              1,
 29          )
 30          .await;
 31  
 32      let params = ReferenceParams {
 33          text_document_position: TextDocumentPositionParams {
 34              text_document: TextDocumentIdentifier { uri: uri.clone() },
 35              position: Position {
 36                  line: 0,
 37                  character: 24,
 38              },
 39          },
 40          work_done_progress_params: Default::default(),
 41          partial_result_params: Default::default(),
 42          context: ReferenceContext {
 43              include_declaration: true,
 44          },
 45      };
 46  
 47      let result = handle_references(params, &fixture.state).await;
 48  
 49      assert!(result.is_some(), "Expected to find references");
 50      let locations = result.unwrap();
 51  
 52      assert!(
 53          locations.len() >= 2,
 54          "Expected at least 2 references, found {}",
 55          locations.len()
 56      );
 57  }
 58  
 59  #[tokio::test]
 60  async fn test_find_references_across_files() {
 61      let fixture = TestFixture::new().await;
 62  
 63      fixture.create_file("a.js", "const key = process.env.API_KEY;");
 64      fixture.create_file("b.ts", "const apiKey = process.env.API_KEY;");
 65  
 66      fixture.index_workspace().await;
 67  
 68      let uri_a = fixture.create_file("a.js", "const key = process.env.API_KEY;");
 69      fixture
 70          .state
 71          .document_manager
 72          .open(
 73              uri_a.clone(),
 74              "javascript".to_string(),
 75              "const key = process.env.API_KEY;".to_string(),
 76              1,
 77          )
 78          .await;
 79  
 80      let params = ReferenceParams {
 81          text_document_position: TextDocumentPositionParams {
 82              text_document: TextDocumentIdentifier { uri: uri_a },
 83              position: Position {
 84                  line: 0,
 85                  character: 24,
 86              },
 87          },
 88          work_done_progress_params: Default::default(),
 89          partial_result_params: Default::default(),
 90          context: ReferenceContext {
 91              include_declaration: true,
 92          },
 93      };
 94  
 95      let result = handle_references(params, &fixture.state).await;
 96  
 97      assert!(result.is_some(), "Expected to find references");
 98      let locations = result.unwrap();
 99  
100      assert!(
101          locations.len() >= 2,
102          "Expected at least 2 references across files, found {}",
103          locations.len()
104      );
105  }
106  
107  #[tokio::test]
108  async fn test_find_references_no_refs_for_unknown_var() {
109      let fixture = TestFixture::new().await;
110  
111      let uri = fixture.create_file("test.js", "const x = process.env.UNKNOWN_VAR;");
112  
113      fixture.index_workspace().await;
114  
115      fixture
116          .state
117          .document_manager
118          .open(
119              uri.clone(),
120              "javascript".to_string(),
121              "const x = process.env.UNKNOWN_VAR;".to_string(),
122              1,
123          )
124          .await;
125  
126      let params = ReferenceParams {
127          text_document_position: TextDocumentPositionParams {
128              text_document: TextDocumentIdentifier { uri },
129              position: Position {
130                  line: 0,
131                  character: 22,
132              },
133          },
134          work_done_progress_params: Default::default(),
135          partial_result_params: Default::default(),
136          context: ReferenceContext {
137              include_declaration: false,
138          },
139      };
140  
141      let result = handle_references(params, &fixture.state).await;
142  
143      assert!(result.is_some());
144  }
145  
146  #[tokio::test]
147  async fn test_prepare_rename_valid_env_var() {
148      let fixture = TestFixture::new().await;
149  
150      let uri = fixture.create_file("test.js", "const url = process.env.DB_URL;");
151  
152      fixture
153          .state
154          .document_manager
155          .open(
156              uri.clone(),
157              "javascript".to_string(),
158              "const url = process.env.DB_URL;".to_string(),
159              1,
160          )
161          .await;
162  
163      let params = TextDocumentPositionParams {
164          text_document: TextDocumentIdentifier { uri },
165          position: Position {
166              line: 0,
167              character: 24,
168          },
169      };
170  
171      let result = handle_prepare_rename(params, &fixture.state).await;
172  
173      assert!(
174          result.is_some(),
175          "Prepare rename should succeed for valid env var"
176      );
177  }
178  
179  #[tokio::test]
180  async fn test_rename_env_var() {
181      let fixture = TestFixture::new().await;
182  
183      let uri = fixture.create_file(
184          "test.js",
185          "const url = process.env.DB_URL;\nconst x = process.env.DB_URL;",
186      );
187  
188      fixture.index_workspace().await;
189  
190      fixture
191          .state
192          .document_manager
193          .open(
194              uri.clone(),
195              "javascript".to_string(),
196              "const url = process.env.DB_URL;\nconst x = process.env.DB_URL;".to_string(),
197              1,
198          )
199          .await;
200  
201      let params = RenameParams {
202          text_document_position: TextDocumentPositionParams {
203              text_document: TextDocumentIdentifier { uri },
204              position: Position {
205                  line: 0,
206                  character: 24,
207              },
208          },
209          new_name: "DATABASE_URL".to_string(),
210          work_done_progress_params: Default::default(),
211      };
212  
213      let result = handle_rename(params, &fixture.state).await;
214  
215      assert!(result.is_some(), "Rename should return edits");
216      let edit = result.unwrap();
217  
218      assert!(edit.changes.is_some(), "WorkspaceEdit should have changes");
219      let changes = edit.changes.unwrap();
220  
221      assert!(
222          !changes.is_empty(),
223          "Should have edits for at least one file"
224      );
225  }
226  
227  #[tokio::test]
228  async fn test_rename_invalid_new_name() {
229      let fixture = TestFixture::new().await;
230  
231      let uri = fixture.create_file("test.js", "const url = process.env.DB_URL;");
232  
233      fixture
234          .state
235          .document_manager
236          .open(
237              uri.clone(),
238              "javascript".to_string(),
239              "const url = process.env.DB_URL;".to_string(),
240              1,
241          )
242          .await;
243  
244      let params = RenameParams {
245          text_document_position: TextDocumentPositionParams {
246              text_document: TextDocumentIdentifier { uri },
247              position: Position {
248                  line: 0,
249                  character: 24,
250              },
251          },
252          new_name: "123_INVALID".to_string(),
253          work_done_progress_params: Default::default(),
254      };
255  
256      let result = handle_rename(params, &fixture.state).await;
257  
258      assert!(
259          result.is_none(),
260          "Rename with invalid name should return None"
261      );
262  }
263  
264  #[tokio::test]
265  async fn test_prepare_rename_in_env_file() {
266      let fixture = TestFixture::new().await;
267  
268      let env_uri = fixture.create_file(".env", "API_KEY=secret123\nDB_URL=postgres://localhost");
269  
270      fixture
271          .state
272          .document_manager
273          .open(
274              env_uri.clone(),
275              "plaintext".to_string(),
276              "API_KEY=secret123\nDB_URL=postgres://localhost".to_string(),
277              1,
278          )
279          .await;
280  
281      let params = TextDocumentPositionParams {
282          text_document: TextDocumentIdentifier { uri: env_uri },
283          position: Position {
284              line: 0,
285              character: 3,
286          },
287      };
288  
289      let result = handle_prepare_rename(params, &fixture.state).await;
290  
291      assert!(
292          result.is_some(),
293          "Prepare rename should succeed for env var in .env file"
294      );
295  }
296  
297  #[tokio::test]
298  async fn test_rename_from_env_file() {
299      let fixture = TestFixture::new().await;
300  
301      let env_uri = fixture.create_file(".env", "API_KEY=secret123");
302  
303      let js_uri = fixture.create_file("app.js", "const key = process.env.API_KEY;");
304      let ts_uri = fixture.create_file("config.ts", "const apiKey = process.env.API_KEY;");
305  
306      fixture.index_workspace().await;
307  
308      fixture
309          .state
310          .document_manager
311          .open(
312              env_uri.clone(),
313              "plaintext".to_string(),
314              "API_KEY=secret123".to_string(),
315              1,
316          )
317          .await;
318      fixture
319          .state
320          .document_manager
321          .open(
322              js_uri.clone(),
323              "javascript".to_string(),
324              "const key = process.env.API_KEY;".to_string(),
325              1,
326          )
327          .await;
328      fixture
329          .state
330          .document_manager
331          .open(
332              ts_uri.clone(),
333              "typescript".to_string(),
334              "const apiKey = process.env.API_KEY;".to_string(),
335              1,
336          )
337          .await;
338  
339      let params = RenameParams {
340          text_document_position: TextDocumentPositionParams {
341              text_document: TextDocumentIdentifier {
342                  uri: env_uri.clone(),
343              },
344              position: Position {
345                  line: 0,
346                  character: 3,
347              },
348          },
349          new_name: "AUTH_TOKEN".to_string(),
350          work_done_progress_params: Default::default(),
351      };
352  
353      let result = handle_rename(params, &fixture.state).await;
354  
355      assert!(
356          result.is_some(),
357          "Rename from .env file should return edits"
358      );
359      let edit = result.unwrap();
360  
361      assert!(edit.changes.is_some(), "WorkspaceEdit should have changes");
362      let changes = edit.changes.unwrap();
363  
364      assert!(
365          !changes.is_empty(),
366          "Should have edits for at least one file"
367      );
368  
369      assert!(
370          changes.contains_key(&env_uri),
371          "Changes should include the .env file"
372      );
373  }
374  
375  #[tokio::test]
376  async fn test_rename_from_env_file_updates_code_files() {
377      let fixture = TestFixture::new().await;
378  
379      let env_uri = fixture.create_file(".env", "DB_HOST=localhost");
380  
381      let js_content = "const host = process.env.DB_HOST;\nconst url = `http://${host}`;";
382      let js_uri = fixture.create_file("server.js", js_content);
383  
384      fixture.index_workspace().await;
385  
386      fixture
387          .state
388          .document_manager
389          .open(
390              env_uri.clone(),
391              "plaintext".to_string(),
392              "DB_HOST=localhost".to_string(),
393              1,
394          )
395          .await;
396      fixture
397          .state
398          .document_manager
399          .open(
400              js_uri.clone(),
401              "javascript".to_string(),
402              js_content.to_string(),
403              1,
404          )
405          .await;
406  
407      let params = RenameParams {
408          text_document_position: TextDocumentPositionParams {
409              text_document: TextDocumentIdentifier {
410                  uri: env_uri.clone(),
411              },
412              position: Position {
413                  line: 0,
414                  character: 2,
415              },
416          },
417          new_name: "DATABASE_HOST".to_string(),
418          work_done_progress_params: Default::default(),
419      };
420  
421      let result = handle_rename(params, &fixture.state).await;
422  
423      assert!(result.is_some(), "Rename should succeed");
424      let edit = result.unwrap();
425      let changes = edit.changes.expect("Should have changes");
426  
427      if changes.contains_key(&js_uri) {
428          let js_edits = &changes[&js_uri];
429  
430          assert!(js_edits.len() >= 1, "JS file should have at least 1 edit");
431      }
432  
433      assert!(
434          changes.contains_key(&env_uri),
435          ".env file should be in changes"
436      );
437  }
438  
439  #[tokio::test]
440  async fn test_workspace_index_stats() {
441      let fixture = TestFixture::new().await;
442  
443      fixture.create_file("a.js", "process.env.API_KEY");
444      fixture.create_file("b.ts", "process.env.DB_URL");
445      fixture.create_file("c.py", "os.environ['DEBUG']");
446  
447      fixture.index_workspace().await;
448  
449      let stats = fixture.state.workspace_index.stats();
450  
451      assert!(
452          stats.total_files >= 1,
453          "Should have indexed at least 1 file"
454      );
455      assert!(
456          stats.total_env_vars >= 1,
457          "Should have indexed at least 1 env var"
458      );
459  }
460  
461  #[tokio::test]
462  async fn test_workspace_index_files_for_env_var() {
463      let fixture = TestFixture::new().await;
464  
465      fixture.create_file("a.js", "const k = process.env.API_KEY;");
466      fixture.create_file("b.ts", "const key = process.env.API_KEY;");
467      fixture.create_file("c.js", "const port = process.env.PORT;");
468  
469      fixture.index_workspace().await;
470  
471      let api_key_files = fixture.state.workspace_index.files_for_env_var("API_KEY");
472  
473      assert!(
474          api_key_files.len() >= 2,
475          "Expected at least 2 files with API_KEY, found {}",
476          api_key_files.len()
477      );
478  }
479  
480  #[tokio::test]
481  async fn test_rename_does_not_affect_similar_names() {
482      let fixture = TestFixture::new().await;
483  
484      let env_uri = fixture.create_file(".env", "DEBUG=on\nDEBUGHAT=value");
485  
486      let js_content = "const d = process.env.DEBUG;\nconst dh = process.env.DEBUGHAT;";
487      let js_uri = fixture.create_file("app.js", js_content);
488  
489      fixture.index_workspace().await;
490  
491      fixture
492          .state
493          .document_manager
494          .open(
495              env_uri.clone(),
496              "plaintext".to_string(),
497              "DEBUG=on\nDEBUGHAT=value".to_string(),
498              1,
499          )
500          .await;
501      fixture
502          .state
503          .document_manager
504          .open(
505              js_uri.clone(),
506              "javascript".to_string(),
507              js_content.to_string(),
508              1,
509          )
510          .await;
511  
512      let params = RenameParams {
513          text_document_position: TextDocumentPositionParams {
514              text_document: TextDocumentIdentifier {
515                  uri: env_uri.clone(),
516              },
517              position: Position {
518                  line: 0,
519                  character: 0,
520              },
521          },
522          new_name: "SOMEWHAT".to_string(),
523          work_done_progress_params: Default::default(),
524      };
525  
526      let result = handle_rename(params, &fixture.state).await;
527  
528      assert!(result.is_some(), "Rename should succeed");
529      let edit = result.unwrap();
530      let changes = edit.changes.expect("Should have changes");
531  
532      let env_edits = changes.get(&env_uri).expect(".env should have edits");
533      assert_eq!(env_edits.len(), 1, "Should only rename DEBUG, not DEBUGHAT");
534  
535      let first_edit = &env_edits[0];
536      assert_eq!(first_edit.new_text, "SOMEWHAT");
537      assert_eq!(first_edit.range.start.line, 0);
538      assert_eq!(first_edit.range.start.character, 0);
539      assert_eq!(first_edit.range.end.line, 0);
540      assert_eq!(first_edit.range.end.character, 5);
541  
542      if let Some(js_edits) = changes.get(&js_uri) {
543          assert_eq!(
544              js_edits.len(),
545              1,
546              "Should only rename DEBUG reference, not DEBUGHAT"
547          );
548          let js_edit = &js_edits[0];
549          assert_eq!(js_edit.new_text, "SOMEWHAT");
550      }
551  }