package web import ( "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // R-1028: the claim box renders only when the household still has to give the code — render test // per branch (the seam-wiring rule). // // v0.315.0: the box is no longer Plex-specific markup behind a `.PlexClaim` flag. It is the GENERIC // after-start card, and the thing it branches on is whether afterStartFields put the field in the // page data at all — which is where the „registered action AND Needed()" decision now lives. // COMPANION RED-PROOF: render the card unconditionally (drop the range over .AfterStartFields) → // the two negative cases fail. Restored. func TestPlexClaimBox_RendersPerBranch(t *testing.T) { st := lcStack("plex", "", true) st.State = stacks.StateRunning claimField := stacks.DeployField{EnvVar: "PLEX_CLAIM", Label: "Plex kod", AfterStart: true, Placeholder: "claim-xxxxxxxxxxxxxxxxxxxx"} for _, c := range []struct { name string fields []stacks.DeployField want bool }{ {"the code is still wanted", []stacks.DeployField{claimField}, true}, {"nothing is wanted", nil, false}, } { d := appInfoData(st) d["AfterStartFields"] = c.fields h := renderBackupPage(t, "app_settings", d) if got := strings.Contains(h, `data-afterstart="PLEX_CLAIM"`) && strings.Contains(h, `id="after-start-PLEX_CLAIM"`); got != c.want { t.Errorf("%s: claim box shown=%v, want %v", c.name, got, c.want) } } } // The code is checked for its shape before any call: no call for something that is not a claim code. func TestPlexClaimCodeShape(t *testing.T) { for code, ok := range map[string]bool{"claim-AbCdEf123456789_-xyz": true, "claim-short": false, "claim-abcdefgh": true, "claim-": false, "https://plex.tv/claim": false, "claim-abc def": false, "": false} { if got := plexClaimCode.MatchString(code); got != ok { t.Errorf("%q: accepted=%v, want %v", code, got, ok) } } } // R-1045: the household's „Kész" never opens an UNCLAIMED Plex (it answers everything to a private address with no // token); once Plex says claimed, the press opens it. No template probe is needed (an old controller would refuse the // press forever with one). COMPANION RED-PROOF: drop the plexClaimed check in appSetupGateOpenHandler → the first press // opens the gate (200). Restored. func TestPlexDonePress_RefusedWhileUnclaimed(t *testing.T) { s := gateHarness(t) app := filepath.Join(s.cfg.Paths.StacksDir, "gapp") if err := os.WriteFile(filepath.Join(app, ".felhom.yml"), []byte("display_name: Plex\nslug: plex\nsetup_gate: true\n"), 0o644); err != nil { t.Fatal(err) } if err := s.stackMgr.ScanStacks(); err != nil { t.Fatal(err) } claimed := false old := plexClaimed plexClaimed = func() bool { return claimed } defer func() { plexClaimed = old }() press := func() int { w := httptest.NewRecorder() s.appSetupGateOpenHandler(w, httptest.NewRequest(http.MethodPost, "/apps/plex/setup-gate/open", nil), "plex") return w.Code } if c := press(); c != http.StatusConflict { t.Fatalf("„Kész” on an unclaimed Plex: HTTP %d, want 409 — the gate must stay closed", c) } claimed = true if c := press(); c != http.StatusOK { t.Fatalf("„Kész” on a claimed Plex: HTTP %d, want 200", c) } } // R-1065: a claim that Plex confirms schedules the media wiring (which sets the libraries — it could // not while Plex was unclaimed); a claim Plex refuses, or one Plex never reports as claimed, does not. // The consequence asserted is the armed wiring timer — the same one a media app coming up arms. // COMPANION RED-PROOF: drop the RunMediaWiringSoon call in applyPlexClaim → the claimed case fails. func TestPlexClaim_TriggersMediaWiring(t *testing.T) { oldClaim, oldClaimed, oldPoll := plexClaim, plexClaimed, plexClaimPoll t.Cleanup(func() { plexClaim, plexClaimed, plexClaimPoll = oldClaim, oldClaimed, oldPoll }) plexClaimPoll = 0 for _, c := range []struct { name string status int claimed bool wantErr bool wantWiring bool }{ {"claimed", http.StatusOK, true, false, true}, {"plex refused the code", http.StatusUnauthorized, true, true, false}, {"plex never says claimed", http.StatusOK, false, true, false}, } { t.Run(c.name, func(t *testing.T) { s := gateHarness(t) t.Cleanup(func() { s.mediaMu.Lock() if s.mediaSoonTmr != nil { s.mediaSoonTmr.Stop() } s.mediaMu.Unlock() }) plexClaim = func(string) (int, error) { return c.status, nil } plexClaimed = func() bool { return c.claimed } err := s.applyPlexClaim("claim-AbCdEf123456789xyz") if (err != nil) != c.wantErr { t.Fatalf("applyPlexClaim err=%v, want error=%v", err, c.wantErr) } s.mediaMu.Lock() armed := s.mediaSoonTmr != nil s.mediaMu.Unlock() if armed != c.wantWiring { t.Errorf("media wiring scheduled=%v, want %v", armed, c.wantWiring) } }) } }