Skip to content

Commit

Permalink
feat: use same issuer for monthly and annual products (#2654)
Browse files Browse the repository at this point in the history
* feat: use same issuer for monthly and annual product

* fix: fix typo in parseVerifyCredOpaque

* fix: use better field name and preserve error response

* fix: use more precise language in log messages
  • Loading branch information
pavelbrm authored Sep 12, 2024
1 parent e3b825a commit 35f7182
Show file tree
Hide file tree
Showing 9 changed files with 526 additions and 175 deletions.
118 changes: 90 additions & 28 deletions services/skus/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,12 @@ func Router(
func CredentialRouter(svc *Service, authMwr middlewareFn) chi.Router {
r := chi.NewRouter()

valid := validator.New()

r.Method(
http.MethodPost,
"/subscription/verifications",
middleware.InstrumentHandler("VerifyCredentialV1", authMwr(VerifyCredentialV1(svc))),
middleware.InstrumentHandler("handleVerifyCredV1", authMwr(handleVerifyCredV1(svc, valid))),
)

return r
Expand All @@ -153,10 +155,12 @@ func CredentialRouter(svc *Service, authMwr middlewareFn) chi.Router {
func CredentialV2Router(svc *Service, authMwr middlewareFn) chi.Router {
r := chi.NewRouter()

valid := validator.New()

r.Method(
http.MethodPost,
"/subscription/verifications",
middleware.InstrumentHandler("VerifyCredentialV2", authMwr(VerifyCredentialV2(svc))),
middleware.InstrumentHandler("handleVerifyCredV2", authMwr(handleVerifyCredV2(svc, valid))),
)

return r
Expand Down Expand Up @@ -942,54 +946,73 @@ func MerchantTransactions(service *Service) handlers.AppHandler {
})
}

func VerifyCredentialV2(service *Service) handlers.AppHandler {
func handleVerifyCredV2(svc *Service, valid *validator.Validate) handlers.AppHandler {
return func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
ctx := r.Context()

l := logging.Logger(ctx, "skus").With().Str("func", "VerifyCredentialV2").Logger()
lg := logging.Logger(ctx, "skus").With().Str("func", "handleVerifyCredV2").Logger()

data, err := io.ReadAll(io.LimitReader(r.Body, reqBodyLimit10MB))
if err != nil {
lg.Warn().Err(err).Msg("failed to read body")

return handlers.WrapError(err, "Error in request body", http.StatusBadRequest)
}

req, err := parseVerifyCredRequestV2(data)
if err != nil {
lg.Warn().Err(err).Msg("failed to parse request")

return handlers.WrapError(err, "Error in request body", http.StatusBadRequest)
}

if err := validateVerifyCredRequestV2(valid, req); err != nil {
lg.Warn().Err(err).Msg("failed to validate request")

req := &VerifyCredentialRequestV2{}
if err := inputs.DecodeAndValidateReader(ctx, req, r.Body); err != nil {
l.Error().Err(err).Msg("failed to read request")
return handlers.WrapError(err, "Error in request body", http.StatusBadRequest)
}

appErr := service.verifyCredential(ctx, req, w)
if appErr != nil {
l.Error().Err(appErr).Msg("failed to verify credential")
aerr := svc.verifyCredential(ctx, req, w)
if aerr != nil {
lg.Err(aerr).Msg("failed to verify credential")
}

return appErr
return aerr
}
}

// VerifyCredentialV1 is the handler for verifying subscription credentials
func VerifyCredentialV1(service *Service) handlers.AppHandler {
func handleVerifyCredV1(svc *Service, valid *validator.Validate) handlers.AppHandler {
return func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
ctx := r.Context()
l := logging.Logger(r.Context(), "VerifyCredentialV1")

var req = new(VerifyCredentialRequestV1)
lg := logging.Logger(ctx, "skus").With().Str("func", "handleVerifyCredV1").Logger()

err := requestutils.ReadJSON(r.Context(), r.Body, &req)
data, err := io.ReadAll(io.LimitReader(r.Body, reqBodyLimit10MB))
if err != nil {
l.Error().Err(err).Msg("failed to read request")
lg.Warn().Err(err).Msg("failed to read body")

return handlers.WrapError(err, "Error in request body", http.StatusBadRequest)
}
l.Debug().Msg("read verify credential post body")

_, err = govalidator.ValidateStruct(req)
if err != nil {
l.Error().Err(err).Msg("failed to validate request")
req := &model.VerifyCredentialRequestV1{}
if err := json.Unmarshal(data, req); err != nil {
lg.Warn().Err(err).Msg("failed to parse request")

return handlers.WrapError(err, "Error in request body", http.StatusBadRequest)
}

if err := valid.StructCtx(ctx, req); err != nil {
lg.Warn().Err(err).Msg("failed to validate request")

return handlers.WrapError(err, "Error in request validation", http.StatusBadRequest)
}

appErr := service.verifyCredential(ctx, req, w)
if appErr != nil {
l.Error().Err(appErr).Msg("failed to verify credential")
aerr := svc.verifyCredential(ctx, req, w)
if aerr != nil {
lg.Err(aerr).Msg("failed to verify credential")
}

return appErr
return aerr
}
}

Expand Down Expand Up @@ -1345,7 +1368,7 @@ func handleSubmitReceipt(svc *Service, valid *validator.Validate) handlers.AppHa

req, err := parseSubmitReceiptRequest(payload)
if err != nil {
l.Warn().Err(err).Msg("failed to deserialize request")
l.Warn().Err(err).Msg("failed to parse request")

return handlers.ValidationError("request", map[string]interface{}{"request-body": err.Error()})
}
Expand Down Expand Up @@ -1412,7 +1435,7 @@ func handleCreateOrderFromReceiptH(w http.ResponseWriter, r *http.Request, svc *

req, err := parseSubmitReceiptRequest(raw)
if err != nil {
lg.Warn().Err(err).Msg("failed to deserialize request")
lg.Warn().Err(err).Msg("failed to parse request")

return handlers.ValidationError("request", map[string]interface{}{"request-body": err.Error()})
}
Expand Down Expand Up @@ -1480,7 +1503,7 @@ func handleCheckOrderReceiptH(w http.ResponseWriter, r *http.Request, svc *Servi

req, err := parseSubmitReceiptRequest(raw)
if err != nil {
lg.Warn().Err(err).Msg("failed to deserialize request")
lg.Warn().Err(err).Msg("failed to parse request")

return handlers.ValidationError("request", map[string]interface{}{"request-body": err.Error()})
}
Expand Down Expand Up @@ -1601,3 +1624,42 @@ func collectValidationErrors(err error) (map[string]string, bool) {

return result, true
}

func parseVerifyCredRequestV2(raw []byte) (*model.VerifyCredentialRequestV2, error) {
result := &model.VerifyCredentialRequestV2{}

if err := json.Unmarshal(raw, result); err != nil {
return nil, err
}

copaque, err := parseVerifyCredOpaque(result.Credential)
if err != nil {
return nil, err
}

result.CredentialOpaque = copaque

return result, nil
}

func parseVerifyCredOpaque(raw string) (*model.VerifyCredentialOpaque, error) {
data, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, err
}

result := &model.VerifyCredentialOpaque{}
if err = json.Unmarshal(data, result); err != nil {
return nil, err
}

return result, nil
}

func validateVerifyCredRequestV2(valid *validator.Validate, req *model.VerifyCredentialRequestV2) error {
if err := valid.Struct(req); err != nil {
return err
}

return valid.Struct(req.CredentialOpaque)
}
149 changes: 149 additions & 0 deletions services/skus/controllers_noint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package skus
import (
"context"
"net/http"
"reflect"
"testing"

"github.com/go-playground/validator/v10"
Expand Down Expand Up @@ -222,3 +223,151 @@ func TestHandleReceiptErr(t *testing.T) {
})
}
}

func TestParseVerifyCredRequestV2(t *testing.T) {
type tcExpected struct {
val *model.VerifyCredentialRequestV2
mustErr must.ErrorAssertionFunc
}

type testCase struct {
name string
given []byte
exp tcExpected
}

tests := []testCase{
{
name: "error_malformed_payload",
given: []byte(`nonsense`),
exp: tcExpected{
mustErr: func(tt must.TestingT, err error, i ...interface{}) {
must.Equal(tt, true, err != nil)
},
},
},

{
name: "error_malformed_credential",
given: []byte(`{"sku":"sku","merchantId":"merchantId"}`),
exp: tcExpected{
mustErr: func(tt must.TestingT, err error, i ...interface{}) {
must.Equal(tt, true, err != nil)
},
},
},

{
name: "success_complete",
given: []byte(`{"sku": "sku","merchantId": "merchantId","credential":"eyJ0eXBlIjoidGltZS1saW1pdGVkLXYyIiwicHJlc2VudGF0aW9uIjoiVG1GMGRYSmxJR0ZpYUc5eWN5QmhJSFpoWTNWMWJTNEsifQo="}`),
exp: tcExpected{
val: &model.VerifyCredentialRequestV2{
SKU: "sku",
MerchantID: "merchantId",
Credential: "eyJ0eXBlIjoidGltZS1saW1pdGVkLXYyIiwicHJlc2VudGF0aW9uIjoiVG1GMGRYSmxJR0ZpYUc5eWN5QmhJSFpoWTNWMWJTNEsifQo=",
CredentialOpaque: &model.VerifyCredentialOpaque{
Type: "time-limited-v2",
Presentation: "TmF0dXJlIGFiaG9ycyBhIHZhY3V1bS4K",
},
},
mustErr: func(tt must.TestingT, err error, i ...interface{}) {
must.Equal(tt, true, err == nil)
},
},
},
}

for i := range tests {
tc := tests[i]

t.Run(tc.name, func(t *testing.T) {
actual, err := parseVerifyCredRequestV2(tc.given)
tc.exp.mustErr(t, err)

should.Equal(t, tc.exp.val, actual)
})
}
}

func TestValidateVerifyCredRequestV2(t *testing.T) {
type tcGiven struct {
valid *validator.Validate
req *model.VerifyCredentialRequestV2
}

tests := []struct {
name string
given tcGiven
exp error
}{
{
name: "error_credential_opaque_nil",
given: tcGiven{
valid: validator.New(),
req: &model.VerifyCredentialRequestV2{
SKU: "sku",
MerchantID: "merchantId",
Credential: "eyJ0eXBlIjoic2luZ2xlLXVzZSIsInByZXNlbnRhdGlvbiI6IlRtRjBkWEpsSUdGaWFHOXljeUJoSUhaaFkzVjFiUzRLIn0K",
},
},
exp: &validator.InvalidValidationError{Type: reflect.TypeOf((*model.VerifyCredentialOpaque)(nil))},
},

{
name: "valid_single_use",
given: tcGiven{
valid: validator.New(),
req: &model.VerifyCredentialRequestV2{
SKU: "sku",
MerchantID: "merchantId",
Credential: "eyJ0eXBlIjoic2luZ2xlLXVzZSIsInByZXNlbnRhdGlvbiI6IlRtRjBkWEpsSUdGaWFHOXljeUJoSUhaaFkzVjFiUzRLIn0K",
CredentialOpaque: &model.VerifyCredentialOpaque{
Type: "single-use",
Presentation: "TmF0dXJlIGFiaG9ycyBhIHZhY3V1bS4K",
},
},
},
},

{
name: "valid_time_limited",
given: tcGiven{
valid: validator.New(),
req: &model.VerifyCredentialRequestV2{
SKU: "sku",
MerchantID: "merchantId",
Credential: "eyJ0eXBlIjoidGltZS1saW1pdGVkIiwicHJlc2VudGF0aW9uIjoiVG1GMGRYSmxJR0ZpYUc5eWN5QmhJSFpoWTNWMWJTNEsifQo=",
CredentialOpaque: &model.VerifyCredentialOpaque{
Type: "time-limited",
Presentation: "TmF0dXJlIGFiaG9ycyBhIHZhY3V1bS4K",
},
},
},
},

{
name: "valid_time_limited_v2",
given: tcGiven{
valid: validator.New(),
req: &model.VerifyCredentialRequestV2{
SKU: "sku",
MerchantID: "merchantId",
Credential: "eyJ0eXBlIjoidGltZS1saW1pdGVkLXYyIiwicHJlc2VudGF0aW9uIjoiVG1GMGRYSmxJR0ZpYUc5eWN5QmhJSFpoWTNWMWJTNEsifQo=",
CredentialOpaque: &model.VerifyCredentialOpaque{
Type: "time-limited-v2",
Presentation: "TmF0dXJlIGFiaG9ycyBhIHZhY3V1bS4K",
},
},
},
},
}

for i := range tests {
tc := tests[i]

t.Run(tc.name, func(t *testing.T) {
actual := validateVerifyCredRequestV2(tc.given.valid, tc.given.req)
should.Equal(t, tc.exp, actual)
})
}
}
6 changes: 3 additions & 3 deletions services/skus/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var (
//
// This only happens in the event of a new sku being created.
func (s *Service) CreateIssuer(ctx context.Context, dbi sqlx.QueryerContext, merchID string, item *OrderItem) error {
encMerchID, err := encodeIssuerID(merchID, item.SKU)
encMerchID, err := encodeIssuerID(merchID, item.SKUForIssuer())
if err != nil {
return errorutils.Wrap(err, "error encoding issuer name")
}
Expand Down Expand Up @@ -114,7 +114,7 @@ func (s *Service) CreateIssuer(ctx context.Context, dbi sqlx.QueryerContext, mer
//
// This only happens in the event of a new sku being created.
func (s *Service) CreateIssuerV3(ctx context.Context, dbi sqlx.QueryerContext, merchID string, item *OrderItem, issuerCfg model.IssuerConfig) error {
encMerchID, err := encodeIssuerID(merchID, item.SKU)
encMerchID, err := encodeIssuerID(merchID, item.SKUForIssuer())
if err != nil {
return errorutils.Wrap(err, "error encoding issuer name")
}
Expand Down Expand Up @@ -266,7 +266,7 @@ func (s *Service) CreateOrderItemCredentials(ctx context.Context, orderID, itemI
return err
}

issuerID, err := encodeIssuerID(order.MerchantID, item.SKU)
issuerID, err := encodeIssuerID(order.MerchantID, item.SKUForIssuer())
if err != nil {
return errorutils.Wrap(err, "error encoding issuer name")
}
Expand Down
Loading

0 comments on commit 35f7182

Please sign in to comment.