Skip to content

Commit

Permalink
test(cosmos): Test gRPC endpoint "/agoric.vstorage.Query/CapData"
Browse files Browse the repository at this point in the history
  • Loading branch information
gibson042 committed Jul 24, 2023
1 parent 36b921d commit a7f17fe
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 6 deletions.
12 changes: 6 additions & 6 deletions golang/cosmos/x/vstorage/capdata/capdata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ func ptr[T any](v T) *T {
return &v
}

func mustMarshal(val any) string {
func mustJsonMarshal(val any) string {
jsonText, err := json.Marshal(val)
if err != nil {
panic(err)
}
return string(jsonText)
}

func mustUnmarshalString(jsonText string, ptr any) {
func mustJsonUnmarshal(jsonText string, ptr any) {
if err := json.Unmarshal([]byte(jsonText), ptr); err != nil {
panic(err)
}
Expand Down Expand Up @@ -283,9 +283,9 @@ func Test_DecodeSerializedCapdata(t *testing.T) {
}
var expected interface{}
if desc.expected != "" {
mustUnmarshalString(desc.expected, &expected)
mustJsonUnmarshal(desc.expected, &expected)
} else {
mustUnmarshalString(desc.body, &expected)
mustJsonUnmarshal(desc.body, &expected)
}
for _, format := range []string{"smallcaps", "legacy"} {
if desc.format != "" && desc.format != format {
Expand All @@ -299,10 +299,10 @@ func Test_DecodeSerializedCapdata(t *testing.T) {
if format == "smallcaps" {
capdata.Body = "#" + capdata.Body
}
intermediate, err := DecodeSerializedCapdata(mustMarshal(capdata), desc.transformations)
intermediate, err := DecodeSerializedCapdata(mustJsonMarshal(capdata), desc.transformations)
// Replace each Remotable with its representation before comparing.
var got interface{}
mustUnmarshalString(mustMarshal(intermediate), &got)
mustJsonUnmarshal(mustJsonMarshal(intermediate), &got)
if desc.errContains == nil {
if err != nil {
t.Errorf("%s: got unexpected error %v", label, err)
Expand Down
300 changes: 300 additions & 0 deletions golang/cosmos/x/vstorage/keeper/keeper_grpc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
package keeper

import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"

grpcCodes "google.golang.org/grpc/codes"
grpcStatus "google.golang.org/grpc/status"

//"github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage/capdata"
"github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage/types"

sdk "github.com/cosmos/cosmos-sdk/types"
)

func ptr[T any](v T) *T {
return &v
}

func mustJsonMarshal(val any) string {
jsonText, err := json.Marshal(val)
if err != nil {
panic(err)
}
return string(jsonText)
}

func mustMarshalStreamCell(blockHeight string, values []string) string {
cell := map[string]any{
"blockHeight": blockHeight,
"values": values,
}
return mustJsonMarshal(cell)
}

func TestCapData(t *testing.T) {
testKit := makeTestKit()
ctx, keeper := testKit.ctx, testKit.vstorageKeeper
querier := Querier{keeper}

type testCase struct {
label string
data *string
request types.QueryCapDataRequest
expected types.QueryCapDataResponse
errCode grpcCodes.Code
errContains *string
}
testCases := []testCase{}

// Test simple cases (various kinds of bad data up to simple flat JSON-compatible CapData).
decodableSmallcaps := `{"body":"#true","slots":[]}`
decodableLegacy := `{"body":"true","slots":[]}`
testCases = append(testCases, []testCase{
{label: "no data",
data: nil,
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
errCode: grpcCodes.FailedPrecondition,
errContains: ptr("no data"),
},
{label: "zero-length data",
data: ptr(""),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
errCode: grpcCodes.FailedPrecondition,
errContains: ptr("JSON"),
},
{label: "cell with empty string",
data: ptr(mustMarshalStreamCell("1", []string{decodableSmallcaps, "", decodableLegacy})),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
errCode: grpcCodes.FailedPrecondition,
errContains: ptr("JSON"),
},
{label: "non-JSON data",
data: ptr("foo"),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
errCode: grpcCodes.FailedPrecondition,
errContains: ptr("invalid"),
},
{label: "cell with non-JSON",
data: ptr(mustMarshalStreamCell("1", []string{decodableSmallcaps, "foo", decodableLegacy})),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
errCode: grpcCodes.FailedPrecondition,
errContains: ptr("invalid"),
},
{label: "lone non-CapData",
data: ptr("{}"),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
errCode: grpcCodes.FailedPrecondition,
errContains: ptr("invalid CapData"),
},
{label: "cell with non-CapData",
data: ptr(mustMarshalStreamCell("1", []string{decodableSmallcaps, "{}", decodableLegacy})),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
errCode: grpcCodes.FailedPrecondition,
errContains: ptr("invalid CapData"),
},
{label: "lone smallcaps CapData",
data: ptr(decodableSmallcaps),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
expected: types.QueryCapDataResponse{Value: `true`},
},
{label: "cell with smallcaps CapData",
data: ptr(mustMarshalStreamCell("1", []string{decodableSmallcaps})),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
expected: types.QueryCapDataResponse{BlockHeight: "1", Value: `true`},
},
{label: "lone legacy CapData",
data: ptr(decodableLegacy),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
expected: types.QueryCapDataResponse{Value: `true`},
},
{label: "cell with legacy CapData",
data: ptr(mustMarshalStreamCell("1", []string{decodableLegacy})),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
expected: types.QueryCapDataResponse{BlockHeight: "1", Value: `true`},
},
}...)

// Test option validation.
testCases = append(testCases, []testCase{
{label: "explicit JSON Lines",
data: ptr(decodableSmallcaps),
request: types.QueryCapDataRequest{MediaType: "JSON Lines", RemotableValueFormat: "string"},
expected: types.QueryCapDataResponse{Value: `true`},
},
{label: "invalid media type",
data: ptr(decodableSmallcaps),
request: types.QueryCapDataRequest{MediaType: "JSONLines", RemotableValueFormat: "string"},
errCode: grpcCodes.InvalidArgument,
errContains: ptr("media_type"),
},
{label: "invalid item format",
data: ptr(decodableSmallcaps),
request: types.QueryCapDataRequest{ItemFormat: "deep", RemotableValueFormat: "string"},
errCode: grpcCodes.InvalidArgument,
errContains: ptr("item_format"),
},
{label: "missing remotable value format",
data: ptr(decodableSmallcaps),
request: types.QueryCapDataRequest{},
errCode: grpcCodes.InvalidArgument,
errContains: ptr("remotable_value_format"),
},
{label: "invalid remotable value format",
data: ptr(decodableSmallcaps),
request: types.QueryCapDataRequest{RemotableValueFormat: "foo"},
errCode: grpcCodes.InvalidArgument,
errContains: ptr("remotable_value_format"),
},
}...)

// Test formatting options against sufficiently complex CapData,
// deriving legacy encoding from smallcaps encoding to ensure equivalence
// and deriving expectations from marshalling to avoid spurious mismatches
// from Go's unpredictable field ordering (e.g., `{"a":0,"b":1}` vs. `{"b":1,"a":0}`).
slots := []any{"a"}
deepSmallcapsBody := `{"arr":[{"bigint":"+42","remotable":"$0.Foo","ref2":"$0"}]}`
deepLegacyBody := deepSmallcapsBody
legacyFromSmallcaps := [][2]string{
[2]string{`"+42"`, `{"@qclass":"bigint","digits":"42"}`},
[2]string{`"$0.Foo"`, `{"@qclass":"slot","index":0,"iface":"Foo"}`},
[2]string{`"$0"`, `{"@qclass":"slot","index":0}`},
}
for _, pair := range legacyFromSmallcaps {
deepLegacyBody = strings.Replace(deepLegacyBody, pair[0], pair[1], -1)
}
cell := mustMarshalStreamCell("1", []string{
mustJsonMarshal(map[string]any{"body": "#" + deepSmallcapsBody, "slots": slots}),
mustJsonMarshal(map[string]any{"body": deepLegacyBody, "slots": slots}),
})
mustMarshalTwoLines := func(val any) string {
line := mustJsonMarshal(val)
return fmt.Sprintf("%s\n%s", line, line)
}
testCases = append(testCases, testCase{label: "remotables as strings",
data: ptr(cell),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
expected: types.QueryCapDataResponse{
BlockHeight: "1",
Value: mustMarshalTwoLines(map[string]any{
"arr": []any{
map[string]any{
"bigint": "42",
"remotable": "[Foo {}]",
"ref2": "[Foo {}]",
},
},
}),
},
})
testCases = append(testCases, testCase{label: "remotables as strings, flat",
data: ptr(cell),
request: types.QueryCapDataRequest{ItemFormat: "flat", RemotableValueFormat: "string"},
expected: types.QueryCapDataResponse{
BlockHeight: "1",
Value: mustMarshalTwoLines(map[string]any{
"arr-0-bigint": "42",
"arr-0-remotable": "[Foo {}]",
"arr-0-ref2": "[Foo {}]",
}),
},
})
testCases = append(testCases, testCase{label: "remotables as objects",
data: ptr(cell),
request: types.QueryCapDataRequest{RemotableValueFormat: "object"},
expected: types.QueryCapDataResponse{
BlockHeight: "1",
Value: mustMarshalTwoLines(map[string]any{
"arr": []any{
map[string]any{
"bigint": "42",
"remotable": map[string]any{"id": "a", "allegedName": "Foo"},
"ref2": map[string]any{"id": "a", "allegedName": "Foo"},
},
},
}),
},
})
testCases = append(testCases, testCase{label: "remotables as objects, flat",
data: ptr(cell),
request: types.QueryCapDataRequest{ItemFormat: "flat", RemotableValueFormat: "object"},
expected: types.QueryCapDataResponse{
BlockHeight: "1",
Value: mustMarshalTwoLines(map[string]any{
"arr-0-bigint": "42",
"arr-0-remotable-id": "a",
"arr-0-remotable-allegedName": "Foo",
"arr-0-ref2-id": "a",
"arr-0-ref2-allegedName": "Foo",
}),
},
})

// Test errors from CapData that includes unsupported values.
expectNotImplemented := func(label, capdataBody string, slots []any) testCase {
if slots == nil {
slots = []any{}
}
serialized := mustJsonMarshal(map[string]any{
"body": capdataBody,
"slots": slots,
})
return testCase{
label: label,
data: ptr(serialized),
request: types.QueryCapDataRequest{RemotableValueFormat: "string"},
errCode: grpcCodes.FailedPrecondition,
errContains: ptr("not implemented"),
}
}
testCases = append(testCases, []testCase{
expectNotImplemented("smallcaps undefined", `#"#undefined"`, nil),
expectNotImplemented("smallcaps NaN", `#"#NaN"`, nil),
expectNotImplemented("smallcaps infinity", `#"#Infinity"`, nil),
expectNotImplemented("smallcaps negative infinity", `#"#-Infinity"`, nil),
expectNotImplemented("smallcaps symbol", `#"%foo"`, nil),
expectNotImplemented("smallcaps promise", `#"&0"`, []any{"a"}),
expectNotImplemented("smallcaps tagged", `#{"#tag":"copySet","payload":[]}`, nil),
expectNotImplemented("smallcaps error", `#{"#error":"foo","name":"Error"}`, nil),
expectNotImplemented("legacy undefined", `{"@qclass":"undefined"}`, nil),
expectNotImplemented("legacy NaN", `{"@qclass":"NaN"}`, nil),
expectNotImplemented("legacy infinity", `{"@qclass":"Infinity"}`, nil),
expectNotImplemented("legacy negative infinity", `{"@qclass":"-Infinity"}`, nil),
expectNotImplemented("legacy symbol", `{"@qclass":"symbol","name":"foo"}`, nil),
expectNotImplemented("smallcaps tagged", `{"@qclass":"tagged","tag":"copySet","payload":[]}`, nil),
expectNotImplemented("smallcaps error", `{"@qclass":"error","message":"foo","name":"Error"}`, nil),
expectNotImplemented("smallcaps Hilbert Hotel", `{"@qclass":"hilbert","original":"foo"}`, nil),
}...)
for _, desc := range testCases {
desc.request.Path = "key"
if desc.data == nil {
keeper.SetStorage(ctx, types.NewStorageEntryWithNoData(desc.request.Path))
} else {
keeper.SetStorage(ctx, types.NewStorageEntry(desc.request.Path, *desc.data))
}
resp, err := querier.CapData(sdk.WrapSDKContext(ctx), &desc.request)
if desc.errCode == grpcCodes.OK {
if err != nil {
t.Errorf("%s: got unexpected error %v", desc.label, err)
} else if reflect.DeepEqual(resp, &desc.expected) {
continue
}
if resp.Value != desc.expected.Value {
lines := strings.Split(resp.Value, "\n")
t.Errorf("%s: wrong result value lines: %#q", desc.label, lines)
} else {
t.Errorf("%s: wrong result: %#v", desc.label, resp)
}
} else if err == nil {
t.Errorf("%s: got no error, want error %q", desc.label, *desc.errContains)
} else if code := grpcStatus.Code(err); code != desc.errCode {
t.Errorf("%s: got error code %q, want %q", desc.label, code, desc.errCode)
} else if desc.errContains != nil && !strings.Contains(err.Error(), *desc.errContains) {
t.Errorf("%s: got error %v, want error %q", desc.label, err, *desc.errContains)
}
}
}

0 comments on commit a7f17fe

Please sign in to comment.