Files
member-console/test/e2e/operator-walkthroughs/discourse_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

125 lines
5.4 KiB
Go

// walkthrough: discourse
// covers: POST /partials/operator/discourse/mappings
//
// DELETE /partials/operator/discourse/mappings/{mappingID}
//
// Exercises bullets 1 (422 + field errors, twice: empty input and
// unknown-group validation against the forum), 2 (success toast + DOM
// update), and 3 (destructive confirm modal with mapping-specific copy)
// for the Discourse group-mapping surface. Bullet 4 (forced 5xx) is owned
// generically by error_contract_test.go.
//
// Requires the app to be booted with the Discourse integration configured
// against the fake forum — skips cleanly otherwise:
//
// go run ./internal/integrations/discourse/discoursetest/fakeserver \
// -addr 127.0.0.1:9457 -api-key test-key -seed-group walkthrough-members
// cd test && go run .. start --config mc-config.yaml \
// --discourse-base-url http://127.0.0.1:9457 --discourse-api-key test-key
package operatorwalkthroughs_test
import (
"strings"
"testing"
"time"
"github.com/go-rod/rod"
)
const walkthroughGroup = "walkthrough-members"
// skipUnlessForumReachable probes the forum base URL the app is actually
// configured with — the effective discourse-base-url shown on the integration
// settings page — and skips when the forum is down. The admin page's
// "not configured" guard cannot catch this case: the app can be fully
// configured against the opt-in discourse compose profile (or a fakeserver)
// that simply is not running, and every forum-touching phase would then fail
// on connection errors instead of skipping.
//
// The probe itself is the shared one in helpers_test.go, which FedWiki's
// guard also binds; only the key and the operator-facing remedy differ.
// Leaves the page on the settings surface; callers must navigate onward
// themselves.
func skipUnlessForumReachable(t *testing.T, page *rod.Page, base string) {
t.Helper()
skipUnlessIntegrationEndpointReachable(t, page, base, "discourse", "discourse-base-url",
"start the discourse compose profile or the fakeserver (see this file's header)")
}
// waitBodyContains polls #operator-body until it contains substr —
// content-based synchronization, because element-presence waits can match
// the PREVIOUS swap's DOM (phase N's .is-invalid is indistinguishable from
// phase N+1's) and HTMX swaps replace elements mid-interaction.
func waitBodyContains(t *testing.T, page *rod.Page, substr, phase string) string {
t.Helper()
deadline := time.Now().Add(10 * time.Second)
var body string
for {
body = page.MustElement(`#operator-body`).MustHTML()
if strings.Contains(body, substr) {
return body
}
if time.Now().After(deadline) {
t.Fatalf("%s: %q never appeared in #operator-body; body: %.1500s", phase, substr, body)
}
time.Sleep(150 * time.Millisecond)
}
}
func submitMappingForm(page *rod.Page, groupName string) {
if groupName != "" {
page.MustElement(`#discourse-mapping-group-name`).MustSelectAllText().MustInput(groupName)
}
page.MustElement(`form[hx-post="/partials/operator/discourse/mappings"] button[type=submit]`).MustClick()
}
func TestDiscourseMappingWalkthrough(t *testing.T) {
base := baseURL(t)
browser := newBrowser(t)
page := browser.MustPage()
loginAsOperator(t, page, base, "/operator/integrations/discourse")
if strings.Contains(page.MustElement(`#operator-body`).MustHTML(), "Discourse is not configured") {
t.Skip("app booted without Discourse config; see this file's header for the fakeserver + flags bootstrap")
}
skipUnlessForumReachable(t, page, base)
// The probe leaves the page on the settings surface; return to the
// mapping surface before driving the form phases.
page.Timeout(15 * time.Second).MustNavigate(base + "/operator/integrations/discourse")
page.Timeout(10 * time.Second).MustElement(`form[hx-post="/partials/operator/discourse/mappings"]`)
t.Logf("phase 1a: empty group name → 422 + field error")
page.MustEval(`() => { const f = document.querySelector('form[hx-post="/partials/operator/discourse/mappings"]'); if (f) f.noValidate = true; }`)
submitMappingForm(page, "")
waitBodyContains(t, page, "Enter the Discourse group name", "phase 1a")
t.Logf("phase 1b: unknown group → 422 + forum-validation error")
submitMappingForm(page, "no-such-group-zz")
body := waitBodyContains(t, page, "No group with that name", "phase 1b")
if !strings.Contains(body, "is-invalid") {
t.Errorf("phase 1b: field not marked is-invalid")
}
t.Logf("phase 2: valid group → success toast + mapping row")
submitMappingForm(page, walkthroughGroup)
body = waitBodyContains(t, page, walkthroughGroup+"</td>", "phase 2")
if !strings.Contains(body, "Remove") {
t.Errorf("phase 2: mapping row lacks its Remove action")
}
page.Timeout(10 * time.Second).MustElement(`#successToast.show`)
if toast := page.MustElement(`#successToastBody`).MustText(); !strings.Contains(strings.ToLower(toast), "created") {
t.Errorf("phase 2: toast = %q, want it to mention 'created'", toast)
}
t.Logf("phase 3: remove mapping → confirm modal with mapping copy, then deletion")
page.MustElement(`button[data-bs-target="#confirmActionModal"][data-action-method="delete"]`).MustClick()
page.Timeout(10 * time.Second).MustElement(`#confirmActionModal.show`)
if modalText := page.MustElement(`#confirmActionModal`).MustText(); !strings.Contains(modalText, "Stop managing membership") {
t.Errorf("phase 3: modal copy = %q, want the stop-managing warning", modalText)
}
page.MustElement(`#confirmActionSubmit`).MustClick()
waitBodyContains(t, page, "No groups are mapped yet", "phase 3")
}