Add profile owners to debug bundle

This commit is contained in:
Theodor S. Midtlien
2026-09-15 09:05:42 +02:00
parent 5f900fe38f
commit 29e0073519
2 changed files with 386 additions and 1 deletions
+157
View File
@@ -4,6 +4,7 @@ import (
"archive/zip"
"bytes"
"encoding/json"
"io"
"net"
"net/netip"
"net/url"
@@ -970,3 +971,159 @@ func renderAddConfigSpecific(g *BundleGenerator) string {
func newAnonymizerForTest() *anonymize.Anonymizer {
return anonymize.NewAnonymizer(anonymize.DefaultAddresses())
}
// writeProfileJSON writes a profile config with the given display name and
// owner principals, mirroring what the profile manager persists.
func writeProfileJSON(t *testing.T, path, name string, owners []string) {
t.Helper()
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
data, err := json.Marshal(map[string]any{"Name": name, "Owners": owners})
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, data, 0o600))
}
// setupProfilesDir points the profile manager at a temporary state directory
// laid out the way the daemon writes it, with the default profile at the top
// level and further profiles in subdirectories.
func setupProfilesDir(t *testing.T) string {
t.Helper()
dir := t.TempDir()
origDir := profilemanager.DefaultConfigPathDir
origDefault := profilemanager.DefaultConfigPath
origActive := profilemanager.ActiveProfileStatePath
t.Cleanup(func() {
profilemanager.DefaultConfigPathDir = origDir
profilemanager.DefaultConfigPath = origDefault
profilemanager.ActiveProfileStatePath = origActive
})
profilemanager.DefaultConfigPathDir = dir
profilemanager.DefaultConfigPath = filepath.Join(dir, "default.json")
profilemanager.ActiveProfileStatePath = filepath.Join(dir, "active_profile.json")
return dir
}
func bundleFiles(t *testing.T, add func(g *BundleGenerator) error) map[string]string {
t.Helper()
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
g := &BundleGenerator{
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
archive: zw,
}
require.NoError(t, add(g))
require.NoError(t, zw.Close())
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
require.NoError(t, err)
files := make(map[string]string, len(zr.File))
for _, f := range zr.File {
rc, err := f.Open()
require.NoError(t, err)
content, err := io.ReadAll(rc)
require.NoError(t, rc.Close())
require.NoError(t, err)
files[f.Name] = string(content)
}
return files
}
func TestAddProfiles(t *testing.T) {
t.Run("collects id, name and owners per profile", func(t *testing.T) {
dir := setupProfilesDir(t)
writeProfileJSON(t, profilemanager.DefaultConfigPath, "default", nil)
writeProfileJSON(t, filepath.Join(dir, "alice", "aaaa1111.json"), "work", []string{"uid:1000"})
writeProfileJSON(t, filepath.Join(dir, "bob", "bbbb2222.json"), "home", []string{"uid:1001"})
// State files sit next to the profiles and must not be listed as one.
require.NoError(t, os.WriteFile(filepath.Join(dir, "alice", "aaaa1111.state.json"), []byte(`{"email":"a@b.c"}`), 0o600))
active, err := json.Marshal(map[string]string{"name": "aaaa1111", "username": "alice"})
require.NoError(t, err)
require.NoError(t, os.WriteFile(profilemanager.ActiveProfileStatePath, active, 0o600))
files := bundleFiles(t, (*BundleGenerator).addProfiles)
profiles := files[profilesBundleFile]
require.NotEmpty(t, profiles, "bundle should contain %s", profilesBundleFile)
assert.Contains(t, profiles, "Profiles found: 3", "default plus both subdirectory profiles should be listed")
assert.Contains(t, profiles, "[aaaa1111]")
assert.Contains(t, profiles, "Name: work")
assert.Contains(t, profiles, "Owners: uid:1000")
assert.Contains(t, profiles, "[bbbb2222]")
assert.Contains(t, profiles, "Owners: uid:1001")
assert.NotContains(t, profiles, "aaaa1111.state.json", "state files are not profiles")
// The default profile carries no owners yet.
assert.Contains(t, profiles, "Owners: (none)")
assert.Equal(t, string(active), files[activeProfileBundleFile], "active profile state should be dumped verbatim")
})
t.Run("reports every owner, not only the honored one", func(t *testing.T) {
dir := setupProfilesDir(t)
// The client honors the first owner only, so extra entries are invisible
// to it and worth surfacing in the bundle.
writeProfileJSON(t, filepath.Join(dir, "alice", "aaaa1111.json"), "work", []string{"uid:1000", "uid:1001", "bogus"})
entries := collectProfileEntries(nil)
require.Len(t, entries, 1)
assert.Equal(t, []string{"uid:1000", "uid:1001", "bogus"}, entries[0].owners, "the whole owners list should be reported")
})
t.Run("marks the active profile in any subdirectory", func(t *testing.T) {
dir := setupProfilesDir(t)
writeProfileJSON(t, filepath.Join(dir, "some-dir", "aaaa1111.json"), "work", []string{"uid:1000"})
writeProfileJSON(t, filepath.Join(dir, "other-dir", "bbbb2222.json"), "home", []string{"uid:1001"})
active, err := json.Marshal(map[string]string{"name": "aaaa1111", "username": "nonexistent"})
require.NoError(t, err)
require.NoError(t, os.WriteFile(profilemanager.ActiveProfileStatePath, active, 0o600))
entries := collectProfileEntries(mustActiveState(t))
require.Len(t, entries, 2)
for _, entry := range entries {
assert.Equal(t, entry.id == "aaaa1111", entry.isActive, "active state should match on ID alone, got %+v", entry)
}
})
t.Run("missing active profile state still lists profiles", func(t *testing.T) {
dir := setupProfilesDir(t)
writeProfileJSON(t, filepath.Join(dir, "alice", "aaaa1111.json"), "work", []string{"uid:1000"})
files := bundleFiles(t, (*BundleGenerator).addProfiles)
assert.Contains(t, files[profilesBundleFile], "[aaaa1111]")
assert.Contains(t, files[profilesBundleFile], "Active profile: none recorded")
assert.NotContains(t, files, activeProfileBundleFile, "no state file means nothing to dump")
})
t.Run("records a parse error and keeps the other profiles", func(t *testing.T) {
dir := setupProfilesDir(t)
require.NoError(t, os.MkdirAll(filepath.Join(dir, "alice"), 0o700))
require.NoError(t, os.WriteFile(filepath.Join(dir, "alice", "aaaa1111.json"), []byte("{broken"), 0o600))
writeProfileJSON(t, filepath.Join(dir, "alice", "bbbb2222.json"), "work", []string{"uid:1000"})
entries := collectProfileEntries(nil)
require.Len(t, entries, 2)
assert.Error(t, entries[0].loadErr, "the unreadable profile should carry its error")
assert.Equal(t, "aaaa1111", entries[0].name, "an unreadable profile falls back to its ID as the name")
assert.NoError(t, entries[1].loadErr)
assert.Equal(t, "work", entries[1].name)
})
}
func mustActiveState(t *testing.T) *profilemanager.ActiveProfileState {
t.Helper()
state, _, err := readActiveProfileState()
require.NoError(t, err)
require.NotNil(t, state)
return state
}