fix: serialize persistent profile mutations
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeIdentityEnvironment struct {
|
||||
current UserIdentity
|
||||
users map[string]UserIdentity
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
func (f fakeIdentityEnvironment) CurrentUser() (UserIdentity, error) { return f.current, nil }
|
||||
func (f fakeIdentityEnvironment) LookupUser(name string) (UserIdentity, error) {
|
||||
identity, ok := f.users[name]
|
||||
if !ok {
|
||||
return UserIdentity{}, errors.New("not found")
|
||||
}
|
||||
return identity, nil
|
||||
}
|
||||
func (f fakeIdentityEnvironment) LookupEnv(name string) (string, bool) {
|
||||
value, ok := f.env[name]
|
||||
return value, ok
|
||||
}
|
||||
|
||||
func TestFileStoreLockSerializesAcrossProcesses(t *testing.T) {
|
||||
if path := os.Getenv("PROXYCTL_LOCK_HELPER_PATH"); path != "" {
|
||||
store := NewFileStore(path)
|
||||
_, err := store.Mutate(func(state State) (State, error) {
|
||||
state.Profiles = append(state.Profiles, Profile{Name: "child"})
|
||||
return state, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
path := filepath.Join(t.TempDir(), "proxyctl", "profiles.json")
|
||||
store := NewFileStore(path)
|
||||
if _, err := store.Mutate(func(state State) (State, error) { return state, nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lock, err := os.OpenFile(path+".lock", os.O_RDWR, 0o600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer lock.Close()
|
||||
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
command := exec.Command(os.Args[0], "-test.run=^TestFileStoreLockSerializesAcrossProcesses$")
|
||||
command.Env = append(os.Environ(), "PROXYCTL_LOCK_HELPER_PATH="+path)
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- command.Run() }()
|
||||
select {
|
||||
case err := <-done:
|
||||
t.Fatalf("child mutation did not wait for lock: %v", err)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_UN); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("child mutation after unlock: %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
_ = command.Process.Kill()
|
||||
t.Fatal("child mutation remained blocked after unlock")
|
||||
}
|
||||
state, err := store.Load()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(state.Profiles) != 1 || state.Profiles[0].Name != "child" {
|
||||
t.Fatalf("state after serialized child mutation = %#v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPathUsesInvokingUserIdentityUnderSudo(t *testing.T) {
|
||||
environment := fakeIdentityEnvironment{
|
||||
current: UserIdentity{Username: "root", HomeDir: "/root"},
|
||||
users: map[string]UserIdentity{"alice": {Username: "alice", HomeDir: "/home/alice"}},
|
||||
env: map[string]string{
|
||||
"SUDO_USER": "alice",
|
||||
"XDG_CONFIG_HOME": "/home/alice/.xdg/config",
|
||||
},
|
||||
}
|
||||
path, err := ConfigPathWith(environment)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := filepath.Join("/home/alice/.xdg/config", "proxyctl", "profiles.json")
|
||||
if path != want {
|
||||
t.Fatalf("configuration path = %q, want invoking user's XDG path %q", path, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPathDoesNotReuseRootXDGForInvokingUser(t *testing.T) {
|
||||
environment := fakeIdentityEnvironment{
|
||||
current: UserIdentity{Username: "root", HomeDir: "/root"},
|
||||
users: map[string]UserIdentity{"alice": {Username: "alice", HomeDir: "/home/alice"}},
|
||||
env: map[string]string{
|
||||
"SUDO_USER": "alice",
|
||||
"XDG_CONFIG_HOME": "/root/.config",
|
||||
},
|
||||
}
|
||||
path, err := ConfigPathWith(environment)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := filepath.Join("/home/alice/.config", "proxyctl", "profiles.json")
|
||||
if path != want {
|
||||
t.Fatalf("configuration path = %q, want invoking user's fallback %q", path, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPathUsesRootForDirectRootLogin(t *testing.T) {
|
||||
environment := fakeIdentityEnvironment{
|
||||
current: UserIdentity{Username: "root", HomeDir: "/root"},
|
||||
users: map[string]UserIdentity{},
|
||||
env: map[string]string{"XDG_CONFIG_HOME": "/root/custom-config"},
|
||||
}
|
||||
path, err := ConfigPathWith(environment)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := filepath.Join("/root/custom-config", "proxyctl", "profiles.json")
|
||||
if path != want {
|
||||
t.Fatalf("configuration path = %q, want direct root path %q", path, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user