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  }