Skip to content

Commit

Permalink
added controller tests
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Oct 24, 2023
1 parent 05f8e23 commit 7c5e2e2
Show file tree
Hide file tree
Showing 14 changed files with 515 additions and 10 deletions.
4 changes: 4 additions & 0 deletions core/identity-hub-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ plugins {

dependencies {
api(libs.edc.spi.jsonld)
api(libs.edc.spi.core)
api(project(":spi:identity-hub-spi"))
implementation(libs.edc.spi.validator)
implementation(libs.edc.spi.web)
implementation(libs.jakarta.rsApi)
testImplementation(libs.edc.junit)
testImplementation(libs.edc.ext.jsonld)
testImplementation(testFixtures(libs.edc.core.jersey))
testImplementation(testFixtures(project(":spi:identity-hub-spi")))
testImplementation(libs.nimbus.jwt)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

package org.eclipse.edc.identityservice.api;

import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator;
import org.eclipse.edc.identityhub.spi.model.PresentationQuery;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.identityservice.api.v1.PresentationApiController;
import org.eclipse.edc.identityservice.api.validation.PresentationQueryValidator;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
Expand All @@ -35,10 +38,19 @@ public class PresentationApiExtension implements ServiceExtension {
@Inject
private WebService webService;

@Inject
private AccessTokenVerifier accessTokenVerifier;

@Inject
private CredentialQueryResolver credentialResolver;

@Inject
private PresentationGenerator presentationGenerator;

@Override
public void initialize(ServiceExtensionContext context) {
validatorRegistry.register(PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY, new PresentationQueryValidator());
var controller = new PresentationApiController(validatorRegistry, typeTransformer);
var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, presentationGenerator, context.getMonitor());

webService.registerResource("presentation", controller);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.json.JsonObject;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.identityservice.api.v1.ApiSchema.ApiErrorDetailSchema;

@OpenAPIDefinition(
Expand Down Expand Up @@ -58,5 +59,5 @@ public interface PresentationApi {
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetailSchema.class)), mediaType = "application/json"))
}
)
JsonObject queryPresentation(JsonObject query);
Response queryPresentation(String authHeader, JsonObject query);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,25 @@
package org.eclipse.edc.identityservice.api.v1;

import jakarta.json.JsonObject;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator;
import org.eclipse.edc.identityhub.spi.model.PresentationQuery;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;
import org.eclipse.edc.web.spi.ApiErrorDetail;
import org.eclipse.edc.web.spi.exception.AuthenticationFailedException;
import org.eclipse.edc.web.spi.exception.InvalidRequestException;
import org.eclipse.edc.web.spi.exception.NotAuthorizedException;
import org.eclipse.edc.web.spi.exception.ValidationFailureException;

import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
import static org.eclipse.edc.identityhub.spi.model.PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY;


