diff --git a/package-lock.json b/package-lock.json index 7a155132..6c55f7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@cheqd/credential-service", - "version": "2.9.4-develop.1", + "version": "2.9.4-develop.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cheqd/credential-service", - "version": "2.9.4-develop.1", + "version": "2.9.4-develop.2", "license": "Apache-2.0", "dependencies": { "@cheqd/did-provider-cheqd": "^3.6.8", - "@cheqd/sdk": "^3.7.0", + "@cheqd/sdk": "^3.7.1", "@cheqd/ts-proto": "^3.3.1", "@cosmjs/amino": "^0.31.1", "@cosmjs/encoding": "^0.31.1", @@ -2625,11 +2625,11 @@ } }, "node_modules/@cheqd/sdk": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@cheqd/sdk/-/sdk-3.7.0.tgz", - "integrity": "sha512-/ik+sPc63Nc3VOeeu2isk3+5IQAoMlWZwVS6kmyj+tUoNv5y3pghZ0StftAktNwU7Hh1NAq2s6tri2uBOIQ1lg==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@cheqd/sdk/-/sdk-3.7.1.tgz", + "integrity": "sha512-cVek7qerGnhPrujncybThuNi/DzKwkbHY87pGBsHKswNdQVwJWOWf5gRwQMg0FC3NaKeH2moSZpTrTvGGCK+Uw==", "dependencies": { - "@cheqd/ts-proto": "^3.3.1", + "@cheqd/ts-proto": "^3.3.2", "@cosmjs/amino": "^0.31.1", "@cosmjs/crypto": "^0.31.1", "@cosmjs/encoding": "^0.31.1", @@ -2641,7 +2641,7 @@ "@stablelib/ed25519": "^1.0.3", "@types/secp256k1": "^4.0.3", "cosmjs-types": "^0.8.0", - "did-jwt": "^7.2.7", + "did-jwt": "^7.2.8", "did-resolver": "^4.1.0", "file-type": "^18.5.0", "multiformats": "^12.1.1", @@ -2653,12 +2653,15 @@ } }, "node_modules/@cheqd/ts-proto": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@cheqd/ts-proto/-/ts-proto-3.3.1.tgz", - "integrity": "sha512-/Xebb3fPGujw8Fwbu+wneVh5bP7CWEwX9iE01N2wh3cmSyHpbLBlEIcaJQ0Fzq6pSCQIomS4z4+V3cOw2Bkd+g==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@cheqd/ts-proto/-/ts-proto-3.3.2.tgz", + "integrity": "sha512-CpA7d4MPquWFPuAS7Ex5WtErLKhdblyYex4j87RGxMAdyCm4RrAK2dJC5gZqpHVX4aMtlG1/qfeoaTNuczZYrg==", "dependencies": { "long": "^5.2.3", - "protobufjs": "^7.2.4" + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@colors/colors": { diff --git a/package.json b/package.json index 22cb32e2..ce8a69df 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ ], "dependencies": { "@cheqd/did-provider-cheqd": "^3.6.8", - "@cheqd/sdk": "^3.7.0", + "@cheqd/sdk": "^3.7.1", "@cheqd/ts-proto": "^3.3.1", "@cosmjs/amino": "^0.31.1", "@cosmjs/encoding": "^0.31.1", diff --git a/src/controllers/issuer.ts b/src/controllers/issuer.ts index 8d1ec915..571b8496 100644 --- a/src/controllers/issuer.ts +++ b/src/controllers/issuer.ts @@ -1,15 +1,16 @@ import type { Request, Response } from 'express'; import { check, param, validationResult } from 'express-validator'; -import { fromString } from 'uint8arrays'; -import type { DIDDocument, Service, VerificationMethod } from 'did-resolver'; +import { fromString, toString } from 'uint8arrays'; import { v4 } from 'uuid'; -import { MethodSpecificIdAlgo, VerificationMethods, CheqdNetwork } from '@cheqd/sdk'; +import { CheqdNetwork, DIDDocument, MethodSpecificIdAlgo, Service, VerificationMethod, VerificationMethods, createDidVerificationMethod } from '@cheqd/sdk'; import type { MsgCreateResourcePayload } from '@cheqd/ts-proto/cheqd/resource/v2/index.js'; import { StatusCodes } from 'http-status-codes'; - import { IdentityServiceStrategySetup } from '../services/identity/index.js'; -import { generateDidDoc, getQueryParams, validateSpecCompliantPayload } from '../helpers/helpers.js'; +import { generateDidDoc, getQueryParams, validateDidCreatePayload, validateSpecCompliantPayload } from '../helpers/helpers.js'; import { DIDMetadataDereferencingResult, DefaultResolverUrl } from '@cheqd/did-provider-cheqd'; +import { bases } from "multiformats/basics"; +import { base64ToBytes } from "did-jwt"; +import type { CreateDidRequestBody } from '../types/shared.js'; export class IssuerController { public static createValidator = [ @@ -17,7 +18,7 @@ export class IssuerController { .optional() .isObject() .custom((value) => { - const { valid } = validateSpecCompliantPayload(value); + const { valid } = validateDidCreatePayload(value); return valid; }) .withMessage('Invalid didDocument'), @@ -26,11 +27,11 @@ export class IssuerController { .isString() .isIn([VerificationMethods.Ed255192020, VerificationMethods.Ed255192018, VerificationMethods.JWK]) .withMessage('Invalid verificationMethod'), - check('methodSpecificIdAlgo') + check('identifierFormatType') .optional() .isString() .isIn([MethodSpecificIdAlgo.Base58, MethodSpecificIdAlgo.Uuid]) - .withMessage('Invalid methodSpecificIdAlgo'), + .withMessage('Invalid identifierFormatType'), check('network') .optional() .isString() @@ -187,10 +188,10 @@ export class IssuerController { * content: * application/x-www-form-urlencoded: * schema: - * $ref: '#/components/schemas/DidCreateRequest' + * $ref: '#/components/schemas/DidCreateRequestFormBased' * application/json: * schema: - * $ref: '#/components/schemas/DidCreateRequest' + * $ref: '#/components/schemas/DidCreateRequestJson' * responses: * 200: * description: The request was successful. @@ -226,45 +227,83 @@ export class IssuerController { } const { - methodSpecificIdAlgo, + identifierFormatType, network, verificationMethodType, - assertionMethod = true, - serviceEndpoint, - } = request.body; + service, + key, + options, + } = request.body satisfies CreateDidRequestBody; let didDocument: DIDDocument; try { if (request.body.didDocument) { didDocument = request.body.didDocument; + if (options) { + const publicKeyHex = options.key || (await new IdentityServiceStrategySetup(response.locals.customerId).agent.createKey( + 'Ed25519', + response.locals.customerId + )).publicKeyHex; + const pkBase64 = publicKeyHex.length == 43 ? publicKeyHex : toString(fromString(publicKeyHex, 'hex'), 'base64'); + + didDocument.verificationMethod = createDidVerificationMethod([options.verificationMethodType], [{ + methodSpecificId: bases['base58btc'].encode(base64ToBytes(pkBase64)), + didUrl: didDocument.id, + keyId: `${didDocument.id}#key-1`, + publicKey: pkBase64 + }]); + } else { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'Provide options section to create a DID', + }); + } } else if (verificationMethodType) { - const key = await new IdentityServiceStrategySetup(response.locals.customerId).agent.createKey( + const publicKeyHex = key || (await new IdentityServiceStrategySetup(response.locals.customerId).agent.createKey( 'Ed25519', response.locals.customerId - ); + )).publicKeyHex; didDocument = generateDidDoc({ - verificationMethod: verificationMethodType || VerificationMethods.Ed255192018, + verificationMethod: verificationMethodType, verificationMethodId: 'key-1', - methodSpecificIdAlgo: (methodSpecificIdAlgo as MethodSpecificIdAlgo) || MethodSpecificIdAlgo.Uuid, + methodSpecificIdAlgo: identifierFormatType || MethodSpecificIdAlgo.Uuid, network, - publicKey: key.publicKeyHex, + publicKey: publicKeyHex, }); - if (assertionMethod) { - didDocument.assertionMethod = didDocument.authentication; + if (Array.isArray(request.body['@context'])) { + didDocument['@context'] = request.body['@context']; + } + if (typeof request.body['@context'] === 'string') { + didDocument['@context'] = [request.body['@context']]; } - if (serviceEndpoint) { - didDocument.service = [ - { - id: `${didDocument.id}#service-1`, - type: 'service-1', - serviceEndpoint: [serviceEndpoint], - }, - ]; + if (service) { + if (Array.isArray(service)) { + try { + const services = JSON.parse(`[${service.toString()}]`); + didDocument.service = []; + for (const service of services) { + didDocument.service.push({ + id: `${didDocument.id}#${service.idFragment}`, + type: service.type, + serviceEndpoint: service.serviceEndpoint, + }) + } + } catch (e) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'Provide the correct service section to create a DID', + }); + }; + } else { + didDocument.service = [{ + id: `${didDocument.id}#${service.idFragment}`, + type: service.type, + serviceEndpoint: service.serviceEndpoint, + }]; + } } } else { return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'Provide a DID Document or the network type to create a DID', + error: 'Provide a DID Document or the VerificationMethodType to create a DID', }); } @@ -507,7 +546,7 @@ export class IssuerController { if (result) { const url = new URL( `${process.env.RESOLVER_URL || DefaultResolverUrl}${did}?` + - `resourceId=${resourcePayload.id}&resourceMetadata=true` + `resourceId=${resourcePayload.id}&resourceMetadata=true` ); const didDereferencing = (await (await fetch(url)).json()) as DIDMetadataDereferencingResult; @@ -651,11 +690,11 @@ export class IssuerController { try { const did = request.params.did ? await new IdentityServiceStrategySetup(response.locals.customerId).agent.resolveDid( - request.params.did - ) + request.params.did + ) : await new IdentityServiceStrategySetup(response.locals.customerId).agent.listDids( - response.locals.customerId - ); + response.locals.customerId + ); return response.status(StatusCodes.OK).json(did); } catch (error) { diff --git a/src/helpers/helpers.ts b/src/helpers/helpers.ts index 0e5c489e..c357580f 100644 --- a/src/helpers/helpers.ts +++ b/src/helpers/helpers.ts @@ -66,6 +66,16 @@ export function toDefaultDkg(did: string): DkgOptions { } } +export function validateDidCreatePayload(didDocument: DIDDocument): SpecValidationResult { + if (!didDocument) return { valid: true }; + + // id is required, validated on both compile and runtime + if (!didDocument.id || !didDocument.id.startsWith('did:cheqd:')) return { valid: false, error: 'id is required' }; + + if (!isValidService(didDocument)) return { valid: false, error: 'Service is Invalid' }; + return { valid: true } as SpecValidationResult; +} + export function validateSpecCompliantPayload(didDocument: DIDDocument): SpecValidationResult { // id is required, validated on both compile and runtime if (!didDocument.id && !didDocument.id.startsWith('did:cheqd:')) return { valid: false, error: 'id is required' }; @@ -92,8 +102,8 @@ export function validateSpecCompliantPayload(didDocument: DIDDocument): SpecVali export function isValidService(didDocument: DIDDocument): boolean { return didDocument.service ? didDocument?.service?.every((s) => { - return s?.serviceEndpoint && s?.id && s?.type; - }) + return s?.serviceEndpoint && s?.id && s?.type; + }) : true; } diff --git a/src/static/swagger.json b/src/static/swagger.json index 0af095b0..28114bb5 100644 --- a/src/static/swagger.json +++ b/src/static/swagger.json @@ -581,12 +581,12 @@ "content": { "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/DidCreateRequest" + "$ref": "#/components/schemas/DidCreateRequestFormBased" } }, "application/json": { "schema": { - "$ref": "#/components/schemas/DidCreateRequest" + "$ref": "#/components/schemas/DidCreateRequestJson" } } } @@ -2476,7 +2476,84 @@ ] } }, - "DidCreateRequest": { + "DidDocumentWithoutVerificationMethod": { + "type": "object", + "properties": { + "@context": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "controllers": { + "type": "array", + "items": { + "type": "string" + } + }, + "service": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + } + }, + "authentication": { + "type": "array", + "items": { + "type": "string" + } + }, + "assertionMethod": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityInvocation": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilityDelegation": { + "type": "array", + "items": { + "type": "string" + } + }, + "keyAgreement": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0", + "controller": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0" + ], + "authentication": [ + "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1" + ], + "service": [ + { + "id": "did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1", + "type": "LinkedDomains", + "serviceEndpoint": [ + "https://example.com" + ] + } + ] + } + }, + "DidCreateRequestFormBased": { "type": "object", "properties": { "network": { @@ -2487,7 +2564,7 @@ "mainnet" ] }, - "methodSpecificIdAlgo": { + "identifierFormatType": { "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", "type": "string", "enum": [ @@ -2504,13 +2581,89 @@ "Ed25519VerificationKey2020" ] }, + "service": { + "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", + "type": "array", + "items": { + "type": "object", + "properties": { + "idFragment": { + "type": "string", + "example": "service-1" + }, + "type": { + "type": "string", + "example": "LinkedDomains" + }, + "serviceEndpoint": { + "type": "array", + "items": { + "type": "string", + "example": "https://example.com" + } + } + } + } + }, + "key": { + "description": "The unique identifier in hexadecimal public key format used in the verification method to create the DID.", + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "https://www.w3.org/ns/did/v1" + ] + } + } + }, + "DidCreateRequestJson": { + "type": "object", + "properties": { + "network": { + "description": "Network to create the DID on (testnet or mainnet)", + "type": "string", + "enum": [ + "testnet", + "mainnet" + ] + }, + "identifierFormatType": { + "description": "Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details.", + "type": "string", + "enum": [ + "uuid", + "base58btc" + ] + }, "assertionMethod": { "description": "Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details.", "type": "boolean", "default": true }, + "options": { + "type": "object", + "properties": { + "key": { + "type": "string", + "example": "8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a" + }, + "verificationMethodType": { + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", + "type": "string", + "enum": [ + "Ed25519VerificationKey2018", + "JsonWebKey2020", + "Ed25519VerificationKey2020" + ] + } + } + }, "didDocument": { - "$ref": "#/components/schemas/DidDocument" + "$ref": "#/components/schemas/DidDocumentWithoutVerificationMethod" } } }, @@ -2567,6 +2720,7 @@ } }, "Service": { + "description": "Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details.", "type": "object", "properties": { "id": { diff --git a/src/types/shared.ts b/src/types/shared.ts index 1cfdeb1d..97ce9791 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -29,8 +29,9 @@ import type { ICredentialIssuerLD } from '@veramo/credential-ld'; import type { AbstractIdentifierProvider } from '@veramo/did-manager'; import type { AbstractKeyManagementSystem } from '@veramo/key-manager'; import type { DataSource } from 'typeorm'; -import { CheqdNetwork } from '@cheqd/sdk'; +import { CheqdNetwork, MethodSpecificIdAlgo, Service, VerificationMethods } from '@cheqd/sdk'; import type { AlternativeUri } from '@cheqd/ts-proto/cheqd/resource/v2'; +import type { DIDDocument } from 'did-resolver'; const DefaultUuidPattern = '([a-zA-Z0-9-]{36})'; const DefaultMethodSpecificIdPattern = `(?:[a-zA-Z0-9]{21,22}|${DefaultUuidPattern})`; @@ -61,6 +62,20 @@ export type MinimalPaymentCondition = { feePaymentWindow: number; // in minutes, strictly integer, e.g. 5 minutes, 10 minutes }; +export type CreateDidRequestBody = { + didDocument?: DIDDocument + identifierFormatType: MethodSpecificIdAlgo; + network: CheqdNetwork; + verificationMethodType?: VerificationMethods; + service?: Service | Service[]; + '@context'?: string | string[]; + key?: string; + options?: { + verificationMethodType: VerificationMethods; + key: string; + }; +}; + export type CreateUnencryptedStatusListRequestBody = { did: string; statusListName: string; diff --git a/src/types/swagger-types.ts b/src/types/swagger-types.ts index 9edaf6c8..4869e061 100644 --- a/src/types/swagger-types.ts +++ b/src/types/swagger-types.ts @@ -726,7 +726,57 @@ * type: LinkedDomains * serviceEndpoint: * - https://example.com - * DidCreateRequest: + * DidDocumentWithoutVerificationMethod: + * type: object + * properties: + * '@context': + * type: array + * items: + * type: string + * id: + * type: string + * controllers: + * type: array + * items: + * type: string + * service: + * type: array + * items: + * $ref: '#/components/schemas/Service' + * authentication: + * type: array + * items: + * type: string + * assertionMethod: + * type: array + * items: + * type: string + * capabilityInvocation: + * type: array + * items: + * type: string + * capabilityDelegation: + * type: array + * items: + * type: string + * keyAgreement: + * type: array + * items: + * type: string + * example: + * '@context': + * - https://www.w3.org/ns/did/v1 + * id: did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0 + * controller: + * - did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0 + * authentication: + * - did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#key-1 + * service: + * - id: did:cheqd:testnet:7bf81a20-633c-4cc7-bc4a-5a45801005e0#service-1 + * type: LinkedDomains + * serviceEndpoint: + * - https://example.com + * DidCreateRequestFormBased: * type: object * properties: * network: @@ -735,7 +785,7 @@ * enum: * - testnet * - mainnet - * methodSpecificIdAlgo: + * identifierFormatType: * description: Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details. * type: string * enum: @@ -748,12 +798,67 @@ * - Ed25519VerificationKey2018 * - JsonWebKey2020 * - Ed25519VerificationKey2020 + * service: + * description: Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details. + * type: array + * items: + * type: object + * properties: + * idFragment: + * type: string + * example: service-1 + * type: + * type: string + * example: LinkedDomains + * serviceEndpoint: + * type: array + * items: + * type: string + * example: https://example.com + * key: + * description: The unique identifier in hexadecimal public key format used in the verification method to create the DID. + * type: string + * '@context': + * type: array + * items: + * type: string + * example: + * - https://www.w3.org/ns/did/v1 + * + * DidCreateRequestJson: + * type: object + * properties: + * network: + * description: Network to create the DID on (testnet or mainnet) + * type: string + * enum: + * - testnet + * - mainnet + * identifierFormatType: + * description: Algorithm to use for generating the method-specific ID. The two styles supported are UUIDs and Indy-style Base58. See cheqd DID method documentation for more details. + * type: string + * enum: + * - uuid + * - base58btc * assertionMethod: * description: Usually a reference to a Verification Method. An Assertion Method is required to issue JSON-LD credentials. See DID Core specification for more details. * type: boolean * default: true + * options: + * type: object + * properties: + * key: + * type: string + * example: 8255ddadd75695e01f3d98fcec8ccc7861a030b317d4326b0e48a4d579ddc43a + * verificationMethodType: + * description: Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported. + * type: string + * enum: + * - Ed25519VerificationKey2018 + * - JsonWebKey2020 + * - Ed25519VerificationKey2020 * didDocument: - * $ref: '#/components/schemas/DidDocument' + * $ref: '#/components/schemas/DidDocumentWithoutVerificationMethod' * DidResult: * type: object * properties: @@ -790,6 +895,7 @@ * publicKeyBase58: BTJiso1S4iSiReP6wGksSneGfiKHxz9SYcm2KknpqBJt * type: Ed25519VerificationKey2018 * Service: + * description: Communicating or interacting with the DID subject or associated entities via one or more service endpoints. See DID Core specification for more details. * type: object * properties: * id: diff --git a/tests/constants.ts b/tests/constants.ts index 74863f99..b5debb99 100644 --- a/tests/constants.ts +++ b/tests/constants.ts @@ -13,3 +13,27 @@ export const DEFAULT_SUBJECT_DID = 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLG // Messages export const DEFAULT_DOES_NOT_HAVE_PERMISSIONS = 'Unauthorized error: Your account is not authorized to carry out this action.'; + +export const DEFAULT_CONTEXT="https://www.w3.org/ns/did/v1" + +export const NOT_EXISTENT_KEY = "88888888888895e01f3d98fcec8ccc7861a030b317d4326b0e48a88888888888" +export const NOT_SUPPORTED_VERIFICATION_METHOD_TYPE = "not_supported_vm_type" + +export const INVALID_ID = "invalid_id"; +export const INVALID_DID = "invalid_did"; + +export enum NETWORK { + MAINNET = "mainnet", + TESTNET = "testnet" +}; + +export enum ID_TYPE { + UUID = "uuid", + BASE58BTC = "base58btc" +}; + +export enum VERIFICATION_METHOD_TYPES { + Ed25519VerificationKey2018 = "Ed25519VerificationKey2018", + Ed25519VerificationKey2020 = "Ed25519VerificationKey2020", + JsonWebKey2020 = "JsonWebKey2020" +}; diff --git a/tests/did/create.negative.spec.ts b/tests/did/create.negative.spec.ts new file mode 100644 index 00000000..77dc913c --- /dev/null +++ b/tests/did/create.negative.spec.ts @@ -0,0 +1,186 @@ +import { + NETWORK, + ID_TYPE, + INVALID_ID, + INVALID_DID, + NOT_EXISTENT_KEY, + VERIFICATION_METHOD_TYPES, + DEFAULT_DOES_NOT_HAVE_PERMISSIONS, + NOT_SUPPORTED_VERIFICATION_METHOD_TYPE +} from '../constants'; +import * as fs from 'fs'; +import { v4 } from 'uuid'; +import { test, expect } from '@playwright/test'; +import { StatusCodes } from 'http-status-codes'; + +const PAYLOADS_BASE_PATH = './tests/payloads/did'; + +test.use({ storageState: 'playwright/.auth/user.json' }); + +test('[Negative] It cannot create DID with missed verificationMethodType field in request body (Form based)', async ({ request }) => { + const response = await request.post(`/did/create`, { + data: `network=${NETWORK.TESTNET}&identifierFormatType=${ID_TYPE.BASE58BTC}`, + headers: { "Content-Type": "application/x-www-form-urlencoded" } + }); + expect(response.status()).toBe(StatusCodes.BAD_REQUEST); + expect(await response.text()).toEqual(expect.stringContaining("Provide a DID Document or the VerificationMethodType to create a DID")); +}); + +test('[Negative] It cannot create DID with not existent key in request body (Form based)', async ({ request }) => { + const response = await request.post(`/did/create`, { + data: `network=${NETWORK.TESTNET}&identifierFormatType=${ID_TYPE.BASE58BTC}&` + + `verificationMethodType=${VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020}&` + + `key=${NOT_EXISTENT_KEY}`, + headers: { "Content-Type": "application/x-www-form-urlencoded" } + }); + expect(response.status()).toBe(StatusCodes.INTERNAL_SERVER_ERROR); + expect(await response.text()).toEqual(expect.stringContaining("Key not found")); +}); + +test('[Negative] It cannot create DID with not existent key in request body (JSON based)', async ({ request }) => { + const did = `did:cheqd:testnet:${v4()}`; + const response = await request.post('/did/create', { + data: { + network: NETWORK.TESTNET, + identifierFormatType: ID_TYPE.UUID, + options: { + verificationMethodType: VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020, + key: NOT_EXISTENT_KEY + }, + didDocument: { + id: did, + controller: [ + did + ], + authentication: [ + `${did}#key-1` + ] + } + }, + headers: { "Content-Type": "application/json" } + }); + expect(response.status()).toBe(StatusCodes.INTERNAL_SERVER_ERROR); + expect(await response.text()).toEqual(expect.stringContaining("Key not found")); +}); + +test('[Negative] It cannot create DID with an invalid VerificationMethodType in request body (JSON based)', async ({ request }) => { + const did = `did:cheqd:testnet:${v4()}`; + const response = await request.post('/did/create', { + data: { + network: NETWORK.TESTNET, + identifierFormatType: ID_TYPE.UUID, + options: { + verificationMethodType: NOT_SUPPORTED_VERIFICATION_METHOD_TYPE + }, + didDocument: { + id: did, + controller: [ + did + ], + authentication: [ + `${did}#key-1` + ] + } + }, + headers: { "Content-Type": "application/json" } + }); + expect(response.status()).toBe(StatusCodes.INTERNAL_SERVER_ERROR); + expect(await response.text()).toEqual(expect.stringContaining("Unsupported verificationMethod type")); +}); + +test('[Negative] It cannot create DID with an invalid length of id in DIDDocument in request body (JSON based)', async ({ request }) => { + const invalidDidLength = `did:cheqd:testnet:${INVALID_ID}`; + const response = await request.post('/did/create', { + data: { + network: NETWORK.TESTNET, + identifierFormatType: ID_TYPE.UUID, + options: { + verificationMethodType: VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2018 + }, + didDocument: { + id: invalidDidLength, + controller: [ + invalidDidLength + ], + authentication: [ + `${invalidDidLength}#key-1` + ] + } + }, + headers: { "Content-Type": "application/json" } + }); + expect(response.status()).toBe(StatusCodes.INTERNAL_SERVER_ERROR); + expect(await response.text()).toEqual(expect.stringContaining("unique id should be one of: 16 bytes of decoded base58 string or UUID")); +}); + +test('[Negative] It cannot create DID with an invalid id format in DIDDocument in request body (JSON based)', async ({ request }) => { + const response = await request.post('/did/create', { + data: { + network: NETWORK.TESTNET, + identifierFormatType: ID_TYPE.UUID, + options: { + verificationMethodType: VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2018 + }, + didDocument: { + id: INVALID_DID, + controller: [ + INVALID_DID + ], + authentication: [ + `${INVALID_DID}#key-1` + ] + } + }, + headers: { "Content-Type": "application/json" } + }); + expect(response.status()).toBe(StatusCodes.BAD_REQUEST); + expect(await response.text()).toEqual(expect.stringContaining("Invalid didDocument")); +}); + +test('[Negative] It cannot create DID without VerificationMethodType in request body (JSON based)', async ({ request }) => { + const response = await request.post('/did/create', { + data: { + network: NETWORK.TESTNET, + identifierFormatType: ID_TYPE.UUID, + didDocument: { + id: INVALID_DID, + controller: [ + INVALID_DID + ], + authentication: [ + `${INVALID_DID}#key-1` + ] + } + }, + headers: { "Content-Type": "application/json" } + }); + expect(response.status()).toBe(StatusCodes.BAD_REQUEST); + expect(await response.text()).toEqual(expect.stringContaining("Invalid didDocument")); +}); + +test('[Negative] It cannot create DID without DidDocument in request body (JSON based)', async ({ request }) => { + const response = await request.post('/did/create', { + data: { + network: NETWORK.TESTNET, + identifierFormatType: ID_TYPE.UUID, + options: { + verificationMethodType: VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020 + }, + }, + headers: { "Content-Type": "application/json" } + }); + expect(response.status()).toBe(StatusCodes.BAD_REQUEST); + expect(await response.text()).toEqual(expect.stringContaining("Provide a DID Document or the VerificationMethodType to create a DID")); +}); + +test('[Negative] It cannot create DID in mainnet network for user with testnet role', async ({ request }) => { + const response = await request.post(`/did/create`, { + data: JSON.parse(fs.readFileSync(`${PAYLOADS_BASE_PATH}/did-create-without-permissions.json`, 'utf-8')), + headers: { + 'Content-Type': 'application/json', + }, + }); + expect(response).not.toBeOK(); + expect(response.status()).toBe(StatusCodes.FORBIDDEN); + expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); +}); diff --git a/tests/did/create.positive.spec.ts b/tests/did/create.positive.spec.ts new file mode 100644 index 00000000..43c94a6f --- /dev/null +++ b/tests/did/create.positive.spec.ts @@ -0,0 +1,185 @@ +import { + ID_TYPE, + NETWORK, + DEFAULT_CONTEXT, + VERIFICATION_METHOD_TYPES +} from '../constants'; +import { v4 } from 'uuid'; +import { buildSimpleService } from 'helpers'; +import { test, expect } from '@playwright/test'; +import { StatusCodes } from 'http-status-codes'; +import { MethodSpecificIdAlgo, createVerificationKeys } from '@cheqd/sdk'; + +test.use({ storageState: 'playwright/.auth/user.json' }); + +test('[Positive] It can create DID with mandatory properties (Form based + Indy style)', async ({ request }) => { + // send request to create DID + let response = await request.post(`/did/create`, { + data: `network=${NETWORK.TESTNET}&identifierFormatType=${ID_TYPE.BASE58BTC}&` + + `verificationMethodType=${VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020}`, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + // resolve a created DID + response = await request.get(`/did/search/${(await response.json()).did}`, { + headers: { 'Content-Type': 'application/json' } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + const didDocument = (await response.json()).didDocument; + + // Check mandatory properties + expect(didDocument.id.split(":")[2]).toBe(NETWORK.TESTNET); + expect(didDocument.verificationMethod[0].type).toBe(VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020); +}); + +test('[Positive] It can create DID with mandatory and optional properties (Form based + UUID style)', async ({ request }) => { + // send request to create key + let response = await request.post('/key/create', { + headers: { "Content-Type": "application/json" } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + const kid = (await response.json()).kid; + + // send request to create DID + response = await request.post(`/did/create`, { + data: `network=${NETWORK.TESTNET}&identifierFormatType=${ID_TYPE.BASE58BTC}&` + + `verificationMethodType=${VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020}&` + + `service=${JSON.stringify(buildSimpleService())}&key=${kid}&@context=${DEFAULT_CONTEXT}`, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + const body = await response.json(); + + // Check mandatory properties + expect(body.did.split(":")[2]).toBe(NETWORK.TESTNET); + expect(body.controllerKeyId).toBe(kid); + + // resolve a created DID + response = await request.get(`/did/search/${body.did}`, { + headers: { 'Content-Type': 'application/json' } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + const didDocument = (await response.json()).didDocument; + + // check optional properties + expect(didDocument.verificationMethod[0].type).toBe(VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020); + expect(didDocument.service[0].id).toBe(`${body.did}#service-1`); + expect(didDocument.service[0].type).toBe("LinkedDomains"); + expect(didDocument.service[0].serviceEndpoint[0]).toBe("https://example.com"); +}); + +test('[Positive] It can create DID with mandatory properties (JSON based + Indy style)', async ({ request }) => { + // send request to create key + let response = await request.post('/key/create', { + headers: { "Content-Type": "application/json" } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + const did = createVerificationKeys((await response.json()).kid, MethodSpecificIdAlgo.Base58, "key-1").didUrl; + + // send request to create DID + response = await request.post('/did/create', { + data: { + options: { + verificationMethodType: VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020 + }, + didDocument: { + id: did, + controller: [ + did + ], + authentication: [ + `${did}#key-1` + ] + } + }, + headers: { "Content-Type": "application/json" } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + // resolve a created DID + response = await request.get(`/did/search/${(await response.json()).did}`, { + headers: { 'Content-Type': 'application/json' } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + const didDocument = (await response.json()).didDocument; + + // Check mandatory properties + expect(didDocument.id.split(":")[2]).toBe(NETWORK.TESTNET); + expect(didDocument.verificationMethod[0].type).toBe(VERIFICATION_METHOD_TYPES.Ed25519VerificationKey2020); +}); + +test('[Positive] It can create DID with mandatory and optional properties (JSON based + UUID style)', async ({ request }) => { + // send request to create key + let response = await request.post('/key/create', { + headers: { "Content-Type": "application/json" } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + const kid = (await response.json()).kid; + + const did = `did:cheqd:testnet:${v4()}`; + response = await request.post('/did/create', { + data: { + network: NETWORK.TESTNET, + identifierFormatType: ID_TYPE.UUID, + assertionMethod: true, + options: { + verificationMethodType: VERIFICATION_METHOD_TYPES.JsonWebKey2020, + key: kid + }, + didDocument: { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + id: did, + controller: [ + did + ], + authentication: [ + `${did}#key-1` + ], + service: [{ + id: `${did}#service-1`, + type: "LinkedDomains", + serviceEndpoint: [ + "https://example.com" + ] + }] + } + }, + headers: { "Content-Type": "application/json" } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + // resolve a created DID + response = await request.get(`/did/search/${(did)}`, { + headers: { 'Content-Type': 'application/json' } + }); + expect(response).toBeOK(); + expect(response.status()).toBe(StatusCodes.OK); + + const didDocument = (await response.json()).didDocument; + + // check optional properties + expect(didDocument.verificationMethod[0].type).toBe(VERIFICATION_METHOD_TYPES.JsonWebKey2020); + expect(didDocument.service[0].id).toBe(`${did}#service-1`); + expect(didDocument.service[0].type).toBe("LinkedDomains"); + expect(didDocument.service[0].serviceEndpoint[0]).toBe("https://example.com"); +}); diff --git a/tests/did/deactivate.negative.spec.ts b/tests/did/deactivate.negative.spec.ts new file mode 100644 index 00000000..11ab2240 --- /dev/null +++ b/tests/did/deactivate.negative.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test'; +import { StatusCodes } from 'http-status-codes'; +import { DEFAULT_DOES_NOT_HAVE_PERMISSIONS, DEFAULT_MAINNET_DID } from '../constants'; + +test.use({ storageState: 'playwright/.auth/user.json' }); + +test('[Negative] It cannot deactivated DID in mainnet network for user with testnet role', async ({ request }) => { + const response = await request.post(`/did/deactivate/${DEFAULT_MAINNET_DID}`, {}); + expect(response).not.toBeOK(); + expect(response.status()).toBe(StatusCodes.FORBIDDEN); + expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); +}); diff --git a/tests/did/did.spec.ts b/tests/did/did.spec.ts deleted file mode 100644 index 3d3e1cec..00000000 --- a/tests/did/did.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { test, expect } from '@playwright/test'; -import { StatusCodes } from 'http-status-codes'; -import { DEFAULT_DOES_NOT_HAVE_PERMISSIONS, DEFAULT_MAINNET_DID } from '../constants'; -import * as fs from 'fs'; - -test.use({ storageState: 'playwright/.auth/user.json' }); - -const PAYLOADS_BASE_PATH = './tests/payloads/did'; - -// Negative tests. All of this tests should return 403 Forbidden -// cause here the user tries to make mainnet operations with testnet role -test('Create DID for user with testnet role but network is mainnet', async ({ request }) => { - const response = await request.post(`/did/create`, { - data: JSON.parse(fs.readFileSync(`${PAYLOADS_BASE_PATH}/did-create-without-permissions.json`, 'utf-8')), - headers: { - 'Content-Type': 'application/json', - }, - }); - expect(response).not.toBeOK(); - expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); -}); - -test('Update DID for user with testnet role but network is mainnet', async ({ request }) => { - const response = await request.post(`/did/update`, { - data: JSON.parse(fs.readFileSync(`${PAYLOADS_BASE_PATH}/did-update-without-permissions.json`, 'utf-8')), - headers: { - 'Content-Type': 'application/json', - }, - }); - expect(response).not.toBeOK(); - expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); -}); - -test('Deactivate DID for user with testnet role but network is mainnet', async ({ request }) => { - const response = await request.post(`/did/deactivate/${DEFAULT_MAINNET_DID}`, {}); - expect(response).not.toBeOK(); - expect(response.status()).toBe(StatusCodes.FORBIDDEN); - expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); -}); diff --git a/tests/did/update.negative.spec.ts b/tests/did/update.negative.spec.ts new file mode 100644 index 00000000..9e7f650a --- /dev/null +++ b/tests/did/update.negative.spec.ts @@ -0,0 +1,20 @@ +import * as fs from 'fs'; +import { test, expect } from '@playwright/test'; +import { StatusCodes } from 'http-status-codes'; +import { DEFAULT_DOES_NOT_HAVE_PERMISSIONS } from '../constants'; + +const PAYLOADS_BASE_PATH = './tests/payloads/did'; + +test.use({ storageState: 'playwright/.auth/user.json' }); + +test('[Negative] It cannot update DID in mainnet network for user with testnet role', async ({ request }) => { + const response = await request.post(`/did/update`, { + data: JSON.parse(fs.readFileSync(`${PAYLOADS_BASE_PATH}/did-update-without-permissions.json`, 'utf-8')), + headers: { + 'Content-Type': 'application/json', + }, + }); + expect(response).not.toBeOK(); + expect(response.status()).toBe(StatusCodes.FORBIDDEN); + expect(await response.text()).toEqual(expect.stringContaining(DEFAULT_DOES_NOT_HAVE_PERMISSIONS)); +}); diff --git a/tests/helpers.ts b/tests/helpers.ts index 3daa5952..7e96f6e3 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -8,7 +8,7 @@ import { export const buildSimpleCreateDID = (network = 'testnet') => { return { - methodSpecificIdAlgo: 'uuid', + identifierFormatType: 'uuid', verificationMethodType: 'Ed25519VerificationKey2018', assertionMethod: true, network: network, @@ -137,3 +137,16 @@ export const buildSimpleIssueCredentialRequest = ( }, }; }; + +export const buildSimpleService = ( + idFragment = "service-1", + type = "LinkedDomains", + serviceEndpoint = ["https://example.com"] +) => { + return { + idFragment: idFragment, + type: type, + serviceEndpoint: serviceEndpoint + }; +}; + diff --git a/tests/payloads/did/did-create-without-permissions.json b/tests/payloads/did/did-create-without-permissions.json index f6e1ecd8..6cb224bc 100644 --- a/tests/payloads/did/did-create-without-permissions.json +++ b/tests/payloads/did/did-create-without-permissions.json @@ -1,5 +1,5 @@ { - "methodSpecificIdAlgo": "uuid", + "identifierFormatType": "uuid", "verificationMethodType": "Ed25519VerificationKey2018", "assertionMethod": true, "network": "mainnet"