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