42a73e667a
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
96 lines
3.6 KiB
Go
96 lines
3.6 KiB
Go
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")
|
|
}
|
|
}
|