Skip to content

Commit d827fe0

Browse files
committed
test(api): parameterised API & RPC tests for v2
1 parent 0e3a578 commit d827fe0

File tree

4 files changed

+257
-68
lines changed

4 files changed

+257
-68
lines changed

chain/types/tipset_key.go

-18
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ import (
1717

1818
var EmptyTSK = TipSetKey{}
1919

20-
// FinalizedTSK is a sentinel value, similar to EmptyTSK, but used to represent
21-
// the "finalized" tipset key as per F3 or EC.
22-
// The CID bafkqaclgnfxgc3djpjswi is a valid raw codec CID with an identity
23-
// multihash of "finalized".
24-
var FinalizedTSK = NewTipSetKey(cid.MustParse("bafkqaclgnfxgc3djpjswi"))
25-
2620
// The length of a block header CID in bytes.
2721
var blockHeaderCIDLen int
2822

@@ -98,18 +92,6 @@ func (k TipSetKey) MarshalJSON() ([]byte, error) {
9892
}
9993

10094
func (k *TipSetKey) UnmarshalJSON(b []byte) error {
101-
if len(b) > 0 && b[0] == '"' {
102-
switch string(b) {
103-
case `"finalized"`:
104-
*k = FinalizedTSK
105-
return nil
106-
case `"latest"`:
107-
*k = EmptyTSK
108-
return nil
109-
default:
110-
return fmt.Errorf(`json: invalid tipset key ("%s")`, b)
111-
}
112-
}
11395
var cids []cid.Cid
11496
if err := json.Unmarshal(b, &cids); err != nil {
11597
return err

itests/api_test.go

+237-45
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ package itests
44
import (
55
"context"
66
"errors"
7+
"fmt"
8+
"reflect"
79
"strings"
810
"testing"
911
"time"
1012

1113
logging "github.com/ipfs/go-log/v2"
14+
"github.com/ipld/go-ipld-prime"
15+
"github.com/ipld/go-ipld-prime/codec/dagjson"
16+
"github.com/ipld/go-ipld-prime/traversal"
1217
"github.com/libp2p/go-libp2p/core/peer"
1318
"github.com/stretchr/testify/require"
1419

1520
"github.com/filecoin-project/go-address"
1621
"github.com/filecoin-project/go-jsonrpc"
22+
"github.com/filecoin-project/go-state-types/abi"
1723
"github.com/filecoin-project/go-state-types/big"
1824
"github.com/filecoin-project/go-state-types/exitcode"
1925

@@ -23,6 +29,7 @@ import (
2329
"github.com/filecoin-project/lotus/chain/actors/policy"
2430
"github.com/filecoin-project/lotus/chain/types"
2531
"github.com/filecoin-project/lotus/itests/kit"
32+
"github.com/filecoin-project/lotus/lib/must"
2633
)
2734

2835
func TestAPI(t *testing.T) {
@@ -322,54 +329,239 @@ func (ts *apiSuite) testNonGenesisMiner(t *testing.T) {
322329

323330
func TestAPIV2(t *testing.T) {
324331
req := require.New(t)
332+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
333+
defer cancel()
325334

326335
kit.QuietMiningLogs()
327-
full, _, ens := kit.EnsembleMinimal(t)
328-
ens.BeginMining(20 * time.Millisecond)
336+
full, _, ens := kit.EnsembleMinimal(t, kit.ThroughRPC())
337+
ens.BeginMining(10 * time.Millisecond)
329338

330339
full.WaitTillChain(context.Background(), kit.HeightAtLeast(policy.ChainFinality+20))
331340

332-
ts, err := full.ChainGetTipSetByHeight(context.Background(), 15, types.EmptyTSK)
333-
req.NoError(err)
334-
req.NotNil(ts)
335-
t.Logf("/v1/ChainGetTipSetByHeight(15, []): %d", ts.Height())
336-
337-
ts, err = full.V2.ChainGetTipSetByHeight(context.Background(), 15, types.NewTipSetSelector(types.EmptyTSK))
338-
req.NoError(err)
339-
req.NotNil(ts)
340-
t.Logf(`/v2/ChainGetTipSetByHeight(15, []): %d`, ts.Height())
341-
342-
ts, err = full.V2.ChainGetTipSetByHeight(context.Background(), 15, types.TipSetSelectorLatest)
343-
req.NoError(err)
344-
req.NotNil(ts)
345-
t.Logf(`/v2/ChainGetTipSetByHeight(15, "latest"): %d`, ts.Height())
346-
347-
ts, err = full.V2.ChainGetTipSetByHeight(context.Background(), 15, types.TipSetSelectorFinalized)
348-
req.NoError(err)
349-
req.NotNil(ts)
350-
t.Logf(`/v2/ChainGetTipSetByHeight(15, "finalized"): %d`, ts.Height())
351-
352-
_, err = full.V2.ChainGetTipSetByHeight(context.Background(), 200, types.TipSetSelectorFinalized)
353-
req.Error(err)
354-
t.Logf(`/v2/ChainGetTipSetByHeight(15, "finalized"): %s`, err.Error())
355-
356-
ts, err = full.ChainHead(context.Background())
357-
req.NoError(err)
358-
req.NotNil(ts)
359-
t.Logf("/v1/ChainHead(): %d", ts.Height())
360-
361-
ts, err = full.V2.ChainHead(context.Background(), nil)
362-
req.NoError(err)
363-
req.NotNil(ts)
364-
t.Logf("/v2/ChainHead(null): %d", ts.Height())
365-
366-
ts, err = full.V2.ChainHead(context.Background(), jsonrpc.RawParams(`["latest"]`))
367-
req.NoError(err)
368-
req.NotNil(ts)
369-
t.Logf(`/v2/ChainHead("latest"): %d`, ts.Height())
370-
371-
ts, err = full.V2.ChainHead(context.Background(), jsonrpc.RawParams(`["finalized"]`))
372-
req.NoError(err)
373-
req.NotNil(ts)
374-
t.Logf(`/v2/ChainHead("finalized"): %d`, ts.Height())
341+
callAndVerify := func(version int, method, params, responsePath string, expectValue any, verify func(req *require.Assertions, head *types.TipSet, node ipld.Node)) {
342+
var response string
343+
head, err := full.ChainHead(ctx)
344+
for {
345+
req.NoError(err)
346+
var statusCode int
347+
statusCode, response = full.HttpRpcRequest(version, fmt.Sprintf(`{"jsonrpc":"2.0","method":"%s","params":%s,"id":1}`, method, params))
348+
req.Equal(200, statusCode)
349+
afterHead, err := full.ChainHead(ctx)
350+
req.NoError(err)
351+
if head.Height() == afterHead.Height() {
352+
// chain hasn't advanced while we were waiting for the response
353+
break
354+
}
355+
head = afterHead
356+
}
357+
node, err := ipld.Decode([]byte(response), dagjson.Decode)
358+
req.NoError(err, "failed to decode JSON response")
359+
req.Equal(ipld.Kind_Map, node.Kind())
360+
result, err := traversal.Get(node, ipld.ParsePath(responsePath))
361+
req.NoError(err)
362+
if verify != nil {
363+
verify(req, head, result)
364+
} else {
365+
switch v := expectValue.(type) {
366+
case int:
367+
req.Equal(ipld.Kind_Int, result.Kind())
368+
req.EqualValues(v, must.One(result.AsInt()))
369+
default:
370+
req.FailNowf("unexpected type", "%T, maybe add support for this?", expectValue)
371+
}
372+
}
373+
t.Logf(`/rpc/v1 {"method":"%s","params":%s}: %s`, method, params, string(must.One(ipld.Encode(result, dagjson.Encode))))
374+
}
375+
376+
testCases := []struct {
377+
name string
378+
method string
379+
version int // 1 or 2
380+
apiArgs []any
381+
apiVerify func(req *require.Assertions, head *types.TipSet, result any) // nil to skip api call
382+
rpcParams string // empty to skip rpc call, otherwise JSON array
383+
rpcVerifyPath string // IPLD path to verify
384+
rpcVerifyValue any // expected value at path
385+
rpcVerify func(req *require.Assertions, head *types.TipSet, node ipld.Node) // if not using rpcVerifyValue, use this
386+
}{
387+
{
388+
name: "v1/ChainGetTipSetByHeight/15/head",
389+
method: "ChainGetTipSetByHeight",
390+
version: 1,
391+
apiArgs: []any{abi.ChainEpoch(15), types.EmptyTSK},
392+
apiVerify: func(req *require.Assertions, head *types.TipSet, result any) {
393+
ts, ok := result.(*types.TipSet)
394+
req.True(ok)
395+
req.EqualValues(15, ts.Height())
396+
},
397+
rpcParams: `[15,[]]`,
398+
rpcVerifyPath: "result/Height",
399+
rpcVerifyValue: 15,
400+
},
401+
{
402+
name: "v2/ChainGetTipSetByHeight/15/head",
403+
method: "ChainGetTipSetByHeight",
404+
version: 2,
405+
apiArgs: []any{abi.ChainEpoch(15), types.NewTipSetSelector(types.EmptyTSK)},
406+
apiVerify: func(req *require.Assertions, head *types.TipSet, result any) {
407+
ts, ok := result.(*types.TipSet)
408+
req.True(ok)
409+
req.EqualValues(15, ts.Height())
410+
},
411+
rpcParams: `[15,[]]`,
412+
rpcVerifyPath: "result/Height",
413+
rpcVerifyValue: 15,
414+
},
415+
{
416+
name: "v2/ChainGetTipSetByHeight/15/latest",
417+
method: "ChainGetTipSetByHeight",
418+
version: 2,
419+
apiArgs: []any{abi.ChainEpoch(15), types.TipSetSelectorLatest},
420+
apiVerify: func(req *require.Assertions, head *types.TipSet, result any) {
421+
ts, ok := result.(*types.TipSet)
422+
req.True(ok)
423+
req.EqualValues(15, ts.Height())
424+
},
425+
rpcParams: `[15,"latest"]`,
426+
rpcVerifyPath: "result/Height",
427+
rpcVerifyValue: 15,
428+
},
429+
{
430+
name: "v2/ChainGetTipSetByHeight/15/-",
431+
method: "ChainGetTipSetByHeight",
432+
version: 2,
433+
rpcParams: `[15,""]`,
434+
rpcVerifyPath: "result/Height",
435+
rpcVerifyValue: 15,
436+
},
437+
{
438+
name: "v2/ChainGetTipSetByHeight/15/finalized",
439+
method: "ChainGetTipSetByHeight",
440+
version: 2,
441+
apiArgs: []any{abi.ChainEpoch(15), types.TipSetSelectorFinalized},
442+
apiVerify: func(req *require.Assertions, head *types.TipSet, result any) {
443+
ts, ok := result.(*types.TipSet)
444+
req.True(ok)
445+
req.EqualValues(15, ts.Height())
446+
},
447+
rpcParams: `[15,"finalized"]`,
448+
rpcVerifyPath: "result/Height",
449+
rpcVerifyValue: 15,
450+
},
451+
{
452+
name: "v1/ChainHead",
453+
method: "ChainHead",
454+
version: 1,
455+
rpcParams: `[]`,
456+
rpcVerifyPath: "result/Height",
457+
rpcVerify: func(req *require.Assertions, head *types.TipSet, node ipld.Node) {
458+
req.EqualValues(head.Height(), must.One(node.AsInt()))
459+
},
460+
},
461+
{
462+
name: "v2/ChainHead",
463+
method: "ChainHead",
464+
version: 2,
465+
apiArgs: []any{(jsonrpc.RawParams)(nil)},
466+
apiVerify: func(req *require.Assertions, head *types.TipSet, result any) {
467+
ts, ok := result.(*types.TipSet)
468+
req.True(ok)
469+
req.EqualValues(head.Height(), ts.Height())
470+
},
471+
rpcParams: `[]`,
472+
rpcVerifyPath: "result/Height",
473+
rpcVerify: func(req *require.Assertions, head *types.TipSet, node ipld.Node) {
474+
req.EqualValues(head.Height(), must.One(node.AsInt()))
475+
},
476+
},
477+
{
478+
name: "v2/ChainHead/latest",
479+
method: "ChainHead",
480+
version: 2,
481+
apiArgs: []any{jsonrpc.RawParams(`["latest"]`)},
482+
apiVerify: func(req *require.Assertions, head *types.TipSet, result any) {
483+
ts, ok := result.(*types.TipSet)
484+
req.True(ok)
485+
req.EqualValues(head.Height(), ts.Height())
486+
},
487+
rpcParams: `["latest"]`,
488+
rpcVerifyPath: "result/Height",
489+
rpcVerify: func(req *require.Assertions, head *types.TipSet, node ipld.Node) {
490+
req.EqualValues(head.Height(), must.One(node.AsInt()))
491+
},
492+
},
493+
{
494+
name: "v2/ChainHead/-",
495+
method: "ChainHead",
496+
version: 2,
497+
apiArgs: []any{jsonrpc.RawParams(`[""]`)},
498+
apiVerify: func(req *require.Assertions, head *types.TipSet, result any) {
499+
ts, ok := result.(*types.TipSet)
500+
req.True(ok)
501+
req.EqualValues(head.Height(), ts.Height())
502+
},
503+
rpcParams: `[""]`,
504+
rpcVerifyPath: "result/Height",
505+
rpcVerify: func(req *require.Assertions, head *types.TipSet, node ipld.Node) {
506+
req.EqualValues(head.Height(), must.One(node.AsInt()))
507+
},
508+
},
509+
{
510+
name: "v2/ChainHead/finalized",
511+
method: "ChainHead",
512+
version: 2,
513+
apiArgs: []any{jsonrpc.RawParams(`["finalized"]`)},
514+
apiVerify: func(req *require.Assertions, head *types.TipSet, result any) {
515+
ts, ok := result.(*types.TipSet)
516+
req.True(ok)
517+
req.EqualValues(head.Height()-policy.ChainFinality, ts.Height())
518+
},
519+
rpcParams: `["finalized"]`,
520+
rpcVerifyPath: "result/Height",
521+
rpcVerify: func(req *require.Assertions, head *types.TipSet, node ipld.Node) {
522+
req.EqualValues(head.Height()-policy.ChainFinality, must.One(node.AsInt()))
523+
},
524+
},
525+
}
526+
527+
for _, tc := range testCases {
528+
t.Run(tc.name, func(t *testing.T) {
529+
if tc.apiVerify != nil {
530+
args := []reflect.Value{reflect.ValueOf(ctx)}
531+
for _, arg := range tc.apiArgs {
532+
if arg == nil {
533+
args = append(args, reflect.Zero(reflect.TypeOf(arg)))
534+
} else {
535+
args = append(args, reflect.ValueOf(arg))
536+
}
537+
}
538+
api := reflect.ValueOf(full)
539+
if tc.version == 2 {
540+
api = reflect.ValueOf(full.V2)
541+
}
542+
method := api.MethodByName(tc.method)
543+
head, err := full.ChainHead(ctx)
544+
req.NoError(err)
545+
var response any
546+
for {
547+
resp := method.Call(args)
548+
req.Len(resp, 2)
549+
req.Nil(resp[1].Interface(), "error in API call")
550+
afterHead, err := full.ChainHead(ctx)
551+
req.NoError(err)
552+
if head.Height() == afterHead.Height() {
553+
// chain hasn't advanced while we were waiting for the response
554+
response = resp[0].Interface()
555+
break
556+
}
557+
head = afterHead
558+
}
559+
tc.apiVerify(req, head, response)
560+
}
561+
562+
if tc.rpcParams != "" {
563+
callAndVerify(tc.version, "Filecoin."+tc.method, tc.rpcParams, tc.rpcVerifyPath, tc.rpcVerifyValue, tc.rpcVerify)
564+
}
565+
})
566+
}
375567
}

itests/kit/rpc.go

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package kit
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
7+
"io"
68
"net"
79
"net/http"
810
"net/http/httptest"
@@ -44,7 +46,7 @@ func CreateRPCServer(t *testing.T, handler http.Handler, listener net.Listener)
4446
}
4547

4648
func fullRpc(t *testing.T, f *TestFullNode) (*TestFullNode, Closer) {
47-
handler, err := node.FullNodeHandler(f.FullNode, nil, false)
49+
handler, err := node.FullNodeHandler(f.FullNode, f.V2, false)
4850
require.NoError(t, err)
4951

5052
l, err := net.Listen("tcp", "127.0.0.1:0")
@@ -105,3 +107,20 @@ func workerRpc(t *testing.T, m *TestWorker) *TestWorker {
105107
m.ListenAddr, m.Worker = maddr, cl
106108
return m
107109
}
110+
111+
func (full *TestFullNode) HttpRpcRequest(version int, payload string) (int, string) {
112+
full.t.Helper()
113+
req := require.New(full.t)
114+
115+
req.NotEmpty(full.ListenURL, "not listening for rpc, turn on with `kit.ThroughRPC()`")
116+
url := fmt.Sprintf("%s/rpc/v%d", full.ListenURL, version)
117+
request, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(payload)))
118+
req.NoError(err)
119+
request.Header.Set("Content-Type", "application/json")
120+
response, err := http.DefaultClient.Do(request)
121+
req.NoError(err)
122+
defer func() { _ = response.Body.Close() }()
123+
body, err := io.ReadAll(response.Body)
124+
req.NoError(err)
125+
return response.StatusCode, string(body)
126+
}

0 commit comments

Comments
 (0)