package operationlock_test import ( "context" "os" "path/filepath" "testing" "time" "proxyctl/internal/operationlock" ) type recordingLocker struct { name string events *[]string err error } func (l recordingLocker) Lock(context.Context) (func(), error) { *l.events = append(*l.events, "lock "+l.name) if l.err != nil { return nil, l.err } return func() { *l.events = append(*l.events, "unlock "+l.name) }, nil } func TestGroupAcquiresInOrderAndReleasesInReverse(t *testing.T) { var events []string group := operationlock.NewGroup( recordingLocker{name: "system", events: &events}, recordingLocker{name: "user", events: &events}, ) unlock, err := group.Lock(context.Background()) if err != nil { t.Fatal(err) } unlock() want := []string{"lock system", "lock user", "unlock user", "unlock system"} if len(events) != len(want) { t.Fatalf("events = %v, want %v", events, want) } for i := range want { if events[i] != want[i] { t.Fatalf("events = %v, want %v", events, want) } } } func TestLockRejectsSymlinkedStateAncestor(t *testing.T) { root := t.TempDir() victim := filepath.Join(root, "victim") if err := os.Mkdir(victim, 0o700); err != nil { t.Fatal(err) } if err := os.Symlink(victim, filepath.Join(root, "state")); err != nil { t.Fatal(err) } lock := operationlock.New(filepath.Join(root, "state", "proxyctl", "operation.lock")) if _, err := lock.Lock(context.Background()); err == nil { t.Fatal("lock followed a symlinked state ancestor") } entries, err := os.ReadDir(victim) if err != nil { t.Fatal(err) } if len(entries) != 0 { t.Fatalf("victim changed: %v", entries) } } func TestLockSerializesIndependentInstancesAndHonorsContext(t *testing.T) { path := t.TempDir() + "/proxyctl.lock" first := operationlock.New(path) second := operationlock.New(path) unlock, err := first.Lock(context.Background()) if err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond) defer cancel() if _, err := second.Lock(ctx); err == nil { t.Fatal("second lock acquired while first held") } unlock() unlockSecond, err := second.Lock(context.Background()) if err != nil { t.Fatal(err) } unlockSecond() }