W21 sharing switched on without a password asks for it and says why it waits; W25 failed off-site run shows the household a sentence built from the classifier, raw text only to log and operator; W28 share restore skips the controller's drop-zone share; unreleased
gates / gates (push) Successful in 1m40s

This commit is contained in:
2026-10-21 23:19:07 +02:00
parent 28b05d6d68
commit 5776d54901
16 changed files with 330 additions and 7 deletions
+77 -1
View File
@@ -271,6 +271,66 @@ func offsiteFailureMessage(t *settings.OffboxTarget, err error, dur time.Duratio
return fmt.Sprintf("%s (%s): %s", head, dur.Round(time.Second), sanitiseOffsiteErrorFor(t, err))
}
// offsiteSharesName marks the shares source in offboxRunResult.failedApps; offsiteHouseholdFailure names it in
// the household's language.
const offsiteSharesName = SharesPseudoStack
// offsiteHouseholdFailure is the sentence the household reads on the „Távoli mentés" page after a failed run (W25,
// rehearsal 2026-10-20 — it read „Utolsó hiba: offbox backup immich: exit status 1"). Built from the failure
// classifier (07 §6.7) and the run's own result, in the box's language, with NO raw error text: that goes to the
// log and, sanitised, to the operator's event (OffsiteFailureMessage), unchanged.
//
// - apps failed while the run went on: they are named; „the other apps are fine" only when at least one app was
// copied and no other app was left out or left incomplete;
// - otherwise our own keyed sentence when the error carries one (the quota refusal, with its numbers), the apps
// with no local unit when there was nothing to send, or the run's cause, one sentence per class
// (note.offsite.fail_<class>);
// - „the box tries again the next night" only when a nightly run will come AND a retry can help (not for a full
// store, a lost key, or nothing to send — those need a person).
//
// Pinned by TestW25_FailedOffsiteRunSpeaksToTheHousehold.
func offsiteHouseholdFailure(lang string, runErr error, res offboxRunResult, nightly bool) string {
class := ClassifyOffsiteFailure(runErr)
var parts []string
retry := false
if len(res.failedApps) > 0 {
names := make([]string, 0, len(res.failedApps))
for _, n := range res.failedApps {
if n == offsiteSharesName {
n = util.Text(lang, "note.offsite.shares_name")
}
names = append(names, n)
}
parts = append(parts, util.Text(lang, "note.offsite.fail_apps", strings.Join(names, ", ")))
if res.backedUp > 0 && len(res.missingUnprotected) == 0 && len(res.mandatoryGaps) == 0 {
parts = append(parts, util.Text(lang, "note.offsite.others_ok"))
}
retry = class != OffsiteFailQuota && class != OffsiteFailOrphaned
} else if msg, ok := util.AsMsg(runErr); ok {
// Our own sentence (the quota refusal with its numbers and its next step): already the household's.
parts = append(parts, msg.Text(lang))
retry = class == OffsiteFailTransport || class == OffsiteFailLocked || class == OffsiteFailUnknown
} else if class == OffsiteFailNoUnits && len(res.missing) > 0 {
parts = append(parts, util.Text(lang, "note.offsite.no_local_unit", strings.Join(res.missing, ", ")))
} else {
key := map[OffsiteFailureClass]string{
OffsiteFailQuota: "note.offsite.fail_quota", OffsiteFailOrphaned: "note.offsite.fail_orphaned",
OffsiteFailNoRepo: "note.offsite.fail_no_repo", OffsiteFailNoUnits: "note.offsite.fail_no_units",
OffsiteFailTransport: "note.offsite.fail_transport", OffsiteFailUnknown: "note.offsite.fail_unknown",
OffsiteFailLocked: "note.offsite.fail_locked",
}[class]
if key == "" {
key = "note.offsite.fail_head"
}
parts = append(parts, util.Text(lang, key)+".")
retry = class == OffsiteFailTransport || class == OffsiteFailLocked || class == OffsiteFailUnknown
}
if retry && nightly {
parts = append(parts, util.Text(lang, "note.offsite.retry_next_night"))
}
return strings.Join(parts, " ")
}
func defaultOffboxSSH(ctx context.Context, host, user string, port int, keyPath, knownHosts, remoteCmd string) ([]byte, error) {
if port == 0 {
port = 22
@@ -1099,6 +1159,10 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
// everything else that touches settings.json on that box. Written this way once and caught by the
// suite timing out at 25 minutes; TestNoteHelpersAreNotCalledUnderTheSettingsLock keeps it fixed.
lang := m.boxLang()
householdNote := ""
if runErr != nil {
householdNote = offsiteHouseholdFailure(lang, runErr, runResult, t != nil && t.Enabled && t.Schedule == "daily")
}
if perr := m.settings.UpdateOffboxStatus(func(o *settings.OffboxTarget) {
o.LastRun = time.Now().UTC().Format(time.RFC3339)
// R-100: LastRun above records the ATTEMPT; this records the RESULT. The hub's staleness
@@ -1115,7 +1179,7 @@ func (m *Manager) runOffboxBackup(ctx context.Context, withProgress bool) error
o.LastWarningKind = ""
} else if runErr != nil {
o.LastStatus = "error"
o.LastError = runErr.Error()
o.LastError = householdNote // W25: the household's sentence; the raw text is in the log and the operator's event
o.LastWarning = ""
o.LastWarningKind = ""
} else {
@@ -1371,6 +1435,10 @@ type offboxRunResult struct {
// longer installed is named but does not, so a removed app cannot leave the box amber forever.
missingUnprotected []string
missingNotDeployed []string
// failedApps (W25) names, for the household, every app whose push FAILED while the run went on — the
// display name from the app's recovery unit (the stack name when the unit has none), and the shares
// source under its own name. firstErr still carries the first raw error for the log and the operator.
failedApps []string
}
// stackDeployed reports whether the stack is currently deployed on this box. Used only to classify a
@@ -1481,6 +1549,11 @@ func (m *Manager) runOffboxInternal(ctx context.Context, apps, base, env []strin
if firstErr == nil {
firstErr = fmt.Errorf("offbox backup %s: %w", stack, berr)
}
display := stack
if man := readManifest(UnitManifestFile(src)); man != nil && strings.TrimSpace(man.DisplayName) != "" {
display = man.DisplayName
}
res.failedApps = append(res.failedApps, display)
continue
}
res.backedUp++
@@ -1517,6 +1590,9 @@ func (m *Manager) runOffboxInternal(ctx context.Context, apps, base, env []strin
if sharesRes.ran {
res.sharesBackedUp = true
}
if sharesErr != nil {
res.failedApps = append(res.failedApps, offsiteSharesName)
}
if sharesErr != nil && firstErr == nil {
firstErr = sharesErr
}
@@ -241,6 +241,16 @@ func (m *Manager) PlaceSharesRestore(ctx context.Context) (SharesRestoreResult,
// THE PREFIX ASSERT. The snapshot's path is untrusted layout input; a destination that is not
// strictly inside a live registered root is refused, never created.
if !m.liveShareRootOK(sh.Path) {
// W28 (rehearsal 2026-10-20): the controller's OWN drop-zone share („beolvasas", R-75) lives on the
// SYSTEM drive, never under a data-drive root, so it is always refused here — and the household read
// „Nem állítható vissza (…): beolvasas." about a share it never made. Its definition comes back by
// itself (ensureImportShare, whenever sharing is on) and its folder is an inbox apps empty, so it is
// skipped without a word to the household; the refusal of any OTHER share is unchanged.
// Pinned by TestW28_TheSystemDropZoneIsNotNamedAsRefused.
if sh.System {
m.logger.Printf("[INFO] [shares] restore skipped for the system drop-zone share %s — the controller re-creates it while sharing is on", sh.Name)
continue
}
m.logger.Printf("[WARN] [shares] restore refused for %s — destination is not under a live storage root", sh.Name)
res.Refused = append(res.Refused, sh.Name)
continue
@@ -266,3 +266,40 @@ func TestSharesRestoreFlashMessage(t *testing.T) {
// jsonMarshalIndent is a tiny indirection so the test file needs no direct encoding/json import
// beyond this one helper.
func jsonMarshalIndent(v any) ([]byte, error) { return json.MarshalIndent(v, "", " ") }
// W28 (rehearsal 2026-10-20): the share restore said „Nem állítható vissza (a mappa nincs élő adatmeghajtón):
// beolvasas." — „beolvasas" is the controller's own drop-zone share (R-75, System), which lives on the system drive
// and is re-created by the controller while sharing is on. It is skipped without naming it; a household share outside
// every live root is still refused and named.
func TestW28_TheSystemDropZoneIsNotNamedAsRefused(t *testing.T) {
env := newSharesEnv(t, "hdd_1")
dropZone := filepath.Join(env.tmp, "system", "felhom-data", "userdata", "import")
outside := filepath.Join(env.tmp, "kivul", "csaladi")
mf := SharesManifest{
Version: sharesManifestVersion, ServerName: "FELHOM",
Shares: []settings.SMBShare{
{Name: settings.SystemImportShareName, Path: dropZone, System: true, CreatedAt: "2026-07-18T00:00:00Z"},
{Name: "csaladi", Path: outside, Offsite: true, CreatedAt: "2026-07-18T00:00:00Z"},
},
}
seedSharesScratch(t, env, mf, map[string]string{filepath.Join(dropZone, "scan.pdf"): "X"})
var passdbCalls int
wirePlaceSeams(env, &passdbCalls)
res, err := env.m.PlaceSharesRestore(t.Context())
if err != nil {
t.Fatal(err)
}
msg := res.FlashMessage()
if strings.Contains(msg, settings.SystemImportShareName) {
t.Errorf("the household was told about the controller's drop-zone share: %q", msg)
}
if len(res.Refused) != 1 || res.Refused[0] != "csaladi" || !strings.Contains(msg, "csaladi") {
t.Errorf("a household share outside every live root must still be refused and named: %+v %q", res.Refused, msg)
}
if _, sErr := os.Stat(filepath.Join(dropZone, "scan.pdf")); !os.IsNotExist(sErr) {
t.Error("the drop-zone's files were placed — the skip must write nothing")
}
if len(env.sett.GetSMBShares()) != 0 {
t.Errorf("a skipped share must not enter the registry here: %+v", env.sett.GetSMBShares())
}
}
@@ -0,0 +1,69 @@
package backup
import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"testing"
"time"
)
// W25 (rehearsal 2026-10-20): a failed off-site run showed the household „Utolsó hiba: offbox backup immich: exit
// status 1". The household now reads which app failed (its own name), that the other apps are fine when they are,
// and that the box tries again the next night; the raw text stays in the log and reaches the operator's event.
func TestW25_FailedOffsiteRunSpeaksToTheHousehold(t *testing.T) {
m, sett := newOffboxManager(t)
usb := t.TempDir()
addSchedulablePath(t, sett, usb)
ns := m.namespaceRoot(usb)
writeUnit(t, ns, "immich", "2026-10-20T00:00:00Z")
writeUnit(t, ns, "mealie", "2026-10-20T00:00:00Z")
b, _ := json.Marshal(RecoveryManifest{AppName: "immich", DisplayName: "Immich Fotók", CreatedAt: "2026-10-20T00:00:00Z"})
if err := os.WriteFile(RecoveryUnitManifestPath(ns, "immich"), b, 0o644); err != nil {
t.Fatal(err)
}
_ = sett.SetAppOffbox("immich", true)
_ = sett.SetAppOffbox("mealie", true)
rr := &recordingOffboxRunner{backupErr: map[string]error{"immich": errors.New("exit status 1")}}
m.SetOffboxRunner(rr.run)
var operatorErr error
m.SetOffboxNotify(func(_ time.Duration, _ int, err error) { operatorErr = err })
if err := m.RunOffboxBackup(context.Background()); err == nil {
t.Fatal("a run with a failed app must report an error")
}
got := sett.GetOffboxTarget().LastError
for _, want := range []string{"Nem sikerült a távoli mentés: Immich Fotók.", "A többi alkalmazás távoli mentése rendben van.",
"A doboz a következő éjjel újra próbálkozik."} {
if !strings.Contains(got, want) {
t.Errorf("the household's note lacks %q: %q", want, got)
}
}
for _, raw := range []string{"exit status", "offbox backup", "immich:"} {
if strings.Contains(got, raw) {
t.Errorf("raw technical text %q reached the household's note: %q", raw, got)
}
}
// the operator's event keeps the technical detail, sanitised
if op := m.OffsiteFailureMessage(operatorErr, time.Second); !strings.Contains(op, "offbox backup immich: exit status 1") {
t.Errorf("the operator's event lost the technical detail: %q", op)
}
// English, and the cases where the other apps are NOT fine or no night comes
res := offboxRunResult{backedUp: 1, failedApps: []string{"Immich", offsiteSharesName}}
runErr := errors.New("offbox backup immich: exit status 1")
en := offsiteHouseholdFailure("en", runErr, res, true)
if en != "The remote backup did not work for: Immich, Shared folders. The remote backup of the other apps is fine. The box tries again the next night." {
t.Errorf("en: %q", en)
}
res.missingUnprotected = []string{"paperless"}
if hu := offsiteHouseholdFailure("hu", runErr, res, false); strings.Contains(hu, "többi") || strings.Contains(hu, "éjjel") {
t.Errorf("with an app left out the others are not all fine, and with no nightly run there is no retry: %q", hu)
}
// a whole-run failure: its class sentence, never the raw text
tr := offsiteHouseholdFailure("hu", errors.New("ssh: connect to host x port 23: connection refused"), offboxRunResult{}, true)
if tr != "A távoli tárhely nem érhető el (hálózat vagy bejelentkezés). A doboz a következő éjjel újra próbálkozik." {
t.Errorf("transport: %q", tr)
}
}
+7
View File
@@ -1599,6 +1599,7 @@
"flash.sharing.save_failed": "That could not be saved.",
"flash.sharing.saved": "Setting saved.",
"flash.sharing.saved_preparing": "Setting saved. The sharing service is being prepared…",
"flash.sharing.saved_needs_password": "Setting saved. Sharing starts when you set the sharing password.",
"flash.tier2.app_email_off": "Email sending is off for this app.",
"flash.tier2.app_email_on": "Email sending is on for this app.",
"flash.tier2.save_failed": "The setting could not be saved.",
@@ -1844,6 +1845,10 @@
"note.offsite.fail_quota": "The remote backup did not fit inside its storage limit",
"note.offsite.fail_transport": "The remote store cannot be reached (network or sign-in)",
"note.offsite.fail_unknown": "The remote backup failed for an unknown reason",
"note.offsite.fail_apps": "The remote backup did not work for: %s.",
"note.offsite.others_ok": "The remote backup of the other apps is fine.",
"note.offsite.retry_next_night": "The box tries again the next night.",
"note.offsite.shares_name": "Shared folders",
"note.offsite.no_local_unit": "These apps did NOT go into the remote backup, because they have no local backup unit yet: %s. The next backup usually makes one — if they are still listed after the second run, tell your operator.",
"note.offsite.not_installed": "These apps are selected for remote backup but are not installed, so they cannot be backed up: %s. If you no longer need them, clear their selection on the Remote backup page.",
"note.offsite.quota_partial": "Warning: because of the storage limit, %d apps got only a settings and database backup: %s.",
@@ -2189,6 +2194,8 @@
"sharing.egy_kozos_jelszo_tartozik_a": "The household has one shared password. When connecting, use this as the username:",
"sharing.egyes_halozatokon_a_gep_kesve": "On some networks the machine appears late in the device list. Typing the",
"sharing.eloszor_adj_meg_jelszot": "set a password first",
"sharing.needs_password_why": "Set a sharing password. Without it, sharing does not start.",
"sharing.needs_password_link": "Set the password",
"sharing.ezen_a_neven_jelenik_meg": "The machine appears on the network under this name (",
"sharing.ezt_valasztom": "Choose this",
"sharing.felhomentes": "Cloud backup",
+7
View File
@@ -1590,6 +1590,7 @@
"flash.sharing.save_failed": "A mentés nem sikerült.",
"flash.sharing.saved": "Beállítás mentve.",
"flash.sharing.saved_preparing": "Beállítás mentve. A megosztási szolgáltatás előkészítése folyamatban…",
"flash.sharing.saved_needs_password": "Beállítás mentve. A megosztás akkor indul el, amikor beállítod a megosztási jelszót.",
"flash.tier2.app_email_off": "Email-küldés kikapcsolva ennél az alkalmazásnál.",
"flash.tier2.app_email_on": "Email-küldés bekapcsolva ennél az alkalmazásnál.",
"flash.tier2.save_failed": "A beállítás mentése nem sikerült.",
@@ -1832,6 +1833,10 @@
"note.offsite.fail_quota": "A távoli mentés nem fért el a tárhelykereten belül",
"note.offsite.fail_transport": "A távoli tárhely nem érhető el (hálózat vagy bejelentkezés)",
"note.offsite.fail_unknown": "A távoli mentés ismeretlen okból nem sikerült",
"note.offsite.fail_apps": "Nem sikerült a távoli mentés: %s.",
"note.offsite.others_ok": "A többi alkalmazás távoli mentése rendben van.",
"note.offsite.retry_next_night": "A doboz a következő éjjel újra próbálkozik.",
"note.offsite.shares_name": "Megosztások",
"note.offsite.no_local_unit": "Ezek az alkalmazások NEM kerültek be a távoli mentésbe, mert még nincs helyi mentési egységük: %s. A következő mentés általában már elkészíti — ha a második futás után is itt szerepelnek, szólj az üzemeltetőnek.",
"note.offsite.not_installed": "Ezek az alkalmazások ki vannak jelölve távoli mentésre, de nincsenek telepítve, ezért nem menthetők: %s. Ha már nincs rájuk szükséged, vedd ki a kijelölésüket a Távoli mentés oldalon.",
"note.offsite.quota_partial": "Figyelmeztetés: a tárhelykeret miatt %d alkalmazásnál csak konfiguráció- és adatbázis-mentés készült: %s.",
@@ -2177,6 +2182,8 @@
"sharing.egy_kozos_jelszo_tartozik_a": "Egy közös jelszó tartozik a háztartáshoz. A csatlakozáskor felhasználónévnek add meg:",
"sharing.egyes_halozatokon_a_gep_kesve": "Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A",
"sharing.eloszor_adj_meg_jelszot": "először adj meg jelszót",
"sharing.needs_password_why": "Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el.",
"sharing.needs_password_link": "Jelszó beállítása",
"sharing.ezen_a_neven_jelenik_meg": "Ezen a néven jelenik meg a gép a hálózaton (",
"sharing.ezt_valasztom": "Ezt választom",
"sharing.felhomentes": "Felhőmentés",
@@ -296,6 +296,16 @@ func (s *Server) sharingEnableHandler(w http.ResponseWriter, r *http.Request) {
s.logger.Printf("[WARN] [sharing] could not ensure the import share: %v", err)
}
// W21 (rehearsal 2026-10-20): with no household password the service is never deployed (reconcile
// skips it: „deploy skipped — household SMB password not set yet"), yet the flash said the service was
// being prepared and the page stayed „áll" for ever. The answer now says it waits for the password, and
// the page asks for it (sharing.html, the needs-password alert). Nothing to prepare, so no job.
// Pinned by TestW21_SwitchOnWithoutPasswordAsksForItAtOnce.
if !s.settings.GetSMBSettings().UserSet {
s.logger.Printf("[INFO] [sharing] sharing switched on; the service waits for the household password")
sharingRedirect(w, r, "flash.sharing.saved_needs_password")
return
}
// v0.147.0 (4b): the bring-up runs DETACHED and the page polls it. Synchronously it was a form
// post that hung for minutes on a first-enable image pull and then flashed „Beállítás mentve."
// regardless of whether anything actually came up.
@@ -53,6 +53,12 @@
<span class="badge badge-warn">{{T "sharing.eloszor_adj_meg_jelszot"}}</span>
{{end}}
</div>
{{- /* W21 (rehearsal 2026-10-20): the service waits for the password; the page says so at once. */}}
{{- if and .SMBEnabled (not .SMBUserSet)}}
<div class="alert alert-warning" id="smb-needs-password">
{{T "sharing.needs_password_why"}} <a href="#smb_password">{{T "sharing.needs_password_link"}}</a>
</div>
{{- end}}
<div class="form-hint">
{{T "sharing.egyes_halozatokon_a_gep_kesve"}}
<span class="mono">\\{{.SMBServerName}}</span> {{T "sharing.cim_beirasa_a_cimsorba_ilyenkor"}}
@@ -97,7 +103,7 @@
<div class="form-group">
<label for="smb_password">{{T "sharing.uj_jelszo"}}</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required{{if and .SMBEnabled (not .SMBUserSet)}} autofocus{{end}}>
</div>
<div class="form-group">
<label for="smb_password_confirm">{{T "sharing.jelszo_megerositese"}}</label>
@@ -221,6 +221,9 @@
<span class="badge badge-warn">először adj meg jelszót</span>
</div>
<div class="alert alert-warning" id="smb-needs-password">
Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. <a href="#smb_password">Jelszó beállítása</a>
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\FELHOM</span> cím beírása a címsorba ilyenkor is működik.
@@ -253,7 +256,7 @@
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required autofocus>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
@@ -223,6 +223,9 @@
<span class="badge badge-warn">először adj meg jelszót</span>
</div>
<div class="alert alert-warning" id="smb-needs-password">
Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. <a href="#smb_password">Jelszó beállítása</a>
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\FELHOM</span> cím beírása a címsorba ilyenkor is működik.
@@ -263,7 +266,7 @@
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required autofocus>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
@@ -221,6 +221,9 @@
<span class="badge badge-warn">először adj meg jelszót</span>
</div>
<div class="alert alert-warning" id="smb-needs-password">
Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. <a href="#smb_password">Jelszó beállítása</a>
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\FELHOM</span> cím beírása a címsorba ilyenkor is működik.
@@ -253,7 +256,7 @@
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required autofocus>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
@@ -221,6 +221,9 @@
<span class="badge badge-warn">először adj meg jelszót</span>
</div>
<div class="alert alert-warning" id="smb-needs-password">
Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el. <a href="#smb_password">Jelszó beállítása</a>
</div>
<div class="form-hint">
Egyes hálózatokon a gép késve jelenik meg az eszközlistában. A
<span class="mono">\\FELHOM</span> cím beírása a címsorba ilyenkor is működik.
@@ -253,7 +256,7 @@
<div class="form-group">
<label for="smb_password">Új jelszó</label>
<input type="password" id="smb_password" name="smb_password" class="form-control"
autocomplete="new-password" minlength="8" required>
autocomplete="new-password" minlength="8" required autofocus>
</div>
<div class="form-group">
<label for="smb_password_confirm">Jelszó megerősítése</label>
@@ -0,0 +1,57 @@
package web
import (
"html"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
// W21 (rehearsal 2026-10-20): „switch on" with no household SMB password answered „Beállítás mentve. A megosztási
// szolgáltatás előkészítése folyamatban…" — but reconcile never deploys without the password („deploy skipped —
// household SMB password not set yet") and the page stayed „Állapot: áll" for ever. The answer and the page now ask
// for the password at once and say why the service waits; with a password set, the bring-up is unchanged.
func TestW21_SwitchOnWithoutPasswordAsksForItAtOnce(t *testing.T) {
s := i18nTestServer(t)
enable := func() string {
r := httptest.NewRequest(http.MethodPost, "/sharing/enable", strings.NewReader(url.Values{"enabled": {"on"}}.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.sharingEnableHandler(w, r)
if w.Code != http.StatusSeeOther {
t.Fatalf("enable: %d", w.Code)
}
return w.Header().Get("Location")
}
loc := enable()
if !strings.Contains(loc, "flash.sharing.saved_needs_password") {
t.Fatalf("switch-on without a password must say the service waits for it: %q", loc)
}
for lang, want := range map[string][2]string{
"hu": {"A megosztás akkor indul el, amikor beállítod a megosztási jelszót.", "Állíts be egy megosztási jelszót — enélkül a megosztás nem indul el."},
"en": {"Sharing starts when you set the sharing password.", "Set a sharing password. Without it, sharing does not start."},
} {
r := httptest.NewRequest(http.MethodGet, loc, nil)
r.AddCookie(&http.Cookie{Name: langCookieName, Value: lang})
w := httptest.NewRecorder()
s.sharingPageHandler(w, r)
page := html.UnescapeString(w.Body.String())
if !strings.Contains(page, want[0]) || !strings.Contains(page, want[1]) || !strings.Contains(page, `id="smb-needs-password"`) {
t.Errorf("%s: the page does not ask for the password at once", lang)
}
if strings.Contains(page, "szolgáltatás előkészítése folyamatban") || strings.Contains(page, "is being prepared") {
t.Errorf("%s: the page still says the service is being prepared", lang)
}
}
// with the password set the page asks for nothing (the bring-up path is the existing one, pinned elsewhere)
if err := s.settings.SetSMBUserSet(true); err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
s.sharingPageHandler(w, httptest.NewRequest(http.MethodGet, "/sharing", nil))
if strings.Contains(w.Body.String(), `id="smb-needs-password"`) {
t.Fatal("the needs-password alert shows although a password is set")
}
}