package vault_test import ( "testing" "time" "git.eeqj.de/sneak/secret/internal/vault" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( // lockWait is how long a test waits for the lock before deciding it // will never come free. lockWait = 10 * time.Second // heldWait is how long a test watches a second holder fail to take a // lock that is held. Broken exclusion lets it in at once. heldWait = 100 * time.Millisecond ) // lockFilesystem is a filesystem LockStateDir can lock, with a state // directory on it. type lockFilesystem struct { name string fs afero.Fs stateDir string } // lockFilesystems returns the real filesystem, locked with flock, and the // in-memory one, locked with a mutex. func lockFilesystems(t *testing.T) []lockFilesystem { t.Helper() return []lockFilesystem{ {"memory", afero.NewMemMapFs(), testStateDir}, {"real", afero.NewOsFs(), t.TempDir()}, } } // lockInBackground starts taking the lock and returns a channel that // delivers the function releasing it once it has been taken. func lockInBackground( t *testing.T, fs afero.Fs, stateDir string, ) <-chan func() { t.Helper() taken := make(chan func(), 1) go func() { release, err := vault.LockStateDir(fs, stateDir) if assert.NoError(t, err) { taken <- release } }() return taken } // TestLockStateDirExcludes checks that while the lock is held a second // holder, with its own open lock file on the real filesystem, waits, and // that it gets the lock once the first releases it. func TestLockStateDirExcludes(t *testing.T) { t.Parallel() for _, lfs := range lockFilesystems(t) { t.Run(lfs.name, func(t *testing.T) { t.Parallel() release, err := vault.LockStateDir(lfs.fs, lfs.stateDir) require.NoError(t, err) taken := lockInBackground(t, lfs.fs, lfs.stateDir) select { case second := <-taken: second() release() t.Fatal("a second holder took the lock while it was held") case <-time.After(heldWait): } release() select { case second := <-taken: second() case <-time.After(lockWait): t.Fatal("the second holder never got the lock") } }) } } // TestLockStateDirFreeAfterPanic checks that a holder that panics, and // releases the lock with defer as every command does, leaves it free. func TestLockStateDirFreeAfterPanic(t *testing.T) { t.Parallel() for _, lfs := range lockFilesystems(t) { t.Run(lfs.name, func(t *testing.T) { t.Parallel() assert.Panics(t, func() { release, err := vault.LockStateDir(lfs.fs, lfs.stateDir) require.NoError(t, err) defer release() panic("the command failed") }) select { case release := <-lockInBackground(t, lfs.fs, lfs.stateDir): release() case <-time.After(lockWait): t.Fatal("the lock was still held after its holder panicked") } }) } } // TestLockStateDirRefusesOtherFilesystems checks that a filesystem with no // lock implementation is refused instead of being used unlocked. func TestLockStateDirRefusesOtherFilesystems(t *testing.T) { t.Parallel() fs := afero.NewReadOnlyFs(afero.NewMemMapFs()) release, err := vault.LockStateDir(fs, testStateDir) require.ErrorIs(t, err, vault.ErrNoLockForFilesystem) assert.Nil(t, release) }