value() {
+ return PassportService.dataUseToTermArray(dataset.getDataUse());
+ }
+
+ @Override
+ public String source() {
+ return PassportService.ISS;
+ }
+
+ @Override
+ public String by() {
+ return VisaBy.DAC.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/org/broadinstitute/consent/http/service/passport/ControlledAccessGrants.java b/src/main/java/org/broadinstitute/consent/http/service/passport/ControlledAccessGrants.java
index 11afe011fa..d2aef0d741 100644
--- a/src/main/java/org/broadinstitute/consent/http/service/passport/ControlledAccessGrants.java
+++ b/src/main/java/org/broadinstitute/consent/http/service/passport/ControlledAccessGrants.java
@@ -1,7 +1,7 @@
package org.broadinstitute.consent.http.service.passport;
+import java.time.Instant;
import java.util.Calendar;
-import java.util.Date;
import org.broadinstitute.consent.http.models.ApprovedDataset;
/**
@@ -32,12 +32,12 @@ public Long asserted() {
calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR) - 1);
return PassportService.getEpochSeconds(calendar.toInstant());
}
- // If there is no expiration date, we will use the current time as the asserted time.
- return PassportService.getEpochSeconds(new Date().toInstant());
+ // If there is no expiration date, use the current time as the asserted time.
+ return PassportService.getEpochSeconds(Instant.now());
}
@Override
- public String value() {
+ public Object value() {
return String.format(
"%s/dataset/%s", PassportService.ISS, approvedDataset.getDatasetIdentifier());
}
diff --git a/src/main/java/org/broadinstitute/consent/http/service/passport/OversightBodiesVisa.java b/src/main/java/org/broadinstitute/consent/http/service/passport/OversightBodiesVisa.java
new file mode 100644
index 0000000000..04b456b7b8
--- /dev/null
+++ b/src/main/java/org/broadinstitute/consent/http/service/passport/OversightBodiesVisa.java
@@ -0,0 +1,54 @@
+package org.broadinstitute.consent.http.service.passport;
+
+import java.time.Instant;
+import org.broadinstitute.consent.http.models.Dac;
+
+/**
+ * Data Passport visa describing the entity responsible for governing access to the dataset. Maps to
+ * the DAC (Data Access Committee) that oversees the dataset in DUOS.
+ *
+ * @see GA4GH Data Passports
+ * specification
+ */
+public class OversightBodiesVisa implements VisaClaimType {
+
+ private final Dac dac;
+
+ public OversightBodiesVisa(Dac dac) {
+ this.dac = dac;
+ }
+
+ @Override
+ public String type() {
+ return VisaClaimTypes.OVERSIGHT_BODIES.type;
+ }
+
+ @Override
+ public Long asserted() {
+ if (dac.getCreateDate() != null) {
+ // java.sql.Date#toInstant throws UnsupportedOperationException; use epoch millis instead.
+ return PassportService.getEpochSeconds(Instant.ofEpochMilli(dac.getCreateDate().getTime()));
+ }
+ return PassportService.getEpochSeconds(Instant.now());
+ }
+
+ /**
+ * Returns a stable URL identifying the DAC within DUOS. Consumers can dereference this URL to
+ * retrieve details about the oversight body, including its members and chairpersons. TODO: We
+ * need a public and stable identifier to point users to for DACs.
+ */
+ @Override
+ public Object value() {
+ return "%s/dac/%d".formatted(PassportService.ISS, dac.getDacId());
+ }
+
+ @Override
+ public String source() {
+ return PassportService.ISS;
+ }
+
+ @Override
+ public String by() {
+ return VisaBy.DAC.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/org/broadinstitute/consent/http/service/passport/PassportService.java b/src/main/java/org/broadinstitute/consent/http/service/passport/PassportService.java
index 6763b3605a..e0ae32fc19 100644
--- a/src/main/java/org/broadinstitute/consent/http/service/passport/PassportService.java
+++ b/src/main/java/org/broadinstitute/consent/http/service/passport/PassportService.java
@@ -3,6 +3,7 @@
import com.google.inject.Inject;
import jakarta.ws.rs.NotFoundException;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -11,9 +12,13 @@
import java.util.stream.Stream;
import org.broadinstitute.consent.http.db.DatasetDAO;
import org.broadinstitute.consent.http.models.ApprovedDataset;
+import org.broadinstitute.consent.http.models.Dac;
+import org.broadinstitute.consent.http.models.DataUse;
+import org.broadinstitute.consent.http.models.Dataset;
import org.broadinstitute.consent.http.models.DuosUser;
import org.broadinstitute.consent.http.models.User;
import org.broadinstitute.consent.http.models.sam.UserStatusInfo;
+import org.broadinstitute.consent.http.service.DacService;
import org.broadinstitute.consent.http.util.ConsentLogger;
/** GA4GH Passport */
@@ -23,10 +28,12 @@ public class PassportService implements ConsentLogger {
public static final int EXPIRATION_SECONDS = 3600;
private final DatasetDAO datasetDAO;
+ private final DacService dacService;
@Inject
- public PassportService(DatasetDAO datasetDAO) {
+ public PassportService(DatasetDAO datasetDAO, DacService dacService) {
this.datasetDAO = datasetDAO;
+ this.dacService = dacService;
}
public PassportClaim generatePassport(DuosUser duosUser) {
@@ -57,6 +64,70 @@ public PassportClaim generatePassport(DuosUser duosUser) {
return new PassportClaim(allVisas);
}
+ /**
+ * Generates a Data Passport for a specific dataset, as proposed in the GA4GH Data Passports
+ * specification (see GA4GH Data Passports).
+ * The returned {@link PassportClaim} uses the same envelope as a Researcher Passport but contains
+ * dataset-centric visas:
+ *
+ *
+ * - {@link ApprovedUsersVisa} — links to the dataset's approved user API endpoint
+ *
- {@link ConsentedDataUseTermsVisa} — links to the dataset's DUO-coded data use terms
+ *
- {@link OversightBodiesVisa} — identifies the DAC governing the dataset
+ *
- {@link RequiredAgreementsVisa} — references the DAA users must accept (if one exists)
+ *
+ *
+ * The {@code sub} field of each visa is the dataset identifier (e.g. {@code DUOS-000001})
+ * rather than a user subject ID, reflecting the dataset-centric nature of the passport.
+ *
+ * @param datasetIdentifier the formatted DUOS identifier, e.g. {@code DUOS-000001}
+ * @return a {@link PassportClaim} containing the Data Passport visas for the dataset
+ * @throws NotFoundException if the dataset does not exist
+ */
+ public PassportClaim generateDataPassport(String datasetIdentifier) {
+ Integer alias = Dataset.parseIdentifierToAlias(datasetIdentifier);
+ Dataset dataset = datasetDAO.findDatasetByAlias(alias);
+ if (dataset == null) {
+ throw new NotFoundException("Dataset not found: " + datasetIdentifier);
+ }
+
+ List visas = new ArrayList<>();
+
+ // ApprovedUsers - links to the API endpoint describing approved users for the dataset
+ visas.add(visaFromVisaClaimType(datasetIdentifier, new ApprovedUsersVisa(datasetIdentifier)));
+
+ // ConsentedDataUseTerms — always present if the dataset exists
+ visas.add(visaFromVisaClaimType(datasetIdentifier, new ConsentedDataUseTermsVisa(dataset)));
+
+ // OversightBodies + RequiredAgreements — only when the dataset is associated with a DAC
+ if (dataset.getDacId() != null) {
+ try {
+ Dac dac = dacService.findById(dataset.getDacId());
+ addDacBackedVisas(datasetIdentifier, visas, dac);
+ } catch (UnsupportedOperationException e) {
+ logWarn(
+ "Unable to build DAC-backed visas for dataset %s; returning consented-data-use visa only"
+ .formatted(datasetIdentifier),
+ e);
+ }
+ }
+
+ return new PassportClaim(visas);
+ }
+
+ private void addDacBackedVisas(String datasetIdentifier, List visas, Dac dac) {
+ if (dac == null) {
+ return;
+ }
+ visas.add(visaFromVisaClaimType(datasetIdentifier, new OversightBodiesVisa(dac)));
+ if (dac.getAssociatedDaa() != null) {
+ visas.add(
+ visaFromVisaClaimType(
+ datasetIdentifier, new RequiredAgreementsVisa(dac.getAssociatedDaa())));
+ }
+ }
+
protected List buildControlledAccessGrants(
String userSubjectId, List approvedDatasets) {
return approvedDatasets.stream()
@@ -87,4 +158,88 @@ private Visa visaFromVisaClaimType(String userSubjectId, VisaClaimType type) {
public static long getEpochSeconds(Instant instant) {
return instant.getEpochSecond();
}
+
+ public static String getApprovedUsersEndpoint(String datasetIdentifier) {
+ return "https://consent.dsde-prod.broadinstitute.org/api/datataset/%s/approvedUsers"
+ .formatted(datasetIdentifier);
+ }
+
+ /**
+ * Converts a {@link DataUse} object to a list of GA4GH DUO ontology term identifiers that
+ * reflect the active data use conditions. Boolean fields are included when {@code true};
+ * String/List fields are included when non-blank/non-empty. Fields that have no standard DUO term
+ * are omitted.
+ *
+ * @param dataUse the DataUse to convert
+ * @return a list of DUO term identifiers, e.g. {@code ["DUO:0000004", "DUO:0000021"]}
+ */
+ public static List dataUseToTermArray(DataUse dataUse) {
+ List terms = new ArrayList<>();
+ if (dataUse == null) {
+ return terms;
+ }
+ // Note that fields like gender, pediatric, aiLlmUse, illegalBehavior, sexualDiseases,
+ // stigmatizeDiseases, vulnerablePopulations, psychologicalTraits, notHealth, controls,
+ // population, other, and secondaryOther are intentionally omitted as they have no standard DUO
+ // ontology term.
+
+ // ── Primary data use permissions ─────────────────────────────────────
+ // DUO:0000004 – no restriction (General Research Use, GRU)
+ if (Boolean.TRUE.equals(dataUse.getGeneralUse())) {
+ terms.add("DUO:0000004");
+ }
+ // DUO:0000006 – health or medical or biomedical research (HMB)
+ if (Boolean.TRUE.equals(dataUse.getHmbResearch())) {
+ terms.add("DUO:0000006");
+ }
+ // DUO:0000007 – disease specific research (DS); the restriction list already contains
+ // ontology term IDs (e.g. MONDO/HP/DOID), so include both the DUO classifier and each term.
+ if (dataUse.getDiseaseRestrictions() != null && !dataUse.getDiseaseRestrictions().isEmpty()) {
+ terms.add("DUO:0000007");
+ terms.addAll(dataUse.getDiseaseRestrictions());
+ }
+ // DUO:0000011 – population origins or ancestry research only (POA)
+ if (Boolean.TRUE.equals(dataUse.getPopulationOriginsAncestry())) {
+ terms.add("DUO:0000011");
+ }
+ // DUO:0000016 – genetic studies only (GSO)
+ if (Boolean.TRUE.equals(dataUse.getGeneticStudiesOnly())) {
+ terms.add("DUO:0000016");
+ }
+
+ // ── Secondary / modifier terms ────────────────────────────────────────
+ // DUO:0000015 – no general methods research (NMDS)
+ if (Boolean.TRUE.equals(dataUse.getMethodsResearch())) {
+ terms.add("DUO:0000015");
+ }
+ // DUO:0000018 – not-for-profit use only (NPU / NCU)
+ if (Boolean.TRUE.equals(dataUse.getNonProfitUse())) {
+ terms.add("DUO:0000018");
+ }
+ // DUO:0000019 – publication required (PUB)
+ if (Boolean.TRUE.equals(dataUse.getPublicationResults())) {
+ terms.add("DUO:0000019");
+ }
+ // DUO:0000020 – collaboration required (COL)
+ if (Boolean.TRUE.equals(dataUse.getCollaboratorRequired())) {
+ terms.add("DUO:0000020");
+ }
+ // DUO:0000021 – ethics approval required (IRB)
+ if (Boolean.TRUE.equals(dataUse.getEthicsApprovalRequired())) {
+ terms.add("DUO:0000021");
+ }
+ // DUO:0000022 – geographical restriction (GS)
+ if (dataUse.getGeographicalRestrictions() != null
+ && !dataUse.getGeographicalRestrictions().isBlank()) {
+ terms.add("DUO:0000022");
+ }
+ // DUO:0000024 – publication moratorium (MOR)
+ if (dataUse.getPublicationMoratorium() != null
+ && !dataUse.getPublicationMoratorium().isBlank()) {
+ terms.add("DUO:0000024");
+ }
+
+ return terms;
+ }
}
diff --git a/src/main/java/org/broadinstitute/consent/http/service/passport/RequiredAgreementsVisa.java b/src/main/java/org/broadinstitute/consent/http/service/passport/RequiredAgreementsVisa.java
new file mode 100644
index 0000000000..24e88db84a
--- /dev/null
+++ b/src/main/java/org/broadinstitute/consent/http/service/passport/RequiredAgreementsVisa.java
@@ -0,0 +1,53 @@
+package org.broadinstitute.consent.http.service.passport;
+
+import java.time.Instant;
+import org.broadinstitute.consent.http.models.DataAccessAgreement;
+
+/**
+ * Data Passport visa listing the Data Access Agreement (DAA) that users must accept in order to
+ * access the dataset. References the DAA document managed in DUOS by the DAC.
+ *
+ * @see GA4GH Data Passports
+ * specification
+ */
+public class RequiredAgreementsVisa implements VisaClaimType {
+
+ private final DataAccessAgreement daa;
+
+ public RequiredAgreementsVisa(DataAccessAgreement daa) {
+ this.daa = daa;
+ }
+
+ @Override
+ public String type() {
+ return VisaClaimTypes.REQUIRED_AGREEMENTS.type;
+ }
+
+ @Override
+ public Long asserted() {
+ if (daa.getCreateDate() != null) {
+ return PassportService.getEpochSeconds(daa.getCreateDate());
+ }
+ return PassportService.getEpochSeconds(Instant.now());
+ }
+
+ /**
+ * Returns a stable URL pointing to the DAA within DUOS. Consumers can use this to retrieve the
+ * full agreement document and verify that a researcher's Library Card includes acceptance of this
+ * agreement before granting access.
+ */
+ @Override
+ public Object value() {
+ return "%s/daa/%d".formatted(PassportService.ISS, daa.getDaaId());
+ }
+
+ @Override
+ public String source() {
+ return PassportService.ISS;
+ }
+
+ @Override
+ public String by() {
+ return VisaBy.SO.name().toLowerCase();
+ }
+}
diff --git a/src/main/java/org/broadinstitute/consent/http/service/passport/ResearcherStatus.java b/src/main/java/org/broadinstitute/consent/http/service/passport/ResearcherStatus.java
index 8e3aee686b..39f901281c 100644
--- a/src/main/java/org/broadinstitute/consent/http/service/passport/ResearcherStatus.java
+++ b/src/main/java/org/broadinstitute/consent/http/service/passport/ResearcherStatus.java
@@ -27,11 +27,15 @@ public Long asserted() {
Optional.ofNullable(user.getLibraryCard())
.map(LibraryCard::getCreateDate)
.orElse(user.getCreateDate());
- return PassportService.getEpochSeconds(assertedDate.toInstant());
+ if (assertedDate == null) {
+ return PassportService.getEpochSeconds(java.time.Instant.now());
+ }
+ // java.sql.Date#toInstant throws UnsupportedOperationException; use epoch millis instead.
+ return PassportService.getEpochSeconds(java.time.Instant.ofEpochMilli(assertedDate.getTime()));
}
@Override
- public String value() {
+ public Object value() {
// See https://broadworkbench.atlassian.net/browse/DT-2863
// This will be replaced with an external profile link.
return PassportService.ISS;
diff --git a/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaim.java b/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaim.java
index 7f0831de41..72af09eced 100644
--- a/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaim.java
+++ b/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaim.java
@@ -1,3 +1,3 @@
package org.broadinstitute.consent.http.service.passport;
-public record VisaClaim(String type, Long asserted, String value, String source, String by) {}
+public record VisaClaim(String type, Long asserted, Object value, String source, String by) {}
diff --git a/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaimType.java b/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaimType.java
index 717c86d3a7..2d12c2f643 100644
--- a/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaimType.java
+++ b/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaimType.java
@@ -5,7 +5,7 @@ public interface VisaClaimType {
Long asserted();
- String value();
+ Object value();
String source();
diff --git a/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaimTypes.java b/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaimTypes.java
index 25723b08c5..230a9c422a 100644
--- a/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaimTypes.java
+++ b/src/main/java/org/broadinstitute/consent/http/service/passport/VisaClaimTypes.java
@@ -1,9 +1,17 @@
package org.broadinstitute.consent.http.service.passport;
public enum VisaClaimTypes {
+ // GA4GH Researcher Passport visa types
AFFILIATION_AND_ROLE("AffiliationAndRole"),
CONTROLLED_ACCESS_GRANTS("ControlledAccessGrants"),
- RESEARCHER_STATUS("ResearcherStatus");
+ RESEARCHER_STATUS("ResearcherStatus"),
+
+ // GA4GH Data Passport visa types (see
+ // https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5372874)
+ APPROVED_USERS("ApprovedUsers"),
+ CONSENTED_DATA_USE_TERMS("ConsentedDataUseTerms"),
+ OVERSIGHT_BODIES("OversightBodies"),
+ REQUIRED_AGREEMENTS("RequiredAgreements");
public final String type;
diff --git a/src/main/resources/assets/api-docs.yaml b/src/main/resources/assets/api-docs.yaml
index 4365ea64bf..1511608a8f 100644
--- a/src/main/resources/assets/api-docs.yaml
+++ b/src/main/resources/assets/api-docs.yaml
@@ -719,6 +719,8 @@ paths:
$ref: './paths/darSummariesByDatasetId.yaml'
/api/passport/userinfo:
$ref: './paths/passportUserInfo.yaml'
+ /api/passport/dataset/{datasetIdentifier}:
+ $ref: './paths/passportDataset.yaml'
/api/user:
$ref: './paths/user.yaml'
/api/user/me:
diff --git a/src/main/resources/assets/paths/passportDataset.yaml b/src/main/resources/assets/paths/passportDataset.yaml
new file mode 100644
index 0000000000..e4a9fee2f2
--- /dev/null
+++ b/src/main/resources/assets/paths/passportDataset.yaml
@@ -0,0 +1,43 @@
+get:
+ summary: Get Data Passport
+ description: |
+ Returns a Data Passport for a given dataset, as proposed in the
+ [GA4GH Data Passports specification](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5372874).
+
+ The response uses the same `ga4gh_passport_v1` envelope as a Researcher Passport
+ (see `/api/passport/userinfo`) but contains dataset-centric visas that describe
+ the governance requirements of the dataset rather than the permissions of a researcher:
+
+ - **ApprovedUsers** — links to the dataset's approved user API endpoint
+ - **ConsentedDataUseTerms** — links to the dataset's DUO-coded data use terms
+ - **OversightBodies** — identifies the DAC responsible for governing access
+ - **RequiredAgreements** — references the Data Access Agreement users must accept (when one exists)
+
+ The `sub` field of each visa is the dataset identifier (e.g. `DUOS-000001`) rather
+ than a user subject ID, reflecting the dataset-centric nature of the passport.
+ tags:
+ - Admin
+ parameters:
+ - name: datasetIdentifier
+ in: path
+ description: The formatted DUOS dataset identifier, e.g. DUOS-000001
+ required: true
+ schema:
+ type: string
+ example: DUOS-000001
+ responses:
+ 200:
+ description: A Passport Object containing dataset-centric visas for the specified dataset
+ content:
+ application/json:
+ schema:
+ $ref: '../schemas/PassportClaim.yaml'
+ 400:
+ description: Bad Request — the dataset identifier could not be parsed
+ 403:
+ description: Forbidden — the authenticated user does not have permission to access this resource
+ 404:
+ description: Dataset not found
+ 500:
+ description: Internal Server Error
+
diff --git a/src/test/java/org/broadinstitute/consent/http/resources/PassportResourceTest.java b/src/test/java/org/broadinstitute/consent/http/resources/PassportResourceTest.java
index 0fe8cc6f99..e80b290dfd 100644
--- a/src/test/java/org/broadinstitute/consent/http/resources/PassportResourceTest.java
+++ b/src/test/java/org/broadinstitute/consent/http/resources/PassportResourceTest.java
@@ -8,12 +8,16 @@
import jakarta.ws.rs.core.Response.Status;
import java.sql.Timestamp;
import java.time.Instant;
+import java.util.List;
import org.broadinstitute.consent.http.AbstractTestHelper;
import org.broadinstitute.consent.http.models.AuthUser;
import org.broadinstitute.consent.http.models.DuosUser;
import org.broadinstitute.consent.http.models.User;
import org.broadinstitute.consent.http.models.sam.UserStatusInfo;
+import org.broadinstitute.consent.http.service.passport.PassportClaim;
import org.broadinstitute.consent.http.service.passport.PassportService;
+import org.broadinstitute.consent.http.service.passport.Visa;
+import org.broadinstitute.consent.http.service.passport.VisaClaim;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -34,8 +38,9 @@ void testGetPassportSuccess() {
duosUser.setUserStatusInfo(userStatusInfo);
PassportResource resource = new PassportResource(passportService);
- Response response = resource.getPassport(duosUser);
- assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ try (Response response = resource.getPassport(duosUser)) {
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ }
}
@Test
@@ -45,8 +50,9 @@ void testGetPassportFailure() {
.thenThrow(new RuntimeException("Passport generation failed"));
PassportResource resource = new PassportResource(passportService);
- Response response = resource.getPassport(duosUser);
- assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus());
+ try (Response response = resource.getPassport(duosUser)) {
+ assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus());
+ }
}
@Test
@@ -54,8 +60,9 @@ void testGetPassportNotFoundNullDuosUser() {
PassportResource resource = new PassportResource(passportService);
when(passportService.generatePassport(null)).thenThrow(new NotFoundException("User not found"));
- Response response = resource.getPassport(null);
- assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+ try (Response response = resource.getPassport(null)) {
+ assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+ }
}
@Test
@@ -65,8 +72,78 @@ void testGetPassportNotFoundNullUser() {
when(passportService.generatePassport(duosUser))
.thenThrow(new NotFoundException("User not found"));
- Response response = resource.getPassport(duosUser);
- assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+ try (Response response = resource.getPassport(duosUser)) {
+ assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // getDataPassport
+ // -----------------------------------------------------------------------
+
+ @Test
+ void testGetDataPassportSuccess() {
+ PassportClaim mockClaim = new PassportClaim(List.of(mockVisa()));
+ when(passportService.generateDataPassport("DUOS-000001")).thenReturn(mockClaim);
+
+ PassportResource resource = new PassportResource(passportService);
+ try (Response response =
+ resource.getDataPassport(new DuosUser(authUser, createUser()), "DUOS-000001")) {
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ assertEquals(mockClaim, response.getEntity());
+ }
+ }
+
+ @Test
+ void testGetDataPassportNotFound() {
+ when(passportService.generateDataPassport("DUOS-000001"))
+ .thenThrow(new NotFoundException("Dataset not found: DUOS-000001"));
+
+ PassportResource resource = new PassportResource(passportService);
+ try (Response response =
+ resource.getDataPassport(new DuosUser(authUser, createUser()), "DUOS-000001")) {
+ assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+ }
+ }
+
+ @Test
+ void testGetDataPassportInvalidIdentifier() {
+ when(passportService.generateDataPassport("INVALID"))
+ .thenThrow(new IllegalArgumentException("Could not parse identifier (INVALID)"));
+
+ PassportResource resource = new PassportResource(passportService);
+ try (Response response =
+ resource.getDataPassport(new DuosUser(authUser, createUser()), "INVALID")) {
+ assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+ }
+ }
+
+ @Test
+ void testGetDataPassportInternalError() {
+ when(passportService.generateDataPassport("DUOS-000001"))
+ .thenThrow(new RuntimeException("Unexpected error"));
+
+ PassportResource resource = new PassportResource(passportService);
+ try (Response response =
+ resource.getDataPassport(new DuosUser(authUser, createUser()), "DUOS-000001")) {
+ assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus());
+ }
+ }
+
+ private Visa mockVisa() {
+ VisaClaim claim =
+ new VisaClaim(
+ "ConsentedDataUseTerms",
+ Instant.now().getEpochSecond(),
+ PassportService.ISS + "/dataset/DUOS-000001/dataUse",
+ PassportService.ISS,
+ "dac");
+ return new Visa(
+ PassportService.ISS,
+ "DUOS-000001",
+ Instant.now().getEpochSecond(),
+ Instant.now().getEpochSecond() + PassportService.EXPIRATION_SECONDS,
+ claim);
}
private User createUser() {
diff --git a/src/test/java/org/broadinstitute/consent/http/service/passport/DataPassportVisaTest.java b/src/test/java/org/broadinstitute/consent/http/service/passport/DataPassportVisaTest.java
new file mode 100644
index 0000000000..0272b698ce
--- /dev/null
+++ b/src/test/java/org/broadinstitute/consent/http/service/passport/DataPassportVisaTest.java
@@ -0,0 +1,425 @@
+package org.broadinstitute.consent.http.service.passport;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import org.broadinstitute.consent.http.models.Dac;
+import org.broadinstitute.consent.http.models.DataAccessAgreement;
+import org.broadinstitute.consent.http.models.DataUse;
+import org.broadinstitute.consent.http.models.DataUseBuilder;
+import org.broadinstitute.consent.http.models.Dataset;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the Data Passport visa types introduced in GA4GH Data Passports:
+ * ConsentedDataUseTermsVisa, OversightBodiesVisa, and RequiredAgreementsVisa.
+ */
+class DataPassportVisaTest {
+
+ // -----------------------------------------------------------------------
+ // ConsentedDataUseTermsVisa
+ // -----------------------------------------------------------------------
+
+ @Test
+ void consentedDataUseTerms_type() {
+ ConsentedDataUseTermsVisa visa = new ConsentedDataUseTermsVisa(datasetWithAlias(42));
+ assertEquals(VisaClaimTypes.CONSENTED_DATA_USE_TERMS.type, visa.type());
+ }
+
+ @Test
+ void consentedDataUseTerms_value_isListOfDuoTerms() {
+ Dataset dataset = datasetWithAlias(42);
+ ConsentedDataUseTermsVisa visa = new ConsentedDataUseTermsVisa(dataset);
+ assertInstanceOf(List.class, visa.value());
+ }
+
+ @Test
+ void consentedDataUseTerms_value_emptyListWhenDataUseIsNull() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDataUse(null);
+ List> terms = (List>) new ConsentedDataUseTermsVisa(dataset).value();
+ assertNotNull(terms);
+ assertTrue(terms.isEmpty());
+ }
+
+ @Test
+ void consentedDataUseTerms_value_generalUse() {
+ DataUse dataUse = new DataUseBuilder().setGeneralUse(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000004"), "GRU should map to DUO:0000004");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_hmbResearch() {
+ DataUse dataUse = new DataUseBuilder().setHmbResearch(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000006"), "HMB should map to DUO:0000006");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_diseaseRestrictions_includesDuoClassifierAndTermIds() {
+ DataUse dataUse =
+ new DataUseBuilder().setDiseaseRestrictions(List.of("MONDO:0005267", "HP:0001250")).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000007"), "DS should include DUO:0000007 classifier");
+ assertTrue(terms.contains("MONDO:0005267"), "disease term MONDO:0005267 should be included");
+ assertTrue(terms.contains("HP:0001250"), "disease term HP:0001250 should be included");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_diseaseRestrictions_emptyListDoesNotAddClassifier() {
+ DataUse dataUse = new DataUseBuilder().setDiseaseRestrictions(List.of()).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(
+ terms.stream().noneMatch("DUO:0000007"::equals),
+ "Empty disease list should not add DUO:0000007");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_populationOriginsAncestry() {
+ DataUse dataUse = new DataUseBuilder().setPopulationOriginsAncestry(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000011"), "POA should map to DUO:0000011");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_geneticStudiesOnly() {
+ DataUse dataUse = new DataUseBuilder().setGeneticStudiesOnly(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000016"), "GSO should map to DUO:0000016");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_methodsResearch() {
+ DataUse dataUse = new DataUseBuilder().setMethodsResearch(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000015"), "NMDS should map to DUO:0000015");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_nonProfitUse() {
+ DataUse dataUse = new DataUseBuilder().setNonProfitUse(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000018"), "NPU should map to DUO:0000018");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_publicationResults() {
+ DataUse dataUse = new DataUseBuilder().setPublicationResults(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000019"), "PUB should map to DUO:0000019");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_collaboratorRequired() {
+ DataUse dataUse = new DataUseBuilder().setCollaboratorRequired(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000020"), "COL should map to DUO:0000020");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_ethicsApprovalRequired() {
+ DataUse dataUse = new DataUseBuilder().setEthicsApprovalRequired(true).build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000021"), "IRB should map to DUO:0000021");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_geographicalRestrictions() {
+ DataUse dataUse = new DataUseBuilder().setGeographicalRestrictions("US-only").build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000022"), "GS should map to DUO:0000022");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_geographicalRestrictions_blankStringNotIncluded() {
+ DataUse dataUse = new DataUseBuilder().setGeographicalRestrictions(" ").build();
+ List> terms = valueFor(dataUse);
+ assertTrue(
+ terms.stream().noneMatch("DUO:0000022"::equals),
+ "Blank geographical restriction should not add DUO:0000022");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_publicationMoratorium() {
+ DataUse dataUse = new DataUseBuilder().setPublicationMoratorium("2027-01-01").build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000024"), "MOR should map to DUO:0000024");
+ }
+
+ @Test
+ void consentedDataUseTerms_value_multipleCombinedTerms() {
+ DataUse dataUse =
+ new DataUseBuilder()
+ .setHmbResearch(true)
+ .setEthicsApprovalRequired(true)
+ .setNonProfitUse(true)
+ .build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.contains("DUO:0000006"));
+ assertTrue(terms.contains("DUO:0000021"));
+ assertTrue(terms.contains("DUO:0000018"));
+ }
+
+ @Test
+ void consentedDataUseTerms_value_falseBooleansNotIncluded() {
+ DataUse dataUse =
+ new DataUseBuilder()
+ .setGeneralUse(false)
+ .setHmbResearch(false)
+ .setNonProfitUse(false)
+ .build();
+ List> terms = valueFor(dataUse);
+ assertTrue(terms.isEmpty(), "False boolean fields should not produce any DUO terms");
+ }
+
+ // Helper: build a dataset with the given DataUse and return the visa value
+ @SuppressWarnings("unchecked")
+ private List valueFor(DataUse dataUse) {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDataUse(dataUse);
+ return (List) new ConsentedDataUseTermsVisa(dataset).value();
+ }
+
+ @Test
+ void consentedDataUseTerms_source_isIss() {
+ assertEquals(PassportService.ISS, new ConsentedDataUseTermsVisa(datasetWithAlias(1)).source());
+ }
+
+ @Test
+ void consentedDataUseTerms_by_isDac() {
+ assertEquals(
+ VisaBy.DAC.name().toLowerCase(), new ConsentedDataUseTermsVisa(datasetWithAlias(1)).by());
+ }
+
+ @Test
+ void consentedDataUseTerms_asserted_usesDatasetCreateDate() {
+ Dataset dataset = datasetWithAlias(1);
+ Date createDate = new Date(1_000_000_000L);
+ dataset.setCreateDate(createDate);
+ ConsentedDataUseTermsVisa visa = new ConsentedDataUseTermsVisa(dataset);
+ assertEquals(PassportService.getEpochSeconds(createDate.toInstant()), visa.asserted());
+ }
+
+ @Test
+ void consentedDataUseTerms_asserted_fallsBackToNowWhenCreateDateNull() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setCreateDate(null);
+ long before = Instant.now().getEpochSecond();
+ long asserted = new ConsentedDataUseTermsVisa(dataset).asserted();
+ long after = Instant.now().getEpochSecond();
+ assertTrue(asserted >= before && asserted <= after);
+ }
+
+ @Test
+ void consentedDataUseTerms_asserted_handlesSqlDate() {
+ Dataset dataset = datasetWithAlias(42);
+ java.sql.Date createDate = new java.sql.Date(2_100_000_000L);
+ dataset.setCreateDate(createDate);
+
+ ConsentedDataUseTermsVisa visa = new ConsentedDataUseTermsVisa(dataset);
+
+ assertEquals(
+ PassportService.getEpochSeconds(Instant.ofEpochMilli(createDate.getTime())),
+ visa.asserted());
+ }
+
+ // -----------------------------------------------------------------------
+ // OversightBodiesVisa
+ // -----------------------------------------------------------------------
+
+ @Test
+ void oversightBodies_type() {
+ assertEquals(VisaClaimTypes.OVERSIGHT_BODIES.type, new OversightBodiesVisa(dac(7)).type());
+ }
+
+ @Test
+ void oversightBodies_value_containsDacId() {
+ Dac dac = dac(7);
+ assertEquals(PassportService.ISS + "/dac/7", new OversightBodiesVisa(dac).value().toString());
+ }
+
+ @Test
+ void oversightBodies_source_isIss() {
+ assertEquals(PassportService.ISS, new OversightBodiesVisa(dac(1)).source());
+ }
+
+ @Test
+ void oversightBodies_by_isDac() {
+ assertEquals(VisaBy.DAC.name().toLowerCase(), new OversightBodiesVisa(dac(1)).by());
+ }
+
+ @Test
+ void oversightBodies_asserted_usesDacCreateDate() {
+ Dac dac = dac(1);
+ Date createDate = new Date(2_000_000_000L);
+ dac.setCreateDate(createDate);
+ assertEquals(
+ PassportService.getEpochSeconds(createDate.toInstant()),
+ new OversightBodiesVisa(dac).asserted());
+ }
+
+ @Test
+ void oversightBodies_asserted_fallsBackToNowWhenCreateDateNull() {
+ Dac dac = dac(1);
+ dac.setCreateDate(null);
+ long before = Instant.now().getEpochSecond();
+ long asserted = new OversightBodiesVisa(dac).asserted();
+ long after = Instant.now().getEpochSecond();
+ assertTrue(asserted >= before && asserted <= after);
+ }
+
+ @Test
+ void oversightBodies_asserted_handlesSqlDate() {
+ Dac dac = dac(1);
+ java.sql.Date createDate = new java.sql.Date(2_000_000_000L);
+ dac.setCreateDate(createDate);
+ assertEquals(
+ PassportService.getEpochSeconds(Instant.ofEpochMilli(createDate.getTime())),
+ new OversightBodiesVisa(dac).asserted());
+ }
+
+ // -----------------------------------------------------------------------
+ // RequiredAgreementsVisa
+ // -----------------------------------------------------------------------
+
+ @Test
+ void requiredAgreements_type() {
+ assertEquals(
+ VisaClaimTypes.REQUIRED_AGREEMENTS.type, new RequiredAgreementsVisa(daa(5)).type());
+ }
+
+ @Test
+ void requiredAgreements_value_containsDaaId() {
+ assertEquals(
+ PassportService.ISS + "/daa/5", new RequiredAgreementsVisa(daa(5)).value().toString());
+ }
+
+ @Test
+ void requiredAgreements_source_isIss() {
+ assertEquals(PassportService.ISS, new RequiredAgreementsVisa(daa(1)).source());
+ }
+
+ @Test
+ void requiredAgreements_by_isSo() {
+ assertEquals(VisaBy.SO.name().toLowerCase(), new RequiredAgreementsVisa(daa(1)).by());
+ }
+
+ @Test
+ void requiredAgreements_asserted_usesDaaCreateDate() {
+ Instant createDate = Instant.ofEpochSecond(3_000_000L);
+ DataAccessAgreement daa = daa(1);
+ daa.setCreateDate(createDate);
+ assertEquals(
+ PassportService.getEpochSeconds(createDate), new RequiredAgreementsVisa(daa).asserted());
+ }
+
+ @Test
+ void requiredAgreements_asserted_fallsBackToNowWhenCreateDateNull() {
+ DataAccessAgreement daa = daa(1);
+ daa.setCreateDate(null);
+ long before = Instant.now().getEpochSecond();
+ long asserted = new RequiredAgreementsVisa(daa).asserted();
+ long after = Instant.now().getEpochSecond();
+ assertTrue(asserted >= before && asserted <= after);
+ }
+
+ // -----------------------------------------------------------------------
+ // ApprovedUsersVisa
+ // -----------------------------------------------------------------------
+
+ @Test
+ void approvedUsers_type() {
+ assertEquals(VisaClaimTypes.APPROVED_USERS.type, new ApprovedUsersVisa("DUOS-000001").type());
+ }
+
+ @Test
+ void approvedUsers_value_containsDatasetIdentifier() {
+ ApprovedUsersVisa visa = new ApprovedUsersVisa("DUOS-000042");
+ assertEquals(PassportService.getApprovedUsersEndpoint("DUOS-000042"), visa.value().toString());
+ }
+
+ @Test
+ void approvedUsers_value_containsDatasetIdentifierInUrl() {
+ ApprovedUsersVisa visa = new ApprovedUsersVisa("DUOS-000042");
+ assertTrue(visa.value().toString().contains("DUOS-000042"));
+ }
+
+ @Test
+ void approvedUsers_source_isIss() {
+ assertEquals(PassportService.ISS, new ApprovedUsersVisa("DUOS-000001").source());
+ }
+
+ @Test
+ void approvedUsers_by_isDac() {
+ assertEquals(VisaBy.DAC.name().toLowerCase(), new ApprovedUsersVisa("DUOS-000001").by());
+ }
+
+ @Test
+ void approvedUsers_asserted_isEpochSeconds() {
+ long before = Instant.now().getEpochSecond();
+ long asserted = new ApprovedUsersVisa("DUOS-000001").asserted();
+ long after = Instant.now().getEpochSecond();
+ assertTrue(asserted >= before && asserted <= after);
+ }
+
+ @Test
+ void approvedUsers_asserted_isNotMilliseconds() {
+ long asserted = new ApprovedUsersVisa("DUOS-000001").asserted();
+ long nowSeconds = Instant.now().getEpochSecond();
+ // If asserted were in milliseconds it would be ~1000x larger than nowSeconds
+ assertTrue(asserted <= nowSeconds + 5, "asserted should be seconds, not milliseconds");
+ }
+
+ // -----------------------------------------------------------------------
+ // Common contract across all four visa types
+ // -----------------------------------------------------------------------
+
+ @Test
+ void allVisaTypes_haveNonNullFields() {
+ VisaClaimType[] visas = {
+ new ConsentedDataUseTermsVisa(datasetWithAlias(1)),
+ new OversightBodiesVisa(dac(1)),
+ new RequiredAgreementsVisa(daa(1)),
+ new ApprovedUsersVisa("DUOS-000001")
+ };
+ for (VisaClaimType v : visas) {
+ assertNotNull(v.type(), "type must not be null for " + v.getClass().getSimpleName());
+ assertNotNull(v.value(), "value must not be null for " + v.getClass().getSimpleName());
+ assertNotNull(v.source(), "source must not be null for " + v.getClass().getSimpleName());
+ assertNotNull(v.by(), "by must not be null for " + v.getClass().getSimpleName());
+ assertTrue(v.asserted() > 0, "asserted must be positive for " + v.getClass().getSimpleName());
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Helpers
+ // -----------------------------------------------------------------------
+
+ private Dataset datasetWithAlias(int alias) {
+ Dataset d = new Dataset();
+ d.setAlias(alias);
+ d.setCreateDate(new Date());
+ return d;
+ }
+
+ private Dac dac(int dacId) {
+ Dac dac = new Dac();
+ dac.setDacId(dacId);
+ dac.setCreateDate(new Date());
+ return dac;
+ }
+
+ private DataAccessAgreement daa(int daaId) {
+ DataAccessAgreement daa = new DataAccessAgreement();
+ daa.setDaaId(daaId);
+ daa.setCreateDate(Instant.now());
+ return daa;
+ }
+}
diff --git a/src/test/java/org/broadinstitute/consent/http/service/passport/PassportServiceTest.java b/src/test/java/org/broadinstitute/consent/http/service/passport/PassportServiceTest.java
index 0e06478f60..e3e2bfb9f6 100644
--- a/src/test/java/org/broadinstitute/consent/http/service/passport/PassportServiceTest.java
+++ b/src/test/java/org/broadinstitute/consent/http/service/passport/PassportServiceTest.java
@@ -9,14 +9,19 @@
import jakarta.ws.rs.NotFoundException;
import java.sql.Timestamp;
import java.time.Instant;
+import java.util.Date;
import java.util.List;
import org.broadinstitute.consent.http.AbstractTestHelper;
import org.broadinstitute.consent.http.db.DatasetDAO;
import org.broadinstitute.consent.http.models.ApprovedDataset;
+import org.broadinstitute.consent.http.models.Dac;
+import org.broadinstitute.consent.http.models.DataAccessAgreement;
+import org.broadinstitute.consent.http.models.Dataset;
import org.broadinstitute.consent.http.models.DuosUser;
import org.broadinstitute.consent.http.models.LibraryCard;
import org.broadinstitute.consent.http.models.User;
import org.broadinstitute.consent.http.models.sam.UserStatusInfo;
+import org.broadinstitute.consent.http.service.DacService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -29,13 +34,14 @@
class PassportServiceTest extends AbstractTestHelper {
@Mock private DatasetDAO datasetDAO;
+ @Mock private DacService dacService;
@Mock private DuosUser duosUser;
private PassportService service;
@BeforeEach
void setUp() {
- service = new PassportService(datasetDAO);
+ service = new PassportService(datasetDAO, dacService);
}
@Test
@@ -140,7 +146,7 @@ void testAffiliationAndRole_nullEmail(String email) {
User user = createUser();
user.setEmail(email);
AffiliationAndRole affiliationAndRole = new AffiliationAndRole(user);
- assertEquals(AffiliationAndRole.DEFAULT_VALUE, affiliationAndRole.value());
+ assertEquals(AffiliationAndRole.DEFAULT_VALUE, affiliationAndRole.value().toString());
assertEquals(PassportService.ISS, affiliationAndRole.source());
assertTrue(affiliationAndRole.asserted() > 0);
}
@@ -151,7 +157,7 @@ void testAffiliationAndRole_withLibraryCard() {
LibraryCard card = new LibraryCard();
user.setLibraryCard(card);
AffiliationAndRole affiliationAndRole = new AffiliationAndRole(user);
- assertTrue(affiliationAndRole.value().contains("faculty@example.org"));
+ assertTrue(affiliationAndRole.value().toString().contains("faculty@example.org"));
assertEquals(PassportService.ISS, affiliationAndRole.source());
assertEquals(VisaBy.SO.name().toLowerCase(), affiliationAndRole.by());
assertTrue(affiliationAndRole.asserted() > 0);
@@ -208,6 +214,165 @@ private UserStatusInfo createUserStatusInfo(User user) {
return info;
}
+ // -----------------------------------------------------------------------
+ // generateDataPassport
+ // -----------------------------------------------------------------------
+
+ @Test
+ void generateDataPassport_datasetNotFound_throwsNotFoundException() {
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(null);
+ assertThrows(NotFoundException.class, () -> service.generateDataPassport("DUOS-000001"));
+ }
+
+ @Test
+ void generateDataPassport_datasetWithNoDac_returnsApprovedUsersAndConsentedDataUseTermsVisas() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDacId(null);
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(dataset);
+
+ PassportClaim claim = service.generateDataPassport("DUOS-000001");
+
+ assertNotNull(claim);
+ assertEquals(2, claim.ga4gh_passport_v1().size());
+ assertVisaTypePresent(claim, VisaClaimTypes.APPROVED_USERS.type);
+ assertVisaTypePresent(claim, VisaClaimTypes.CONSENTED_DATA_USE_TERMS.type);
+ }
+
+ @Test
+ void generateDataPassport_datasetWithDacAndNoDaa_returnsThreeVisas() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDacId(10);
+ Dac dac = dacWithId(10);
+ dac.setAssociatedDaa(null);
+
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(dataset);
+ when(dacService.findById(10)).thenReturn(dac);
+
+ PassportClaim claim = service.generateDataPassport("DUOS-000001");
+
+ assertNotNull(claim);
+ assertEquals(3, claim.ga4gh_passport_v1().size());
+ assertVisaTypePresent(claim, VisaClaimTypes.APPROVED_USERS.type);
+ assertVisaTypePresent(claim, VisaClaimTypes.CONSENTED_DATA_USE_TERMS.type);
+ assertVisaTypePresent(claim, VisaClaimTypes.OVERSIGHT_BODIES.type);
+ }
+
+ @Test
+ void generateDataPassport_datasetWithDacAndDaa_returnsFourVisas() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDacId(10);
+ Dac dac = dacWithId(10);
+ DataAccessAgreement daa = new DataAccessAgreement();
+ daa.setDaaId(99);
+ daa.setCreateDate(Instant.now());
+ dac.setAssociatedDaa(daa);
+
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(dataset);
+ when(dacService.findById(10)).thenReturn(dac);
+
+ PassportClaim claim = service.generateDataPassport("DUOS-000001");
+
+ assertNotNull(claim);
+ assertEquals(4, claim.ga4gh_passport_v1().size());
+ assertVisaTypePresent(claim, VisaClaimTypes.APPROVED_USERS.type);
+ assertVisaTypePresent(claim, VisaClaimTypes.CONSENTED_DATA_USE_TERMS.type);
+ assertVisaTypePresent(claim, VisaClaimTypes.OVERSIGHT_BODIES.type);
+ assertVisaTypePresent(claim, VisaClaimTypes.REQUIRED_AGREEMENTS.type);
+ }
+
+ @Test
+ void generateDataPassport_subFieldIsDatasetIdentifier() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDacId(null);
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(dataset);
+
+ PassportClaim claim = service.generateDataPassport("DUOS-000001");
+
+ claim
+ .ga4gh_passport_v1()
+ .forEach(v -> assertEquals("DUOS-000001", v.sub(), "sub should be the dataset identifier"));
+ }
+
+ @Test
+ void generateDataPassport_issFieldIsIss() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDacId(null);
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(dataset);
+
+ PassportClaim claim = service.generateDataPassport("DUOS-000001");
+
+ claim.ga4gh_passport_v1().forEach(v -> assertEquals(PassportService.ISS, v.iss()));
+ }
+
+ @Test
+ void generateDataPassport_iatAndExpAreEpochSeconds() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDacId(null);
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(dataset);
+
+ PassportClaim claim = service.generateDataPassport("DUOS-000001");
+
+ long nowSeconds = Instant.now().getEpochSecond();
+ claim
+ .ga4gh_passport_v1()
+ .forEach(
+ v -> {
+ assertTrue(v.iat() <= nowSeconds + 5, "iat should be seconds, not milliseconds");
+ assertTrue(v.exp() > v.iat(), "exp should be after iat");
+ assertEquals(PassportService.EXPIRATION_SECONDS, v.exp() - v.iat());
+ });
+ }
+
+ @Test
+ void generateDataPassport_dacNotFound_returnsApprovedUsersAndConsentedDataUseTermsVisas() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDacId(10);
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(dataset);
+ when(dacService.findById(10)).thenReturn(null);
+
+ PassportClaim claim = service.generateDataPassport("DUOS-000001");
+
+ assertEquals(2, claim.ga4gh_passport_v1().size());
+ assertVisaTypePresent(claim, VisaClaimTypes.APPROVED_USERS.type);
+ assertVisaTypePresent(claim, VisaClaimTypes.CONSENTED_DATA_USE_TERMS.type);
+ }
+
+ @Test
+ void
+ generateDataPassport_unsupportedOperationFromDacLookup_returnsApprovedUsersAndConsentedDataUseTermsVisas() {
+ Dataset dataset = datasetWithAlias(1);
+ dataset.setDacId(10);
+ when(datasetDAO.findDatasetByAlias(1)).thenReturn(dataset);
+ when(dacService.findById(10)).thenThrow(new UnsupportedOperationException("unsupported"));
+
+ PassportClaim claim = service.generateDataPassport("DUOS-000001");
+
+ assertNotNull(claim);
+ assertEquals(2, claim.ga4gh_passport_v1().size());
+ assertVisaTypePresent(claim, VisaClaimTypes.APPROVED_USERS.type);
+ assertVisaTypePresent(claim, VisaClaimTypes.CONSENTED_DATA_USE_TERMS.type);
+ }
+
+ private void assertVisaTypePresent(PassportClaim claim, String type) {
+ assertTrue(
+ claim.ga4gh_passport_v1().stream().anyMatch(v -> type.equals(v.ga4gh_visa_v1().type())),
+ "Expected visa type '%s' to be present".formatted(type));
+ }
+
+ private Dataset datasetWithAlias(int alias) {
+ Dataset d = new Dataset();
+ d.setAlias(alias);
+ d.setCreateDate(new Date());
+ return d;
+ }
+
+ private Dac dacWithId(int dacId) {
+ Dac dac = new Dac();
+ dac.setDacId(dacId);
+ dac.setCreateDate(new Date());
+ return dac;
+ }
+
private int datasetCounter = 0;
private ApprovedDataset createApprovedDataset() {
@@ -223,4 +388,60 @@ private ApprovedDataset createApprovedDataset() {
d.setDatasetIdentifier(datasetIdentifier);
return d;
}
+
+ @Test
+ void testAffiliationAndRole_assertedHandlesSqlDateOnUser() {
+ User user = createUser();
+ java.sql.Date sqlDate = new java.sql.Date(1_700_000_000_000L);
+ user.setCreateDate(sqlDate);
+
+ AffiliationAndRole affiliationAndRole = new AffiliationAndRole(user);
+
+ assertEquals(
+ PassportService.getEpochSeconds(Instant.ofEpochMilli(sqlDate.getTime())),
+ affiliationAndRole.asserted());
+ }
+
+ @Test
+ void testAffiliationAndRole_assertedHandlesSqlDateOnLibraryCard() {
+ User user = createUser();
+ LibraryCard card = new LibraryCard();
+ java.sql.Date sqlDate = new java.sql.Date(1_710_000_000_000L);
+ card.setCreateDate(sqlDate);
+ user.setLibraryCard(card);
+
+ AffiliationAndRole affiliationAndRole = new AffiliationAndRole(user);
+
+ assertEquals(
+ PassportService.getEpochSeconds(Instant.ofEpochMilli(sqlDate.getTime())),
+ affiliationAndRole.asserted());
+ }
+
+ @Test
+ void testResearcherStatus_assertedHandlesSqlDateOnUser() {
+ User user = createUser();
+ java.sql.Date sqlDate = new java.sql.Date(1_720_000_000_000L);
+ user.setCreateDate(sqlDate);
+
+ ResearcherStatus researcherStatus = new ResearcherStatus(user);
+
+ assertEquals(
+ PassportService.getEpochSeconds(Instant.ofEpochMilli(sqlDate.getTime())),
+ researcherStatus.asserted());
+ }
+
+ @Test
+ void testResearcherStatus_assertedHandlesSqlDateOnLibraryCard() {
+ User user = createUser();
+ LibraryCard card = new LibraryCard();
+ java.sql.Date sqlDate = new java.sql.Date(1_730_000_000_000L);
+ card.setCreateDate(sqlDate);
+ user.setLibraryCard(card);
+
+ ResearcherStatus researcherStatus = new ResearcherStatus(user);
+
+ assertEquals(
+ PassportService.getEpochSeconds(Instant.ofEpochMilli(sqlDate.getTime())),
+ researcherStatus.asserted());
+ }
}