Skip to content

Commit

Permalink
feat: make DidManagementApi more explicit (#226)
Browse files Browse the repository at this point in the history
* feat: make DidManagementApi more focused

* add auto-publish flag

* Add parameter doc

* javadoc
  • Loading branch information
paullatzelsperger authored Jan 17, 2024
1 parent a1db57b commit c3636e5
Show file tree
Hide file tree
Showing 6 changed files with 433 additions and 316 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
package org.eclipse.edc.identityhub.did;

import org.eclipse.edc.iam.did.spi.document.DidDocument;
import org.eclipse.edc.iam.did.spi.document.Service;
import org.eclipse.edc.identithub.did.spi.DidDocumentPublisherRegistry;
import org.eclipse.edc.identithub.did.spi.DidDocumentService;
import org.eclipse.edc.identithub.did.spi.model.DidResource;
import org.eclipse.edc.identithub.did.spi.model.DidState;
import org.eclipse.edc.identithub.did.spi.store.DidResourceStore;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.ServiceResult;
Expand All @@ -42,20 +42,6 @@ public DidDocumentServiceImpl(TransactionContext transactionContext, DidResource
this.registry = registry;
}

@Override
public ServiceResult<Void> store(DidDocument document) {
return transactionContext.execute(() -> {
var res = DidResource.Builder.newInstance()
.document(document)
.did(document.getId())
.build();
var result = didResourceStore.save(res);
return result.succeeded() ?
ServiceResult.success() :
ServiceResult.fromFailure(result);
});
}

@Override
public ServiceResult<Void> publish(String did) {
return transactionContext.execute(() -> {
Expand Down Expand Up @@ -94,59 +80,77 @@ public ServiceResult<Void> unpublish(String did) {
});
}


@Override
public ServiceResult<Void> update(DidDocument document) {
public ServiceResult<Collection<DidDocument>> queryDocuments(QuerySpec query) {
return transactionContext.execute(() -> {
// obtain existing resource from storage
var did = document.getId();
var existing = didResourceStore.findById(did);
if (existing == null) {
return ServiceResult.notFound(notFoundMessage(did));
}

//update only the did document
var updatedResource = DidResource.Builder.newInstance()
.document(document)
.did(did)
.state(existing.getState())
.createTimestamp(existing.getCreateTimestamp())
.stateTimeStamp(existing.getStateTimestamp())
.build();

var res = didResourceStore.update(updatedResource);
return res.succeeded() ?
ServiceResult.success() :
ServiceResult.fromFailure(res);
var res = didResourceStore.query(query);
return ServiceResult.success(res.stream().map(DidResource::getDocument).toList());
});
}

@Override
public ServiceResult<Void> deleteById(String did) {
public DidResource findById(String did) {
return transactionContext.execute(() -> didResourceStore.findById(did));
}

@Override
public ServiceResult<Void> addService(String did, Service service) {
return transactionContext.execute(() -> {
var existing = didResourceStore.findById(did);
if (existing == null) {
return ServiceResult.notFound(notFoundMessage(did));
var didResource = didResourceStore.findById(did);
if (didResource == null) {
return ServiceResult.notFound("DID '%s' not found.".formatted(did));
}
if (existing.getState() == DidState.PUBLISHED.code()) {
return ServiceResult.conflict("Cannot delete DID '%s' because it is already published. Un-publish first!".formatted(did));
var services = didResource.getDocument().getService();
if (services.stream().anyMatch(s -> s.getId().equals(service.getId()))) {
return ServiceResult.conflict("DID '%s' already contains a service endpoint with ID '%s'.".formatted(did, service.getId()));
}
var res = didResourceStore.deleteById(did);
return res.succeeded() ?
services.add(service);
var updateResult = didResourceStore.update(didResource);
return updateResult.succeeded() ?
ServiceResult.success() :
ServiceResult.fromFailure(res);
ServiceResult.fromFailure(updateResult);

});
}

@Override
public ServiceResult<Collection<DidDocument>> queryDocuments(QuerySpec query) {
public ServiceResult<Void> replaceService(String did, Service service) {
return transactionContext.execute(() -> {
var res = didResourceStore.query(query);
return ServiceResult.success(res.stream().map(DidResource::getDocument).toList());
var didResource = didResourceStore.findById(did);
if (didResource == null) {
return ServiceResult.notFound("DID '%s' not found.".formatted(did));
}
var services = didResource.getDocument().getService();
if (services.stream().noneMatch(s -> s.getId().equals(service.getId()))) {
return ServiceResult.badRequest("DID '%s' does not contain a service endpoint with ID '%s'.".formatted(did, service.getId()));
}
services.add(service);
var updateResult = didResourceStore.update(didResource);
return updateResult.succeeded() ?
ServiceResult.success() :
ServiceResult.fromFailure(updateResult);

});
}

@Override
public DidResource findById(String did) {
return transactionContext.execute(() -> didResourceStore.findById(did));
public ServiceResult<Void> removeService(String did, String serviceId) {
return transactionContext.execute(() -> {
var didResource = didResourceStore.findById(did);
if (didResource == null) {
return ServiceResult.notFound("DID '%s' not found.".formatted(did));
}
var services = didResource.getDocument().getService();
var hasRemoved = services.removeIf(s -> s.getId().equals(serviceId));
if (!hasRemoved) {
return ServiceResult.badRequest("DID '%s' does not contain a service endpoint with ID '%s'.".formatted(did, serviceId));
}
var updateResult = didResourceStore.update(didResource);
return updateResult.succeeded() ?
ServiceResult.success() :
ServiceResult.fromFailure(updateResult);

});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@

import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

Expand All @@ -56,22 +54,6 @@ void setUp() {
service = new DidDocumentServiceImpl(trx, storeMock, publisherRegistry);
}

@Test
void store() {
var doc = createDidDocument().build();
when(storeMock.save(argThat(dr -> dr.getDocument().equals(doc)))).thenReturn(StoreResult.success());
assertThat(service.store(doc)).isSucceeded();
}

@Test
void store_alreadyExists() {
var doc = createDidDocument().build();
when(storeMock.save(argThat(dr -> dr.getDocument().equals(doc)))).thenReturn(StoreResult.alreadyExists("foo"));
assertThat(service.store(doc)).isFailed().detail().isEqualTo("foo");
verify(storeMock).save(any());
verifyNoInteractions(publisherMock);
}

@Test
void publish() {
var doc = createDidDocument().build();
Expand Down Expand Up @@ -187,90 +169,152 @@ void unpublish_publisherReportsError() {
}

@Test
void update() {
void queryDocuments() {
var q = QuerySpec.max();
var doc = createDidDocument().build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.PUBLISHED).document(doc).build());
when(storeMock.update(any())).thenReturn(StoreResult.success());
var res = DidResource.Builder.newInstance().did(doc.getId()).state(DidState.PUBLISHED).document(doc).build();
when(storeMock.query(any())).thenReturn(List.of(res));

assertThat(service.update(doc)).isSucceeded();
assertThat(service.queryDocuments(q)).isSucceeded();

verify(storeMock).findById(did);
verify(storeMock).update(argThat(dr -> dr.getDocument().equals(doc)));
verify(storeMock).query(eq(q));
verifyNoMoreInteractions(publisherMock, storeMock, publisherRegistry);
}

@Test
void update_notExists() {
void addEndpoint() {
var doc = createDidDocument().build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(null);
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).document(doc).build());
when(storeMock.update(any())).thenReturn(StoreResult.success());
var res = service.addService(did, new Service("new-id", "test-type", "https://test.com"));
assertThat(res).isSucceeded();

assertThat(service.update(doc))
.isFailed()
verify(storeMock).findById(eq(did));
verify(storeMock).update(any());
verifyNoMoreInteractions(storeMock, publisherMock);
}

@Test
void addEndpoint_alreadyExists() {
var newService = new Service("new-id", "test-type", "https://test.com");
var doc = createDidDocument().service(List.of(newService)).build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).document(doc).build());
var res = service.addService(did, newService);
assertThat(res).isFailed()
.detail()
.isEqualTo(service.notFoundMessage(did));
.isEqualTo("DID 'did:web:testdid' already contains a service endpoint with ID 'new-id'.");

verify(storeMock).findById(did);
verifyNoMoreInteractions(publisherMock, storeMock, publisherRegistry);
verify(storeMock).findById(eq(did));
verifyNoMoreInteractions(storeMock, publisherMock);
}

@Test
void deleteById() {
void addEndpoint_didNotFound() {
var doc = createDidDocument().build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.UNPUBLISHED).document(doc).build());
when(storeMock.deleteById(any())).thenReturn(StoreResult.success());
when(storeMock.findById(eq(did))).thenReturn(null);
var res = service.addService(did, new Service("test-id", "test-type", "https://test.com"));
assertThat(res).isFailed()
.detail()
.isEqualTo("DID 'did:web:testdid' not found.");

assertThat(service.deleteById(did)).isSucceeded();
verify(storeMock).findById(eq(did));
verifyNoMoreInteractions(storeMock, publisherMock);
}

verify(storeMock).findById(did);
verify(storeMock).deleteById(did);
verifyNoMoreInteractions(publisherMock, storeMock, publisherRegistry);
@Test
void replaceEndpoint() {
var toReplace = new Service("new-id", "test-type", "https://test.com");
var doc = createDidDocument().service(List.of(toReplace)).build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).document(doc).build());
when(storeMock.update(any())).thenReturn(StoreResult.success());

var res = service.replaceService(did, toReplace);
assertThat(res).isSucceeded();

verify(storeMock).findById(eq(did));
verify(storeMock).update(any());
verifyNoMoreInteractions(storeMock, publisherMock);
}

@Test
void deleteById_alreadyPublished() {
void replaceEndpoint_doesNotExist() {
var replace = new Service("new-id", "test-type", "https://test.com");
var doc = createDidDocument().build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.PUBLISHED).document(doc).build());
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).document(doc).build());

assertThat(service.deleteById(did)).isFailed()
var res = service.replaceService(did, replace);
assertThat(res).isFailed()
.detail()
.isEqualTo("Cannot delete DID '%s' because it is already published. Un-publish first!".formatted(did));
.isEqualTo("DID 'did:web:testdid' does not contain a service endpoint with ID 'new-id'.");

verify(storeMock).findById(did);
verifyNoMoreInteractions(publisherMock, storeMock, publisherRegistry);
verify(storeMock).findById(eq(did));
verifyNoMoreInteractions(storeMock, publisherMock);
}

