diff --git a/.vscode/launch.json b/.vscode/launch.json index e810bf9..b0d0f28 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,7 +16,8 @@ "--reload" ], "env": { - "ARIES_VCR_URL": "http://host.docker.internal:8008", + "ISSER_REGISTRY_URL": "https://bcgov.github.io/digital-trust-toolkit/registrations/issuers/dev.json", + "ARIES_VCR_URL": "http://host.docker.internal:8080", }, "jinja": true }, diff --git a/schemas/credential_type.py b/schemas/credential_type.py index 4328175..76d1c86 100644 --- a/schemas/credential_type.py +++ b/schemas/credential_type.py @@ -1,7 +1,7 @@ from typing import Annotated from fastapi import Body -from schemas import CredentialMapping, CredentialTopic, Options +from schemas import CredentialMapping, CredentialTopic, Options, Path from pydantic import BaseModel @@ -18,3 +18,4 @@ class CredentialType(Options, BaseModel): oca_bundle: Annotated[dict | None, Body(validation_alias="ocaBundle")] = None topic: CredentialTopic mappings: list[CredentialMapping] | None = None + cardinality: list[Path] | None = None diff --git a/schemas/mappings/vcr_credential_type.py b/schemas/mappings/vcr_credential_type.py index 0beaf9b..b0d2dba 100644 --- a/schemas/mappings/vcr_credential_type.py +++ b/schemas/mappings/vcr_credential_type.py @@ -41,4 +41,10 @@ def serialize_model(self) -> dict[str, Any]: mapping.model_dump(mode="json") for mapping in secured_document.mappings ] + if secured_document.cardinality: + model_dump["cardinality"] = [ + cardinality.model_dump(mode="json") + for cardinality in secured_document.cardinality + ] + return model_dump diff --git a/tests/data.py b/tests/data.py index 734af2c..13edd13 100644 --- a/tests/data.py +++ b/tests/data.py @@ -15,6 +15,10 @@ "path": "$.path.to.credential.expiry_date", } +credential_subject_cardinality_spec = { + "path": "$.path.to.credentialSubject.id", +} + credential_type_spec = { "format": "vc_di", "type": "BCPetroleum&NaturalGasTitle", @@ -25,6 +29,9 @@ effective_date_mapping_spec, expiry_date_mapping_spec, ], + "cardinality": [ + credential_subject_cardinality_spec, + ], } secured_credential_type_spec = { @@ -34,16 +41,19 @@ "name": "BC Petroleum & Natural Gas Title", "version": "0.0.3", "verificationMethods": [ - "did:web:untp.traceability.site:parties:regulators:director-of-petroleum-lands#multikey" + "did:web:registry-dev.apps.silver.devops.gov.bc.ca:petroleum-and-natural-gas-act:director-of-petroleum-lands#multikey" ], "topic": { - "type": "my-registration.city-of-vancouver", + "type": "registration.bc-registries", "sourceId": {"path": "$.credentialSubject.issuedTo.identifier"}, }, "mappings": [ {"type": "effective_date", "name": "effective_date", "path": "$.validFrom"}, {"type": "expiry_date", "name": "expiry_date", "path": "$.validUntil"}, ], + "cardinality": [ + {"path": "$.credentialSubject.issuedTo.id"}, + ], "resources": [ { "type": "OverlayCaptureBundle", @@ -57,7 +67,7 @@ { "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", - "verificationMethod": "did:web:untp.traceability.site:parties:regulators:director-of-petroleum-lands#multikey", + "verificationMethod": "did:web:registry-dev.apps.silver.devops.gov.bc.ca:petroleum-and-natural-gas-act:director-of-petroleum-lands#multikey", "proofPurpose": "authentication", "proofValue": "z17CzsxiNiugmX9CYseEkoXxjMqBDxyasiwWwZ58AD5ctKJLjSeoEmSBvj5VVxzATFfpwKdfRmjqLn2wRMhb9jHV", } @@ -75,24 +85,24 @@ "securedDocument": { "@context": ["https://www.w3.org/ns/credentials/v2"], "type": ["VerifiableCredential", "BCPetroleum&NaturalGasTitle"], - "id": "https://localhost:8080/api/vc/topic/0f95da2f-1485-4848-a93c-8abebc223e41/credential/123456", + "id": "https://localhost:8080/api/vc/topic/d6499ae0-4f9f-453b-93e7-07f4585ff703/credential/123456", "issuer": { - "id": "did:web:untp.traceability.site:parties:regulators:director-of-petroleum-lands#multikey" + "id": "did:web:registry-dev.apps.silver.devops.gov.bc.ca:petroleum-and-natural-gas-act:director-of-petroleum-lands#multikey" }, "validFrom": "2024-08-12T05:44:20+00:00", "validUntil": "2025-08-12T05:44:20+00:00", "credentialSubject": { "issuedTo": { - "id": "https://orgbook.gov.bc.ca/api/vc/topic/0f95da2f-1485-4848-a93c-8abebc223e41", + "id": "https://orgbook.gov.bc.ca/api/vc/topic/d6499ae0-4f9f-453b-93e7-07f4585ff703-2", "legalName": "PACIFIC CANBRIAM ENERGY LIMITED", - "identifier": "0f95da2f-1485-4848-a93c-8abebc223e41", + "identifier": "d6499ae0-4f9f-453b-93e7-07f4585ff703", } }, "proof": [ { "type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", - "verificationMethod": "did:web:untp.traceability.site:parties:regulators:director-of-petroleum-lands#multikey", + "verificationMethod": "did:web:registry-dev.apps.silver.devops.gov.bc.ca:petroleum-and-natural-gas-act:director-of-petroleum-lands#multikey", "proofPurpose": "assertionMethod", "proofValue": "z2Nr9eDUfBzircv484R3u7vzdxARh5D8vsbj4ohFRQZhkq2PTdJ9YsLfF18mafaPMtchV5EefmovvFoFbFNmLqrWW", } diff --git a/tests/test_credential_router.py b/tests/test_credential_router.py index 45d3ac7..6127f7e 100644 --- a/tests/test_credential_router.py +++ b/tests/test_credential_router.py @@ -16,7 +16,6 @@ def test_issue_credential(): response = client.post( "/credential-types/", json=copy.deepcopy(secured_credential_type_spec) ) - print(response.json()) assert response.status_code == 201 response = client.post("/credentials/", json=copy.deepcopy(secured_credential_spec)) diff --git a/tests/test_mappings.py b/tests/test_mappings.py index befb4d5..8dd907f 100644 --- a/tests/test_mappings.py +++ b/tests/test_mappings.py @@ -18,9 +18,9 @@ def test_valid_vcr_credential_type_output(): "format": "vc_di", "schema": "BCPetroleum&NaturalGasTitle", "version": "0.0.3", - "origin_did": "did:web:untp.traceability.site:parties:regulators:director-of-petroleum-lands", + "origin_did": "did:web:registry-dev.apps.silver.devops.gov.bc.ca:petroleum-and-natural-gas-act:director-of-petroleum-lands", "topic": { - "type": "my-registration.city-of-vancouver", + "type": "registration.bc-registries", "source_id": {"path": "$.credentialSubject.issuedTo.identifier"}, }, "mappings": [ @@ -35,6 +35,9 @@ def test_valid_vcr_credential_type_output(): "path": "$.validUntil", }, ], + "cardinality": [ + {"path": "$.credentialSubject.issuedTo.id"}, + ], "credential": { "effective_date": { "name": "effective_date", diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 3f048f9..7410f31 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -219,6 +219,7 @@ def test_valid_credential_type_schema(): "source_id": topic_spec.get("sourceId"), }, "mappings": credential_type_spec.get("mappings"), + "cardinality": credential_type_spec.get("cardinality"), } @@ -240,6 +241,7 @@ def test_valid_credential_type_schema_no_mappings(): "type": topic_spec.get("type"), "source_id": topic_spec.get("sourceId"), }, + "cardinality": credential_type_spec.get("cardinality"), } assert "mappings" not in credential_type_output @@ -263,7 +265,9 @@ def test_valid_credential_type_schema_empty_mappings(): "source_id": topic_spec.get("sourceId"), }, "mappings": [], + "cardinality": credential_type_spec.get("cardinality"), } + assert "mappings" in credential_type_output def test_invalid_credential_type_schema_missing_format(): @@ -416,3 +420,72 @@ def test_invalid_credential_type_schema_invalid_mappings(): assert errors.get("msg") == "Input should be a valid list" assert "mappings" in errors.get("loc") assert errors.get("type") == "list_type" + + +def test_invalid_credential_type_schema_invalid_cardinality(): + """Test invalid CredentialType schema invalid cardinality""" + + test_data = { + **credential_type_spec.copy(), + "cardinality": "invalid", + } + + with pytest.raises(ValidationError) as exc_info: + CredentialType(**test_data) + + errors = exc_info.value.errors()[0] + + assert errors.get("msg") == "Input should be a valid list" + assert "cardinality" in errors.get("loc") + assert errors.get("type") == "list_type" + + +def test_valid_credential_type_schema_cardinality_missing(): + """Test valid CredentialType schema cardinality missing""" + + test_data = { + **credential_type_spec.copy(), + "cardinality": None, + } + + credential_type = CredentialType(**test_data) + credential_type_output = credential_type.model_dump(exclude_none=True) + + assert credential_type_output == { + "format": str(credential_type_spec.get("format")), + "type": credential_type_spec.get("type"), + "version": credential_type_spec.get("version"), + "verification_methods": credential_type_spec.get("verificationMethods"), + "topic": { + "type": topic_spec.get("type"), + "source_id": topic_spec.get("sourceId"), + }, + "mappings": credential_type_spec.get("mappings"), + } + assert "cardinality" not in credential_type_output + + +def test_valid_credential_type_schema_cardinality_empty(): + """Test valid CredentialType schema valid cardinality empty list""" + + test_data = { + **credential_type_spec.copy(), + "cardinality": [], + } + + credential_type = CredentialType(**test_data) + credential_type_output = credential_type.model_dump(exclude_none=True) + + assert credential_type_output == { + "format": str(credential_type_spec.get("format")), + "type": credential_type_spec.get("type"), + "version": credential_type_spec.get("version"), + "verification_methods": credential_type_spec.get("verificationMethods"), + "topic": { + "type": topic_spec.get("type"), + "source_id": topic_spec.get("sourceId"), + }, + "mappings": credential_type_spec.get("mappings"), + "cardinality": [], + } + assert "cardinality" in credential_type_output