diff --git a/swarm/storage/mock/explorer/explorer_test.go b/swarm/storage/mock/explorer/explorer_test.go
new file mode 100644
index 0000000000..3f7d334ac0
--- /dev/null
+++ b/swarm/storage/mock/explorer/explorer_test.go
@@ -0,0 +1,463 @@
+// 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 .
+
+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 := "0x0000000000000000000000000000000000000000"
+ // a key that is not present in global store
+ invalidKey := "0000000000000000"
+
+ 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)
+
+ 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)
+ })
+
+ 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, ""))
+ })
+
+ 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))
+ })
+
+ 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, ""))
+ })
+
+ 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])
+ })
+}
diff --git a/swarm/storage/mock/explorer/headers_test.go b/swarm/storage/mock/explorer/headers_test.go
new file mode 100644
index 0000000000..0f9ff06ad3
--- /dev/null
+++ b/swarm/storage/mock/explorer/headers_test.go
@@ -0,0 +1,171 @@
+// 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 .
+
+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), 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 != origin {
+ t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, origin)
+ }
+ })
+
+ t.Run(fmt.Sprintf("preflight %q", origin), 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 != origin {
+ t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, origin)
+ }
+ })
+ }
+
+ t.Run(fmt.Sprintf("get %q", notAllowedOrigin), func(t *testing.T) {
+ req, err := http.NewRequest(http.MethodGet, "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ req.Header.Set("Origin", notAllowedOrigin)
+
+ w := httptest.NewRecorder()
+ handler.ServeHTTP(w, req)
+ resp := w.Result()
+
+ header := resp.Header.Get("Access-Control-Allow-Origin")
+ if header != "" {
+ t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, "")
+ }
+ })
+
+ t.Run(fmt.Sprintf("preflight %q", notAllowedOrigin), func(t *testing.T) {
+ req, err := http.NewRequest(http.MethodOptions, "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ req.Header.Set("Origin", notAllowedOrigin)
+ 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 != "" {
+ t.Errorf("got Access-Control-Allow-Origin header %q, want %q", header, "")
+ }
+ })
+ })
+ }
+}
+
+// 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)
+ }
+ }
+ }
+}