From af1c228b79c5e7c85bfe2aa9576f0542e29d13b5 Mon Sep 17 00:00:00 2001 From: Kevin Joiner <10265309+KevinJoiner@users.noreply.github.com> Date: Fri, 26 Jul 2024 10:05:52 -0400 Subject: [PATCH] Update VIN VC query to return more fields. (#55) * Update VIN VC query to return more fields. * Updates expired and created field names * Handles error when no VC is found * Adds unit test for vc query * Adds docs for vc query --- go.mod | 2 +- go.sum | 4 +- internal/graph/generated.go | 397 ++++++++++++++++-- internal/graph/model/models_gen.go | 22 +- .../vinvc/mock_clickhouse_test.go | 229 ++++++++++ .../repositories/vinvc/mock_service_test.go | 81 ++++ internal/repositories/vinvc/vinvc.go | 48 ++- internal/repositories/vinvc/vinvvc_test.go | 173 ++++++++ schema/vinvc.graphqls | 44 +- 9 files changed, 943 insertions(+), 57 deletions(-) create mode 100644 internal/repositories/vinvc/mock_clickhouse_test.go create mode 100644 internal/repositories/vinvc/mock_service_test.go create mode 100644 internal/repositories/vinvc/vinvvc_test.go diff --git a/go.mod b/go.mod index e1803c2..095a1fb 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.0 require ( github.com/99designs/gqlgen v0.17.49 github.com/ClickHouse/clickhouse-go/v2 v2.26.0 - github.com/DIMO-Network/attestation-api v0.0.2 + github.com/DIMO-Network/attestation-api v0.0.4-0.20240725185510-66575e8ba086 github.com/DIMO-Network/clickhouse-infra v0.0.1 github.com/DIMO-Network/model-garage v0.2.11 github.com/DIMO-Network/nameindexer v0.0.1 diff --git a/go.sum b/go.sum index 4f1fc2a..2f2dc63 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,8 @@ github.com/ClickHouse/clickhouse-go/v2 v2.26.0 h1:j4/y6NYaCcFkJwN/TU700ebW+nmsIy github.com/ClickHouse/clickhouse-go/v2 v2.26.0/go.mod h1:iDTViXk2Fgvf1jn2dbJd1ys+fBkdD1UMRnXlwmhijhQ= github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/DIMO-Network/attestation-api v0.0.2 h1:rHxy0VdCaaXLg5RBg5/bjBhMvzLcTej2sTosI4Khwek= -github.com/DIMO-Network/attestation-api v0.0.2/go.mod h1:xqLVVCSCo/zVbujSCmNwDQq1DPto+8nFI05wDeugz10= +github.com/DIMO-Network/attestation-api v0.0.4-0.20240725185510-66575e8ba086 h1:z1pBfMpJkmfc60K96SgZUN6q6E22x4mBUvISwaRhUu8= +github.com/DIMO-Network/attestation-api v0.0.4-0.20240725185510-66575e8ba086/go.mod h1:PXaKJ19K07siEkisPRrgNL3wL27RElEBKQszlqdwOZ8= github.com/DIMO-Network/clickhouse-infra v0.0.1 h1:4Mp9ayfOQyPWquXYBc2ElOtDxWjms0+VUals2XW43Lc= github.com/DIMO-Network/clickhouse-infra v0.0.1/go.mod h1:SNz+mqccq4AYWFpJ65v3NRXDBY9/f7PKJSHCPtbk9+E= github.com/DIMO-Network/model-garage v0.2.11 h1:3ofR3McKnngQb6ehApA9u80MfCyhugMiM7zBs+svlVM= diff --git a/internal/graph/generated.go b/internal/graph/generated.go index 92a2ae0..e9429b8 100644 --- a/internal/graph/generated.go +++ b/internal/graph/generated.go @@ -146,10 +146,15 @@ type ComplexityRoot struct { } VINVC struct { - ExpirationDate func(childComplexity int) int - IssuanceDate func(childComplexity int) int - RawVc func(childComplexity int) int - Vin func(childComplexity int) int + CountryCode func(childComplexity int) int + RawVc func(childComplexity int) int + RecordedAt func(childComplexity int) int + RecordedBy func(childComplexity int) int + ValidFrom func(childComplexity int) int + ValidTo func(childComplexity int) int + VehicleContractAddress func(childComplexity int) int + VehicleTokenID func(childComplexity int) int + Vin func(childComplexity int) int } } @@ -958,26 +963,61 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.SignalString.Value(childComplexity), true - case "VINVC.expirationDate": - if e.complexity.VINVC.ExpirationDate == nil { + case "VINVC.countryCode": + if e.complexity.VINVC.CountryCode == nil { break } - return e.complexity.VINVC.ExpirationDate(childComplexity), true + return e.complexity.VINVC.CountryCode(childComplexity), true - case "VINVC.issuanceDate": - if e.complexity.VINVC.IssuanceDate == nil { + case "VINVC.rawVC": + if e.complexity.VINVC.RawVc == nil { break } - return e.complexity.VINVC.IssuanceDate(childComplexity), true + return e.complexity.VINVC.RawVc(childComplexity), true - case "VINVC.rawVC": - if e.complexity.VINVC.RawVc == nil { + case "VINVC.recordedAt": + if e.complexity.VINVC.RecordedAt == nil { break } - return e.complexity.VINVC.RawVc(childComplexity), true + return e.complexity.VINVC.RecordedAt(childComplexity), true + + case "VINVC.recordedBy": + if e.complexity.VINVC.RecordedBy == nil { + break + } + + return e.complexity.VINVC.RecordedBy(childComplexity), true + + case "VINVC.validFrom": + if e.complexity.VINVC.ValidFrom == nil { + break + } + + return e.complexity.VINVC.ValidFrom(childComplexity), true + + case "VINVC.validTo": + if e.complexity.VINVC.ValidTo == nil { + break + } + + return e.complexity.VINVC.ValidTo(childComplexity), true + + case "VINVC.vehicleContractAddress": + if e.complexity.VINVC.VehicleContractAddress == nil { + break + } + + return e.complexity.VINVC.VehicleContractAddress(childComplexity), true + + case "VINVC.vehicleTokenId": + if e.complexity.VINVC.VehicleTokenID == nil { + break + } + + return e.complexity.VINVC.VehicleTokenID(childComplexity), true case "VINVC.vin": if e.complexity.VINVC.Vin == nil { @@ -1726,9 +1766,49 @@ extend type SignalCollection { } type VINVC { - issuanceDate: Time - expirationDate: Time + """ + vehicleTokenId is the token ID of the vehicle. + """ + vehicleTokenId: Int + + """ + vin is the vehicle identification number. + """ vin: String + + """ + recordedBy is the entity that recorded the VIN. + """ + recordedBy: String + + """ + The time the VIN was recorded. + """ + recordedAt: Time + + """ + countryCode is the country code that the VIN belongs to. + """ + countryCode: String + + """ + vehicleContractAddress is the address of the vehicle contract. + """ + vehicleContractAddress: String + + """ + validFrom is the time the VC is valid from. + """ + validFrom: Time + + """ + validTo is the time the VC is valid to. + """ + validTo: Time + + """ + rawVC is the raw VC JSON. + """ rawVC: String! } `, BuiltIn: false}, @@ -2780,12 +2860,22 @@ func (ec *executionContext) fieldContext_Query_vinVCLatest(ctx context.Context, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "issuanceDate": - return ec.fieldContext_VINVC_issuanceDate(ctx, field) - case "expirationDate": - return ec.fieldContext_VINVC_expirationDate(ctx, field) + case "vehicleTokenId": + return ec.fieldContext_VINVC_vehicleTokenId(ctx, field) case "vin": return ec.fieldContext_VINVC_vin(ctx, field) + case "recordedBy": + return ec.fieldContext_VINVC_recordedBy(ctx, field) + case "recordedAt": + return ec.fieldContext_VINVC_recordedAt(ctx, field) + case "countryCode": + return ec.fieldContext_VINVC_countryCode(ctx, field) + case "vehicleContractAddress": + return ec.fieldContext_VINVC_vehicleContractAddress(ctx, field) + case "validFrom": + return ec.fieldContext_VINVC_validFrom(ctx, field) + case "validTo": + return ec.fieldContext_VINVC_validTo(ctx, field) case "rawVC": return ec.fieldContext_VINVC_rawVC(ctx, field) } @@ -8971,8 +9061,8 @@ func (ec *executionContext) fieldContext_SignalString_value(_ context.Context, f return fc, nil } -func (ec *executionContext) _VINVC_issuanceDate(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_VINVC_issuanceDate(ctx, field) +func (ec *executionContext) _VINVC_vehicleTokenId(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_VINVC_vehicleTokenId(ctx, field) if err != nil { return graphql.Null } @@ -8985,7 +9075,7 @@ func (ec *executionContext) _VINVC_issuanceDate(ctx context.Context, field graph }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.IssuanceDate, nil + return obj.VehicleTokenID, nil }) if err != nil { ec.Error(ctx, err) @@ -8994,26 +9084,26 @@ func (ec *executionContext) _VINVC_issuanceDate(ctx context.Context, field graph if resTmp == nil { return graphql.Null } - res := resTmp.(*time.Time) + res := resTmp.(*int) fc.Result = res - return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_VINVC_issuanceDate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_VINVC_vehicleTokenId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "VINVC", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Time does not have child fields") + return nil, errors.New("field of type Int does not have child fields") }, } return fc, nil } -func (ec *executionContext) _VINVC_expirationDate(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_VINVC_expirationDate(ctx, field) +func (ec *executionContext) _VINVC_vin(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_VINVC_vin(ctx, field) if err != nil { return graphql.Null } @@ -9026,7 +9116,89 @@ func (ec *executionContext) _VINVC_expirationDate(ctx context.Context, field gra }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.ExpirationDate, nil + return obj.Vin, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_VINVC_vin(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "VINVC", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _VINVC_recordedBy(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_VINVC_recordedBy(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.RecordedBy, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_VINVC_recordedBy(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "VINVC", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _VINVC_recordedAt(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_VINVC_recordedAt(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.RecordedAt, nil }) if err != nil { ec.Error(ctx, err) @@ -9040,7 +9212,7 @@ func (ec *executionContext) _VINVC_expirationDate(ctx context.Context, field gra return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_VINVC_expirationDate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_VINVC_recordedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "VINVC", Field: field, @@ -9053,8 +9225,8 @@ func (ec *executionContext) fieldContext_VINVC_expirationDate(_ context.Context, return fc, nil } -func (ec *executionContext) _VINVC_vin(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_VINVC_vin(ctx, field) +func (ec *executionContext) _VINVC_countryCode(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_VINVC_countryCode(ctx, field) if err != nil { return graphql.Null } @@ -9067,7 +9239,7 @@ func (ec *executionContext) _VINVC_vin(ctx context.Context, field graphql.Collec }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Vin, nil + return obj.CountryCode, nil }) if err != nil { ec.Error(ctx, err) @@ -9081,7 +9253,7 @@ func (ec *executionContext) _VINVC_vin(ctx context.Context, field graphql.Collec return ec.marshalOString2ᚖstring(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_VINVC_vin(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_VINVC_countryCode(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "VINVC", Field: field, @@ -9094,6 +9266,129 @@ func (ec *executionContext) fieldContext_VINVC_vin(_ context.Context, field grap return fc, nil } +func (ec *executionContext) _VINVC_vehicleContractAddress(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_VINVC_vehicleContractAddress(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.VehicleContractAddress, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_VINVC_vehicleContractAddress(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "VINVC", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _VINVC_validFrom(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_VINVC_validFrom(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ValidFrom, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*time.Time) + fc.Result = res + return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_VINVC_validFrom(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "VINVC", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Time does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _VINVC_validTo(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_VINVC_validTo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ValidTo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*time.Time) + fc.Result = res + return ec.marshalOTime2ᚖtimeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_VINVC_validTo(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "VINVC", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Time does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _VINVC_rawVC(ctx context.Context, field graphql.CollectedField, obj *model.Vinvc) (ret graphql.Marshaler) { fc, err := ec.fieldContext_VINVC_rawVC(ctx, field) if err != nil { @@ -12452,12 +12747,22 @@ func (ec *executionContext) _VINVC(ctx context.Context, sel ast.SelectionSet, ob switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("VINVC") - case "issuanceDate": - out.Values[i] = ec._VINVC_issuanceDate(ctx, field, obj) - case "expirationDate": - out.Values[i] = ec._VINVC_expirationDate(ctx, field, obj) + case "vehicleTokenId": + out.Values[i] = ec._VINVC_vehicleTokenId(ctx, field, obj) case "vin": out.Values[i] = ec._VINVC_vin(ctx, field, obj) + case "recordedBy": + out.Values[i] = ec._VINVC_recordedBy(ctx, field, obj) + case "recordedAt": + out.Values[i] = ec._VINVC_recordedAt(ctx, field, obj) + case "countryCode": + out.Values[i] = ec._VINVC_countryCode(ctx, field, obj) + case "vehicleContractAddress": + out.Values[i] = ec._VINVC_vehicleContractAddress(ctx, field, obj) + case "validFrom": + out.Values[i] = ec._VINVC_validFrom(ctx, field, obj) + case "validTo": + out.Values[i] = ec._VINVC_validTo(ctx, field, obj) case "rawVC": out.Values[i] = ec._VINVC_rawVC(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -13283,6 +13588,22 @@ func (ec *executionContext) marshalOFloat2ᚖfloat64(ctx context.Context, sel as return graphql.WrapContextMarshaler(ctx, res) } +func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v interface{}) (*int, error) { + if v == nil { + return nil, nil + } + res, err := graphql.UnmarshalInt(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.SelectionSet, v *int) graphql.Marshaler { + if v == nil { + return graphql.Null + } + res := graphql.MarshalInt(*v) + return res +} + func (ec *executionContext) marshalOSignalAggregations2ᚕᚖgithubᚗcomᚋDIMOᚑNetworkᚋtelemetryᚑapiᚋinternalᚋgraphᚋmodelᚐSignalAggregationsᚄ(ctx context.Context, sel ast.SelectionSet, v []*model.SignalAggregations) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 3e2dac6..1b0c25f 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -145,10 +145,24 @@ type SignalString struct { } type Vinvc struct { - IssuanceDate *time.Time `json:"issuanceDate,omitempty"` - ExpirationDate *time.Time `json:"expirationDate,omitempty"` - Vin *string `json:"vin,omitempty"` - RawVc string `json:"rawVC"` + // vehicleTokenId is the token ID of the vehicle. + VehicleTokenID *int `json:"vehicleTokenId,omitempty"` + // vin is the vehicle identification number. + Vin *string `json:"vin,omitempty"` + // recordedBy is the entity that recorded the VIN. + RecordedBy *string `json:"recordedBy,omitempty"` + // The time the VIN was recorded. + RecordedAt *time.Time `json:"recordedAt,omitempty"` + // countryCode is the country code that the VIN belongs to. + CountryCode *string `json:"countryCode,omitempty"` + // vehicleContractAddress is the address of the vehicle contract. + VehicleContractAddress *string `json:"vehicleContractAddress,omitempty"` + // validFrom is the time the VC is valid from. + ValidFrom *time.Time `json:"validFrom,omitempty"` + // validTo is the time the VC is valid to. + ValidTo *time.Time `json:"validTo,omitempty"` + // rawVC is the raw VC JSON. + RawVc string `json:"rawVC"` } type FloatAggregation string diff --git a/internal/repositories/vinvc/mock_clickhouse_test.go b/internal/repositories/vinvc/mock_clickhouse_test.go new file mode 100644 index 0000000..7995a4b --- /dev/null +++ b/internal/repositories/vinvc/mock_clickhouse_test.go @@ -0,0 +1,229 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ClickHouse/clickhouse-go/v2 (interfaces: Conn) +// +// Generated by this command: +// +// mockgen -destination=mock_clickhouse_test.go -package=vinvc_test github.com/ClickHouse/clickhouse-go/v2 Conn +// + +// Package vinvc_test is a generated GoMock package. +package vinvc_test + +import ( + context "context" + reflect "reflect" + + driver "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + proto "github.com/ClickHouse/clickhouse-go/v2/lib/proto" + gomock "go.uber.org/mock/gomock" +) + +// MockConn is a mock of Conn interface. +type MockConn struct { + ctrl *gomock.Controller + recorder *MockConnMockRecorder +} + +// MockConnMockRecorder is the mock recorder for MockConn. +type MockConnMockRecorder struct { + mock *MockConn +} + +// NewMockConn creates a new mock instance. +func NewMockConn(ctrl *gomock.Controller) *MockConn { + mock := &MockConn{ctrl: ctrl} + mock.recorder = &MockConnMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConn) EXPECT() *MockConnMockRecorder { + return m.recorder +} + +// AsyncInsert mocks base method. +func (m *MockConn) AsyncInsert(arg0 context.Context, arg1 string, arg2 bool, arg3 ...any) error { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AsyncInsert", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// AsyncInsert indicates an expected call of AsyncInsert. +func (mr *MockConnMockRecorder) AsyncInsert(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncInsert", reflect.TypeOf((*MockConn)(nil).AsyncInsert), varargs...) +} + +// Close mocks base method. +func (m *MockConn) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockConnMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConn)(nil).Close)) +} + +// Contributors mocks base method. +func (m *MockConn) Contributors() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Contributors") + ret0, _ := ret[0].([]string) + return ret0 +} + +// Contributors indicates an expected call of Contributors. +func (mr *MockConnMockRecorder) Contributors() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Contributors", reflect.TypeOf((*MockConn)(nil).Contributors)) +} + +// Exec mocks base method. +func (m *MockConn) Exec(arg0 context.Context, arg1 string, arg2 ...any) error { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Exec", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Exec indicates an expected call of Exec. +func (mr *MockConnMockRecorder) Exec(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockConn)(nil).Exec), varargs...) +} + +// Ping mocks base method. +func (m *MockConn) Ping(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Ping", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Ping indicates an expected call of Ping. +func (mr *MockConnMockRecorder) Ping(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockConn)(nil).Ping), arg0) +} + +// PrepareBatch mocks base method. +func (m *MockConn) PrepareBatch(arg0 context.Context, arg1 string, arg2 ...driver.PrepareBatchOption) (driver.Batch, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PrepareBatch", varargs...) + ret0, _ := ret[0].(driver.Batch) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PrepareBatch indicates an expected call of PrepareBatch. +func (mr *MockConnMockRecorder) PrepareBatch(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareBatch", reflect.TypeOf((*MockConn)(nil).PrepareBatch), varargs...) +} + +// Query mocks base method. +func (m *MockConn) Query(arg0 context.Context, arg1 string, arg2 ...any) (driver.Rows, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Query", varargs...) + ret0, _ := ret[0].(driver.Rows) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Query indicates an expected call of Query. +func (mr *MockConnMockRecorder) Query(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockConn)(nil).Query), varargs...) +} + +// QueryRow mocks base method. +func (m *MockConn) QueryRow(arg0 context.Context, arg1 string, arg2 ...any) driver.Row { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "QueryRow", varargs...) + ret0, _ := ret[0].(driver.Row) + return ret0 +} + +// QueryRow indicates an expected call of QueryRow. +func (mr *MockConnMockRecorder) QueryRow(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRow", reflect.TypeOf((*MockConn)(nil).QueryRow), varargs...) +} + +// Select mocks base method. +func (m *MockConn) Select(arg0 context.Context, arg1 any, arg2 string, arg3 ...any) error { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Select", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// Select indicates an expected call of Select. +func (mr *MockConnMockRecorder) Select(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockConn)(nil).Select), varargs...) +} + +// ServerVersion mocks base method. +func (m *MockConn) ServerVersion() (*proto.ServerHandshake, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ServerVersion") + ret0, _ := ret[0].(*proto.ServerHandshake) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ServerVersion indicates an expected call of ServerVersion. +func (mr *MockConnMockRecorder) ServerVersion() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerVersion", reflect.TypeOf((*MockConn)(nil).ServerVersion)) +} + +// Stats mocks base method. +func (m *MockConn) Stats() driver.Stats { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stats") + ret0, _ := ret[0].(driver.Stats) + return ret0 +} + +// Stats indicates an expected call of Stats. +func (mr *MockConnMockRecorder) Stats() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stats", reflect.TypeOf((*MockConn)(nil).Stats)) +} diff --git a/internal/repositories/vinvc/mock_service_test.go b/internal/repositories/vinvc/mock_service_test.go new file mode 100644 index 0000000..68fb4cc --- /dev/null +++ b/internal/repositories/vinvc/mock_service_test.go @@ -0,0 +1,81 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/DIMO-Network/nameindexer/pkg/clickhouse/service (interfaces: ObjectGetter) +// +// Generated by this command: +// +// mockgen -destination=mock_service_test.go -package=vinvc_test github.com/DIMO-Network/nameindexer/pkg/clickhouse/service ObjectGetter +// + +// Package vinvc_test is a generated GoMock package. +package vinvc_test + +import ( + context "context" + reflect "reflect" + + s3 "github.com/aws/aws-sdk-go-v2/service/s3" + gomock "go.uber.org/mock/gomock" +) + +// MockObjectGetter is a mock of ObjectGetter interface. +type MockObjectGetter struct { + ctrl *gomock.Controller + recorder *MockObjectGetterMockRecorder +} + +// MockObjectGetterMockRecorder is the mock recorder for MockObjectGetter. +type MockObjectGetterMockRecorder struct { + mock *MockObjectGetter +} + +// NewMockObjectGetter creates a new mock instance. +func NewMockObjectGetter(ctrl *gomock.Controller) *MockObjectGetter { + mock := &MockObjectGetter{ctrl: ctrl} + mock.recorder = &MockObjectGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockObjectGetter) EXPECT() *MockObjectGetterMockRecorder { + return m.recorder +} + +// GetObject mocks base method. +func (m *MockObjectGetter) GetObject(arg0 context.Context, arg1 *s3.GetObjectInput, arg2 ...func(*s3.Options)) (*s3.GetObjectOutput, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetObject", varargs...) + ret0, _ := ret[0].(*s3.GetObjectOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetObject indicates an expected call of GetObject. +func (mr *MockObjectGetterMockRecorder) GetObject(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockObjectGetter)(nil).GetObject), varargs...) +} + +// PutObject mocks base method. +func (m *MockObjectGetter) PutObject(arg0 context.Context, arg1 *s3.PutObjectInput, arg2 ...func(*s3.Options)) (*s3.PutObjectOutput, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PutObject", varargs...) + ret0, _ := ret[0].(*s3.PutObjectOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutObject indicates an expected call of PutObject. +func (mr *MockObjectGetterMockRecorder) PutObject(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockObjectGetter)(nil).PutObject), varargs...) +} diff --git a/internal/repositories/vinvc/vinvc.go b/internal/repositories/vinvc/vinvc.go index 637bd38..c6b62d2 100644 --- a/internal/repositories/vinvc/vinvc.go +++ b/internal/repositories/vinvc/vinvc.go @@ -2,6 +2,7 @@ package vinvc import ( "context" + "database/sql" "encoding/json" "errors" "fmt" @@ -37,6 +38,9 @@ func (s *Repository) GetLatestVC(ctx context.Context, vehicleTokenID uint32) (*m } data, err := s.indexService.GetLatestData(ctx, s.dataType, subject) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } s.logger.Error().Err(err).Msg("failed to get latest data") return nil, errors.New("internal error") } @@ -46,24 +50,48 @@ func (s *Repository) GetLatestVC(ctx context.Context, vehicleTokenID uint32) (*m } var expiresAt *time.Time - if expirationDate, err := time.Parse(time.RFC3339, msg.ExpirationDate); err == nil { + if expirationDate, err := time.Parse(time.RFC3339, msg.ValidTo); err == nil { expiresAt = &expirationDate } var createdAt *time.Time - if issuanceDate, err := time.Parse(time.RFC3339, msg.IssuanceDate); err == nil { + if issuanceDate, err := time.Parse(time.RFC3339, msg.ValidFrom); err == nil { createdAt = &issuanceDate } credSubject := verifiable.VINSubject{} + if err := json.Unmarshal(msg.CredentialSubject, &credSubject); err != nil { + return nil, fmt.Errorf("failed to unmarshal credential subject: %w", err) + } var vin *string - if err := json.Unmarshal(msg.CredentialSubject, &credSubject); err == nil { + if credSubject.VehicleIdentificationNumber != "" { vin = &credSubject.VehicleIdentificationNumber } - - vc := model.Vinvc{ - IssuanceDate: createdAt, - ExpirationDate: expiresAt, - RawVc: string(data), - Vin: vin, + var recordedBy *string + if credSubject.RecordedBy != "" { + recordedBy = &credSubject.RecordedBy + } + var recordedAt *time.Time + if !credSubject.RecordedAt.IsZero() { + recordedAt = &credSubject.RecordedAt } - return &vc, nil + var countryCode *string + if credSubject.CountryCode != "" { + countryCode = &credSubject.CountryCode + } + var vehicleContractAddress *string + if credSubject.VehicleContractAddress != "" { + vehicleContractAddress = &credSubject.VehicleContractAddress + } + tokenIDInt := int(credSubject.VehicleTokenID) + + return &model.Vinvc{ + ValidFrom: createdAt, + ValidTo: expiresAt, + RawVc: string(data), + Vin: vin, + RecordedBy: recordedBy, + RecordedAt: recordedAt, + CountryCode: countryCode, + VehicleContractAddress: vehicleContractAddress, + VehicleTokenID: &tokenIDInt, + }, nil } diff --git a/internal/repositories/vinvc/vinvvc_test.go b/internal/repositories/vinvc/vinvvc_test.go new file mode 100644 index 0000000..c6ea17c --- /dev/null +++ b/internal/repositories/vinvc/vinvvc_test.go @@ -0,0 +1,173 @@ +//go:generate mockgen -destination=mock_service_test.go -package=vinvc_test github.com/DIMO-Network/nameindexer/pkg/clickhouse/service ObjectGetter +//go:generate mockgen -destination=mock_clickhouse_test.go -package=vinvc_test github.com/ClickHouse/clickhouse-go/v2 Conn +package vinvc_test + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "io" + "testing" + "time" + + "github.com/DIMO-Network/attestation-api/pkg/verifiable" + "github.com/DIMO-Network/telemetry-api/internal/graph/model" + "github.com/DIMO-Network/telemetry-api/internal/repositories/vinvc" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +// MockRow implements sql.Row and returns a string when scanned. +type MockRow struct { + data string + err error +} + +func (m *MockRow) Scan(dest ...interface{}) error { + if m.err != nil { + return m.err + } + if len(dest) > 0 { + if s, ok := dest[0].(*string); ok { + *s = m.data + } + } + return nil +} + +func (m *MockRow) Err() error { + return nil +} + +func (m *MockRow) ScanStruct(any) error { + return nil +} + +//go:generate mockgen -destination=mock_service_test.go -package=vinvc_test github.com/DIMO-Network/nameindexer/pkg/clickhouse/service ObjectGetter +//go:generate mockgen -destination=mock_clickhouse_test.go -package=vinvc_test github.com/ClickHouse/clickhouse-go/v2 Conn + +func TestGetLatestVC(t *testing.T) { + + // Initialize variables + logger := zerolog.New(nil) + ctx := context.Background() + vehicleTokenID := uint32(123) + dataType := "vinvc" + bucketName := "bucket-name" + + // Create mock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create mock services + mockChConn := NewMockConn(ctrl) + mockObjGetter := NewMockObjectGetter(ctrl) + + // Initialize the service with mock dependencies + svc := vinvc.New(mockChConn, mockObjGetter, bucketName, dataType, &logger) + + defaultVC := verifiable.Credential{ + ValidTo: time.Now().Add(24 * time.Hour).Format(time.RFC3339), + ValidFrom: time.Now().Add(-24 * time.Hour).Format(time.RFC3339), + CredentialSubject: json.RawMessage(`{ + "vehicleIdentificationNumber": "VIN123", + "recordedBy": "Recorder", + "recordedAt": "2024-01-01T00:00:00Z", + "countryCode": "US", + "vehicleContractAddress": "0xAddress", + "vehicleTokenID": 123 + }`), + } + defaultData, err := json.Marshal(defaultVC) + require.NoError(t, err, "failed to marshal defaultVC") + + // Test cases + tests := []struct { + name string + mockSetup func() + expectedVC *model.Vinvc + expectedErr error + }{ + { + name: "Success", + mockSetup: func() { + // Create a mock verifiable credential + mockChConn.EXPECT().QueryRow(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&MockRow{data: "filename"}) + mockObjGetter.EXPECT().GetObject(gomock.Any(), gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader(defaultData))}, nil) + }, + expectedVC: &model.Vinvc{ + Vin: ref("VIN123"), + RecordedBy: ref("Recorder"), + RecordedAt: ref(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + CountryCode: ref("US"), + VehicleContractAddress: ref("0xAddress"), + VehicleTokenID: ref(123), + RawVc: string(defaultData), + }, + + expectedErr: nil, + }, + { + name: "No data found", + mockSetup: func() { + mockChConn.EXPECT().QueryRow(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&MockRow{err: sql.ErrNoRows}) + }, + expectedVC: nil, + expectedErr: nil, + }, + { + name: "Internal error", + mockSetup: func() { + mockChConn.EXPECT().QueryRow(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&MockRow{err: errors.New("internal error")}) + }, + expectedVC: nil, + expectedErr: errors.New("internal error"), + }, + { + name: "Invalid data format", + mockSetup: func() { + mockChConn.EXPECT().QueryRow(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&MockRow{data: "filename"}) + mockObjGetter.EXPECT().GetObject(gomock.Any(), gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader([]byte("invalid data")))}, nil) + }, + expectedVC: nil, + expectedErr: fmt.Errorf("failed to unmarshal fingerprint message: invalid character 'i' looking for beginning of value"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up the mock expectations + tt.mockSetup() + + // Call the method + vc, err := svc.GetLatestVC(ctx, vehicleTokenID) + + // Assert the results + if tt.expectedErr != nil { + require.EqualError(t, err, tt.expectedErr.Error()) + } else { + require.NoError(t, err) + } + if tt.expectedVC == nil { + require.Nil(t, vc) + return + } + require.EqualValues(t, tt.expectedVC.Vin, vc.Vin) + require.EqualValues(t, tt.expectedVC.RecordedBy, vc.RecordedBy) + require.EqualValues(t, tt.expectedVC.RecordedAt, vc.RecordedAt) + require.EqualValues(t, tt.expectedVC.CountryCode, vc.CountryCode) + require.EqualValues(t, tt.expectedVC.VehicleContractAddress, vc.VehicleContractAddress) + require.EqualValues(t, tt.expectedVC.VehicleTokenID, vc.VehicleTokenID) + require.JSONEq(t, tt.expectedVC.RawVc, vc.RawVc) + }) + } +} + +func ref[T any](v T) *T { + return &v +} diff --git a/schema/vinvc.graphqls b/schema/vinvc.graphqls index 37e9f0e..23f9683 100644 --- a/schema/vinvc.graphqls +++ b/schema/vinvc.graphqls @@ -15,8 +15,48 @@ extend type Query { } type VINVC { - issuanceDate: Time - expirationDate: Time + """ + vehicleTokenId is the token ID of the vehicle. + """ + vehicleTokenId: Int + + """ + vin is the vehicle identification number. + """ vin: String + + """ + recordedBy is the entity that recorded the VIN. + """ + recordedBy: String + + """ + The time the VIN was recorded. + """ + recordedAt: Time + + """ + countryCode is the country code that the VIN belongs to. + """ + countryCode: String + + """ + vehicleContractAddress is the address of the vehicle contract. + """ + vehicleContractAddress: String + + """ + validFrom is the time the VC is valid from. + """ + validFrom: Time + + """ + validTo is the time the VC is valid to. + """ + validTo: Time + + """ + rawVC is the raw VC JSON. + """ rawVC: String! }