summaryrefslogtreecommitdiff
path: root/internal/api/handlers_test.go
diff options
context:
space:
mode:
authorsergei <sergei@em-sysadmin.xyz>2026-04-14 06:23:55 +0400
committersergei <sergei@em-sysadmin.xyz>2026-04-14 06:23:55 +0400
commit3d51aa455006903345f554a2dd90034993796114 (patch)
tree62a7be2faf047f5eb7886feebc3b815556f03d7f /internal/api/handlers_test.go
downloadvpnem-3d51aa455006903345f554a2dd90034993796114.tar.gz
vpnem-3d51aa455006903345f554a2dd90034993796114.tar.bz2
vpnem-3d51aa455006903345f554a2dd90034993796114.zip
vpnem: VPN infrastructure with load-balanced multi-protocol nodesHEADmain
- Multi-protocol VPS nodes (VLESS-REALITY + Hysteria2 + SOCKS5) - Smart load balancing via recommendation API - Windows/Linux client (Go + Wails + sing-box) - Server API with RealIP detection and connection tracking - Auto-deployment via vpnui control plane - Silent Windows installer with UAC elevation - Load-based server recommendation (no sticky sessions) - Best Server one-click connection workflow
Diffstat (limited to 'internal/api/handlers_test.go')
-rw-r--r--internal/api/handlers_test.go592
1 files changed, 592 insertions, 0 deletions
diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go
new file mode 100644
index 0000000..262ea07
--- /dev/null
+++ b/internal/api/handlers_test.go
@@ -0,0 +1,592 @@
+package api_test
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "vpnem/internal/api"
+ "vpnem/internal/control"
+ "vpnem/internal/models"
+ "vpnem/internal/rules"
+)
+
+func setupTestStore(t *testing.T) *rules.Store {
+ t.Helper()
+ dir := t.TempDir()
+
+ writeJSON(t, filepath.Join(dir, "servers.json"), models.ServersResponse{
+ Servers: []models.Server{
+ {Tag: "test-1", Region: "NL", Type: "socks", Server: "1.2.3.4", ServerPort: 1080},
+ {
+ Tag: "test-vless",
+ Region: "NL",
+ Type: "vless",
+ Server: "nl.example.com",
+ ServerPort: 443,
+ UUID: "11111111-1111-1111-1111-111111111111",
+ TLS: &models.TLS{Enabled: true, ServerName: "nl.example.com"},
+ Transport: &models.Transport{Type: "ws", Path: "/ws"},
+ },
+ {
+ Tag: "test-ss",
+ Region: "DE",
+ Type: "shadowsocks",
+ Server: "de.example.com",
+ ServerPort: 8443,
+ Method: "2022-blake3-aes-128-gcm",
+ Password: "secret",
+ },
+ },
+ })
+
+ writeJSON(t, filepath.Join(dir, "rulesets.json"), models.RuleSetManifest{
+ RuleSets: []models.RuleSet{
+ {Tag: "test-rules", Description: "test", URL: "https://example.com/test.srs", Format: "binary", Type: "domain"},
+ },
+ })
+
+ writeJSON(t, filepath.Join(dir, "version.json"), models.VersionResponse{
+ Version: "0.1.0", Changelog: "test",
+ })
+ writeJSON(t, filepath.Join(dir, "routing-policy.json"), models.RoutingPolicy{
+ Version: "test-policy",
+ AlwaysDirectProcesses: []string{"chromium.exe"},
+ BlockedDomains: []string{"example.com"},
+ })
+ writeJSON(t, filepath.Join(dir, "catalog-v2.json"), models.CatalogV2{
+ Version: "2",
+ Nodes: []models.CatalogNode{
+ {
+ ID: "test-vless",
+ Name: "Test VLESS",
+ Region: "NL",
+ Host: "1.2.3.4",
+ PublicHost: "nl.example.com",
+ Status: "healthy",
+ Protocols: []models.CatalogProtocol{
+ {
+ Type: "vless",
+ Enabled: true,
+ Port: 443,
+ TLS: &models.TLS{Enabled: true, ServerName: "nl.example.com"},
+ Auth: &models.CatalogAuth{UUID: "11111111-1111-1111-1111-111111111111"},
+ Extra: map[string]any{"transport_type": "ws", "path": "/ws"},
+ },
+ {
+ Type: "vmess",
+ Enabled: true,
+ Port: 8444,
+ TLS: &models.TLS{Enabled: true, ServerName: "nl.example.com"},
+ Auth: &models.CatalogAuth{UUID: "22222222-2222-2222-2222-222222222222"},
+ Extra: map[string]any{"path": "/vmess"},
+ },
+ {
+ Type: "hysteria2",
+ Enabled: true,
+ Port: 9443,
+ TLS: &models.TLS{Enabled: true, ServerName: "nl.example.com", Insecure: true, ALPN: []string{"h3"}, MinVersion: "1.3", MaxVersion: "1.3"},
+ Auth: &models.CatalogAuth{Password: "hy2-secret"},
+ Extra: map[string]any{"obfs_password": "obfs-secret"},
+ },
+ {
+ Type: "vless-reality",
+ Enabled: true,
+ Port: 443,
+ TLS: &models.TLS{
+ Enabled: true,
+ ServerName: "login.microsoftonline.com",
+ Reality: &models.Reality{
+ Enabled: true,
+ PublicKey: "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
+ ShortID: "0123456789abcdef",
+ Fingerprint: "chrome",
+ },
+ },
+ Auth: &models.CatalogAuth{UUID: "33333333-3333-3333-3333-333333333333"},
+ },
+ },
+ },
+ },
+ })
+
+ os.MkdirAll(filepath.Join(dir, "rules"), 0o755)
+
+ return rules.NewStore(dir)
+}
+
+func writeJSON(t *testing.T, path string, v any) {
+ t.Helper()
+ data, err := json.MarshalIndent(v, "", " ")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path, data, 0o644); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestServersEndpoint(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v1/servers", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d", w.Code)
+ }
+
+ var resp models.ServersResponse
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("invalid json: %v", err)
+ }
+ if len(resp.Servers) != 3 {
+ t.Fatalf("expected 3 servers, got %d", len(resp.Servers))
+ }
+ if resp.Servers[0].Tag != "test-1" {
+ t.Errorf("expected first tag test-1, got %s", resp.Servers[0].Tag)
+ }
+}
+
+func TestSubscribeEndpoint(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v1/subscribe", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(w.Body.String()))
+ if err != nil {
+ t.Fatalf("expected base64 response: %v", err)
+ }
+ body := string(decoded)
+ if !strings.Contains(body, "vless://11111111-1111-1111-1111-111111111111@nl.example.com:443?") {
+ t.Fatalf("expected vless link in subscription, got %q", body)
+ }
+ if !strings.Contains(body, "vmess://") {
+ t.Fatalf("expected vmess link in subscription, got %q", body)
+ }
+ if !strings.Contains(body, "hysteria2://hy2-secret@nl.example.com:9443/?") {
+ t.Fatalf("expected hysteria2 link in subscription, got %q", body)
+ }
+ if !strings.Contains(body, "insecure=1") || !strings.Contains(body, "alpn=h3") {
+ t.Fatalf("expected hysteria2 insecure/alpn query params in subscription, got %q", body)
+ }
+ if !strings.Contains(body, "security=reality") || !strings.Contains(body, "pbk=jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0") {
+ t.Fatalf("expected reality link in subscription, got %q", body)
+ }
+ if strings.Contains(body, "socks5://1.2.3.4:1080#test-1") {
+ t.Fatalf("did not expect legacy-only socks link when catalog-v2 is available, got %q", body)
+ }
+}
+
+func TestSubscribeEndpointPlain(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v1/subscribe?format=plain", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ if !strings.Contains(w.Body.String(), "vless://") {
+ t.Fatalf("expected plain subscription links, got %q", w.Body.String())
+ }
+ if !strings.Contains(w.Body.String(), "vmess://") {
+ t.Fatalf("expected vmess in plain subscription, got %q", w.Body.String())
+ }
+ if !strings.Contains(w.Body.String(), "hysteria2://") {
+ t.Fatalf("expected hysteria2 in plain subscription, got %q", w.Body.String())
+ }
+ if !strings.Contains(w.Body.String(), "security=reality") {
+ t.Fatalf("expected reality in plain subscription, got %q", w.Body.String())
+ }
+}
+
+func TestSubscribeEndpointPlainLegacyFallbackPreservesTags(t *testing.T) {
+ dir := t.TempDir()
+ writeJSON(t, filepath.Join(dir, "servers.json"), models.ServersResponse{
+ Servers: []models.Server{
+ {Tag: "legacy-socks", Region: "NL", Type: "socks", Server: "1.2.3.4", ServerPort: 1080},
+ {Tag: "legacy-ss", Region: "NL", Type: "shadowsocks", Server: "ss.example.com", ServerPort: 8388, Method: "chacha20-ietf-poly1305", Password: "secret"},
+ },
+ })
+ writeJSON(t, filepath.Join(dir, "rulesets.json"), models.RuleSetManifest{})
+ writeJSON(t, filepath.Join(dir, "version.json"), models.VersionResponse{Version: "0.1.0"})
+ writeJSON(t, filepath.Join(dir, "routing-policy.json"), models.RoutingPolicy{Version: "test"})
+
+ store := rules.NewStore(dir)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v1/subscribe?format=plain", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+ body := w.Body.String()
+ if !strings.Contains(body, "socks5://1.2.3.4:1080#legacy-socks") {
+ t.Fatalf("expected legacy socks tag in subscription, got %q", body)
+ }
+ if !strings.Contains(body, "#legacy-ss") {
+ t.Fatalf("expected legacy shadowsocks tag in subscription, got %q", body)
+ }
+}
+
+func TestRuleSetManifestEndpoint(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v1/ruleset/manifest", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d", w.Code)
+ }
+
+ var resp models.RuleSetManifest
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("invalid json: %v", err)
+ }
+ if len(resp.RuleSets) != 1 {
+ t.Fatalf("expected 1 ruleset, got %d", len(resp.RuleSets))
+ }
+}
+
+func TestVersionEndpoint(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v1/version", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d", w.Code)
+ }
+
+ var resp models.VersionResponse
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("invalid json: %v", err)
+ }
+ if resp.Version != "0.1.0" {
+ t.Errorf("expected version 0.1.0, got %s", resp.Version)
+ }
+}
+
+func TestCatalogV2Endpoint(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v2/catalog", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d", w.Code)
+ }
+
+ var resp models.CatalogV2
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("invalid json: %v", err)
+ }
+ if resp.Version != "2" {
+ t.Fatalf("expected version 2, got %q", resp.Version)
+ }
+ if len(resp.Nodes) != 1 {
+ t.Fatalf("expected 1 node, got %d", len(resp.Nodes))
+ }
+}
+
+func TestCatalogV2EndpointFallsBackToLegacyServers(t *testing.T) {
+ dir := t.TempDir()
+ writeJSON(t, filepath.Join(dir, "servers.json"), models.ServersResponse{
+ Servers: []models.Server{
+ {Tag: "legacy", Region: "NL", Type: "socks", Server: "1.2.3.4", ServerPort: 1080},
+ },
+ })
+ writeJSON(t, filepath.Join(dir, "rulesets.json"), models.RuleSetManifest{})
+ writeJSON(t, filepath.Join(dir, "version.json"), models.VersionResponse{Version: "0.1.0"})
+ writeJSON(t, filepath.Join(dir, "routing-policy.json"), models.RoutingPolicy{Version: "test"})
+
+ store := rules.NewStore(dir)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v2/catalog", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ var resp models.CatalogV2
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("invalid json: %v", err)
+ }
+ if resp.Version != "legacy-adapter" {
+ t.Fatalf("expected legacy-adapter version, got %q", resp.Version)
+ }
+ if len(resp.Nodes) != 1 || resp.Nodes[0].ID != "legacy" {
+ t.Fatalf("unexpected fallback catalog payload: %+v", resp)
+ }
+}
+
+func TestCatalogV2EndpointMissingReturns404(t *testing.T) {
+ dir := t.TempDir()
+ writeJSON(t, filepath.Join(dir, "rulesets.json"), models.RuleSetManifest{})
+ writeJSON(t, filepath.Join(dir, "version.json"), models.VersionResponse{Version: "0.1.0"})
+ writeJSON(t, filepath.Join(dir, "routing-policy.json"), models.RoutingPolicy{Version: "test"})
+
+ store := rules.NewStore(dir)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v2/catalog", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusNotFound {
+ t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String())
+ }
+}
+
+func TestRoutingPolicyEndpoint(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/api/v1/routing-policy", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d", w.Code)
+ }
+
+ var resp models.RoutingPolicy
+ if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("invalid json: %v", err)
+ }
+ if resp.Version != "test-policy" {
+ t.Fatalf("expected version test-policy, got %q", resp.Version)
+ }
+ if len(resp.AlwaysDirectProcesses) != 1 || resp.AlwaysDirectProcesses[0] != "chromium.exe" {
+ t.Fatalf("unexpected routing policy payload: %+v", resp)
+ }
+}
+
+func TestMethodNotAllowed(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("POST", "/api/v1/servers", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code == http.StatusOK {
+ t.Fatal("POST should not return 200")
+ }
+}
+
+func TestVPNUIEndpoint(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ req := httptest.NewRequest("GET", "/vpnui", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusTemporaryRedirect {
+ t.Fatalf("expected 307, got %d", w.Code)
+ }
+ if got := w.Header().Get("Location"); got != "/vpnui/" {
+ t.Fatalf("expected redirect to /vpnui/, got %q", got)
+ }
+
+ req = httptest.NewRequest("GET", "/vpnui/", nil)
+ w = httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200 for /vpnui/, got %d", w.Code)
+ }
+ if !strings.Contains(w.Body.String(), "Панель управления vpnem") {
+ t.Fatal("expected control ui html")
+ }
+}
+
+func TestControlNodeUpsertAndList(t *testing.T) {
+ store := setupTestStore(t)
+ router := api.NewRouter(store)
+
+ body := `{
+ "id":"nl-01",
+ "name":"NL 01",
+ "provider":"custom-vps",
+ "region":"nl",
+ "host":"203.0.113.10",
+ "domain":"nl-01.example.com",
+ "enabled":true,
+ "ssh":{"user":"root","port":22,"auth":"key","identity_file":"~/.ssh/id_ed25519"},
+ "protocols":[
+ {
+ "type":"vless",
+ "enabled":true,
+ "port":443,
+ "tls":{"enabled":true,"server_name":"nl-01.example.com"},
+ "auth":{"uuid":"11111111-1111-1111-1111-111111111111"},
+ "extra":{"transport_type":"ws","path":"/ws"}
+ }
+ ]
+ }`
+
+ req := httptest.NewRequest("POST", "/api/v1/control/nodes", strings.NewReader(body))
+ req.Header.Set("Content-Type", "application/json")
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200 on save, got %d: %s", w.Code, w.Body.String())
+ }
+
+ listReq := httptest.NewRequest("GET", "/api/v1/control/nodes", nil)
+ listW := httptest.NewRecorder()
+ router.ServeHTTP(listW, listReq)
+
+ if listW.Code != http.StatusOK {
+ t.Fatalf("expected 200 on list, got %d", listW.Code)
+ }
+
+ var resp struct {
+ Nodes []control.Node `json:"nodes"`
+ States map[string]*control.NodeState `json:"states"`
+ }
+ if err := json.Unmarshal(listW.Body.Bytes(), &resp); err != nil {
+ t.Fatalf("invalid json: %v", err)
+ }
+ if len(resp.Nodes) != 1 {
+ t.Fatalf("expected 1 node, got %d", len(resp.Nodes))
+ }
+ if resp.Nodes[0].ID != "nl-01" {
+ t.Fatalf("expected nl-01, got %s", resp.Nodes[0].ID)
+ }
+}
+
+func TestControlCatalogPublish(t *testing.T) {
+ store := setupTestStore(t)
+ if _, err := control.SaveNodeFile(filepath.Join(store.DataDir(), "control", "inventory"), control.Node{
+ ID: "nl-01",
+ Name: "NL 01",
+ Provider: "custom-vps",
+ Region: "nl",
+ Host: "203.0.113.10",
+ Domain: "nl-01.example.com",
+ Enabled: true,
+ SSH: control.SSHConfig{User: "root", Port: 22, Auth: "key", IdentityFile: "~/.ssh/id_ed25519"},
+ Protocols: []control.ProtocolProfile{
+ {
+ Type: "vless",
+ Enabled: true,
+ Port: 443,
+ TLS: &control.TLSProfile{Enabled: true, ServerName: "nl-01.example.com"},
+ Auth: &control.AuthProfile{UUID: "11111111-1111-1111-1111-111111111111"},
+ Extra: map[string]any{"transport_type": "ws", "path": "/ws"},
+ },
+ },
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if err := control.SaveNodeState(filepath.Join(store.DataDir(), "control", "state"), control.NodeState{
+ NodeID: "nl-01",
+ BootstrapStatus: "healthy",
+ PublicHost: "nl-01.example.com",
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ router := api.NewRouter(store)
+ req := httptest.NewRequest("POST", "/api/v1/control/catalog/publish", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ data, err := os.ReadFile(filepath.Join(store.DataDir(), "servers.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(data), `"tag": "nl-01-vless"`) {
+ t.Fatal("expected published vless server in servers.json")
+ }
+ catalogData, err := os.ReadFile(filepath.Join(store.DataDir(), "catalog-v2.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(catalogData), `"version": "2"`) {
+ t.Fatal("expected catalog-v2.json to be published")
+ }
+}
+
+func TestDeleteControlNode(t *testing.T) {
+ store := setupTestStore(t)
+ if _, err := control.SaveNodeFile(filepath.Join(store.DataDir(), "control", "inventory"), control.Node{
+ ID: "nl-delete",
+ Name: "Delete Node",
+ Provider: "custom-vps",
+ Region: "nl",
+ Host: "203.0.113.20",
+ Domain: "nl-delete.example.com",
+ Enabled: true,
+ SSH: control.SSHConfig{User: "root", Port: 22, Auth: "key", IdentityFile: "~/.ssh/id_ed25519"},
+ Protocols: []control.ProtocolProfile{
+ {
+ Type: "vless",
+ Enabled: true,
+ Port: 443,
+ TLS: &control.TLSProfile{Enabled: true, ServerName: "nl-delete.example.com"},
+ Auth: &control.AuthProfile{UUID: "11111111-1111-1111-1111-111111111111"},
+ },
+ },
+ }); err != nil {
+ t.Fatal(err)
+ }
+ if err := control.SaveNodeState(filepath.Join(store.DataDir(), "control", "state"), control.NodeState{
+ NodeID: "nl-delete",
+ BootstrapStatus: "healthy",
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ router := api.NewRouter(store)
+ req := httptest.NewRequest("DELETE", "/api/v1/control/nodes/nl-delete", nil)
+ w := httptest.NewRecorder()
+ router.ServeHTTP(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ if _, err := os.Stat(filepath.Join(store.DataDir(), "control", "inventory", "nl-delete.yaml")); !os.IsNotExist(err) {
+ t.Fatalf("expected node file to be deleted, got err=%v", err)
+ }
+ if _, err := os.Stat(filepath.Join(store.DataDir(), "control", "state", "nl-delete.json")); !os.IsNotExist(err) {
+ t.Fatalf("expected node state to be deleted, got err=%v", err)
+ }
+}