From c95a5b0d74dba3005b83a8f982ac77899badbe03 Mon Sep 17 00:00:00 2001 From: Janos Guljas Date: Fri, 15 Feb 2019 15:44:07 +0100 Subject: [PATCH] cmd/swarm/global-store: add chunk explorer tests --- cmd/swarm/global-store/explorer_test.go | 254 ++++++++++++++++++++ cmd/swarm/global-store/global_store_test.go | 64 +++-- 2 files changed, 294 insertions(+), 24 deletions(-) create mode 100644 cmd/swarm/global-store/explorer_test.go diff --git a/cmd/swarm/global-store/explorer_test.go b/cmd/swarm/global-store/explorer_test.go new file mode 100644 index 0000000000..c19e8e075e --- /dev/null +++ b/cmd/swarm/global-store/explorer_test.go @@ -0,0 +1,254 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of go-ethereum. +// +// go-ethereum is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// go-ethereum is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with go-ethereum. If not, see . + +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/swarm/storage/mock/explorer" + mockRPC "github.com/ethereum/go-ethereum/swarm/storage/mock/rpc" +) + +// TestExplorer validates basic chunk explorer functionality by storing +// a small set of chunk and making http requests on exposed endpoint. +// Full chunk explorer validation is done in mock/explorer package. +func TestExplorer(t *testing.T) { + addr := findFreeTCPAddress(t) + explorerAddr := findFreeTCPAddress(t) + testCmd := runGlobalStore(t, "ws", "--addr", addr, "--explorer-address", explorerAddr) + defer testCmd.Interrupt() + + client := websocketClient(t, addr) + + store := mockRPC.NewGlobalStore(client) + defer store.Close() + + nodeKeys := map[string][]string{ + "a1": {"b1", "b2", "b3"}, + "a2": {"b3", "b4", "b5"}, + } + + keyNodes := make(map[string][]string) + + for addr, keys := range nodeKeys { + for _, key := range keys { + keyNodes[key] = append(keyNodes[key], addr) + } + } + + invalidAddr := "c1" + invalidKey := "d1" + + for addr, keys := range nodeKeys { + for _, key := range keys { + err := store.Put(common.HexToAddress(addr), common.Hex2Bytes(key), []byte("data")) + if err != nil { + t.Fatal(err) + } + } + } + + endpoint := "http://" + explorerAddr + + t.Run("has key", func(t *testing.T) { + for addr, keys := range nodeKeys { + for _, key := range keys { + testStatusResponse(t, endpoint+"/api/has-key/"+addr+"/"+key, http.StatusOK) + testStatusResponse(t, endpoint+"/api/has-key/"+invalidAddr+"/"+key, http.StatusNotFound) + } + testStatusResponse(t, endpoint+"/api/has-key/"+addr+"/"+invalidKey, http.StatusNotFound) + } + testStatusResponse(t, endpoint+"/api/has-key/"+invalidAddr+"/"+invalidKey, http.StatusNotFound) + }) + + t.Run("keys", func(t *testing.T) { + var keys []string + for key := range keyNodes { + keys = append(keys, key) + } + sort.Strings(keys) + testKeysResponse(t, endpoint+"/api/keys", explorer.KeysResponse{ + Keys: keys, + }) + }) + + t.Run("nodes", func(t *testing.T) { + var nodes []string + for addr := range nodeKeys { + nodes = append(nodes, common.HexToAddress(addr).Hex()) + } + sort.Strings(nodes) + testNodesResponse(t, endpoint+"/api/nodes", explorer.NodesResponse{ + Nodes: nodes, + }) + }) + + t.Run("node keys", func(t *testing.T) { + for addr, keys := range nodeKeys { + testKeysResponse(t, endpoint+"/api/keys?node="+addr, explorer.KeysResponse{ + Keys: keys, + }) + } + testKeysResponse(t, endpoint+"/api/keys?node="+invalidAddr, explorer.KeysResponse{}) + }) + + t.Run("key nodes", func(t *testing.T) { + for key, addrs := range keyNodes { + var nodes []string + for _, addr := range addrs { + nodes = append(nodes, common.HexToAddress(addr).Hex()) + } + sort.Strings(nodes) + testNodesResponse(t, endpoint+"/api/nodes?key="+key, explorer.NodesResponse{ + Nodes: nodes, + }) + } + testNodesResponse(t, endpoint+"/api/nodes?key="+invalidKey, explorer.NodesResponse{}) + }) +} + +// TestExplorer_CORSOrigin validates if chunk explorer returns +// correct CORS origin header in GET and OPTIONS requests. +func TestExplorer_CORSOrigin(t *testing.T) { + origin := "http://localhost/" + addr := findFreeTCPAddress(t) + explorerAddr := findFreeTCPAddress(t) + testCmd := runGlobalStore(t, "ws", + "--addr", addr, + "--explorer-address", explorerAddr, + "--explorer-cors-origin", origin, + ) + defer testCmd.Interrupt() + + // wait until the server is started + waitHTTPEndpoint(t, explorerAddr) + + url := "http://" + explorerAddr + "/api/keys" + + t.Run("get", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Origin", origin) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + header := resp.Header.Get("Access-Control-Allow-Origin") + if header != origin { + t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, origin) + } + }) + + t.Run("preflight", func(t *testing.T) { + req, err := http.NewRequest(http.MethodOptions, url, nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Origin", origin) + req.Header.Set("Access-Control-Request-Method", "GET") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + header := resp.Header.Get("Access-Control-Allow-Origin") + if header != origin { + t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, origin) + } + }) +} + +// testStatusResponse makes an http request to provided url +// and validates if response is explorer.StatusResponse for +// the expected status code. +func testStatusResponse(t *testing.T, url string, code int) { + t.Helper() + + resp, err := http.Get(url) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != code { + t.Errorf("got status code %v, want %v", resp.StatusCode, code) + } + var r explorer.StatusResponse + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + t.Fatal(err) + } + if r.Code != code { + t.Errorf("got response code %v, want %v", r.Code, code) + } + if r.Message != http.StatusText(code) { + t.Errorf("got response message %q, want %q", r.Message, http.StatusText(code)) + } +} + +// testKeysResponse makes an http request to provided url +// and validates if response machhes expected explorer.KeysResponse. +func testKeysResponse(t *testing.T, url string, want explorer.KeysResponse) { + t.Helper() + + resp, err := http.Get(url) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("got status code %v, want %v", resp.StatusCode, http.StatusOK) + } + var r explorer.KeysResponse + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + t.Fatal(err) + } + if fmt.Sprint(r.Keys) != fmt.Sprint(want.Keys) { + t.Errorf("got keys %v, want %v", r.Keys, want.Keys) + } + if r.Next != want.Next { + t.Errorf("got next %s, want %s", r.Next, want.Next) + } +} + +// testNodeResponse makes an http request to provided url +// and validates if response machhes expected explorer.NodeResponse. +func testNodesResponse(t *testing.T, url string, want explorer.NodesResponse) { + t.Helper() + + resp, err := http.Get(url) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusOK { + t.Errorf("got status code %v, want %v", resp.StatusCode, http.StatusOK) + } + var r explorer.NodesResponse + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + t.Fatal(err) + } + if fmt.Sprint(r.Nodes) != fmt.Sprint(want.Nodes) { + t.Errorf("got nodes %v, want %v", r.Nodes, want.Nodes) + } + if r.Next != want.Next { + t.Errorf("got next %s, want %s", r.Next, want.Next) + } +} diff --git a/cmd/swarm/global-store/global_store_test.go b/cmd/swarm/global-store/global_store_test.go index 85f361ed4f..e73cf6d7a3 100644 --- a/cmd/swarm/global-store/global_store_test.go +++ b/cmd/swarm/global-store/global_store_test.go @@ -69,16 +69,7 @@ func testHTTP(t *testing.T, put bool, args ...string) { // wait until global store process is started as // rpc.DialHTTP is actually not connecting - for i := 0; i < 1000; i++ { - _, err = http.DefaultClient.Get("http://" + addr) - if err == nil { - break - } - time.Sleep(10 * time.Millisecond) - } - if err != nil { - t.Fatal(err) - } + waitHTTPEndpoint(t, addr) store := mockRPC.NewGlobalStore(client) defer store.Close() @@ -137,19 +128,7 @@ func testWebsocket(t *testing.T, put bool, args ...string) { testCmd := runGlobalStore(t, append([]string{"ws", "--addr", addr}, args...)...) defer testCmd.Interrupt() - var client *rpc.Client - var err error - // wait until global store process is started - for i := 0; i < 1000; i++ { - client, err = rpc.DialWebsocket(context.Background(), "ws://"+addr, "") - if err == nil { - break - } - time.Sleep(10 * time.Millisecond) - } - if err != nil { - t.Fatal(err) - } + client := websocketClient(t, addr) store := mockRPC.NewGlobalStore(client) defer store.Close() @@ -160,7 +139,7 @@ func testWebsocket(t *testing.T, put bool, args ...string) { wantValue := "value" if put { - err = node.Put([]byte(wantKey), []byte(wantValue)) + err := node.Put([]byte(wantKey), []byte(wantValue)) if err != nil { t.Fatal(err) } @@ -189,3 +168,40 @@ func findFreeTCPAddress(t *testing.T) (addr string) { return listener.Addr().String() } + +// websocketClient waits until global store process is started +// and returns rpc client. +func websocketClient(t *testing.T, addr string) (client *rpc.Client) { + t.Helper() + + var err error + for i := 0; i < 1000; i++ { + client, err = rpc.DialWebsocket(context.Background(), "ws://"+addr, "") + if err == nil { + break + } + time.Sleep(10 * time.Millisecond) + } + if err != nil { + t.Fatal(err) + } + return client +} + +// waitHTTPEndpoint retries http requests to a provided +// address until the connection is established. +func waitHTTPEndpoint(t *testing.T, addr string) { + t.Helper() + + var err error + for i := 0; i < 1000; i++ { + _, err = http.Get("http://" + addr) + if err == nil { + break + } + time.Sleep(10 * time.Millisecond) + } + if err != nil { + t.Fatal(err) + } +}