Read the walkthrough guard's endpoint from test/.env

The reachability guard looked for the FedWiki farm's and the forum's
URLs as fields on the integration settings page. Both keys are
deployment-only, so the page shows static text and no field, and the
guard returned early: neither the name check nor the reachability skip
ever ran. On a network whose resolver drops loopback answers for
localtest.me, the sites walkthrough again timed out with its site left
Pending instead of naming the host (#236).

The guard now reads the URL from test/.env, under the variable the
console reads for that key, and no longer visits the settings page. A
table test pins the mapping from key to variable for both keys.
This commit is contained in:
2026-10-19 10:30:20 -05:00
parent de059233d6
commit e1bb295972
5 changed files with 36 additions and 36 deletions
@@ -43,11 +43,9 @@ const walkthroughGroup = "walkthrough-members"
//
// 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) {
func skipUnlessForumReachable(t *testing.T) {
t.Helper()
skipUnlessIntegrationEndpointReachable(t, page, base, "discourse", "discourse-base-url",
skipUnlessIntegrationEndpointReachable(t, "discourse", "discourse-base-url",
"start the discourse compose profile or the fakeserver (see this file's header)")
}
@@ -95,16 +93,13 @@ func submitMappingForm(page *rod.Page, groupName string) {
func TestDiscourseMappingWalkthrough(t *testing.T) {
defer browsertest.Recover(t)
page, base := browsertest.OpenOperator(t, "/operator/integrations/discourse")
page, _ := browsertest.OpenOperator(t, "/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")
skipUnlessForumReachable(t)
page.Timeout(10 * time.Second).MustElement(`form[hx-post="/partials/operator/discourse/mappings"]`)
t.Logf("phase 1a: empty group name → 422 + field error")
@@ -127,7 +127,7 @@ type sitesWalk struct {
func TestFedWikiSitesWalkthrough(t *testing.T) {
defer browsertest.Recover(t)
page, base := browsertest.OpenOperator(t, "/operator/integrations")
skipUnlessFarmReachable(t, page, base)
skipUnlessFarmReachable(t)
env := browsertest.EnvFromTestDir(t)
farmURL, farmToken := env["MC_FEDWIKI_FARM_API_URL"], env["MC_FEDWIKI_ADMIN_TOKEN"]
if farmURL == "" || farmToken == "" {
+19 -25
View File
@@ -65,15 +65,19 @@ func foundIn(html string, banned ...string) []string {
}
// skipUnlessIntegrationEndpointReachable probes the endpoint an integration is
// actually configured with — the effective value of key on that integration's
// settings page — and skips when it does not answer. It is the generic half of
// configured with and skips when it does not answer. It is the generic half of
// the pattern skipUnlessForumReachable applies to Discourse: a walkthrough may
// be pointed at a compose profile (or an external service) that simply is not
// composed, and every service-touching phase would then fail on connection
// errors instead of skipping.
//
// Three outcomes, deliberately distinguished:
// - no effective value rendered → return, so a walkthrough's config-plumbing
// The endpoint comes from test/.env, the file the console's environment is
// built from (endpointEnvVar names the variable). The settings page cannot
// supply it: these keys are deployment-only, so the page shows static text
// and no field holding the URL.
//
// Four outcomes, deliberately distinguished:
// - no http value in test/.env → return, so a walkthrough's config-plumbing
// phases still run against an integration nobody configured;
// - configured but refusing connections → skip;
// - configured but answering 5xx → skip (container up, service still booting
@@ -82,27 +86,10 @@ func foundIn(html string, banned ...string) []string {
// point at this machine that resolves elsewhere → fail, naming the
// /etc/hosts lines that fix it (requireResolvesHere).
//
// Reading the URL off the settings page rather than off test/.env keeps the
// probe truthful for every bootstrap — compose profile, fake server, or a
// service hosted outside compose entirely. Leaves the page on the settings
// surface; callers must navigate onward themselves.
// hint names, in the caller's own words, what would make the endpoint answer.
func skipUnlessIntegrationEndpointReachable(t *testing.T, page *rod.Page, base, integrationKey, key, hint string) {
func skipUnlessIntegrationEndpointReachable(t *testing.T, integrationKey, key, hint string) {
t.Helper()
page.Timeout(15 * time.Second).MustNavigate(base + "/operator/integrations/" + integrationKey + "/settings")
// The settings page is one declared form, one field per non-secret key
// (operator.integration.settings, forms-library lane D): the endpoint
// URL is a plain text field named for its key, not a table row, so its
// configured value is the field's own value. Wait for the form, then
// read the field; an absent or non-http value means the endpoint is not
// configured and the guard proceeds (the settings surface never dials
// the endpoint, so an unconfigured one still exercises the phases).
page.Timeout(10 * time.Second).MustElement(`form[data-form="operator.integration.settings"]`)
has, field, err := page.Has(`[name="` + key + `"]`)
if err != nil || !has {
return
}
endpoint := strings.TrimSpace(field.MustProperty("value").String())
endpoint := strings.TrimSpace(browsertest.EnvFromTestDir(t)[endpointEnvVar(key)])
if !strings.HasPrefix(endpoint, "http") {
return
}
@@ -118,6 +105,13 @@ func skipUnlessIntegrationEndpointReachable(t *testing.T, page *rod.Page, base,
}
}
// endpointEnvVar names the environment variable the console reads for a
// config key: the MC prefix, the key upper-cased, and "-" replaced by "_"
// (viper.SetEnvPrefix, SetEnvKeyReplacer and AutomaticEnv in cmd/root.go).
func endpointEnvVar(key string) string {
return "MC_" + strings.ToUpper(strings.ReplaceAll(key, "-", "_"))
}
// resolutionProblem says why host's lookup answer does not let a test reach
// it, or "" when it does: the lookup failed, it returned no address, or a
// name defined to point at this machine (localhost, *.localhost,
@@ -176,9 +170,9 @@ func requireResolvesHere(t *testing.T, integrationKey, endpoint string) {
// skipUnlessFarmReachable is the FedWiki binding of the guard above. The farm
// API URL is the endpoint every FedWiki surface dials, and the `fedwiki`
// compose profile is what composes a farm to answer it.
func skipUnlessFarmReachable(t *testing.T, page *rod.Page, base string) {
func skipUnlessFarmReachable(t *testing.T) {
t.Helper()
skipUnlessIntegrationEndpointReachable(t, page, base, "fedwiki", "fedwiki-farm-api-url",
skipUnlessIntegrationEndpointReachable(t, "fedwiki", "fedwiki-farm-api-url",
`add "fedwiki" to COMPOSE_PROFILES in test/.env, bring the stack up, and uncomment the MC_FEDWIKI_* block`)
}
@@ -47,7 +47,7 @@ func TestIntegrationSettingsWalkthrough(t *testing.T) {
// 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)
skipUnlessFarmReachable(t)
settingsSaveOverride(t, page, base)
originalInterval := settingsRefuseDuration(t, page)
@@ -36,3 +36,14 @@ func TestResolutionProblem(t *testing.T) {
})
}
}
func TestEndpointEnvVar(t *testing.T) {
for key, want := range map[string]string{
"fedwiki-farm-api-url": "MC_FEDWIKI_FARM_API_URL",
"discourse-base-url": "MC_DISCOURSE_BASE_URL",
} {
if got := endpointEnvVar(key); got != want {
t.Errorf("endpointEnvVar(%q) = %q, want %q", key, got, want)
}
}
}