xref: /OpenGrok/opengrok-web/src/test/java/org/opengrok/web/PageConfigTest.java (revision c4b39070085d99df7ea64962c445c3ac0db8b13e) !
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