mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: Add support for background sync * refactor: Maintain single source of truth for command defn * fix: Code review fixes * fix: Code review fixes * docs: Add corner case inline doc
143 lines
4.0 KiB
Go
143 lines
4.0 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestReadLastSyncAttempt(t *testing.T) {
|
|
t.Run("missing file returns zero time", func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "does-not-exist")
|
|
assert.True(t, ReadLastSyncAttempt(path).IsZero())
|
|
})
|
|
|
|
t.Run("empty file returns zero time", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "lastrun")
|
|
require.NoError(t, os.WriteFile(path, []byte(""), 0o600))
|
|
assert.True(t, ReadLastSyncAttempt(path).IsZero())
|
|
})
|
|
|
|
t.Run("unparseable contents return zero time", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "lastrun")
|
|
require.NoError(t, os.WriteFile(path, []byte("not-a-number"), 0o600))
|
|
assert.True(t, ReadLastSyncAttempt(path).IsZero())
|
|
})
|
|
|
|
t.Run("valid epoch with trailing whitespace is parsed", func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "lastrun")
|
|
want := time.Unix(1700000000, 0)
|
|
require.NoError(t, os.WriteFile(path, []byte("1700000000\n"), 0o600))
|
|
assert.True(t, ReadLastSyncAttempt(path).Equal(want))
|
|
})
|
|
}
|
|
|
|
func TestWriteLastSyncAttempt(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "lastrun")
|
|
|
|
before := time.Now()
|
|
require.NoError(t, WriteLastSyncAttempt(path))
|
|
after := time.Now()
|
|
|
|
got := ReadLastSyncAttempt(path)
|
|
require.False(t, got.IsZero())
|
|
|
|
// Allow 1s slack on both ends to absorb second-level truncation by
|
|
// WriteLastSyncAttempt's Unix() serialization.
|
|
assert.False(t, got.Before(before.Add(-time.Second)), "got=%s before=%s", got, before)
|
|
assert.False(t, got.After(after.Add(time.Second)), "got=%s after=%s", got, after)
|
|
}
|
|
|
|
func TestSyncCooldownElapsed(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "lastrun")
|
|
|
|
t.Run("missing lastrun is treated as old enough", func(t *testing.T) {
|
|
assert.True(t, SyncCooldownElapsed(path, time.Hour))
|
|
})
|
|
|
|
t.Run("recent lastrun blocks", func(t *testing.T) {
|
|
require.NoError(t, WriteLastSyncAttempt(path))
|
|
assert.False(t, SyncCooldownElapsed(path, time.Hour))
|
|
})
|
|
|
|
t.Run("old lastrun allows", func(t *testing.T) {
|
|
old := time.Now().Add(-2 * time.Hour).Unix()
|
|
require.NoError(t, os.WriteFile(path, []byte(strconv.FormatInt(old, 10)), 0o600))
|
|
assert.True(t, SyncCooldownElapsed(path, time.Hour))
|
|
})
|
|
}
|
|
|
|
func TestSyncLockSerializes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "sync.lock")
|
|
|
|
first := NewSyncLock(path)
|
|
ok, err := first.TryLock()
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
defer func() {
|
|
require.NoError(t, first.Unlock())
|
|
}()
|
|
|
|
t.Run("TryLock from a sibling lock fails while first is held", func(t *testing.T) {
|
|
second := NewSyncLock(path)
|
|
ok, err := second.TryLock()
|
|
require.NoError(t, err)
|
|
assert.False(t, ok)
|
|
})
|
|
|
|
t.Run("TryLockContext from a sibling times out while first is held", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
|
defer cancel()
|
|
|
|
second := NewSyncLock(path)
|
|
ok, err := second.TryLockContext(ctx, 25*time.Millisecond)
|
|
// On context cancel TryLockContext can return either (false, nil) or
|
|
// (false, context.DeadlineExceeded) depending on timing; both satisfy
|
|
// the "did not acquire" assertion.
|
|
if err != nil {
|
|
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
|
}
|
|
assert.False(t, ok)
|
|
})
|
|
}
|
|
|
|
func TestSyncLockAllowsSecondAcquisitionAfterRelease(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "sync.lock")
|
|
|
|
wg := sync.WaitGroup{}
|
|
wg.Add(1)
|
|
first := NewSyncLock(path)
|
|
ok, err := first.TryLock()
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
// assert (not require) is safe from a goroutine. require.NoError
|
|
// calls t.FailNow which is not goroutine-safe.
|
|
time.Sleep(50 * time.Millisecond)
|
|
assert.NoError(t, first.Unlock())
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
second := NewSyncLock(path)
|
|
ok, err = second.TryLock()
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
require.NoError(t, second.Unlock())
|
|
}
|