...

Source file src/github.com/ory/x/configx/provider_watch_test.go

Documentation: github.com/ory/x/configx

     1  package configx
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/sirupsen/logrus"
    18  	"github.com/sirupsen/logrus/hooks/test"
    19  	"github.com/stretchr/testify/assert"
    20  	"github.com/stretchr/testify/require"
    21  
    22  	"github.com/ory/x/logrusx"
    23  	"github.com/ory/x/watcherx"
    24  )
    25  
    26  func tmpConfigFile(t *testing.T, dsn, foo string) *os.File {
    27  	config := fmt.Sprintf("dsn: %s\nfoo: %s\n", dsn, foo)
    28  
    29  	tdir := os.TempDir() + "/" + strconv.Itoa(time.Now().Nanosecond())
    30  	require.NoError(t,
    31  		os.MkdirAll(tdir, // DO NOT CHANGE THIS: https://github.com/fsnotify/fsnotify/issues/340
    32  			os.ModePerm))
    33  	configFile, err := ioutil.TempFile(tdir, "config-*.yml")
    34  	_, err = io.WriteString(configFile, config)
    35  	require.NoError(t, err)
    36  	require.NoError(t, configFile.Sync())
    37  	t.Cleanup(func() {
    38  		fmt.Printf("removing %s\n", configFile.Name())
    39  		_ = os.Remove(configFile.Name())
    40  	})
    41  
    42  	return configFile
    43  }
    44  
    45  func updateConfigFile(t *testing.T, c <-chan struct{}, configFile *os.File, dsn, foo, bar string) {
    46  	config := fmt.Sprintf(`dsn: %s
    47  foo: %s
    48  bar: %s`, dsn, foo, bar)
    49  
    50  	_, err := configFile.Seek(0, 0)
    51  	require.NoError(t, err)
    52  	require.NoError(t, configFile.Truncate(0))
    53  	_, err = io.WriteString(configFile, config)
    54  	require.NoError(t, configFile.Sync())
    55  	<-c // Wait for changes to propagate
    56  }
    57  
    58  func checkLsof(t *testing.T, file string) string {
    59  	if runtime.GOOS == "windows" {
    60  		return ""
    61  	}
    62  	var b bytes.Buffer
    63  	c := exec.Command("bash", "-c", "lsof -n | grep "+file+" | wc -l")
    64  	c.Stdout = &b
    65  	require.NoError(t, c.Run())
    66  	return b.String()
    67  }
    68  
    69  func TestReload(t *testing.T) {
    70  	setup := func(t *testing.T, cf *os.File, c chan<- struct{}, modifiers ...OptionModifier) (*Provider, *logrusx.Logger) {
    71  		l := logrusx.New("configx", "test")
    72  		ctx, cancel := context.WithCancel(context.Background())
    73  		t.Cleanup(cancel)
    74  		modifiers = append(modifiers,
    75  			WithLogrusWatcher(l),
    76  			WithLogger(l),
    77  			AttachWatcher(func(event watcherx.Event, err error) {
    78  				t.Logf("Received event: %+v error: %+v", event, err)
    79  				c <- struct{}{}
    80  			}),
    81  			WithContext(ctx),
    82  		)
    83  		p, err := newKoanf("./stub/watch/config.schema.json", []string{cf.Name()}, modifiers...)
    84  		require.NoError(t, err)
    85  		return p, l
    86  	}
    87  
    88  	t.Run("case=rejects not validating changes", func(t *testing.T) {
    89  		configFile := tmpConfigFile(t, "memory", "bar")
    90  		defer configFile.Close()
    91  		c := make(chan struct{})
    92  		p, l := setup(t, configFile, c)
    93  		hook := test.NewLocal(l.Entry.Logger)
    94  
    95  		atStart := checkLsof(t, configFile.Name())
    96  
    97  		assert.Equal(t, []*logrus.Entry{}, hook.AllEntries())
    98  		assert.Equal(t, "memory", p.String("dsn"))
    99  		assert.Equal(t, "bar", p.String("foo"))
   100  
   101  		updateConfigFile(t, c, configFile, "memory", "not bar", "bar")
   102  
   103  		entries := hook.AllEntries()
   104  		require.Equal(t, 2, len(entries))
   105  
   106  		assert.Equal(t, "A change to a configuration file was detected.", entries[0].Message)
   107  		assert.Equal(t, "The changed configuration is invalid and could not be loaded. Rolling back to the last working configuration revision. Please address the validation errors before restarting the process.", entries[1].Message)
   108  
   109  		assert.Equal(t, "memory", p.String("dsn"))
   110  		assert.Equal(t, "bar", p.String("foo"))
   111  
   112  		// but it is still watching the files
   113  		updateConfigFile(t, c, configFile, "memory", "bar", "baz")
   114  		assert.Equal(t, "baz", p.String("bar"))
   115  
   116  		atEnd := checkLsof(t, configFile.Name())
   117  		require.EqualValues(t, atStart, atEnd)
   118  	})
   119  
   120  	t.Run("case=rejects to update immutable", func(t *testing.T) {
   121  		configFile := tmpConfigFile(t, "memory", "bar")
   122  		defer configFile.Close()
   123  		c := make(chan struct{})
   124  		p, l := setup(t, configFile, c,
   125  			WithImmutables("dsn"))
   126  		hook := test.NewLocal(l.Entry.Logger)
   127  
   128  		atStart := checkLsof(t, configFile.Name())
   129  
   130  		assert.Equal(t, []*logrus.Entry{}, hook.AllEntries())
   131  		assert.Equal(t, "memory", p.String("dsn"))
   132  		assert.Equal(t, "bar", p.String("foo"))
   133  
   134  		updateConfigFile(t, c, configFile, "some db", "bar", "baz")
   135  
   136  		entries := hook.AllEntries()
   137  		require.Equal(t, 2, len(entries))
   138  		assert.Equal(t, "A change to a configuration file was detected.", entries[0].Message)
   139  		assert.Equal(t, "A configuration value marked as immutable has changed. Rolling back to the last working configuration revision. To reload the values please restart the process.", entries[1].Message)
   140  		assert.Equal(t, "memory", p.String("dsn"))
   141  		assert.Equal(t, "bar", p.String("foo"))
   142  
   143  		// but it is still watching the files
   144  		updateConfigFile(t, c, configFile, "memory", "bar", "baz")
   145  		assert.Equal(t, "baz", p.String("bar"))
   146  
   147  		atEnd := checkLsof(t, configFile.Name())
   148  		require.EqualValues(t, atStart, atEnd)
   149  	})
   150  
   151  	t.Run("case=runs without validation errors", func(t *testing.T) {
   152  		configFile := tmpConfigFile(t, "some string", "bar")
   153  		defer configFile.Close()
   154  		c := make(chan struct{})
   155  		p, l := setup(t, configFile, c)
   156  		hook := test.NewLocal(l.Entry.Logger)
   157  
   158  		assert.Equal(t, []*logrus.Entry{}, hook.AllEntries())
   159  		assert.Equal(t, "some string", p.String("dsn"))
   160  		assert.Equal(t, "bar", p.String("foo"))
   161  	})
   162  
   163  	t.Run("case=runs and reloads", func(t *testing.T) {
   164  		configFile := tmpConfigFile(t, "some string", "bar")
   165  		defer configFile.Close()
   166  		c := make(chan struct{})
   167  		p, l := setup(t, configFile, c)
   168  		hook := test.NewLocal(l.Entry.Logger)
   169  
   170  		assert.Equal(t, []*logrus.Entry{}, hook.AllEntries())
   171  		assert.Equal(t, "some string", p.String("dsn"))
   172  		assert.Equal(t, "bar", p.String("foo"))
   173  
   174  		updateConfigFile(t, c, configFile, "memory", "bar", "baz")
   175  		assert.Equal(t, "baz", p.String("bar"))
   176  	})
   177  
   178  	t.Run("case=has with validation errors", func(t *testing.T) {
   179  		configFile := tmpConfigFile(t, "some string", "not bar")
   180  		defer configFile.Close()
   181  		l := logrusx.New("", "")
   182  		hook := test.NewLocal(l.Entry.Logger)
   183  
   184  		var b bytes.Buffer
   185  		_, err := newKoanf("./stub/watch/config.schema.json", []string{configFile.Name()},
   186  			WithStandardValidationReporter(&b),
   187  			WithLogrusWatcher(l),
   188  		)
   189  		require.Error(t, err)
   190  
   191  		entries := hook.AllEntries()
   192  		require.Equal(t, 0, len(entries))
   193  		assert.Equal(t, "The configuration contains values or keys which are invalid:\nfoo: not bar\n     ^-- value must be \"bar\"\n\n", b.String())
   194  	})
   195  
   196  	t.Run("case=is not leaking open files", func(t *testing.T) {
   197  		if runtime.GOOS == "windows" {
   198  			t.Skip()
   199  		}
   200  
   201  		configFile := tmpConfigFile(t, "some string", "bar")
   202  		defer configFile.Close()
   203  		c := make(chan struct{})
   204  		p, _ := setup(t, configFile, c)
   205  
   206  		atStart := checkLsof(t, configFile.Name())
   207  		for i := 0; i < 30; i++ {
   208  			t.Run(fmt.Sprintf("iteration=%d", i), func(t *testing.T) {
   209  				expected := []string{"foo", "bar", "baz"}[i%3]
   210  				updateConfigFile(t, c, configFile, "memory", "bar", expected)
   211  				require.EqualValues(t, atStart, checkLsof(t, configFile.Name()))
   212  				require.EqualValues(t, expected, p.String("bar"))
   213  			})
   214  		}
   215  
   216  		atEnd := checkLsof(t, configFile.Name())
   217  		require.EqualValues(t, atStart, atEnd)
   218  
   219  		atStartNum, err := strconv.ParseInt(strings.TrimSpace(atStart), 10, 32)
   220  		require.NoError(t, err)
   221  		require.True(t, atStartNum < 20, "should not be unreasonably high: %s", atStartNum)
   222  	})
   223  }
   224  

View as plain text