1// Copyright 2018 The Go Cloud Development Kit Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package filevar
16
17import (
18	"context"
19	"errors"
20	"io/ioutil"
21	"net/url"
22	"os"
23	"path/filepath"
24	"strings"
25	"testing"
26	"time"
27
28	"github.com/google/go-cmp/cmp"
29	"gocloud.dev/runtimevar"
30	"gocloud.dev/runtimevar/driver"
31	"gocloud.dev/runtimevar/drivertest"
32	"gocloud.dev/secrets"
33	_ "gocloud.dev/secrets/localsecrets"
34)
35
36type harness struct {
37	dir    string
38	closer func()
39}
40
41func newHarness(t *testing.T) (drivertest.Harness, error) {
42	dir, err := ioutil.TempDir("", "filevar_test-")
43	if err != nil {
44		return nil, err
45	}
46	return &harness{
47		dir:    dir,
48		closer: func() { _ = os.RemoveAll(dir) },
49	}, nil
50}
51
52func (h *harness) MakeWatcher(ctx context.Context, name string, decoder *runtimevar.Decoder) (driver.Watcher, error) {
53	// filevar uses a goroutine in the background that poll every WaitDuration if
54	// the file is deleted. Make this fast for tests.
55	return newWatcher(filepath.Join(h.dir, name), decoder, &Options{WaitDuration: 1 * time.Millisecond})
56}
57
58func (h *harness) CreateVariable(ctx context.Context, name string, val []byte) error {
59	// Write to a temporary file and rename; otherwise,
60	// Watch can read an empty file during the write.
61	tmp, err := ioutil.TempFile(h.dir, "tmp")
62	if err != nil {
63		return err
64	}
65	if _, err := tmp.Write(val); err != nil {
66		tmp.Close()
67		return err
68	}
69	tmp.Close()
70	return os.Rename(tmp.Name(), filepath.Join(h.dir, name))
71}
72
73func (h *harness) UpdateVariable(ctx context.Context, name string, val []byte) error {
74	return h.CreateVariable(ctx, name, val)
75}
76
77func (h *harness) DeleteVariable(ctx context.Context, name string) error {
78	path := filepath.Join(h.dir, name)
79	return os.Remove(path)
80}
81
82func (h *harness) Close() {
83	h.closer()
84}
85
86func (h *harness) Mutable() bool { return true }
87
88func TestConformance(t *testing.T) {
89	drivertest.RunConformanceTests(t, newHarness, []drivertest.AsTest{verifyAs{}})
90}
91
92type verifyAs struct{}
93
94func (verifyAs) Name() string {
95	return "verify As"
96}
97
98func (verifyAs) SnapshotCheck(s *runtimevar.Snapshot) error {
99	var ss string
100	if s.As(&ss) {
101		return errors.New("Snapshot.As expected to fail")
102	}
103	return nil
104}
105
106func (verifyAs) ErrorCheck(v *runtimevar.Variable, err error) error {
107	var ss string
108	if v.ErrorAs(err, &ss) {
109		return errors.New("runtimevar.ErrorAs expected to fail")
110	}
111	return nil
112}
113
114// Filevar-specific tests.
115
116func TestOpenVariable(t *testing.T) {
117	dir, err := ioutil.TempDir("", "filevar_test-")
118	if err != nil {
119		t.Fatal(err)
120	}
121
122	tests := []struct {
123		description string
124		path        string
125		decoder     *runtimevar.Decoder
126		want        string
127		wantErr     bool
128	}{
129		{
130			description: "empty path results in error",
131			decoder:     runtimevar.StringDecoder,
132			wantErr:     true,
133		},
134		{
135			description: "empty decoder results in error",
136			path:        filepath.Join(dir, "foo.txt"),
137			wantErr:     true,
138		},
139		{
140			description: "basic path works",
141			path:        filepath.Join(dir, "foo.txt"),
142			decoder:     runtimevar.StringDecoder,
143			want:        filepath.Join(dir, "foo.txt"),
144		},
145		{
146			description: "path with extra relative dirs works and is cleaned up",
147			path:        filepath.Join(dir, "bar/../foo.txt"),
148			decoder:     runtimevar.StringDecoder,
149			want:        filepath.Join(dir, "foo.txt"),
150		},
151	}
152
153	for _, test := range tests {
154		t.Run(test.description, func(t *testing.T) {
155			// Create driver impl.
156			drv, err := newWatcher(test.path, test.decoder, nil)
157			if (err != nil) != test.wantErr {
158				t.Errorf("got err %v want error %v", err, test.wantErr)
159			}
160			if drv != nil {
161				if drv.path != test.want {
162					t.Errorf("got %q want %q", drv.path, test.want)
163				}
164				drv.Close()
165			}
166
167			// Create portable type.
168			w, err := OpenVariable(test.path, test.decoder, nil)
169			if (err != nil) != test.wantErr {
170				t.Errorf("got err %v want error %v", err, test.wantErr)
171			}
172			if w != nil {
173				w.Close()
174			}
175		})
176	}
177}
178
179func TestOpenVariableURL(t *testing.T) {
180	dir, err := ioutil.TempDir("", "gcdk-filevar-example")
181	if err != nil {
182		t.Fatal(err)
183	}
184	defer os.RemoveAll(dir)
185
186	jsonPath := filepath.Join(dir, "myvar.json")
187	if err := ioutil.WriteFile(jsonPath, []byte(`{"Foo": "Bar"}`), 0666); err != nil {
188		t.Fatal(err)
189	}
190	txtPath := filepath.Join(dir, "myvar.txt")
191	if err := ioutil.WriteFile(txtPath, []byte("hello world!"), 0666); err != nil {
192		t.Fatal(err)
193	}
194	nonexistentPath := filepath.Join(dir, "filenotfound")
195	ctx := context.Background()
196	secretsPath := filepath.Join(dir, "mysecret.txt")
197	cleanup, err := setupTestSecrets(ctx, dir, secretsPath)
198	if err != nil {
199		t.Fatal(err)
200	}
201	defer cleanup()
202
203	// Convert paths to a URL path, adding a leading "/" if needed on Windows
204	// (on Unix, dirpath already has a leading "/").
205	jsonPath = filepath.ToSlash(jsonPath)
206	txtPath = filepath.ToSlash(txtPath)
207	nonexistentPath = filepath.ToSlash(nonexistentPath)
208	secretsPath = filepath.ToSlash(secretsPath)
209	if os.PathSeparator != '/' {
210		if !strings.HasPrefix(jsonPath, "/") {
211			jsonPath = "/" + jsonPath
212		}
213		if !strings.HasPrefix(txtPath, "/") {
214			txtPath = "/" + txtPath
215		}
216		if !strings.HasPrefix(nonexistentPath, "/") {
217			nonexistentPath = "/" + nonexistentPath
218		}
219		if !strings.HasPrefix(secretsPath, "/") {
220			secretsPath = "/" + secretsPath
221		}
222	}
223
224	tests := []struct {
225		URL          string
226		WantErr      bool
227		WantWatchErr bool
228		Want         interface{}
229	}{
230		// Variable construction succeeds, but the file does not exist.
231		{"file://" + nonexistentPath, false, true, nil},
232		// Variable construction fails due to invalid decoder arg.
233		{"file://" + txtPath + "?decoder=notadecoder", true, false, nil},
234		// Variable construction fails due to invalid arg.
235		{"file://" + txtPath + "?param=value", true, false, nil},
236		// Working example with default decoder.
237		{"file://" + txtPath, false, false, []byte("hello world!")},
238		// Working example with string decoder.
239		{"file://" + txtPath + "?decoder=string", false, false, "hello world!"},
240		// Working example with JSON decoder.
241		{"file://" + jsonPath + "?decoder=jsonmap", false, false, &map[string]interface{}{"Foo": "Bar"}},
242		// Working example with decrypt (default) decoder.
243		{"file://" + secretsPath + "?decoder=decrypt", false, false, []byte(`{"Foo":"Bar"}`)},
244		// Working example with decrypt+bytes decoder.
245		{"file://" + secretsPath + "?decoder=decrypt+bytes", false, false, []byte(`{"Foo":"Bar"}`)},
246		// Working example with decrypt+json decoder.
247		{"file://" + secretsPath + "?decoder=decrypt+jsonmap", false, false, &map[string]interface{}{"Foo": "Bar"}},
248		// Working example with escaped decrypt+json decoder
249		{"file://" + secretsPath + "?decoder=" + url.QueryEscape("decrypt+jsonmap"), false, false, &map[string]interface{}{"Foo": "Bar"}},
250	}
251
252	for _, test := range tests {
253		t.Run(test.URL, func(t *testing.T) {
254			v, err := runtimevar.OpenVariable(ctx, test.URL)
255			if (err != nil) != test.WantErr {
256				t.Errorf("%s: got error %v, want error %v", test.URL, err, test.WantErr)
257			}
258			if err != nil {
259				return
260			}
261			defer v.Close()
262			snapshot, err := v.Watch(ctx)
263			if (err != nil) != test.WantWatchErr {
264				t.Errorf("%s: got Watch error %v, want error %v", test.URL, err, test.WantWatchErr)
265			}
266			if err != nil {
267				return
268			}
269			if !cmp.Equal(snapshot.Value, test.Want) {
270				t.Errorf("%s: got snapshot value\n%v\n  want\n%v", test.URL, snapshot.Value, test.Want)
271			}
272		})
273	}
274}
275
276func setupTestSecrets(ctx context.Context, dir, secretsPath string) (func(), error) {
277	const keeperEnv = "RUNTIMEVAR_KEEPER_URL"
278	const keeperURL = "base64key://smGbjm71Nxd1Ig5FS0wj9SlbzAIrnolCz9bQQ6uAhl4="
279	oldURL := os.Getenv(keeperEnv)
280	os.Setenv(keeperEnv, keeperURL)
281	cleanup := func() { os.Setenv(keeperEnv, oldURL) }
282
283	k, err := secrets.OpenKeeper(ctx, keeperURL)
284	if err != nil {
285		return cleanup, err
286	}
287	sc, err := k.Encrypt(ctx, []byte(`{"Foo":"Bar"}`))
288	if err != nil {
289		return cleanup, err
290	}
291	if err := ioutil.WriteFile(secretsPath, sc, 0666); err != nil {
292		return cleanup, err
293	}
294	return cleanup, nil
295}
296