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