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.http.api;
019
020import static javax.ws.rs.core.Response.Status.METHOD_NOT_ALLOWED;
021import static javax.ws.rs.core.Response.Status.OK;
022import static org.apache.jena.graph.Node.ANY;
023import static org.apache.jena.graph.NodeFactory.createLiteral;
024import static javax.ws.rs.core.Response.Status.CREATED;
025
026import static org.fcrepo.kernel.api.FedoraTypes.FCR_FIXITY;
027import static org.fcrepo.kernel.api.RdfLexicon.HAS_FIXITY_RESULT;
028import static org.fcrepo.kernel.api.RdfLexicon.HAS_FIXITY_STATE;
029import static org.fcrepo.kernel.api.RdfLexicon.HAS_MESSAGE_DIGEST;
030import static org.fcrepo.kernel.api.RdfLexicon.HAS_SIZE;
031import static org.junit.Assert.assertEquals;
032import static org.junit.Assert.assertNotNull;
033import static org.junit.Assert.assertTrue;
034
035import java.io.IOException;
036import java.nio.charset.StandardCharsets;
037import java.nio.file.Files;
038import java.nio.file.StandardOpenOption;
039import java.util.Iterator;
040import java.util.Map;
041import java.util.stream.Collectors;
042
043import edu.wisc.library.ocfl.api.OcflRepository;
044import edu.wisc.library.ocfl.api.model.ObjectVersionId;
045import org.apache.http.client.methods.CloseableHttpResponse;
046import org.apache.http.client.methods.HttpGet;
047
048import org.apache.http.client.methods.HttpPost;
049import org.apache.http.client.methods.HttpPut;
050import org.apache.jena.sparql.core.Quad;
051import org.fcrepo.config.OcflPropsConfig;
052import org.fcrepo.http.commons.test.util.CloseableDataset;
053
054import org.fcrepo.kernel.api.identifiers.FedoraId;
055import org.junit.Before;
056import org.junit.Test;
057
058import org.apache.jena.datatypes.RDFDatatype;
059import org.apache.jena.datatypes.TypeMapper;
060import org.apache.jena.sparql.core.DatasetGraph;
061import org.springframework.test.context.TestExecutionListeners;
062
063/**
064 * <p>FedoraFixityIT class.</p>
065 *
066 * @author awoods
067 * @author ajs6f
068 */
069@TestExecutionListeners(
070        listeners = { TestIsolationExecutionListener.class },
071        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
072public class FedoraFixityIT extends AbstractResourceIT {
073
074    private static final RDFDatatype IntegerType = TypeMapper.getInstance().getTypeByClass(Integer.class);
075
076    private OcflPropsConfig ocflConfig;
077    private OcflRepository ocflRepo;
078
079    @Before
080    public void setup() {
081        ocflConfig = getBean(OcflPropsConfig.class);
082        ocflRepo = getBean(OcflRepository.class);
083    }
084
085    @Test
086    public void testCheckDatastreamFixity() throws IOException {
087        final String id = getRandomUniqueId();
088        createObjectAndClose(id);
089        createDatastream(id, "zxc", "foo");
090
091        final String digestValue = "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33";
092
093        final HttpGet httpGet = getObjMethod(id + "/zxc");
094        httpGet.setHeader("Want-Digest", "sha");
095        try (final CloseableHttpResponse response = execute(httpGet)) {
096            assertEquals(OK.getStatusCode(), getStatus(response));
097            assertDigestEquals(response, "sha", digestValue);
098        }
099    }
100
101    @Test
102    public void testCheckDatastreamFixityMD5() throws IOException {
103        final String id = getRandomUniqueId();
104        final String digestValue = "888c09337d6869fa48bbeff802ddd05a";
105
106        final HttpPut put = putObjMethod(id, "text/plain", "text-body");
107        put.setHeader("Digest", "md5=" + digestValue);
108        assertEquals("Did not create successfully!", CREATED.getStatusCode(), getStatus(put));
109
110        final HttpGet getFixity = getObjMethod(id);
111        getFixity.setHeader("Want-Digest", "md5");
112        try (final CloseableHttpResponse response = execute(getFixity)) {
113            assertEquals(OK.getStatusCode(), getStatus(response));
114            assertDigestEquals(response, "md5", digestValue);
115        }
116    }
117
118    @Test
119    public void testMultipleAlgorithms() throws IOException {
120        final String id = getRandomUniqueId();
121        final String payload = "This is some wonderful test text payload";
122        final Map<String, String> shas = Map.of("sha","dce87c75d05bb3893ff5e72f0596754bcf47cfed",
123            "sha-256", "55bf3e1931dffbf16fa7bfb80c000c04e6f17c75471a622af3b11a3511e17843",
124            "md5", "6ddfc43540f3e0abdb5c54ca832bc9e3");
125
126        final HttpPut httpPut = putObjMethod(id,"text/plain", payload);
127        httpPut.setHeader("Digest", shas.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue())
128                .collect(Collectors.joining(",")));
129        assertEquals(CREATED.getStatusCode(), getStatus(httpPut));
130
131        final HttpGet httpGet = getObjMethod(id);
132        httpGet.setHeader("Want-Digest", String.join(",", shas.keySet()));
133        try (final CloseableHttpResponse response = execute(httpGet)) {
134            assertEquals(OK.getStatusCode(), getStatus(response));
135            for (final Map.Entry<String, String> digest : shas.entrySet()) {
136                assertDigestEquals(response, digest.getKey(), digest.getValue());
137            }
138        }
139
140    }
141
142    @Test
143    public void testBinaryFixity() throws IOException {
144        final String id = getRandomUniqueId();
145        final FedoraId resourceId = FedoraId.create(id);
146        final String uri = createDatastream(id, "foo");
147
148        try (final CloseableDataset dataset =
149                     getDataset(new HttpGet(uri + "/" + FCR_FIXITY))) {
150            final DatasetGraph graphStore = dataset.asDatasetGraph();
151            logger.debug("Got binary content fixity triples {}", graphStore);
152            final Iterator<Quad> stmtIt = graphStore.find(ANY, ANY, HAS_FIXITY_RESULT.asNode(), ANY);
153            assertTrue(stmtIt.hasNext());
154            assertTrue(graphStore.contains(ANY, ANY, HAS_FIXITY_STATE.asNode(), createLiteral("SUCCESS")));
155            assertTrue(graphStore.contains(ANY, ANY, HAS_MESSAGE_DIGEST.asNode(), ANY));
156            assertTrue(graphStore.contains(ANY, ANY, HAS_SIZE.asNode(), createLiteral("3", IntegerType)));
157        }
158
159
160        final var ocflDesc = ocflRepo.describeVersion(
161                ObjectVersionId.head(resourceId.getResourceId()));
162        final var storagePath = ocflDesc.getFile(id).getStorageRelativePath();
163        Files.writeString(ocflConfig.getOcflRepoRoot().resolve(storagePath),
164                "corrupted!", StandardCharsets.UTF_8, StandardOpenOption.APPEND);
165
166        try (final CloseableDataset dataset =
167                     getDataset(new HttpGet(uri + "/" + FCR_FIXITY))) {
168            final DatasetGraph graphStore = dataset.asDatasetGraph();
169            logger.debug("Got binary content fixity triples {}", graphStore);
170            final Iterator<Quad> stmtIt = graphStore.find(ANY, ANY, HAS_FIXITY_RESULT.asNode(), ANY);
171            assertTrue(stmtIt.hasNext());
172            assertTrue(graphStore.contains(ANY, ANY, HAS_FIXITY_STATE.asNode(), createLiteral("BAD_CHECKSUM")));
173            assertTrue(graphStore.contains(ANY, ANY, HAS_MESSAGE_DIGEST.asNode(), ANY));
174            assertTrue(graphStore.contains(ANY, ANY, HAS_SIZE.asNode(), createLiteral("3", IntegerType)));
175        }
176    }
177
178
179    @Test
180    public void testBinaryVersionFixity() throws IOException {
181        final String id = getRandomUniqueId();
182        createDatastream(id,"foo");
183
184        logger.debug("Creating binary content version ...");
185        final String mementoUri = postVersion(id);
186
187        try (final CloseableDataset dataset =
188                     getDataset(new HttpGet(mementoUri + "/" + FCR_FIXITY))) {
189            final DatasetGraph graphStore = dataset.asDatasetGraph();
190            logger.debug("Got binary content versioned fixity triples {}", graphStore);
191            final Iterator<Quad> stmtIt = graphStore.find(ANY, ANY, HAS_FIXITY_RESULT.asNode(), ANY);
192            assertTrue(stmtIt.hasNext());
193            assertTrue(graphStore.contains(ANY, ANY, HAS_FIXITY_STATE.asNode(), createLiteral("SUCCESS")));
194            assertTrue(graphStore.contains(ANY, ANY, HAS_MESSAGE_DIGEST.asNode(), ANY));
195            assertTrue(graphStore.contains(ANY, ANY, HAS_SIZE.asNode(), createLiteral("3", IntegerType)));
196        }
197    }
198
199    @Test
200    public void testInvalidMethods() throws Exception {
201        final String id = getRandomUniqueId();
202        final String fixityId = id + "/" + FCR_FIXITY;
203        createDatastream(id,"foo");
204        assertEquals(METHOD_NOT_ALLOWED.getStatusCode(), getStatus(patchObjMethod(fixityId)));
205        assertEquals(METHOD_NOT_ALLOWED.getStatusCode(), getStatus(deleteObjMethod(fixityId)));
206        assertEquals(METHOD_NOT_ALLOWED.getStatusCode(), getStatus(putObjMethod(fixityId)));
207        assertEquals(METHOD_NOT_ALLOWED.getStatusCode(), getStatus(postObjMethod(fixityId)));
208        assertEquals(METHOD_NOT_ALLOWED.getStatusCode(), getStatus(headObjMethod(fixityId)));
209    }
210
211    private static String postVersion(final String path) throws IOException {
212        logger.debug("Posting version");
213        final HttpPost postVersion = postObjMethod(path + "/fcr:versions");
214        try (final CloseableHttpResponse response = execute(postVersion)) {
215            assertEquals(CREATED.getStatusCode(), getStatus(response));
216            final String locationHeader = getLocation(response);
217            assertNotNull("No version location header found", locationHeader);
218            return locationHeader;
219        }
220    }
221
222    private static void assertDigestEquals(final CloseableHttpResponse response, final String digestName,
223                                    final String digestValue) {
224        final String digest = response.getFirstHeader("Digest").getValue();
225        final Map<String, String> digestHeaders = decodeDigestHeader(digest);
226        assertTrue(digestHeaders.containsKey(digestName));
227        assertEquals("Mismatch on " + digestName + " value", digestValue, digestHeaders.get(digestName));
228    }
229
230}