Skip to content

Commit 495c23e

Browse files
committed
test(upgrade): expand workflow coverage with fake rpc harness
Signed-off-by: kangeunchan <kangeunchan080310@gmail.com>
1 parent 08931a6 commit 495c23e

4 files changed

Lines changed: 1834 additions & 0 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
package upgrade
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
"net/http/httptest"
10+
"strings"
11+
"testing"
12+
"time"
13+
14+
"github.com/altuslabsxyz/devnet-builder/internal/application/dto"
15+
"github.com/altuslabsxyz/devnet-builder/internal/application/ports"
16+
"github.com/ethereum/go-ethereum/common"
17+
"github.com/ethereum/go-ethereum/crypto"
18+
)
19+
20+
type rpcRequest struct {
21+
JSONRPC string `json:"jsonrpc"`
22+
Method string `json:"method"`
23+
Params json.RawMessage `json:"params"`
24+
ID interface{} `json:"id"`
25+
}
26+
27+
type rpcResponse struct {
28+
JSONRPC string `json:"jsonrpc"`
29+
ID interface{} `json:"id"`
30+
Result interface{} `json:"result,omitempty"`
31+
Error interface{} `json:"error,omitempty"`
32+
}
33+
34+
func startFakeEthRPCServer(t *testing.T, proposalID uint64) func() {
35+
t.Helper()
36+
37+
listener, err := net.Listen("tcp", "127.0.0.1:8545")
38+
if err != nil {
39+
t.Skipf("cannot bind fake eth rpc on :8545: %v", err)
40+
}
41+
42+
txHash := "0x" + strings.Repeat("1", 64)
43+
blockHash := "0x" + strings.Repeat("2", 64)
44+
logsBloom := "0x" + strings.Repeat("0", 512)
45+
proposalIDTopic := fmt.Sprintf("0x%064x", proposalID)
46+
47+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
48+
defer r.Body.Close()
49+
50+
var req rpcRequest
51+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
52+
w.WriteHeader(http.StatusBadRequest)
53+
_ = json.NewEncoder(w).Encode(rpcResponse{JSONRPC: "2.0", ID: nil, Error: map[string]interface{}{"code": -32700, "message": "parse error"}})
54+
return
55+
}
56+
57+
res := rpcResponse{JSONRPC: "2.0", ID: req.ID}
58+
switch req.Method {
59+
case "eth_chainId":
60+
res.Result = "0x1"
61+
case "eth_getBalance":
62+
res.Result = "0x56bc75e2d63100000"
63+
case "eth_getTransactionCount":
64+
res.Result = "0x0"
65+
case "eth_gasPrice":
66+
res.Result = "0x3b9aca00"
67+
case "eth_call":
68+
res.Result = "0x"
69+
case "eth_estimateGas":
70+
res.Result = "0x7a120"
71+
case "eth_sendRawTransaction":
72+
res.Result = txHash
73+
case "eth_getTransactionReceipt":
74+
res.Result = map[string]interface{}{
75+
"transactionHash": txHash,
76+
"transactionIndex": "0x0",
77+
"blockHash": blockHash,
78+
"blockNumber": "0x1",
79+
"from": "0x0000000000000000000000000000000000000001",
80+
"to": GovPrecompileAddress,
81+
"cumulativeGasUsed": "0x5208",
82+
"gasUsed": "0x5208",
83+
"contractAddress": nil,
84+
"logsBloom": logsBloom,
85+
"status": "0x1",
86+
"type": "0x0",
87+
"effectiveGasPrice": "0x3b9aca00",
88+
"logs": []map[string]interface{}{
89+
{
90+
"address": GovPrecompileAddress,
91+
"topics": []string{"0x" + strings.Repeat("3", 64), proposalIDTopic},
92+
"data": "0x",
93+
"blockNumber": "0x1",
94+
"transactionHash": txHash,
95+
"transactionIndex": "0x0",
96+
"blockHash": blockHash,
97+
"logIndex": "0x0",
98+
"removed": false,
99+
},
100+
},
101+
}
102+
default:
103+
res.Error = map[string]interface{}{"code": -32601, "message": "method not found"}
104+
}
105+
106+
w.Header().Set("Content-Type", "application/json")
107+
_ = json.NewEncoder(w).Encode(res)
108+
})
109+
110+
srv := &http.Server{Handler: handler}
111+
go func() {
112+
_ = srv.Serve(listener)
113+
}()
114+
115+
return func() {
116+
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
117+
defer cancel()
118+
_ = srv.Shutdown(ctx)
119+
}
120+
}
121+
122+
func testValidatorKey(t *testing.T) ports.ValidatorKey {
123+
t.Helper()
124+
pkHex := "4f3edf983ac636a65a842ce7c78d9aa706d3b113bce036f0f6078ff8b8f4f0c7"
125+
pk, err := crypto.HexToECDSA(pkHex)
126+
if err != nil {
127+
t.Fatalf("failed to parse key: %v", err)
128+
}
129+
addr := crypto.PubkeyToAddress(pk.PublicKey).Hex()
130+
return ports.ValidatorKey{
131+
Name: "validator0",
132+
Bech32Address: "stable1test",
133+
HexAddress: addr,
134+
PrivateKey: pkHex,
135+
}
136+
}
137+
138+
func TestProposeUseCase_Execute_FakeEthRPC(t *testing.T) {
139+
teardown := startFakeEthRPCServer(t, 7)
140+
defer teardown()
141+
142+
key := testValidatorKey(t)
143+
uc := NewProposeUseCase(
144+
&mockDevnetRepo{},
145+
&mockRPCClient{},
146+
&mockValidatorKeyLoader{loadValidatorKeysFunc: func(ctx context.Context, opts ports.ValidatorKeyOptions) ([]ports.ValidatorKey, error) {
147+
return []ports.ValidatorKey{key}, nil
148+
}},
149+
&testLogger{},
150+
)
151+
152+
out, err := uc.Execute(context.Background(), dto.ProposeInput{
153+
HomeDir: "/tmp/devnet",
154+
UpgradeName: "v2.0.0",
155+
UpgradeHeight: 12345,
156+
VotingPeriod: 30 * time.Second,
157+
})
158+
if err != nil {
159+
t.Fatalf("unexpected error: %v", err)
160+
}
161+
if out.ProposalID != 7 {
162+
t.Fatalf("proposal id = %d, want 7", out.ProposalID)
163+
}
164+
if out.TxHash == "" {
165+
t.Fatalf("expected tx hash")
166+
}
167+
}
168+
169+
func TestVoteUseCase_Execute_FakeEthRPC(t *testing.T) {
170+
teardown := startFakeEthRPCServer(t, 9)
171+
defer teardown()
172+
173+
key := testValidatorKey(t)
174+
uc := NewVoteUseCase(
175+
&mockDevnetRepo{loadFunc: func(ctx context.Context, homeDir string) (*ports.DevnetMetadata, error) {
176+
return &ports.DevnetMetadata{NumValidators: 1, ExecutionMode: "local", CurrentVersion: "v1", BinaryName: "stabled"}, nil
177+
}},
178+
&mockRPCClient{getProposalFunc: func(ctx context.Context, id uint64) (*ports.Proposal, error) {
179+
return &ports.Proposal{Status: ports.ProposalStatusVoting}, nil
180+
}},
181+
&mockValidatorKeyLoader{loadValidatorKeysFunc: func(ctx context.Context, opts ports.ValidatorKeyOptions) ([]ports.ValidatorKey, error) {
182+
return []ports.ValidatorKey{key}, nil
183+
}},
184+
&testLogger{},
185+
)
186+
187+
out, err := uc.Execute(context.Background(), dto.VoteInput{
188+
HomeDir: "/tmp/devnet",
189+
ProposalID: 9,
190+
VoteOption: "yes",
191+
FromAll: false,
192+
})
193+
if err != nil {
194+
t.Fatalf("unexpected error: %v", err)
195+
}
196+
if out.VotesCast != 1 || out.TotalVoters != 1 {
197+
t.Fatalf("unexpected vote result: %+v", out)
198+
}
199+
if len(out.TxHashes) != 1 {
200+
t.Fatalf("expected one tx hash")
201+
}
202+
}
203+
204+
func TestWaitForReceiptCancelledContext(t *testing.T) {
205+
ctx, cancel := context.WithCancel(context.Background())
206+
cancel()
207+
208+
_, err := waitForReceipt(ctx, nil, common.Hash{})
209+
if err == nil {
210+
t.Fatalf("expected cancellation error")
211+
}
212+
}
213+
214+
func TestGetLatestProposalID(t *testing.T) {
215+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
216+
_, _ = w.Write([]byte(`{"proposals":[{"id":"42"}]}`))
217+
}))
218+
defer ts.Close()
219+
220+
id, err := getLatestProposalID(ts.URL)
221+
if err != nil {
222+
t.Fatalf("unexpected error: %v", err)
223+
}
224+
if id != 42 {
225+
t.Fatalf("id = %d", id)
226+
}
227+
}

0 commit comments

Comments
 (0)