Files
felhom.eu/hub/internal/web/artifacts_form_render_test.go
T
admin 4a4233059d hub v0.68.1 — fix the Configuration layout broken by the wrapper-sha field
The v0.68.0 row wrapped itself in a <div>, but the artifacts <form> IS the CSS grid
(display:grid, no inner container). The stray </div> closed the surrounding card from
inside the form and the new <div> was never closed — it swallowed the submit button and
ran to </form>, so the row rendered outside the card and Save landed inline. Reported by
the operator on first use.

The field still submitted (it stayed inside the form), so this was layout damage rather
than data loss, but the unbalanced markup put every section below it in the wrong
container.

Fixed as plain grid cells (grid-column: 2/4), no nested elements.

There was no render assertion on this form at all, which is why a hand-edit broke it
silently. The new test asserts the field is inside the form, the button has not escaped,
the form contains ZERO divs, whole-page div balance holds, and the sections after it
survive. Red-proofed against the broken shape.
2026-07-21 10:34:34 +02:00

114 lines
4.6 KiB
Go

package web
import (
"bytes"
"net/http/httptest"
"regexp"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// R-50b(a) — the wrapper-sha field must render INSIDE the artifact form, and the form's grid must
// stay structurally intact.
//
// This test exists because the first attempt at this field shipped broken: it wrapped the row in a
// <div> and, since the <form> ITSELF is the CSS grid (`display: grid`, no inner container), the
// stray `</div>` closed the surrounding CARD from inside the form and the newly-opened <div> was
// never closed — it swallowed the submit button and ran to </form>. The page rendered with the row
// escaped outside the card and the Save button inline. Nothing failed: there was no render
// assertion on this form at all.
func TestConfigurationArtifactsForm_Structure(t *testing.T) {
s, _ := newTestServer(t)
data := map[string]interface{}{
"CSRFToken": "tok",
"CSRFField": s.csrfField(httptest.NewRequest("GET", "/", nil)),
"AssetCount": 0,
"AssetLastSync": "",
"GlobalFloor": "0.86.0",
"Artifacts": store.ArtifactManifest{
AgentVersion: "0.91.2",
GoldenVersion: "0.153.0",
MinAgent: "0.91.2",
WrapperSHA256: "104db0a4401f65bbc476e82bfb1796433bcb36f8f8cce69efb3bb5c40fcb16b3",
},
}
var buf bytes.Buffer
if err := s.templates.ExecuteTemplate(&buf, "configuration.html", data); err != nil {
t.Fatalf("render: %v", err)
}
body := buf.String()
// 1. The field renders, carries its stored value, and is reachable for the operator.
if !strings.Contains(body, `name="wrapper_sha256"`) {
t.Fatal("the wrapper sha256 input is missing from the Configuration page")
}
if !strings.Contains(body, "104db0a4401f65bbc476e82bfb1796433bcb36f8f8cce69efb3bb5c40fcb16b3") {
t.Error("the stored wrapper sha256 is not rendered into the field — the operator cannot see what is vouched")
}
// 2. THE STRUCTURAL ASSERTION. It must sit inside the artifacts <form>, or it is never submitted.
form := artifactsForm(t, body)
if !strings.Contains(form, `name="wrapper_sha256"`) {
t.Error("the wrapper sha256 input is OUTSIDE the artifacts form — it would never be submitted")
}
if !strings.Contains(form, "Save artifact manifest") {
t.Error("the submit button escaped the artifacts form")
}
// 3. The form is the grid: any <div> inside it breaks the column layout and, unbalanced, breaks
// the surrounding card too. That is exactly how this shipped wrong the first time.
if n := strings.Count(form, "<div"); n != 0 {
t.Errorf("the artifacts form contains %d <div> element(s) — the form IS the CSS grid; "+
"a nested div breaks the column layout", n)
}
if o, c := strings.Count(form, "<div"), strings.Count(form, "</div>"); o != c {
t.Errorf("unbalanced div tags inside the artifacts form: %d open, %d close", o, c)
}
// 4. Whole-page div balance — an unclosed div here corrupts every section BELOW it (the login
// password card and the assets card both render after this form).
if o, c := strings.Count(body, "<div"), strings.Count(body, "</div>"); o != c {
t.Errorf("configuration.html has unbalanced divs: %d open, %d close — sections after the "+
"artifact form will render inside the wrong container", o, c)
}
// 5. The sections that follow must still be present and not swallowed.
for _, want := range []string{"Login password", "Change password", "Assets", "Refresh Assets from Image"} {
if !strings.Contains(body, want) {
t.Errorf("section %q missing — likely swallowed by an unclosed element above it", want)
}
}
}
var artifactsFormRe = regexp.MustCompile(`(?s)<form[^>]*action="/configuration/artifacts"[^>]*>(.*?)</form>`)
func artifactsForm(t *testing.T, body string) string {
t.Helper()
m := artifactsFormRe.FindStringSubmatch(body)
if m == nil {
t.Fatal("could not locate the artifacts form")
}
return m[1]
}
// An empty stored value must render an empty field (blank = not vouched), not the string "<no value>"
// or a zero — the operator reads this field to decide whether anything is vouched at all.
func TestConfigurationArtifactsForm_EmptyWrapperSHA(t *testing.T) {
s, _ := newTestServer(t)
var buf bytes.Buffer
err := s.templates.ExecuteTemplate(&buf, "configuration.html", map[string]interface{}{
"CSRFToken": "tok", "CSRFField": s.csrfField(httptest.NewRequest("GET", "/", nil)),
"AssetCount": 0, "AssetLastSync": "", "GlobalFloor": "",
"Artifacts": store.ArtifactManifest{},
})
if err != nil {
t.Fatalf("render: %v", err)
}
form := artifactsForm(t, buf.String())
if !strings.Contains(form, `name="wrapper_sha256" value=""`) {
t.Errorf("an unvouched wrapper must render an empty value; got:\n%s", form)
}
}