v0.236.0: "delete my data too" deletes the data, or says that it could not (R-442)
gates / gates (push) Successful in 13s
gates / gates (push) Successful in 13s
Removal resolves the drive from the app's own app.yaml HDD_PATH (the 07 ~L437 rule), never the global cfg.Paths.HDDPath which no box sets. A data removal that cannot be resolved, or whose drive is absent, is refused with a typed RemoveRefusedError -> 409 + exact Hungarian sentence, before compose down, and the app is kept. SSD app -> hdd_paths_removed: [] never null; missing folders stated; backup-path refusals reach the response. 15 tests, two red-proofs run (pre-fix fallback -> C fails with err=nil and the handler 200s; "no drive refuses" -> D fails). Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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/stacks"
|
||||
)
|
||||
|
||||
// R-442 through the PRODUCTION WIRING: the POST /api/stacks/{name}/remove handler → the real
|
||||
// stacks.Manager (constructor + ScanStacks, no hand-set state) → RemoveStack. Two refusals, both
|
||||
// non-2xx with the exact customer sentence, and in both cases app.yaml is still on disk afterwards.
|
||||
// Neither reaches the compose boundary (the refusal precedes it), so no docker is touched.
|
||||
func newR442Router(t *testing.T, appYAML string) (*Router, string) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
dir := filepath.Join(root, "app")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compose := "services:\n app:\n image: nginx:1.27\n volumes:\n - ${HDD_PATH}/appdata/app:/data\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "docker-compose.yml"), []byte(compose), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte(appYAML), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := &config.Config{}
|
||||
cfg.Paths.StacksDir = root
|
||||
cfg.Paths.SystemDataPath = "/mnt/sys_drive"
|
||||
cfg.Stacks.ComposeCommand = "docker compose" // skips detection; never invoked on the refusal path
|
||||
lg := log.New(io.Discard, "", 0)
|
||||
m, err := stacks.NewManager(cfg, lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.ScanStacks(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &Router{cfg: cfg, stackMgr: m, logger: lg}, dir
|
||||
}
|
||||
|
||||
func postRemove(t *testing.T, r *Router, body string) (int, apiResponse) {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/stacks/app/remove", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.removeStack(w, req, "app")
|
||||
var resp apiResponse
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("non-JSON body %q: %v", w.Body.String(), err)
|
||||
}
|
||||
return w.Code, resp
|
||||
}
|
||||
|
||||
func TestRemoveHandler_R442_UnresolvedDriveIsRefusedNon2xx(t *testing.T) {
|
||||
r, dir := newR442Router(t, "deployed: true\nenv: {}\n")
|
||||
code, resp := postRemove(t, r, `{"remove_hdd_data":true,"remove_backups":false}`)
|
||||
if code/100 == 2 || resp.OK {
|
||||
t.Fatalf("HTTP %d ok=%v — a 2xx over a removal that could not resolve the data location is the R-442 defect (body error=%q)", code, resp.OK, resp.Error)
|
||||
}
|
||||
if code != http.StatusConflict {
|
||||
t.Fatalf("status = %d, want 409", code)
|
||||
}
|
||||
const want = "Az alkalmazás adatainak helye nem állapítható meg, ezért semmit nem töröltünk. Az alkalmazás nem lett eltávolítva."
|
||||
if resp.Error != want {
|
||||
t.Fatalf("error = %q, want the exact customer sentence", resp.Error)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "app.yaml")); err != nil {
|
||||
t.Fatal("app.yaml gone — the app was removed with its data left behind")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveHandler_R442_DriveAbsentIsRefusedNon2xx(t *testing.T) {
|
||||
drive := t.TempDir() // a plain directory is not a mountpoint → the drive is "absent"
|
||||
r, dir := newR442Router(t, "deployed: true\nenv:\n HDD_PATH: "+drive+"\n")
|
||||
code, resp := postRemove(t, r, `{"remove_hdd_data":true}`)
|
||||
if code != http.StatusConflict || resp.OK {
|
||||
t.Fatalf("HTTP %d ok=%v, want 409 refusal (error=%q)", code, resp.OK, resp.Error)
|
||||
}
|
||||
if !strings.Contains(resp.Error, drive) || !strings.Contains(resp.Error, "vissza nem csatlakozik") {
|
||||
t.Fatalf("error = %q, want the drive-absent sentence naming %s", resp.Error, drive)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "app.yaml")); err != nil {
|
||||
t.Fatal("app.yaml gone on a drive-absent refusal")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user