Skip to content

Commit e2eb981

Browse files
authored
feat: implements dcp credential request status api (#624)
1 parent 6c6b6f3 commit e2eb981

File tree

18 files changed

+656
-43
lines changed

18 files changed

+656
-43
lines changed

e2e-tests/admin-api-tests/build.gradle.kts

-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ dependencies {
3636
testImplementation(project(":spi:sts-spi"))
3737
testImplementation(testFixtures(project(":e2e-tests:fixtures")))
3838
testImplementation(testFixtures(project(":spi:verifiable-credential-spi")))
39-
testImplementation(libs.mockserver.netty)
4039
}
4140

4241
edcBuild {

e2e-tests/dcp-issuance-tests/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies {
2929
testImplementation(libs.restAssured)
3030
testImplementation(libs.awaitility)
3131
testImplementation(testFixtures(project(":e2e-tests:fixtures")))
32+
testImplementation(libs.mockserver.netty)
3233
}
3334

3435
edcBuild {

e2e-tests/admin-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/DcpIssuanceApiEndToEndTest.java renamed to e2e-tests/dcp-issuance-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/api/DcpCredentialRequestApiEndToEndTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*
1313
*/
1414

15-
package org.eclipse.edc.identityhub.tests.dcp;
15+
package org.eclipse.edc.identityhub.tests.dcp.api;
1616

1717
import com.nimbusds.jose.JOSEException;
1818
import com.nimbusds.jose.jwk.Curve;
@@ -75,7 +75,7 @@
7575
import static org.mockserver.model.HttpResponse.response;
7676

7777
@SuppressWarnings("JUnitMalformedDeclaration")
78-
public class DcpIssuanceApiEndToEndTest {
78+
public class DcpCredentialRequestApiEndToEndTest {
7979

8080
protected static final DidPublicKeyResolver DID_PUBLIC_KEY_RESOLVER = mock();
8181
protected static final DidResolverRegistry DID_RESOLVER_REGISTRY = mock();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
/*
2+
* Copyright (c) 2025 Cofinity-X
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* Cofinity-X - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.identityhub.tests.dcp.api;
16+
17+
import com.nimbusds.jose.JOSEException;
18+
import com.nimbusds.jose.jwk.Curve;
19+
import com.nimbusds.jose.jwk.ECKey;
20+
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
21+
import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver;
22+
import org.eclipse.edc.identityhub.tests.fixtures.issuerservice.IssuerServiceEndToEndExtension;
23+
import org.eclipse.edc.identityhub.tests.fixtures.issuerservice.IssuerServiceEndToEndTestContext;
24+
import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationDefinitionValidatorRegistry;
25+
import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSourceFactory;
26+
import org.eclipse.edc.issuerservice.spi.issuance.attestation.AttestationSourceFactoryRegistry;
27+
import org.eclipse.edc.issuerservice.spi.issuance.credentialdefinition.CredentialDefinitionService;
28+
import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcess;
29+
import org.eclipse.edc.issuerservice.spi.issuance.model.IssuanceProcessStates;
30+
import org.eclipse.edc.issuerservice.spi.issuance.process.store.IssuanceProcessStore;
31+
import org.eclipse.edc.issuerservice.spi.participant.ParticipantService;
32+
import org.eclipse.edc.issuerservice.spi.participant.model.Participant;
33+
import org.eclipse.edc.junit.annotations.EndToEndTest;
34+
import org.eclipse.edc.junit.annotations.PostgresqlIntegrationTest;
35+
import org.eclipse.edc.spi.query.QuerySpec;
36+
import org.eclipse.edc.spi.result.Result;
37+
import org.eclipse.edc.sql.testfixtures.PostgresqlEndToEndExtension;
38+
import org.eclipse.edc.validator.spi.ValidationResult;
39+
import org.jetbrains.annotations.NotNull;
40+
import org.junit.jupiter.api.AfterEach;
41+
import org.junit.jupiter.api.BeforeAll;
42+
import org.junit.jupiter.api.Nested;
43+
import org.junit.jupiter.api.Order;
44+
import org.junit.jupiter.api.Test;
45+
import org.junit.jupiter.api.extension.BeforeAllCallback;
46+
import org.junit.jupiter.api.extension.RegisterExtension;
47+
48+
import java.util.Base64;
49+
import java.util.Map;
50+
import java.util.UUID;
51+
52+
import static io.restassured.http.ContentType.JSON;
53+
import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
54+
import static org.assertj.core.api.Assertions.assertThat;
55+
import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.JwtCreationUtil.generateJwt;
56+
import static org.eclipse.edc.identityhub.verifiablecredentials.testfixtures.VerifiableCredentialTestUtil.generateEcKey;
57+
import static org.mockito.ArgumentMatchers.eq;
58+
import static org.mockito.Mockito.mock;
59+
import static org.mockito.Mockito.when;
60+
61+
@SuppressWarnings("JUnitMalformedDeclaration")
62+
public class DcpCredentialRequestStatusApiEndToEndTest {
63+
64+
protected static final DidPublicKeyResolver DID_PUBLIC_KEY_RESOLVER = mock();
65+
66+
abstract static class Tests {
67+
68+
public static final String ISSUER_DID = "did:web:issuer";
69+
public static final String PARTICIPANT_DID = "did:web:participant";
70+
public static final String DID_WEB_PARTICIPANT_KEY_1 = "did:web:participant#key1";
71+
public static final ECKey PARTICIPANT_KEY = generateEcKey(DID_WEB_PARTICIPANT_KEY_1);
72+
protected static final AttestationSourceFactory ATTESTATION_SOURCE_FACTORY = mock();
73+
protected static final String ISSUER_ID = "issuer";
74+
private static final String ISSUER_ID_ENCODED = Base64.getUrlEncoder().encodeToString(ISSUER_ID.getBytes());
75+
76+
77+
@BeforeAll
78+
static void beforeAll(IssuerServiceEndToEndTestContext context) {
79+
var pipelineFactory = context.getRuntime().getService(AttestationSourceFactoryRegistry.class);
80+
var validationRegistry = context.getRuntime().getService(AttestationDefinitionValidatorRegistry.class);
81+
pipelineFactory.registerFactory("Attestation", ATTESTATION_SOURCE_FACTORY);
82+
validationRegistry.registerValidator("Attestation", def -> ValidationResult.success());
83+
context.createParticipant(ISSUER_ID);
84+
}
85+
86+
private static @NotNull String issuanceStatusUrl(String id) {
87+
return "/v1alpha/participants/%s/requests/%s".formatted(ISSUER_ID_ENCODED, id);
88+
}
89+
90+
91+
@AfterEach
92+
void teardown(ParticipantService participantService, CredentialDefinitionService credentialDefinitionService) {
93+
participantService.queryParticipants(QuerySpec.max()).getContent()
94+
.forEach(p -> participantService.deleteParticipant(p.participantId()).getContent());
95+
96+
credentialDefinitionService.queryCredentialDefinitions(QuerySpec.max()).getContent()
97+
.forEach(c -> credentialDefinitionService.deleteCredentialDefinition(c.getId()).getContent());
98+
}
99+
100+
@Test
101+
void credentialStatus(IssuerServiceEndToEndTestContext context, ParticipantService participantService,
102+
IssuanceProcessStore issuanceProcessStore) throws JOSEException {
103+
104+
var process = createIssuanceProcess();
105+
106+
participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant"));
107+
issuanceProcessStore.save(process);
108+
109+
var token = generateSiToken();
110+
111+
112+
when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(DID_WEB_PARTICIPANT_KEY_1))).thenReturn(Result.success(PARTICIPANT_KEY.toPublicKey()));
113+
114+
var response = context.getDcpIssuanceEndpoint().baseRequest()
115+
.contentType(JSON)
116+
.header(AUTHORIZATION, token)
117+
.get(issuanceStatusUrl(process.getId()))
118+
.then()
119+
.log().ifValidationFails()
120+
.statusCode(200)
121+
.extract()
122+
.body().as(Map.class);
123+
124+
125+
assertThat(response.get("holderPid")).isEqualTo(process.getHolderPid());
126+
assertThat(response.get("issuerPid")).isEqualTo(process.getId());
127+
assertThat(response.get("status")).isEqualTo("ISSUED");
128+
129+
}
130+
131+
@Test
132+
void credentialStatus_wrongParticipant_shouldReturn401(IssuerServiceEndToEndTestContext context, ParticipantService participantService,
133+
IssuanceProcessStore issuanceProcessStore) throws JOSEException {
134+
135+
var process = createIssuanceProcess();
136+
var wrongParticipant = "wrong-participant";
137+
var wrongParticipantDid = "did:web:%s".formatted(wrongParticipant);
138+
var wrongParticipantKeyId = "%s#key1".formatted(wrongParticipantDid);
139+
var wrongParticipantKey = generateEcKey(wrongParticipantKeyId);
140+
141+
participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant"));
142+
participantService.createParticipant(new Participant(wrongParticipant, wrongParticipantDid, "WrongParticipant"));
143+
144+
issuanceProcessStore.save(process);
145+
146+
var token = generateSiToken(ISSUER_DID, wrongParticipantDid, wrongParticipantKey);
147+
148+
when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(wrongParticipantKeyId))).thenReturn(Result.success(wrongParticipantKey.toPublicKey()));
149+
150+
context.getDcpIssuanceEndpoint().baseRequest()
151+
.contentType(JSON)
152+
.header(AUTHORIZATION, token)
153+
.get(issuanceStatusUrl(process.getId()))
154+
.then()
155+
.log().ifValidationFails()
156+
.statusCode(401);
157+
158+
}
159+
160+
@Test
161+
void credentialStatus_tokenNotPresent_shouldReturn401(IssuerServiceEndToEndTestContext context) {
162+
context.getDcpIssuanceEndpoint().baseRequest()
163+
.contentType(JSON)
164+
.get(issuanceStatusUrl("credentialRequestId"))
165+
.then()
166+
.log().ifValidationFails()
167+
.statusCode(401);
168+
169+
}
170+
171+
@Test
172+
void credentialStatus_participantNotFound_shouldReturn401(IssuerServiceEndToEndTestContext context) {
173+
var token = generateSiToken();
174+
175+
context.getDcpIssuanceEndpoint().baseRequest()
176+
.contentType(JSON)
177+
.header(AUTHORIZATION, token)
178+
.get(issuanceStatusUrl("credentialRequestId"))
179+
.then()
180+
.log().ifValidationFails()
181+
.statusCode(401);
182+
183+
}
184+
185+
@Test
186+
void credentialStatus_tokenVerificationFails_shouldReturn401(IssuerServiceEndToEndTestContext context, ParticipantService participantService, IssuanceProcessStore issuanceProcessStore) throws JOSEException {
187+
188+
var process = createIssuanceProcess();
189+
190+
participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant"));
191+
issuanceProcessStore.save(process);
192+
193+
participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant"));
194+
195+
var spoofedKey = new ECKeyGenerator(Curve.P_256).keyID(DID_WEB_PARTICIPANT_KEY_1).generate();
196+
197+
var token = generateSiToken();
198+
199+
when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(DID_WEB_PARTICIPANT_KEY_1))).thenReturn(Result.success(spoofedKey.toPublicKey()));
200+
201+
context.getDcpIssuanceEndpoint().baseRequest()
202+
.contentType(JSON)
203+
.header(AUTHORIZATION, token)
204+
.get(issuanceStatusUrl(process.getId()))
205+
.then()
206+
.log().ifValidationFails()
207+
.statusCode(401);
208+
209+
}
210+
211+
@Test
212+
void credentialStatus_wrongTokenAudience_shouldReturn401(IssuerServiceEndToEndTestContext context, ParticipantService participantService, IssuanceProcessStore issuanceProcessStore) throws JOSEException {
213+
214+
var process = createIssuanceProcess();
215+
216+
generateEcKey(DID_WEB_PARTICIPANT_KEY_1);
217+
218+
participantService.createParticipant(new Participant(PARTICIPANT_DID, PARTICIPANT_DID, "Participant"));
219+
issuanceProcessStore.save(process);
220+
221+
222+
var token = generateSiToken("wrong-audience");
223+
224+
when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq(DID_WEB_PARTICIPANT_KEY_1))).thenReturn(Result.success(PARTICIPANT_KEY.toPublicKey()));
225+
226+
context.getDcpIssuanceEndpoint().baseRequest()
227+
.contentType(JSON)
228+
.header(AUTHORIZATION, token)
229+
.get(issuanceStatusUrl(process.getId()))
230+
.then()
231+
.log().ifValidationFails()
232+
.statusCode(401);
233+
234+
}
235+
236+
237+
private String generateSiToken() {
238+
return generateSiToken(ISSUER_DID);
239+
}
240+
241+
private String generateSiToken(String audience) {
242+
return generateSiToken(audience, PARTICIPANT_DID, PARTICIPANT_KEY);
243+
}
244+
245+
private String generateSiToken(String audience, String participantDid, ECKey participantKey) {
246+
return generateJwt(audience, participantDid, participantDid, Map.of(), participantKey);
247+
}
248+
249+
private IssuanceProcess createIssuanceProcess() {
250+
return IssuanceProcess.Builder.newInstance()
251+
.id(UUID.randomUUID().toString())
252+
.state(IssuanceProcessStates.DELIVERED.code())
253+
.participantId(PARTICIPANT_DID)
254+
.issuerContextId(ISSUER_ID)
255+
.holderPid(UUID.randomUUID().toString())
256+
.build();
257+
}
258+
259+
}
260+
261+
262+
@Nested
263+
@EndToEndTest
264+
@Order(1)
265+
class InMemory extends Tests {
266+
267+
268+
@RegisterExtension
269+
static IssuerServiceEndToEndExtension runtime;
270+
271+
static {
272+
runtime = new IssuerServiceEndToEndExtension.InMemory();
273+
runtime.registerServiceMock(DidPublicKeyResolver.class, DID_PUBLIC_KEY_RESOLVER);
274+
}
275+
276+
}
277+
278+
@Nested
279+
@PostgresqlIntegrationTest
280+
class Postgres extends Tests {
281+
282+
@Order(0)
283+
@RegisterExtension
284+
static final PostgresqlEndToEndExtension POSTGRESQL_EXTENSION = new PostgresqlEndToEndExtension();
285+
286+
private static final String ISSUER = "issuer";
287+
288+
@Order(1)
289+
@RegisterExtension
290+
static final BeforeAllCallback POSTGRES_CONTAINER_STARTER = context -> {
291+
POSTGRESQL_EXTENSION.createDatabase(ISSUER);
292+
};
293+
294+
@Order(2)
295+
@RegisterExtension
296+
static final IssuerServiceEndToEndExtension ISSUER_SERVICE = IssuerServiceEndToEndExtension.Postgres
297+
.withConfig(cfg -> POSTGRESQL_EXTENSION.configFor(ISSUER));
298+
299+
static {
300+
ISSUER_SERVICE.registerServiceMock(DidPublicKeyResolver.class, DID_PUBLIC_KEY_RESOLVER);
301+
}
302+
}
303+
}
+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*
1313
*/
1414

15-
package org.eclipse.edc.identityhub.tests.dcp;
15+
package org.eclipse.edc.identityhub.tests.dcp.flow;
1616

1717
import io.restassured.http.Header;
1818
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderRequestState;

protocols/dcp/dcp-issuer/dcp-issuer-api/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ plugins {
2222
dependencies {
2323
api(project(":spi:identity-hub-spi"))
2424
api(project(":spi:verifiable-credential-spi"))
25+
api(project(":spi:issuerservice:issuerservice-issuance-spi"))
2526
api(project(":protocols:dcp:dcp-spi"))
2627
api(libs.edc.spi.jsonld)
2728
api(libs.edc.spi.jwt)

0 commit comments

Comments
 (0)