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