@Test
void deleteById_notExists() {
void replaceEndpoint_didNotFound() {
var doc = createDidDocument().build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).state(DidState.UNPUBLISHED).document(doc).build());
when(storeMock.deleteById(any())).thenReturn(StoreResult.notFound("test-message"));
when(storeMock.findById(eq(did))).thenReturn(null);
var res = service.replaceService(did, new Service("test-id", "test-type", "https://test.com"));
assertThat(res).isFailed()
.detail()
.isEqualTo("DID 'did:web:testdid' not found.");

verify(storeMock).findById(eq(did));
verifyNoMoreInteractions(storeMock, publisherMock);
}

@Test
void removeEndpoint() {
var toRemove = new Service("new-id", "test-type", "https://test.com");
var doc = createDidDocument().service(List.of(toRemove)).build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).document(doc).build());
when(storeMock.update(any())).thenReturn(StoreResult.success());

assertThat(service.deleteById(did)).isFailed().detail().isEqualTo("test-message");
var res = service.removeService(did, toRemove.getId());
assertThat(res).isSucceeded();

verify(storeMock).findById(did);
verify(storeMock).deleteById(did);
verifyNoMoreInteractions(publisherMock, storeMock, publisherRegistry);
verify(storeMock).findById(eq(did));
verify(storeMock).update(any());
verifyNoMoreInteractions(storeMock, publisherMock);
}

