package api import ( "encoding/json" "net/http/httptest" "path/filepath" "strings" "testing" "github.com/gin-gonic/gin" "opsxapi/server/internal/mocksrv" ) func TestLocalMockLifecycleAndPeerBoundary(t *testing.T) { gin.SetMode(gin.TestMode) a, err := New(filepath.Join(t.TempDir(), "mock.db")) if err != nil { t.Fatal(err) } defer a.Store().Close() r := gin.New() _ = r.SetTrustedProxies(nil) a.Register(r) mocksrv.New(a.Store()).Register(r) callWithBearer := func(method, path, body, peer, bearer string) *httptest.ResponseRecorder { req := httptest.NewRequest(method, path, strings.NewReader(body)) req.RemoteAddr = peer req.Header.Set("Content-Type", "application/json") if bearer != "" { req.Header.Set("Authorization", "Bearer "+bearer) } req.Header.Set("X-Forwarded-For", "127.0.0.1") w := httptest.NewRecorder() r.ServeHTTP(w, req) return w } call := func(method, path, body, peer string) *httptest.ResponseRecorder { return callWithBearer(method, path, body, peer, "") } if w := call("GET", "/api/mock/local", "", "192.168.1.5:3000"); w.Code != 403 { t.Fatalf("remote management: %d", w.Code) } payload := `{"endpoints":[{"method":"POST","path":"/demo","status":201,"contentType":"application/json","body":"{\"ok\":true}","delayMs":0}],"ttlSeconds":0}` if w := call("PUT", "/api/mock/local", payload, "127.0.0.1:3000"); w.Code != 200 { t.Fatal(w.Body.String()) } w := call("GET", "/api/mock/local", "", "127.0.0.1:3000") var status struct { Token string `json:"token"` Active bool `json:"active"` Endpoints []mocksrv.Endpoint `json:"endpoints"` } if err = json.Unmarshal(w.Body.Bytes(), &status); err != nil { t.Fatal(err) } if !status.Active || len(status.Endpoints) != 1 { t.Fatal(w.Body.String()) } path := "/mock/" + status.Token + "/demo" if w = call("POST", path, "", "192.168.1.5:3000"); w.Code != 201 { t.Fatal(w.Code, w.Body.String()) } if w = call("PUT", "/api/mock/local/access", `{"enabled":true,"token":"test-secret-1234567890"}`, "192.168.1.5:3000"); w.Code != 403 { t.Fatalf("remote access control: %d", w.Code) } if w = call("PUT", "/api/mock/local/access", `{"enabled":true,"token":"short"}`, "127.0.0.1:3000"); w.Code != 400 { t.Fatalf("short token accepted: %d", w.Code) } if w = call("PUT", "/api/mock/local/access", `{"enabled":true,"token":"test-secret-1234567890"}`, "127.0.0.1:3000"); w.Code != 200 { t.Fatal(w.Body.String()) } if w = call("GET", "/api/mock/local", "", "127.0.0.1:3000"); w.Code != 200 || !strings.Contains(w.Body.String(), `"bearerRequired":true`) || strings.Contains(w.Body.String(), "test-secret-1234567890") { t.Fatalf("status leaked secret or wrong access state: %s", w.Body.String()) } if w = call("POST", path, "", "192.168.1.5:3000"); w.Code != 401 || w.Header().Get("WWW-Authenticate") == "" { t.Fatalf("missing bearer: %d", w.Code) } if w = callWithBearer("POST", path, "", "192.168.1.5:3000", "wrong-secret-1234567890"); w.Code != 401 { t.Fatalf("wrong bearer: %d", w.Code) } if w = callWithBearer("POST", path, "", "192.168.1.5:3000", "test-secret-1234567890"); w.Code != 201 { t.Fatalf("correct bearer: %d %s", w.Code, w.Body.String()) } if w = call("PUT", "/api/mock/local/access", `{"enabled":true,"token":"replacement-1234567890"}`, "127.0.0.1:3000"); w.Code != 200 { t.Fatal(w.Body.String()) } if w = callWithBearer("POST", path, "", "192.168.1.5:3000", "test-secret-1234567890"); w.Code != 401 { t.Fatalf("old bearer still works after replacement: %d", w.Code) } if w = callWithBearer("POST", path, "", "192.168.1.5:3000", "replacement-1234567890"); w.Code != 201 { t.Fatalf("replacement bearer: %d", w.Code) } if w = call("PUT", "/api/mock/local/access", `{"enabled":false}`, "127.0.0.1:3000"); w.Code != 200 { t.Fatal(w.Body.String()) } if w = call("POST", path, "", "192.168.1.5:3000"); w.Code != 201 { t.Fatalf("disabled bearer still required: %d", w.Code) } if w = call("PATCH", "/api/mock/local", `{"enabled":false}`, "192.168.1.5:3000"); w.Code != 403 { t.Fatal("remote toggle allowed") } if w = call("PATCH", "/api/mock/local", `{"enabled":false}`, "127.0.0.1:3000"); w.Code != 200 { t.Fatal(w.Body.String()) } if w = call("POST", path, "", "192.168.1.5:3000"); w.Code != 503 { t.Fatal("disabled mock still responds", w.Code) } if w = call("PATCH", "/api/mock/local", `{"enabled":true}`, "127.0.0.1:3000"); w.Code != 200 { t.Fatal(w.Body.String()) } if w = call("POST", path, "", "192.168.1.5:3000"); w.Code != 201 { t.Fatal("toggle lost mock data", w.Code) } if w = call("DELETE", "/api/mock/local", "", "127.0.0.1:3000"); w.Code != 200 { t.Fatal(w.Body.String()) } if w = call("POST", path, "", "192.168.1.5:3000"); w.Code != 404 { t.Fatalf("revoked token: %d", w.Code) } }