Skip to content

Commit

Permalink
feat: more refresh tests
Browse files Browse the repository at this point in the history
  • Loading branch information
skynet2 committed Aug 13, 2024
1 parent 0545579 commit 22106bc
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 26 deletions.
218 changes: 218 additions & 0 deletions pkg/restapi/v1/refresh/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,221 @@ SPDX-License-Identifier: Apache-2.0
*/

package refresh_test

import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/golang/mock/gomock"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"

profileapi "github.com/trustbloc/vcs/pkg/profile"
"github.com/trustbloc/vcs/pkg/restapi/v1/refresh"
refresh2 "github.com/trustbloc/vcs/pkg/service/refresh"
)

const (
orgID = "orgID1"
profileID = "testID"
profileVersion = "v1.0"
)

func TestGetRefreshStatus(t *testing.T) {
t.Run("success", func(t *testing.T) {
recorder := httptest.NewRecorder()

profileSvc := NewMockProfileService(gomock.NewController(t))
refreshSvc := NewMockCredentialRefreshService(gomock.NewController(t))

echoCtx := echoContext(
withRecorder(recorder),
)

echoCtx.Request().URL.Path = "/refresh/" + profileID + "/" + profileVersion

issuer := &profileapi.Issuer{}
profileSvc.EXPECT().GetProfile(profileID, profileVersion).
Return(issuer, nil)

refreshSvc.EXPECT().RequestRefreshStatus(gomock.Any(), "some-cred-id", *issuer).
Return(&refresh2.GetRefreshStateResponse{
Challenge: "challenge",
Domain: "domain",
RefreshServiceType: refresh2.ServiceType{
Type: "someType",
URL: "someURL",
},
}, nil)

ctr := refresh.NewController(&refresh.Config{
ProfileService: profileSvc,
RefreshService: refreshSvc,
IssuerVCSPublicHost: "https://public.local/api/vc",
})

assert.NoError(t, ctr.RequestRefreshStatus(echoCtx, profileID, profileVersion, refresh.RequestRefreshStatusParams{
CredentialID: "some-cred-id",
}))

var res refresh.CredentialRefreshAvailableResponse
assert.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res))

assert.Equal(t, "challenge", res.VerifiablePresentationRequest.Challenge)
assert.Equal(t, "domain", res.VerifiablePresentationRequest.Domain)

assert.Len(t, res.VerifiablePresentationRequest.Interact.Service, 1)
assert.Equal(t, "someType", res.VerifiablePresentationRequest.Interact.Service[0].Type)
assert.Equal(t, "https://public.local/api/vc/refresh/testID/v1.0",
res.VerifiablePresentationRequest.Interact.Service[0].ServiceEndpoint)

assert.EqualValues(t, recorder.Result().StatusCode, http.StatusOK)
})

t.Run("no updates", func(t *testing.T) {
recorder := httptest.NewRecorder()

profileSvc := NewMockProfileService(gomock.NewController(t))
refreshSvc := NewMockCredentialRefreshService(gomock.NewController(t))

echoCtx := echoContext(
withRecorder(recorder),
)

echoCtx.Request().URL.Path = "/refresh/" + profileID + "/" + profileVersion

issuer := &profileapi.Issuer{}
profileSvc.EXPECT().GetProfile(profileID, profileVersion).
Return(issuer, nil)

refreshSvc.EXPECT().RequestRefreshStatus(gomock.Any(), "some-cred-id", *issuer).
Return(nil, nil)

ctr := refresh.NewController(&refresh.Config{
ProfileService: profileSvc,
RefreshService: refreshSvc,
IssuerVCSPublicHost: "https://public.local/api/vc",
})

assert.NoError(t, ctr.RequestRefreshStatus(echoCtx, profileID, profileVersion, refresh.RequestRefreshStatusParams{
CredentialID: "some-cred-id",
}))

assert.EqualValues(t, recorder.Result().StatusCode, http.StatusNoContent)
})

t.Run("refresh err", func(t *testing.T) {
recorder := httptest.NewRecorder()

profileSvc := NewMockProfileService(gomock.NewController(t))
refreshSvc := NewMockCredentialRefreshService(gomock.NewController(t))

echoCtx := echoContext(
withRecorder(recorder),
)

echoCtx.Request().URL.Path = "/refresh/" + profileID + "/" + profileVersion

issuer := &profileapi.Issuer{}
profileSvc.EXPECT().GetProfile(profileID, profileVersion).
Return(issuer, nil)

refreshSvc.EXPECT().RequestRefreshStatus(gomock.Any(), "some-cred-id", *issuer).
Return(nil, errors.New("refresh err"))

ctr := refresh.NewController(&refresh.Config{
ProfileService: profileSvc,
RefreshService: refreshSvc,
IssuerVCSPublicHost: "https://public.local/api/vc",
})

assert.ErrorContains(t, ctr.RequestRefreshStatus(echoCtx, profileID, profileVersion, refresh.RequestRefreshStatusParams{
CredentialID: "some-cred-id",
}), "refresh err")
})

