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}