github.com/btcsuite/btcd@v0.24.0/rpcclient/chain_test.go (about) 1 package rpcclient 2 3 import ( 4 "errors" 5 "github.com/gorilla/websocket" 6 "net/http" 7 "net/http/httptest" 8 "strings" 9 "sync" 10 "testing" 11 "time" 12 ) 13 14 var upgrader = websocket.Upgrader{} 15 16 // TestUnmarshalGetBlockChainInfoResult ensures that the SoftForks and 17 // UnifiedSoftForks fields of GetBlockChainInfoResult are properly unmarshaled 18 // when using the expected backend version. 19 func TestUnmarshalGetBlockChainInfoResultSoftForks(t *testing.T) { 20 t.Parallel() 21 22 tests := []struct { 23 name string 24 version BackendVersion 25 res []byte 26 compatible bool 27 }{ 28 { 29 name: "bitcoind < 0.19.0 with separate softforks", 30 version: BitcoindPre19, 31 res: []byte(`{"softforks": [{"version": 2}]}`), 32 compatible: true, 33 }, 34 { 35 name: "bitcoind >= 0.19.0 with separate softforks", 36 version: BitcoindPost19, 37 res: []byte(`{"softforks": [{"version": 2}]}`), 38 compatible: false, 39 }, 40 { 41 name: "bitcoind < 0.19.0 with unified softforks", 42 version: BitcoindPre19, 43 res: []byte(`{"softforks": {"segwit": {"type": "bip9"}}}`), 44 compatible: false, 45 }, 46 { 47 name: "bitcoind >= 0.19.0 with unified softforks", 48 version: BitcoindPost19, 49 res: []byte(`{"softforks": {"segwit": {"type": "bip9"}}}`), 50 compatible: true, 51 }, 52 } 53 54 for _, test := range tests { 55 success := t.Run(test.name, func(t *testing.T) { 56 // We'll start by unmarshaling the JSON into a struct. 57 // The SoftForks and UnifiedSoftForks field should not 58 // be set yet, as they are unmarshaled within a 59 // different function. 60 info, err := unmarshalPartialGetBlockChainInfoResult(test.res) 61 if err != nil { 62 t.Fatal(err) 63 } 64 if info.SoftForks != nil { 65 t.Fatal("expected SoftForks to be empty") 66 } 67 if info.UnifiedSoftForks != nil { 68 t.Fatal("expected UnifiedSoftForks to be empty") 69 } 70 71 // Proceed to unmarshal the softforks of the response 72 // with the expected version. If the version is 73 // incompatible with the response, then this should 74 // fail. 75 err = unmarshalGetBlockChainInfoResultSoftForks( 76 info, test.version, test.res, 77 ) 78 if test.compatible && err != nil { 79 t.Fatalf("unable to unmarshal softforks: %v", err) 80 } 81 if !test.compatible && err == nil { 82 t.Fatal("expected to not unmarshal softforks") 83 } 84 if !test.compatible { 85 return 86 } 87 88 // If the version is compatible with the response, we 89 // should expect to see the proper softforks field set. 90 if test.version == BitcoindPost19 && 91 info.SoftForks != nil { 92 t.Fatal("expected SoftForks to be empty") 93 } 94 if test.version == BitcoindPre19 && 95 info.UnifiedSoftForks != nil { 96 t.Fatal("expected UnifiedSoftForks to be empty") 97 } 98 }) 99 if !success { 100 return 101 } 102 } 103 } 104 105 func TestFutureGetBlockCountResultReceiveErrors(t *testing.T) { 106 responseChan := FutureGetBlockCountResult(make(chan *Response)) 107 response := Response{ 108 result: []byte{}, 109 err: errors.New("blah blah something bad happened"), 110 } 111 go func() { 112 responseChan <- &response 113 }() 114 115 _, err := responseChan.Receive() 116 if err == nil || err.Error() != "blah blah something bad happened" { 117 t.Fatalf("unexpected error: %s", err.Error()) 118 } 119 } 120 121 func TestFutureGetBlockCountResultReceiveMarshalsResponseCorrectly(t *testing.T) { 122 responseChan := FutureGetBlockCountResult(make(chan *Response)) 123 response := Response{ 124 result: []byte{0x36, 0x36}, 125 err: nil, 126 } 127 go func() { 128 responseChan <- &response 129 }() 130 131 res, err := responseChan.Receive() 132 if err != nil { 133 t.Fatalf("unexpected error: %s", err.Error()) 134 } 135 136 if res != 66 { 137 t.Fatalf("unexpected response: %d (0x%X)", res, res) 138 } 139 } 140 141 func TestClientConnectedToWSServerRunner(t *testing.T) { 142 type TestTableItem struct { 143 Name string 144 TestCase func(t *testing.T) 145 } 146 147 testTable := []TestTableItem{ 148 TestTableItem{ 149 Name: "TestGetChainTxStatsAsyncSuccessTx", 150 TestCase: func(t *testing.T) { 151 client, serverReceivedChannel, cleanup := makeClient(t) 152 defer cleanup() 153 client.GetChainTxStatsAsync() 154 155 message := <-serverReceivedChannel 156 if message != "{\"jsonrpc\":\"1.0\",\"method\":\"getchaintxstats\",\"params\":[],\"id\":1}" { 157 t.Fatalf("received unexpected message: %s", message) 158 } 159 }, 160 }, 161 TestTableItem{ 162 Name: "TestGetChainTxStatsAsyncShutdownError", 163 TestCase: func(t *testing.T) { 164 client, _, cleanup := makeClient(t) 165 defer cleanup() 166 167 // a bit of a hack here: since there are multiple places where we read 168 // from the shutdown channel, and it is not buffered, ensure that a shutdown 169 // message is sent every time it is read from, this will ensure that 170 // when client.GetChainTxStatsAsync() gets called, it hits the non-blocking 171 // read from the shutdown channel 172 go func() { 173 type shutdownMessage struct{} 174 for { 175 client.shutdown <- shutdownMessage{} 176 } 177 }() 178 179 var response *Response = nil 180 181 for response == nil { 182 respChan := client.GetChainTxStatsAsync() 183 select { 184 case response = <-respChan: 185 default: 186 } 187 } 188 189 if response.err == nil || response.err.Error() != "the client has been shutdown" { 190 t.Fatalf("unexpected error: %s", response.err.Error()) 191 } 192 }, 193 }, 194 TestTableItem{ 195 Name: "TestGetBestBlockHashAsync", 196 TestCase: func(t *testing.T) { 197 client, serverReceivedChannel, cleanup := makeClient(t) 198 defer cleanup() 199 ch := client.GetBestBlockHashAsync() 200 201 message := <-serverReceivedChannel 202 if message != "{\"jsonrpc\":\"1.0\",\"method\":\"getbestblockhash\",\"params\":[],\"id\":1}" { 203 t.Fatalf("received unexpected message: %s", message) 204 } 205 206 expectedResponse := Response{} 207 208 wg := sync.WaitGroup{} 209 210 wg.Add(1) 211 go func() { 212 defer wg.Done() 213 for { 214 client.requestLock.Lock() 215 if client.requestList.Len() > 0 { 216 r := client.requestList.Back() 217 r.Value.(*jsonRequest).responseChan <- &expectedResponse 218 client.requestLock.Unlock() 219 return 220 } 221 client.requestLock.Unlock() 222 } 223 }() 224 225 response := <-ch 226 227 if &expectedResponse != response { 228 t.Fatalf("received unexepcted response") 229 } 230 231 // ensure the goroutine created in this test exists, 232 // the test is ran with a timeout 233 wg.Wait() 234 }, 235 }, 236 } 237 238 // since these tests rely on concurrency, ensure there is a resonable timeout 239 // that they should run within 240 for _, testCase := range testTable { 241 done := make(chan bool) 242 243 go func() { 244 t.Run(testCase.Name, testCase.TestCase) 245 done <- true 246 }() 247 248 select { 249 case <-done: 250 case <-time.After(5 * time.Second): 251 t.Fatalf("timeout exceeded for: %s", testCase.Name) 252 } 253 } 254 } 255 256 func makeClient(t *testing.T) (*Client, chan string, func()) { 257 serverReceivedChannel := make(chan string) 258 s := httptest.NewServer(http.HandlerFunc(makeUpgradeOnConnect(serverReceivedChannel))) 259 url := strings.TrimPrefix(s.URL, "http://") 260 261 config := ConnConfig{ 262 DisableTLS: true, 263 User: "username", 264 Pass: "password", 265 Host: url, 266 } 267 268 client, err := New(&config, nil) 269 if err != nil { 270 t.Fatalf("error when creating new client %s", err.Error()) 271 } 272 return client, serverReceivedChannel, func() { 273 s.Close() 274 } 275 } 276 277 func makeUpgradeOnConnect(ch chan string) func(http.ResponseWriter, *http.Request) { 278 return func(w http.ResponseWriter, r *http.Request) { 279 c, err := upgrader.Upgrade(w, r, nil) 280 if err != nil { 281 return 282 } 283 defer c.Close() 284 for { 285 _, message, err := c.ReadMessage() 286 if err != nil { 287 break 288 } 289 290 go func() { 291 ch <- string(message) 292 }() 293 } 294 } 295 }