package web import ( "net/http" "net/http/httptest" "net/url" "os" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/infra" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // R-691 (v0.275.0) — the read-only „Megőrzött adatok" view could not open nextcloud's kept folder // (`www-data` 33, mode 0770; the view runs as 1000). The rule: join the folder's OWNING GROUP when it // is group-readable — never root's, never the view's own — and change nothing on the household's files. // // COMPANION RED-PROOF (REPORT.md): return nil from keptReadGroups — the compose then carries no // group_add and this fails at "group_add missing". func TestR691_KeptReadGroups(t *testing.T) { owners := map[string]struct { gid int mode os.FileMode }{ "/d/kept/nextcloud/2026-09-25_141014": {33, 0o770}, // the measured case → 33 "/d/appdata/nextcloud": {33, 0o770}, // the same group once "/d/kept/romm/x": {1000, 0o755}, // the view's own group: nothing to add "/d/kept/secret/x": {0, 0o750}, // root's group: NEVER "/d/kept/private/x": {999, 0o700}, // not group-readable: adding it would not help "/d/kept/jellyfin/x": {911, 0o750}, // another readable group → 911 } var items []stacks.KeptItem for p := range owners { items = append(items, stacks.KeptItem{Path: p}) } got := keptReadGroups(items, func(p string) (int, os.FileMode, bool) { o, ok := owners[p] return o.gid, o.mode, ok }) if len(got) != 2 || got[0] != 33 || got[1] != 911 { t.Fatalf("groups = %v, want [33 911]", got) } compose := infra.RenderFileBrowserCompose("example.hu", keptBindLines([]stacks.KeptItem{{Path: "/d/kept/nextcloud/2026-09-25_141014", App: "nextcloud"}}), got...) if !strings.Contains(compose, "group_add:\n - \"33\"\n - \"911\"") { t.Fatalf("group_add missing or malformed:\n%s", compose) } if !strings.Contains(compose, ":/srv/"+infra.FileBrowserKeptMount+"/") || !strings.Contains(compose, ":ro") { t.Fatalf("the kept bind must stay read-only:\n%s", compose) } // No kept groups → the compose is exactly what it was before v0.275.0. if strings.Contains(infra.RenderFileBrowserCompose("example.hu", nil), "group_add") { t.Fatal("a box with no kept data got a group_add") } } // The source's name follows the box's language: switching the language triggers the file-browser sync // that writes it (before, it waited for the next unrelated change — seen on 9202 2026-09-25). func TestR691_ALanguageSwitchResyncsTheFileBrowser(t *testing.T) { s := noteServer(t) called := 0 s.langSwitchSync = func() { called++ } req := httptest.NewRequest(http.MethodPost, "/settings/language", strings.NewReader(url.Values{"lang": {"en"}}.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() s.languageSwitchHandler(rec, req) if rec.Code != http.StatusFound || called != 1 { t.Fatalf("status %d, file-browser syncs %d — want a redirect and one sync", rec.Code, called) } }