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