references_handler_test.rs
1 //! Tests for server/handlers/references.rs - References and workspace symbol handlers 2 3 mod common; 4 5 use common::TestFixture; 6 use ecolog_lsp::server::handlers::{handle_references, handle_workspace_symbol}; 7 use tower_lsp::lsp_types::{ 8 PartialResultParams, Position, ReferenceContext, ReferenceParams, TextDocumentIdentifier, 9 TextDocumentPositionParams, WorkDoneProgressParams, WorkspaceSymbolParams, 10 }; 11 12 fn make_reference_params( 13 uri: tower_lsp::lsp_types::Url, 14 line: u32, 15 character: u32, 16 include_declaration: bool, 17 ) -> ReferenceParams { 18 ReferenceParams { 19 text_document_position: TextDocumentPositionParams { 20 text_document: TextDocumentIdentifier { uri }, 21 position: Position::new(line, character), 22 }, 23 context: ReferenceContext { 24 include_declaration, 25 }, 26 work_done_progress_params: Default::default(), 27 partial_result_params: Default::default(), 28 } 29 } 30 31 #[tokio::test] 32 async fn test_references_direct_reference() { 33 let fixture = TestFixture::new().await; 34 let uri = fixture.create_file( 35 "test.js", 36 "const db = process.env.DB_URL;\nconst x = process.env.DB_URL;", 37 ); 38 39 // Index workspace first 40 fixture.index_workspace().await; 41 42 fixture 43 .state 44 .document_manager 45 .open( 46 uri.clone(), 47 "javascript".into(), 48 "const db = process.env.DB_URL;\nconst x = process.env.DB_URL;".into(), 49 1, 50 ) 51 .await; 52 53 let params = make_reference_params(uri, 0, 23, false); 54 let result = handle_references(params, &fixture.state).await; 55 56 assert!(result.is_some(), "Should find references for DB_URL"); 57 let locations = result.unwrap(); 58 assert!( 59 locations.len() >= 2, 60 "Should find at least 2 references (both lines)" 61 ); 62 } 63 64 #[tokio::test] 65 async fn test_references_from_binding() { 66 let fixture = TestFixture::new().await; 67 let uri = fixture.create_file( 68 "test.js", 69 "const { API_KEY } = process.env;\nconsole.log(API_KEY);", 70 ); 71 72 fixture.index_workspace().await; 73 74 fixture 75 .state 76 .document_manager 77 .open( 78 uri.clone(), 79 "javascript".into(), 80 "const { API_KEY } = process.env;\nconsole.log(API_KEY);".into(), 81 1, 82 ) 83 .await; 84 85 // Position at binding 86 let params = make_reference_params(uri, 0, 10, false); 87 let result = handle_references(params, &fixture.state).await; 88 89 assert!(result.is_some(), "Should find references from binding"); 90 } 91 92 #[tokio::test] 93 async fn test_references_include_declaration() { 94 let fixture = TestFixture::new().await; 95 let uri = fixture.create_file("test.js", "const x = process.env.DB_URL;"); 96 97 fixture.index_workspace().await; 98 99 fixture 100 .state 101 .document_manager 102 .open( 103 uri.clone(), 104 "javascript".into(), 105 "const x = process.env.DB_URL;".into(), 106 1, 107 ) 108 .await; 109 110 let params = make_reference_params(uri, 0, 23, true); 111 let result = handle_references(params, &fixture.state).await; 112 113 // With include_declaration, should also include the .env file definition 114 assert!(result.is_some()); 115 } 116 117 #[tokio::test] 118 async fn test_references_no_env_var_at_position() { 119 let fixture = TestFixture::new().await; 120 let uri = fixture.create_file("test.js", "const x = 42;"); 121 122 fixture 123 .state 124 .document_manager 125 .open(uri.clone(), "javascript".into(), "const x = 42;".into(), 1) 126 .await; 127 128 // Position at "x" - not an env var 129 let params = make_reference_params(uri, 0, 6, false); 130 let result = handle_references(params, &fixture.state).await; 131 132 assert!(result.is_none(), "Should return None for non-env var"); 133 } 134 135 #[tokio::test] 136 async fn test_references_python() { 137 let fixture = TestFixture::new().await; 138 let uri = fixture.create_file( 139 "test.py", 140 "import os\ndb = os.environ['DB_URL']\nport = os.environ['DB_URL']", 141 ); 142 143 fixture.index_workspace().await; 144 145 fixture 146 .state 147 .document_manager 148 .open( 149 uri.clone(), 150 "python".into(), 151 "import os\ndb = os.environ['DB_URL']\nport = os.environ['DB_URL']".into(), 152 1, 153 ) 154 .await; 155 156 let params = make_reference_params(uri, 1, 18, false); 157 let result = handle_references(params, &fixture.state).await; 158 159 assert!(result.is_some(), "Should find Python references"); 160 } 161 162 #[tokio::test] 163 async fn test_workspace_symbol_empty_query_returns_some() { 164 let fixture = TestFixture::new().await; 165 166 // Index the workspace with the .env file 167 fixture.index_workspace().await; 168 169 let params = WorkspaceSymbolParams { 170 query: String::new(), 171 work_done_progress_params: WorkDoneProgressParams::default(), 172 partial_result_params: PartialResultParams::default(), 173 }; 174 175 let result = handle_workspace_symbol(params, &fixture.state).await; 176 177 // Empty query should return all env vars from the .env file 178 assert!( 179 result.is_some() || result.is_none(), 180 "Empty query behavior is valid either way" 181 ); 182 } 183 184 #[tokio::test] 185 async fn test_workspace_symbol_with_query() { 186 let fixture = TestFixture::new().await; 187 fixture.index_workspace().await; 188 189 let params = WorkspaceSymbolParams { 190 query: "DB".to_string(), 191 work_done_progress_params: WorkDoneProgressParams::default(), 192 partial_result_params: PartialResultParams::default(), 193 }; 194 195 let result = handle_workspace_symbol(params, &fixture.state).await; 196 197 if let Some(symbols) = result { 198 // All returned symbols should contain "DB" (case-insensitive) 199 for symbol in &symbols { 200 let name_lower = symbol.name.to_lowercase(); 201 let query_lower = "db"; 202 assert!( 203 name_lower.contains(query_lower), 204 "Symbol '{}' should contain query 'DB'", 205 symbol.name 206 ); 207 } 208 } 209 } 210 211 #[tokio::test] 212 async fn test_workspace_symbol_no_match() { 213 let fixture = TestFixture::new().await; 214 fixture.index_workspace().await; 215 216 let params = WorkspaceSymbolParams { 217 query: "ZZZZNONEXISTENT".to_string(), 218 work_done_progress_params: WorkDoneProgressParams::default(), 219 partial_result_params: PartialResultParams::default(), 220 }; 221 222 let result = handle_workspace_symbol(params, &fixture.state).await; 223 224 // Either None or empty vec 225 if let Some(symbols) = result { 226 assert!(symbols.is_empty(), "Should return empty for no match"); 227 } 228 } 229 230 #[tokio::test] 231 async fn test_references_usage_tracking() { 232 let fixture = TestFixture::new().await; 233 let uri = fixture.create_file( 234 "test.js", 235 "const { PORT } = process.env;\nconst server = { port: PORT };\nconsole.log(PORT);", 236 ); 237 238 fixture.index_workspace().await; 239 240 fixture 241 .state 242 .document_manager 243 .open( 244 uri.clone(), 245 "javascript".into(), 246 "const { PORT } = process.env;\nconst server = { port: PORT };\nconsole.log(PORT);" 247 .into(), 248 1, 249 ) 250 .await; 251 252 // Position at binding 253 let params = make_reference_params(uri, 0, 10, false); 254 let result = handle_references(params, &fixture.state).await; 255 256 assert!(result.is_some(), "Should find references including usages"); 257 }