github.com/ethereum/go-ethereum@v1.16.1/rpc/subscription_test.go (about) 1 // Copyright 2016 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package rpc 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "io" 25 "math/big" 26 "net" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/ethereum/go-ethereum/common" 32 "github.com/ethereum/go-ethereum/core/types" 33 ) 34 35 func TestNewID(t *testing.T) { 36 t.Parallel() 37 38 hexchars := "0123456789ABCDEFabcdef" 39 for i := 0; i < 100; i++ { 40 id := string(NewID()) 41 if !strings.HasPrefix(id, "0x") { 42 t.Fatalf("invalid ID prefix, want '0x...', got %s", id) 43 } 44 45 id = id[2:] 46 if len(id) == 0 || len(id) > 32 { 47 t.Fatalf("invalid ID length, want len(id) > 0 && len(id) <= 32), got %d", len(id)) 48 } 49 50 for i := 0; i < len(id); i++ { 51 if strings.IndexByte(hexchars, id[i]) == -1 { 52 t.Fatalf("unexpected byte, want any valid hex char, got %c", id[i]) 53 } 54 } 55 } 56 } 57 58 func TestSubscriptions(t *testing.T) { 59 t.Parallel() 60 61 var ( 62 namespaces = []string{"eth"} 63 service = ¬ificationTestService{} 64 subCount = len(namespaces) 65 notificationCount = 3 66 67 server = NewServer() 68 clientConn, serverConn = net.Pipe() 69 out = json.NewEncoder(clientConn) 70 in = json.NewDecoder(clientConn) 71 successes = make(chan subConfirmation) 72 notifications = make(chan subscriptionResult) 73 errors = make(chan error, subCount*notificationCount+1) 74 ) 75 76 // setup and start server 77 for _, namespace := range namespaces { 78 if err := server.RegisterName(namespace, service); err != nil { 79 t.Fatalf("unable to register test service %v", err) 80 } 81 } 82 go server.ServeCodec(NewCodec(serverConn), 0) 83 defer server.Stop() 84 85 // wait for message and write them to the given channels 86 go waitForMessages(in, successes, notifications, errors) 87 88 // create subscriptions one by one 89 for i, namespace := range namespaces { 90 request := map[string]interface{}{ 91 "id": i, 92 "method": fmt.Sprintf("%s_subscribe", namespace), 93 "jsonrpc": "2.0", 94 "params": []interface{}{"someSubscription", notificationCount, i}, 95 } 96 if err := out.Encode(&request); err != nil { 97 t.Fatalf("Could not create subscription: %v", err) 98 } 99 } 100 101 timeout := time.After(30 * time.Second) 102 subids := make(map[string]string, subCount) 103 count := make(map[string]int, subCount) 104 allReceived := func() bool { 105 done := len(count) == subCount 106 for _, c := range count { 107 if c < notificationCount { 108 done = false 109 } 110 } 111 return done 112 } 113 for !allReceived() { 114 select { 115 case confirmation := <-successes: // subscription created 116 subids[namespaces[confirmation.reqid]] = string(confirmation.subid) 117 case notification := <-notifications: 118 count[notification.ID]++ 119 case err := <-errors: 120 t.Fatal(err) 121 case <-timeout: 122 for _, namespace := range namespaces { 123 subid, found := subids[namespace] 124 if !found { 125 t.Errorf("subscription for %q not created", namespace) 126 continue 127 } 128 if count, found := count[subid]; !found || count < notificationCount { 129 t.Errorf("didn't receive all notifications (%d<%d) in time for namespace %q", count, notificationCount, namespace) 130 } 131 } 132 t.Fatal("timed out") 133 } 134 } 135 } 136 137 // This test checks that unsubscribing works. 138 func TestServerUnsubscribe(t *testing.T) { 139 t.Parallel() 140 141 p1, p2 := net.Pipe() 142 defer p2.Close() 143 144 // Start the server. 145 server := newTestServer() 146 service := ¬ificationTestService{unsubscribed: make(chan string, 1)} 147 server.RegisterName("nftest2", service) 148 go server.ServeCodec(NewCodec(p1), 0) 149 150 // Subscribe. 151 p2.SetDeadline(time.Now().Add(10 * time.Second)) 152 p2.Write([]byte(`{"jsonrpc":"2.0","id":1,"method":"nftest2_subscribe","params":["someSubscription",0,10]}`)) 153 154 // Handle received messages. 155 var ( 156 resps = make(chan subConfirmation) 157 notifications = make(chan subscriptionResult) 158 errors = make(chan error, 1) 159 ) 160 go waitForMessages(json.NewDecoder(p2), resps, notifications, errors) 161 162 // Receive the subscription ID. 163 var sub subConfirmation 164 select { 165 case sub = <-resps: 166 case err := <-errors: 167 t.Fatal(err) 168 } 169 170 // Unsubscribe and check that it is handled on the server side. 171 p2.Write([]byte(`{"jsonrpc":"2.0","method":"nftest2_unsubscribe","params":["` + sub.subid + `"]}`)) 172 for { 173 select { 174 case id := <-service.unsubscribed: 175 if id != string(sub.subid) { 176 t.Errorf("wrong subscription ID unsubscribed") 177 } 178 return 179 case err := <-errors: 180 t.Fatal(err) 181 case <-notifications: 182 // drop notifications 183 } 184 } 185 } 186 187 type subConfirmation struct { 188 reqid int 189 subid ID 190 } 191 192 // waitForMessages reads RPC messages from 'in' and dispatches them into the given channels. 193 // It stops if there is an error. 194 func waitForMessages(in *json.Decoder, successes chan subConfirmation, notifications chan subscriptionResult, errors chan error) { 195 for { 196 resp, notification, err := readAndValidateMessage(in) 197 if err != nil { 198 errors <- err 199 return 200 } else if resp != nil { 201 successes <- *resp 202 } else { 203 notifications <- *notification 204 } 205 } 206 } 207 208 func readAndValidateMessage(in *json.Decoder) (*subConfirmation, *subscriptionResult, error) { 209 var msg jsonrpcMessage 210 if err := in.Decode(&msg); err != nil { 211 return nil, nil, fmt.Errorf("decode error: %v", err) 212 } 213 switch { 214 case msg.isNotification(): 215 var res subscriptionResult 216 if err := json.Unmarshal(msg.Params, &res); err != nil { 217 return nil, nil, fmt.Errorf("invalid subscription result: %v", err) 218 } 219 return nil, &res, nil 220 case msg.isResponse(): 221 var c subConfirmation 222 if msg.Error != nil { 223 return nil, nil, msg.Error 224 } else if err := json.Unmarshal(msg.Result, &c.subid); err != nil { 225 return nil, nil, fmt.Errorf("invalid response: %v", err) 226 } else { 227 json.Unmarshal(msg.ID, &c.reqid) 228 return &c, nil, nil 229 } 230 default: 231 return nil, nil, fmt.Errorf("unrecognized message: %v", msg) 232 } 233 } 234 235 type mockConn struct { 236 enc *json.Encoder 237 } 238 239 // writeJSON writes a message to the connection. 240 func (c *mockConn) writeJSON(ctx context.Context, msg interface{}, isError bool) error { 241 return c.enc.Encode(msg) 242 } 243 244 // closed returns a channel which is closed when the connection is closed. 245 func (c *mockConn) closed() <-chan interface{} { return nil } 246 247 // remoteAddr returns the peer address of the connection. 248 func (c *mockConn) remoteAddr() string { return "" } 249 250 // BenchmarkNotify benchmarks the performance of notifying a subscription. 251 func BenchmarkNotify(b *testing.B) { 252 id := ID("test") 253 notifier := &Notifier{ 254 h: &handler{conn: &mockConn{json.NewEncoder(io.Discard)}}, 255 sub: &Subscription{ID: id}, 256 activated: true, 257 } 258 msg := &types.Header{ 259 ParentHash: common.HexToHash("0x01"), 260 Number: big.NewInt(100), 261 } 262 b.ResetTimer() 263 for i := 0; i < b.N; i++ { 264 notifier.Notify(id, msg) 265 } 266 } 267 268 func TestNotify(t *testing.T) { 269 t.Parallel() 270 271 out := new(bytes.Buffer) 272 id := ID("test") 273 notifier := &Notifier{ 274 h: &handler{conn: &mockConn{json.NewEncoder(out)}}, 275 sub: &Subscription{ID: id}, 276 activated: true, 277 } 278 notifier.Notify(id, "hello") 279 have := strings.TrimSpace(out.String()) 280 want := `{"jsonrpc":"2.0","method":"_subscription","params":{"subscription":"test","result":"hello"}}` 281 if have != want { 282 t.Errorf("have:\n%v\nwant:\n%v\n", have, want) 283 } 284 }