t.Run("profile err", func(t *testing.T) {
recorder := httptest.NewRecorder()

profileSvc := NewMockProfileService(gomock.NewController(t))
refreshSvc := NewMockCredentialRefreshService(gomock.NewController(t))

echoCtx := echoContext(
withRecorder(recorder),
)

echoCtx.Request().URL.Path = "/refresh/" + profileID + "/" + profileVersion

profileSvc.EXPECT().GetProfile(profileID, profileVersion).
Return(nil, errors.New("profile err"))

ctr := refresh.NewController(&refresh.Config{
ProfileService: profileSvc,
RefreshService: refreshSvc,
IssuerVCSPublicHost: "https://public.local/api/vc",
})

assert.ErrorContains(t, ctr.RequestRefreshStatus(echoCtx, profileID, profileVersion, refresh.RequestRefreshStatusParams{
CredentialID: "some-cred-id",
}), "profile err")
})
}

type options struct {
tenantID string
requestBody []byte
responseWriter http.ResponseWriter
}

type contextOpt func(*options)

func withTenantID(tenantID string) contextOpt {

Check failure on line 181 in pkg/restapi/v1/refresh/controller_test.go

View workflow job for this annotation

GitHub Actions / Checks

func `withTenantID` is unused (unused)
return func(o *options) {
o.tenantID = tenantID
}
}

func withRequestBody(body []byte) contextOpt {

Check failure on line 187 in pkg/restapi/v1/refresh/controller_test.go

View workflow job for this annotation

GitHub Actions / Checks

func `withRequestBody` is unused (unused)
return func(o *options) {
o.requestBody = body
}
}

func withRecorder(w http.ResponseWriter) contextOpt {
return func(o *options) {
o.responseWriter = w
}
}

func echoContext(opts ...contextOpt) echo.Context {
o := &options{
tenantID: orgID,
responseWriter: httptest.NewRecorder(),
}

for _, fn := range opts {
fn(o)
}

e := echo.New()

var body io.Reader = http.NoBody

if o.requestBody != nil {
body = bytes.NewReader(o.requestBody)
}

req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

if o.tenantID != "" {
req.Header.Set("X-Tenant-ID", o.tenantID)
}

return e.NewContext(req, o.responseWriter)
}
2 changes: 2 additions & 0 deletions pkg/restapi/v1/refresh/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/trustbloc/vcs/pkg/service/refresh"
)

//go:generate mockgen -destination interfaces_mocks_test.go -package refresh_test -source=interfaces.go

