package profile_test import ( "os" "path/filepath" "strings" "syscall" "testing" "time" "proxyctl/internal/profile" ) func TestReadOnlyLoadValidatesWithoutRepairingOrCreatingStorage(t *testing.T) { directory := filepath.Join(t.TempDir(), "proxyctl") if err := os.MkdirAll(directory, 0o755); err != nil { t.Fatal(err) } path := filepath.Join(directory, "profiles.json") if err := os.WriteFile(path, []byte(`{"profiles":[{"name":"office","mode":"single","singleUrl":"http://proxy.test:8080","httpUrl":"http://proxy.test:8080","httpsUrl":"http://proxy.test:8080","allUrl":"http://proxy.test:8080"}],"activeProfile":"office"}`), 0o644); err != nil { t.Fatal(err) } store := profile.NewFileStore(path) state, err := store.ReadOnlyLoad() if err != nil || state.ActiveProfile != "office" { t.Fatalf("read-only load state=%#v err=%v", state, err) } info, err := os.Stat(path) if err != nil { t.Fatal(err) } if info.Mode().Perm() != 0o644 { t.Fatalf("read-only load changed mode to %o", info.Mode().Perm()) } if _, err := os.Stat(path + ".lock"); !os.IsNotExist(err) { t.Fatalf("read-only load created lock: %v", err) } } func TestReadOnlyLoadPreservesMissingAndMalformedBehavior(t *testing.T) { t.Run("missing", func(t *testing.T) { path := filepath.Join(t.TempDir(), "missing", "profiles.json") state, err := profile.NewFileStore(path).ReadOnlyLoad() if err != nil || len(state.Profiles) != 0 || state.ActiveProfile != "" { t.Fatalf("missing read-only load state=%#v err=%v", state, err) } if _, statErr := os.Stat(filepath.Dir(path)); !os.IsNotExist(statErr) { t.Fatalf("missing read-only load created storage: %v", statErr) } }) t.Run("malformed", func(t *testing.T) { path := filepath.Join(t.TempDir(), "profiles.json") if err := os.WriteFile(path, []byte("not JSON"), 0o640); err != nil { t.Fatal(err) } _, err := profile.NewFileStore(path).ReadOnlyLoad() if err == nil || !strings.Contains(err.Error(), "parse Profile configuration") { t.Fatalf("malformed read-only load error = %v", err) } }) } func TestReadOnlyLoadUsesConfiguredTrustedRoot(t *testing.T) { trustedRoot := t.TempDir() outside := filepath.Join(t.TempDir(), "profiles.json") if err := os.WriteFile(outside, []byte(`{"profiles":[]}`), 0o600); err != nil { t.Fatal(err) } owner := profile.UserIdentity{HomeDir: trustedRoot, UID: os.Getuid(), GID: os.Getgid()} _, err := profile.NewOwnedFileStoreAt(outside, owner, trustedRoot).ReadOnlyLoad() if err == nil || !strings.Contains(err.Error(), "escapes its trusted root") { t.Fatalf("configured trusted root was not enforced: %v", err) } } func TestReadOnlyLoadRejectsSymlinksWithoutReadingOrChangingVictim(t *testing.T) { for _, test := range []struct { name string path func(t *testing.T, root, victim string) string }{ { name: "final symlink", path: func(t *testing.T, root, victim string) string { t.Helper() directory := filepath.Join(root, "proxyctl") if err := os.MkdirAll(directory, 0o755); err != nil { t.Fatal(err) } path := filepath.Join(directory, "profiles.json") if err := os.Symlink(victim, path); err != nil { t.Fatal(err) } return path }, }, { name: "ancestor symlink", path: func(t *testing.T, root, victim string) string { t.Helper() link := filepath.Join(root, "proxyctl") if err := os.Symlink(filepath.Dir(victim), link); err != nil { t.Fatal(err) } return filepath.Join(link, filepath.Base(victim)) }, }, } { t.Run(test.name, func(t *testing.T) { victim := filepath.Join(t.TempDir(), "profiles.json") const content = "victim content that is deliberately not JSON" if err := os.WriteFile(victim, []byte(content), 0o640); err != nil { t.Fatal(err) } if err := os.Chtimes(victim, time.Unix(1, 0), time.Now()); err != nil { t.Fatal(err) } before := fileIdentity(t, victim) path := test.path(t, t.TempDir(), victim) _, err := profile.NewFileStore(path).ReadOnlyLoad() if err == nil { t.Fatal("ReadOnlyLoad accepted a symlinked Profile configuration") } if strings.Contains(err.Error(), "parse Profile configuration") { t.Fatalf("ReadOnlyLoad followed and parsed the victim: %v", err) } after := fileIdentity(t, victim) if before != after { t.Fatalf("victim was read or its metadata changed: before=%+v after=%+v", before, after) } got, readErr := os.ReadFile(victim) if readErr != nil { t.Fatal(readErr) } if string(got) != content { t.Fatalf("victim content changed to %q", got) } if _, statErr := os.Lstat(path + ".lock"); !os.IsNotExist(statErr) { t.Fatalf("ReadOnlyLoad created a lock: %v", statErr) } }) } } type diagnosticFileIdentity struct { mode os.FileMode uid uint32 gid uint32 atime syscall.Timespec } func fileIdentity(t *testing.T, path string) diagnosticFileIdentity { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatal(err) } stat, ok := info.Sys().(*syscall.Stat_t) if !ok { t.Fatalf("stat data has type %T", info.Sys()) } return diagnosticFileIdentity{mode: info.Mode(), uid: stat.Uid, gid: stat.Gid, atime: stat.Atim} }