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.
150 lines
6.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|