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