From 3d33e5fb3fa53544b2c951a723dc9657ba4ceacf Mon Sep 17 00:00:00 2001 From: SashaGonch228 Date: Sat, 10 Oct 2026 18:28:11 +0500 Subject: [PATCH] test: add e2e tests for API v2 and fix Windows DB cleanup --- api/apiv2_e2e_test.go | 194 ++++++++++++++++++++++++++++++++++++++++++ api/login_e2e_test.go | 11 ++- 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 api/apiv2_e2e_test.go diff --git a/api/apiv2_e2e_test.go b/api/apiv2_e2e_test.go new file mode 100644 index 0000000..f0a35ce --- /dev/null +++ b/api/apiv2_e2e_test.go @@ -0,0 +1,194 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + "github.com/gin-gonic/gin" + + "github.com/alireza0/s-ui/database" + "github.com/alireza0/s-ui/database/model" +) + +func apiv2TestServer(t *testing.T) (*httptest.Server, string) { + t.Helper() + + dbPath := filepath.Join(t.TempDir(), "test.db") + if err := database.InitDB(dbPath); err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + if db := database.GetDB(); db != nil { + if sqlDB, err := db.DB(); err == nil { + sqlDB.Close() + } + } + }) + + var admin model.User + if err := database.GetDB().Where("username = ?", "admin").First(&admin).Error; err != nil { + t.Fatalf("finding seeded admin user: %v", err) + } + + testToken := "test-valid-api-token-32-chars!!" + tokenRecord := &model.Tokens{ + Token: testToken, + Desc: "e2e-testing-token", + Expiry: 0, + UserId: admin.Id, + } + if err := database.GetDB().Create(tokenRecord).Error; err != nil { + t.Fatalf("seeding test token: %v", err) + } + + engine := gin.New() + apiv2 := NewAPIv2Handler(engine.Group("/apiv2")) + apiv2.ReloadTokens() + + srv := httptest.NewServer(engine) + t.Cleanup(srv.Close) + + return srv, testToken +} + +func TestAPIv2TokenAuthentication(t *testing.T) { + srv, validToken := apiv2TestServer(t) + + testCases := []struct { + name string + token string + sendHeader bool + wantSuccess bool + }{ + {"missing token header", "", false, false}, + {"empty token", "", true, false}, + {"invalid token", "wrong-secret-token", true, false}, + {"valid token", validToken, true, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/apiv2/status", nil) + if err != nil { + t.Fatal(err) + } + if tc.sendHeader { + req.Header.Set("Token", tc.token) + } + + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + + var body Msg + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatalf("decoding response body: %v", err) + } + + if body.Success != tc.wantSuccess { + t.Errorf("body.Success = %v, want %v (msg: %s)", body.Success, tc.wantSuccess, body.Msg) + } + }) + } +} + +func TestAPIv2UnknownActionHandling(t *testing.T) { + srv, validToken := apiv2TestServer(t) + + testCases := []struct { + name string + method string + path string + }{ + {"unknown GET action", http.MethodGet, "/apiv2/unknownAction123"}, + {"unknown POST action", http.MethodPost, "/apiv2/unknownAction123"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(tc.method, srv.URL+tc.path, nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Token", validToken) + + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("status = %d, want 200", resp.StatusCode) + } + + var body Msg + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatalf("decoding response: %v", err) + } + + if body.Success { + t.Error("expected success: false for unknown action") + } + if !strings.Contains(body.Msg, "unknown action") { + t.Errorf("expected error message to mention 'unknown action', got %q", body.Msg) + } + }) + } +} + +func TestAPIv2CoreGetActions(t *testing.T) { + srv, validToken := apiv2TestServer(t) + + actions := []string{ + "/apiv2/status", + "/apiv2/clients", + "/apiv2/config", + "/apiv2/inbounds", + "/apiv2/outbounds", + "/apiv2/tls", + "/apiv2/users", + "/apiv2/settings", + "/apiv2/stats", + } + + for _, path := range actions { + t.Run(path, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+path, nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Token", validToken) + + resp, err := srv.Client().Do(req) + if err != nil { + t.Fatalf("request to %s failed: %v", path, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Fatalf("%s HTTP status = %d, want 200", path, resp.StatusCode) + } + + var body Msg + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + t.Fatalf("decoding %s response: %v", path, err) + } + + if !body.Success { + t.Errorf("%s returned success: false, msg: %s", path, body.Msg) + } + }) + } +} diff --git a/api/login_e2e_test.go b/api/login_e2e_test.go index 6b0d756..da9a39b 100644 --- a/api/login_e2e_test.go +++ b/api/login_e2e_test.go @@ -12,10 +12,11 @@ import ( "github.com/alireza0/s-ui/database" "github.com/alireza0/s-ui/middleware" + "net/http/httptest" + "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" - "net/http/httptest" ) // loginEngine wires the same pieces web.go does: the cookie session store with @@ -27,6 +28,14 @@ func loginEngine(t *testing.T) *gin.Engine { t.Fatal(err) } + t.Cleanup(func() { + if db := database.GetDB(); db != nil { + if sqlDB, err := db.DB(); err == nil { + sqlDB.Close() + } + } + }) + engine := gin.New() store := cookie.NewStore([]byte("test-secret-not-used-anywhere-else")) store.Options(BaseSessionOptions(0))