R-839: the deploy API refuses an HDD_PATH that is a folder inside a registered drive
Diagnosed: 9202's paperless-ngx app.yaml named <drive>/userdata/paperless-ngx, written by the 2026-09-22 drill harness through POST /api/stacks/<n>/deploy, which accepted it (RefuseAsAppNamespace matches by prefix). The boot sweep then asked a folder whether it was a mountpoint and HELD the app. The deploy page offers drive roots only, so a household could not reach this; every other caller is now refused with a sentence naming the drive (settings.DriveOfSubPath). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
// R-839: a deploy whose HDD_PATH names a folder INSIDE a registered drive is refused at the API, so
|
||||
// the boot sweep can never again hold an app for asking a folder whether it is a mountpoint. Through
|
||||
// the real deployStack handler. PATH is emptied so a regression that lets the deploy proceed can
|
||||
// never reach a real docker (tests that reach real docker act on DooPlex).
|
||||
func TestR839_DeployRefusesASubPathOfADrive(t *testing.T) {
|
||||
t.Setenv("PATH", "")
|
||||
lg := log.New(io.Discard, "", 0)
|
||||
root := t.TempDir()
|
||||
sett, err := settings.Load(filepath.Join(root, "settings.json"), lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const drive = "/mnt/felhom-drives/scratch_hdd"
|
||||
if err := sett.AddStoragePath(settings.StoragePath{Path: drive, Label: "HDD", Schedulable: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
cfg.Paths.StacksDir = filepath.Join(root, "stacks")
|
||||
cfg.Stacks.ComposeCommand = "docker compose"
|
||||
if err := os.MkdirAll(cfg.Paths.StacksDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m, err := stacks.NewManager(cfg, lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := &Router{stackMgr: m, cfg: cfg, sett: sett, logger: lg}
|
||||
r.classifyFSPath = func(string) string { return "" }
|
||||
|
||||
deploy := func(hdd string) (int, string) {
|
||||
w := httptest.NewRecorder()
|
||||
body := `{"values":{"HDD_PATH":"` + hdd + `"}}`
|
||||
r.deployStack(w, httptest.NewRequest(http.MethodPost, "/api/stacks/paperless-ngx/deploy", strings.NewReader(body)), "paperless-ngx")
|
||||
return w.Code, w.Body.String()
|
||||
}
|
||||
|
||||
code, body := deploy(drive + "/userdata/paperless-ngx")
|
||||
if code != http.StatusConflict || !strings.Contains(body, "nem egy mappa") || !strings.Contains(body, drive) {
|
||||
t.Fatalf("R-839: a folder inside a drive must be refused naming the drive; got %d %s", code, body)
|
||||
}
|
||||
// Controls: the drive itself and an unregistered path are not this gate's business.
|
||||
for _, ok := range []string{drive, drive + "/", "/srv/elsewhere"} {
|
||||
if _, b := deploy(ok); strings.Contains(b, "nem egy mappa") {
|
||||
t.Errorf("R-839: %q must not be refused as a folder inside a drive: %s", ok, b)
|
||||
}
|
||||
}
|
||||
if d, in := sett.DriveOfSubPath(drive + "x/userdata"); in {
|
||||
t.Errorf("a sibling path sharing the prefix (%s) is not inside the drive", d)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user