Files
member-console/test/e2e/operator-walkthroughs/integration_settings_test.go
T
cgalo5758 d45a51d2e8 Profile-gate FedWiki and Discourse in the test stack
Put the fedwiki chain (init, render, farm, caddy) behind a fedwiki
compose profile symmetric with discourse's; the default composition is
neither, selected via COMPOSE_PROFILES in test/.env, so a default stack
no longer binds host 443.

Guard every script and walkthrough on service presence: shared
skipUnlessIntegrationEndpointReachable helper, seed-stack presence
checks (also repairing its unsourced .env and container-native render
invocation), generic root-owned testdata reclaim in teardown, discourse
coverage in verify-stack-isolation, and fedwiki's 8090 base in the port
probe.

Update stack docs and finalize status bookkeeping for all three
changes; archives the test-stack-integration-profiles change.
2026-08-01 04:14:05 -05:00

150 lines
6.5 KiB
Go

// walkthrough: integration-settings
// covers: POST /operator/integrations/{slug}/settings
//
// DELETE /operator/integrations/config-orphans/{key}
//
// Exercises the runtime-config surface end to end: save an override on an
// enum key (choosing whichever member is NOT currently effective, so the
// pending-restart badge must appear), clear it again, and delete a seeded
// orphan override row via the shared confirm-action modal. Overrides apply
// on restart by design, so the walkthrough asserts stored-state feedback
// (alerts, badges, row presence), not live behavior changes.
package operatorwalkthroughs_test
import (
"context"
"database/sql"
"strings"
"testing"
"time"
"github.com/google/uuid"
_ "github.com/jackc/pgx/v5/stdlib"
)
func TestIntegrationSettingsWalkthrough(t *testing.T) {
base := baseURL(t)
browser := newBrowser(t)
page := browser.MustPage()
loginAsOperator(t, page, base, "/operator/integrations")
t.Logf("phase 1: landing lists every integration with Settings links")
body := page.Timeout(10 * time.Second).MustElement(`#operator-body`).MustHTML()
for _, want := range []string{"/operator/integrations/fedwiki/settings", "/operator/integrations/stripe/settings", "/operator/integrations/discourse/settings"} {
if !strings.Contains(body, want) {
t.Fatalf("phase 1: landing missing settings link %q", want)
}
}
if !strings.Contains(body, "Stripe") {
t.Fatalf("phase 1: payments integration missing from the unified table")
}
// Phases 2 and 3 drive FedWiki's settings surface because it is the
// integration with an enum key to toggle, not because this walkthrough is
// about wikis. FedWiki is profile-gated like every other integration
// (COMPOSE_PROFILES in test/.env), so a stack composed without it must
// skip here rather than fail — the same contract Discourse's walkthrough
// has always had. An unconfigured farm is not a skip: the settings surface
// is config plumbing and never dials the farm, so the guard returns and
// the phases run.
skipUnlessFarmReachable(t, page, base)
t.Logf("phase 2: save an override for fedwiki-site-scheme (enum select)")
page.MustNavigate(base + "/operator/integrations/fedwiki/settings")
rowForm := `form:has(input[name="key"][value="fedwiki-site-scheme"])`
page.Timeout(10 * time.Second).MustElement(rowForm)
current := page.MustElement(rowForm + ` select[name="value"]`).MustProperty("value").String()
target := "http"
if current == "http" {
target = "https"
}
page.MustElement(rowForm + ` select[name="value"]`).MustSelect(target)
page.MustElement(rowForm + ` button[value="save"]`).MustClick()
page.Timeout(10*time.Second).MustElementR(`.alert-success`, "saved")
saved := page.MustElement(`#operator-body`).MustHTML()
if !strings.Contains(saved, "Pending restart") {
t.Errorf("phase 2: saved override differing from boot-effective value must show a Pending restart badge")
}
if !strings.Contains(saved, ">Override<") {
// Source badge stays on the boot-effective source until restart;
// the row's Clear button proves the stored override instead.
if !strings.Contains(saved, `button[value="clear"]`) && !strings.Contains(saved, `value="clear"`) {
t.Errorf("phase 2: stored override row must offer Clear")
}
}
t.Logf("phase 3: clear the override")
page.MustElement(rowForm + ` button[value="clear"]`).MustClick()
page.Timeout(10*time.Second).MustElementR(`.alert-success`, "cleared")
cleared := page.MustElement(`#operator-body`).MustHTML()
if strings.Contains(cleared, `value="clear"`) {
t.Errorf("phase 3: Clear button must disappear once no override is stored")
}
if strings.Contains(cleared, "Pending restart") {
t.Errorf("phase 3: cleared override matching boot state must not show Pending restart")
}
t.Logf("phase 4: delete a seeded orphan override via the confirm-action modal")
orphanKey := "e2e-orphan-" + uuid.New().String()[:8]
dsn := envFromTestDir(t)["MC_DB_DSN"]
if dsn == "" {
t.Fatal("MC_DB_DSN not set in test/.env")
}
database, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatalf("open database: %v", err)
}
t.Cleanup(func() { database.Close() })
ctx := context.Background()
if _, err := database.ExecContext(ctx,
"INSERT INTO core.integration_config_overrides (key, value) VALUES ($1, 'leftover')", orphanKey); err != nil {
t.Fatalf("seed orphan override: %v", err)
}
t.Cleanup(func() {
_, _ = database.ExecContext(ctx, "DELETE FROM core.integration_config_overrides WHERE key = $1", orphanKey)
})
page.MustNavigate(base + "/operator/integrations")
page.Timeout(10*time.Second).MustElementR(`#operator-body`, orphanKey)
page.MustElement(`button[data-action-url="/operator/integrations/config-orphans/` + orphanKey + `"]`).MustClick()
page.Timeout(10 * time.Second).MustElement(`#confirmActionModal.show`)
page.MustElement(`#confirmActionSubmit`).MustClick()
// HX-Redirect reloads the landing; the orphan row must be gone.
page.Timeout(10 * time.Second).MustWaitStable()
if after := page.MustElement(`#operator-body`).MustHTML(); strings.Contains(after, orphanKey) {
t.Errorf("phase 4: orphan override %q still listed after delete", orphanKey)
}
var n int
if err := database.QueryRowContext(ctx,
"SELECT count(*) FROM core.integration_config_overrides WHERE key = $1", orphanKey).Scan(&n); err != nil {
t.Fatalf("verify orphan deletion: %v", err)
}
if n != 0 {
t.Errorf("phase 4: orphan override row still in database after delete")
}
t.Logf("phase 5: unknown operator paths 404 in-shell; legacy fedwiki URL redirects")
page.MustNavigate(base + "/operator/integrations/stripe")
notFound := page.Timeout(10 * time.Second).MustElement(`#operator-body`).MustHTML()
if !strings.Contains(notFound, "Page not found") {
t.Errorf("phase 5: truncated settings URL should render the operator 404, got a different page")
}
if strings.Contains(notFound, "Your Wiki") || strings.Contains(notFound, "member-dashboard") {
t.Errorf("phase 5: unknown operator path leaked to the member dashboard")
}
page.MustNavigate(base + "/operator/fedwiki-sites")
page.Timeout(10 * time.Second).MustElement(`#operator-body`)
if got := page.MustInfo().URL; !strings.HasSuffix(got, "/operator/integrations/fedwiki") {
t.Errorf("phase 5: legacy /operator/fedwiki-sites should redirect to /operator/integrations/fedwiki, landed on %s", got)
}
fedwikiAdmin := page.MustElement(`#operator-body`).MustHTML()
for _, want := range []string{"Sync health", "Last sync", "/operator/integrations/fedwiki/settings"} {
if !strings.Contains(fedwikiAdmin, want) {
t.Errorf("phase 5: FedWiki admin page missing %q", want)
}
}
}