@Test
void queryDocuments() {
var q = QuerySpec.max();
void removeEndpoint_doesNotExist() {
var doc = createDidDocument().build();
var res = DidResource.Builder.newInstance().did(doc.getId()).state(DidState.PUBLISHED).document(doc).build();
when(storeMock.query(any())).thenReturn(List.of(res));
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(DidResource.Builder.newInstance().did(did).document(doc).build());

assertThat(service.queryDocuments(q)).isSucceeded();
var res = service.removeService(did, "not-exist-id");
assertThat(res).isFailed()
.detail().isEqualTo("DID 'did:web:testdid' does not contain a service endpoint with ID 'not-exist-id'.");

verify(storeMock).query(eq(q));
verifyNoMoreInteractions(publisherMock, storeMock, publisherRegistry);
verify(storeMock).findById(eq(did));
verifyNoMoreInteractions(storeMock, publisherMock);
}

@Test
void removeEndpoint_didNotFound() {
var doc = createDidDocument().build();
var did = doc.getId();
when(storeMock.findById(eq(did))).thenReturn(null);
var res = service.removeService(did, "does-not-matter-id");
assertThat(res).isFailed()
.detail()
.isEqualTo("DID 'did:web:testdid' not found.");

verify(storeMock).findById(eq(did));
verifyNoMoreInteractions(storeMock, publisherMock);
}


private DidDocument.Builder createDidDocument() {
return DidDocument.Builder.newInstance()
.id("did:web:testdid")
Expand Down
Loading

0 comments on commit c3636e5

Please sign in to comment.