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 */
006package org.fcrepo.kernel.impl.services;
007
008import static org.apache.jena.rdf.model.ResourceFactory.createProperty;
009import static org.fcrepo.kernel.api.RdfLexicon.BASIC_CONTAINER;
010import static org.fcrepo.kernel.api.rdf.DefaultRdfStream.fromModel;
011import static org.junit.Assert.assertEquals;
012import static org.junit.Assert.assertNotEquals;
013import static org.junit.Assert.assertNotNull;
014import static org.junit.Assert.assertNull;
015import static org.junit.Assert.assertTrue;
016import static org.mockito.ArgumentMatchers.any;
017import static org.mockito.ArgumentMatchers.eq;
018import static org.mockito.ArgumentMatchers.isNull;
019import static org.mockito.ArgumentMatchers.nullable;
020import static org.mockito.Mockito.when;
021import static org.springframework.test.util.ReflectionTestUtils.setField;
022
023import java.time.Instant;
024import java.util.Arrays;
025import java.util.List;
026import java.util.UUID;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import javax.inject.Inject;
031
032import org.fcrepo.config.OcflPropsConfig;
033import org.fcrepo.kernel.api.ContainmentIndex;
034import org.fcrepo.kernel.api.RdfLexicon;
035import org.fcrepo.kernel.api.RdfStream;
036import org.fcrepo.kernel.api.ReadOnlyTransaction;
037import org.fcrepo.kernel.api.Transaction;
038import org.fcrepo.kernel.api.identifiers.FedoraId;
039import org.fcrepo.kernel.api.models.ResourceHeaders;
040import org.fcrepo.kernel.api.rdf.DefaultRdfStream;
041import org.fcrepo.kernel.api.services.MembershipService;
042import org.fcrepo.kernel.impl.TestTransactionHelper;
043import org.fcrepo.persistence.api.PersistentStorageSession;
044import org.fcrepo.persistence.api.PersistentStorageSessionManager;
045import org.fcrepo.persistence.api.exceptions.PersistentItemNotFoundException;
046import org.fcrepo.persistence.common.ResourceHeadersImpl;
047
048import org.apache.jena.graph.NodeFactory;
049import org.apache.jena.graph.Triple;
050import org.apache.jena.rdf.model.Model;
051import org.apache.jena.rdf.model.ModelFactory;
052import org.apache.jena.rdf.model.Property;
053import org.apache.jena.rdf.model.Resource;
054import org.flywaydb.test.FlywayTestExecutionListener;
055import org.flywaydb.test.annotation.FlywayTest;
056import org.junit.Before;
057import org.junit.Test;
058import org.junit.runner.RunWith;
059import org.mockito.Mock;
060import org.mockito.MockitoAnnotations;
061import org.mockito.invocation.InvocationOnMock;
062import org.mockito.stubbing.Answer;
063import org.springframework.test.context.ContextConfiguration;
064import org.springframework.test.context.TestExecutionListeners;
065import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
066import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
067
068/**
069 * @author bbpennel
070 */
071@RunWith(SpringJUnit4ClassRunner.class)
072@ContextConfiguration("/membershipServiceTest.xml")
073@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, FlywayTestExecutionListener.class })
074public class MembershipServiceImplTest {
075
076    private final static Instant CREATED_DATE = Instant.parse("2019-11-12T10:00:30.0Z");
077
078    private final static String CREATED_BY = "user1";
079
080    private final static Instant LAST_MODIFIED_DATE = Instant.parse("2019-11-12T14:11:05.0Z");
081
082    private final static Instant LAST_MODIFIED_DATE2 = Instant.parse("2019-11-12T16:10:00.0Z");
083
084    private final static String LAST_MODIFIED_BY = "user2";
085
086    private final static String STATE_TOKEN = "stately_value";
087
088    private final static Property MEMBER_OF = createProperty("http://example.com/memberOf");
089
090    private final static Property OTHER_MEMBER_OF = createProperty("http://example.com/otherMemberOf");
091
092    private final static Property OTHER_HAS_MEMBER = createProperty("http://example.com/anotherHasMember");
093
094    public static final Property PROXY_FOR = createProperty("http://example.com/proxyFor");
095
096    @Inject
097    private PersistentStorageSessionManager pSessionManager;
098    @Mock
099    private PersistentStorageSession psSession;
100    @Inject
101    private MembershipService membershipService;
102    @Inject
103    private MembershipIndexManager indexManager;
104    @Inject
105    private ContainmentIndex containmentIndex;
106    @Inject
107    private OcflPropsConfig propsConfig;
108
109    private Transaction transaction;
110
111    private Transaction shortLivedTx;
112
113    private final FedoraId rootId = FedoraId.getRepositoryRootId();
114
115    private FedoraId membershipRescId;
116
117    private String txId;
118    private String shortLivedTxId;
119
120    private Transaction readOnlyTx;
121
122    @Before
123    @FlywayTest
124    public void setup() {
125        MockitoAnnotations.openMocks(this);
126
127        txId = UUID.randomUUID().toString();
128        transaction = TestTransactionHelper.mockTransaction(txId, false);
129        shortLivedTxId = UUID.randomUUID().toString();
130        shortLivedTx = TestTransactionHelper.mockTransaction(shortLivedTxId, true);
131
132        when(pSessionManager.getSession(transaction)).thenReturn(psSession);
133        when(pSessionManager.getSession(shortLivedTx)).thenReturn(psSession);
134
135        mockGetHeaders(populateHeaders(rootId, BASIC_CONTAINER));
136        when(psSession.getTriples(any(FedoraId.class), nullable(Instant.class))).thenAnswer(new Answer<RdfStream>() {
137            @Override
138            public RdfStream answer(final InvocationOnMock invocation) throws Throwable {
139                final var fedoraId = (FedoraId) invocation.getArgument(0);
140                final var subject = NodeFactory.createURI(fedoraId.getFullId());
141                return new DefaultRdfStream(subject, Stream.empty());
142            }
143        });
144
145        membershipRescId = mintFedoraId();
146
147        setField(propsConfig, "autoVersioningEnabled", Boolean.TRUE);
148        readOnlyTx = ReadOnlyTransaction.INSTANCE;
149    }
150
151    @Test
152    public void getMembers_NoMembership() throws Exception {
153        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
154        containmentIndex.addContainedBy(transaction, rootId, membershipRescId);
155        membershipService.resourceCreated(transaction, membershipRescId);
156
157        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
158
159        assertNull(membershipService.getLastUpdatedTimestamp(transaction, membershipRescId));
160    }
161
162    @Test
163    public void getMembers_WithDC_NoMembers() throws Exception {
164        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
165        membershipService.resourceCreated(transaction, membershipRescId);
166
167        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
168        membershipService.resourceCreated(transaction, dcId);
169
170        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
171
172        assertNull(membershipService.getLastUpdatedTimestamp(transaction, membershipRescId));
173    }
174
175    @Test
176    public void getMembers_WithDC_AddedMembers_HasMemberRelation() throws Exception {
177        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
178        membershipService.resourceCreated(transaction, membershipRescId);
179
180        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
181        membershipService.resourceCreated(transaction, dcId);
182
183        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
184        final var member2Id = createDCMember(dcId, RdfLexicon.NON_RDF_SOURCE);
185
186        assertHasMembers(transaction, membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
187
188        final var lastUpdated = membershipService.getLastUpdatedTimestamp(transaction, membershipRescId);
189        assertNotNull(lastUpdated);
190
191        // Commit the transaction and verify we can still get the added members
192        membershipService.commitTransaction(transaction);
193
194        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
195        assertEquals("Last updated timestamp should not change during commit",
196                lastUpdated, membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipRescId));
197    }
198
199    @Test
200    public void getMembers_WithDC_AddedMembers_DefaultHasMemberRelation() throws Exception {
201        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
202        membershipService.resourceCreated(transaction, membershipRescId);
203
204        // Don't specify a membership relation
205        final var dcId = createDirectContainer(membershipRescId, null, false);
206        membershipService.resourceCreated(transaction, dcId);
207
208        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
209
210        assertHasMembers(transaction, membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
211    }
212
213    @Test
214    public void getMembers_WithDC_AddedMembers_IsMemberOfRelation() throws Exception {
215        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
216        membershipService.resourceCreated(transaction, membershipRescId);
217
218        final var dcId = createDirectContainer(membershipRescId, MEMBER_OF, true);
219        membershipService.resourceCreated(transaction, dcId);
220
221        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
222        final var member2Id = createDCMember(dcId, RdfLexicon.NON_RDF_SOURCE);
223
224        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
225        assertIsMemberOf(transaction, member1Id, MEMBER_OF, membershipRescId);
226        assertIsMemberOf(transaction, member2Id, MEMBER_OF, membershipRescId);
227
228        final var member1Updated = membershipService.getLastUpdatedTimestamp(transaction, member1Id);
229        assertNotNull(member1Updated);
230        final var member2Updated = membershipService.getLastUpdatedTimestamp(transaction, member2Id);
231        assertNotNull(member2Updated);
232        assertNull("No membership expected for the membership resource",
233                membershipService.getLastUpdatedTimestamp(transaction, membershipRescId));
234
235        // Commit the transaction and verify we can still get the added members
236        membershipService.commitTransaction(transaction);
237
238        assertCommittedMembershipCount(membershipRescId, 0);
239
240        assertHasMembersNoTx(member1Id, MEMBER_OF, membershipRescId);
241        assertHasMembersNoTx(member2Id, MEMBER_OF, membershipRescId);
242
243        assertEquals(member1Updated, membershipService.getLastUpdatedTimestamp(readOnlyTx, member1Id));
244        assertEquals(member2Updated, membershipService.getLastUpdatedTimestamp(readOnlyTx, member2Id));
245    }
246
247    @Test
248    public void getMembers_WithDC_BinaryAsMembershipResc() throws Exception {
249        mockGetHeaders(populateHeaders(membershipRescId, RdfLexicon.NON_RDF_SOURCE));
250        membershipService.resourceCreated(transaction, membershipRescId);
251
252        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
253        membershipService.resourceCreated(transaction, dcId);
254
255        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
256
257        final var descId = membershipRescId.asDescription();
258
259        assertHasMembers(transaction, descId, RdfLexicon.LDP_MEMBER, member1Id);
260
261        membershipService.commitTransaction(transaction);
262
263        assertHasMembersNoTx(descId, RdfLexicon.LDP_MEMBER, member1Id);
264    }
265
266    @Test
267    public void deleteMember_InDC_AddedInSameTx_HasMemberRelation() throws Exception {
268        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
269        membershipService.resourceCreated(transaction, membershipRescId);
270
271        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
272        membershipService.resourceCreated(transaction, dcId);
273
274        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
275
276        assertUncommittedMembershipCount(transaction, membershipRescId, 1);
277
278        assertNotNull(membershipService.getLastUpdatedTimestamp(transaction, membershipRescId));
279
280        mockDeleteHeaders(member1Id, dcId, BASIC_CONTAINER);
281        // Notify that the member was deleted
282        membershipService.resourceDeleted(transaction, member1Id);
283
284        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
285        assertCommittedMembershipCount(membershipRescId, 0);
286
287        assertNull(membershipService.getLastUpdatedTimestamp(transaction, membershipRescId));
288
289        membershipService.commitTransaction(transaction);
290
291        assertCommittedMembershipCount(membershipRescId, 0);
292
293        assertNull(membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipRescId));
294    }
295
296    @Test
297    public void deleteExistingMember_InDC_HasMemberRelation() throws Exception {
298        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
299        membershipService.resourceCreated(transaction, membershipRescId);
300
301        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
302        membershipService.resourceCreated(transaction, dcId);
303
304        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
305
306        membershipService.commitTransaction(transaction);
307
308        assertCommittedMembershipCount(membershipRescId, 1);
309
310        final var lastUpdated = membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipRescId);
311        assertNotNull(lastUpdated);
312
313        mockDeleteHeaders(member1Id, dcId, BASIC_CONTAINER);
314        // Notify that the member was deleted
315        membershipService.resourceDeleted(transaction, member1Id);
316
317        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
318        assertCommittedMembershipCount(membershipRescId, 1);
319
320        membershipService.commitTransaction(transaction);
321
322        assertCommittedMembershipCount(membershipRescId, 0);
323
324        final var afterDeleteUpdated = membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipRescId);
325        assertNotNull(afterDeleteUpdated);
326        assertNotEquals(lastUpdated, afterDeleteUpdated);
327    }
328
329    @Test
330    public void deleteExistingMember_InDC_MultipleMembers_HasMemberRelation() throws Exception {
331        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
332        membershipService.resourceCreated(transaction, membershipRescId);
333
334        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
335        membershipService.resourceCreated(transaction, dcId);
336
337        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
338        final var member2Id = createDCMember(dcId, BASIC_CONTAINER);
339
340        membershipService.commitTransaction(transaction);
341
342        assertCommittedMembershipCount(membershipRescId, 2);
343
344        mockDeleteHeaders(member1Id, dcId, BASIC_CONTAINER);
345        // Notify that the member was deleted
346        membershipService.resourceDeleted(transaction, member1Id);
347
348        assertUncommittedMembershipCount(transaction, membershipRescId, 1);
349        assertCommittedMembershipCount(membershipRescId, 2);
350
351        membershipService.commitTransaction(transaction);
352
353        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member2Id);
354    }
355
356    @Test
357    public void deleteExistingMember_InDC_IsMemberOfRelation() throws Exception {
358        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
359        membershipService.resourceCreated(transaction, membershipRescId);
360
361        final var dcId = createDirectContainer(membershipRescId, MEMBER_OF, true);
362        membershipService.resourceCreated(transaction, dcId);
363
364        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
365
366        membershipService.commitTransaction(transaction);
367
368        assertCommittedMembershipCount(member1Id, 1);
369
370        mockDeleteHeaders(member1Id, dcId, BASIC_CONTAINER);
371        // Notify that the member was deleted
372        membershipService.resourceDeleted(transaction, member1Id);
373
374        assertCommittedMembershipCount(member1Id, 1);
375        assertUncommittedMembershipCount(transaction, member1Id, 0);
376
377        membershipService.commitTransaction(transaction);
378
379        assertCommittedMembershipCount(member1Id, 0);
380    }
381
382    @Test
383    public void deleteDC_WithMember_CreatedInSameTx() throws Exception {
384        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
385        membershipService.resourceCreated(transaction, membershipRescId);
386
387        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
388        membershipService.resourceCreated(transaction, dcId);
389
390        createDCMember(dcId, BASIC_CONTAINER);
391
392        assertCommittedMembershipCount(membershipRescId, 0);
393        assertUncommittedMembershipCount(transaction, membershipRescId, 1);
394
395        mockDeleteHeaders(dcId, rootId, RdfLexicon.DIRECT_CONTAINER);
396
397        // Notify that the DC was deleted
398        membershipService.resourceDeleted(transaction, dcId);
399
400        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
401        assertCommittedMembershipCount(membershipRescId, 0);
402
403        membershipService.commitTransaction(transaction);
404
405        assertCommittedMembershipCount(membershipRescId, 0);
406    }
407
408    @Test
409    public void deleteExistingDC_WithExistingMember() throws Exception {
410        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
411        membershipService.resourceCreated(transaction, membershipRescId);
412
413        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
414        membershipService.resourceCreated(transaction, dcId);
415
416        createDCMember(dcId, BASIC_CONTAINER);
417
418        membershipService.commitTransaction(transaction);
419
420        assertCommittedMembershipCount(membershipRescId, 1);
421
422        mockDeleteHeaders(dcId, rootId, RdfLexicon.DIRECT_CONTAINER);
423        // Notify that the DC was deleted
424        membershipService.resourceDeleted(transaction, dcId);
425
426        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
427        assertCommittedMembershipCount(membershipRescId, 1);
428
429        membershipService.commitTransaction(transaction);
430
431        assertCommittedMembershipCount(membershipRescId, 0);
432    }
433
434    @Test
435    public void deleteExistingMemberAndDC_InSameTx() throws Exception {
436        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
437        membershipService.resourceCreated(transaction, membershipRescId);
438
439        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
440        membershipService.resourceCreated(transaction, dcId);
441
442        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
443
444        membershipService.commitTransaction(transaction);
445
446        mockDeleteHeaders(dcId, rootId, RdfLexicon.DIRECT_CONTAINER);
447        mockDeleteHeaders(member1Id, dcId, BASIC_CONTAINER);
448        // Delete the member
449        membershipService.resourceDeleted(transaction, member1Id);
450        // Delete the DC itself
451        membershipService.resourceDeleted(transaction, dcId);
452
453        assertCommittedMembershipCount(membershipRescId, 1);
454        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
455
456        membershipService.commitTransaction(transaction);
457
458        assertCommittedMembershipCount(membershipRescId, 0);
459    }
460
461    @Test
462    public void purgeDC() throws Exception {
463        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
464        membershipService.resourceCreated(transaction, membershipRescId);
465
466        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
467        membershipService.resourceCreated(transaction, dcId);
468
469        createDCMember(dcId, BASIC_CONTAINER);
470
471        membershipService.commitTransaction(transaction);
472
473        assertCommittedMembershipCount(membershipRescId, 1);
474
475        when(psSession.getHeaders(eq(dcId), nullable(Instant.class))).thenThrow(
476                new PersistentItemNotFoundException(""));
477
478        membershipService.resourceDeleted(transaction, dcId);
479
480        assertCommittedMembershipCount(membershipRescId, 0);
481        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
482    }
483
484    @Test
485    public void purgeMember() throws Exception {
486        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
487        membershipService.resourceCreated(transaction, membershipRescId);
488
489        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
490        membershipService.resourceCreated(transaction, dcId);
491
492        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
493        final var member2Id = createDCMember(dcId, BASIC_CONTAINER);
494
495        membershipService.commitTransaction(transaction);
496
497        assertCommittedMembershipCount(membershipRescId, 2);
498
499        final var lastUpdated = membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipRescId);
500        assertNotNull(lastUpdated);
501
502        when(psSession.getHeaders(eq(member1Id), nullable(Instant.class))).thenThrow(
503                new PersistentItemNotFoundException(""));
504
505        membershipService.resourceDeleted(transaction, member1Id);
506
507        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member2Id);
508        assertUncommittedMembershipCount(transaction, membershipRescId, 1);
509
510        assertEquals(lastUpdated, membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipRescId));
511    }
512
513    @Test
514    public void purgeMembershipResource_isMemberOfRelation() throws Exception {
515        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
516        membershipService.resourceCreated(transaction, membershipRescId);
517
518        final var dcId = createDirectContainer(membershipRescId, MEMBER_OF, true);
519        membershipService.resourceCreated(transaction, dcId);
520
521        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
522
523        membershipService.commitTransaction(transaction);
524
525        assertCommittedMembershipCount(member1Id, 1);
526
527        when(psSession.getHeaders(eq(membershipRescId), nullable(Instant.class))).thenThrow(
528                new PersistentItemNotFoundException(""));
529
530        membershipService.resourceDeleted(transaction, membershipRescId);
531
532        assertCommittedMembershipCount(member1Id, 0);
533        assertUncommittedMembershipCount(transaction, member1Id, 0);
534    }
535
536    @Test
537    public void recreateExistingMember_InDC_HasMemberRelation() throws Exception {
538        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
539        membershipService.resourceCreated(transaction, membershipRescId);
540
541        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
542        membershipService.resourceCreated(transaction, dcId);
543
544        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
545
546        membershipService.commitTransaction(transaction);
547
548        assertCommittedMembershipCount(membershipRescId, 1);
549
550        // Notify that the member was deleted
551        membershipService.resourceDeleted(transaction, member1Id);
552
553        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
554        assertCommittedMembershipCount(membershipRescId, 1);
555
556        // Recreate the resource in the same TX
557        membershipService.resourceCreated(transaction, member1Id);
558
559        assertUncommittedMembershipCount(transaction, membershipRescId, 1);
560        assertCommittedMembershipCount(membershipRescId, 1);
561
562        membershipService.commitTransaction(transaction);
563
564        assertCommittedMembershipCount(membershipRescId, 1);
565    }
566
567    @Test
568    public void getMembers_MultipleDCsSameMembershipResource_HasMemberRelation() throws Exception {
569        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
570        membershipService.resourceCreated(transaction, membershipRescId);
571
572        final var dc1Id = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
573        membershipService.resourceCreated(transaction, dc1Id);
574
575        // Add a child to the outer DC
576        final var member1Id = createDCMember(dc1Id, BASIC_CONTAINER);
577
578        final var dc2Id = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
579        membershipService.resourceCreated(transaction, dc2Id);
580
581        // Add a child to the outer DC
582        final var member2Id = createDCMember(dc2Id, BASIC_CONTAINER);
583
584        assertUncommittedMembershipCount(transaction, membershipRescId, 2);
585
586        membershipService.commitTransaction(transaction);
587
588        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
589
590        // Delete one to ensure only those members are cleaned up
591        membershipService.resourceDeleted(transaction, member2Id);
592
593        membershipService.commitTransaction(transaction);
594
595        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
596    }
597
598    @Test
599    public void getMembers_MultipleDCsSameMembershipResource_IsMemberOfRelation() throws Exception {
600        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
601        membershipService.resourceCreated(transaction, membershipRescId);
602
603        final var dc1Id = createDirectContainer(membershipRescId, MEMBER_OF, true);
604        membershipService.resourceCreated(transaction, dc1Id);
605
606        // Add a child to the outer DC
607        final var member1Id = createDCMember(dc1Id, BASIC_CONTAINER);
608
609        final var dc2Id = createDirectContainer(membershipRescId, MEMBER_OF, true);
610        membershipService.resourceCreated(transaction, dc2Id);
611
612        // Add a child to the outer DC
613        final var member2Id = createDCMember(dc2Id, BASIC_CONTAINER);
614
615        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
616        assertUncommittedMembershipCount(transaction, member1Id, 1);
617        assertUncommittedMembershipCount(transaction, member2Id, 1);
618
619        membershipService.commitTransaction(transaction);
620
621        assertIsMemberOfNoTx(member1Id, MEMBER_OF, membershipRescId);
622        assertIsMemberOfNoTx(member2Id, MEMBER_OF, membershipRescId);
623
624        // Delete one to ensure only those members are cleaned up
625        membershipService.resourceDeleted(transaction, member2Id);
626
627        membershipService.commitTransaction(transaction);
628
629        assertIsMemberOfNoTx(member1Id, MEMBER_OF, membershipRescId);
630        assertCommittedMembershipCount(member2Id, 0);
631    }
632
633    @Test
634    public void getMembers_DCmemberOfDC() throws Exception {
635        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
636        membershipService.resourceCreated(transaction, membershipRescId);
637
638        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
639        membershipService.resourceCreated(transaction, dcId);
640
641        // Add a child to the outer DC
642        final var outerMemberId = createDCMember(dcId, BASIC_CONTAINER);
643
644        // Add a DC as the child of the first DC
645        final var nestedDcId = createDirectContainer(dcId, membershipRescId, RdfLexicon.LDP_MEMBER, false);
646        membershipService.resourceCreated(transaction, nestedDcId);
647
648        // Add a child to the nested DC
649        final var nestedMemberId = createDCMember(nestedDcId, BASIC_CONTAINER);
650
651        assertCommittedMembershipCount(membershipRescId, 0);
652        assertUncommittedMembershipCount(transaction, membershipRescId, 3);
653
654        membershipService.commitTransaction(transaction);
655
656        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, outerMemberId, nestedDcId, nestedMemberId);
657
658        // Delete the nested DC to ensure that it gets cleaned up as both a DC and a member
659        membershipService.resourceDeleted(transaction, nestedDcId);
660
661        membershipService.commitTransaction(transaction);
662
663        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, outerMemberId);
664    }
665
666    @Test
667    public void changeMembershipResource_ForDC() throws Exception {
668        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
669        membershipService.resourceCreated(transaction, membershipRescId);
670
671        final var membershipResc2Id = mintFedoraId();
672        mockGetHeaders(populateHeaders(membershipResc2Id, BASIC_CONTAINER));
673        membershipService.resourceCreated(transaction, membershipResc2Id);
674
675        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
676        membershipService.resourceCreated(transaction, dcId);
677
678        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
679        final var member2Id = createDCMember(dcId, BASIC_CONTAINER);
680
681        membershipService.commitTransaction(transaction);
682
683        assertCommittedMembershipCount(membershipRescId, 2);
684        assertCommittedMembershipCount(membershipResc2Id, 0);
685
686        final var msRescUpdated = membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipRescId);
687        assertNotNull(msRescUpdated);
688        assertNull(membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipResc2Id));
689
690        // Change the membership resource for the DC
691        mockGetTriplesForDC(dcId, LAST_MODIFIED_DATE, membershipResc2Id, RdfLexicon.LDP_MEMBER, false);
692        membershipService.resourceModified(transaction, dcId);
693
694        assertHasMembers(transaction, membershipResc2Id, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
695
696        membershipService.commitTransaction(transaction);
697
698        assertCommittedMembershipCount(membershipRescId, 0);
699        assertHasMembersNoTx(membershipResc2Id, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
700
701        final var msRescUpdatedAfter = membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipRescId);
702        assertNotNull(msRescUpdatedAfter);
703        assertNotEquals("First membership resc should have changed last_updated timestamp",
704                msRescUpdated, msRescUpdatedAfter);
705        assertNotNull(membershipService.getLastUpdatedTimestamp(readOnlyTx, membershipResc2Id));
706    }
707
708    @Test
709    public void changeMembershipResource_ForDC_ManualVersioning() throws Exception {
710        setField(propsConfig, "autoVersioningEnabled", Boolean.FALSE);
711
712        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
713        membershipService.resourceCreated(transaction, membershipRescId);
714
715        final var membershipResc2Id = mintFedoraId();
716        mockGetHeaders(populateHeaders(membershipResc2Id, BASIC_CONTAINER));
717        membershipService.resourceCreated(transaction, membershipResc2Id);
718
719        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
720        membershipService.resourceCreated(transaction, dcId);
721
722        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
723
724        membershipService.commitTransaction(transaction);
725
726        assertCommittedMembershipCount(membershipRescId, 1);
727        assertCommittedMembershipCount(membershipResc2Id, 0);
728
729        // Change the membership resource for the DC without creating a version
730        mockListVersion(dcId);
731        mockGetTriplesForDCHead(dcId, CREATED_DATE, membershipResc2Id, RdfLexicon.LDP_MEMBER, false);
732        membershipService.resourceModified(transaction, dcId);
733
734        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
735        assertHasMembers(transaction, membershipResc2Id, RdfLexicon.LDP_MEMBER, member1Id);
736
737        membershipService.commitTransaction(transaction);
738
739        assertCommittedMembershipCount(membershipRescId, 0);
740        assertHasMembersNoTx(membershipResc2Id, RdfLexicon.LDP_MEMBER, member1Id);
741
742        // Change membership property without versioning
743        mockGetTriplesForDCHead(dcId, CREATED_DATE, membershipResc2Id, OTHER_HAS_MEMBER, false);
744        membershipService.resourceModified(transaction, dcId);
745
746        assertHasMembersNoTx(membershipResc2Id, RdfLexicon.LDP_MEMBER, member1Id);
747        assertHasMembers(transaction, membershipResc2Id, OTHER_HAS_MEMBER, member1Id);
748
749        membershipService.commitTransaction(transaction);
750
751        assertCommittedMembershipCount(membershipRescId, 0);
752        assertHasMembersNoTx(membershipResc2Id, OTHER_HAS_MEMBER, member1Id);
753
754        // Create version from former head version
755        final var versionChangeTime = Instant.parse("2019-11-13T12:00:00.0Z");
756        mockListVersion(dcId, versionChangeTime);
757        // New head state matches previous head state for the moment
758        mockGetTriplesForDC(dcId, versionChangeTime, membershipResc2Id, OTHER_HAS_MEMBER, false);
759        mockGetHeaders(transaction, dcId.asMemento(versionChangeTime), populateHeaders(dcId, rootId,
760                RdfLexicon.DIRECT_CONTAINER, CREATED_DATE, versionChangeTime), rootId);
761
762        // Change membership resource after having created version
763        final var afterVersionChangeTime = Instant.parse("2019-11-13T14:00:00.0Z");
764        mockGetHeaders(transaction, dcId, populateHeaders(dcId, rootId,
765                RdfLexicon.DIRECT_CONTAINER, CREATED_DATE, afterVersionChangeTime), rootId);
766        mockGetTriplesForDCHead(dcId, afterVersionChangeTime, membershipRescId, OTHER_HAS_MEMBER, false);
767        membershipService.resourceModified(transaction, dcId);
768
769        // Membership resc 2 should still have a member prior to the version creation/last property update
770        assertHasMembers(transaction, membershipResc2Id.asMemento(CREATED_DATE), OTHER_HAS_MEMBER,
771                member1Id);
772        assertUncommittedMembershipCount(transaction, membershipResc2Id, 0);
773        assertHasMembersNoTx(membershipResc2Id, OTHER_HAS_MEMBER, member1Id);
774        assertHasMembers(transaction, membershipRescId, OTHER_HAS_MEMBER, member1Id);
775
776        membershipService.commitTransaction(transaction);
777
778        assertCommittedMembershipCount(membershipResc2Id, 0);
779        assertHasMembersNoTx(membershipResc2Id.asMemento(CREATED_DATE), OTHER_HAS_MEMBER,
780                member1Id);
781        assertCommittedMembershipCount(membershipRescId.asMemento(CREATED_DATE), 0);
782        assertHasMembersNoTx(membershipRescId, OTHER_HAS_MEMBER, member1Id);
783    }
784
785    @Test
786    public void changeMembershipRelation_DC_HasMember() throws Exception {
787        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
788        membershipService.resourceCreated(transaction, membershipRescId);
789
790        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
791        membershipService.resourceCreated(transaction, dcId);
792
793        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
794        final var member2Id = createDCMember(dcId, BASIC_CONTAINER);
795
796        assertUncommittedMembershipCount(transaction, membershipRescId, 2);
797
798        membershipService.commitTransaction(transaction);
799
800        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
801
802        // Change the membership relation
803        mockGetTriplesForDC(dcId, LAST_MODIFIED_DATE, membershipRescId, OTHER_HAS_MEMBER, false);
804        membershipService.resourceModified(transaction, dcId);
805
806        assertHasMembers(transaction, membershipRescId, OTHER_HAS_MEMBER, member1Id, member2Id);
807
808        membershipService.commitTransaction(transaction);
809
810        assertHasMembersNoTx(membershipRescId, OTHER_HAS_MEMBER, member1Id, member2Id);
811    }
812
813    @Test
814    public void changeResource_DC_HasMemberToIsMemberOf() throws Exception {
815        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
816        membershipService.resourceCreated(transaction, membershipRescId);
817
818        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
819        membershipService.resourceCreated(transaction, dcId);
820
821        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
822
823        assertUncommittedMembershipCount(transaction, membershipRescId, 1);
824
825        membershipService.commitTransaction(transaction);
826
827        assertHasMembers(readOnlyTx, membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
828
829        final var msRescUpdated1 = indexManager.getLastUpdated(readOnlyTx, membershipRescId);
830        assertNotNull(msRescUpdated1);
831        assertNull(indexManager.getLastUpdated(readOnlyTx, member1Id));
832
833        // Change the membership direction from a ldp:hasMemberRelation to a ldp:isMemberOfRelation
834        mockGetTriplesForDC(dcId, LAST_MODIFIED_DATE, membershipRescId, MEMBER_OF, true);
835        membershipService.resourceModified(transaction, dcId);
836
837        assertCommittedMembershipCount(membershipRescId, 1);
838        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
839        assertIsMemberOf(transaction, member1Id, MEMBER_OF, membershipRescId);
840
841        membershipService.commitTransaction(transaction);
842
843        assertIsMemberOfNoTx(member1Id, MEMBER_OF, membershipRescId);
844
845        final var memRescUpdated1 = indexManager.getLastUpdated(readOnlyTx, member1Id);
846        assertNotNull(memRescUpdated1);
847        final var msRescUpdated2 = indexManager.getLastUpdated(readOnlyTx, membershipRescId);
848        assertNotNull(msRescUpdated2);
849        assertNotEquals(msRescUpdated1, msRescUpdated2);
850
851        // Reverse the membership direction again
852        mockGetTriplesForDC(dcId, LAST_MODIFIED_DATE2, membershipRescId, OTHER_HAS_MEMBER, false);
853        mockGetHeaders(transaction, dcId, rootId, RdfLexicon.DIRECT_CONTAINER, CREATED_DATE, LAST_MODIFIED_DATE2);
854        membershipService.resourceModified(transaction, dcId);
855
856        assertCommittedMembershipCount(member1Id, 1);
857        assertUncommittedMembershipCount(transaction, member1Id, 0);
858        assertHasMembers(transaction, membershipRescId, OTHER_HAS_MEMBER, member1Id);
859
860        membershipService.commitTransaction(transaction);
861
862        assertCommittedMembershipCount(member1Id, 0);
863        assertHasMembersNoTx(membershipRescId, OTHER_HAS_MEMBER, member1Id);
864
865        final var memRescUpdated2 = indexManager.getLastUpdated(readOnlyTx, member1Id);
866        assertNotNull(memRescUpdated2);
867        assertNotEquals(memRescUpdated1, memRescUpdated2);
868        final var msRescUpdated3 = indexManager.getLastUpdated(readOnlyTx, membershipRescId);
869        assertNotNull(msRescUpdated3);
870        assertNotEquals(msRescUpdated2, msRescUpdated3);
871    }
872
873    @Test
874    public void changeResource_DC_IsMemberOf() throws Exception {
875        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
876        membershipService.resourceCreated(transaction, membershipRescId);
877
878        final var dcId = createDirectContainer(membershipRescId, MEMBER_OF, true);
879        membershipService.resourceCreated(transaction, dcId);
880
881        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
882
883        membershipService.commitTransaction(transaction);
884
885        assertIsMemberOfNoTx(member1Id, MEMBER_OF, membershipRescId);
886
887        // Switch DC to a different ldp:isMemberOfRelation
888        mockGetTriplesForDC(dcId, LAST_MODIFIED_DATE, membershipRescId, OTHER_MEMBER_OF, true);
889        membershipService.resourceModified(transaction, dcId);
890
891        assertIsMemberOf(transaction, member1Id, OTHER_MEMBER_OF, membershipRescId);
892
893        membershipService.commitTransaction(transaction);
894
895        assertIsMemberOfNoTx(member1Id, OTHER_MEMBER_OF, membershipRescId);
896
897        // Switch back again
898        mockGetTriplesForDC(dcId, LAST_MODIFIED_DATE, membershipRescId, MEMBER_OF, true);
899        membershipService.resourceModified(transaction, dcId);
900
901        assertIsMemberOf(transaction, member1Id, MEMBER_OF, membershipRescId);
902
903        membershipService.commitTransaction(transaction);
904
905        assertIsMemberOfNoTx(member1Id, MEMBER_OF, membershipRescId);
906    }
907
908    @Test
909    public void getMementoMembership_AllCreatedAtSameTime_NoChanges() throws Exception {
910        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
911        membershipService.resourceCreated(transaction, membershipRescId);
912
913        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
914        membershipService.resourceCreated(transaction, dcId);
915
916        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
917
918        final var beforeCreated = Instant.parse("2019-11-10T00:00:00.0Z");
919        final var beforeCreatedId = membershipRescId.asMemento(beforeCreated);
920
921        final var afterLastModified = Instant.parse("2019-12-10T00:00:00.0Z");
922        final var afterLastModifiedId = membershipRescId.asMemento(afterLastModified);
923
924        // No membership before creation time
925        assertUncommittedMembershipCount(transaction, beforeCreatedId, 0);
926        assertUncommittedMembershipCount(transaction, membershipRescId, 1);
927        assertUncommittedMembershipCount(transaction, afterLastModifiedId, 1);
928
929        membershipService.commitTransaction(transaction);
930
931        assertCommittedMembershipCount(beforeCreatedId, 0);
932        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
933        assertCommittedMembershipCount(afterLastModifiedId, 1);
934    }
935
936    @Test
937    public void getMementoMembership_OneMembershipAddition_hasMemberRelation() throws Exception {
938        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
939        membershipService.resourceCreated(transaction, membershipRescId);
940
941        final var headMementoId = membershipRescId.asMemento(LAST_MODIFIED_DATE);
942
943        final var beforeAddMementoInstant = Instant.parse("2019-11-12T12:00:00.0Z");
944        final var beforeAddMementoId = membershipRescId.asMemento(beforeAddMementoInstant);
945        mockGetHeaders(populateHeaders(membershipRescId, rootId, BASIC_CONTAINER, CREATED_DATE,
946                beforeAddMementoInstant));
947
948        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
949        membershipService.resourceCreated(transaction, dcId);
950
951        final var memberCreated = Instant.parse("2019-11-12T13:00:00.0Z");
952        final var member1Id = createDCMember(dcId, BASIC_CONTAINER, memberCreated);
953
954        // No membership at first memento timestamp
955        assertUncommittedMembershipCount(transaction, beforeAddMementoId, 0);
956        assertUncommittedMembershipCount(transaction, membershipRescId, 1);
957
958        membershipService.commitTransaction(transaction);
959
960        // No membership at first memento timestamp
961        assertCommittedMembershipCount(beforeAddMementoId, 0);
962        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
963        // Memento request for the head version should return the membership
964        assertHasMembersNoTx(headMementoId, RdfLexicon.LDP_MEMBER, member1Id);
965        // Request at the exact time of the membership addition should return member
966        final var atAddMementoId = membershipRescId.asMemento(memberCreated);
967        assertHasMembersNoTx(atAddMementoId, RdfLexicon.LDP_MEMBER, member1Id);
968    }
969
970    @Test
971    public void getMementoMembership_AddAndDelete_isMemberOfRelation() throws Exception {
972        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
973        membershipService.resourceCreated(transaction, membershipRescId);
974
975        final var dcId = createDirectContainer(membershipRescId, MEMBER_OF, true);
976        membershipService.resourceCreated(transaction, dcId);
977
978        final var memberCreated = Instant.parse("2019-11-12T13:00:00.0Z");
979        final var member1Id = createDCMember(dcId, BASIC_CONTAINER, memberCreated);
980        final var memberCreatedId = member1Id.asMemento(memberCreated);
981        final var beforeCreate = Instant.parse("2019-11-10T00:00:00.0Z");
982        final var beforeCreateId = member1Id.asMemento(beforeCreate);
983
984        assertUncommittedMembershipCount(transaction, beforeCreateId, 0);
985        assertUncommittedMembershipCount(transaction, member1Id, 1);
986
987        membershipService.commitTransaction(transaction);
988
989        // No membership before member created
990        assertCommittedMembershipCount(beforeCreateId, 0);
991        assertIsMemberOfNoTx(member1Id, MEMBER_OF, membershipRescId);
992        // Explicitly request memento of head
993        assertIsMemberOfNoTx(memberCreatedId, MEMBER_OF, membershipRescId);
994        // Request memento after last modified
995        final var afterModified = Instant.parse("2019-12-10T00:00:00.0Z");
996        final var afterModifiedId = member1Id.asMemento(afterModified);
997        assertIsMemberOfNoTx(afterModifiedId, MEMBER_OF, membershipRescId);
998
999        final var deleteInstant = Instant.parse("2019-11-13T12:00:00.0Z");
1000        final var deletedMemberId = member1Id.asMemento(deleteInstant);
1001        mockDeleteHeaders(member1Id, dcId, BASIC_CONTAINER, memberCreated, deleteInstant);
1002
1003        final var afterDeleteMemberId = member1Id.asMemento(Instant.parse("2019-11-13T16:00:00.0Z"));
1004
1005        membershipService.resourceDeleted(transaction, member1Id);
1006
1007        // Make sure delete hasn't leaked
1008        assertCommittedMembershipCount(member1Id, 1);
1009
1010        assertIsMemberOf(transaction, memberCreatedId, MEMBER_OF, membershipRescId);
1011        assertUncommittedMembershipCount(transaction, deletedMemberId, 0);
1012        assertUncommittedMembershipCount(transaction, afterDeleteMemberId, 0);
1013        assertUncommittedMembershipCount(transaction, member1Id, 0);
1014
1015        membershipService.commitTransaction(transaction);
1016
1017        assertCommittedMembershipCount(beforeCreateId, 0);
1018        assertIsMemberOfNoTx(memberCreatedId, MEMBER_OF, membershipRescId);
1019        assertCommittedMembershipCount(deletedMemberId, 0);
1020        assertCommittedMembershipCount(afterDeleteMemberId, 0);
1021        assertCommittedMembershipCount(member1Id, 0);
1022    }
1023
1024    @Test
1025    public void getMementoMembership_AddAndDelete_hasMemberRelation() throws Exception {
1026        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
1027        membershipService.resourceCreated(transaction, membershipRescId);
1028
1029        final var beforeAddMementoInstant = Instant.parse("2019-11-12T12:00:00.0Z");
1030        final var beforeAddMementoId = membershipRescId.asMemento(beforeAddMementoInstant);
1031
1032        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
1033        membershipService.resourceCreated(transaction, dcId);
1034
1035        final var member1Created = Instant.parse("2019-11-12T13:00:00.0Z");
1036        final var member1Id = createDCMember(dcId, BASIC_CONTAINER, member1Created);
1037
1038        final var member2Created = Instant.parse("2019-11-12T20:00:00.0Z");
1039        final var member2Id = createDCMember(dcId, BASIC_CONTAINER, member2Created);
1040
1041        final var membershipRescAtMember1Create1Id = membershipRescId.asMemento(member1Created);
1042        final var membershipRescAtMember2Create1Id = membershipRescId.asMemento(member2Created);
1043
1044        // No membership before members added
1045        assertUncommittedMembershipCount(transaction, beforeAddMementoId, 0);
1046        // Check membership at the times members were added
1047        assertUncommittedMembershipCount(transaction, membershipRescAtMember1Create1Id, 1);
1048        assertUncommittedMembershipCount(transaction, membershipRescAtMember2Create1Id, 2);
1049        assertUncommittedMembershipCount(transaction, membershipRescId, 2);
1050
1051        membershipService.commitTransaction(transaction);
1052
1053        // No membership before members added
1054        assertCommittedMembershipCount(beforeAddMementoId, 0);
1055        assertHasMembersNoTx(membershipRescAtMember1Create1Id, RdfLexicon.LDP_MEMBER, member1Id);
1056        // Inbetween the members being added
1057        assertHasMembersNoTx(membershipRescId.asMemento(Instant.parse("2019-11-12T15:00:00.0Z")),
1058                RdfLexicon.LDP_MEMBER, member1Id);
1059        assertHasMembersNoTx(membershipRescAtMember2Create1Id, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
1060        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
1061
1062        // Delete one of the members
1063        final var deleteInstant = Instant.parse("2019-11-13T12:00:00.0Z");
1064        mockDeleteHeaders(member2Id, dcId, BASIC_CONTAINER, member2Created, deleteInstant);
1065
1066        membershipService.resourceDeleted(transaction, member2Id);
1067
1068        final var membershipRescAtDeleteId = membershipRescId.asMemento(deleteInstant);
1069        final var membershipRescAfterDeleteId = membershipRescId.asMemento(Instant.parse("2019-11-13T15:00:00.0Z"));
1070
1071        assertUncommittedMembershipCount(transaction, membershipRescAtMember2Create1Id, 2);
1072        assertUncommittedMembershipCount(transaction, membershipRescAtDeleteId, 1);
1073        assertUncommittedMembershipCount(transaction, membershipRescAfterDeleteId, 1);
1074
1075        membershipService.commitTransaction(transaction);
1076
1077        // No membership before members added
1078        assertCommittedMembershipCount(beforeAddMementoId, 0);
1079        assertHasMembersNoTx(membershipRescAtMember1Create1Id, RdfLexicon.LDP_MEMBER, member1Id);
1080        // Inbetween the members being added
1081        assertHasMembersNoTx(membershipRescId.asMemento(Instant.parse("2019-11-12T15:00:00.0Z")),
1082                RdfLexicon.LDP_MEMBER, member1Id);
1083        assertHasMembersNoTx(membershipRescAtMember2Create1Id, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
1084        assertHasMembersNoTx(membershipRescAtDeleteId, RdfLexicon.LDP_MEMBER, member1Id);
1085        assertHasMembersNoTx(membershipRescAfterDeleteId, RdfLexicon.LDP_MEMBER, member1Id);
1086        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
1087    }
1088
1089    @Test
1090    public void rollbackTransaction() throws Exception {
1091        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
1092        membershipService.resourceCreated(transaction, membershipRescId);
1093
1094        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
1095        membershipService.resourceCreated(transaction, dcId);
1096
1097        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
1098
1099        membershipService.commitTransaction(transaction);
1100
1101        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
1102
1103        final var member2Id = createDCMember(dcId, RdfLexicon.NON_RDF_SOURCE);
1104
1105        assertHasMembers(transaction, membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
1106
1107        membershipService.rollbackTransaction(transaction);
1108
1109        assertHasMembers(transaction, membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
1110
1111        // Commit the transaction and verify the non-rollback entries persist
1112        membershipService.commitTransaction(transaction);
1113
1114        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
1115    }
1116
1117    @Test
1118    public void resetMembershipIndex() throws Exception {
1119        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
1120        membershipService.resourceCreated(transaction, membershipRescId);
1121
1122        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
1123        membershipService.resourceCreated(transaction, dcId);
1124
1125        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
1126
1127        membershipService.commitTransaction(transaction);
1128
1129        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
1130
1131        final var member2Id = createDCMember(dcId, RdfLexicon.NON_RDF_SOURCE);
1132
1133        assertHasMembers(transaction, membershipRescId, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
1134
1135        membershipService.reset();
1136
1137        assertUncommittedMembershipCount(transaction, membershipRescId, 0);
1138        assertCommittedMembershipCount(membershipRescId, 0);
1139    }
1140
1141    @Test
1142    public void populateMembershipHistory_DC_DeletedMember() throws Exception {
1143        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
1144        membershipService.resourceCreated(transaction, membershipRescId);
1145
1146        final var beforeAddMementoInstant = Instant.parse("2019-11-12T12:00:00.0Z");
1147        final var beforeAddMementoId = membershipRescId.asMemento(beforeAddMementoInstant);
1148
1149        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
1150        membershipService.resourceCreated(transaction, dcId);
1151
1152        final var member1Created = Instant.parse("2019-11-12T13:00:00.0Z");
1153        final var member1Id = createDCMember(dcId, BASIC_CONTAINER, member1Created);
1154
1155        final var member2Created = Instant.parse("2019-11-12T20:00:00.0Z");
1156        final var member2Id = createDCMember(dcId, BASIC_CONTAINER, member2Created);
1157
1158        final var membershipRescAtMember1Create1Id = membershipRescId.asMemento(member1Created);
1159        final var membershipRescAtMember2Create1Id = membershipRescId.asMemento(member2Created);
1160
1161        membershipService.commitTransaction(transaction);
1162
1163        // Delete one of the members
1164        final var deleteInstant = Instant.parse("2019-11-13T12:00:00.0Z");
1165        mockDeleteHeaders(member2Id, dcId, BASIC_CONTAINER, member2Created, deleteInstant);
1166
1167        membershipService.resourceDeleted(transaction, member2Id);
1168
1169        final var membershipRescAtDeleteId = membershipRescId.asMemento(deleteInstant);
1170        final var membershipRescAfterDeleteId = membershipRescId.asMemento(Instant.parse("2019-11-13T15:00:00.0Z"));
1171
1172        membershipService.commitTransaction(transaction);
1173
1174        // Clear the index
1175        membershipService.reset();
1176
1177        mockListVersion(dcId, CREATED_DATE);
1178
1179        // Repopulate index
1180        membershipService.populateMembershipHistory(transaction, dcId);
1181
1182        membershipService.commitTransaction(transaction);
1183
1184        // No membership before members added
1185        assertCommittedMembershipCount(beforeAddMementoId, 0);
1186        assertHasMembersNoTx(membershipRescAtMember1Create1Id, RdfLexicon.LDP_MEMBER, member1Id);
1187        // Inbetween the members being added
1188        assertHasMembersNoTx(membershipRescId.asMemento(Instant.parse("2019-11-12T15:00:00.0Z")),
1189                RdfLexicon.LDP_MEMBER, member1Id);
1190        assertHasMembersNoTx(membershipRescAtMember2Create1Id, RdfLexicon.LDP_MEMBER, member1Id, member2Id);
1191        assertHasMembersNoTx(membershipRescAtDeleteId, RdfLexicon.LDP_MEMBER, member1Id);
1192        assertHasMembersNoTx(membershipRescAfterDeleteId, RdfLexicon.LDP_MEMBER, member1Id);
1193        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
1194    }
1195
1196    @Test
1197    public void populateMembershipHistory_DC_ChangeRelation_AddedMemberAfter() throws Exception {
1198        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
1199        membershipService.resourceCreated(transaction, membershipRescId);
1200
1201        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
1202        membershipService.resourceCreated(transaction, dcId);
1203
1204        membershipService.commitTransaction(transaction);
1205
1206        // Change the membership relation
1207        final var changeRelationInstant = Instant.parse("2019-11-14T12:00:00.0Z");
1208        final var dcAtChangeRelation = dcId.asMemento(changeRelationInstant);
1209
1210        final var member2Id = createDCMember(dcId, BASIC_CONTAINER, changeRelationInstant);
1211
1212        mockGetTriplesForDC(dcId, changeRelationInstant, membershipRescId, OTHER_HAS_MEMBER, false);
1213        mockGetHeaders(transaction, dcAtChangeRelation, populateHeaders(dcId, rootId,
1214                RdfLexicon.DIRECT_CONTAINER, CREATED_DATE, changeRelationInstant), rootId);
1215        membershipService.resourceModified(transaction, dcId);
1216
1217        membershipService.commitTransaction(transaction);
1218
1219        assertHasMembersNoTx(membershipRescId.asMemento(changeRelationInstant), OTHER_HAS_MEMBER, member2Id);
1220
1221        final var member1Created = Instant.parse("2019-11-15T12:00:00.0Z");
1222        final var member1Id = createDCMember(dcId, BASIC_CONTAINER, member1Created);
1223
1224        membershipService.commitTransaction(transaction);
1225
1226        membershipService.reset();
1227
1228        mockListVersion(dcId, CREATED_DATE, changeRelationInstant);
1229
1230        membershipService.populateMembershipHistory(transaction, dcId);
1231
1232        membershipService.commitTransaction(transaction);
1233
1234        // No membership before member added
1235        assertCommittedMembershipCount(membershipRescId.asMemento(Instant.parse("2019-11-13T12:00:00.0Z")), 0);
1236        assertHasMembersNoTx(membershipRescId.asMemento(changeRelationInstant), OTHER_HAS_MEMBER, member2Id);
1237        assertHasMembersNoTx(membershipRescId.asMemento(member1Created), OTHER_HAS_MEMBER,
1238                member1Id, member2Id);
1239        assertHasMembersNoTx(membershipRescId.asMemento(member1Created), OTHER_HAS_MEMBER,
1240                member1Id, member2Id);
1241        assertHasMembersNoTx(membershipRescId, OTHER_HAS_MEMBER, member1Id, member2Id);
1242    }
1243
1244    @Test
1245    public void populateMembershipHistory_DC_ChangeRelation_AddMemberBefore() throws Exception {
1246        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
1247        membershipService.resourceCreated(transaction, membershipRescId);
1248
1249        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
1250        membershipService.resourceCreated(transaction, dcId);
1251
1252        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
1253
1254        membershipService.commitTransaction(transaction);
1255
1256        final var member2Created = Instant.parse("2019-11-13T12:00:00.0Z");
1257        final var member2Id = createDCMember(dcId, BASIC_CONTAINER, member2Created);
1258
1259        membershipService.commitTransaction(transaction);
1260
1261        // Change the membership relation
1262        final var changeRelationInstant = Instant.parse("2019-11-14T12:00:00.0Z");
1263        final var dcAtChangeRelation = dcId.asMemento(changeRelationInstant);
1264
1265        // Mock triples change for changed DC
1266        mockGetTriplesForDC(dcId, changeRelationInstant, membershipRescId, OTHER_HAS_MEMBER, false);
1267        mockGetHeaders(transaction, dcAtChangeRelation, populateHeaders(dcId, rootId,
1268                RdfLexicon.DIRECT_CONTAINER, CREATED_DATE, changeRelationInstant), rootId);
1269        membershipService.resourceModified(transaction, dcId);
1270
1271        membershipService.commitTransaction(transaction);
1272
1273        membershipService.reset();
1274
1275        mockListVersion(dcId, CREATED_DATE, changeRelationInstant);
1276
1277        membershipService.populateMembershipHistory(transaction, dcId);
1278
1279        membershipService.commitTransaction(transaction);
1280
1281        // No membership before creation
1282        assertCommittedMembershipCount(membershipRescId.asMemento(Instant.parse("2019-01-01T12:00:00.0Z")), 0);
1283        assertHasMembersNoTx(membershipRescId.asMemento(CREATED_DATE), RdfLexicon.LDP_MEMBER, member1Id);
1284        assertHasMembersNoTx(membershipRescId.asMemento(changeRelationInstant), OTHER_HAS_MEMBER,
1285                member1Id, member2Id);
1286        assertHasMembersNoTx(membershipRescId, OTHER_HAS_MEMBER, member1Id, member2Id);
1287    }
1288
1289    @Test
1290    public void populateMembershipHistory_DC_ChangedRelation_DeleteMemberBefore() throws Exception {
1291        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
1292        membershipService.resourceCreated(transaction, membershipRescId);
1293
1294        final var dcId = createDirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
1295        membershipService.resourceCreated(transaction, dcId);
1296
1297        final var member1Id = createDCMember(dcId, BASIC_CONTAINER);
1298
1299        membershipService.commitTransaction(transaction);
1300
1301        final var member2Created = Instant.parse("2019-11-13T12:00:00.0Z");
1302        final var member2Id = createDCMember(dcId, BASIC_CONTAINER, member2Created);
1303
1304        // Delete one of the members
1305        final var deleteInstant = Instant.parse("2019-11-13T20:00:00.0Z");
1306        mockDeleteHeaders(member1Id, dcId, BASIC_CONTAINER, CREATED_DATE, deleteInstant);
1307        membershipService.resourceDeleted(transaction, member1Id);
1308
1309        membershipService.commitTransaction(transaction);
1310
1311        // Change the membership relation
1312        final var changeRelationInstant = Instant.parse("2019-11-14T12:00:00.0Z");
1313        final var dcAtChangeRelation = dcId.asMemento(changeRelationInstant);
1314
1315        // Mock triples change for changed DC
1316        mockGetTriplesForDC(dcId, changeRelationInstant, membershipRescId, OTHER_HAS_MEMBER, false);
1317        mockGetHeaders(transaction, dcAtChangeRelation, populateHeaders(dcId, rootId,
1318                RdfLexicon.DIRECT_CONTAINER, CREATED_DATE, changeRelationInstant), rootId);
1319        membershipService.resourceModified(transaction, dcId);
1320
1321        membershipService.commitTransaction(transaction);
1322
1323        membershipService.reset();
1324
1325        mockListVersion(dcId, CREATED_DATE, changeRelationInstant);
1326
1327        membershipService.populateMembershipHistory(transaction, dcId);
1328
1329        membershipService.commitTransaction(transaction);
1330
1331        // No membership before creation
1332        assertCommittedMembershipCount(membershipRescId.asMemento(Instant.parse("2019-01-01T12:00:00.0Z")), 0);
1333        // Member 1 is deleted before relationship change, but after member 2 is created
1334        assertHasMembersNoTx(membershipRescId.asMemento(CREATED_DATE), RdfLexicon.LDP_MEMBER,
1335                member1Id);
1336        assertHasMembersNoTx(membershipRescId.asMemento(member2Created), RdfLexicon.LDP_MEMBER,
1337                member1Id, member2Id);
1338        // Member 2 exists before and after the relation change, so its history contains both
1339        assertHasMembersNoTx(membershipRescId.asMemento(deleteInstant), RdfLexicon.LDP_MEMBER,
1340                member2Id);
1341        assertHasMembersNoTx(membershipRescId.asMemento(changeRelationInstant), OTHER_HAS_MEMBER,
1342                member2Id);
1343        assertHasMembersNoTx(membershipRescId, OTHER_HAS_MEMBER, member2Id);
1344    }
1345
1346    @Test
1347    public void changeMembershipResource_ForIDC_ManualVersioning() throws Exception {
1348        setField(propsConfig, "autoVersioningEnabled", Boolean.FALSE);
1349
1350        mockGetHeaders(populateHeaders(membershipRescId, BASIC_CONTAINER));
1351        membershipService.resourceCreated(transaction, membershipRescId);
1352
1353        final var membershipResc2Id = mintFedoraId();
1354        mockGetHeaders(populateHeaders(membershipResc2Id, BASIC_CONTAINER));
1355        membershipService.resourceCreated(transaction, membershipResc2Id);
1356
1357        final var idcId = createIndirectContainer(membershipRescId, RdfLexicon.LDP_MEMBER, false);
1358        membershipService.resourceCreated(transaction, idcId);
1359
1360        final var member1Id = createDCMember(rootId, BASIC_CONTAINER);
1361
1362        createProxy(idcId, member1Id, CREATED_DATE, true);
1363
1364        membershipService.commitTransaction(transaction);
1365
1366        assertCommittedMembershipCount(membershipRescId, 1);
1367        assertCommittedMembershipCount(membershipResc2Id, 0);
1368
1369        // Change the membership resource for the IDC without creating a version
1370        mockListVersion(idcId);
1371        mockGetTriplesForDC(idcId, CREATED_DATE, membershipResc2Id, RdfLexicon.LDP_MEMBER, false, PROXY_FOR, true);
1372        membershipService.resourceModified(transaction, idcId);
1373
1374        assertHasMembersNoTx(membershipRescId, RdfLexicon.LDP_MEMBER, member1Id);
1375        assertHasMembers(transaction, membershipResc2Id, RdfLexicon.LDP_MEMBER, member1Id);
1376
1377        membershipService.commitTransaction(transaction);
1378
1379        assertCommittedMembershipCount(membershipRescId, 0);
1380        assertHasMembersNoTx(membershipResc2Id, RdfLexicon.LDP_MEMBER, member1Id);
1381
1382        // Change membership property without versioning
1383        mockGetTriplesForDC(idcId, CREATED_DATE, membershipResc2Id, OTHER_HAS_MEMBER, false, PROXY_FOR, true);
1384        membershipService.resourceModified(transaction, idcId);
1385
1386        assertHasMembersNoTx(membershipResc2Id, RdfLexicon.LDP_MEMBER, member1Id);
1387        assertHasMembers(transaction, membershipResc2Id, OTHER_HAS_MEMBER, member1Id);
1388
1389        membershipService.commitTransaction(transaction);
1390
1391        assertCommittedMembershipCount(membershipRescId, 0);
1392        assertHasMembersNoTx(membershipResc2Id, OTHER_HAS_MEMBER, member1Id);
1393
1394        // Create version from former head version
1395        final var versionChangeTime = Instant.parse("2019-11-13T12:00:00.0Z");
1396        mockListVersion(idcId, versionChangeTime);
1397        // New head state matches previous head state for the moment
1398        mockGetTriplesForDC(idcId, versionChangeTime, membershipResc2Id, OTHER_HAS_MEMBER, false, PROXY_FOR, false);
1399        mockGetHeaders(transaction, idcId.asMemento(versionChangeTime), populateHeaders(idcId, rootId,
1400                RdfLexicon.INDIRECT_CONTAINER, CREATED_DATE, versionChangeTime), rootId);
1401
1402        // Change membership resource after having created version
1403        final var afterVersionChangeTime = Instant.parse("2019-11-13T14:00:00.0Z");
1404        mockGetHeaders(transaction, idcId, populateHeaders(idcId, rootId,
1405                RdfLexicon.INDIRECT_CONTAINER, CREATED_DATE, afterVersionChangeTime), rootId);
1406        mockGetTriplesForDC(idcId, afterVersionChangeTime, membershipRescId, OTHER_HAS_MEMBER, false, PROXY_FOR, true);
1407        membershipService.resourceModified(transaction, idcId);
1408
1409        // Membership resc 2 should still have a member prior to the version creation/last property update
1410        assertHasMembers(transaction, membershipResc2Id.asMemento(CREATED_DATE), OTHER_HAS_MEMBER,
1411                member1Id);
1412        assertUncommittedMembershipCount(transaction, membershipResc2Id, 0);
1413        assertHasMembersNoTx(membershipResc2Id, OTHER_HAS_MEMBER, member1Id);
1414        assertHasMembers(transaction, membershipRescId, OTHER_HAS_MEMBER, member1Id);
1415
1416        membershipService.commitTransaction(transaction);
1417
1418        assertCommittedMembershipCount(membershipResc2Id, 0);
1419        assertHasMembersNoTx(membershipResc2Id.asMemento(CREATED_DATE), OTHER_HAS_MEMBER,
1420                member1Id);
1421        assertCommittedMembershipCount(membershipRescId.asMemento(CREATED_DATE), 0);
1422        assertHasMembersNoTx(membershipRescId, OTHER_HAS_MEMBER, member1Id);
1423    }
1424
1425    private void mockListVersion(final FedoraId fedoraId, final Instant... versions) {
1426        when(psSession.listVersions(fedoraId.asResourceId())).thenReturn(Arrays.asList(versions));
1427    }
1428
1429    private void assertHasMembersNoTx(final FedoraId membershipRescId,
1430            final Property hasMemberRelation, final FedoraId... memberIds) {
1431        assertHasMembers(shortLivedTx, membershipRescId, hasMemberRelation, memberIds);
1432    }
1433
1434    private void assertHasMembers(final Transaction transaction, final FedoraId membershipRescId,
1435            final Property hasMemberRelation, final FedoraId... memberIds) {
1436        final var membershipList = getMembershipList(transaction, membershipRescId);
1437        assertEquals(memberIds.length, membershipList.size());
1438        final var subjectId = membershipRescId.asBaseId();
1439        for (final FedoraId memberId : memberIds) {
1440            assertContainsMembership(membershipList, subjectId, hasMemberRelation, memberId);
1441        }
1442    }
1443
1444    private void assertIsMemberOfNoTx(final FedoraId memberId, final Property isMemberOf,
1445            final FedoraId membershipRescId) {
1446        assertIsMemberOf(shortLivedTx, memberId, isMemberOf, membershipRescId);
1447    }
1448
1449    private void assertIsMemberOf(final Transaction transaction, final FedoraId memberId, final Property isMemberOf,
1450            final FedoraId membershipRescId) {
1451        final var membershipList = getMembershipList(transaction, memberId);
1452        assertEquals(1, membershipList.size());
1453        assertContainsMembership(membershipList, memberId.asBaseId(), isMemberOf, membershipRescId);
1454    }
1455
1456    private List<Triple> getMembershipList(final Transaction transaction, final FedoraId fedoraId) {
1457        final var results = membershipService.getMembership(transaction, fedoraId);
1458        return results.collect(Collectors.toList());
1459    }
1460
1461    private FedoraId mintFedoraId() {
1462        return FedoraId.create(UUID.randomUUID().toString());
1463    }
1464
1465    private void mockGetHeaders(final ResourceHeaders headers) {
1466        mockGetHeaders(transaction, headers.getId(), headers, rootId);
1467    }
1468
1469    private void mockGetHeaders(final Transaction transaction, final FedoraId fedoraId, final ResourceHeaders headers,
1470            final FedoraId parentId) {
1471        when(psSession.getHeaders(eq(fedoraId), nullable(Instant.class))).thenReturn(headers);
1472        if (!fedoraId.isMemento()) {
1473            when(psSession.getHeaders(eq(headers.getId().asMemento(headers.getCreatedDate())),
1474                    nullable(Instant.class))).thenReturn(headers);
1475        }
1476        containmentIndex.addContainedBy(transaction, parentId, fedoraId);
1477    }
1478
1479    private void mockGetHeaders(final Transaction transaction, final FedoraId fedoraId, final FedoraId parentId,
1480            final Resource ixModel, final Instant createdDate, final Instant lastModified) {
1481        final var headers = populateHeaders(fedoraId, parentId, ixModel, createdDate, lastModified);
1482        mockGetHeaders(transaction, fedoraId, headers, parentId);
1483    }
1484
1485    private void mockDeleteHeaders(final FedoraId fedoraId, final FedoraId parentId, final Resource ixModel) {
1486        mockDeleteHeaders(fedoraId, parentId, ixModel, CREATED_DATE, LAST_MODIFIED_DATE);
1487    }
1488
1489    private void mockDeleteHeaders(final FedoraId fedoraId, final FedoraId parentId, final Resource ixModel,
1490            final Instant createdDate, final Instant deleteInstant) {
1491        final var deletedHeaders = populateHeaders(fedoraId, parentId, ixModel, createdDate, deleteInstant);
1492        deletedHeaders.setDeleted(true);
1493        when(psSession.getHeaders(eq(fedoraId), isNull())).thenReturn(deletedHeaders);
1494    }
1495
1496    private FedoraId createDCMember(final FedoraId dcId, final Resource ixModel) {
1497        final var memberId = mintFedoraId();
1498        mockGetHeaders(transaction, memberId, dcId, ixModel, CREATED_DATE, LAST_MODIFIED_DATE);
1499        membershipService.resourceCreated(transaction, memberId);
1500        return memberId;
1501    }
1502
1503    private FedoraId createDCMember(final FedoraId dcId, final Resource ixModel, final Instant lastModified) {
1504        final var memberId = mintFedoraId();
1505        mockGetHeaders(transaction, memberId, dcId, ixModel, lastModified, lastModified);
1506        membershipService.resourceCreated(transaction, memberId);
1507        return memberId;
1508    }
1509
1510    private ResourceHeaders populateHeaders(final FedoraId fedoraId, final Resource ixModel) {
1511        return populateHeaders(fedoraId, rootId, ixModel);
1512    }
1513
1514    private static ResourceHeaders populateHeaders(final FedoraId fedoraId, final FedoraId parentId,
1515            final Resource ixModel) {
1516        return populateHeaders(fedoraId, parentId, ixModel, CREATED_DATE, LAST_MODIFIED_DATE);
1517    }
1518
1519    private static ResourceHeadersImpl populateHeaders(final FedoraId fedoraId, final FedoraId parentId,
1520            final Resource ixModel, final Instant createdDate, final Instant lastModifiedDate) {
1521        final var headers = new ResourceHeadersImpl();
1522        headers.setId(fedoraId);
1523        headers.setParent(parentId);
1524        headers.setInteractionModel(ixModel.getURI());
1525        headers.setCreatedBy(CREATED_BY);
1526        headers.setCreatedDate(createdDate);
1527        headers.setLastModifiedBy(LAST_MODIFIED_BY);
1528        headers.setLastModifiedDate(lastModifiedDate);
1529        headers.setStateToken(STATE_TOKEN);
1530        return headers;
1531    }
1532
1533    private FedoraId createIndirectContainer(final FedoraId membershipRescId, final Property relation,
1534            final boolean useIsMemberOf) {
1535        return createIndirectContainer(rootId, membershipRescId, relation, useIsMemberOf);
1536    }
1537
1538    private FedoraId createIndirectContainer(final FedoraId parentId, final FedoraId membershipRescId,
1539            final Property relation, final boolean useIsMemberOf) {
1540        final var dcId = mintFedoraId();
1541        mockGetHeaders(populateHeaders(dcId, parentId, RdfLexicon.INDIRECT_CONTAINER));
1542        mockGetTriplesForDC(dcId, CREATED_DATE, membershipRescId, relation, useIsMemberOf, PROXY_FOR, false);
1543        return dcId;
1544    }
1545
1546    private FedoraId createProxy(final FedoraId idcId, final FedoraId memberId,
1547            final Instant lastModified, final boolean isHead) {
1548        final var proxyId = mintFedoraId();
1549        mockGetHeaders(transaction, proxyId, idcId, BASIC_CONTAINER, lastModified, lastModified);
1550        final var model = ModelFactory.createDefaultModel();
1551        final var proxyRdfResc = model.getResource(proxyId.getBaseId());
1552        final var memberRdfResc = model.getResource(memberId.getFullId());
1553        proxyRdfResc.addProperty(PROXY_FOR, memberRdfResc);
1554        mockGetTriplesForDC(proxyId, null, model);
1555        if (!isHead) {
1556            mockGetTriplesForDC(proxyId, lastModified, model);
1557        }
1558        membershipService.resourceCreated(transaction, proxyId);
1559        return memberId;
1560    }
1561
1562    private FedoraId createDirectContainer(final FedoraId membershipRescId, final Property relation,
1563            final boolean useIsMemberOf) {
1564        return createDirectContainer(rootId, membershipRescId, relation, useIsMemberOf);
1565    }
1566
1567    private FedoraId createDirectContainer(final FedoraId parentId, final FedoraId membershipRescId,
1568            final Property relation, final boolean useIsMemberOf) {
1569        final var dcId = mintFedoraId();
1570        mockGetHeaders(populateHeaders(dcId, parentId, RdfLexicon.DIRECT_CONTAINER));
1571        mockGetTriplesForDC(dcId, CREATED_DATE, membershipRescId, relation, useIsMemberOf);
1572        return dcId;
1573    }
1574
1575    private void mockGetTriplesForDCHead(final FedoraId dcId, final Instant startTime, final FedoraId membershipRescId,
1576            final Property relation, final boolean useIsMemberOf) {
1577        mockGetTriplesForDC(dcId, startTime, membershipRescId, relation, useIsMemberOf, true);
1578    }
1579
1580    private void mockGetTriplesForDC(final FedoraId dcId, final Instant startTime, final FedoraId membershipRescId,
1581            final Property relation, final boolean useIsMemberOf) {
1582        mockGetTriplesForDC(dcId, startTime, membershipRescId, relation, useIsMemberOf, false);
1583    }
1584
1585    private void mockGetTriplesForDC(final FedoraId dcId, final Instant startTime, final FedoraId membershipRescId,
1586            final Property relation, final boolean useIsMemberOf, final boolean isHead) {
1587        mockGetTriplesForDC(dcId, startTime, membershipRescId, relation, useIsMemberOf, null, isHead);
1588    }
1589
1590    private void mockGetTriplesForDC(final FedoraId dcId, final Instant startTime, final FedoraId membershipRescId,
1591            final Property relation, final boolean useIsMemberOf, final Property insertedContentRelation,
1592            final boolean isHead) {
1593        final var model = ModelFactory.createDefaultModel();
1594        final var dcRdfResc = model.getResource(dcId.getBaseId());
1595        final var membershipRdfResc = model.getResource(membershipRescId.getFullId());
1596        dcRdfResc.addProperty(RdfLexicon.MEMBERSHIP_RESOURCE, membershipRdfResc);
1597        if (relation != null) {
1598            if (useIsMemberOf) {
1599                dcRdfResc.addProperty(RdfLexicon.IS_MEMBER_OF_RELATION, relation);
1600            } else {
1601                dcRdfResc.addProperty(RdfLexicon.HAS_MEMBER_RELATION, relation);
1602            }
1603        }
1604        if (insertedContentRelation != null) {
1605            dcRdfResc.addProperty(RdfLexicon.INSERTED_CONTENT_RELATION, insertedContentRelation);
1606        }
1607
1608        mockGetTriplesForDC(dcId, null, model);
1609        if (!isHead) {
1610            mockGetTriplesForDC(dcId, startTime, model);
1611        }
1612    }
1613
1614    private void mockGetTriplesForDC(final FedoraId dcId, final Instant startTime, final Model model) {
1615        when(psSession.getTriples(eq(dcId), eq(startTime))).thenAnswer(new Answer<RdfStream>() {
1616            @Override
1617            public RdfStream answer(final InvocationOnMock invocation) throws Throwable {
1618                return fromModel(model.getResource(dcId.getBaseId()).asNode(), model);
1619            }
1620        });
1621    }
1622
1623    private void assertContainsMembership(final List<Triple> membershipList, final FedoraId subjectId,
1624            final Property property, final FedoraId objectId) {
1625
1626        final var subjectNode = NodeFactory.createURI(subjectId.getFullId());
1627        final var objectNode = NodeFactory.createURI(objectId.getFullId());
1628
1629        assertTrue("Membership set did not contain: " + subjectId + " " + property.getURI() + " " + objectId,
1630                membershipList.stream().anyMatch(t -> t.getSubject().equals(subjectNode)
1631                        && t.getPredicate().equals(property.asNode())
1632                        && t.getObject().equals(objectNode)));
1633    }
1634
1635    private void assertCommittedMembershipCount(final FedoraId subjectId, final int expected) {
1636        final var results = membershipService.getMembership(shortLivedTx, subjectId);
1637        assertEquals("Incorrect number of committed membership properties for " + subjectId,
1638                expected, results.count());
1639    }
1640
1641    private void assertUncommittedMembershipCount(final Transaction transaction,
1642                                                  final FedoraId subjectId,
1643                                                  final int expected) {
1644        final var results = membershipService.getMembership(transaction, subjectId);
1645        assertEquals("Incorrect number of uncommitted membership properties for " + subjectId,
1646                expected, results.count());
1647    }
1648}