33c397380e
An export to a registered network drive (Kind=network) with no bundle password now answers 400 with a household sentence (hu+en); with a password it runs; a local drive is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
106 lines
4.1 KiB
Go
106 lines
4.1 KiB
Go
package web
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/appexport"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// R-126 (operator ruling 2026-10-05, 09 §3 decision 128): an export WITHOUT a bundle password to a
|
|
// network drive is refused; WITH a password it runs; a local drive without a password is unchanged.
|
|
// The assertions are the consequence — whether a .fab lands on the destination — not only the status.
|
|
func TestExportStart_NetworkDriveNeedsPassword(t *testing.T) {
|
|
build := func(t *testing.T, kind string) (*Server, *appexport.Exporter, string) {
|
|
s := testServer(t)
|
|
s.cfg.Paths.DataDir = t.TempDir()
|
|
drive := t.TempDir()
|
|
stackDir := t.TempDir()
|
|
os.WriteFile(filepath.Join(stackDir, "docker-compose.yml"), []byte("services: {}\n"), 0644)
|
|
fabWrite(t, drive, "userdata/media/books/a.epub", "BOOK")
|
|
prov := &fabWebProvider{stackDir: stackDir, stacksDir: t.TempDir(), hddPath: drive,
|
|
binds: []appbackup.ClassifiedBind{
|
|
{ComposeBind: appbackup.ComposeBind{Root: appbackup.RootUserdata, RelPath: "media/books"}, Class: appbackup.ClassMandatory},
|
|
}}
|
|
e := appexport.NewExporter(prov, s.logger, "test")
|
|
s.appExporter = e
|
|
if err := s.settings.AddStoragePath(settings.StoragePath{Path: drive, Label: "d", Kind: kind, Schedulable: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := s.settings.IsNetworkStoragePath(drive); got != (kind == settings.StorageKindNetwork) {
|
|
t.Fatalf("fixture: IsNetworkStoragePath(%q)=%v for kind %q", drive, got, kind)
|
|
}
|
|
return s, e, drive
|
|
}
|
|
start := func(s *Server, drive, password string) (*httptest.ResponseRecorder, map[string]interface{}) {
|
|
body, _ := json.Marshal(map[string]interface{}{"stack_name": "calibre-web", "dest_drive": drive, "password": password})
|
|
rr := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/export/start", strings.NewReader(string(body)))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.apiExportStart(rr, req)
|
|
var resp map[string]interface{}
|
|
json.Unmarshal(rr.Body.Bytes(), &resp)
|
|
return rr, resp
|
|
}
|
|
fabCount := func(dir string) int {
|
|
n := 0
|
|
filepath.Walk(dir, func(p string, info os.FileInfo, err error) error {
|
|
if err == nil && strings.HasSuffix(p, ".fab") {
|
|
n++
|
|
}
|
|
return nil
|
|
})
|
|
return n
|
|
}
|
|
|
|
t.Run("network drive, no password: refused, nothing written", func(t *testing.T) {
|
|
s, e, drive := build(t, settings.StorageKindNetwork)
|
|
rr, resp := start(s, drive, "")
|
|
if rr.Code != http.StatusBadRequest || resp["ok"] != false {
|
|
t.Fatalf("want 400 ok=false, got %d %s", rr.Code, rr.Body.String())
|
|
}
|
|
msg, _ := resp["error"].(string)
|
|
// ASCII fragments of the Hungarian sentence (positive), and never the raw key (negative).
|
|
if !strings.Contains(msg, "(NAS)") || !strings.Contains(msg, "jelsz") || strings.Contains(msg, "app_export.") {
|
|
t.Errorf("refusal text is not the household sentence: %q", msg)
|
|
}
|
|
if j := e.GetActiveJob(); j != nil {
|
|
t.Errorf("an export job started despite the refusal: %v", j.Snapshot())
|
|
}
|
|
if n := fabCount(drive); n != 0 {
|
|
t.Errorf("%d .fab file(s) landed on the network drive without a password", n)
|
|
}
|
|
})
|
|
|
|
t.Run("network drive, with password: runs", func(t *testing.T) {
|
|
s, e, drive := build(t, settings.StorageKindNetwork)
|
|
rr, resp := start(s, drive, "correct horse battery")
|
|
if rr.Code != http.StatusOK || resp["ok"] != true {
|
|
t.Fatalf("want 200 ok=true, got %d %s", rr.Code, rr.Body.String())
|
|
}
|
|
waitExportDone(t, e)
|
|
if n := fabCount(drive); n != 1 {
|
|
t.Errorf("want 1 .fab on the network drive, got %d", n)
|
|
}
|
|
})
|
|
|
|
t.Run("local drive, no password: unchanged", func(t *testing.T) {
|
|
s, e, drive := build(t, "")
|
|
rr, resp := start(s, drive, "")
|
|
if rr.Code != http.StatusOK || resp["ok"] != true {
|
|
t.Fatalf("want 200 ok=true, got %d %s", rr.Code, rr.Body.String())
|
|
}
|
|
waitExportDone(t, e)
|
|
if n := fabCount(drive); n != 1 {
|
|
t.Errorf("want 1 .fab on the local drive, got %d", n)
|
|
}
|
|
})
|
|
}
|