001/* 002 * The contents of this file are subject to the license and copyright 003 * detailed in the LICENSE and NOTICE files at the root of the source 004 * tree. 005 */ 006package org.fcrepo.integration; 007 008import static com.gargoylesoftware.htmlunit.BrowserVersion.FIREFOX; 009import static com.google.common.collect.Lists.transform; 010import static java.util.UUID.randomUUID; 011import static javax.ws.rs.core.HttpHeaders.ACCEPT; 012import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE; 013import static javax.ws.rs.core.Response.Status.NOT_FOUND; 014import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA; 015import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE; 016import static org.junit.Assert.assertEquals; 017import static org.junit.Assert.assertNotEquals; 018import static org.junit.Assert.assertNotNull; 019import static org.junit.Assert.assertTrue; 020import static org.junit.Assert.fail; 021 022import java.io.IOException; 023import java.nio.charset.StandardCharsets; 024import java.util.List; 025import java.util.concurrent.TimeUnit; 026 027import org.apache.http.HttpResponse; 028import org.apache.http.auth.AuthScope; 029import org.apache.http.client.CredentialsProvider; 030import org.apache.http.client.methods.HttpPatch; 031import org.apache.http.entity.StringEntity; 032import org.junit.After; 033import org.junit.Before; 034import org.junit.Test; 035 036import com.gargoylesoftware.htmlunit.DefaultCredentialsProvider; 037import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; 038import com.gargoylesoftware.htmlunit.IncorrectnessListener; 039import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController; 040import com.gargoylesoftware.htmlunit.Page; 041import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; 042import com.gargoylesoftware.htmlunit.WebClient; 043import com.gargoylesoftware.htmlunit.html.DomAttr; 044import com.gargoylesoftware.htmlunit.html.DomElement; 045import com.gargoylesoftware.htmlunit.html.DomText; 046import com.gargoylesoftware.htmlunit.html.HtmlButton; 047import com.gargoylesoftware.htmlunit.html.HtmlElement; 048import com.gargoylesoftware.htmlunit.html.HtmlFileInput; 049import com.gargoylesoftware.htmlunit.html.HtmlForm; 050import com.gargoylesoftware.htmlunit.html.HtmlInput; 051import com.gargoylesoftware.htmlunit.html.HtmlPage; 052import com.gargoylesoftware.htmlunit.html.HtmlSelect; 053import com.gargoylesoftware.htmlunit.html.HtmlTextArea; 054 055/** 056 * <p>FedoraHtmlResponsesIT class.</p> 057 * 058 * @author cbeer 059 */ 060public class FedoraHtmlResponsesIT extends AbstractResourceIT { 061 062 private WebClient webClient; 063 private WebClient javascriptlessWebClient; 064 065 @Before 066 public void setUp() { 067 webClient = getDefaultWebClient(); 068 069 javascriptlessWebClient = getDefaultWebClient(); 070 javascriptlessWebClient.getOptions().setJavaScriptEnabled(false); 071 } 072 073 @After 074 public void cleanUp() { 075 webClient.close(); 076 javascriptlessWebClient.close(); 077 } 078 079 @Test 080 public void testDescribeHtml() throws IOException { 081 final HtmlPage page = webClient.getPage(serverAddress); 082 ((HtmlElement)page.getFirstByXPath("//h4[text()='Properties']")).click(); 083 084 checkForHeaderBranding(page); 085 final String namespaceLabel = page 086 .getFirstByXPath("//span[@title='" + REPOSITORY_NAMESPACE + "']/text()") 087 .toString(); 088 089 assertEquals("Expected to find namespace URIs displayed as their prefixes", "fedora:", 090 namespaceLabel); 091 } 092 093 @Test 094 public void testCreateNewNodeWithProvidedId() throws IOException { 095 createAndVerifyObjectWithIdFromRootPage(newPid()); 096 } 097 098 private String newPid() { 099 return randomUUID().toString(); 100 } 101 102 private HtmlPage createAndVerifyObjectWithIdFromRootPage(final String pid) throws IOException { 103 return createAndVerifyObjectWithIdFromRootPage(pid, "basic container"); 104 } 105 106 private HtmlPage createAndVerifyObjectWithIdFromRootPage(final String pid, final String containerType) 107 throws IOException { 108 final HtmlPage page = webClient.getPage(serverAddress); 109 final HtmlForm form = (HtmlForm)page.getElementById("action_create"); 110 final HtmlSelect type = (HtmlSelect)page.getElementById("new_mixin"); 111 type.getOptionByValue(containerType).setSelected(true); 112 113 final HtmlInput new_id = (HtmlInput)page.getElementById("new_id"); 114 new_id.setValueAttribute(pid); 115 final HtmlButton button = form.getFirstByXPath("button"); 116 button.click(); 117 118 119 try { 120 final HtmlPage page1 = webClient.getPage(serverAddress + pid); 121 assertEquals("Page had wrong title!", serverAddress + pid, page1.getTitleText()); 122 return page1; 123 } catch (final FailingHttpStatusCodeException e) { 124 fail("Did not successfully retrieve created page! Got HTTP code: " + e.getStatusCode()); 125 return null; 126 } 127 } 128 129 @Test 130 public void testCreateNewNodeWithGeneratedId() throws IOException { 131 132 final HtmlPage page = webClient.getPage(serverAddress); 133 final HtmlForm form = (HtmlForm)page.getElementById("action_create"); 134 final HtmlSelect type = (HtmlSelect)page.getElementById("new_mixin"); 135 type.getOptionByValue("basic container").setSelected(true); 136 final HtmlButton button = form.getFirstByXPath("button"); 137 button.click(); 138 139 final HtmlPage page1 = javascriptlessWebClient.getPage(serverAddress); 140 assertTrue("Didn't see new information in page!", !page1.asText().equals(page.asText())); 141 } 142 143 @Test 144 public void testCreateNewBasicContainer() throws IOException { 145 final HtmlPage newPage = createAndVerifyObjectWithIdFromRootPage(newPid(), "basic container"); 146 assertTrue("Set container type to ldp:BasicContainer", newPage.asText().contains( 147 "http://www.w3.org/ns/ldp#BasicContainer")); 148 } 149 150 @Test 151 public void testCreateNewDirectContainer() throws IOException { 152 final HtmlPage newPage = createAndVerifyObjectWithIdFromRootPage(newPid(), "direct container"); 153 assertTrue("Set container type to ldp:DirectContainer", newPage.asText().contains( 154 "http://www.w3.org/ns/ldp#DirectContainer")); 155 } 156 157 @Test 158 public void testCreateNewIndirectContainer() throws IOException { 159 final HtmlPage newPage = createAndVerifyObjectWithIdFromRootPage(newPid(), "indirect container"); 160 assertTrue("Set container type to ldp:IndirectContainer", newPage.asText().contains( 161 "http://www.w3.org/ns/ldp#IndirectContainer")); 162 } 163 164 @Test 165 public void testCreateNewDatastream() throws Exception { 166 167 // can't do this with javascript, because HTMLUnit doesn't speak the HTML5 file api 168 final HtmlPage page = javascriptlessWebClient.getPage(serverAddress); 169 170 final HtmlSelect type = (HtmlSelect)page.getElementById("new_mixin"); 171 type.getOptionByValue("binary").setSelected(true); 172 173 final HtmlFileInput fileInput = (HtmlFileInput)page.getElementById("binary_payload"); 174 fileInput.setData("abcdef".getBytes()); 175 fileInput.setContentType("application/pdf"); 176 177 final HtmlButton button = (HtmlButton)page.getElementById("btn_action_create"); 178 button.click(); 179 180 // Without Javascript you end up at a blank page with just the newly generated URI as text. 181 final Page resultPage = javascriptlessWebClient.getCurrentWindow().getEnclosedPage(); 182 final String newUri = resultPage.getWebResponse().getContentAsString(); 183 184 final Page page1 = javascriptlessWebClient.getPage(newUri + "/" + FCR_METADATA); 185 assertTrue(page1.isHtmlPage()); 186 assertEquals(newUri, ((HtmlPage)page1).getTitleText()); 187 } 188 189 @Test 190 public void testCreateNewDatastreamWithNoFileAttached() throws IOException { 191 192 final String pid = newPid(); 193 194 // can't do this with javascript, because HTMLUnit doesn't speak the HTML5 file api 195 final HtmlPage page = webClient.getPage(serverAddress); 196 final HtmlForm form = (HtmlForm)page.getElementById("action_create"); 197 198 final HtmlInput slug = form.getInputByName("slug"); 199 slug.setValueAttribute(pid); 200 201 final HtmlSelect type = (HtmlSelect)page.getElementById("new_mixin"); 202 type.getOptionByValue("binary").setSelected(true); 203 204 final HtmlButton button = form.getFirstByXPath("button"); 205 button.click(); 206 207 javascriptlessWebClient.getOptions().setThrowExceptionOnFailingStatusCode(false); 208 final int status = javascriptlessWebClient.getPage(serverAddress + pid).getWebResponse().getStatusCode(); 209 assertEquals(NOT_FOUND.getStatusCode(), status); 210 } 211 212 @Test 213 public void testCreateNewObjectAndDeleteIt() throws IOException { 214 final boolean throwExceptionOnFailingStatusCode = webClient.getOptions().isThrowExceptionOnFailingStatusCode(); 215 webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); 216 217 final String pid = createNewObject(); 218 final HtmlPage page = webClient.getPage(serverAddress + pid); 219 final HtmlForm action_delete = page.getFormByName("action_delete"); 220 action_delete.getButtonByName("delete-button").click(); 221 webClient.waitForBackgroundJavaScript(1000); 222 webClient.waitForBackgroundJavaScriptStartingBefore(10000); 223 224 final Page page2 = webClient.getPage(serverAddress + pid); 225 assertEquals("Didn't get a 410!", 410, page2.getWebResponse() 226 .getStatusCode()); 227 228 webClient.getOptions().setThrowExceptionOnFailingStatusCode(throwExceptionOnFailingStatusCode); 229 } 230 231 @Test 232 public void testVersionsListWorksWhenNoVersionsPresent() throws IOException { 233 final boolean throwExceptionOnFailingStatusCode = webClient.getOptions().isThrowExceptionOnFailingStatusCode(); 234 webClient.getOptions().setThrowExceptionOnFailingStatusCode(true); 235 236 final String pid = createNewObject(); 237 final HtmlPage page = webClient.getPage(serverAddress + pid); 238 final DomElement viewVersions = page.getElementById("view_versions"); 239 final Page versionsPage = viewVersions.click(); 240 assertEquals("Didn't get a 200!", 200, versionsPage.getWebResponse() 241 .getStatusCode()); 242 webClient.getOptions().setThrowExceptionOnFailingStatusCode(throwExceptionOnFailingStatusCode); 243 } 244 245 /** 246 * This test walks through the steps for creating an object, setting some 247 * metadata, creating a version, updating that metadata, viewing the 248 * version history to find that old version. 249 * 250 * @throws IOException exception thrown during this function 251 */ 252 @Test 253 public void testVersionCreationAndNavigation() throws Exception { 254 final String pid = newPid(); 255 createAndVerifyObjectWithIdFromRootPage(pid); 256 257 TimeUnit.SECONDS.sleep(1); 258 259 final String updateSparql = "PREFIX dc: <http://purl.org/dc/elements/1.1/>\n" + 260 "PREFIX fedora: <" + REPOSITORY_NAMESPACE + ">\n" + 261 "\n" + 262 "INSERT DATA { <> dc:title \"Object Title\". }"; 263 postSparqlUpdateUsingHttpClient(updateSparql, pid); 264 265 final HtmlPage objectPage = javascriptlessWebClient.getPage(serverAddress + pid); 266 assertEquals("Title should be set.", "Object Title", 267 objectPage.getFirstByXPath("//span[@property='http://purl.org/dc/elements/1.1/title']/text()") 268 .toString()); 269 270 TimeUnit.SECONDS.sleep(1); 271 final String updateSparql2 = "PREFIX dc: <http://purl.org/dc/elements/1.1/>\n" + 272 "\n" + 273 "DELETE { <> dc:title ?t }\n" + 274 "INSERT { <> dc:title \"Updated Title\" }" + 275 "WHERE { <> dc:title ?t }"; 276 postSparqlUpdateUsingHttpClient(updateSparql2, pid); 277 278 final HtmlPage versions = 279 javascriptlessWebClient.getPage(serverAddress + pid + "/fcr:versions"); 280 final List<DomAttr> versionLinks = 281 castList(versions.getByXPath("//a[@class='version_link']/@href")); 282 assertEquals("There should be three versions.", 3, versionLinks.size()); 283 284 // get the labels 285 // will look like "Version from 2013-00-0T00:00:00.000Z" 286 // and will sort chronologically based on a String comparison 287 final List<DomText> labels = 288 castList(versions 289 .getByXPath("//a[@class='version_link']/text()")); 290 final boolean chronological = labels.get(0).asText().compareTo(labels.get(1).toString()) < 0; 291 logger.debug("Versions {} in chronological order: {}, {}", 292 chronological ? "are" : "are not", labels.get(0).asText(), labels.get(1).asText()); 293 294 final HtmlPage firstRevision = 295 javascriptlessWebClient.getPage(versionLinks.get(chronological ? 1 : 2) 296 .getNodeValue()); 297 final List<DomText> v1Titles = 298 castList(firstRevision 299 .getByXPath("//span[@property='http://purl.org/dc/elements/1.1/title']/text()")); 300 final HtmlPage secondRevision = 301 javascriptlessWebClient.getPage(versionLinks.get(chronological ? 2 : 1) 302 .getNodeValue()); 303 final List<DomText> v2Titles = 304 castList(secondRevision 305 .getByXPath("//span[@property='http://purl.org/dc/elements/1.1/title']/text()")); 306 307 assertEquals("Version one should have one title.", 1, v1Titles.size()); 308 assertEquals("Version two should have one title.", 1, v2Titles.size()); 309 assertNotEquals("Each version should have a different title.", v1Titles.get(0), v2Titles.get(0)); 310 assertEquals("First version should be preserved.", "Object Title", v1Titles.get(0).getWholeText()); 311 assertEquals("Second version should be preserved.", "Updated Title", v2Titles.get(0).getWholeText()); 312 } 313 314 private static void postSparqlUpdateUsingHttpClient(final String sparql, final String pid) throws IOException { 315 final HttpPatch method = new HttpPatch(serverAddress + pid); 316 method.addHeader(CONTENT_TYPE, "application/sparql-update"); 317 final StringEntity entity = new StringEntity(sparql, StandardCharsets.UTF_8); 318 method.setEntity(entity); 319 final HttpResponse response = client.execute(method); 320 assertEquals("Expected successful response.", 204, 321 response.getStatusLine().getStatusCode()); 322 } 323 324 @Test 325 public void testCreateNewObjectAndSetProperties() throws Exception { 326 final String pid = createNewObject(); 327 328 final HtmlPage page = webClient.getPage(serverAddress + pid); 329 final HtmlForm form = (HtmlForm)page.getElementById("action_sparql_update"); 330 final HtmlTextArea sparql_update_query = (HtmlTextArea)page.getElementById("sparql_update_query"); 331 sparql_update_query.setText("INSERT { <> <info:some-predicate> 'asdf' } WHERE { }"); 332 333 final HtmlButton button = form.getFirstByXPath("button"); 334 button.click(); 335 336 final HtmlPage page1 = webClient.getPage(serverAddress + pid); 337 assertTrue(page1.getElementById("metadata").asText().contains("some-predicate")); 338 } 339 340 @Test 341 public void testSimpleSearch() throws Exception { 342 final HtmlPage page = webClient.getPage(serverAddress); 343 page.getAnchorByText("Search").click(); 344 345 final HtmlPage searchPage = (HtmlPage)webClient.getCurrentWindow().getEnclosedPage(); 346 347 final HtmlForm form = (HtmlForm)searchPage.getElementById("action_search"); 348 final HtmlInput q = (HtmlInput)searchPage.getElementById("search_value_1"); 349 q.setValueAttribute("info:fedora/*"); 350 final HtmlButton button = form.getFirstByXPath("button"); 351 button.click(); 352 } 353 354 355 private static void checkForHeaderBranding(final HtmlPage page) { 356 assertNotNull( 357 page.getFirstByXPath("//nav[@role='navigation']/div[@class='navbar-header']/a[@class='navbar-brand']")); 358 } 359 360 private String createNewObject() throws IOException { 361 362 final String pid = newPid(); 363 364 final HtmlPage page = webClient.getPage(serverAddress); 365 final HtmlForm form = page.getFormByName("action_create"); 366 367 final HtmlInput slug = form.getInputByName("slug"); 368 slug.setValueAttribute(pid); 369 370 final HtmlButton button = form.getFirstByXPath("button"); 371 button.click(); 372 373 webClient.waitForBackgroundJavaScript(1000); 374 webClient.waitForBackgroundJavaScriptStartingBefore(10000); 375 return pid; 376 } 377 378 private CredentialsProvider getFedoraAdminCredentials() { 379 final CredentialsProvider credentials = new DefaultCredentialsProvider(); 380 credentials.setCredentials(AuthScope.ANY, FEDORA_ADMIN_CREDENTIALS); 381 return credentials; 382 } 383 384 private WebClient getDefaultWebClient() { 385 386 final WebClient webClient = new WebClient(FIREFOX); 387 webClient.addRequestHeader(ACCEPT, "text/html"); 388 webClient.setCredentialsProvider(getFedoraAdminCredentials()); 389 webClient.waitForBackgroundJavaScript(1000); 390 webClient.waitForBackgroundJavaScriptStartingBefore(10000); 391 webClient.setAjaxController(new NicelyResynchronizingAjaxController()); 392 //Suppress warning from IncorrectnessListener 393 webClient.setIncorrectnessListener(new SuppressWarningIncorrectnessListener()); 394 //Suppress css warning with the silent error handler. 395 webClient.setCssErrorHandler(new SilentCssErrorHandler()); 396 return webClient; 397 398 } 399 400 @SuppressWarnings("unchecked") 401 private static <T> List<T> castList(final List<?> l) { 402 return transform(l, x -> (T) x); 403 } 404 405 private static class SuppressWarningIncorrectnessListener 406 implements IncorrectnessListener { 407 @Override 408 public void notify(final String arg0, final Object arg1) { 409 410 } 411 } 412 413}