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.
125 lines
5.4 KiB
Go
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")
|
|
}
|