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