View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import static java.nio.charset.StandardCharsets.ISO_8859_1;
18  
19  import java.io.File;
20  import java.net.UnknownHostException;
21  import java.text.DateFormat;
22  import java.text.SimpleDateFormat;
23  import java.util.HashMap;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.concurrent.TimeUnit;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  
32  import org.apache.commons.io.FileUtils;
33  import org.apache.commons.lang3.StringUtils;
34  import org.htmlunit.html.DomNode;
35  import org.htmlunit.html.DomNodeList;
36  import org.htmlunit.html.HtmlAnchor;
37  import org.htmlunit.html.HtmlPage;
38  import org.htmlunit.xml.XmlPage;
39  import org.junit.jupiter.api.Assertions;
40  import org.junit.jupiter.api.Test;
41  
42  /**
43   * Tests against external web sites, this should be done once every while.
44   *
45   * @author Ahmed Ashour
46   * @author Ronald Brill
47   */
48  public class ExternalTest {
49  
50      static String SONATYPE_SNAPSHOT_REPO_URL_ =
51                          "https://central.sonatype.com/repository/maven-snapshots/";
52      static String MAVEN_REPO_URL_ = "https://repo1.maven.org/maven2/";
53  
54      /** Chrome driver. */
55      static String CHROME_DRIVER_ = "140.0.7339";
56      static String CHROME_DRIVER_URL_ =
57              "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json";
58  
59      static String EDGE_DRIVER_ = "140.0.3485";
60      static String EDGE_DRIVER_URL_ = "https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/";
61  
62      /** Gecko driver. */
63      static String GECKO_DRIVER_ = "0.36.0";
64      static String GECKO_DRIVER_URL_ = "https://github.com/mozilla/geckodriver/releases/latest";
65  
66      /**
67       * Tests the current environment matches the expected setup.
68       * @throws Exception if an error occurs
69       */
70      @Test
71      public void testEnvironment() throws Exception {
72          Assertions.assertEquals("en_US", Locale.getDefault().toString());
73      }
74  
75      /**
76       * Tests that POM dependencies are the latest.
77       *
78       * Currently it is configured to check every week.
79       *
80       * @throws Exception if an error occurs
81       */
82      @Test
83      public void pom() throws Exception {
84          final Map<String, String> properties = new HashMap<>();
85          final List<String> lines = FileUtils.readLines(new File("pom.xml"), ISO_8859_1);
86  
87          final List<String> wrongVersions = new LinkedList<>();
88          boolean inComment = false;
89          for (int i = 0; i < lines.size(); i++) {
90              String line = lines.get(i);
91  
92              String cleaned = "";
93              if (!inComment) {
94                  final int startIdx = line.indexOf("<!--");
95                  if (startIdx != -1) {
96                      cleaned += line.substring(0, startIdx);
97                      inComment = true;
98                  }
99              }
100             if (inComment) {
101                 final int endIdx = line.indexOf("-->");
102                 if (endIdx != -1) {
103                     cleaned += line.substring(endIdx + 3, line.length());
104                     inComment = false;
105                 }
106                 line = cleaned;
107             }
108 
109             if (line.trim().equals("<properties>")) {
110                 processProperties(lines, i + 1, properties);
111             }
112             if (line.contains("artifactId")
113                     && !line.contains(">htmlunit<")
114                     && !line.contains(">selenium-devtools-v")) {
115                 final String artifactId = getValue(line);
116                 final String groupId = getValue(lines.get(i - 1));
117                 if (!lines.get(i + 1).contains("</exclusion>")) {
118                     String version = getValue(lines.get(i + 1));
119                     if (version.startsWith("${")) {
120                         version = properties.get(version.substring(2, version.length() - 1));
121                     }
122                     try {
123                         assertVersion(groupId, artifactId, version);
124                     }
125                     catch (final AssertionError e) {
126                         wrongVersions.add(e.getMessage());
127                     }
128                 }
129             }
130         }
131 
132         if (wrongVersions.size() > 0) {
133             Assertions.fail(String.join("\n ", wrongVersions));
134         }
135 
136         assertVersion("org.sonatype.oss", "oss-parent", "9");
137     }
138 
139     private static void processProperties(final List<String> lines, int i, final Map<String, String> map) {
140         for ( ; i < lines.size(); i++) {
141             final String line = lines.get(i).trim();
142             if (StringUtils.isNotBlank(line) && !line.startsWith("<!--")) {
143                 if ("</properties>".equals(line)) {
144                     break;
145                 }
146                 final String name = line.substring(line.indexOf('<') + 1, line.indexOf('>'));
147                 map.put(name, getValue(line));
148             }
149         }
150     }
151 
152     /**
153      * Tests that we use the latest chrome driver.
154      * @throws Exception if an error occurs
155      */
156     @Test
157     public void assertChromeDriver() throws Exception {
158         try (WebClient webClient = new WebClient(BrowserVersion.FIREFOX, false, null, -1)) {
159             final Page page = webClient.getPage(CHROME_DRIVER_URL_);
160             final String content = page.getWebResponse().getContentAsString();
161 
162             String version = "0.0.0.0";
163             final Pattern regex =
164                     Pattern.compile("\"channels\":\\{\"Stable\":\\{.*?\"version\":\"(\\d*\\.\\d*\\.\\d*)\\.\\d*\"");
165             final Matcher matcher = regex.matcher(content);
166             while (matcher.find()) {
167                 if (version.compareTo(matcher.group(1)) < 0) {
168                     version = matcher.group(1);
169                     break;
170                 }
171             }
172             Assertions.assertEquals(version, CHROME_DRIVER_);
173         }
174     }
175 
176     /**
177      * Tests that we use the latest edge driver.
178      * @throws Exception if an error occurs
179      */
180     @Test
181     // @Disabled("javascript errors")
182     public void assertEdgeDriver() throws Exception {
183         try (WebClient webClient = new WebClient(BrowserVersion.FIREFOX)) {
184             webClient.getOptions().setThrowExceptionOnScriptError(false);
185 
186             final HtmlPage page = webClient.getPage(EDGE_DRIVER_URL_);
187             String content = page.asNormalizedText();
188             content = content.substring(content.indexOf("Current general public release channel."));
189             content = content.replace("\r\n", "");
190 
191             String version = "0.0.0.0";
192             final Pattern regex =
193                     Pattern.compile("Version ("
194                                 + BrowserVersion.EDGE.getBrowserVersionNumeric()
195                                 + "\\.\\d*\\.\\d*)\\.\\d*\\s");
196             final Matcher matcher = regex.matcher(content);
197             while (matcher.find()) {
198                 if (version.compareTo(matcher.group(1)) < 0) {
199                     version = matcher.group(1);
200                     break;
201                 }
202             }
203             Assertions.assertEquals(version, EDGE_DRIVER_);
204         }
205     }
206 
207     /**
208      * Tests that we use the latest gecko driver.
209      * @throws Exception if an error occurs
210      */
211     @Test
212     public void assertGeckoDriver() throws Exception {
213         try (WebClient webClient = new WebClient(BrowserVersion.FIREFOX, false, null, -1)) {
214             try {
215                 final HtmlPage page = webClient.getPage(GECKO_DRIVER_URL_);
216                 final DomNodeList<DomNode> divs = page.querySelectorAll("li.breadcrumb-item-selected");
217                 Assertions.assertEquals(divs.get(0).asNormalizedText(), "v" + GECKO_DRIVER_);
218             }
219             catch (final FailingHttpStatusCodeException e) {
220                 // ignore
221             }
222         }
223     }
224 
225     /**
226      * Tests that the deployed snapshot is not more than two weeks old.
227      *
228      * Currently it is configured to check every week.
229      *
230      * @throws Exception if an error occurs
231      */
232     @Test
233     public void snapshot() throws Exception {
234         final List<String> lines = FileUtils.readLines(new File("pom.xml"), ISO_8859_1);
235         String version = null;
236         for (int i = 0; i < lines.size(); i++) {
237             if ("<artifactId>htmlunit</artifactId>".equals(lines.get(i).trim())) {
238                 version = getValue(lines.get(i + 1));
239                 break;
240             }
241         }
242         Assertions.assertNotNull(version);
243         if (version.contains("SNAPSHOT")) {
244             try (WebClient webClient = new WebClient(BrowserVersion.FIREFOX, false, null, -1)) {
245                 final String url = SONATYPE_SNAPSHOT_REPO_URL_
246                         + "org/htmlunit/htmlunit/" + version + "/maven-metadata.xml";
247 
248                 final XmlPage page = webClient.getPage(url);
249                 final String timestamp = page.getElementsByTagName("timestamp").get(0).getTextContent();
250                 final DateFormat format = new SimpleDateFormat("yyyyMMdd.HHmmss", Locale.ROOT);
251                 final long snapshotMillis = format.parse(timestamp).getTime();
252                 final long nowMillis = System.currentTimeMillis();
253                 final long days = TimeUnit.MILLISECONDS.toDays(nowMillis - snapshotMillis);
254                 Assertions.assertTrue(days < 14, "Snapshot not deployed for " + days + " days");
255             }
256         }
257     }
258 
259     private static void assertVersion(final String groupId, final String artifactId, final String pomVersion)
260             throws Exception {
261         String latestMavenCentralVersion = null;
262         String url = MAVEN_REPO_URL_
263                         + groupId.replace('.', '/') + '/'
264                         + artifactId.replace('.', '/');
265         if (!url.endsWith("/")) {
266             url += "/";
267         }
268         try (WebClient webClient = new WebClient(BrowserVersion.FIREFOX, false, null, -1)) {
269             webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
270 
271             try {
272                 final HtmlPage page = webClient.getPage(url);
273                 for (final HtmlAnchor anchor : page.getAnchors()) {
274                     String mavenCentralVersion = anchor.getTextContent();
275                     mavenCentralVersion = mavenCentralVersion.substring(0, mavenCentralVersion.length() - 1);
276                     if (!isIgnored(groupId, artifactId, mavenCentralVersion)) {
277                         if (isVersionAfter(mavenCentralVersion, latestMavenCentralVersion)) {
278                             latestMavenCentralVersion = mavenCentralVersion;
279                         }
280                     }
281                 }
282             }
283             catch (final UnknownHostException e) {
284                 // ignore because our ci machine sometimes fails
285             }
286         }
287         if (!pomVersion.endsWith("-SNAPSHOT")
288                 || !isVersionAfter(
289                         pomVersion.substring(0, pomVersion.length() - "-SNAPSHOT".length()),
290                         latestMavenCentralVersion)) {
291 
292             // it is ok if the pom uses a more recent version
293             if (!isVersionAfter(pomVersion, latestMavenCentralVersion)) {
294                 Assertions.assertEquals(latestMavenCentralVersion, pomVersion, groupId + ":" + artifactId);
295             }
296         }
297     }
298 
299     private static boolean isVersionAfter(final String pomVersion, final String centralVersion) {
300         if (centralVersion == null) {
301             return true;
302         }
303         final String[] pomValues = pomVersion.split("\\.");
304         final String[] centralValues = centralVersion.split("\\.");
305         for (int i = 0; i < pomValues.length; i++) {
306             if (pomValues[i].startsWith("v")) {
307                 pomValues[i] = pomValues[i].substring(1);
308             }
309             try {
310                 Integer.parseInt(pomValues[i]);
311             }
312             catch (final NumberFormatException e) {
313                 return false;
314             }
315         }
316         for (int i = 0; i < centralValues.length; i++) {
317             if (centralValues[i].startsWith("v")) {
318                 centralValues[i] = centralValues[i].substring(1);
319             }
320             try {
321                 Integer.parseInt(centralValues[i]);
322             }
323             catch (final NumberFormatException e) {
324                 return true;
325             }
326         }
327         for (int i = 0; i < pomValues.length; i++) {
328             if (i == centralValues.length) {
329                 return true;
330             }
331             final int pomValuePart = Integer.parseInt(pomValues[i]);
332             final int centralValuePart = Integer.parseInt(centralValues[i]);
333             if (pomValuePart < centralValuePart) {
334                 return false;
335             }
336             if (pomValuePart > centralValuePart) {
337                 return true;
338             }
339         }
340         return false;
341     }
342 
343     private static boolean isIgnored(@SuppressWarnings("unused") final String groupId,
344             @SuppressWarnings("unused") final String artifactId, @SuppressWarnings("unused") final String version) {
345         if (groupId.startsWith("org.eclipse.jetty")
346                 && (version.startsWith("11.") || version.startsWith("10."))) {
347             return true;
348         }
349 
350         // version > 3.12.0 does not work with our site.xml and also not with a refactored one
351         if ("maven-site-plugin".equals(artifactId)
352                 && (version.startsWith("3.12.1") || version.startsWith("3.20.") || version.startsWith("3.21."))) {
353             return true;
354         }
355 
356         // >= 11.x requires java11
357         if ("org.owasp".equals(groupId)
358                 && (version.startsWith("11.") || version.startsWith("12."))) {
359             return true;
360         }
361 
362         // 6.x requires java11
363         if ("org.apache.felix".equals(groupId)
364                 && version.startsWith("6.")) {
365             return true;
366         }
367 
368         // 6.x requires java17
369         if ("org.junit.jupiter".equals(groupId)
370                 && version.startsWith("6.")) {
371             return true;
372         }
373         if ("org.junit.platform".equals(groupId)
374                 && version.startsWith("6.")) {
375             return true;
376         }
377 
378         // really old common versions
379         if ("commons-io".equals(artifactId) && (version.startsWith("2003"))) {
380             return true;
381         }
382         if ("commons-net".equals(artifactId) && (version.startsWith("2003"))) {
383             return true;
384         }
385 
386         return false;
387     }
388 
389     private static String getValue(final String line) {
390         return line.substring(line.indexOf('>') + 1, line.lastIndexOf('<'));
391     }
392 }