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.http.api;
019
020import com.google.common.base.Charsets;
021import com.google.common.collect.ImmutableMap;
022import com.google.common.collect.ImmutableSet;
023import org.apache.commons.io.IOUtils;
024import org.apache.jena.graph.Triple;
025import org.apache.jena.rdf.model.Model;
026import org.apache.jena.rdf.model.RDFNode;
027
028import org.fcrepo.common.db.DbTransactionExecutor;
029import org.fcrepo.config.FedoraPropsConfig;
030import org.fcrepo.http.api.services.EtagService;
031import org.fcrepo.http.api.services.HttpRdfService;
032import org.fcrepo.http.commons.api.rdf.HttpIdentifierConverter;
033import org.fcrepo.http.commons.domain.MultiPrefer;
034import org.fcrepo.http.commons.domain.PreferTag;
035import org.fcrepo.http.commons.responses.RdfNamespacedStream;
036import org.fcrepo.kernel.api.Transaction;
037import org.fcrepo.kernel.api.exception.CannotCreateResourceException;
038import org.fcrepo.kernel.api.exception.ExternalMessageBodyException;
039import org.fcrepo.kernel.api.exception.InsufficientStorageException;
040import org.fcrepo.kernel.api.exception.InvalidChecksumException;
041import org.fcrepo.kernel.api.exception.MalformedRdfException;
042import org.fcrepo.kernel.api.exception.PathNotFoundException;
043import org.fcrepo.kernel.api.exception.PreconditionException;
044import org.fcrepo.kernel.api.exception.UnsupportedAlgorithmException;
045import org.fcrepo.kernel.api.exception.UnsupportedMediaTypeException;
046import org.fcrepo.kernel.api.identifiers.FedoraId;
047import org.fcrepo.kernel.api.models.Binary;
048import org.fcrepo.kernel.api.models.Container;
049import org.fcrepo.kernel.api.models.FedoraResource;
050import org.fcrepo.kernel.api.models.NonRdfSourceDescription;
051import org.fcrepo.kernel.api.models.ResourceFactory;
052import org.fcrepo.kernel.api.models.ResourceHelper;
053import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
054import org.fcrepo.kernel.api.rdf.LdpTriplePreferences;
055import org.fcrepo.kernel.api.rdf.RdfNamespaceRegistry;
056import org.fcrepo.kernel.api.services.CreateResourceService;
057import org.fcrepo.kernel.api.services.DeleteResourceService;
058import org.fcrepo.kernel.api.services.ReplacePropertiesService;
059import org.fcrepo.kernel.api.services.ResourceTripleService;
060import org.fcrepo.kernel.api.services.UpdatePropertiesService;
061import org.junit.Before;
062import org.junit.Ignore;
063import org.junit.Test;
064import org.junit.runner.RunWith;
065import org.mockito.Mock;
066import org.mockito.junit.MockitoJUnitRunner;
067import org.mockito.stubbing.Answer;
068import org.slf4j.Logger;
069import org.springframework.mock.web.MockHttpServletResponse;
070
071import javax.servlet.ServletContext;
072import javax.servlet.http.HttpServletResponse;
073import javax.ws.rs.BadRequestException;
074import javax.ws.rs.ClientErrorException;
075import javax.ws.rs.core.EntityTag;
076import javax.ws.rs.core.HttpHeaders;
077import javax.ws.rs.core.Link;
078import javax.ws.rs.core.MediaType;
079import javax.ws.rs.core.Request;
080import javax.ws.rs.core.Response;
081import javax.ws.rs.core.SecurityContext;
082import java.io.IOException;
083import java.io.InputStream;
084import java.net.URI;
085import java.security.Principal;
086import java.util.ArrayList;
087import java.util.Arrays;
088import java.util.Date;
089import java.util.HashMap;
090import java.util.HashSet;
091import java.util.List;
092import java.util.Set;
093import java.util.stream.Stream;
094
095import static com.google.common.base.Predicates.containsPattern;
096import static java.nio.charset.StandardCharsets.UTF_8;
097import static java.util.Arrays.asList;
098import static java.util.Collections.singleton;
099import static java.util.Collections.singletonList;
100import static java.util.stream.Stream.of;
101import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
102import static javax.ws.rs.core.HttpHeaders.LINK;
103import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
104import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM_TYPE;
105import static javax.ws.rs.core.MediaType.TEXT_PLAIN_TYPE;
106import static javax.ws.rs.core.Response.Status.CREATED;
107import static javax.ws.rs.core.Response.Status.NOT_MODIFIED;
108import static javax.ws.rs.core.Response.Status.NO_CONTENT;
109import static javax.ws.rs.core.Response.Status.OK;
110import static javax.ws.rs.core.Response.Status.TEMPORARY_REDIRECT;
111import static org.apache.commons.io.IOUtils.toInputStream;
112import static org.apache.jena.graph.NodeFactory.createURI;
113import static org.apache.jena.riot.WebContent.contentTypeSPARQLUpdate;
114import static org.fcrepo.config.ServerManagedPropsMode.STRICT;
115import static org.fcrepo.http.api.ContentExposingResource.buildLink;
116import static org.fcrepo.http.api.ContentExposingResource.getSimpleContentType;
117import static org.fcrepo.http.api.FedoraLdp.HTTP_HEADER_ACCEPT_PATCH;
118import static org.fcrepo.http.commons.domain.RDFMediaType.NTRIPLES_TYPE;
119import static org.fcrepo.http.commons.test.util.TestHelpers.getServletContextImpl;
120import static org.fcrepo.http.commons.test.util.TestHelpers.getUriInfoImpl;
121import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
122import static org.fcrepo.kernel.api.RdfCollectors.toModel;
123import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
124import static org.fcrepo.kernel.api.RdfLexicon.DIRECT_CONTAINER;
125import static org.fcrepo.kernel.api.RdfLexicon.EXTERNAL_CONTENT;
126import static org.fcrepo.kernel.api.RdfLexicon.FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI;
127import static org.fcrepo.kernel.api.RdfLexicon.INBOUND_REFERENCES;
128import static org.fcrepo.kernel.api.RdfLexicon.INDIRECT_CONTAINER;
129import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
130import static org.fcrepo.kernel.api.RdfLexicon.PREFER_CONTAINMENT;
131import static org.fcrepo.kernel.api.RdfLexicon.PREFER_MEMBERSHIP;
132import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE;
133import static org.fcrepo.kernel.api.RdfLexicon.RESOURCE;
134import static org.fcrepo.kernel.api.RdfLexicon.VERSIONED_RESOURCE;
135import static org.junit.Assert.assertEquals;
136import static org.junit.Assert.assertFalse;
137import static org.junit.Assert.assertTrue;
138import static org.junit.Assert.fail;
139import static org.mockito.ArgumentMatchers.any;
140import static org.mockito.ArgumentMatchers.anyInt;
141import static org.mockito.ArgumentMatchers.anyList;
142import static org.mockito.ArgumentMatchers.anyLong;
143import static org.mockito.ArgumentMatchers.anyString;
144import static org.mockito.ArgumentMatchers.contains;
145import static org.mockito.ArgumentMatchers.eq;
146import static org.mockito.ArgumentMatchers.isNull;
147import static org.mockito.ArgumentMatchers.nullable;
148import static org.mockito.Mockito.doAnswer;
149import static org.mockito.Mockito.doReturn;
150import static org.mockito.Mockito.doThrow;
151import static org.mockito.Mockito.mock;
152import static org.mockito.Mockito.spy;
153import static org.mockito.Mockito.times;
154import static org.mockito.Mockito.verify;
155import static org.mockito.Mockito.when;
156import static org.slf4j.LoggerFactory.getLogger;
157import static org.springframework.test.util.ReflectionTestUtils.setField;
158
159/**
160 * @author cabeer
161 * @author ajs6f
162 */
163@RunWith(MockitoJUnitRunner.Silent.class)
164public class FedoraLdpTest {
165
166    private final String path = "/some/path";
167    private final String binaryPath = "/some/binary/path";
168    private final String binaryDescriptionPath = binaryPath + "/" + FCR_METADATA;
169
170    private final FedoraId pathId = FedoraId.create(path);
171    private final FedoraId binaryPathId = FedoraId.create(binaryPath);
172    private final FedoraId binaryDescId = binaryPathId.resolve(FCR_METADATA);
173
174    private FedoraLdp testObj;
175
176    private final List<String> nonRDFSourceLink = singletonList(
177            Link.fromUri(NON_RDF_SOURCE.toString()).rel("type").build().toString());
178
179    private final HttpIdentifierConverter identifierConverter = new HttpIdentifierConverter(
180            getUriInfoImpl().getBaseUriBuilder().clone().path("/{path: .*}"));
181
182    private final InputStream emptyStream = IOUtils.toInputStream("", Charsets.UTF_8);
183
184    @Mock
185    private Request mockRequest;
186
187    private HttpServletResponse mockResponse;
188
189    @Mock
190    private Transaction mockTransaction;
191
192    @Mock
193    private Container mockContainer;
194
195    @Mock
196    private NonRdfSourceDescription mockNonRdfSourceDescription;
197
198    @Mock
199    private Binary mockBinary;
200
201    @Mock
202    private ResourceFactory resourceFactory;
203
204    @Mock
205    private ResourceHelper resourceHelper;
206
207    @Mock
208    private FedoraHttpConfiguration mockHttpConfiguration;
209
210    @Mock
211    private RdfNamespaceRegistry rdfNamespaceRegistry;
212
213    @Mock
214    private HttpHeaders mockHeaders;
215
216    @Mock
217    private SecurityContext mockSecurityContext;
218
219    @Mock
220    private ServletContext mockServletContext;
221
222    @Mock
223    private MultiPrefer prefer;
224
225    @Mock
226    private PreferTag preferTag;
227
228    @Mock
229    private ExternalContentHandlerFactory extContentHandlerFactory;
230
231    @Mock
232    private ReplacePropertiesService replacePropertiesService;
233
234    @Mock
235    private UpdatePropertiesService updatePropertiesService;
236
237    @Mock
238    private DeleteResourceService deleteResourceService;
239
240    @Mock
241    private CreateResourceService createResourceService;
242
243    @Mock
244    private Principal principal;
245
246    @Mock
247    private ResourceTripleService resourceTripleService;
248
249    @Mock
250    private EtagService etagService;
251
252    @Mock
253    private FedoraPropsConfig fedoraPropsConfig;
254
255    private final List<URI> typeList = new ArrayList<>();
256
257    private static final Logger log = getLogger(FedoraLdpTest.class);
258
259    @Before
260    public void setUp() {
261        testObj = spy(new FedoraLdp(path));
262
263        when(resourceHelper.doesResourceExist(any(Transaction.class), eq(pathId), eq(false))).thenReturn(true);
264
265        mockResponse = new MockHttpServletResponse();
266
267        final HttpRdfService httpRdfService = new HttpRdfService();
268        setField(httpRdfService, "fedoraPropsConfig", fedoraPropsConfig);
269        when(fedoraPropsConfig.getServerManagedPropsMode()).thenReturn(STRICT);
270
271        setField(testObj, "request", mockRequest);
272        setField(testObj, "servletResponse", mockResponse);
273        setField(testObj, "uriInfo", getUriInfoImpl());
274        setField(testObj, "headers", mockHeaders);
275        setField(testObj, "resourceFactory", resourceFactory);
276        setField(testObj, "httpConfiguration", mockHttpConfiguration);
277        setField(testObj, "transaction", mockTransaction);
278        setField(testObj, "securityContext", mockSecurityContext);
279        setField(testObj, "prefer", prefer);
280        setField(testObj, "context", getServletContextImpl());
281        setField(testObj, "extContentHandlerFactory", extContentHandlerFactory);
282        setField(testObj, "namespaceRegistry", rdfNamespaceRegistry);
283        setField(testObj, "deleteResourceService", deleteResourceService);
284        setField(testObj, "resourceTripleService", resourceTripleService);
285        setField(testObj, "httpRdfService", httpRdfService);
286        setField(testObj, "createResourceService", createResourceService);
287        setField(testObj, "replacePropertiesService", replacePropertiesService);
288        setField(testObj, "updatePropertiesService", updatePropertiesService);
289        setField(testObj, "resourceHelper", resourceHelper);
290        setField(testObj, "etagService", etagService);
291        setField(testObj, "dbTransactionExecutor", new DbTransactionExecutor());
292
293        when(rdfNamespaceRegistry.getNamespaces()).thenReturn(new HashMap<>());
294
295        when(mockHttpConfiguration.putRequiresIfMatch()).thenReturn(false);
296
297        when(principal.getName()).thenReturn("testUser");
298        when(mockSecurityContext.getUserPrincipal()).thenReturn(principal);
299
300        when(mockContainer.getEtagValue()).thenReturn("");
301        when(mockContainer.getStateToken()).thenReturn("");
302        when(mockContainer.getDescription()).thenReturn(mockContainer);
303        when(mockContainer.getDescribedResource()).thenReturn(mockContainer);
304        when(mockContainer.getFedoraId()).thenReturn(pathId);
305
306        when(resourceHelper.doesResourceExist(mockTransaction, pathId, true)).thenReturn(false);
307
308        when(mockNonRdfSourceDescription.getEtagValue()).thenReturn("");
309        when(mockNonRdfSourceDescription.getStateToken()).thenReturn("");
310        when(mockNonRdfSourceDescription.getDescribedResource()).thenReturn(mockBinary);
311        when(mockNonRdfSourceDescription.getOriginalResource()).thenReturn(mockNonRdfSourceDescription);
312        when(mockNonRdfSourceDescription.getFedoraId()).thenReturn(binaryDescId);
313
314        when(mockBinary.getEtagValue()).thenReturn("");
315        when(mockBinary.getStateToken()).thenReturn("");
316        when(mockBinary.getDescription()).thenReturn(mockNonRdfSourceDescription);
317        when(mockBinary.getDescribedResource()).thenReturn(mockBinary);
318        when(mockBinary.getOriginalResource()).thenReturn(mockBinary);
319        when(mockBinary.getFedoraId()).thenReturn(binaryPathId);
320
321        when(mockHeaders.getHeaderString("user-agent")).thenReturn("Test UserAgent");
322        when(mockHeaders.getHeaderString("X-If-State-Token")).thenReturn(null);
323
324        when(mockTransaction.getId()).thenReturn("foo1234");
325        when(mockTransaction.isShortLived()).thenReturn(true);
326
327        when(mockServletContext.getContextPath()).thenReturn("/");
328
329        when(prefer.getReturn()).thenReturn(preferTag);
330
331        doAnswer((Answer<HttpServletResponse>) invocation -> {
332            mockResponse.addHeader("Preference-Applied", "return=representation");
333            return null;
334        }).when(preferTag).addResponseHeaders(mockResponse);
335
336        when(etagService.getRdfResourceEtag(nullable(Transaction.class), any(FedoraResource.class),
337                nullable(LdpTriplePreferences.class), any())).thenReturn("etagval");
338    }
339
340    private FedoraResource setResource(final Class<? extends FedoraResource> klass) {
341        final List<tripleTypes> defaultTriples = List.of(
342                tripleTypes.PROPERTIES,
343                tripleTypes.LDP_CONTAINMENT,
344                tripleTypes.SERVER_MANAGED,
345                tripleTypes.LDP_MEMBERSHIP
346        );
347        return setResource(klass, defaultTriples);
348    }
349
350    private FedoraResource setResource(final Class<? extends FedoraResource> klass,
351                                       final List<tripleTypes> tripleTypesList) {
352        final FedoraResource mockResource = mock(klass);
353        typeList.add(URI.create(RESOURCE.toString()));
354        if (mockResource instanceof Binary) {
355            when(((Binary) mockResource).getContentSize()).thenReturn(1L);
356            when(mockResource.getOriginalResource()).thenReturn(mockResource);
357            when(mockResource.getDescription()).thenReturn(mockNonRdfSourceDescription);
358            typeList.add(URI.create(NON_RDF_SOURCE.toString()));
359        } else if (mockResource instanceof NonRdfSourceDescription) {
360            when(mockResource.getOriginalResource()).thenReturn(mockBinary);
361            when(mockBinary.getDescription()).thenReturn(mockResource);
362            typeList.addAll(List.of(URI.create(FEDORA_NON_RDF_SOURCE_DESCRIPTION_URI),
363                    URI.create(RDF_SOURCE.toString())));
364        } else if (mockResource instanceof Container) {
365            typeList.addAll(List.of(URI.create(BASIC_CONTAINER.toString()), URI.create(RDF_SOURCE.toString())));
366        }
367        when(mockResource.getTypes()).thenReturn(typeList);
368
369        doReturn(mockResource).when(testObj).resource();
370        when(mockResource.getFedoraId()).thenReturn(FedoraId.create(path));
371        when(mockResource.getEtagValue()).thenReturn("");
372        when(mockResource.getStateToken()).thenReturn("");
373        when(mockResource.getDescribedResource()).thenReturn(mockResource);
374        setupResourceService(mockResource, tripleTypesList);
375        return mockResource;
376    }
377
378    private enum tripleTypes {
379        PROPERTIES, SERVER_MANAGED, LDP_CONTAINMENT, INBOUND_REFERENCES, LDP_MEMBERSHIP
380    }
381
382    private void setupResourceService(final FedoraResource mockResource, final List<tripleTypes> response) {
383        final var testUri = createURI("test");
384        final List<Triple> triples = new ArrayList<>();
385        if (response.contains(tripleTypes.PROPERTIES)) {
386            triples.add(Triple.create(testUri, createURI("called"), createURI("PROPERTIES")));
387        }
388        if (response.contains(tripleTypes.SERVER_MANAGED)) {
389            triples.add(Triple.create(testUri, createURI("managed"), createURI("SERVER_MANAGED")));
390        }
391        if (response.contains(tripleTypes.LDP_CONTAINMENT)) {
392            triples.add(Triple.create(testUri, createURI("contains"), createURI("LDP_CONTAINMENT")));
393        }
394        if (response.contains(tripleTypes.INBOUND_REFERENCES)) {
395            triples.add(Triple.create(testUri, createURI("references"), createURI("INBOUND_REFERENCES")));
396        }
397        if (response.contains(tripleTypes.LDP_MEMBERSHIP)) {
398            triples.add(Triple.create(testUri, createURI("membership"), createURI("LDP_MEMBERSHIP")));
399        }
400        final Answer<Stream<Triple>> answer = invocationOnMock -> triples.stream();
401        when(resourceTripleService.getResourceTriples(any(Transaction.class),
402                eq(mockResource), any(LdpTriplePreferences.class), anyInt())).thenAnswer(answer);
403    }
404
405    @Test
406    public void testHead() throws Exception {
407        setResource(FedoraResource.class);
408        when(mockRequest.getMethod()).thenReturn("HEAD");
409        final Response actual = testObj.head();
410        assertEquals(OK.getStatusCode(), actual.getStatus());
411        assertTrue("Should have a Link header", mockResponse.containsHeader(LINK));
412        assertTrue("Should have an Allow header", mockResponse.containsHeader("Allow"));
413        assertTrue("Should have a Preference-Applied header", mockResponse.containsHeader("Preference-Applied"));
414        assertTrue("Should have a Vary header", mockResponse.containsHeader("Vary"));
415        assertTrue("Should be an LDP Resource",
416                mockResponse.getHeaders(LINK).contains("<" + RESOURCE + ">; rel=\"type\""));
417    }
418
419    @Test
420    public void testHeadWithObject() throws Exception {
421        setResource(Container.class);
422        when(mockRequest.getMethod()).thenReturn("HEAD");
423        final Response actual = testObj.head();
424        assertEquals(OK.getStatusCode(), actual.getStatus());
425        assertTrue("Should advertise Accept-Post flavors", mockResponse.containsHeader("Accept-Post"));
426        assertTrue("Should advertise Accept-Patch flavors", mockResponse.containsHeader(
427                FedoraLdp.HTTP_HEADER_ACCEPT_PATCH));
428    }
429
430    @Test
431    public void testHeadWithDefaultContainer() throws Exception {
432        setResource(Container.class);
433        when(mockRequest.getMethod()).thenReturn("HEAD");
434        final Response actual = testObj.head();
435        assertEquals(OK.getStatusCode(), actual.getStatus());
436        assertShouldBeAnLDPBasicContainer();
437    }
438
439    private void assertShouldBeAnLDPBasicContainer() {
440        assertTrue("Should be an LDP BasicContainer",
441                mockResponse.getHeaders(LINK).contains("<" + BASIC_CONTAINER.getURI() + ">; rel=\"type\""));
442    }
443
444    @Test
445    public void testHeadWithBasicContainer() throws Exception {
446        final FedoraResource resource = setResource(Container.class);
447        when(mockRequest.getMethod()).thenReturn("HEAD");
448        when(resource.hasType(BASIC_CONTAINER.toString())).thenReturn(true);
449        final Response actual = testObj.head();
450        assertEquals(OK.getStatusCode(), actual.getStatus());
451        assertShouldBeAnLDPBasicContainer();
452    }
453
454    @Test
455    public void testHeadWithDirectContainer() throws Exception {
456        final FedoraResource resource = setResource(Container.class);
457        when(mockRequest.getMethod()).thenReturn("HEAD");
458        typeList.add(URI.create(DIRECT_CONTAINER.toString()));
459        final Response actual = testObj.head();
460        assertEquals(OK.getStatusCode(), actual.getStatus());
461        assertShouldBeAnLDPDirectContainer();
462    }
463
464    private void assertShouldBeAnLDPDirectContainer() {
465        assertTrue("Should be an LDP DirectContainer",
466                mockResponse.getHeaders(LINK).contains("<" + DIRECT_CONTAINER.getURI() + ">; rel=\"type\""));
467    }
468
469    @Test
470    public void testHeadWithIndirectContainer() throws Exception {
471        final FedoraResource resource = setResource(Container.class);
472        when(mockRequest.getMethod()).thenReturn("HEAD");
473        typeList.add(URI.create(INDIRECT_CONTAINER.toString()));
474        final Response actual = testObj.head();
475        assertEquals(OK.getStatusCode(), actual.getStatus());
476        assertShouldBeAnLDPIndirectContainer();
477    }
478
479    private void assertShouldBeAnLDPIndirectContainer() {
480        assertTrue("Should be an LDP IndirectContainer",
481                mockResponse.getHeaders(LINK).contains("<" + INDIRECT_CONTAINER.getURI() + ">; rel=\"type\""));
482    }
483
484    @Test
485    public void testHeadWithBinary() throws Exception {
486        final Binary mockResource = (Binary)setResource(Binary.class);
487        when(mockRequest.getMethod()).thenReturn("HEAD");
488        when(mockResource.getDescription()).thenReturn(mockNonRdfSourceDescription);
489        when(mockResource.getOriginalResource()).thenReturn(mockResource);
490        when(mockResource.getMimeType()).thenReturn("image/jpeg");
491        final Response actual = testObj.head();
492        assertEquals(OK.getStatusCode(), actual.getStatus());
493        assertContentLengthGreaterThan0(mockResponse.getHeader(CONTENT_LENGTH));
494        assertShouldBeAnLDPNonRDFSource();
495        assertShouldNotAdvertiseAcceptPatchFlavors();
496        assertShouldContainLinkToBinaryDescription();
497    }
498
499    private void assertContentLengthGreaterThan0(final String contentLength) {
500        assertTrue("Should have a content length header greater than 0", Integer.parseInt(contentLength) > 0);
501    }
502
503    private void assertShouldContainLinkToBinaryDescription() {
504        final String described = identifierConverter.toDomain(binaryPath + "/fcr:metadata");
505        assertTrue("Should contain a link to the binary description",
506                mockResponse.getHeaders(LINK)
507                        .contains("<" + described + ">; rel=\"describedby\""));
508    }
509
510    private void assertShouldNotAdvertiseAcceptPatchFlavors() {
511        assertFalse("Should not advertise Accept-Patch flavors", mockResponse.containsHeader(HTTP_HEADER_ACCEPT_PATCH));
512    }
513
514    private void assertShouldNotAdvertiseAcceptPostFlavors() {
515        assertFalse("Should not advertise Accept-Post flavors", mockResponse.containsHeader("Accept-Post"));
516    }
517
518    private void assertShouldHaveAcceptExternalContentHandlingHeader() {
519        assertTrue("Should have Accept-External-Content-Handling header",
520                mockResponse.containsHeader(FedoraLdp.ACCEPT_EXTERNAL_CONTENT));
521        assertEquals("Should support copy, redirect, and proxy", "copy,redirect,proxy",
522                mockResponse.getHeader(FedoraLdp.ACCEPT_EXTERNAL_CONTENT));
523    }
524
525    @Test
526    public void testHeadWithExternalBinary() throws Exception {
527        final Binary mockResource = (Binary)setResource(Binary.class);
528        when(mockRequest.getMethod()).thenReturn("HEAD");
529        final String url = "http://example.com/some/url";
530        when(mockResource.getDescription()).thenReturn(mockNonRdfSourceDescription);
531        when(mockResource.getMimeType()).thenReturn("text/plain");
532        when(mockResource.isProxy()).thenReturn(false);
533        when(mockResource.isRedirect()).thenReturn(true);
534        when(mockResource.getOriginalResource()).thenReturn(mockResource);
535        when(mockResource.getExternalURL()).thenReturn(url);
536        when(mockResource.getExternalURI()).thenReturn(URI.create(url));
537        final Response actual = testObj.head();
538        assertEquals(TEMPORARY_REDIRECT.getStatusCode(), actual.getStatus());
539        assertEquals(new URI(url), actual.getLocation());
540        assertShouldBeAnLDPNonRDFSource();
541        assertShouldNotAdvertiseAcceptPatchFlavors();
542        assertShouldContainLinkToBinaryDescription();
543    }
544
545    @Test
546    public void testHeadWithBinaryDescription() throws Exception {
547        final NonRdfSourceDescription mockResource
548                = (NonRdfSourceDescription)setResource(NonRdfSourceDescription.class);
549        when(mockRequest.getMethod()).thenReturn("HEAD");
550        when(mockResource.getDescribedResource()).thenReturn(mockBinary);
551        when(mockResource.getOriginalResource()).thenReturn(mockResource);
552        final Response actual = testObj.head();
553        assertEquals(OK.getStatusCode(), actual.getStatus());
554        assertTrue("Should be an LDP RDFSource",
555            mockResponse.getHeaders(LINK).contains(buildLink(RDF_SOURCE.getURI(), "type")));
556        assertShouldNotAdvertiseAcceptPostFlavors();
557        assertShouldAdvertiseAcceptPatchFlavors();
558        assertShouldContainLinkToTheBinary();
559        assertShouldAllowOnlyResourceDescriptionMethods();
560    }
561
562    private void assertShouldContainLinkToTheBinary() {
563        assertTrue("Should contain a link to the binary",
564                mockResponse.getHeaders(LINK)
565                        .contains("<" + identifierConverter.toDomain(binaryPath) + ">; rel=\"describes\""));
566    }
567
568    private void assertShouldAdvertiseAcceptPatchFlavors() {
569        assertTrue("Should advertise Accept-Patch flavors", mockResponse.containsHeader(HTTP_HEADER_ACCEPT_PATCH));
570    }
571
572    @Test
573    public void testOption() {
574        setResource(FedoraResource.class);
575        final Response actual = testObj.options();
576        assertEquals(OK.getStatusCode(), actual.getStatus());
577        assertTrue("Should have an Allow header", mockResponse.containsHeader("Allow"));
578        assertShouldHaveAcceptExternalContentHandlingHeader();
579    }
580
581    @Test
582    public void testOptionWithLDPRS() {
583        setResource(Container.class);
584        final Response actual = testObj.options();
585        assertEquals(OK.getStatusCode(), actual.getStatus());
586        assertTrue("Should advertise Accept-Post flavors", mockResponse.containsHeader("Accept-Post"));
587        assertShouldAdvertiseAcceptPatchFlavors();
588        assertShouldHaveAcceptExternalContentHandlingHeader();
589    }
590
591    @Test
592    public void testOptionWithBinary() throws Exception {
593        setField(testObj, "externalPath", binaryPath);
594        when(resourceFactory.getResource(mockTransaction, binaryPathId)).thenReturn(mockBinary);
595        final Response actual = testObj.options();
596        assertEquals(OK.getStatusCode(), actual.getStatus());
597        assertShouldNotAdvertiseAcceptPostFlavors();
598        assertShouldNotAdvertiseAcceptPatchFlavors();
599        assertShouldContainLinkToBinaryDescription();
600        assertShouldHaveAcceptExternalContentHandlingHeader();
601    }
602
603    @Test
604    public void testOptionWithBinaryDescription() throws Exception {
605        setField(testObj, "externalPath", binaryDescriptionPath);
606        when(resourceFactory.getResource(mockTransaction, binaryDescId))
607                .thenReturn(mockNonRdfSourceDescription);
608        final Response actual = testObj.options();
609        assertEquals(OK.getStatusCode(), actual.getStatus());
610        assertShouldNotAdvertiseAcceptPostFlavors();
611        assertShouldAdvertiseAcceptPatchFlavors();
612        assertShouldContainLinkToTheBinary();
613        assertShouldHaveAcceptExternalContentHandlingHeader();
614    }
615
616
617    @Test
618    public void testGet() throws Exception {
619        final var resource = setResource(Container.class);
620        setField(testObj, "externalPath", path);
621        when(mockRequest.getMethod()).thenReturn("GET");
622        when(resourceFactory.getResource((Transaction)any(), any(FedoraId.class))).thenReturn(resource);
623        final Response actual = testObj.getResource(null);
624        assertEquals(OK.getStatusCode(), actual.getStatus());
625        assertTrue("Should have a Link header", mockResponse.containsHeader(LINK));
626        assertTrue("Should have an Allow header", mockResponse.containsHeader("Allow"));
627        assertTrue("Should be an LDP Resource",
628                mockResponse.getHeaders(LINK).contains("<" + RESOURCE + ">; rel=\"type\""));
629
630        try (final RdfNamespacedStream entity = (RdfNamespacedStream) actual.getEntity()) {
631            final Model model = entity.stream.collect(toModel());
632            final List<String> rdfNodes = model.listObjects().mapWith(RDFNode::toString).toList();
633            assertTrue("Expected RDF contexts missing", rdfNodes.containsAll(ImmutableSet.of(
634                    "LDP_CONTAINMENT", "PROPERTIES", "SERVER_MANAGED", "LDP_MEMBERSHIP")));
635        }
636    }
637
638    @Test
639    public void testGetWithObject() throws Exception {
640        setResource(Container.class);
641        when(mockRequest.getMethod()).thenReturn("GET");
642        final Response actual = testObj.getResource(null);
643        assertEquals(OK.getStatusCode(), actual.getStatus());
644        assertTrue("Should advertise Accept-Post flavors", mockResponse.containsHeader("Accept-Post"));
645        assertShouldAdvertiseAcceptPatchFlavors();
646
647        try (final RdfNamespacedStream entity = (RdfNamespacedStream) actual.getEntity()) {
648            final Model model = entity.stream.collect(toModel());
649            final List<String> rdfNodes = model.listObjects().mapWith(RDFNode::toString).toList();
650            assertTrue("Expected RDF contexts missing", rdfNodes.containsAll(ImmutableSet.of(
651                    "LDP_CONTAINMENT", "PROPERTIES", "SERVER_MANAGED")));
652            // TODO: Above list is missing LDP_MEMBERSHIP - https://jira.lyrasis.org/browse/FCREPO-3165
653        }
654    }
655
656
657    @Test
658    public void testGetWithBasicContainer() throws Exception {
659        final FedoraResource resource = setResource(Container.class);
660        when(mockRequest.getMethod()).thenReturn("GET");
661        final Response actual = testObj.getResource(null);
662        assertEquals(OK.getStatusCode(), actual.getStatus());
663        assertShouldBeAnLDPBasicContainer();
664    }
665
666    @Test
667    public void testGetWithDirectContainer() throws Exception {
668        final FedoraResource resource = setResource(Container.class);
669        when(mockRequest.getMethod()).thenReturn("GET");
670        typeList.add(URI.create(DIRECT_CONTAINER.toString()));
671        final Response actual = testObj.getResource(null);
672        assertEquals(OK.getStatusCode(), actual.getStatus());
673        assertShouldBeAnLDPDirectContainer();
674    }
675
676    @Test
677    public void testGetWithIndirectContainer() throws Exception {
678        final FedoraResource resource = setResource(Container.class);
679        when(mockRequest.getMethod()).thenReturn("GET");
680        typeList.add(URI.create(INDIRECT_CONTAINER.toString()));
681        final Response actual = testObj.getResource(null);
682        assertEquals(OK.getStatusCode(), actual.getStatus());
683        assertShouldBeAnLDPIndirectContainer();
684    }
685
686    @Ignore("Requires minimal representation - https://jira.lyrasis.org/browse/FCREPO-3334")
687    @Test
688    public void testGetWithObjectPreferMinimal() throws Exception {
689        setResource(Container.class);
690        when(mockRequest.getMethod()).thenReturn("GET");
691        setField(testObj, "prefer", new MultiPrefer("return=minimal"));
692        final Response actual = testObj.getResource(null);
693        assertEquals(OK.getStatusCode(), actual.getStatus());
694
695        try (final RdfNamespacedStream entity = (RdfNamespacedStream) actual.getEntity()) {
696            final Model model = entity.stream.collect(toModel());
697            final List<String> rdfNodes = model.listObjects().mapWith(RDFNode::toString).toList();
698            assertTrue("Expected RDF contexts missing", rdfNodes.stream()
699                    .anyMatch(x -> x.contains("PROPERTIES") && x.contains("MINIMAL")));
700            assertFalse("Included non-minimal contexts", rdfNodes.contains("LDP_MEMBERSHIP"));
701            assertFalse("Included non-minimal contexts", rdfNodes.contains("LDP_CONTAINMENT"));
702        }
703
704    }
705
706    /**
707     * Emulates an 'If-None-Match' precondition failing for a GET request.  There should not be any entity body set
708     * on the response.
709     *
710     */
711    @Test
712    public void testGetWhenIfNoneMatchPreconditionFails() {
713        setResource(FedoraResource.class);
714
715        // Set up the expectations for the ResponseBuilder
716        final Response.ResponseBuilder builder = mock(Response.ResponseBuilder.class);
717        final Response response = mock(Response.class);
718
719        when(builder.entity(any())).thenReturn(builder);
720        when(builder.cacheControl(any())).thenReturn(builder);
721        when(builder.lastModified((any()))).thenReturn(builder);
722        when(builder.tag(any(EntityTag.class))).thenReturn(builder);
723        when(builder.build()).thenReturn(response);
724        when(response.getEntity()).thenReturn("precondition failed message");
725        when(response.getStatus()).thenReturn(NOT_MODIFIED.getStatusCode());
726
727        // Set up expectations for the Request; returning a ResponseBuilder from evaluatePreconditions(...) indicates
728        // that satisfying the precondition fails, which is what we want to simulate in this test
729        when(mockRequest.evaluatePreconditions(any(EntityTag.class))).thenReturn(builder);
730
731        // Execute the method under test.  Preconditions should fail, resulting in an exception being thrown.
732        try {
733            testObj.evaluateRequestPreconditions(mockRequest, mockResponse, testObj.resource(),
734                    mockTransaction, true);
735            fail("Expected " + PreconditionException.class.getName() + " to be thrown.");
736        } catch (final PreconditionException e) {
737            // expected
738        }
739
740        // an entity body should _not_ be set under these conditions
741        verify(mockRequest).evaluatePreconditions(any(EntityTag.class));
742        verify(builder, times(0)).entity(any());
743    }
744
745    /**
746     * Emulates an 'If-Modified-Since' precondition failing for a GET request.  There should not be any entity body set
747     * on the response.
748     *
749     */
750    @Test
751    public void testGetWhenIfModifiedSincePreconditionFails() {
752        setResource(FedoraResource.class);
753
754        // Set up the expectations for the ResponseBuilder
755        final Response.ResponseBuilder builder = mock(Response.ResponseBuilder.class);
756        final Response response = mock(Response.class);
757
758        when(builder.entity(any())).thenReturn(builder);
759        when(builder.cacheControl(any())).thenReturn(builder);
760        when(builder.lastModified((any()))).thenReturn(builder);
761        when(builder.tag(any(EntityTag.class))).thenReturn(builder);
762        when(builder.build()).thenReturn(response);
763        when(response.getStatus()).thenReturn(NOT_MODIFIED.getStatusCode());
764
765        when(response.getEntity()).thenReturn("precondition failed message");
766        // Set up expectations for the Request; returning a ResponseBuilder from evaluatePreconditions(...) indicates
767        // that satisfying the precondition fails, which is what we want to simulate in this test
768        when(mockRequest.evaluatePreconditions(any(Date.class))).thenReturn(builder);
769
770        // Execute the method under test.  Preconditions should fail, resulting in an exception being thrown.
771        try {
772            testObj.evaluateRequestPreconditions(mockRequest, mockResponse, testObj.resource(),
773                    mockTransaction, true);
774            fail("Expected " + PreconditionException.class.getName() + " to be thrown.");
775        } catch (final PreconditionException e) {
776            // expected
777        }
778
779        // an entity body should _not_ be set under these conditions
780        verify(mockRequest).evaluatePreconditions(any(Date.class));
781        verify(builder, times(0)).entity(any());
782    }
783
784    @Ignore("Needs membership triples - FCREPO-3165")
785    @Test
786    public void testGetWithObjectOmitContainment() throws Exception {
787        setResource(Container.class);
788        when(mockRequest.getMethod()).thenReturn("GET");
789        setField(testObj, "prefer",
790                new MultiPrefer("return=representation; omit=\"" + PREFER_CONTAINMENT + "\""));
791        final Response actual = testObj.getResource( null);
792        assertEquals(OK.getStatusCode(), actual.getStatus());
793
794        try (final RdfNamespacedStream entity = (RdfNamespacedStream) actual.getEntity()) {
795            final Model model = entity.stream.collect(toModel());
796            final List<String> rdfNodes = model.listObjects().mapWith(RDFNode::toString).toList();
797            assertTrue("Should include membership contexts", rdfNodes.contains("LDP_MEMBERSHIP"));
798            assertFalse("Should not include containment contexts", rdfNodes.contains("LDP_CONTAINMENT"));
799        }
800    }
801
802    @Ignore("Membership Triples not implemented - FCREPO-3165")
803    @Test
804    public void testGetWithObjectOmitMembership() throws Exception {
805        setResource(Container.class);
806        when(mockRequest.getMethod()).thenReturn("GET");
807        setField(testObj, "prefer",
808                new MultiPrefer("return=representation; omit=\"" + PREFER_MEMBERSHIP + "\""));
809        final Response actual = testObj.getResource(null);
810        assertEquals(OK.getStatusCode(), actual.getStatus());
811
812        try (final RdfNamespacedStream entity = (RdfNamespacedStream) actual.getEntity()) {
813            final Model model = entity.stream.collect(toModel());
814            final List<String> rdfNodes = model.listObjects().mapWith(RDFNode::toString).toList();
815            assertFalse("Should not include membership contexts", rdfNodes.contains("LDP_MEMBERSHIP"));
816            assertTrue("Should include containment contexts", rdfNodes.contains("LDP_CONTAINMENT"));
817        }
818    }
819
820    @Test
821    public void testGetWithObjectIncludeReferences()
822            throws IOException, UnsupportedAlgorithmException {
823        final List<tripleTypes> triples = List.of(
824                tripleTypes.PROPERTIES,
825                tripleTypes.LDP_CONTAINMENT,
826                tripleTypes.SERVER_MANAGED,
827                tripleTypes.LDP_MEMBERSHIP,
828                tripleTypes.INBOUND_REFERENCES
829        );
830        setResource(Container.class, triples);
831        when(mockRequest.getMethod()).thenReturn("GET");
832        setField(testObj, "prefer", new MultiPrefer("return=representation; include=\"" + INBOUND_REFERENCES + "\""));
833        final Response actual = testObj.getResource(null);
834        assertEquals(OK.getStatusCode(), actual.getStatus());
835
836        try (final RdfNamespacedStream entity = (RdfNamespacedStream) actual.getEntity()) {
837            final Model model = entity.stream.collect(toModel());
838
839            final List<String> rdfNodes = model.listObjects().mapWith(RDFNode::toString).toList();
840            log.debug("Received RDF nodes: {}", rdfNodes);
841            assertTrue("Should include references contexts",
842                    rdfNodes.stream().anyMatch(containsPattern("REFERENCES")::apply));
843        }
844    }
845
846    @Test
847    public void testGetWithBinary() throws Exception {
848        final Binary mockResource = (Binary)setResource(Binary.class);
849        when(mockRequest.getMethod()).thenReturn("GET");
850        when(mockResource.getDescription()).thenReturn(mockNonRdfSourceDescription);
851        when(mockResource.getMimeType()).thenReturn("text/plain");
852        when(mockResource.getContent()).thenReturn(toInputStream("xyz", UTF_8));
853        when(mockResource.getOriginalResource()).thenReturn(mockResource);
854        final Response actual = testObj.getResource(null);
855        assertEquals(OK.getStatusCode(), actual.getStatus());
856        assertShouldBeAnLDPNonRDFSource();
857        assertShouldNotAdvertiseAcceptPatchFlavors();
858        assertShouldContainLinkToBinaryDescription();
859        assertEquals("xyz", IOUtils.toString((InputStream) actual.getEntity(), UTF_8));
860    }
861
862    private void assertShouldBeAnLDPNonRDFSource() {
863        assertTrue("Should be an LDP NonRDFSource",
864                mockResponse.getHeaders(LINK).contains("<" + NON_RDF_SOURCE + ">; rel=\"type\""));
865        assertShouldNotAdvertiseAcceptPostFlavors();
866    }
867
868    @Test
869    public void testGetWithExternalMessageBinary() throws Exception {
870        final Binary mockResource = (Binary)setResource(Binary.class);
871        when(mockRequest.getMethod()).thenReturn("GET");
872        final String url = "http://example.com/some/url";
873        when(mockResource.getDescription()).thenReturn(mockNonRdfSourceDescription);
874        when(mockResource.getMimeType()).thenReturn("text/plain");
875        when(mockResource.isProxy()).thenReturn(false);
876        when(mockResource.isRedirect()).thenReturn(true);
877        when(mockResource.getOriginalResource()).thenReturn(mockResource);
878        when(mockResource.getExternalURI()).thenReturn(URI.create(url));
879        when(mockResource.getExternalURL()).thenReturn(url);
880        when(mockResource.getContent()).thenReturn(toInputStream("xyz", UTF_8));
881        final Response actual = testObj.getResource(null);
882        assertEquals(TEMPORARY_REDIRECT.getStatusCode(), actual.getStatus());
883        assertTrue("Should be an LDP NonRDFSource", mockResponse.getHeaders(LINK).contains("<" +
884                NON_RDF_SOURCE + ">; rel=\"type\""));
885        assertShouldContainLinkToBinaryDescription();
886        assertEquals(new URI(url), actual.getLocation());
887    }
888
889    @Test(expected = ExternalMessageBodyException.class)
890    public void testGetWithExternalMessageMissingURLBinary() throws Exception {
891        when(extContentHandlerFactory.createFromLinks(anyList()))
892                .thenThrow(new ExternalMessageBodyException(""));
893
894        final String badExternal = Link.fromUri("http://test.com")
895                .rel(EXTERNAL_CONTENT.toString())
896                .param("handling", "proxy")
897                .type("text/plain")
898                .build()
899                .toString()
900                .replaceAll("<.*>", "< >");
901
902        testObj.createObject(null, null, null, null, singletonList(badExternal), null);
903    }
904
905    @Test(expected = ExternalMessageBodyException.class)
906    public void testPostWithExternalMessageBadHandling() throws Exception {
907        when(extContentHandlerFactory.createFromLinks(anyList()))
908                .thenThrow(new ExternalMessageBodyException(""));
909
910         final String badExternal = Link.fromUri("http://test.com")
911            .rel(EXTERNAL_CONTENT.toString()).param("handling", "boogie").type("text/plain").build().toString();
912        testObj.createObject(null, null, null, null, singletonList(badExternal), null);
913    }
914
915    @Test
916    public void testGetWithBinaryDescription()
917            throws IOException, UnsupportedAlgorithmException {
918
919        final NonRdfSourceDescription mockResource
920                = (NonRdfSourceDescription)setResource(NonRdfSourceDescription.class);
921        when(mockRequest.getMethod()).thenReturn("GET");
922        when(mockResource.getDescribedResource()).thenReturn(mockBinary);
923        when(mockResource.getOriginalResource()).thenReturn(mockResource);
924        when(mockBinary.getTriples())
925            .thenReturn(new DefaultRdfStream(createURI("mockBinary")));
926        when(mockBinary.getTriples())
927            .thenReturn(new DefaultRdfStream(createURI("mockBinary"), of(new Triple
928                    (createURI("mockBinary"), createURI("called"), createURI("child:properties")))));
929        final Response actual = testObj.getResource(null);
930        assertEquals(OK.getStatusCode(), actual.getStatus());
931
932        assertTrue("Should be an LDP RDFSource",
933            mockResponse.getHeaders(LINK).contains(buildLink(RDF_SOURCE.getURI(), "type")));
934        assertShouldNotAdvertiseAcceptPostFlavors();
935        assertShouldAdvertiseAcceptPatchFlavors();
936        assertShouldContainLinkToTheBinary();
937        assertShouldAllowOnlyResourceDescriptionMethods();
938
939        final Model model = ((RdfNamespacedStream) actual.getEntity()).stream.collect(toModel());
940        final List<String> rdfNodes = model.listObjects().mapWith(RDFNode::toString).toList();
941        log.info("Found RDF objects\n{}", rdfNodes);
942        assertTrue("Expected RDF contexts missing", rdfNodes.containsAll(ImmutableSet.of(
943                "LDP_CONTAINMENT", "PROPERTIES", "SERVER_MANAGED")));
944        // TODO: Above list is missing LDP_MEMBERSHIP - https://jira.lyrasis.org/browse/FCREPO-3165
945
946    }
947
948    private void assertShouldAllowOnlyResourceDescriptionMethods() {
949        final String[] allows = mockResponse.getHeader(HttpHeaders.ALLOW).split(",");
950
951        final Set<String> allowedMethods = new HashSet<>(Arrays.asList(allows));
952        final Set<String> validMethods = ImmutableSet.of("GET", "HEAD", "DELETE", "PUT", "PATCH",
953                "OPTIONS");
954
955        allowedMethods.removeAll(validMethods);
956
957        assertEquals("Allow header contains invalid methods: " + allowedMethods, 0, allowedMethods.size());
958    }
959
960    @Test
961    public void testDelete() throws Exception {
962        when(resourceFactory.getResource(mockTransaction, pathId)).thenReturn(mockContainer);
963        when(mockRequest.getMethod()).thenReturn("DELETE");
964        final Response actual = testObj.deleteObject();
965        assertEquals(NO_CONTENT.getStatusCode(), actual.getStatus());
966        verify(deleteResourceService).perform(
967                eq(mockTransaction),
968                eq(mockContainer),
969                anyString());
970    }
971
972    @Test
973    public void testPutNewObject() throws Exception {
974        setField(testObj, "externalPath", "some/path");
975        final Binary mockObject = (Binary)setResource(Binary.class);
976        when(mockRequest.getMethod()).thenReturn("PUT");
977        when(resourceFactory.getResource(mockTransaction, FedoraId.create("some/path"))).thenReturn(mockObject);
978
979        final Response actual = testObj.createOrReplaceObjectRdf(null, emptyStream,
980                null, null, null, null);
981
982        assertEquals(CREATED.getStatusCode(), actual.getStatus());
983    }
984
985    @Test(expected = CannotCreateResourceException.class)
986    public void testPutNewObjectLdpr() throws Exception {
987        testObj.createOrReplaceObjectRdf(null, null, null, null,
988                singletonList("<http://www.w3.org/ns/ldp#Resource>; rel=\"type\""), null);
989    }
990
991    @Test
992    public void testPutNewObjectWithRdf() throws Exception {
993
994        when(mockRequest.getMethod()).thenReturn("PUT");
995        when(resourceFactory.getResource(mockTransaction, pathId)).thenReturn(mockContainer);
996
997        final Response actual = testObj.createOrReplaceObjectRdf(NTRIPLES_TYPE,
998                toInputStream("_:a <info:x> _:c .", UTF_8), null, null, null, null);
999
1000        assertEquals(CREATED.getStatusCode(), actual.getStatus());
1001        verify(createResourceService).perform(
1002                eq(mockTransaction),
1003                anyString(),
1004                eq(pathId),
1005                isNull(),
1006                any(Model.class));
1007    }
1008
1009    @Test
1010    public void testPutNewBinary() throws Exception {
1011        setField(testObj, "externalPath", "some/path");
1012        final Binary mockObject = (Binary)setResource(Binary.class);
1013        when(mockRequest.getMethod()).thenReturn("PUT");
1014        when(resourceFactory.getResource(mockTransaction, FedoraId.create("some/path"))).thenReturn(mockObject);
1015
1016        final Response actual = testObj.createOrReplaceObjectRdf(TEXT_PLAIN_TYPE,
1017                toInputStream("xyz", UTF_8), null, null, nonRDFSourceLink, null);
1018
1019        assertEquals(CREATED.getStatusCode(), actual.getStatus());
1020    }
1021
1022    @Test
1023    public void testPutReplaceRdfObject() throws Exception {
1024
1025        setField(testObj, "externalPath", "some/path");
1026        final Container mockObject = (Container)setResource(Container.class);
1027        when(mockRequest.getMethod()).thenReturn("PUT");
1028        when(resourceFactory.getResource(mockTransaction, pathId)).thenReturn(mockObject);
1029        when(resourceHelper.doesResourceExist(mockTransaction, pathId, true)).thenReturn(true);
1030
1031        final Response actual = testObj.createOrReplaceObjectRdf(NTRIPLES_TYPE,
1032                toInputStream("_:a <info:x> _:c .", UTF_8), null, null, null, null);
1033
1034        assertEquals(NO_CONTENT.getStatusCode(), actual.getStatus());
1035        verify(replacePropertiesService).perform(eq(mockTransaction), anyString(), eq(pathId),
1036                any(Model.class));
1037    }
1038
1039    @Test(expected = ClientErrorException.class)
1040    public void testPutWithStrictIfMatchHandling() throws Exception {
1041
1042        when(mockHttpConfiguration.putRequiresIfMatch()).thenReturn(true);
1043        when(resourceFactory.getResource(mockTransaction, pathId)).thenReturn(mockContainer);
1044        when(resourceHelper.doesResourceExist(mockTransaction, pathId, true)).thenReturn(true);
1045
1046        testObj.createOrReplaceObjectRdf(NTRIPLES_TYPE,
1047                toInputStream("_:a <info:x> _:c .", UTF_8), null, null, null, null);
1048
1049    }
1050
1051    @Test
1052    public void testPatchObject() throws Exception {
1053
1054        setResource(Container.class);
1055        when(mockRequest.getMethod()).thenReturn("PATCH");
1056        testObj.updateSparql(toInputStream("INSERT DATA { <> <http://some/predicate> \"xyz\" }", UTF_8));
1057    }
1058
1059
1060    @Test
1061    public void testPatchBinaryDescription() throws Exception {
1062        setField(testObj, "externalPath", binaryDescriptionPath);
1063        when(mockRequest.getMethod()).thenReturn("PATCH");
1064
1065        when(mockNonRdfSourceDescription.getTriples())
1066            .thenReturn(new DefaultRdfStream(createURI("mockBinary"),
1067                        of(new Triple(createURI("mockBinary"), createURI("called"),
1068                            createURI("child:properties")))));
1069
1070        when(resourceFactory.getResource(mockTransaction, binaryDescId))
1071                .thenReturn(mockNonRdfSourceDescription);
1072        testObj.updateSparql(toInputStream("INSERT DATA { <> <http://some/predicate> \"xyz\" }", UTF_8));
1073        verify(updatePropertiesService).updateProperties(
1074                eq(mockTransaction),
1075                anyString(),
1076                eq(binaryDescId),
1077                contains("<http://some/predicate> \"xyz\"")
1078        );
1079    }
1080
1081    @Test(expected = BadRequestException.class)
1082    public void testPatchWithoutContent() throws MalformedRdfException, IOException {
1083        testObj.updateSparql(null);
1084    }
1085
1086    @Test(expected = BadRequestException.class)
1087    public void testPatchWithMissingContent() throws MalformedRdfException, IOException {
1088        setResource(Container.class);
1089        testObj.updateSparql(toInputStream("", UTF_8));
1090    }
1091
1092    @Test(expected = BadRequestException.class)
1093    public void testPatchBinary() throws MalformedRdfException, IOException {
1094        setResource(Binary.class);
1095        testObj.updateSparql(toInputStream("", UTF_8));
1096    }
1097
1098    @Test
1099    public void testCreateNewObject() throws Exception {
1100        final var resource = setResource(Container.class);
1101        final var finalId = pathId.resolve("b");
1102        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(resource);
1103        final Response actual = testObj.createObject(null, null, "b",
1104                emptyStream, null, null);
1105        assertEquals(CREATED.getStatusCode(), actual.getStatus());
1106    }
1107
1108    @Test
1109    public void testCreateNewObjectWithVersionedResource() throws Exception {
1110        final var resource = setResource(Container.class);
1111        final var finalId = pathId.resolve("b");
1112        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(resource);
1113        final String versionedResourceLink = "<" + VERSIONED_RESOURCE.getURI() + ">;rel=\"type\"";
1114        final Response actual = testObj.createObject(null, null, "b",
1115                toInputStream("", UTF_8), singletonList(versionedResourceLink), null);
1116        assertEquals(CREATED.getStatusCode(), actual.getStatus());
1117    }
1118
1119    @Test(expected = UnsupportedMediaTypeException.class)
1120    public void testCreateNewObjectWithSparql() throws Exception {
1121        final var resource = setResource(Container.class);
1122        final var finalId = pathId.resolve("b");
1123        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(resource);
1124        final Response actual = testObj.createObject(null,
1125                MediaType.valueOf(contentTypeSPARQLUpdate),
1126                "b",
1127                toInputStream("INSERT DATA { <> <http://example.org/somePredicate> \"x\" }", UTF_8),
1128                null,
1129                null);
1130    }
1131
1132    @Test
1133    public void testCreateNewObjectWithRdf() throws Exception {
1134        final var resource = setResource(Container.class);
1135        final var finalId = pathId.resolve("b");
1136        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(resource);
1137        final Response actual = testObj.createObject(null, NTRIPLES_TYPE, "b",
1138                toInputStream("_:a <info:b> _:c .", UTF_8), null, null);
1139        assertEquals(CREATED.getStatusCode(), actual.getStatus());
1140        verify(createResourceService).perform(
1141                eq(mockTransaction),
1142                anyString(),
1143                eq(finalId),
1144               any(),
1145               any(Model.class));
1146    }
1147
1148
1149    @Test
1150    public void testCreateNewBinary() throws MalformedRdfException, InvalidChecksumException,
1151           IOException, UnsupportedAlgorithmException, PathNotFoundException {
1152        setResource(Container.class);
1153        final var finalId = pathId.resolve("b");
1154        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(mockBinary);
1155        try (final InputStream content = toInputStream("x", UTF_8)) {
1156            final Response actual = testObj.createObject(null, APPLICATION_OCTET_STREAM_TYPE, "b", content,
1157                nonRDFSourceLink, null);
1158            assertEquals(CREATED.getStatusCode(), actual.getStatus());
1159            verify(createResourceService).perform(
1160                    eq(mockTransaction),
1161                    any(),
1162                    eq(finalId),
1163                    eq(APPLICATION_OCTET_STREAM),
1164                    eq(""),
1165                    anyLong(),
1166                    any(),
1167                    any(),
1168                    eq(content),
1169                    eq(null));
1170        }
1171    }
1172
1173    @Ignore("Insufficient space root exception not thrown and checkForInsufficientStorageException is not called")
1174    @Test(expected = InsufficientStorageException.class)
1175    public void testCreateNewBinaryWithInsufficientResources() throws Exception {
1176        final var finalId = pathId.resolve("b");
1177        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(mockBinary);
1178
1179        try (final InputStream content = toInputStream("x", UTF_8)) {
1180            final RuntimeException ex = new RuntimeException(new IOException("root exception", new IOException(
1181                    FedoraLdp.INSUFFICIENT_SPACE_IDENTIFYING_MESSAGE)));
1182            doThrow(ex).when(createResourceService).perform(any(), anyString(), eq(finalId), anyString(),
1183                    anyString(), any(Long.class), anyList(), isNull(), any(InputStream.class), isNull());
1184
1185            testObj.createObject(null, APPLICATION_OCTET_STREAM_TYPE, "b", content, nonRDFSourceLink, null);
1186        }
1187    }
1188
1189    @Test
1190    public void testCreateNewBinaryWithContentTypeWithParams() throws MalformedRdfException,
1191           InvalidChecksumException, IOException, UnsupportedAlgorithmException, PathNotFoundException {
1192
1193        setResource(Container.class);
1194        final var finalId = pathId.resolve("b");
1195        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(mockBinary);
1196        try (final InputStream content = toInputStream("x", UTF_8)) {
1197            final MediaType requestContentType = MediaType.valueOf("some/mime-type; with=some; param=s");
1198            final Response actual = testObj.createObject(null, requestContentType, "b", content, nonRDFSourceLink,
1199                null);
1200            assertEquals(CREATED.getStatusCode(), actual.getStatus());
1201            verify(createResourceService).perform(
1202                    any(),
1203                    any(),
1204                    eq(finalId),
1205                    eq(requestContentType.toString()),
1206                    eq(""),
1207                    anyLong(),
1208                    eq(nonRDFSourceLink),
1209                    any(),
1210                    eq(content),
1211                    eq(null));
1212        }
1213    }
1214
1215    @Test
1216    public void testCreateNewBinaryWithChecksumSHA() throws MalformedRdfException,
1217           InvalidChecksumException, IOException, UnsupportedAlgorithmException, PathNotFoundException {
1218
1219        final var finalId = pathId.resolve("b");
1220        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(mockBinary);
1221        try (final InputStream content = toInputStream("x", UTF_8)) {
1222            final MediaType requestContentType = MediaType.valueOf("some/mime-type; with=some; param=s");
1223            final String sha = "07a4d371f3b7b6283a8e1230b7ec6764f8287bf2";
1224            final String requestSHA = "sha=" + sha;
1225            final Set<URI> shaURI = singleton(URI.create("urn:sha1:" + sha));
1226            final Response actual = testObj.createObject(null, requestContentType, "b", content, nonRDFSourceLink,
1227                requestSHA);
1228            assertEquals(CREATED.getStatusCode(), actual.getStatus());
1229            verify(createResourceService).perform(
1230                    any(),
1231                    any(),
1232                    eq(finalId),
1233                    eq(requestContentType.toString()),
1234                    eq(""),
1235                    anyLong(),
1236                    eq(nonRDFSourceLink),
1237                    eq(shaURI),
1238                    eq(content),
1239                    eq(null));
1240        }
1241    }
1242
1243    @Test
1244    public void testCreateNewBinaryWithChecksumSHA256() throws MalformedRdfException,
1245        InvalidChecksumException, IOException, UnsupportedAlgorithmException, PathNotFoundException {
1246
1247        final var finalId = pathId.resolve("b");
1248        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(mockBinary);
1249        try (final InputStream content = toInputStream("x", UTF_8)) {
1250            final MediaType requestContentType = MediaType.valueOf("some/mime-type; with=some; param=s");
1251            final String sha = "73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac";
1252            final String requestSHA = "sha-256=" + sha;
1253            final Set<URI> shaURI = singleton(URI.create("urn:sha-256:" + sha));
1254            final Response actual = testObj.createObject(null, requestContentType, "b", content, nonRDFSourceLink,
1255                requestSHA);
1256            assertEquals(CREATED.getStatusCode(), actual.getStatus());
1257            verify(createResourceService).perform(
1258                    any(),
1259                    any(),
1260                    eq(finalId),
1261                    eq(requestContentType.toString()),
1262                    eq(""),
1263                    anyLong(),
1264                    eq(nonRDFSourceLink),
1265                    eq(shaURI),
1266                    eq(content),
1267                    eq(null));
1268        }
1269    }
1270
1271    @Test
1272    public void testCreateNewBinaryWithChecksumMD5() throws MalformedRdfException,
1273            InvalidChecksumException, IOException, UnsupportedAlgorithmException, PathNotFoundException {
1274
1275        final var finalId = pathId.resolve("b");
1276        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(mockBinary);
1277        try (final InputStream content = toInputStream("x", UTF_8)) {
1278            final MediaType requestContentType = MediaType.valueOf("some/mime-type; with=some; param=s");
1279            final String md5 = "HUXZLQLMuI/KZ5KDcJPcOA==";
1280            final String requestMD5 = "md5=" + md5;
1281            final Set<URI> md5URI = singleton(URI.create("urn:md5:" + md5));
1282            final Response actual = testObj.createObject(null, requestContentType, "b", content, nonRDFSourceLink,
1283                requestMD5);
1284            assertEquals(CREATED.getStatusCode(), actual.getStatus());
1285            verify(createResourceService).perform(
1286                    any(),
1287                    any(),
1288                    eq(finalId),
1289                    eq(requestContentType.toString()),
1290                    eq(""),
1291                    anyLong(),
1292                    eq(nonRDFSourceLink),
1293                    eq(md5URI),
1294                    eq(content),
1295                    eq(null));
1296        }
1297    }
1298
1299    @Test
1300    public void testCreateNewBinaryWithChecksumSHAandMD5() throws MalformedRdfException,
1301           InvalidChecksumException, IOException, UnsupportedAlgorithmException, PathNotFoundException {
1302
1303        final var finalId = pathId.resolve("b");
1304        when(resourceFactory.getResource((Transaction)any(), eq(finalId))).thenReturn(mockBinary);
1305        try (final InputStream content = toInputStream("x", UTF_8)) {
1306            final MediaType requestContentType = MediaType.valueOf("some/mime-type; with=some; param=s");
1307
1308            final String sha = "07a4d371f3b7b6283a8e1230b7ec6764f8287bf2";
1309            final String requestSHA = "sha=" + sha;
1310            final URI shaURI = URI.create("urn:sha1:" + sha);
1311
1312            final String md5 = "HUXZLQLMuI/KZ5KDcJPcOA==";
1313            final String requestMD5 = "md5=" + md5;
1314            final URI md5URI = URI.create("urn:md5:" + md5);
1315
1316            final String requestChecksum = requestSHA + "," + requestMD5;
1317            final HashSet<URI> checksumURIs = new HashSet<>(asList(shaURI, md5URI));
1318
1319            final Response actual = testObj.createObject(null, requestContentType, "b", content, nonRDFSourceLink,
1320                requestChecksum);
1321            assertEquals(CREATED.getStatusCode(), actual.getStatus());
1322            verify(createResourceService).perform(
1323                    any(),
1324                    any(),
1325                    eq(finalId),
1326                    eq(requestContentType.toString()),
1327                    eq(""),
1328                    anyLong(),
1329                    eq(nonRDFSourceLink),
1330                    eq(checksumURIs),
1331                    eq(content),
1332                    eq(null));
1333        }
1334    }
1335
1336    @Test(expected = CannotCreateResourceException.class)
1337    public void testLDPRNotImplemented() throws Exception {
1338        final var resource = setResource(Container.class);
1339        when(resourceFactory.getResource(mockTransaction, pathId.resolve("x"))).thenReturn(resource);
1340        testObj.createObject(null, null, "x", null,
1341                singletonList("<http://www.w3.org/ns/ldp#Resource>; rel=\"type\""), null);
1342    }
1343
1344    @Test(expected = ClientErrorException.class)
1345    public void testLDPRNotImplementedInvalidLink() throws Exception {
1346        final var resource = setResource(Container.class);
1347        when(resourceFactory.getResource(mockTransaction, pathId.resolve("x"))).thenReturn(resource);
1348        testObj.createObject(null, null, "x", null, singletonList("<http://foo;rel=\"type\""), null);
1349    }
1350
1351    @Test
1352    public void testGetSimpleContentType() {
1353        final MediaType mediaType = new MediaType("text", "plain", ImmutableMap.of("charset", "UTF-8"));
1354        final MediaType sanitizedMediaType = MediaType.valueOf(getSimpleContentType(mediaType));
1355        assertEquals("text/plain", sanitizedMediaType.toString());
1356    }
1357
1358}