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) } }