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 */
018
019package org.fcrepo.kernel.impl.observer;
020
021import com.google.common.eventbus.EventBus;
022
023import org.fcrepo.config.AuthPropsConfig;
024import org.fcrepo.config.ServerManagedPropsMode;
025import org.fcrepo.kernel.api.Transaction;
026import org.fcrepo.kernel.api.exception.PathNotFoundException;
027import org.fcrepo.kernel.api.identifiers.FedoraId;
028import org.fcrepo.kernel.api.models.FedoraResource;
029import org.fcrepo.kernel.api.models.ResourceFactory;
030import org.fcrepo.kernel.api.observer.Event;
031import org.fcrepo.kernel.api.observer.EventType;
032import org.fcrepo.kernel.api.operations.ResourceOperation;
033import org.fcrepo.kernel.impl.operations.DeleteResourceOperationFactoryImpl;
034import org.fcrepo.kernel.impl.operations.RdfSourceOperationFactoryImpl;
035import org.hamcrest.Description;
036import org.hamcrest.Matcher;
037import org.hamcrest.TypeSafeMatcher;
038import org.junit.Before;
039import org.junit.Test;
040import org.junit.runner.RunWith;
041import org.mockito.ArgumentCaptor;
042import org.mockito.Mock;
043import org.mockito.Mockito;
044import org.mockito.junit.MockitoJUnitRunner;
045
046import java.net.URI;
047import java.net.URISyntaxException;
048import java.util.Arrays;
049import java.util.Objects;
050import java.util.Set;
051
052import static org.fcrepo.kernel.api.RdfLexicon.RDF_SOURCE;
053import static org.hamcrest.MatcherAssert.assertThat;
054import static org.hamcrest.Matchers.contains;
055import static org.hamcrest.Matchers.containsInAnyOrder;
056import static org.junit.Assert.assertEquals;
057import static org.mockito.ArgumentMatchers.any;
058import static org.mockito.ArgumentMatchers.eq;
059import static org.mockito.Mockito.times;
060import static org.mockito.Mockito.verify;
061import static org.mockito.Mockito.when;
062import static org.springframework.test.util.ReflectionTestUtils.setField;
063
064/**
065 * @author pwinckles
066 */
067@RunWith(MockitoJUnitRunner.class)
068public class EventAccumulatorImplTest {
069
070    private static final String TX_ID = "tx1";
071    private static final String BASE_URL = "http://localhost/rest";
072    private static final String USER_AGENT = "user-agent";
073    private static final String USER = "user";
074
075    private static final URI CONTAINER_TYPE = uri("http://www.w3.org/ns/ldp#Container");
076    private static final URI RESOURCE_TYPE = uri("http://fedora.info/definitions/v4/repository#Resource");
077    private static final URI RDF_TYPE = uri("http://www.w3.org/ns/ldp#RDFSource");
078
079    private EventAccumulatorImpl accumulator;
080
081    @Mock
082    private ResourceFactory resourceFactory;
083
084    @Mock
085    private EventBus eventBus;
086
087    private Transaction transaction;
088
089    private ArgumentCaptor<Event> eventCaptor;
090
091    private AuthPropsConfig authPropsConfig;
092
093    @Before
094    public void setup() {
095        authPropsConfig = new AuthPropsConfig();
096        accumulator = new EventAccumulatorImpl();
097        transaction = mockTransaction(TX_ID);
098        setField(accumulator, "resourceFactory", resourceFactory);
099        setField(accumulator, "eventBus", eventBus);
100        setField(accumulator, "authPropsConfig", authPropsConfig);
101        eventCaptor = ArgumentCaptor.forClass(Event.class);
102    }
103
104    @Test
105    public void emitEventsWhenEventsOnTransactionNoMerge() throws PathNotFoundException {
106        final var fId1 = FedoraId.create("/test/1");
107        final var fId2 = FedoraId.create("/test/2");
108
109        final var op1 = createOp(fId1);
110        final var op2 = updateOp(fId2);
111
112        accumulator.recordEventForOperation(transaction, fId1, op1);
113        accumulator.recordEventForOperation(transaction, fId2, op2);
114
115        expectResource(fId1, CONTAINER_TYPE);
116        expectResource(fId2, CONTAINER_TYPE, RESOURCE_TYPE);
117
118        accumulator.emitEvents(transaction, BASE_URL, USER_AGENT);
119
120        verify(eventBus, times(2)).post(eventCaptor.capture());
121
122        final var events = eventCaptor.getAllValues();
123
124        assertThat(events, containsInAnyOrder(
125                defaultEvent(fId1, Set.of(EventType.RESOURCE_CREATION), Set.of(CONTAINER_TYPE.toString())),
126                defaultEvent(fId2, Set.of(EventType.RESOURCE_MODIFICATION),
127                        Set.of(CONTAINER_TYPE.toString(), RESOURCE_TYPE.toString()))
128        ));
129    }
130
131    @Test
132    public void emitEventsWhenEventsOnTransactionWithMerge() throws PathNotFoundException {
133        final var fId1 = FedoraId.create("/test/1");
134        final var fId2 = FedoraId.create("/test/2");
135
136        final var op1 = createOp(fId1);
137        final var op2 = updateOp(fId2);
138        final var op3 = updateOp(fId1);
139
140        accumulator.recordEventForOperation(transaction, fId1, op1);
141        accumulator.recordEventForOperation(transaction, fId2, op2);
142        accumulator.recordEventForOperation(transaction, fId1, op3);
143
144        expectResource(fId1, CONTAINER_TYPE, RDF_TYPE);
145        expectResource(fId2, CONTAINER_TYPE, RESOURCE_TYPE);
146
147        accumulator.emitEvents(transaction, BASE_URL, USER_AGENT);
148
149        verify(eventBus, times(2)).post(eventCaptor.capture());
150
151        final var events = eventCaptor.getAllValues();
152
153        assertThat(events, containsInAnyOrder(
154                defaultEvent(fId1, Set.of(EventType.RESOURCE_CREATION, EventType.RESOURCE_MODIFICATION),
155                        Set.of(CONTAINER_TYPE.toString(), RDF_TYPE.toString())),
156                defaultEvent(fId2, Set.of(EventType.RESOURCE_MODIFICATION),
157                        Set.of(CONTAINER_TYPE.toString(), RESOURCE_TYPE.toString()))
158        ));
159    }
160
161    @Test
162    public void onlyEmitEventsForSameSpecifiedTransaction() throws PathNotFoundException {
163        final var fId1 = FedoraId.create("/test/1");
164        final var fId2 = FedoraId.create("/test/2");
165
166        final var op1 = createOp(fId1);
167        final var op2 = updateOp(fId2);
168
169        final Transaction transaction2 = mockTransaction("tx2");
170
171        accumulator.recordEventForOperation(transaction, fId1, op1);
172        accumulator.recordEventForOperation(transaction2, fId2, op2);
173
174        expectResource(fId2, CONTAINER_TYPE);
175
176        accumulator.emitEvents(transaction2, BASE_URL, USER_AGENT);
177
178        verify(eventBus, times(1)).post(eventCaptor.capture());
179
180        final var events = eventCaptor.getAllValues();
181
182        assertThat(events, contains(
183                defaultEvent(fId2, Set.of(EventType.RESOURCE_MODIFICATION),
184                        Set.of(CONTAINER_TYPE.toString()))
185        ));
186    }
187
188    @Test
189    public void doNothingWhenTransactionHasNoEvents() throws PathNotFoundException {
190        final var fId1 = FedoraId.create("/test/1");
191        final var fId2 = FedoraId.create("/test/2");
192
193        final var op1 = createOp(fId1);
194        final var op2 = updateOp(fId2);
195
196        final Transaction transaction2 = mockTransaction("tx2");
197        final Transaction transaction3 = mockTransaction("tx3");
198
199        accumulator.recordEventForOperation(transaction, fId1, op1);
200        accumulator.recordEventForOperation(transaction2, fId2, op2);
201
202        expectResource(fId2, CONTAINER_TYPE);
203
204        accumulator.emitEvents(transaction3, BASE_URL, USER_AGENT);
205
206        verify(eventBus, times(0)).post(eventCaptor.capture());
207
208        final var events = eventCaptor.getAllValues();
209
210        assertEquals(0, events.size());
211    }
212
213    @Test
214    public void clearTransactionEventsWhenCleared() throws PathNotFoundException {
215        final var fId1 = FedoraId.create("/test/1");
216        final var fId2 = FedoraId.create("/test/2");
217
218        final var op1 = createOp(fId1);
219        final var op2 = updateOp(fId2);
220        final var op3 = updateOp(fId1);
221
222        accumulator.recordEventForOperation(transaction, fId1, op1);
223        accumulator.recordEventForOperation(transaction, fId2, op2);
224        accumulator.recordEventForOperation(transaction, fId1, op3);
225
226        expectResource(fId1, CONTAINER_TYPE, RDF_TYPE);
227        expectResource(fId2, CONTAINER_TYPE, RESOURCE_TYPE);
228
229        accumulator.clearEvents(transaction);
230
231        accumulator.emitEvents(transaction, BASE_URL, USER_AGENT);
232
233        verify(eventBus, times(0)).post(eventCaptor.capture());
234
235        final var events = eventCaptor.getAllValues();
236        assertEquals(0, events.size());
237    }
238
239    @Test
240    public void nonDefaultValues() throws PathNotFoundException {
241        final var tx = "tx4";
242        final Transaction transaction4 = mockTransaction(tx);
243        final var url = "http://example.com/rest";
244        final var agent = "me";
245
246        final var fId1 = FedoraId.create("/example/1");
247        final var fId2 = FedoraId.create("/example/2");
248
249        final var op1 = createOp(fId1);
250        final var op2 = updateOp(fId2);
251        final var op3 = deleteOp(fId1);
252
253        accumulator.recordEventForOperation(transaction4, fId1, op1);
254        accumulator.recordEventForOperation(transaction4, fId2, op2);
255        accumulator.recordEventForOperation(transaction4, fId1, op3);
256
257        expectResource(fId1, RDF_TYPE);
258        expectResource(fId2, RESOURCE_TYPE);
259
260        accumulator.emitEvents(transaction4, url, agent);
261
262        verify(eventBus, times(2)).post(eventCaptor.capture());
263
264        final var events = eventCaptor.getAllValues();
265
266        assertThat(events, containsInAnyOrder(
267                event(fId1, Set.of(EventType.RESOURCE_CREATION, EventType.RESOURCE_DELETION),
268                        Set.of(RDF_TYPE.toString()), url, agent),
269                event(fId2, Set.of(EventType.RESOURCE_MODIFICATION),
270                        Set.of(RESOURCE_TYPE.toString()), url, agent)
271        ));
272    }
273
274    @Test
275    public void doNotSetResourceTypesWhenCannotLoad() throws PathNotFoundException {
276        final var fId1 = FedoraId.create("/test/1");
277        final var fId2 = FedoraId.create("/test/2");
278        final var fId3 = FedoraId.create("/test/3");
279
280        final var op1 = createOp(fId1);
281        final var op2 = deleteOp(fId2);
282        final var op3 = updateOp(fId3);
283
284        accumulator.recordEventForOperation(transaction, fId1, op1);
285        accumulator.recordEventForOperation(transaction, fId2, op2);
286        accumulator.recordEventForOperation(transaction, fId3, op3);
287
288        expectResource(fId1, CONTAINER_TYPE);
289        when(resourceFactory.getResource(any(Transaction.class), eq(fId2))).thenThrow(new PathNotFoundException("not " +
290                "found"));
291        expectResource(fId3, RESOURCE_TYPE);
292
293        accumulator.emitEvents(transaction, BASE_URL, USER_AGENT);
294
295        verify(eventBus, times(3)).post(eventCaptor.capture());
296
297        final var events = eventCaptor.getAllValues();
298
299        assertThat(events, containsInAnyOrder(
300                defaultEvent(fId1, Set.of(EventType.RESOURCE_CREATION),
301                        Set.of(CONTAINER_TYPE.toString())),
302                defaultEvent(fId2, Set.of(EventType.RESOURCE_DELETION),
303                        Set.of()),
304                defaultEvent(fId3, Set.of(EventType.RESOURCE_MODIFICATION),
305                        Set.of(RESOURCE_TYPE.toString()))
306        ));
307    }
308
309    private ResourceOperation createOp(final FedoraId fedoraId) {
310        return new RdfSourceOperationFactoryImpl().createBuilder(transaction, fedoraId, RDF_SOURCE.toString(),
311                ServerManagedPropsMode.RELAXED)
312                .userPrincipal(USER)
313                .build();
314    }
315
316    private ResourceOperation updateOp(final FedoraId fedoraId) {
317        return new RdfSourceOperationFactoryImpl().updateBuilder(transaction, fedoraId, ServerManagedPropsMode.RELAXED)
318                .userPrincipal(USER)
319                .build();
320    }
321
322    private ResourceOperation deleteOp(final FedoraId fedoraId) {
323        return new DeleteResourceOperationFactoryImpl().deleteBuilder(transaction, fedoraId)
324                .userPrincipal(USER)
325                .build();
326    }
327
328    private void expectResource(final FedoraId fedoraId, final URI... types) throws PathNotFoundException {
329        final var resource = mockResource(types);
330        when(resourceFactory.getResource(any(Transaction.class), eq(fedoraId))).thenReturn(resource);
331    }
332
333    private FedoraResource mockResource(final URI... types) {
334        final var resource = Mockito.mock(FedoraResource.class);
335        when(resource.getTypes()).thenReturn(Arrays.asList(types));
336        return resource;
337    }
338
339    private static URI uri(final String uri) {
340        try {
341            return new URI(uri);
342        } catch (final URISyntaxException e) {
343            throw new RuntimeException(e);
344        }
345    }
346
347    private static Matcher<Event> defaultEvent(final FedoraId fedoraId,
348                                               final Set<EventType> eventTypes,
349                                               final Set<String> resourceTypes) {
350        return event(fedoraId, eventTypes, resourceTypes, BASE_URL, USER_AGENT);
351    }
352
353    private static Matcher<Event> event(final FedoraId fedoraId,
354                                        final Set<EventType> eventTypes,
355                                        final Set<String> resourceTypes,
356                                        final String baseUrl,
357                                        final String userAgent) {
358        return new TypeSafeMatcher<>() {
359            @Override
360            protected boolean matchesSafely(final Event item) {
361                if (! Objects.equals(item.getFedoraId(), fedoraId)) {
362                    return false;
363                } else if (! Objects.equals(item.getTypes(), eventTypes)) {
364                    return false;
365                } else if (! Objects.equals(item.getResourceTypes(), resourceTypes)) {
366                    return false;
367                } else if (! Objects.equals(item.getBaseUrl(), baseUrl)) {
368                    return false;
369                } else if (! Objects.equals(item.getUserAgent(), userAgent)) {
370                    return false;
371                }
372                return true;
373            }
374
375            @Override
376            public void describeTo(final Description description) {
377                description.appendText("fedoraId=").appendValue(fedoraId)
378                        .appendText(", eventTypes=").appendValue(eventTypes)
379                        .appendText(", resourceTyps=").appendValue(resourceTypes)
380                        .appendText(", baseUrl=").appendValue(baseUrl)
381                        .appendText(", userAgent=").appendValue(userAgent);
382            }
383        };
384    }
385
386    /**
387     * Create a mock transaction.
388     * @param transactionId the id of the transaction
389     * @return the mock transaction.
390     */
391    private static Transaction mockTransaction(final String transactionId) {
392        final var transaction = Mockito.mock(Transaction.class);
393        when(transaction.getId()).thenReturn(transactionId);
394        return transaction;
395    }
396}