// ProfileService defines issuer profile service interface.
type ProfileService interface {
GetProfile(profileID profileapi.ID, profileVersion profileapi.Version) (*profileapi.Issuer, error)
Expand Down
56 changes: 36 additions & 20 deletions pkg/service/refresh/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,14 @@ func (s *Service) GetRefreshedCredential(
}
cred := presentation.Credentials()[0]

allTypes := cred.Contents().Types
if len(allTypes) == 0 {
return nil, errors.New("no types in credential")
}

lastType := allTypes[len(cred.Contents().Types)-1]

var template *profile.CredentialTemplate
for _, t := range issuer.CredentialTemplates {
if t.Type == lastType {
template = t
break
}
}

if template == nil {
return nil, fmt.Errorf("no credential template found for credential type %v", lastType)
template, err := s.findCredentialTemplate(cred.Contents().Types, issuer)
if err != nil {
return nil, err

Check warning on line 78 in pkg/service/refresh/refresh.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/refresh/refresh.go#L78

Added line #L78 was not covered by tests
}

config, configID := s.findCredConfigSupported(issuer, lastType)
config, configID := s.findCredConfigSupported(issuer, template.Type)
if config == nil {
return nil, fmt.Errorf("no credential configuration found for credential type %v", lastType)
return nil, fmt.Errorf("no credential configuration found for credential type %v", template.Type)

Check warning on line 83 in pkg/service/refresh/refresh.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/refresh/refresh.go#L83

Added line #L83 was not covered by tests
}

tx, err := s.cfg.TxStore.FindByOpState(ctx, s.getOpState(cred.Contents().ID, issuer.ID))
Expand Down Expand Up @@ -150,6 +136,31 @@ func (s *Service) GetRefreshedCredential(
return updatedCred, err
}

func (s *Service) findCredentialTemplate(
allTypes []string,
issuer profile.Issuer,
) (*profile.CredentialTemplate, error) {
if len(allTypes) == 0 {
return nil, errors.New("no types in credential")

Check warning on line 144 in pkg/service/refresh/refresh.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/refresh/refresh.go#L144

Added line #L144 was not covered by tests
}

lastType := allTypes[len(allTypes)-1]

var template *profile.CredentialTemplate
for _, t := range issuer.CredentialTemplates {
if t.Type == lastType {
template = t
break
}
}

if template == nil {
return nil, fmt.Errorf("no credential template found for credential type %v", lastType)

Check warning on line 158 in pkg/service/refresh/refresh.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/refresh/refresh.go#L158

Added line #L158 was not covered by tests
}

return template, nil
}

func (s *Service) findCredConfigSupported(
issuer profile.Issuer,
lastType string,
Expand Down Expand Up @@ -245,14 +256,19 @@ func (s *Service) CreateRefreshState(

opState := s.getOpState(req.CredentialID, req.Issuer.ID)

refreshServiceEnabled := false
if req.Issuer.VCConfig != nil {
refreshServiceEnabled = req.Issuer.VCConfig.RefreshServiceEnabled

Check warning on line 261 in pkg/service/refresh/refresh.go

View check run for this annotation

Codecov / codecov/patch

pkg/service/refresh/refresh.go#L261

Added line #L261 was not covered by tests
}

tx, err := s.cfg.TxStore.Create(ctx, ttl, &issuecredential.TransactionData{
ProfileID: req.Issuer.ID,
ProfileVersion: req.Issuer.Version,
IsPreAuthFlow: true,
OrgID: req.Issuer.OrganizationID,
OpState: opState,
WebHookURL: req.Issuer.WebHook,
RefreshServiceEnabled: req.Issuer.VCConfig.RefreshServiceEnabled,
RefreshServiceEnabled: refreshServiceEnabled,
CredentialConfiguration: []*issuecredential.TxCredentialConfiguration{
{
ClaimDataType: issuecredential.ClaimDataTypeClaims,
Expand Down
8 changes: 4 additions & 4 deletions test/bdd/features/oidc4vc_api.feature
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ Feature: OIDC4VC REST API
Examples:
| issuerProfile | credentialType | clientRegistrationMethod | credentialTemplate | verifierProfile | presentationDefinitionID | fields |
# SDJWT issuer, JWT verifier, no limit disclosure in PD query.
# | bank_issuer/v1.0 | UniversityDegreeCredential | dynamic | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |
| bank_issuer/v1.0 | UniversityDegreeCredential | dynamic | universityDegreeTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-single-field | degree_type_id |
# SDJWT issuer, JWT verifier, limit disclosure and optional fields in PD query.
# | bank_issuer/v1.0 | CrudeProductCredential | discoverable | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address |
| bank_issuer/v1.0 | CrudeProductCredential | discoverable | crudeProductCredentialTemplateID | v_myprofile_jwt/v1.0 | 3c8b1d9a-limit-disclosure-optional-fields | unit_of_measure_barrel,api_gravity,category,supplier_address |
# JWT issuer, JWT verifier, no limit disclosure and optional fields in PD query.
# | i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | pre-registered | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification |
| i_myprofile_ud_es256k_jwt/v1.0 | PermanentResidentCard | pre-registered | permanentResidentCardTemplateID | v_myprofile_jwt/v1.0 | 32f54163-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification |
# LDP Data Integrity issuer, LDP verifier, no limit disclosure and schema match in PD query.
| i_myprofile_ud_di_ecdsa-2019/v1.0 | PermanentResidentCard | pre-registered | permanentResidentCardTemplateID | v_myprofile_ldp/v1.0 | 062759b1-no-limit-disclosure-optional-fields | lpr_category_id,registration_city,commuter_classification |
# LDP issuer, LDP verifier, no limit disclosure and schema match in PD query.
# | i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | pre-registered | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id |
| i_myprofile_cmtr_p256_ldp/v1.0 | CrudeProductCredential | pre-registered | crudeProductCredentialTemplateID | v_myprofile_ldp/v1.0 | lp403pb9-schema-match | schema_id |

@oidc4vc_rest_auth_flow_batch_credential_configuration_id
Scenario Outline: OIDC Batch credential issuance and verification Auth flow (request all credentials by credentialConfigurationID)
Expand Down
Loading

0 comments on commit 22106bc

Please sign in to comment.