From d318db2a2c45df970fe142fbe4de1574ef1c1f17 Mon Sep 17 00:00:00 2001
From: Lyuben Penkovski <lyuben.penkovski@vereign.com>
Date: Tue, 31 Oct 2023 11:41:46 +0200
Subject: [PATCH] Unit and integration tests for create VC endpoint

---
 integration/integration_test.go         | 135 +++++++++++++++----
 integration/internal/client/signer.go   |  19 +++
 internal/service/signer/service_test.go | 165 +++++++++++++++++++++++-
 3 files changed, 293 insertions(+), 26 deletions(-)

diff --git a/integration/integration_test.go b/integration/integration_test.go
index 445abcb..e2fe64d 100644
--- a/integration/integration_test.go
+++ b/integration/integration_test.go
@@ -5,13 +5,13 @@ import (
 	"fmt"
 	"net/http"
 	"os"
+	"strings"
 	"testing"
 
+	"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
 	"github.com/hyperledger/aries-framework-go/pkg/vdr"
 	"github.com/hyperledger/aries-framework-go/pkg/vdr/key"
 	"github.com/hyperledger/aries-framework-go/pkg/vdr/web"
-
-	"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
 	"github.com/piprate/json-gold/ld"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -119,7 +119,7 @@ func TestCreateCredentialProof(t *testing.T) {
 		{
 			name:   "credential with invalid subject id",
 			vc:     []byte(credentialInvalidSubjectID),
-			errMsg: "invalid format of subject id",
+			errMsg: "invalid subject id: must be URI",
 		},
 		{
 			name:   "credential with numerical subject id",
@@ -256,7 +256,7 @@ func TestCreatePresentationProof(t *testing.T) {
 		{
 			name:   "presentation with credential with invalid subject id",
 			vp:     []byte(presentationWithInvalidSubjectID),
-			errMsg: "invalid format of subject id",
+			errMsg: "invalid subject id: must be URI",
 		},
 		{
 			name:   "presentation with credential with numerical subject id",
@@ -299,6 +299,90 @@ func TestCreatePresentationProof(t *testing.T) {
 	}
 }
 
+func TestCreateCredential(t *testing.T) {
+	initTests(t)
+
+	tests := []struct {
+		name     string
+		req      map[string]interface{}
+		contexts []string
+		errtext  string
+	}{
+		{
+			name:    "empty request",
+			errtext: "400 Bad Request",
+		},
+		{
+			name: "invalid request because issuer is missing",
+			req: map[string]interface{}{
+				"namespace":         "transit",
+				"key":               "key1",
+				"credentialSubject": map[string]interface{}{"cred1": "value1"},
+			},
+			errtext: "400 Bad Request",
+		},
+		{
+			name: "valid request with single credentialSubject claim",
+			req: map[string]interface{}{
+				"issuer":            "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
+				"namespace":         "transit",
+				"key":               "key1",
+				"credentialSubject": map[string]interface{}{"cred1": "value1"},
+			},
+		},
+		{
+			name: "valid request with multiple credentialSubject claims",
+			req: map[string]interface{}{
+				"issuer":    "did:web:comic-bullfrog-smoothly.ngrok-free.app:policy:example:returnDID:1.0:evaluation",
+				"namespace": "transit",
+				"key":       "key1",
+				"credentialSubject": map[string]interface{}{
+					"cred1": "value1",
+					"cred2": "value2",
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			reqData, err := json.Marshal(test.req)
+			require.NoError(t, err)
+
+			signer := client.NewSigner(addr)
+			vcWithProof, err := signer.CreateCredential(reqData)
+			if test.errtext != "" {
+				require.Error(t, err, fmt.Sprintf("got no error but expected %q", test.errtext))
+				assert.Contains(t, err.Error(), test.errtext)
+				return
+			}
+
+			vc, err := verifyCredentialProofs(vcWithProof)
+			require.NoError(t, err)
+			assert.NotNil(t, vc)
+
+			assert.NotEmpty(t, vc.Proofs)
+			assert.NotEmpty(t, vc.Proofs[0]["jws"])
+			assert.NotEmpty(t, vc.Proofs[0]["created"])
+			assert.NotEmpty(t, vc.Proofs[0]["verificationMethod"])
+			assert.Equal(t, "assertionMethod", vc.Proofs[0]["proofPurpose"])
+			assert.Equal(t, "JsonWebSignature2020", vc.Proofs[0]["type"])
+
+			// hyperledger aries always parse the subject map into an array (unless it's just a string)
+			subject, ok := vc.Subject.([]verifiable.Subject)
+			assert.True(t, ok)
+
+			expectedClaims, ok := test.req["credentialSubject"].(map[string]interface{})
+			assert.True(t, ok)
+			assert.Equal(t, len(expectedClaims), len(subject[0].CustomFields))
+
+			for key := range expectedClaims {
+				assert.Equal(t, expectedClaims[key], subject[0].CustomFields[key])
+			}
+		})
+	}
+}
+
 func TestCreatePresentation(t *testing.T) {
 	initTests(t)
 
@@ -317,7 +401,9 @@ func TestCreatePresentation(t *testing.T) {
 			req: map[string]interface{}{
 				"namespace": "transit",
 				"key":       "key1",
-				"data":      map[string]interface{}{"cred1": "value1"},
+				"data": []map[string]interface{}{
+					{"cred1": "value1"},
+				},
 			},
 			errtext: "400 Bad Request",
 		},
@@ -393,7 +479,7 @@ func TestCreatePresentation(t *testing.T) {
 			creds := vp.Credentials()
 			requiredCreds, ok := test.req["data"].([]map[string]interface{})
 			assert.True(t, ok)
-			assert.Equal(t, len(creds), len(requiredCreds))
+			assert.Equal(t, len(requiredCreds), len(creds))
 
 			for i, cred := range creds {
 				c, ok := cred.(map[string]interface{})
@@ -468,15 +554,9 @@ func TestCreateCredentialMultipleProofs(t *testing.T) {
 		// modify JWS value of the first proof by removing the last character
 		proof1 := parsedVC.Proofs[0]
 		if jws, ok := proof1["jws"].(string); ok && jws != "" {
-			// substitute last char of JWS with another one, so that the signature
-			// should become invalid
-			var modifiedJWS string
-			if jws[len(jws)-1] != 'a' {
-				modifiedJWS = jws[:len(jws)-1] + "a"
-			} else {
-				modifiedJWS = jws[:len(jws)-1] + "b"
-			}
-			parsedVC.Proofs[0]["jws"] = modifiedJWS
+			modifiedSignature, err := modifySignature(jws)
+			require.NoError(t, err)
+			parsedVC.Proofs[0]["jws"] = modifiedSignature
 		} else {
 			t.Errorf("expected to have proof 1 but it's missing or invalid")
 		}
@@ -487,7 +567,7 @@ func TestCreateCredentialMultipleProofs(t *testing.T) {
 		assert.NotNil(t, modifiedVC)
 
 		_, err = verifyCredentialProofs(modifiedVC)
-		assert.Error(t, err)
+		require.Error(t, err)
 		assert.Contains(t, err.Error(), "invalid signature")
 	})
 
@@ -501,15 +581,9 @@ func TestCreateCredentialMultipleProofs(t *testing.T) {
 		// modify JWS value of the second proof by removing the last character
 		proof2 := parsedVC.Proofs[1]
 		if jws, ok := proof2["jws"].(string); ok && jws != "" {
-			// substitute last char of JWS with another one, so that the signature
-			// should become invalid
-			var modifiedJWS string
-			if jws[len(jws)-1] != 'a' {
-				modifiedJWS = jws[:len(jws)-1] + "a"
-			} else {
-				modifiedJWS = jws[:len(jws)-1] + "b"
-			}
-			parsedVC.Proofs[1]["jws"] = modifiedJWS
+			modifiedSignature, err := modifySignature(jws)
+			require.NoError(t, err)
+			parsedVC.Proofs[1]["jws"] = modifiedSignature
 		} else {
 			t.Errorf("expected to have proof 2 but it's missing or invalid")
 		}
@@ -546,3 +620,14 @@ func verifyCredentialProofs(vcBytes []byte) (*verifiable.Credential, error) {
 
 	return vc, err
 }
+
+func modifySignature(jws string) (string, error) {
+	parts := strings.Split(jws, ".")
+	if len(parts) != 3 {
+		return "", fmt.Errorf("invalid jws signature")
+	}
+
+	modifiedJWS := parts[0] + "." + parts[1] + "." + "8hiz2aWSW_AWnZ_GnoQyHrYgGia0HxdYTQGYOVYkPLU"
+
+	return modifiedJWS, nil
+}
diff --git a/integration/internal/client/signer.go b/integration/internal/client/signer.go
index 1095e63..1014800 100644
--- a/integration/internal/client/signer.go
+++ b/integration/internal/client/signer.go
@@ -170,6 +170,25 @@ func (s *Signer) CreatePresentation(data []byte) ([]byte, error) {
 	return io.ReadAll(resp.Body)
 }
 
+func (s *Signer) CreateCredential(data []byte) ([]byte, error) {
+	req, err := http.NewRequest(http.MethodPost, s.addr+"/v1/credential", bytes.NewReader(data))
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, newErrorResponse(resp)
+	}
+
+	return io.ReadAll(resp.Body)
+}
+
 type errorResponse struct {
 	Code     int
 	Status   string
diff --git a/internal/service/signer/service_test.go b/internal/service/signer/service_test.go
index cdb6d1d..e7f724b 100644
--- a/internal/service/signer/service_test.go
+++ b/internal/service/signer/service_test.go
@@ -6,6 +6,7 @@ import (
 	"crypto/ecdsa"
 	"encoding/base64"
 	"encoding/json"
+	"fmt"
 	"net/http"
 	"testing"
 
@@ -294,7 +295,7 @@ func TestService_CredentialProof(t *testing.T) {
 			name:       "credential with invalid subject id",
 			credential: []byte(credentialWithInvalidSubjectID),
 			errkind:    errors.BadRequest,
-			errtext:    "invalid format of subject id",
+			errtext:    "invalid subject id: must be URI",
 		},
 		{
 			name:       "valid credential but signer cannot find key",
@@ -641,6 +642,168 @@ func TestService_PresentationProof(t *testing.T) {
 	}
 }
 
+func TestService_CreateCredential(t *testing.T) {
+	tests := []struct {
+		name          string
+		signer        *signerfakes.FakeVault
+		supportedKeys []string
+
+		issuer            string
+		namespace         string
+		keyname           string
+		credentialSubject map[string]interface{}
+
+		errkind errors.Kind
+		errtext string
+
+		contexts                []string
+		types                   []string
+		proofPurpose            string
+		proofType               string
+		proofVerificationMethod string
+		wantedCredentialSubject verifiable.Subject
+	}{
+		{
+			name:    "missing credential subject",
+			errtext: "invalid credential subject: non-empty map is expected",
+			errkind: errors.BadRequest,
+		},
+		{
+			name:              "invalid credential subject id",
+			credentialSubject: map[string]interface{}{"id": "invalid credential subject id"},
+			errtext:           "invalid subject id: must be URI",
+			errkind:           errors.BadRequest,
+		},
+		{
+			name:              "valid credential subject, but error finding signing key",
+			supportedKeys:     []string{"ed25519", "ecdsa-p256"},
+			issuer:            "https://example.com",
+			namespace:         "transit",
+			keyname:           "key2",
+			credentialSubject: map[string]interface{}{"id": "https://example.com"},
+			signer: &signerfakes.FakeVault{
+				KeyStub: func(ctx context.Context, namespace, key string) (*signer.VaultKey, error) {
+					return nil, fmt.Errorf("no such key")
+				},
+			},
+			errtext: "error getting signing key: no such key",
+			errkind: errors.Unknown,
+		},
+		{
+			name:              "valid credential subject and signing is successful",
+			supportedKeys:     []string{"ed25519", "ecdsa-p256"},
+			issuer:            "https://example.com",
+			namespace:         "transit",
+			keyname:           "key2",
+			credentialSubject: map[string]interface{}{"id": "https://example.com"},
+			signer: &signerfakes.FakeVault{
+				KeyStub: func(ctx context.Context, namespace, key string) (*signer.VaultKey, error) {
+					return &signer.VaultKey{
+						Name: "key2",
+						Type: "ecdsa-p256",
+					}, nil
+				},
+				WithKeyStub: func(namespace, key string) signer.Vault {
+					return &signerfakes.FakeVault{
+						SignStub: func(data []byte) ([]byte, error) {
+							return []byte("test signature"), nil
+						},
+					}
+				},
+			},
+			// expected attributes the VC must have
+			contexts: []string{
+				"https://www.w3.org/2018/credentials/v1",
+				"https://w3id.org/security/suites/jws-2020/v1",
+				"https://schema.org",
+			},
+			types:                   []string{verifiable.VCType},
+			proofPurpose:            "assertionMethod",
+			proofType:               "JsonWebSignature2020",
+			proofVerificationMethod: "https://example.com#key2",
+			wantedCredentialSubject: verifiable.Subject{
+				ID:           "https://example.com",
+				CustomFields: map[string]interface{}{},
+			},
+		},
+		{
+			name:              "valid credential with multiple claims and signing is successful",
+			supportedKeys:     []string{"ed25519", "ecdsa-p256"},
+			issuer:            "https://example.com",
+			namespace:         "transit",
+			keyname:           "key2",
+			credentialSubject: map[string]interface{}{"id": "https://example.com", "email": "test@mymail.com"},
+			signer: &signerfakes.FakeVault{
+				KeyStub: func(ctx context.Context, namespace, key string) (*signer.VaultKey, error) {
+					return &signer.VaultKey{
+						Name: "key2",
+						Type: "ecdsa-p256",
+					}, nil
+				},
+				WithKeyStub: func(namespace, key string) signer.Vault {
+					return &signerfakes.FakeVault{
+						SignStub: func(data []byte) ([]byte, error) {
+							return []byte("test signature"), nil
+						},
+					}
+				},
+			},
+			// expected attributes the VC must have
+			contexts: []string{
+				"https://www.w3.org/2018/credentials/v1",
+				"https://w3id.org/security/suites/jws-2020/v1",
+				"https://schema.org",
+			},
+			types:                   []string{verifiable.VCType},
+			proofPurpose:            "assertionMethod",
+			proofType:               "JsonWebSignature2020",
+			proofVerificationMethod: "https://example.com#key2",
+			wantedCredentialSubject: verifiable.Subject{
+				ID: "https://example.com",
+				CustomFields: map[string]interface{}{
+					"email": "test@mymail.com",
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			signer := signer.New(test.signer, test.supportedKeys, http.DefaultClient, zap.NewNop())
+
+			req := &goasigner.CreateCredentialRequest{
+				Issuer:            test.issuer,
+				Namespace:         test.namespace,
+				Key:               test.keyname,
+				CredentialSubject: test.credentialSubject,
+			}
+
+			credential, err := signer.CreateCredential(context.Background(), req)
+			if err != nil {
+				require.NotEmpty(t, test.errtext, "received error, but test case has no error: %v", err)
+				assert.Contains(t, err.Error(), test.errtext)
+				if e, ok := err.(*errors.Error); ok {
+					assert.Equal(t, test.errkind, e.Kind)
+				}
+				assert.Nil(t, credential)
+			} else {
+				require.Empty(t, test.errtext, "test case expects error, but got none")
+				assert.NotNil(t, credential)
+
+				vc, ok := credential.(*verifiable.Credential)
+				assert.True(t, ok)
+				assert.Equal(t, test.contexts, vc.Context)
+				assert.Equal(t, test.types, vc.Types)
+				assert.Equal(t, test.proofPurpose, vc.Proofs[0]["proofPurpose"])
+				assert.Equal(t, test.proofType, vc.Proofs[0]["type"])
+				assert.Equal(t, test.proofVerificationMethod, vc.Proofs[0]["verificationMethod"])
+				assert.NotEmpty(t, vc.Proofs[0]["jws"])
+				assert.Equal(t, test.wantedCredentialSubject, vc.Subject)
+			}
+		})
+	}
+}
+
 // ---------- Verifiable Credentials ---------- //
 
 //nolint:gosec
-- 
GitLab