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