forked from mirror/go-ethereum
swarm: mock store listings (#19157)
* swarm/storage/mock: implement listings methods for mem and rpc stores * swarm/storage/mock/rpc: add comments and newTestStore helper function * swarm/storage/mock/mem: add missing comments * swarm/storage/mock: add comments to new types and constants * swarm/storage/mock/db: implement listings for mock/db global store * swarm/storage/mock/test: add comments for MockStoreListings * swarm/storage/mock/explorer: initial implementation * cmd/swarm/global-store: add chunk explorer * cmd/swarm/global-store: add chunk explorer tests * swarm/storage/mock/explorer: add tests * swarm/storage/mock/explorer: add swagger api definition * swarm/storage/mock/explorer: not-zero test values for invalid addr and key * swarm/storage/mock/explorer: test wildcard cors origin * swarm/storage/mock/db: renames based on Fabio's suggestions * swarm/storage/mock/explorer: add more comments to testHandler function * cmd/swarm/global-store: terminate subprocess with Kill in testsChrisChinchilla-patch-3
parent
02c28046a0
commit
64d10c0872
@ -0,0 +1,66 @@ |
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"net" |
||||
"net/http" |
||||
"time" |
||||
|
||||
"github.com/ethereum/go-ethereum/log" |
||||
"github.com/ethereum/go-ethereum/swarm/storage/mock" |
||||
"github.com/ethereum/go-ethereum/swarm/storage/mock/explorer" |
||||
cli "gopkg.in/urfave/cli.v1" |
||||
) |
||||
|
||||
// serveChunkExplorer starts an http server in background with chunk explorer handler
|
||||
// using the provided global store. Server is started if the returned shutdown function
|
||||
// is not nil.
|
||||
func serveChunkExplorer(ctx *cli.Context, globalStore mock.GlobalStorer) (shutdown func(), err error) { |
||||
if !ctx.IsSet("explorer-address") { |
||||
return nil, nil |
||||
} |
||||
|
||||
corsOrigins := ctx.StringSlice("explorer-cors-origin") |
||||
server := &http.Server{ |
||||
Handler: explorer.NewHandler(globalStore, corsOrigins), |
||||
IdleTimeout: 30 * time.Minute, |
||||
ReadTimeout: 2 * time.Minute, |
||||
WriteTimeout: 2 * time.Minute, |
||||
} |
||||
listener, err := net.Listen("tcp", ctx.String("explorer-address")) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("explorer: %v", err) |
||||
} |
||||
log.Info("chunk explorer http", "address", listener.Addr().String(), "origins", corsOrigins) |
||||
|
||||
go func() { |
||||
if err := server.Serve(listener); err != nil { |
||||
log.Error("chunk explorer", "err", err) |
||||
} |
||||
}() |
||||
|
||||
return func() { |
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
||||
defer cancel() |
||||
if err := server.Shutdown(ctx); err != nil { |
||||
log.Error("chunk explorer: shutdown", "err", err) |
||||
} |
||||
}, nil |
||||
} |
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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.Kill() |
||||
|
||||
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.Kill() |
||||
|
||||
// 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) |
||||
} |
||||
} |
@ -0,0 +1,257 @@ |
||||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package explorer |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"io" |
||||
"net/http" |
||||
"net/url" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/swarm/log" |
||||
"github.com/ethereum/go-ethereum/swarm/storage/mock" |
||||
"github.com/rs/cors" |
||||
) |
||||
|
||||
const jsonContentType = "application/json; charset=utf-8" |
||||
|
||||
// NewHandler constructs an http.Handler with router
|
||||
// that servers requests required by chunk explorer.
|
||||
//
|
||||
// /api/has-key/{node}/{key}
|
||||
// /api/keys?start={key}&node={node}&limit={int[0..1000]}
|
||||
// /api/nodes?start={node}&key={key}&limit={int[0..1000]}
|
||||
//
|
||||
// Data from global store will be served and appropriate
|
||||
// CORS headers will be sent if allowed origins are provided.
|
||||
func NewHandler(store mock.GlobalStorer, corsOrigins []string) (handler http.Handler) { |
||||
mux := http.NewServeMux() |
||||
mux.Handle("/api/has-key/", newHasKeyHandler(store)) |
||||
mux.Handle("/api/keys", newKeysHandler(store)) |
||||
mux.Handle("/api/nodes", newNodesHandler(store)) |
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
||||
jsonStatusResponse(w, http.StatusNotFound) |
||||
}) |
||||
handler = noCacheHandler(mux) |
||||
if corsOrigins != nil { |
||||
handler = cors.New(cors.Options{ |
||||
AllowedOrigins: corsOrigins, |
||||
AllowedMethods: []string{"GET"}, |
||||
MaxAge: 600, |
||||
}).Handler(handler) |
||||
} |
||||
return handler |
||||
} |
||||
|
||||
// newHasKeyHandler returns a new handler that serves
|
||||
// requests for HasKey global store method.
|
||||
// Possible responses are StatusResponse with
|
||||
// status codes 200 or 404 if the chunk is found or not.
|
||||
func newHasKeyHandler(store mock.GlobalStorer) http.HandlerFunc { |
||||
return func(w http.ResponseWriter, r *http.Request) { |
||||
addr, key, ok := parseHasKeyPath(r.URL.Path) |
||||
if !ok { |
||||
jsonStatusResponse(w, http.StatusNotFound) |
||||
return |
||||
} |
||||
found := store.HasKey(addr, key) |
||||
if !found { |
||||
jsonStatusResponse(w, http.StatusNotFound) |
||||
return |
||||
} |
||||
jsonStatusResponse(w, http.StatusOK) |
||||
} |
||||
} |
||||
|
||||
// KeysResponse is a JSON-encoded response for global store
|
||||
// Keys and NodeKeys methods.
|
||||
type KeysResponse struct { |
||||
Keys []string `json:"keys"` |
||||
Next string `json:"next,omitempty"` |
||||
} |
||||
|
||||
// newKeysHandler returns a new handler that serves
|
||||
// requests for Key global store method.
|
||||
// HTTP response body will be JSON-encoded KeysResponse.
|
||||
func newKeysHandler(store mock.GlobalStorer) http.HandlerFunc { |
||||
return func(w http.ResponseWriter, r *http.Request) { |
||||
q := r.URL.Query() |
||||
node := q.Get("node") |
||||
start, limit := listingPage(q) |
||||
|
||||
var keys mock.Keys |
||||
if node == "" { |
||||
var err error |
||||
keys, err = store.Keys(common.Hex2Bytes(start), limit) |
||||
if err != nil { |
||||
log.Error("chunk explorer: keys handler: get keys", "start", start, "err", err) |
||||
jsonStatusResponse(w, http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} else { |
||||
var err error |
||||
keys, err = store.NodeKeys(common.HexToAddress(node), common.Hex2Bytes(start), limit) |
||||
if err != nil { |
||||
log.Error("chunk explorer: keys handler: get node keys", "node", node, "start", start, "err", err) |
||||
jsonStatusResponse(w, http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
ks := make([]string, len(keys.Keys)) |
||||
for i, k := range keys.Keys { |
||||
ks[i] = common.Bytes2Hex(k) |
||||
} |
||||
data, err := json.Marshal(KeysResponse{ |
||||
Keys: ks, |
||||
Next: common.Bytes2Hex(keys.Next), |
||||
}) |
||||
if err != nil { |
||||
log.Error("chunk explorer: keys handler: json marshal", "err", err) |
||||
jsonStatusResponse(w, http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", jsonContentType) |
||||
_, err = io.Copy(w, bytes.NewReader(data)) |
||||
if err != nil { |
||||
log.Error("chunk explorer: keys handler: write response", "err", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// NodesResponse is a JSON-encoded response for global store
|
||||
// Nodes and KeyNodes methods.
|
||||
type NodesResponse struct { |
||||
Nodes []string `json:"nodes"` |
||||
Next string `json:"next,omitempty"` |
||||
} |
||||
|
||||
// newNodesHandler returns a new handler that serves
|
||||
// requests for Nodes global store method.
|
||||
// HTTP response body will be JSON-encoded NodesResponse.
|
||||
func newNodesHandler(store mock.GlobalStorer) http.HandlerFunc { |
||||
return func(w http.ResponseWriter, r *http.Request) { |
||||
q := r.URL.Query() |
||||
key := q.Get("key") |
||||
var start *common.Address |
||||
queryStart, limit := listingPage(q) |
||||
if queryStart != "" { |
||||
s := common.HexToAddress(queryStart) |
||||
start = &s |
||||
} |
||||
|
||||
var nodes mock.Nodes |
||||
if key == "" { |
||||
var err error |
||||
nodes, err = store.Nodes(start, limit) |
||||
if err != nil { |
||||
log.Error("chunk explorer: nodes handler: get nodes", "start", queryStart, "err", err) |
||||
jsonStatusResponse(w, http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} else { |
||||
var err error |
||||
nodes, err = store.KeyNodes(common.Hex2Bytes(key), start, limit) |
||||
if err != nil { |
||||
log.Error("chunk explorer: nodes handler: get key nodes", "key", key, "start", queryStart, "err", err) |
||||
jsonStatusResponse(w, http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
ns := make([]string, len(nodes.Addrs)) |
||||
for i, n := range nodes.Addrs { |
||||
ns[i] = n.Hex() |
||||
} |
||||
var next string |
||||
if nodes.Next != nil { |
||||
next = nodes.Next.Hex() |
||||
} |
||||
data, err := json.Marshal(NodesResponse{ |
||||
Nodes: ns, |
||||
Next: next, |
||||
}) |
||||
if err != nil { |
||||
log.Error("chunk explorer: nodes handler", "err", err) |
||||
jsonStatusResponse(w, http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", jsonContentType) |
||||
_, err = io.Copy(w, bytes.NewReader(data)) |
||||
if err != nil { |
||||
log.Error("chunk explorer: nodes handler: write response", "err", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// parseHasKeyPath extracts address and key from HTTP request
|
||||
// path for HasKey route: /api/has-key/{node}/{key}.
|
||||
// If ok is false, the provided path is not matched.
|
||||
func parseHasKeyPath(p string) (addr common.Address, key []byte, ok bool) { |
||||
p = strings.TrimPrefix(p, "/api/has-key/") |
||||
parts := strings.SplitN(p, "/", 2) |
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" { |
||||
return addr, nil, false |
||||
} |
||||
addr = common.HexToAddress(parts[0]) |
||||
key = common.Hex2Bytes(parts[1]) |
||||
return addr, key, true |
||||
} |
||||
|
||||
// listingPage returns start value and listing limit
|
||||
// from url query values.
|
||||
func listingPage(q url.Values) (start string, limit int) { |
||||
// if limit is not a valid integer (or blank string),
|
||||
// ignore the error and use the returned 0 value
|
||||
limit, _ = strconv.Atoi(q.Get("limit")) |
||||
return q.Get("start"), limit |
||||
} |
||||
|
||||
// StatusResponse is a standardized JSON-encoded response
|
||||
// that contains information about HTTP response code
|
||||
// for easier status identification.
|
||||
type StatusResponse struct { |
||||
Message string `json:"message"` |
||||
Code int `json:"code"` |
||||
} |
||||
|
||||
// jsonStatusResponse writes to the response writer
|
||||
// JSON-encoded StatusResponse based on the provided status code.
|
||||
func jsonStatusResponse(w http.ResponseWriter, code int) { |
||||
w.Header().Set("Content-Type", jsonContentType) |
||||
w.WriteHeader(code) |
||||
err := json.NewEncoder(w).Encode(StatusResponse{ |
||||
Message: http.StatusText(code), |
||||
Code: code, |
||||
}) |
||||
if err != nil { |
||||
log.Error("chunk explorer: json status response", "err", err) |
||||
} |
||||
} |
||||
|
||||
// noCacheHandler sets required HTTP headers to prevent
|
||||
// response caching at the client side.
|
||||
func noCacheHandler(h http.Handler) http.Handler { |
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") |
||||
w.Header().Set("Pragma", "no-cache") |
||||
w.Header().Set("Expires", "0") |
||||
h.ServeHTTP(w, r) |
||||
}) |
||||
} |
@ -0,0 +1,471 @@ |
||||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package explorer |
||||
|
||||
import ( |
||||
"encoding/binary" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"net/url" |
||||
"os" |
||||
"sort" |
||||
"strconv" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/common" |
||||
"github.com/ethereum/go-ethereum/swarm/storage/mock" |
||||
"github.com/ethereum/go-ethereum/swarm/storage/mock/db" |
||||
"github.com/ethereum/go-ethereum/swarm/storage/mock/mem" |
||||
) |
||||
|
||||
// TestHandler_memGlobalStore runs a set of tests
|
||||
// to validate handler with mem global store.
|
||||
func TestHandler_memGlobalStore(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
globalStore := mem.NewGlobalStore() |
||||
|
||||
testHandler(t, globalStore) |
||||
} |
||||
|
||||
// TestHandler_dbGlobalStore runs a set of tests
|
||||
// to validate handler with database global store.
|
||||
func TestHandler_dbGlobalStore(t *testing.T) { |
||||
t.Parallel() |
||||
|
||||
dir, err := ioutil.TempDir("", "swarm-mock-explorer-db-") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
defer os.RemoveAll(dir) |
||||
|
||||
globalStore, err := db.NewGlobalStore(dir) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
defer globalStore.Close() |
||||
|
||||
testHandler(t, globalStore) |
||||
} |
||||
|
||||
// testHandler stores data distributed by node addresses
|
||||
// and validates if this data is correctly retrievable
|
||||
// by using the http.Handler returned by NewHandler function.
|
||||
// This test covers all HTTP routes and various get parameters
|
||||
// on them to check paginated results.
|
||||
func testHandler(t *testing.T, globalStore mock.GlobalStorer) { |
||||
const ( |
||||
nodeCount = 350 |
||||
keyCount = 250 |
||||
keysOnNodeCount = 150 |
||||
) |
||||
|
||||
// keys for every node
|
||||
nodeKeys := make(map[string][]string) |
||||
|
||||
// a node address that is not present in global store
|
||||
invalidAddr := "0x7b8b72938c254cf002c4e1e714d27e022be88d93" |
||||
|
||||
// a key that is not present in global store
|
||||
invalidKey := "f9824192fb515cfb" |
||||
|
||||
for i := 1; i <= nodeCount; i++ { |
||||
b := make([]byte, 8) |
||||
binary.BigEndian.PutUint64(b, uint64(i)) |
||||
addr := common.BytesToAddress(b).Hex() |
||||
nodeKeys[addr] = make([]string, 0) |
||||
} |
||||
|
||||
for i := 1; i <= keyCount; i++ { |
||||
b := make([]byte, 8) |
||||
binary.BigEndian.PutUint64(b, uint64(i)) |
||||
|
||||
key := common.Bytes2Hex(b) |
||||
|
||||
var c int |
||||
for addr := range nodeKeys { |
||||
nodeKeys[addr] = append(nodeKeys[addr], key) |
||||
c++ |
||||
if c >= keysOnNodeCount { |
||||
break |
||||
} |
||||
} |
||||
} |
||||
|
||||
// sort keys for every node as they are expected to be
|
||||
// sorted in HTTP responses
|
||||
for _, keys := range nodeKeys { |
||||
sort.Strings(keys) |
||||
} |
||||
|
||||
// nodes for every key
|
||||
keyNodes := make(map[string][]string) |
||||
|
||||
// construct a reverse mapping of nodes for every key
|
||||
for addr, keys := range nodeKeys { |
||||
for _, key := range keys { |
||||
keyNodes[key] = append(keyNodes[key], addr) |
||||
} |
||||
} |
||||
|
||||
// sort node addresses with case insensitive sort,
|
||||
// as hex letters in node addresses are in mixed caps
|
||||
for _, addrs := range keyNodes { |
||||
sortCaseInsensitive(addrs) |
||||
} |
||||
|
||||
// find a key that is not stored at the address
|
||||
var ( |
||||
unmatchedAddr string |
||||
unmatchedKey string |
||||
) |
||||
for addr, keys := range nodeKeys { |
||||
for key := range keyNodes { |
||||
var found bool |
||||
for _, k := range keys { |
||||
if k == key { |
||||
found = true |
||||
break |
||||
} |
||||
} |
||||
if !found { |
||||
unmatchedAddr = addr |
||||
unmatchedKey = key |
||||
} |
||||
break |
||||
} |
||||
if unmatchedAddr != "" { |
||||
break |
||||
} |
||||
} |
||||
// check if unmatched key/address pair is found
|
||||
if unmatchedAddr == "" || unmatchedKey == "" { |
||||
t.Fatalf("could not find a key that is not associated with a node") |
||||
} |
||||
|
||||
// store the data
|
||||
for addr, keys := range nodeKeys { |
||||
for _, key := range keys { |
||||
err := globalStore.Put(common.HexToAddress(addr), common.Hex2Bytes(key), []byte("data")) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
handler := NewHandler(globalStore, nil) |
||||
|
||||
// this subtest confirms that it has uploaded key and that it does not have invalid keys
|
||||
t.Run("has key", func(t *testing.T) { |
||||
for addr, keys := range nodeKeys { |
||||
for _, key := range keys { |
||||
testStatusResponse(t, handler, "/api/has-key/"+addr+"/"+key, http.StatusOK) |
||||
testStatusResponse(t, handler, "/api/has-key/"+invalidAddr+"/"+key, http.StatusNotFound) |
||||
} |
||||
testStatusResponse(t, handler, "/api/has-key/"+addr+"/"+invalidKey, http.StatusNotFound) |
||||
} |
||||
testStatusResponse(t, handler, "/api/has-key/"+invalidAddr+"/"+invalidKey, http.StatusNotFound) |
||||
testStatusResponse(t, handler, "/api/has-key/"+unmatchedAddr+"/"+unmatchedKey, http.StatusNotFound) |
||||
}) |
||||
|
||||
// this subtest confirms that all keys are are listed in correct order with expected pagination
|
||||
t.Run("keys", func(t *testing.T) { |
||||
var allKeys []string |
||||
for key := range keyNodes { |
||||
allKeys = append(allKeys, key) |
||||
} |
||||
sort.Strings(allKeys) |
||||
|
||||
t.Run("limit 0", testKeys(handler, allKeys, 0, "")) |
||||
t.Run("limit default", testKeys(handler, allKeys, mock.DefaultLimit, "")) |
||||
t.Run("limit 2x default", testKeys(handler, allKeys, 2*mock.DefaultLimit, "")) |
||||
t.Run("limit 0.5x default", testKeys(handler, allKeys, mock.DefaultLimit/2, "")) |
||||
t.Run("limit max", testKeys(handler, allKeys, mock.MaxLimit, "")) |
||||
t.Run("limit 2x max", testKeys(handler, allKeys, 2*mock.MaxLimit, "")) |
||||
t.Run("limit negative", testKeys(handler, allKeys, -10, "")) |
||||
}) |
||||
|
||||
// this subtest confirms that all keys are are listed for every node in correct order
|
||||
// and that for one node different pagination options are correct
|
||||
t.Run("node keys", func(t *testing.T) { |
||||
var limitCheckAddr string |
||||
|
||||
for addr, keys := range nodeKeys { |
||||
testKeys(handler, keys, 0, addr)(t) |
||||
if limitCheckAddr == "" { |
||||
limitCheckAddr = addr |
||||
} |
||||
} |
||||
testKeys(handler, nil, 0, invalidAddr)(t) |
||||
|
||||
limitCheckKeys := nodeKeys[limitCheckAddr] |
||||
t.Run("limit 0", testKeys(handler, limitCheckKeys, 0, limitCheckAddr)) |
||||
t.Run("limit default", testKeys(handler, limitCheckKeys, mock.DefaultLimit, limitCheckAddr)) |
||||
t.Run("limit 2x default", testKeys(handler, limitCheckKeys, 2*mock.DefaultLimit, limitCheckAddr)) |
||||
t.Run("limit 0.5x default", testKeys(handler, limitCheckKeys, mock.DefaultLimit/2, limitCheckAddr)) |
||||
t.Run("limit max", testKeys(handler, limitCheckKeys, mock.MaxLimit, limitCheckAddr)) |
||||
t.Run("limit 2x max", testKeys(handler, limitCheckKeys, 2*mock.MaxLimit, limitCheckAddr)) |
||||
t.Run("limit negative", testKeys(handler, limitCheckKeys, -10, limitCheckAddr)) |
||||
}) |
||||
|
||||
// this subtest confirms that all nodes are are listed in correct order with expected pagination
|
||||
t.Run("nodes", func(t *testing.T) { |
||||
var allNodes []string |
||||
for addr := range nodeKeys { |
||||
allNodes = append(allNodes, addr) |
||||
} |
||||
sortCaseInsensitive(allNodes) |
||||
|
||||
t.Run("limit 0", testNodes(handler, allNodes, 0, "")) |
||||
t.Run("limit default", testNodes(handler, allNodes, mock.DefaultLimit, "")) |
||||
t.Run("limit 2x default", testNodes(handler, allNodes, 2*mock.DefaultLimit, "")) |
||||
t.Run("limit 0.5x default", testNodes(handler, allNodes, mock.DefaultLimit/2, "")) |
||||
t.Run("limit max", testNodes(handler, allNodes, mock.MaxLimit, "")) |
||||
t.Run("limit 2x max", testNodes(handler, allNodes, 2*mock.MaxLimit, "")) |
||||
t.Run("limit negative", testNodes(handler, allNodes, -10, "")) |
||||
}) |
||||
|
||||
// this subtest confirms that all nodes are are listed that contain a a particular key in correct order
|
||||
// and that for one key different node pagination options are correct
|
||||
t.Run("key nodes", func(t *testing.T) { |
||||
var limitCheckKey string |
||||
|
||||
for key, addrs := range keyNodes { |
||||
testNodes(handler, addrs, 0, key)(t) |
||||
if limitCheckKey == "" { |
||||
limitCheckKey = key |
||||
} |
||||
} |
||||
testNodes(handler, nil, 0, invalidKey)(t) |
||||
|
||||
limitCheckKeys := keyNodes[limitCheckKey] |
||||
t.Run("limit 0", testNodes(handler, limitCheckKeys, 0, limitCheckKey)) |
||||
t.Run("limit default", testNodes(handler, limitCheckKeys, mock.DefaultLimit, limitCheckKey)) |
||||
t.Run("limit 2x default", testNodes(handler, limitCheckKeys, 2*mock.DefaultLimit, limitCheckKey)) |
||||
t.Run("limit 0.5x default", testNodes(handler, limitCheckKeys, mock.DefaultLimit/2, limitCheckKey)) |
||||
t.Run("limit max", testNodes(handler, limitCheckKeys, mock.MaxLimit, limitCheckKey)) |
||||
t.Run("limit 2x max", testNodes(handler, limitCheckKeys, 2*mock.MaxLimit, limitCheckKey)) |
||||
t.Run("limit negative", testNodes(handler, limitCheckKeys, -10, limitCheckKey)) |
||||
}) |
||||
} |
||||
|
||||
// testsKeys returns a test function that validates wantKeys against a series of /api/keys
|
||||
// HTTP responses with provided limit and node options.
|
||||
func testKeys(handler http.Handler, wantKeys []string, limit int, node string) func(t *testing.T) { |
||||
return func(t *testing.T) { |
||||
t.Helper() |
||||
|
||||
wantLimit := limit |
||||
if wantLimit <= 0 { |
||||
wantLimit = mock.DefaultLimit |
||||
} |
||||
if wantLimit > mock.MaxLimit { |
||||
wantLimit = mock.MaxLimit |
||||
} |
||||
wantKeysLen := len(wantKeys) |
||||
var i int |
||||
var startKey string |
||||
for { |
||||
var wantNext string |
||||
start := i * wantLimit |
||||
end := (i + 1) * wantLimit |
||||
if end < wantKeysLen { |
||||
wantNext = wantKeys[end] |
||||
} else { |
||||
end = wantKeysLen |
||||
} |
||||
testKeysResponse(t, handler, node, startKey, limit, KeysResponse{ |
||||
Keys: wantKeys[start:end], |
||||
Next: wantNext, |
||||
}) |
||||
if wantNext == "" { |
||||
break |
||||
} |
||||
startKey = wantNext |
||||
i++ |
||||
} |
||||
} |
||||
} |
||||
|
||||
// testNodes returns a test function that validates wantAddrs against a series of /api/nodes
|
||||
// HTTP responses with provided limit and key options.
|
||||
func testNodes(handler http.Handler, wantAddrs []string, limit int, key string) func(t *testing.T) { |
||||
return func(t *testing.T) { |
||||
t.Helper() |
||||
|
||||
wantLimit := limit |
||||
if wantLimit <= 0 { |
||||
wantLimit = mock.DefaultLimit |
||||
} |
||||
if wantLimit > mock.MaxLimit { |
||||
wantLimit = mock.MaxLimit |
||||
} |
||||
wantAddrsLen := len(wantAddrs) |
||||
var i int |
||||
var startKey string |
||||
for { |
||||
var wantNext string |
||||
start := i * wantLimit |
||||
end := (i + 1) * wantLimit |
||||
if end < wantAddrsLen { |
||||
wantNext = wantAddrs[end] |
||||
} else { |
||||
end = wantAddrsLen |
||||
} |
||||
testNodesResponse(t, handler, key, startKey, limit, NodesResponse{ |
||||
Nodes: wantAddrs[start:end], |
||||
Next: wantNext, |
||||
}) |
||||
if wantNext == "" { |
||||
break |
||||
} |
||||
startKey = wantNext |
||||
i++ |
||||
} |
||||
} |
||||
} |
||||
|
||||
// testStatusResponse validates a response made on url if it matches
|
||||
// the expected StatusResponse.
|
||||
func testStatusResponse(t *testing.T, handler http.Handler, url string, code int) { |
||||
t.Helper() |
||||
|
||||
resp := httpGet(t, handler, url) |
||||
|
||||
if resp.StatusCode != code { |
||||
t.Errorf("got status code %v, want %v", resp.StatusCode, code) |
||||
} |
||||
if got := resp.Header.Get("Content-Type"); got != jsonContentType { |
||||
t.Errorf("got Content-Type header %q, want %q", got, jsonContentType) |
||||
} |
||||
var r 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 validates response returned from handler on /api/keys
|
||||
// with node, start and limit options against KeysResponse.
|
||||
func testKeysResponse(t *testing.T, handler http.Handler, node, start string, limit int, want KeysResponse) { |
||||
t.Helper() |
||||
|
||||
u, err := url.Parse("/api/keys") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
q := u.Query() |
||||
if node != "" { |
||||
q.Set("node", node) |
||||
} |
||||
if start != "" { |
||||
q.Set("start", start) |
||||
} |
||||
if limit != 0 { |
||||
q.Set("limit", strconv.Itoa(limit)) |
||||
} |
||||
u.RawQuery = q.Encode() |
||||
|
||||
resp := httpGet(t, handler, u.String()) |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
t.Errorf("got status code %v, want %v", resp.StatusCode, http.StatusOK) |
||||
} |
||||
if got := resp.Header.Get("Content-Type"); got != jsonContentType { |
||||
t.Errorf("got Content-Type header %q, want %q", got, jsonContentType) |
||||
} |
||||
var r 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) |
||||
} |
||||
} |
||||
|
||||
// testNodesResponse validates response returned from handler on /api/nodes
|
||||
// with key, start and limit options against NodesResponse.
|
||||
func testNodesResponse(t *testing.T, handler http.Handler, key, start string, limit int, want NodesResponse) { |
||||
t.Helper() |
||||
|
||||
u, err := url.Parse("/api/nodes") |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
q := u.Query() |
||||
if key != "" { |
||||
q.Set("key", key) |
||||
} |
||||
if start != "" { |
||||
q.Set("start", start) |
||||
} |
||||
if limit != 0 { |
||||
q.Set("limit", strconv.Itoa(limit)) |
||||
} |
||||
u.RawQuery = q.Encode() |
||||
|
||||
resp := httpGet(t, handler, u.String()) |
||||
|
||||
if resp.StatusCode != http.StatusOK { |
||||
t.Errorf("got status code %v, want %v", resp.StatusCode, http.StatusOK) |
||||
} |
||||
if got := resp.Header.Get("Content-Type"); got != jsonContentType { |
||||
t.Errorf("got Content-Type header %q, want %q", got, jsonContentType) |
||||
} |
||||
var r 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) |
||||
} |
||||
} |
||||
|
||||
// httpGet uses httptest recorder to provide a response on handler's url.
|
||||
func httpGet(t *testing.T, handler http.Handler, url string) (r *http.Response) { |
||||
t.Helper() |
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
w := httptest.NewRecorder() |
||||
handler.ServeHTTP(w, req) |
||||
return w.Result() |
||||
} |
||||
|
||||
// sortCaseInsensitive performs a case insensitive sort on a string slice.
|
||||
func sortCaseInsensitive(s []string) { |
||||
sort.Slice(s, func(i, j int) bool { |
||||
return strings.ToLower(s[i]) < strings.ToLower(s[j]) |
||||
}) |
||||
} |
@ -0,0 +1,163 @@ |
||||
// Copyright 2019 The go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package explorer |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"testing" |
||||
|
||||
"github.com/ethereum/go-ethereum/swarm/storage/mock/mem" |
||||
) |
||||
|
||||
// TestHandler_CORSOrigin validates that the correct Access-Control-Allow-Origin
|
||||
// header is served with various allowed origin settings.
|
||||
func TestHandler_CORSOrigin(t *testing.T) { |
||||
notAllowedOrigin := "http://not-allowed-origin.com/" |
||||
|
||||
for _, tc := range []struct { |
||||
name string |
||||
origins []string |
||||
}{ |
||||
{ |
||||
name: "no origin", |
||||
origins: nil, |
||||
}, |
||||
{ |
||||
name: "single origin", |
||||
origins: []string{"http://localhost/"}, |
||||
}, |
||||
{ |
||||
name: "multiple origins", |
||||
origins: []string{"http://localhost/", "http://ethereum.org/"}, |
||||
}, |
||||
} { |
||||
t.Run(tc.name, func(t *testing.T) { |
||||
handler := NewHandler(mem.NewGlobalStore(), tc.origins) |
||||
|
||||
origins := tc.origins |
||||
if origins == nil { |
||||
// handle the "no origin" test case
|
||||
origins = []string{""} |
||||
} |
||||
|
||||
for _, origin := range origins { |
||||
t.Run(fmt.Sprintf("get %q", origin), newTestCORSOrigin(handler, origin, origin)) |
||||
t.Run(fmt.Sprintf("preflight %q", origin), newTestCORSPreflight(handler, origin, origin)) |
||||
} |
||||
|
||||
t.Run(fmt.Sprintf("get %q", notAllowedOrigin), newTestCORSOrigin(handler, notAllowedOrigin, "")) |
||||
t.Run(fmt.Sprintf("preflight %q", notAllowedOrigin), newTestCORSPreflight(handler, notAllowedOrigin, "")) |
||||
}) |
||||
} |
||||
|
||||
t.Run("wildcard", func(t *testing.T) { |
||||
handler := NewHandler(mem.NewGlobalStore(), []string{"*"}) |
||||
|
||||
for _, origin := range []string{ |
||||
"http://example.com/", |
||||
"http://ethereum.org", |
||||
"http://localhost", |
||||
} { |
||||
t.Run(fmt.Sprintf("get %q", origin), newTestCORSOrigin(handler, origin, origin)) |
||||
t.Run(fmt.Sprintf("preflight %q", origin), newTestCORSPreflight(handler, origin, origin)) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// newTestCORSOrigin returns a test function that validates if wantOrigin CORS header is
|
||||
// served by the handler for a GET request.
|
||||
func newTestCORSOrigin(handler http.Handler, origin, wantOrigin string) func(t *testing.T) { |
||||
return func(t *testing.T) { |
||||
req, err := http.NewRequest(http.MethodGet, "/", nil) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
req.Header.Set("Origin", origin) |
||||
|
||||
w := httptest.NewRecorder() |
||||
handler.ServeHTTP(w, req) |
||||
resp := w.Result() |
||||
|
||||
header := resp.Header.Get("Access-Control-Allow-Origin") |
||||
if header != wantOrigin { |
||||
t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, wantOrigin) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// newTestCORSPreflight returns a test function that validates if wantOrigin CORS header is
|
||||
// served by the handler for an OPTIONS CORS preflight request.
|
||||
func newTestCORSPreflight(handler http.Handler, origin, wantOrigin string) func(t *testing.T) { |
||||
return func(t *testing.T) { |
||||
req, err := http.NewRequest(http.MethodOptions, "/", nil) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
req.Header.Set("Origin", origin) |
||||
req.Header.Set("Access-Control-Request-Method", "GET") |
||||
|
||||
w := httptest.NewRecorder() |
||||
handler.ServeHTTP(w, req) |
||||
resp := w.Result() |
||||
|
||||
header := resp.Header.Get("Access-Control-Allow-Origin") |
||||
if header != wantOrigin { |
||||
t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, wantOrigin) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// TestHandler_noCacheHeaders validates that no cache headers are server.
|
||||
func TestHandler_noCacheHeaders(t *testing.T) { |
||||
handler := NewHandler(mem.NewGlobalStore(), nil) |
||||
|
||||
for _, tc := range []struct { |
||||
url string |
||||
}{ |
||||
{ |
||||
url: "/", |
||||
}, |
||||
{ |
||||
url: "/api/nodes", |
||||
}, |
||||
{ |
||||
url: "/api/keys", |
||||
}, |
||||
} { |
||||
req, err := http.NewRequest(http.MethodGet, tc.url, nil) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
w := httptest.NewRecorder() |
||||
handler.ServeHTTP(w, req) |
||||
resp := w.Result() |
||||
|
||||
for header, want := range map[string]string{ |
||||
"Cache-Control": "no-cache, no-store, must-revalidate", |
||||
"Pragma": "no-cache", |
||||
"Expires": "0", |
||||
} { |
||||
got := resp.Header.Get(header) |
||||
if got != want { |
||||
t.Errorf("got %q header %q for url %q, want %q", header, tc.url, got, want) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,176 @@ |
||||
swagger: '2.0' |
||||
info: |
||||
title: Swarm Global Store API |
||||
version: 0.1.0 |
||||
tags: |
||||
- name: Has Key |
||||
description: Checks if a Key is stored on a Node |
||||
- name: Keys |
||||
description: Lists Keys |
||||
- name: Nodes |
||||
description: Lists Node addresses |
||||
|
||||
paths: |
||||
'/api/has-key/{node}/{key}': |
||||
get: |
||||
tags: |
||||
- Has Key |
||||
summary: Checks if a Key is stored on a Node |
||||
operationId: hasKey |
||||
produces: |
||||
- application/json |
||||
|
||||
parameters: |
||||
- name: node |
||||
in: path |
||||
required: true |
||||
type: string |
||||
format: hex-endoded |
||||
description: Node address. |
||||
|
||||
- name: key |
||||
in: path |
||||
required: true |
||||
type: string |
||||
format: hex-endoded |
||||
description: Key. |
||||
|
||||
responses: |
||||
'200': |
||||
description: Key is stored on Node |
||||
schema: |
||||
$ref: '#/definitions/Status' |
||||
'404': |
||||
description: Key is not stored on Node |
||||
schema: |
||||
$ref: '#/definitions/Status' |
||||
'500': |
||||
description: Internal Server Error |
||||
schema: |
||||
$ref: '#/definitions/Status' |
||||
|
||||
'/api/keys': |
||||
get: |
||||
tags: |
||||
- Keys |
||||
summary: Lists Keys |
||||
operationId: keys |
||||
produces: |
||||
- application/json |
||||
|
||||
parameters: |
||||
- name: start |
||||
in: query |
||||
required: false |
||||
type: string |
||||
format: hex-encoded Key |
||||
description: A Key as the starting point for the returned list. It is usually a value from the returned "next" field in the Keys repsonse. |
||||
|
||||
- name: limit |
||||
in: query |
||||
required: false |
||||
type: integer |
||||
default: 100 |
||||
minimum: 1 |
||||
maximum: 1000 |
||||
description: Limits the number of Keys returned in on response. |
||||
|
||||
- name: node |
||||
in: query |
||||
required: false |
||||
type: string |
||||
format: hex-encoded Node address |
||||
description: If this parameter is provided, only Keys that are stored on this Node be returned in the response. If not, all known Keys will be returned. |
||||
|
||||
responses: |
||||
'200': |
||||
description: List of Keys |
||||
schema: |
||||
$ref: '#/definitions/Keys' |
||||
'500': |
||||
description: Internal Server Error |
||||
schema: |
||||
$ref: '#/definitions/Status' |
||||
|
||||
'/api/nodes': |
||||
get: |
||||
tags: |
||||
- Nodes |
||||
summary: Lists Node addresses |
||||
operationId: nodes |
||||
produces: |
||||
- application/json |
||||
|
||||
parameters: |
||||
- name: start |
||||
in: query |
||||
required: false |
||||
type: string |
||||
format: hex-encoded Node address |
||||
description: A Node address as the starting point for the returned list. It is usually a value from the returned "next" field in the Nodes repsonse. |
||||
|
||||
- name: limit |
||||
in: query |
||||
required: false |
||||
type: integer |
||||
default: 100 |
||||
minimum: 1 |
||||
maximum: 1000 |
||||
description: Limits the number of Node addresses returned in on response. |
||||
|
||||
- name: key |
||||
in: query |
||||
required: false |
||||
type: string |
||||
format: hex-encoded Key |
||||
description: If this parameter is provided, only addresses of Nodes that store this Key will be returned in the response. If not, all known Node addresses will be returned. |
||||
|
||||
responses: |
||||
'200': |
||||
description: List of Node addresses |
||||
schema: |
||||
$ref: '#/definitions/Nodes' |
||||
'500': |
||||
description: Internal Server Error |
||||
schema: |
||||
$ref: '#/definitions/Status' |
||||
|
||||
definitions: |
||||
|
||||
Status: |
||||
type: object |
||||
properties: |
||||
message: |
||||
type: string |
||||
description: HTTP Status Code name. |
||||
code: |
||||
type: integer |
||||
description: HTTP Status Code. |
||||
|
||||
Keys: |
||||
type: object |
||||
properties: |
||||
keys: |
||||
type: array |
||||
description: A list of Keys. |
||||
items: |
||||
type: string |
||||
format: hex-encoded Key |
||||
next: |
||||
type: string |
||||
format: hex-encoded Key |
||||
description: If present, the next Key in listing. Can be passed as "start" query parameter to continue the listing. If not present, the end of the listing is reached. |
||||
|
||||
Nodes: |
||||
type: object |
||||
properties: |
||||
nodes: |
||||
type: array |
||||
description: A list of Node addresses. |
||||
items: |
||||
type: string |
||||
format: hex-encoded Node address |
||||
next: |
||||
type: string |
||||
format: hex-encoded Node address |
||||
description: If present, the next Node address in listing. Can be passed as "start" query parameter to continue the listing. If not present, the end of the listing is reached. |
Loading…
Reference in new issue