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