// Webhook form differential updates, ephemeral secret protection, and session picker filtering.
// Differential step generation and modal reveal dismissal guards operate inside unexported
// form state; daemon E2E only sees executed RPCs and cannot verify zero-command emission.
package tui
import (
"albedo/cli/internal/daemon"
"errors"
"net/http"
"strings"
"testing"
tea "charm.land/bubbletea/v2"
"github.com/charmbracelet/x/ansi"
)
var webhookSessions = []daemon.Session{
{ID: "s", Title: "infra bot", Workspace: "/srv/infra"},
{ID: "t", Title: "release notes", Workspace: "/srv/docs"},
}
func loadedWebhooksPage(t *testing.T) WebhooksPageModel {
t.Helper()
m := NewWebhooksPageModel(nil, "s")
m.SetSize(100, 30)
m, _ = m.Update(webhooksLoadedMsg{Gen: m.Generation, Mounted: true, Sessions: webhookSessions, Hooks: []webhookEntry{
{ID: "wh1", Session: "s", Name: "deploy", Enabled: true, URL: "/webhooks/wh1", Header: "x-hub-signature-256", Prefix: "sha256=", Queued: 2, Deferred: "session busy"},
{ID: "wh2", Session: "t", Name: "grafana", URL: "/webhooks/wh2", Header: "x-albedo-signature", Prefix: "sha256="},
}})
return m
}
func TestWebhookEditOnlySendsWhatChanged(t *testing.T) {
m := loadedWebhooksPage(t)
m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
if m.Form == nil || m.Form.current() != hookFieldSecret || m.Form.Chosen != "s" {
t.Fatal("enter should edit the selected hook, starting at its secret")
}
if steps, _, _ := m.Form.steps(); len(steps) != 0 {
t.Fatalf("unchanged form produced %v", steps)
}
m.Form.Inputs[hookFieldSecret].SetValue("a-new-sixteen-byte-secret")
steps, _, err := m.Form.steps()
if err != nil || len(steps) != 1 || steps[0] != (daemon.WebhookRequest{Action: daemon.WebhookRotate, HookID: "wh1", Secret: "a-new-sixteen-byte-secret"}) {
t.Fatalf("steps = %v, %v", steps, err)
}
if strings.Contains(m.View(), "a-new-sixteen-byte-secret") {
t.Fatal("an entered secret was rendered")
}
}
func TestGeneratedSecretIsShownUntilDismissed(t *testing.T) {
m := loadedWebhooksPage(t)
m.Saving = true
m, _ = m.Update(webhooksSavedMsg{Gen: m.Generation, Notice: "added ci", Reveal: &webhookSecret{Hook: "ci", Secret: "whsec_generated"}})
if !strings.Contains(m.View(), "whsec_generated") {
t.Fatal("generated secret was not shown")
}
m, _ = m.Update(tea.KeyPressMsg{Code: 'd', Text: "d"})
if m.Reveal == nil || m.Confirm != "" {
t.Fatal("keys other than enter must not dismiss the secret")
}
m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
if m.Reveal != nil || strings.Contains(m.View(), "whsec_generated") {
t.Fatal("secret should be gone once dismissed")
}
}
func TestWebhookFormPicksTheSessionToWake(t *testing.T) {
m := loadedWebhooksPage(t)
m, _ = m.Update(tea.KeyPressMsg{Code: 'n', Text: "n"})
if m.Form.Chosen != "s" || !strings.Contains(ansi.Strip(m.View()), "session this session · infra bot · /srv/infra") {
t.Fatalf("the form should default to the session it was opened from:\n%s", ansi.Strip(m.View()))
}
m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyTab, Mod: tea.ModShift})
for _, r := range "release" {
m, _ = m.Update(tea.KeyPressMsg{Code: r, Text: string(r)})
}
if m.Form.Chosen != "t" {
t.Fatalf("filtering should choose the matching session, got %q", m.Form.Chosen)
}
m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyBackspace})
m.Form.Inputs[hookFieldSession].SetValue("")
m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyLeft})
if m.Form.Chosen != "s" {
t.Fatalf("← should step back to the first session, got %q", m.Form.Chosen)
}
m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyRight})
m.Form.Inputs[hookFieldName].SetValue("notes")
steps, notice, err := m.Form.steps()
if err != nil || steps[0] != (daemon.WebhookRequest{Action: daemon.WebhookCreate, SessionID: "t", Name: "notes"}) || !strings.Contains(notice, "release notes") {
t.Fatalf("steps = %v, %q, %v", steps, notice, err)
}
}
// A failed extension read cannot establish that deliveries are mounted.
func TestWebhookLoadSurfacesExtensionFailure(t *testing.T) {
requests := 0
conn := commandTestConnection(t, func(w http.ResponseWriter, _ *http.Request) {
requests++
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"error":"extension load refused"}`))
})
m := NewWebhooksPageModel(conn, "session")
msg := m.loadCmd(m.Generation)().(webhooksLoadedMsg)
if msg.Err == nil || msg.Mounted || requests != 1 {
t.Fatalf("extension failure treated as mounted: %+v requests=%d", msg, requests)
}
m, _ = m.Update(msg)
if m.Loaded || m.Error == "" {
t.Fatal("extension failure did not surface in screen")
}
}
// Creation is already committed when a following signature update fails.
func TestWebhookSaveRetainsCreatedSecretAfterLaterFailure(t *testing.T) {
requests := 0
conn := commandTestConnection(t, func(w http.ResponseWriter, _ *http.Request) {
requests++
if requests == 1 {
_, _ = w.Write([]byte(`{"result":{"hook":{"id":"created","session":"session","name":"deploy","url":"/webhooks/created","signatureHeader":"x-albedo-signature","signaturePrefix":"sha256=","enabled":true,"revision":1},"secret":"whsec_created","message":"created"}}`))
return
}
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"error":"signature refused"}`))
})
m := NewWebhooksPageModel(conn, "session")
msg := m.save(m.Generation, "added", daemon.WebhookRequest{Action: daemon.WebhookCreate, SessionID: "session", Name: "deploy"}, daemon.WebhookRequest{Action: daemon.WebhookSignature, Header: "x-custom", Prefix: ""})().(webhooksSavedMsg)
if msg.Err == nil || msg.Reveal == nil || msg.Reveal.Secret != "whsec_created" || requests != 2 {
t.Fatalf("lost committed secret: %+v requests=%d", msg, requests)
}
var apiErr *daemon.APIError
if !errors.As(msg.Err, &apiErr) {
t.Fatalf("lost failure: %v", msg.Err)
}
m, _ = m.Update(msg)
if m.Reveal == nil || m.Error == "" {
t.Fatal("partial success did not retain reveal and error")
}
}