fix: stabilize software updates

This commit is contained in:
admin committed 2026-08-29 18:38:38 +02:00
1 parent fc59092030
commit 26a960c6bd
25 files changed
+487 -146

No files matched your search

+47 -7
View File
@@ -6,6 +6,7 @@ import (
"encoding/base64"
"encoding/json"
"log/slog"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
@@ -204,6 +205,31 @@ func TestRedirectNoticePreservesQueryAndAddsKind(t *testing.T) {
}
}
func TestParseFormAcceptsMultipartUpdateFields(t *testing.T) {
t.Parallel()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("csrf", "csrf-token"); err != nil {
t.Fatal(err)
}
if err := writer.WriteField("version", "v1.4.2"); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
request := httptest.NewRequest(http.MethodPost, "/system/update", &body)
request.Header.Set("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
server := &Server{}
if err := server.parseForm(response, request); err != nil {
t.Fatalf("multipart update form was rejected: status=%d body=%s", response.Code, response.Body.String())
}
if request.FormValue("csrf") != "csrf-token" || request.FormValue("version") != "v1.4.2" {
t.Fatalf("multipart fields were not parsed: %#v", request.Form)
}
}
func TestLoginErrorRendersAsToast(t *testing.T) {
t.Parallel()
templates, err := loadTemplates()
@@ -242,6 +268,7 @@ func TestDashboardRendersLiveUpdateHooks(t *testing.T) {
t.Fatalf("load templates: %v", err)
}
var rendered bytes.Buffer
now := time.Date(2026, time.August, 29, 11, 12, 0, 0, time.UTC)
data := viewData{
Title: "Übersicht",
Page: "dashboard",
@@ -251,7 +278,8 @@ func TestDashboardRendersLiveUpdateHooks(t *testing.T) {
Dashboard: model.Dashboard{
Settings: model.Settings{UpdaterEnabled: true, IPv6Enabled: true},
SchedulerRunning: true,
RecentRuns: []model.UpdateRun{{ID: 1, StartedAt: time.Now(), Status: "running"}},
RecentRuns: []model.UpdateRun{{ID: 1, StartedAt: now, Status: "running"}},
Zones: []model.Zone{{ID: 7, Name: "example.de", LastSyncAt: now}},
},
}
if err := templates["dashboard"].ExecuteTemplate(&rendered, "dashboard", data); err != nil {
@@ -264,7 +292,10 @@ func TestDashboardRendersLiveUpdateHooks(t *testing.T) {
`data-dashboard-ipv6`,
`data-recent-runs aria-live="polite" aria-busy="true"`,
`Aktualisierung läuft`,
`features-24`,
`features-27`,
`data-local-time="1788001920"`,
`dashboard-zone-item`,
`DNS-Zone · example.de`,
} {
if !strings.Contains(body, expected) {
t.Errorf("rendered dashboard is missing %q", expected)
@@ -480,13 +511,14 @@ func TestSoftwareUpdateRendersAdminNotificationAndProgressUI(t *testing.T) {
t.Fatal(err)
}
admin := model.User{ID: 1, Username: "admin", DisplayName: "Administrator", Role: "admin", Active: true}
updateState := appupdate.State{CurrentVersion: "v1.4.0", LatestVersion: "v2.0.0", UpdateAvailable: true, AgentAvailable: true, AgentCompatible: true, AgentVersion: "v1.4.0", AgentProtocol: 2, CredentialConfigured: true}
checkedAt := time.Date(2026, time.August, 29, 11, 12, 0, 0, time.UTC)
updateState := appupdate.State{CurrentVersion: "v1.4.0", LatestVersion: "v2.0.0", LastCheckedAt: checkedAt, UpdateAvailable: true, AgentAvailable: true, AgentCompatible: true, AgentVersion: "v1.4.0", AgentProtocol: 2, CredentialConfigured: true, CredentialUsername: "registry-user"}
var notification bytes.Buffer
accountData := viewData{Title: "Mein Konto", Page: "account", CSRF: "csrf", User: admin, IsAdmin: true, SoftwareUpdate: updateState}
if err := templates["security"].ExecuteTemplate(&notification, "security", accountData); err != nil {
t.Fatal(err)
}
for _, expected := range []string{"Neue Version verfügbar", "v2.0.0 ist bereit", `data-software-update-form`, `data-software-update-overlay`} {
for _, expected := range []string{"Neue Version verfügbar", "v2.0.0 ist bereit", `data-software-update-banner="v2.0.0"`, `data-software-update-form`, `data-software-update-overlay`} {
if !strings.Contains(notification.String(), expected) {
t.Errorf("global update notification is missing %q", expected)
}
@@ -497,7 +529,7 @@ func TestSoftwareUpdateRendersAdminNotificationAndProgressUI(t *testing.T) {
if err := templates["system"].ExecuteTemplate(&page, "system", systemData); err != nil {
t.Fatal(err)
}
for _, expected := range []string{"Software-Update", "Jetzt aktualisieren", "Image laden", "Dienst wechseln", "Healthcheck", "Update-Zugang ersetzen"} {
for _, expected := range []string{"Software-Update", "Jetzt aktualisieren", "Image laden", "Dienst wechseln", "Healthcheck", "Mit Gitea verbunden", "@registry-user", "Zugang ändern", "Passwort/Token", `<details class="update-credential-editor">`, `data-software-update-title`, `data-software-update-progress-label`, `data-software-update-progress-track`, `data-software-update-percent`, `data-local-time="1788001920"`} {
if !strings.Contains(page.String(), expected) {
t.Errorf("software update page is missing %q", expected)
}
@@ -507,7 +539,7 @@ func TestSoftwareUpdateRendersAdminNotificationAndProgressUI(t *testing.T) {
if err := templates["system"].ExecuteTemplate(&freshPage, "system", freshData); err != nil {
t.Fatal(err)
}
for _, expected := range []string{"Nach Update suchen", "Installiert", "Verfügbar", "Zugang fehlt", "Update-Zugang einrichten"} {
for _, expected := range []string{"Nach Update suchen", "Installiert", "Status", "Zugang fehlt", "Update-Zugang einrichten"} {
if !strings.Contains(freshPage.String(), expected) {
t.Errorf("fresh software update page is missing %q", expected)
}
@@ -515,6 +547,14 @@ func TestSoftwareUpdateRendersAdminNotificationAndProgressUI(t *testing.T) {
if strings.Contains(freshPage.String(), "Update-Kanal") {
t.Error("confusing legacy update channel box is still rendered")
}
var currentPage bytes.Buffer
currentData := viewData{Title: "Systemupdate", Page: "system", CSRF: "csrf", User: admin, IsAdmin: true, SoftwareUpdate: appupdate.State{CurrentVersion: "v1.4.1", LatestVersion: "v1.4.1", LastCheckedAt: checkedAt, AgentAvailable: true, AgentCompatible: true, CredentialConfigured: true}}
if err := templates["system"].ExecuteTemplate(&currentPage, "system", currentData); err != nil {
t.Fatal(err)
}
if !strings.Contains(currentPage.String(), `Status</span><strong>Aktuell</strong>`) || strings.Count(currentPage.String(), `>v1.4.1<`) != 1 {
t.Fatal("an installed current release is still rendered as a second available version")
}
var syncPage bytes.Buffer
syncData := viewData{Title: "Systemupdate", Page: "system", CSRF: "csrf", User: admin, IsAdmin: true, SoftwareUpdate: appupdate.State{CurrentVersion: "v1.4.0", LatestVersion: "v2.0.0", UpdateAvailable: true, AgentAvailable: true, ComposeSyncRequired: true, CredentialConfigured: true}}
if err := templates["system"].ExecuteTemplate(&syncPage, "system", syncData); err != nil {
@@ -608,7 +648,7 @@ func TestDomainDetailPlacesStatusAfterTimeAndRendersTTLAndCNAMEControls(t *testi
}
body := rendered.String()
statusAt := strings.Index(body, `A unverändert`)
timeAt := strings.Index(body, `class="target-meta"><time>`)
timeAt := strings.Index(body, `class="target-meta"><time data-local-time=`)
if timeAt < 0 || statusAt < timeAt {
t.Fatalf("target time must be rendered before the status pill: %s", body)
}