github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/config/flag_test.go (about) 1 // Copyright 2019 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache 2.0 3 // license that can be found in the LICENSE file. 4 5 package config 6 7 import ( 8 "errors" 9 "flag" 10 "fmt" 11 "os" 12 "runtime" 13 "strconv" 14 "testing" 15 16 "github.com/Schaudge/grailbase/must" 17 ) 18 19 // This test uses a mock "app" to demonstrate various aspects of package config. 20 21 type credentials interface { 22 Creds() string 23 } 24 25 type userCredentials string 26 27 func (u userCredentials) Creds() string { return string(u) } 28 29 type envCredentials struct{} 30 31 func (e envCredentials) Creds() string { return "environment" } 32 33 type database struct { 34 table string 35 creds credentials 36 } 37 38 type frontend struct { 39 db database 40 creds credentials 41 limit int 42 } 43 44 func init() { 45 Register("app/auth/env", func(constr *Constructor[envCredentials]) { 46 constr.New = func() (envCredentials, error) { 47 return envCredentials{}, nil 48 } 49 }) 50 Register("app/auth/login", func(constr *Constructor[userCredentials]) { 51 var ( 52 username = constr.String("user", "test", "the username") 53 password = constr.String("password", "secret", "the password") 54 ) 55 constr.New = func() (userCredentials, error) { 56 return userCredentials(fmt.Sprintf("%s:%s", *username, *password)), nil 57 } 58 }) 59 60 Register("app/database", func(constr *Constructor[database]) { 61 var db database 62 constr.StringVar(&db.table, "table", "defaulttable", "the database table") 63 constr.InstanceVar(&db.creds, "credentials", "app/auth/env", "credentials used for database access") 64 constr.New = func() (database, error) { 65 if db.creds == nil { 66 return database{}, errors.New("credentials not defined") 67 } 68 return db, nil 69 } 70 }) 71 72 Register("app/frontend", func(constr *Constructor[frontend]) { 73 var fe frontend 74 constr.InstanceVar(&fe.db, "database", "app/database", "the database to be used") 75 constr.InstanceVar(&fe.creds, "credentials", "app/auth/env", "credentials to use for authentication") 76 constr.IntVar(&fe.limit, "limit", 128, "maximum number of concurrent requests to handle") 77 constr.New = func() (frontend, error) { 78 if fe.db == (database{}) || fe.creds == nil { 79 return frontend{}, errors.New("missing configuration") 80 } 81 return fe, nil 82 } 83 }) 84 } 85 86 func TestFlag(t *testing.T) { 87 profile := func(args ...string) *Profile { 88 t.Helper() 89 p := New() 90 f, err := os.Open("testdata/profile") 91 must.Nil(err) 92 defer f.Close() 93 if err := p.Parse(f); err != nil { 94 t.Fatal(err) 95 } 96 fs := flag.NewFlagSet("test", flag.PanicOnError) 97 p.RegisterFlags(fs, "", "testdata/profile") 98 if err := fs.Parse(args); err != nil { 99 t.Fatal(err) 100 } 101 if err := p.ProcessFlags(); err != nil { 102 t.Fatal(err) 103 } 104 return p 105 } 106 107 for _, test := range []struct { 108 line int 109 args []string 110 wantFE, wantDB string 111 }{ 112 { 113 callerLine(), 114 nil, 115 "marius:supersecret", "marius:supersecret", 116 }, 117 { 118 callerLine(), 119 []string{"-set", "app/auth/login.password=public"}, 120 "marius:public", "marius:public", 121 }, 122 { 123 callerLine(), 124 []string{"-set", "app/frontend.credentials=app/auth/env"}, 125 "environment", "marius:supersecret", 126 }, 127 { 128 callerLine(), 129 []string{"-profileinline", `param app/auth/login password = "public"`}, 130 "marius:public", "marius:public", 131 }, 132 { 133 callerLine(), 134 []string{ 135 "-set", "app/auth/login.password=public", 136 "-profile", "testdata/profile", 137 }, 138 // Parameter settings in profile file should override, since they come later. 139 "marius:supersecret", "marius:supersecret", 140 }, 141 { 142 callerLine(), 143 []string{ 144 "-set", "app/auth/login.password=public", 145 "-profile", "testdata/profile", 146 "-set", "app/auth/login.password=hunter2", 147 }, 148 "marius:hunter2", "marius:hunter2", 149 }, 150 { 151 callerLine(), 152 []string{ 153 "-set", "app/auth/login.password=public", 154 "-profile", "testdata/profile", 155 "-set", "app/auth/login.password=hunter2", 156 "-profileinline", ` 157 instance test/felogin app/auth/login ( 158 user = "tester" 159 ) 160 param app/frontend credentials = test/felogin 161 `, 162 }, 163 "tester:hunter2", "marius:hunter2", 164 }, 165 { 166 callerLine(), 167 []string{ 168 "-set", "app/auth/login.password=public", 169 "-profile", "testdata/profile", 170 "-set", "app/auth/login.password=hunter2", 171 "-profileinline", ` 172 instance test/felogin app/auth/login ( 173 user = "tester" 174 ) 175 param app/frontend credentials = test/felogin 176 param test/felogin password = "abc" 177 `, 178 }, 179 "tester:abc", "marius:hunter2", 180 }, 181 { 182 callerLine(), 183 []string{ 184 "-set", "app/auth/login.password=public", 185 "-profile", "testdata/profile", 186 "-set", "app/auth/login.password=hunter2", 187 "-profileinline", ` 188 instance test/felogin app/auth/login ( 189 user = "tester" 190 ) 191 param app/frontend credentials = test/felogin 192 `, 193 "-profile", "testdata/profile_felogin_password", 194 }, 195 "tester:abc", "marius:hunter2", 196 }, 197 } { 198 t.Run(strconv.Itoa(test.line), func(t *testing.T) { 199 p := profile(test.args...) 200 var fe frontend 201 if err := p.Instance("app/frontend", &fe); err != nil { 202 t.Fatal(err) 203 } 204 if got, want := fe.creds.Creds(), test.wantFE; got != want { 205 t.Errorf("got %v, want %v", got, want) 206 } 207 if got, want := fe.db.creds.Creds(), test.wantDB; got != want { 208 t.Errorf("got %v, want %v", got, want) 209 } 210 }) 211 } 212 } 213 214 func callerLine() int { 215 _, _, line, _ := runtime.Caller(1) // 1 skips the callerLine() frame. 216 return line 217 }