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