1 /* 2 * CDDL HEADER START 3 * 4 * The contents of this file are subject to the terms of the 5 * Common Development and Distribution License (the "License"). 6 * You may not use this file except in compliance with the License. 7 * 8 * See LICENSE.txt included in this distribution for the specific 9 * language governing permissions and limitations under the License. 10 * 11 * When distributing Covered Code, include this CDDL HEADER in each 12 * file and include the License file at LICENSE.txt. 13 * If applicable, add the following below this CDDL HEADER, with the 14 * fields enclosed by brackets "[]" replaced with your own identifying 15 * information: Portions Copyright [yyyy] [name of copyright owner] 16 * 17 * CDDL HEADER END 18 */ 19 20 /* 21 * Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved. 22 * Portions Copyright (c) 2020, Chris Fraire <cfraire@me.com>. 23 */ 24 package org.opengrok.web; 25 26 import java.io.File; 27 import java.io.FileNotFoundException; 28 import java.io.IOException; 29 import java.nio.file.Files; 30 import java.nio.file.Path; 31 import java.nio.file.Paths; 32 import java.util.ArrayList; 33 import java.util.Arrays; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.stream.Collectors; 37 38 import jakarta.servlet.http.HttpServletRequest; 39 import org.junit.jupiter.api.AfterAll; 40 import org.junit.jupiter.api.BeforeAll; 41 import org.junit.jupiter.api.Test; 42 import org.junit.jupiter.api.condition.EnabledOnOs; 43 import org.junit.jupiter.api.condition.OS; 44 import org.opengrok.indexer.authorization.AuthControlFlag; 45 import org.opengrok.indexer.authorization.AuthorizationFramework; 46 import org.opengrok.indexer.authorization.AuthorizationPlugin; 47 import org.opengrok.indexer.authorization.TestPlugin; 48 import org.opengrok.indexer.condition.EnabledForRepository; 49 import org.opengrok.indexer.configuration.Project; 50 import org.opengrok.indexer.configuration.RuntimeEnvironment; 51 import org.opengrok.indexer.history.Annotation; 52 import org.opengrok.indexer.history.HistoryGuru; 53 import org.opengrok.indexer.history.RepositoryFactory; 54 import org.opengrok.indexer.index.Indexer; 55 import org.opengrok.indexer.util.TestRepository; 56 import org.opengrok.indexer.web.DummyHttpServletRequest; 57 58 import static org.junit.jupiter.api.Assertions.assertEquals; 59 import static org.junit.jupiter.api.Assertions.assertFalse; 60 import static org.junit.jupiter.api.Assertions.assertNotNull; 61 import static org.junit.jupiter.api.Assertions.assertNull; 62 import static org.junit.jupiter.api.Assertions.assertThrows; 63 import static org.junit.jupiter.api.Assertions.assertTrue; 64 import static org.junit.jupiter.api.Assumptions.assumeTrue; 65 import static org.opengrok.indexer.condition.RepositoryInstalled.Type.MERCURIAL; 66 67 /** 68 * Unit tests for the {@code PageConfig} class. 69 */ 70 public class PageConfigTest { 71 private static TestRepository repository = new TestRepository(); 72 73 @BeforeAll setUpClass()74 public static void setUpClass() throws Exception { 75 repository = new TestRepository(); 76 repository.create(HistoryGuru.class.getResource("/repositories")); 77 RuntimeEnvironment.getInstance().setRepositories(repository.getSourceRoot()); 78 } 79 80 @AfterAll tearDownClass()81 public static void tearDownClass() throws Exception { 82 repository.destroy(); 83 repository = null; 84 } 85 86 @Test testRequestAttributes()87 void testRequestAttributes() { 88 HttpServletRequest req = new DummyHttpServletRequest(); 89 PageConfig cfg = PageConfig.get(req); 90 91 String[] attrs = {"a", "b", "c", "d"}; 92 93 Object[] values = { 94 "some object", 95 new DummyHttpServletRequest(), 96 1, 97 this 98 }; 99 100 assertEquals(attrs.length, values.length); 101 102 for (int i = 0; i < attrs.length; i++) { 103 cfg.setRequestAttribute(attrs[i], values[i]); 104 105 Object attribute = req.getAttribute(attrs[i]); 106 assertNotNull(attribute); 107 assertEquals(values[i], attribute); 108 109 attribute = cfg.getRequestAttribute(attrs[i]); 110 assertNotNull(attribute); 111 assertEquals(values[i], attribute); 112 } 113 } 114 115 @Test 116 @EnabledForRepository(MERCURIAL) canProcessHistory()117 void canProcessHistory() { 118 // Expect no redirection (that is, empty string is returned) for a 119 // file that exists. 120 assertCanProcess("", "/source", "/history", "/mercurial/main.c"); 121 122 // Expect directories without trailing slash to get a trailing slash 123 // appended. 124 assertCanProcess("/source/history/mercurial/", "/source", "/history", "/mercurial"); 125 126 // Expect no redirection (that is, empty string is returned) if the 127 // directories already have a trailing slash. 128 assertCanProcess("", "/source", "/history", "/mercurial/"); 129 130 // Expect null if the file or directory doesn't exist. 131 assertCanProcess(null, "/source", "/history", "/mercurial/xyz"); 132 assertCanProcess(null, "/source", "/history", "/mercurial/xyz/"); 133 } 134 135 @Test canProcessXref()136 void canProcessXref() { 137 // Expect no redirection (that is, empty string is returned) for a 138 // file that exists. 139 assertCanProcess("", "/source", "/xref", "/mercurial/main.c"); 140 141 // Expect directories without trailing slash to get a trailing slash 142 // appended. 143 assertCanProcess("/source/xref/mercurial/", 144 "/source", "/xref", "/mercurial"); 145 146 // Expect no redirection (that is, empty string is returned) if the 147 // directories already have a trailing slash. 148 assertCanProcess("", "/source", "/xref", "/mercurial/"); 149 150 // Expect null if the file or directory doesn't exist. 151 assertCanProcess(null, "/source", "/xref", "/mercurial/xyz"); 152 assertCanProcess(null, "/source", "/xref", "/mercurial/xyz/"); 153 } 154 155 /** 156 * Testing the root of /xref for authorization filtering. 157 */ 158 @Test testGetResourceFileList()159 void testGetResourceFileList() { 160 RuntimeEnvironment env = RuntimeEnvironment.getInstance(); 161 162 // backup original values 163 String oldSourceRootPath = env.getSourceRootPath(); 164 AuthorizationFramework oldAuthorizationFramework = env.getAuthorizationFramework(); 165 Map<String, Project> oldProjects = env.getProjects(); 166 167 // Set up the source root directory containing some projects. 168 env.setSourceRoot(repository.getSourceRoot()); 169 env.setProjectsEnabled(true); 170 171 // Enable projects. 172 for (String file : new File(repository.getSourceRoot()).list()) { 173 Project proj = new Project(file); 174 proj.setIndexed(true); 175 env.getProjects().put(file, proj); 176 } 177 178 HttpServletRequest req = createRequest("/source", "/xref", ""); 179 PageConfig cfg = PageConfig.get(req); 180 List<String> allFiles = new ArrayList<>(cfg.getResourceFileList()); 181 182 /* 183 * Check if there are some files (the "5" here is just a sufficient 184 * value for now which won't break any future repository tests) without 185 * any authorization. 186 */ 187 assertTrue(allFiles.size() > 5); 188 assertTrue(allFiles.contains("git")); 189 assertTrue(allFiles.contains("mercurial")); 190 191 /* 192 * Now set up the same projects with authorization plugin enabling only 193 * some of them. 194 * <pre> 195 * - disabling "git" 196 * - disabling "mercurial" 197 * </pre> 198 */ 199 env.setAuthorizationFramework(new AuthorizationFramework()); 200 env.getAuthorizationFramework().reload(); 201 env.getAuthorizationFramework().getStack() 202 .add(new AuthorizationPlugin(AuthControlFlag.REQUIRED, new TestPlugin() { 203 @Override 204 public boolean isAllowed(HttpServletRequest request, Project project) { 205 return !project.getName().startsWith("git") 206 && !project.getName().startsWith("mercurial"); 207 } 208 })); 209 210 req = createRequest("/source", "/xref", ""); 211 cfg = PageConfig.get(req); 212 List<String> filteredFiles = new ArrayList<>(cfg.getResourceFileList()); 213 // list subtraction - retains only disabled files 214 allFiles.removeAll(filteredFiles); 215 216 assertEquals(2, allFiles.size()); 217 assertTrue(allFiles.contains("git")); 218 assertTrue(allFiles.contains("mercurial")); 219 220 // restore original values 221 env.setAuthorizationFramework(oldAuthorizationFramework); 222 env.setSourceRoot(oldSourceRootPath); 223 env.setProjects(oldProjects); 224 } 225 226 @SuppressWarnings("ResultOfMethodCallIgnored") 227 @EnabledOnOs({OS.LINUX, OS.MAC, OS.SOLARIS, OS.AIX, OS.OTHER}) 228 @Test testGetSortedFilesDirsFirst()229 void testGetSortedFilesDirsFirst() throws IOException { 230 RuntimeEnvironment env = RuntimeEnvironment.getInstance(); 231 env.setListDirsFirst(true); 232 // Cannot spy/mock final class. 233 HttpServletRequest req = createRequest("/source", "/xref", ""); 234 PageConfig pageConfig = PageConfig.get(req); 235 236 // Make sure the source root has just directories. 237 File sourceRootFile = new File(repository.getSourceRoot()); 238 assertTrue(Arrays.stream(sourceRootFile.listFiles()).filter(File::isFile). 239 collect(Collectors.toSet()).isEmpty()); 240 241 // Create regular file under source root. 242 File file = new File(sourceRootFile, "foo.txt"); 243 assertTrue(file.createNewFile()); 244 assertTrue(file.isFile()); 245 246 // Make sure the regular file is last. 247 List<String> entries = pageConfig.getSortedFiles(sourceRootFile.listFiles()); 248 assertNotNull(entries); 249 assertFalse(entries.isEmpty()); 250 int numEntries = entries.size(); 251 assertEquals("foo.txt", entries.get(entries.size() - 1)); 252 253 // Create symbolic link to non-existent target. 254 Path link = Path.of(sourceRootFile.getCanonicalPath(), "link"); 255 Path target = Paths.get("/nonexistent"); 256 Files.createSymbolicLink(link, target); 257 258 // Check the symlink was sorted as file. 259 entries = pageConfig.getSortedFiles(sourceRootFile.listFiles()); 260 assertNotNull(entries); 261 assertFalse(entries.isEmpty()); 262 assertEquals(numEntries + 1, entries.size()); 263 assertEquals("link", entries.get(entries.size() - 1)); 264 265 // Cleanup. 266 file.delete(); 267 link.toFile().delete(); 268 } 269 270 @Test testGetIntParam()271 void testGetIntParam() { 272 String[] attrs = {"a", "b", "c", "d", "e", "f", "g", "h"}; 273 int[] values = {1, 100, -1, 2, 200, 3000, -200, 3000}; 274 DummyHttpServletRequest req = new DummyHttpServletRequest() { 275 @Override 276 public String getParameter(String name) { 277 switch (name) { 278 case "a": 279 return "1"; 280 case "b": 281 return "100"; 282 case "c": 283 return null; 284 case "d": 285 return "2"; 286 case "e": 287 return "200"; 288 case "f": 289 return "3000"; 290 case "g": 291 return null; 292 case "h": 293 return "abcdef"; 294 } 295 return null; 296 } 297 }; 298 PageConfig cfg = PageConfig.get(req); 299 300 assertEquals(attrs.length, values.length); 301 for (int i = 0; i < attrs.length; i++) { 302 assertEquals(values[i], cfg.getIntParam(attrs[i], values[i])); 303 } 304 } 305 306 @Test testGetLatestRevisionValid()307 void testGetLatestRevisionValid() { 308 DummyHttpServletRequest req1 = new DummyHttpServletRequest() { 309 @Override 310 public String getPathInfo() { 311 return "/git/main.c"; 312 } 313 }; 314 315 PageConfig cfg = PageConfig.get(req1); 316 String rev = cfg.getLatestRevision(); 317 318 assertEquals("aa35c258", rev); 319 } 320 321 @Test testGetLatestRevisionViaIndex()322 void testGetLatestRevisionViaIndex() throws Exception { 323 // Run the indexer. 324 RuntimeEnvironment env = RuntimeEnvironment.getInstance(); 325 env.setSourceRoot(repository.getSourceRoot()); 326 env.setDataRoot(repository.getDataRoot()); 327 env.setProjectsEnabled(true); 328 env.setHistoryEnabled(true); 329 RepositoryFactory.initializeIgnoredNames(env); 330 331 Indexer indexer = Indexer.getInstance(); 332 indexer.prepareIndexer( 333 env, 334 true, // search for repositories 335 true, // scan and add projects 336 false, // don't create dictionary 337 null, // subFiles - needed when refreshing history partially 338 null); // repositories - needed when refreshing history partially 339 indexer.doIndexerExecution(true, null, null); 340 341 DummyHttpServletRequest req1 = new DummyHttpServletRequest() { 342 @Override 343 public String getPathInfo() { 344 return "/git/main.c"; 345 } 346 }; 347 348 PageConfig cfg = PageConfig.get(req1); 349 String rev = cfg.getLastRevFromIndex(); 350 assertNotNull(rev); 351 assertEquals("aa35c258", rev); 352 } 353 354 @Test testGetRevisionLocation()355 void testGetRevisionLocation() { 356 DummyHttpServletRequest req1 = new DummyHttpServletRequest() { 357 @Override 358 public String getPathInfo() { 359 return "/git/main.c"; 360 } 361 362 @Override 363 public String getContextPath() { 364 return "source"; 365 } 366 367 @Override 368 public String getQueryString() { 369 return "a=true"; 370 } 371 }; 372 373 PageConfig cfg = PageConfig.get(req1); 374 375 String location = cfg.getRevisionLocation(cfg.getLatestRevision()); 376 assertNotNull(location); 377 assertEquals("source/xref/git/main.c?r=aa35c258&a=true", location); 378 } 379 380 @Test testGetRevisionLocationNullQuery()381 void testGetRevisionLocationNullQuery() { 382 DummyHttpServletRequest req1 = new DummyHttpServletRequest() { 383 @Override 384 public String getPathInfo() { 385 return "/git/main.c"; 386 } 387 388 @Override 389 public String getContextPath() { 390 return "source"; 391 } 392 393 @Override 394 public String getQueryString() { 395 return null; 396 } 397 }; 398 399 PageConfig cfg = PageConfig.get(req1); 400 401 String location = cfg.getRevisionLocation(cfg.getLatestRevision()); 402 assertNotNull(location); 403 assertEquals("source/xref/git/main.c?r=aa35c258", location); 404 } 405 406 @Test testGetLatestRevisionNotValid()407 void testGetLatestRevisionNotValid() { 408 DummyHttpServletRequest req2 = new DummyHttpServletRequest() { 409 @Override 410 public String getPathInfo() { 411 return "/git/nonexistent_file"; 412 } 413 }; 414 415 PageConfig cfg = PageConfig.get(req2); 416 String rev = cfg.getLatestRevision(); 417 assertNull(rev); 418 } 419 420 @Test testGetRequestedRevision()421 void testGetRequestedRevision() { 422 final String[] revisions = {"6c5588de", "", "6c5588de", "6c5588de", "6c5588de"}; 423 for (int i = 0; i < revisions.length; i++) { 424 final int index = i; 425 DummyHttpServletRequest req = new DummyHttpServletRequest() { 426 @Override 427 public String getParameter(String name) { 428 if (name.equals("r")) { 429 return revisions[index]; 430 } 431 return null; 432 } 433 }; 434 435 PageConfig cfg = PageConfig.get(req); 436 String rev = cfg.getRequestedRevision(); 437 438 assertNotNull(rev); 439 assertEquals(revisions[i], rev); 440 assertFalse(rev.contains("r=")); 441 442 PageConfig.cleanup(req); 443 } 444 } 445 446 @Test testGetAnnotation()447 void testGetAnnotation() { 448 final String[] revisions = {"aa35c258", "bb74b7e8"}; 449 450 for (int i = 0; i < revisions.length; i++) { 451 final int index = i; 452 HttpServletRequest req = new DummyHttpServletRequest() { 453 @Override 454 public String getContextPath() { 455 return "/source"; 456 } 457 458 @Override 459 public String getServletPath() { 460 return "/history"; 461 } 462 463 @Override 464 public String getPathInfo() { 465 return "/git/main.c"; 466 } 467 468 @Override 469 public String getParameter(String name) { 470 switch (name) { 471 case "r": 472 return revisions[index]; 473 case "a": 474 return "true"; 475 } 476 return null; 477 } 478 }; 479 PageConfig cfg = PageConfig.get(req); 480 481 Annotation annotation = cfg.getAnnotation(); 482 assertNotNull(annotation); 483 assertEquals("main.c", annotation.getFilename()); 484 assertEquals(revisions.length - i, annotation.getFileVersionsCount()); 485 486 for (int j = 1; j <= annotation.size(); j++) { 487 String tmp = annotation.getRevision(j); 488 assertTrue(Arrays.asList(revisions).contains(tmp)); 489 } 490 491 assertEquals(revisions.length - i, annotation.getFileVersion(revisions[i]), 492 "The version should be reflected through the revision"); 493 494 PageConfig.cleanup(req); 495 } 496 } 497 498 /** 499 * Test the case when the source root is null. 500 */ 501 @Test testCheckSourceRootExistence1()502 void testCheckSourceRootExistence1() { 503 assertThrows(FileNotFoundException.class, () -> { 504 HttpServletRequest req = new DummyHttpServletRequest(); 505 PageConfig cfg = PageConfig.get(req); 506 String path = RuntimeEnvironment.getInstance().getSourceRootPath(); 507 System.out.println(path); 508 RuntimeEnvironment.getInstance().setSourceRoot(null); 509 try { 510 cfg.checkSourceRootExistence(); 511 } finally { 512 RuntimeEnvironment.getInstance().setSourceRoot(path); 513 PageConfig.cleanup(req); 514 } 515 }); 516 } 517 518 /** 519 * Test the case when source root is empty. 520 */ 521 @Test testCheckSourceRootExistence2()522 void testCheckSourceRootExistence2() { 523 assertThrows(FileNotFoundException.class, () -> { 524 HttpServletRequest req = new DummyHttpServletRequest(); 525 PageConfig cfg = PageConfig.get(req); 526 String path = RuntimeEnvironment.getInstance().getSourceRootPath(); 527 RuntimeEnvironment.getInstance().setSourceRoot(""); 528 try { 529 cfg.checkSourceRootExistence(); 530 } finally { 531 RuntimeEnvironment.getInstance().setSourceRoot(path); 532 PageConfig.cleanup(req); 533 } 534 }); 535 } 536 537 /** 538 * Test the case when source root does not exist. 539 * @throws IOException I/O exception 540 */ 541 @Test testCheckSourceRootExistence3()542 void testCheckSourceRootExistence3() throws IOException { 543 HttpServletRequest req = new DummyHttpServletRequest(); 544 PageConfig cfg = PageConfig.get(req); 545 String path = RuntimeEnvironment.getInstance().getSourceRootPath(); 546 File temp = File.createTempFile("opengrok", "-test-file.tmp"); 547 Files.delete(temp.toPath()); 548 RuntimeEnvironment.getInstance().setSourceRoot(temp.getAbsolutePath()); 549 assertThrows(IOException.class, () -> cfg.checkSourceRootExistence(), 550 "This should throw an exception when the file does not exist"); 551 RuntimeEnvironment.getInstance().setSourceRoot(path); 552 PageConfig.cleanup(req); 553 } 554 555 /** 556 * Test the case when source root can not be read. 557 * @throws IOException I/O exception 558 */ 559 @Test 560 @EnabledOnOs({OS.LINUX, OS.MAC, OS.SOLARIS, OS.AIX, OS.OTHER}) testCheckSourceRootExistence4()561 void testCheckSourceRootExistence4() throws IOException { 562 HttpServletRequest req = new DummyHttpServletRequest(); 563 PageConfig cfg = PageConfig.get(req); 564 String path = RuntimeEnvironment.getInstance().getSourceRootPath(); 565 File temp = File.createTempFile("opengrok", "-test-file.tmp"); 566 Files.delete(temp.toPath()); 567 Files.createDirectories(temp.toPath()); 568 // skip the test if the implementation does not permit setting permissions 569 assumeTrue(temp.setReadable(false)); 570 RuntimeEnvironment.getInstance().setSourceRoot(temp.getAbsolutePath()); 571 assertThrows(IOException.class, () -> cfg.checkSourceRootExistence(), 572 "This should throw an exception when the file is not readable"); 573 RuntimeEnvironment.getInstance().setSourceRoot(path); 574 575 PageConfig.cleanup(req); 576 temp.deleteOnExit(); 577 } 578 579 /** 580 * Test a successful check. 581 * @throws IOException I/O exception 582 */ 583 @Test testCheckSourceRootExistence5()584 void testCheckSourceRootExistence5() throws IOException { 585 HttpServletRequest req = new DummyHttpServletRequest(); 586 PageConfig cfg = PageConfig.get(req); 587 String path = RuntimeEnvironment.getInstance().getSourceRootPath(); 588 File temp = File.createTempFile("opengrok", "-test-file.tmp"); 589 temp.delete(); 590 temp.mkdirs(); 591 RuntimeEnvironment.getInstance().setSourceRoot(temp.getAbsolutePath()); 592 cfg.checkSourceRootExistence(); 593 RuntimeEnvironment.getInstance().setSourceRoot(path); 594 temp.deleteOnExit(); 595 PageConfig.cleanup(req); 596 } 597 598 /** 599 * Assert that {@code canProcess()} returns the expected value for the 600 * specified path. 601 * @param expected the expected return value 602 * @param context the context path 603 * @param servlet the servlet path 604 * @param pathInfo the path info 605 */ assertCanProcess(String expected, String context, String servlet, String pathInfo)606 private void assertCanProcess(String expected, String context, String servlet, String pathInfo) { 607 PageConfig config = PageConfig.get(createRequest(context, servlet, pathInfo)); 608 assertEquals(expected, config.canProcess()); 609 } 610 611 /** 612 * Create a request with the specified path elements. 613 * @param contextPath the context path 614 * @param servletPath the path of the servlet 615 * @param pathInfo the path info 616 * @return a servlet request for the specified path 617 */ createRequest( final String contextPath, final String servletPath, final String pathInfo)618 private static HttpServletRequest createRequest( 619 final String contextPath, final String servletPath, 620 final String pathInfo) { 621 return new DummyHttpServletRequest() { 622 @Override 623 public String getContextPath() { 624 return contextPath; 625 } 626 627 @Override 628 public String getServletPath() { 629 return servletPath; 630 } 631 632 @Override 633 public String getPathInfo() { 634 return pathInfo; 635 } 636 }; 637 } 638 } 639