Expand All @@ -30,20 +42,60 @@ public class PresentationApiController implements PresentationApi {

private final JsonObjectValidatorRegistry validatorRegistry;
private final TypeTransformerRegistry transformerRegistry;
private final CredentialQueryResolver queryResolver;
private final AccessTokenVerifier accessTokenVerifier;
private final PresentationGenerator presentationGenerator;
private final Monitor monitor;

public PresentationApiController(JsonObjectValidatorRegistry validatorRegistry, TypeTransformerRegistry transformerRegistry) {
public PresentationApiController(JsonObjectValidatorRegistry validatorRegistry, TypeTransformerRegistry transformerRegistry, CredentialQueryResolver queryResolver, AccessTokenVerifier accessTokenVerifier,
PresentationGenerator presentationGenerator, Monitor monitor) {
this.validatorRegistry = validatorRegistry;
this.transformerRegistry = transformerRegistry;
this.queryResolver = queryResolver;
this.accessTokenVerifier = accessTokenVerifier;
this.presentationGenerator = presentationGenerator;
this.monitor = monitor;
}


@POST
@Path("query")
@Override
public JsonObject queryPresentation(JsonObject query) {
public Response queryPresentation(@HeaderParam(AUTHORIZATION) String token, JsonObject query) {
if (token == null) {
throw new AuthenticationFailedException("Authorization header missing");
}
validatorRegistry.validate(PRESENTATION_QUERY_TYPE_PROPERTY, query).orElseThrow(ValidationFailureException::new);

var transformResult = transformerRegistry.transform(query, PresentationQuery.class);
var presentationQuery = transformerRegistry.transform(query, PresentationQuery.class).orElseThrow(InvalidRequestException::new);

if (presentationQuery.getPresentationDefinition() != null) {
monitor.warning("DIF Presentation Queries are not supported yet. This will get implemented in future iterations.");
return notImplemented();
}

// verify and validate the requestor's SI token
var issuerScopes = accessTokenVerifier.verify(token).orElseThrow(f -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail())));

// query the database
var credentials = queryResolver.query(presentationQuery, issuerScopes).orElseThrow(f -> new NotAuthorizedException(f.getFailureDetail()));

return null;
// package the credentials in a VP and sign
var presentationResponse = presentationGenerator.createPresentation(credentials, presentationQuery.getPresentationDefinition())
.orElseThrow(failure -> new EdcException("Error creating VerifiablePresentation: %s".formatted(failure.getFailureDetail())));
return Response.ok()
.entity(presentationResponse)
.build();
}

private Response notImplemented() {
var error = ApiErrorDetail.Builder.newInstance()
.message("Not implemented.")
.type("Not implemented.")
.build();
return Response.status(503)
.entity(error)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.identityservice.api.v1;

import com.nimbusds.jwt.JWTClaimsSet;
import jakarta.json.JsonObject;
import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator;
import org.eclipse.edc.identityhub.spi.model.InputDescriptorMapping;
import org.eclipse.edc.identityhub.spi.model.PresentationQuery;
import org.eclipse.edc.identityhub.spi.model.PresentationResponse;
import org.eclipse.edc.identityhub.spi.model.PresentationSubmission;
import org.eclipse.edc.identityhub.spi.model.presentationdefinition.PresentationDefinition;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;
import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase;
import org.eclipse.edc.web.spi.ApiErrorDetail;
import org.eclipse.edc.web.spi.exception.AuthenticationFailedException;
import org.eclipse.edc.web.spi.exception.InvalidRequestException;
import org.eclipse.edc.web.spi.exception.NotAuthorizedException;
import org.eclipse.edc.web.spi.exception.ValidationFailureException;
import org.junit.jupiter.api.Test;

import java.sql.Date;
import java.time.Instant;
import java.util.List;
import java.util.UUID;

import static jakarta.json.Json.createObjectBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.buildSignedJwt;
import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateEcKey;
import static org.eclipse.edc.identityhub.spi.model.PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY;
import static org.eclipse.edc.validator.spi.ValidationResult.failure;
import static org.eclipse.edc.validator.spi.ValidationResult.success;
import static org.eclipse.edc.validator.spi.Violation.violation;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

@SuppressWarnings("resource")
class PresentationApiControllerTest extends RestControllerTestBase {

private final JsonObjectValidatorRegistry validatorRegistryMock = mock();
private final TypeTransformerRegistry typeTransformerRegistry = mock();
private final CredentialQueryResolver queryResolver = mock();
private final AccessTokenVerifier accessTokenVerifier = mock();
private final PresentationGenerator generator = mock();


@Test
void query_tokenNotPresent_shouldReturn401() {
assertThatThrownBy(() -> controller().queryPresentation(null, createObjectBuilder().build()))
.isInstanceOf(AuthenticationFailedException.class)
.hasMessage("Authorization header missing");
}

@Test
void query_validationError_shouldReturn400() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(failure(violation("foo", "bar")));

assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build()))
.isInstanceOf(ValidationFailureException.class)
.hasMessage("foo");
}

@Test
void query_transformationError_shouldReturn400() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.failure("cannot transform"));

assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build()))
.isInstanceOf(InvalidRequestException.class)
.hasMessage("cannot transform");
verifyNoInteractions(accessTokenVerifier, queryResolver, generator);
}

@Test
void query_withPresentationDefinition_shouldReturn501() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder()
.presentationDefinition(PresentationDefinition.Builder.newInstance().id(UUID.randomUUID().toString()).build());
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder.build()));

var response = controller().queryPresentation(generateJwt(), createObjectBuilder().build());
assertThat(response.getStatus()).isEqualTo(503);
assertThat(response.getEntity()).extracting(o -> (ApiErrorDetail) o).satisfies(ed -> {
assertThat(ed.getMessage()).isEqualTo("Not implemented.");
assertThat(ed.getType()).isEqualTo("Not implemented.");
});
verifyNoInteractions(accessTokenVerifier, queryResolver, generator);
}


@Test
void query_tokenVerificationFails_shouldReturn401() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.failure("test-failure"));

assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build()))
.isExactlyInstanceOf(AuthenticationFailedException.class)
.hasMessage("ID token verification failed: test-failure");
verifyNoInteractions(queryResolver, generator);
}

@Test
void query_queryResolutionFails_shouldReturn403() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(Result.failure("test-failure"));

assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build()))
.isInstanceOf(NotAuthorizedException.class)
.hasMessage("test-failure");
verifyNoInteractions(generator);
}

@Test
void query_presentationGenerationFails_shouldReturn500() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(Result.success(List.of()));

when(generator.createPresentation(anyList(), any())).thenReturn(Result.failure("test-failure"));

assertThatThrownBy(() -> controller().queryPresentation(generateJwt(), createObjectBuilder().build()))
.isExactlyInstanceOf(EdcException.class)
.hasMessage("Error creating VerifiablePresentation: test-failure");
}

@Test
void query_success() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQuery.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(Result.success(List.of()));

var pres = new PresentationResponse(generateJwt(), new PresentationSubmission("id", "def-id", List.of(new InputDescriptorMapping("id", "ldp_vp", "$.verifiableCredentials[0]"))));
when(generator.createPresentation(anyList(), any())).thenReturn(Result.success(pres));

var response = controller().queryPresentation(generateJwt(), createObjectBuilder().build());
assertThat(response).isNotNull();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getEntity()).isEqualTo(pres);

}

@Override
protected PresentationApiController controller() {
return new PresentationApiController(validatorRegistryMock, typeTransformerRegistry, queryResolver, accessTokenVerifier, generator, mock());
}

private String generateJwt() {
var ecKey = generateEcKey();
var jwt = buildSignedJwt(new JWTClaimsSet.Builder().audience("test-audience")
.expirationTime(Date.from(Instant.now().plusSeconds(3600)))
.issuer("test-issuer")
.subject("test-subject")
.jwtID(UUID.randomUUID().toString()).build(), ecKey);

return jwt.serialize();
}

private PresentationQuery.Builder createPresentationQueryBuilder() {
return PresentationQuery.Builder.newinstance()
.scopes(List.of("test-scope1", "test-scope2"));
}
}
10 changes: 6 additions & 4 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,17 @@ edc-spi-transform = { module = "org.eclipse.edc:transform-spi", version.ref = "e
edc-spi-transaction-datasource = { module = "org.eclipse.edc:transaction-datasource-spi", version.ref = "edc" }
edc-spi-identity-did = { module = "org.eclipse.edc:identity-did-spi", version.ref = "edc" }
edc-spi-aggregate-service = { module = "org.eclipse.edc:aggregate-service-spi", version.ref = "edc" }
edc-spi-jsonld = { module="org.eclipse.edc:json-ld-spi", version.ref= "edc"}
edc-spi-validator = { module="org.eclipse.edc:validator-spi", version.ref= "edc"}
edc-spi-web = { module="org.eclipse.edc:web-spi", version.ref= "edc"}
edc-spi-jsonld = { module = "org.eclipse.edc:json-ld-spi", version.ref = "edc" }
edc-spi-validator = { module = "org.eclipse.edc:validator-spi", version.ref = "edc" }
edc-spi-web = { module = "org.eclipse.edc:web-spi", version.ref = "edc" }
edc-spi-identitytrust = { module = "org.eclipse.edc:identity-trust-spi", version.ref = "edc" }
edc-core-connector = { module = "org.eclipse.edc:connector-core", version.ref = "edc" }
edc-core-controlPlane = { module = "org.eclipse.edc:control-plane-core", version.ref = "edc" }
edc-core-micrometer = { module = "org.eclipse.edc:micrometer-core", version.ref = "edc" }
edc-core-api = { module = "org.eclipse.edc:api-core", version.ref = "edc" }
edc-core-stateMachine = { module = "org.eclipse.edc:state-machine", version.ref = "edc" }
edc-core-sql = { module = "org.eclipse.edc:sql-core", version.ref = "edc" }
edc-core-jersey = { module = "org.eclipse.edc:jersey-core", version.ref = "edc" }
edc-core-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" }
edc-core-transform = { module = "org.eclipse.edc:transform-core", version.ref = "edc" }
edc-ext-identity-did-crypto = { module = "org.eclipse.edc:identity-did-crypto", version.ref = "edc" }
Expand All @@ -58,7 +60,7 @@ swagger-jaxrs = { module = "io.swagger.core.v3:swagger-jaxrs2-jakarta", version.
jakarta-rsApi = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version.ref = "rsApi" }

[bundles]
connector = [ "edc-boot", "edc-core-connector", "edc-ext-http", "edc-ext-observability", "edc-ext-jsonld"]
connector = ["edc-boot", "edc-core-connector", "edc-ext-http", "edc-ext-observability", "edc-ext-jsonld"]

[plugins]
shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }
Loading

0 comments on commit 7c5e2e2

Please sign in to comment.