1// Copyright (C) 2019 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package trust_test
5
6import (
7	"bytes"
8	"context"
9	"errors"
10	"os"
11	"path/filepath"
12	"regexp"
13	"testing"
14
15	"github.com/stretchr/testify/require"
16	"go.uber.org/zap"
17	"go.uber.org/zap/zaptest"
18
19	"storj.io/common/storj"
20	"storj.io/common/testcontext"
21	"storj.io/storj/storagenode/trust"
22)
23
24func TestNewList(t *testing.T) {
25	ctx := testcontext.New(t)
26	defer ctx.Cleanup()
27
28	log := zaptest.NewLogger(t)
29
30	cache := newTestCache(t, ctx.Dir(), nil)
31
32	for _, tt := range []struct {
33		name  string
34		log   *zap.Logger
35		cache *trust.Cache
36		err   string
37	}{
38		{
39			name:  "missing logger",
40			cache: cache,
41			err:   "trust: logger cannot be nil",
42		},
43		{
44			name: "missing cache",
45			log:  log,
46			err:  "trust: cache cannot be nil",
47		},
48		{
49			name:  "success",
50			log:   log,
51			cache: cache,
52		},
53	} {
54		tt := tt // quiet linting
55		t.Run(tt.name, func(t *testing.T) {
56			list, err := trust.NewList(tt.log, nil, nil, tt.cache)
57			if tt.err != "" {
58				require.EqualError(t, err, tt.err)
59				require.Nil(t, list)
60				return
61			}
62			require.NoError(t, err)
63			require.NotNil(t, list)
64		})
65	}
66}
67
68func TestListAgainstSpec(t *testing.T) {
69	ctx := testcontext.New(t)
70	defer ctx.Cleanup()
71
72	idReplacer := regexp.MustCompile(`^(\d)(@.*)$`)
73	fixURL := func(s string) string {
74		m := idReplacer.FindStringSubmatch(s)
75		if m == nil {
76			return s
77		}
78		return makeTestID(m[1][0]).String() + m[2]
79	}
80
81	makeNodeURL := func(s string) storj.NodeURL {
82		u, err := storj.ParseNodeURL(fixURL(s))
83		require.NoError(t, err)
84		return u
85	}
86
87	makeSatelliteURL := func(s string) trust.SatelliteURL {
88		u, err := trust.ParseSatelliteURL(fixURL(s))
89		require.NoError(t, err)
90		return u
91	}
92
93	makeEntry := func(s string, authoritative bool) trust.Entry {
94		return trust.Entry{
95			SatelliteURL:  makeSatelliteURL(s),
96			Authoritative: authoritative,
97		}
98	}
99
100	fileSource := &fakeSource{
101		name:   "file:///path/to/some/trusted-satellites.txt",
102		static: true,
103		entries: []trust.Entry{
104			makeEntry("1@bar.test:7777", true),
105		},
106	}
107
108	fooSource := &fakeSource{
109		name:   "https://foo.test/trusted-satellites",
110		static: false,
111		entries: []trust.Entry{
112			makeEntry("2@f.foo.test:7777", true),
113			makeEntry("2@buz.test:7777", false),
114			makeEntry("2@qiz.test:7777", false),
115			makeEntry("5@ohno.test:7777", false),
116		},
117	}
118
119	barSource := &fakeSource{
120		name:   "https://bar.test/trusted-satellites",
121		static: false,
122		entries: []trust.Entry{
123			makeEntry("3@f.foo.test:7777", false),
124			makeEntry("3@bar.test:7777", true),
125			makeEntry("3@baz.test:7777", false),
126			makeEntry("3@buz.test:7777", false),
127			makeEntry("3@quz.test:7777", false),
128		},
129	}
130
131	bazSource := &fakeSource{
132		name:   "https://baz.test/trusted-satellites",
133		static: false,
134		entries: []trust.Entry{
135			makeEntry("4@baz.test:7777", true),
136			makeEntry("4@qiz.test:7777", false),
137			makeEntry("4@subdomain.quz.test:7777", false),
138		},
139	}
140
141	fixedSource := &fakeSource{
142		name:   "0@f.foo.test:7777",
143		static: true,
144		entries: []trust.Entry{
145			makeEntry("0@f.foo.test:7777", true),
146		},
147	}
148
149	rules := trust.Rules{
150		trust.NewHostExcluder("quz.test"),
151		trust.NewURLExcluder(makeSatelliteURL("2@qiz.test:7777")),
152		trust.NewIDExcluder(makeTestID('5')),
153	}
154
155	cache := newTestCache(t, ctx.Dir(), nil)
156
157	sources := []trust.Source{
158		fileSource,
159		fooSource,
160		barSource,
161		bazSource,
162		fixedSource,
163	}
164
165	log := zaptest.NewLogger(t)
166	list, err := trust.NewList(log, sources, rules, cache)
167	require.NoError(t, err)
168
169	urls, err := list.FetchURLs(context.Background())
170	require.NoError(t, err)
171
172	t.Logf("0@ = %s", makeTestID('0'))
173	t.Logf("1@ = %s", makeTestID('1'))
174	t.Logf("2@ = %s", makeTestID('2'))
175	t.Logf("3@ = %s", makeTestID('3'))
176	t.Logf("4@ = %s", makeTestID('4'))
177	t.Logf("5@ = %s", makeTestID('5'))
178
179	require.Equal(t, []storj.NodeURL{
180		makeNodeURL("1@bar.test:7777"),
181		makeNodeURL("2@f.foo.test:7777"),
182		makeNodeURL("2@buz.test:7777"),
183		makeNodeURL("4@baz.test:7777"),
184		makeNodeURL("4@qiz.test:7777"),
185	}, urls)
186}
187
188func TestListCacheInteraction(t *testing.T) {
189	entry1 := trust.Entry{
190		SatelliteURL: trust.SatelliteURL{
191			Host: "host1",
192			Port: 7777,
193		},
194	}
195	url1 := entry1.SatelliteURL.NodeURL()
196
197	entry2 := trust.Entry{
198		SatelliteURL: trust.SatelliteURL{
199			Host: "host2",
200			Port: 7777,
201		},
202	}
203	url2 := entry2.SatelliteURL.NodeURL()
204
205	makeNormal := func(entries ...trust.Entry) *fakeSource {
206		return &fakeSource{
207			name:    "normal",
208			static:  false,
209			entries: entries,
210		}
211	}
212
213	makeFixed := func(entries ...trust.Entry) *fakeSource {
214		return &fakeSource{
215			name:    "static",
216			static:  true,
217			entries: entries,
218		}
219	}
220
221	badNormal := &fakeSource{
222		name:   "normal",
223		static: false,
224		err:    errors.New("ohno"),
225	}
226
227	badFixed := &fakeSource{
228		name:   "static",
229		static: true,
230		err:    errors.New("ohno"),
231	}
232
233	for _, tt := range []struct {
234		name           string
235		sources        []trust.Source
236		cacheBefore    map[string][]trust.Entry
237		cacheAfter     map[string][]trust.Entry
238		urls           []storj.NodeURL
239		killCacheEarly bool
240		err            string
241	}{
242		{
243			name:    "entries are cached for normal sources",
244			sources: []trust.Source{makeNormal(entry1)},
245			urls:    []storj.NodeURL{url1},
246			cacheAfter: map[string][]trust.Entry{
247				"normal": {entry1},
248			},
249		},
250		{
251			name:       "entries are not cached for static sources",
252			sources:    []trust.Source{makeFixed(entry1)},
253			urls:       []storj.NodeURL{url1},
254			cacheAfter: map[string][]trust.Entry{},
255		},
256		{
257			name:    "entries are updated on success for normal sources",
258			sources: []trust.Source{makeNormal(entry2)},
259			cacheBefore: map[string][]trust.Entry{
260				"normal": {entry1},
261			},
262			urls: []storj.NodeURL{url2},
263			cacheAfter: map[string][]trust.Entry{
264				"normal": {entry2},
265			},
266		},
267		{
268			name:    "fetch fails if no cached entry on failure for normal source",
269			sources: []trust.Source{badNormal},
270			err:     `trust: failed to fetch from source "normal": ohno`,
271		},
272		{
273			name:    "cached entries are used on failure for normal sources",
274			sources: []trust.Source{badNormal},
275			cacheBefore: map[string][]trust.Entry{
276				"normal": {entry1},
277			},
278			urls: []storj.NodeURL{url1},
279			cacheAfter: map[string][]trust.Entry{
280				"normal": {entry1},
281			},
282		},
283		{
284			name:    "fetch fails on failure for static source",
285			sources: []trust.Source{badFixed},
286			err:     `trust: failed to fetch from source "static": ohno`,
287		},
288		{
289			name:           "failure to save cache is not fatal",
290			sources:        []trust.Source{makeNormal(entry1)},
291			urls:           []storj.NodeURL{url1},
292			killCacheEarly: true,
293		},
294	} {
295		tt := tt // quiet linting
296		t.Run(tt.name, func(t *testing.T) {
297			ctx := testcontext.New(t)
298			defer ctx.Cleanup()
299
300			cache := newTestCache(t, ctx.Dir(), tt.cacheBefore)
301
302			log := zaptest.NewLogger(t)
303			list, err := trust.NewList(log, tt.sources, nil, cache)
304			require.NoError(t, err)
305
306			if tt.killCacheEarly {
307				require.NoError(t, os.Remove(cache.Path()))
308			}
309
310			urls, err := list.FetchURLs(context.Background())
311			if tt.err != "" {
312				require.EqualError(t, err, tt.err)
313				return
314			}
315			require.NoError(t, err)
316			require.Equal(t, tt.urls, urls)
317
318			if !tt.killCacheEarly {
319				cacheAfter, err := trust.LoadCacheData(cache.Path())
320				require.NoError(t, err)
321				require.Equal(t, &trust.CacheData{Entries: tt.cacheAfter}, cacheAfter)
322			}
323		})
324	}
325}
326
327func newTestCache(t *testing.T, dir string, entries map[string][]trust.Entry) *trust.Cache {
328	cachePath := filepath.Join(dir, "cache.json")
329
330	err := trust.SaveCacheData(cachePath, &trust.CacheData{
331		Entries: entries,
332	})
333	require.NoError(t, err)
334
335	cache, err := trust.LoadCache(cachePath)
336	require.NoError(t, err)
337
338	return cache
339}
340
341type fakeSource struct {
342	name    string
343	static  bool
344	entries []trust.Entry
345	err     error
346}
347
348func (s *fakeSource) String() string {
349	return s.name
350}
351
352func (s *fakeSource) Static() bool {
353	return s.static
354}
355
356func (s *fakeSource) FetchEntries(context.Context) ([]trust.Entry, error) {
357	return s.entries, s.err
358}
359
360func makeTestID(x byte) storj.NodeID {
361	var id storj.NodeID
362	copy(id[:], bytes.Repeat([]byte{x}, len(id)))
363	return storj.NewVersionedID(id, storj.IDVersions[storj.V0])
364}
365