Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make DidManagementApi more explicit #226

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading