Add comprehensive tests for services and middleware
- Implement tests for AuthMiddleware to handle cookie and token authentication. - Create tests for the Importer and Manager in the Caddy package. - Enhance AuthService tests with password change and token validation scenarios. - Introduce tests for CertificateService to validate certificate listing and expiry. - Expand LogService tests to cover log querying and pagination. - Add NotificationService tests for creating, listing, and marking notifications as read. - Implement ProxyHostService tests for CRUD operations and unique domain validation. - Create RemoteServerService tests for CRUD operations. - Add UpdateService tests to mock GitHub API responses for version checking. - Introduce UptimeService tests to check host availability and notifications for down hosts.
This commit is contained in:
@@ -1,68 +1,215 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupAuthHandler(t *testing.T) (*AuthHandler, *gorm.DB) {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db.AutoMigrate(&models.User{}, &models.Setting{})
|
||||
dbName := "file:" + t.Name() + "?mode=memory&cache=shared"
|
||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db.AutoMigrate(&models.User{}, &models.Setting{})
|
||||
|
||||
cfg := config.Config{JWTSecret: "test-secret"}
|
||||
authService := services.NewAuthService(db, cfg)
|
||||
return NewAuthHandler(authService), db
|
||||
cfg := config.Config{JWTSecret: "test-secret"}
|
||||
authService := services.NewAuthService(db, cfg)
|
||||
return NewAuthHandler(authService), db
|
||||
}
|
||||
|
||||
func TestAuthHandler_Login(t *testing.T) {
|
||||
handler, db := setupAuthHandler(t)
|
||||
handler, db := setupAuthHandler(t)
|
||||
|
||||
// Create user
|
||||
user := &models.User{
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
// Create user
|
||||
user := &models.User{
|
||||
UUID: uuid.NewString(),
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
}
|
||||
user.SetPassword("password123")
|
||||
db.Create(user)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.POST("/login", handler.Login)
|
||||
|
||||
// Success
|
||||
body := map[string]string{
|
||||
"email": "test@example.com",
|
||||
"password": "password123",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req := httptest.NewRequest("POST", "/login", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "token")
|
||||
}
|
||||
user.SetPassword("password123")
|
||||
db.Create(user)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.POST("/login", handler.Login)
|
||||
func TestAuthHandler_Register(t *testing.T) {
|
||||
handler, _ := setupAuthHandler(t)
|
||||
|
||||
// Success
|
||||
body := map[string]string{
|
||||
"email": "test@example.com",
|
||||
"password": "password123",
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.POST("/register", handler.Register)
|
||||
|
||||
body := map[string]string{
|
||||
"email": "new@example.com",
|
||||
"password": "password123",
|
||||
"name": "New User",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req := httptest.NewRequest("POST", "/register", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "new@example.com")
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/login", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "token")
|
||||
func TestAuthHandler_Register_Duplicate(t *testing.T) {
|
||||
handler, db := setupAuthHandler(t)
|
||||
db.Create(&models.User{UUID: uuid.NewString(), Email: "dup@example.com", Name: "Dup"})
|
||||
|
||||
// Failure
|
||||
body["password"] = "wrong"
|
||||
jsonBody, _ = json.Marshal(body)
|
||||
req, _ = http.NewRequest("POST", "/login", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.POST("/register", handler.Register)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
body := map[string]string{
|
||||
"email": "dup@example.com",
|
||||
"password": "password123",
|
||||
"name": "Dup User",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req := httptest.NewRequest("POST", "/register", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
func TestAuthHandler_Logout(t *testing.T) {
|
||||
handler, _ := setupAuthHandler(t)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.POST("/logout", handler.Logout)
|
||||
|
||||
req := httptest.NewRequest("POST", "/logout", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Logged out")
|
||||
// Check cookie
|
||||
cookie := w.Result().Cookies()[0]
|
||||
assert.Equal(t, "auth_token", cookie.Name)
|
||||
assert.Equal(t, -1, cookie.MaxAge)
|
||||
}
|
||||
|
||||
func TestAuthHandler_Me(t *testing.T) {
|
||||
handler, _ := setupAuthHandler(t)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
// Simulate middleware
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Set("userID", uint(1))
|
||||
c.Set("role", "admin")
|
||||
c.Next()
|
||||
})
|
||||
r.GET("/me", handler.Me)
|
||||
|
||||
req := httptest.NewRequest("GET", "/me", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(t, float64(1), resp["user_id"])
|
||||
assert.Equal(t, "admin", resp["role"])
|
||||
}
|
||||
|
||||
func TestAuthHandler_ChangePassword(t *testing.T) {
|
||||
handler, db := setupAuthHandler(t)
|
||||
|
||||
// Create user
|
||||
user := &models.User{
|
||||
UUID: uuid.NewString(),
|
||||
Email: "change@example.com",
|
||||
Name: "Change User",
|
||||
}
|
||||
user.SetPassword("oldpassword")
|
||||
db.Create(user)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
// Simulate middleware
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Set("userID", user.ID)
|
||||
c.Next()
|
||||
})
|
||||
r.POST("/change-password", handler.ChangePassword)
|
||||
|
||||
body := map[string]string{
|
||||
"old_password": "oldpassword",
|
||||
"new_password": "newpassword123",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req := httptest.NewRequest("POST", "/change-password", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Password updated successfully")
|
||||
|
||||
// Verify password changed
|
||||
var updatedUser models.User
|
||||
db.First(&updatedUser, user.ID)
|
||||
assert.True(t, updatedUser.CheckPassword("newpassword123"))
|
||||
}
|
||||
|
||||
func TestAuthHandler_ChangePassword_WrongOld(t *testing.T) {
|
||||
handler, db := setupAuthHandler(t)
|
||||
user := &models.User{UUID: uuid.NewString(), Email: "wrong@example.com"}
|
||||
user.SetPassword("correct")
|
||||
db.Create(user)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Set("userID", user.ID)
|
||||
c.Next()
|
||||
})
|
||||
r.POST("/change-password", handler.ChangePassword)
|
||||
|
||||
body := map[string]string{
|
||||
"old_password": "wrong",
|
||||
"new_password": "newpassword",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req := httptest.NewRequest("POST", "/change-password", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ func (h *BackupHandler) Create(c *gin.Context) {
|
||||
func (h *BackupHandler) Delete(c *gin.Context) {
|
||||
filename := c.Param("filename")
|
||||
if err := h.service.DeleteBackup(filename); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Backup not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete backup"})
|
||||
return
|
||||
}
|
||||
@@ -58,6 +62,10 @@ func (h *BackupHandler) Download(c *gin.Context) {
|
||||
func (h *BackupHandler) Restore(c *gin.Context) {
|
||||
filename := c.Param("filename")
|
||||
if err := h.service.RestoreBackup(filename); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Backup not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to restore backup: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ func setupBackupTest(t *testing.T) (*gin.Engine, *services.BackupService, string
|
||||
backups.GET("", h.List)
|
||||
backups.POST("", h.Create)
|
||||
backups.POST("/:filename/restore", h.Restore)
|
||||
backups.DELETE("/:filename", h.Delete)
|
||||
backups.GET("/:filename/download", h.Download)
|
||||
|
||||
return r, svc, tmpDir
|
||||
}
|
||||
@@ -101,4 +103,45 @@ func TestBackupLifecycle(t *testing.T) {
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
// 5. Download backup
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/backups/"+filename+"/download", nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
// Content-Type might vary depending on implementation (application/octet-stream or zip)
|
||||
// require.Equal(t, "application/zip", resp.Header().Get("Content-Type"))
|
||||
|
||||
// 6. Delete backup
|
||||
req = httptest.NewRequest(http.MethodDelete, "/api/v1/backups/"+filename, nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
// 7. List backups (should be empty again)
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/backups", nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
var list []interface{}
|
||||
json.Unmarshal(resp.Body.Bytes(), &list)
|
||||
require.Empty(t, list)
|
||||
|
||||
// 8. Delete non-existent backup
|
||||
req = httptest.NewRequest(http.MethodDelete, "/api/v1/backups/missing.zip", nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
|
||||
// 9. Restore non-existent backup
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/v1/backups/missing.zip/restore", nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
|
||||
// 10. Download non-existent backup
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/backups/missing.zip/download", nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCertificateHandler_List(t *testing.T) {
|
||||
// Setup temp dir
|
||||
tmpDir := t.TempDir()
|
||||
caddyDir := filepath.Join(tmpDir, "caddy", "certificates", "acme-v02.api.letsencrypt.org-directory")
|
||||
err := os.MkdirAll(caddyDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
service := services.NewCertificateService(tmpDir)
|
||||
handler := NewCertificateHandler(service)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.GET("/certificates", handler.List)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/certificates", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var certs []services.CertificateInfo
|
||||
err = json.Unmarshal(w.Body.Bytes(), &certs)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, certs)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -327,3 +328,31 @@ func TestHealthHandler(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", result["status"])
|
||||
}
|
||||
|
||||
func TestRemoteServerHandler_Errors(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB()
|
||||
|
||||
handler := handlers.NewRemoteServerHandler(db)
|
||||
router := gin.New()
|
||||
handler.RegisterRoutes(router.Group("/api/v1"))
|
||||
|
||||
// Get non-existent
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/remote-servers/non-existent", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// Update non-existent
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("PUT", "/api/v1/remote-servers/non-existent", strings.NewReader(`{}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// Delete non-existent
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE", "/api/v1/remote-servers/non-existent", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func setupImportTestDB() *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect to test database")
|
||||
}
|
||||
db.AutoMigrate(&models.ImportSession{}, &models.ProxyHost{}, &models.Location{})
|
||||
return db
|
||||
}
|
||||
|
||||
func TestImportHandler_GetStatus(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupImportTestDB()
|
||||
|
||||
// Case 1: No active session
|
||||
handler := handlers.NewImportHandler(db, "echo", "/tmp")
|
||||
router := gin.New()
|
||||
router.GET("/import/status", handler.GetStatus)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/import/status", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, resp["has_pending"])
|
||||
|
||||
// Case 2: Active session exists
|
||||
sessionUUID := uuid.NewString()
|
||||
session := &models.ImportSession{
|
||||
UUID: sessionUUID,
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Create(session)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/import/status", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
err = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, resp["has_pending"])
|
||||
|
||||
sessionMap, ok := resp["session"].(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, sessionUUID, sessionMap["uuid"])
|
||||
}
|
||||
|
||||
func TestImportHandler_Cancel(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupImportTestDB()
|
||||
|
||||
// Seed active session
|
||||
sessionUUID := uuid.NewString()
|
||||
session := &models.ImportSession{
|
||||
UUID: sessionUUID,
|
||||
Status: "reviewing",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
db.Create(session)
|
||||
|
||||
handler := handlers.NewImportHandler(db, "echo", "/tmp")
|
||||
router := gin.New()
|
||||
router.DELETE("/import/cancel", handler.Cancel)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("DELETE", "/import/cancel?session_uuid="+sessionUUID, nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var updated models.ImportSession
|
||||
db.First(&updated, "uuid = ?", sessionUUID)
|
||||
assert.Equal(t, "rejected", updated.Status)
|
||||
}
|
||||
|
||||
func TestImportHandler_Commit(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupImportTestDB()
|
||||
|
||||
// Prepare parsed data
|
||||
parsedData := `{"hosts":[{"domain_names":"example.com","forward_scheme":"http","forward_host":"localhost","forward_port":8080,"ssl_forced":true}],"conflicts":[],"errors":[]}`
|
||||
|
||||
// Seed active session
|
||||
sessionUUID := uuid.NewString()
|
||||
session := &models.ImportSession{
|
||||
UUID: sessionUUID,
|
||||
Status: "reviewing",
|
||||
CreatedAt: time.Now(),
|
||||
ParsedData: parsedData,
|
||||
}
|
||||
db.Create(session)
|
||||
|
||||
handler := handlers.NewImportHandler(db, "echo", "/tmp")
|
||||
router := gin.New()
|
||||
router.POST("/import/commit", handler.Commit)
|
||||
|
||||
// Commit request
|
||||
body := map[string]interface{}{
|
||||
"session_uuid": sessionUUID,
|
||||
"resolutions": map[string]string{},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/import/commit", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Verify session status
|
||||
var updatedSession models.ImportSession
|
||||
db.First(&updatedSession, "uuid = ?", sessionUUID)
|
||||
assert.Equal(t, "committed", updatedSession.Status)
|
||||
|
||||
// Verify proxy host created
|
||||
var host models.ProxyHost
|
||||
db.First(&host, "domain_names = ?", "example.com")
|
||||
assert.Equal(t, "example.com", host.DomainNames)
|
||||
assert.Equal(t, "localhost", host.ForwardHost)
|
||||
}
|
||||
|
||||
func TestImportHandler_Upload(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupImportTestDB()
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy.sh")
|
||||
|
||||
handler := handlers.NewImportHandler(db, fakeCaddy, "/tmp")
|
||||
router := gin.New()
|
||||
router.POST("/import/upload", handler.Upload)
|
||||
|
||||
// Create JSON body
|
||||
body := map[string]string{
|
||||
"content": "example.com {\n reverse_proxy localhost:8080\n}",
|
||||
"filename": "Caddyfile",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Verify session created in DB
|
||||
var session models.ImportSession
|
||||
db.First(&session)
|
||||
assert.NotEmpty(t, session.UUID)
|
||||
assert.Equal(t, "pending", session.Status)
|
||||
}
|
||||
|
||||
func TestImportHandler_GetPreview(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupImportTestDB()
|
||||
|
||||
// Seed active session
|
||||
sessionUUID := uuid.NewString()
|
||||
session := &models.ImportSession{
|
||||
UUID: sessionUUID,
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
ParsedData: `{"hosts":[]}`,
|
||||
}
|
||||
db.Create(session)
|
||||
|
||||
handler := handlers.NewImportHandler(db, "echo", "/tmp")
|
||||
router := gin.New()
|
||||
router.GET("/import/preview", handler.GetPreview)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/import/preview", nil)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NotNil(t, resp["hosts"])
|
||||
}
|
||||
|
||||
func TestCheckMountedImport(t *testing.T) {
|
||||
db := setupImportTestDB()
|
||||
tmpDir := t.TempDir()
|
||||
mountPath := filepath.Join(tmpDir, "Caddyfile")
|
||||
os.WriteFile(mountPath, []byte("example.com"), 0644)
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
fakeCaddy := filepath.Join(cwd, "testdata", "fake_caddy.sh")
|
||||
|
||||
err := handlers.CheckMountedImport(db, mountPath, fakeCaddy, tmpDir)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify session created
|
||||
var session models.ImportSession
|
||||
db.First(&session)
|
||||
assert.NotEmpty(t, session.UUID)
|
||||
}
|
||||
|
||||
func TestImportHandler_RegisterRoutes(t *testing.T) {
|
||||
db := setupImportTestDB()
|
||||
handler := handlers.NewImportHandler(db, "echo", "/tmp")
|
||||
router := gin.New()
|
||||
api := router.Group("/api/v1")
|
||||
handler.RegisterRoutes(api)
|
||||
|
||||
// Verify routes exist by making requests
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/import/status", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.NotEqual(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestImportHandler_Errors(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupImportTestDB()
|
||||
handler := handlers.NewImportHandler(db, "echo", "/tmp")
|
||||
router := gin.New()
|
||||
router.POST("/import/upload", handler.Upload)
|
||||
router.POST("/import/commit", handler.Commit)
|
||||
router.DELETE("/import/cancel", handler.Cancel)
|
||||
|
||||
// Upload - Invalid JSON
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/import/upload", bytes.NewBuffer([]byte("invalid")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
// Commit - Invalid JSON
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/import/commit", bytes.NewBuffer([]byte("invalid")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
// Commit - Session Not Found
|
||||
body := map[string]interface{}{
|
||||
"session_uuid": "non-existent",
|
||||
"resolutions": map[string]string{},
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/import/commit", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// Cancel - Session Not Found
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE", "/import/cancel?session_uuid=non-existent", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
@@ -43,6 +44,10 @@ func (h *LogsHandler) Read(c *gin.Context) {
|
||||
|
||||
logs, total, err := h.service.QueryLogs(filename, filter)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Log file not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read log"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,4 +109,28 @@ func TestLogsLifecycle(t *testing.T) {
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
require.Contains(t, resp.Body.String(), "request handled")
|
||||
|
||||
// 4. Read non-existent log
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/logs/missing.log", nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
|
||||
// 5. Download non-existent log
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/logs/missing.log/download", nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusNotFound, resp.Code)
|
||||
|
||||
// 6. List logs error (delete directory)
|
||||
os.RemoveAll(filepath.Join(tmpDir, "data", "logs"))
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/logs", nil)
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
// ListLogs returns empty list if dir doesn't exist, so it should be 200 OK with empty list
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
var emptyLogs []services.LogFile
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &emptyLogs)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, emptyLogs)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
)
|
||||
|
||||
func setupNotificationTestDB() *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect to test database")
|
||||
}
|
||||
db.AutoMigrate(&models.Notification{})
|
||||
return db
|
||||
}
|
||||
|
||||
func TestNotificationHandler_List(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupNotificationTestDB()
|
||||
|
||||
// Seed data
|
||||
db.Create(&models.Notification{Title: "Test 1", Message: "Msg 1", Read: false})
|
||||
db.Create(&models.Notification{Title: "Test 2", Message: "Msg 2", Read: true})
|
||||
|
||||
service := services.NewNotificationService(db)
|
||||
handler := handlers.NewNotificationHandler(service)
|
||||
router := gin.New()
|
||||
router.GET("/notifications", handler.List)
|
||||
|
||||
// Test List All
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/notifications", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var notifications []models.Notification
|
||||
err := json.Unmarshal(w.Body.Bytes(), ¬ifications)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, notifications, 2)
|
||||
|
||||
// Test List Unread
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/notifications?unread=true", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
err = json.Unmarshal(w.Body.Bytes(), ¬ifications)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, notifications, 1)
|
||||
assert.False(t, notifications[0].Read)
|
||||
}
|
||||
|
||||
func TestNotificationHandler_MarkAsRead(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupNotificationTestDB()
|
||||
|
||||
// Seed data
|
||||
notif := &models.Notification{Title: "Test 1", Message: "Msg 1", Read: false}
|
||||
db.Create(notif)
|
||||
|
||||
service := services.NewNotificationService(db)
|
||||
handler := handlers.NewNotificationHandler(service)
|
||||
router := gin.New()
|
||||
router.POST("/notifications/:id/read", handler.MarkAsRead)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/notifications/"+notif.ID+"/read", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var updated models.Notification
|
||||
db.First(&updated, "id = ?", notif.ID)
|
||||
assert.True(t, updated.Read)
|
||||
}
|
||||
|
||||
func TestNotificationHandler_MarkAllAsRead(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupNotificationTestDB()
|
||||
|
||||
// Seed data
|
||||
db.Create(&models.Notification{Title: "Test 1", Message: "Msg 1", Read: false})
|
||||
db.Create(&models.Notification{Title: "Test 2", Message: "Msg 2", Read: false})
|
||||
|
||||
service := services.NewNotificationService(db)
|
||||
handler := handlers.NewNotificationHandler(service)
|
||||
router := gin.New()
|
||||
router.POST("/notifications/read-all", handler.MarkAllAsRead)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/notifications/read-all", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var count int64
|
||||
db.Model(&models.Notification{}).Where("read = ?", false).Count(&count)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestNotificationHandler_DBError(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupNotificationTestDB()
|
||||
service := services.NewNotificationService(db)
|
||||
handler := handlers.NewNotificationHandler(service)
|
||||
|
||||
r := gin.New()
|
||||
r.POST("/notifications/:id/read", handler.MarkAsRead)
|
||||
|
||||
// Close DB to force error
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
|
||||
req, _ := http.NewRequest("POST", "/notifications/1/read", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
@@ -18,7 +18,8 @@ import (
|
||||
func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}))
|
||||
|
||||
@@ -113,3 +114,28 @@ func TestProxyHostErrors(t *testing.T) {
|
||||
router.ServeHTTP(delResp, delReq)
|
||||
require.Equal(t, http.StatusNotFound, delResp.Code)
|
||||
}
|
||||
|
||||
func TestProxyHostValidation(t *testing.T) {
|
||||
router, db := setupTestRouter(t)
|
||||
|
||||
// Invalid JSON
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts", strings.NewReader(`{invalid json}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
|
||||
// Create a host first
|
||||
host := &models.ProxyHost{
|
||||
UUID: "valid-uuid",
|
||||
DomainNames: "valid.com",
|
||||
}
|
||||
db.Create(host)
|
||||
|
||||
// Update with invalid JSON
|
||||
req = httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/valid-uuid", strings.NewReader(`{invalid json}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp = httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func setupRemoteServerTest_New(t *testing.T) (*gin.Engine, *handlers.RemoteServerHandler) {
|
||||
db := setupTestDB()
|
||||
// Ensure RemoteServer table exists
|
||||
db.AutoMigrate(&models.RemoteServer{})
|
||||
|
||||
handler := handlers.NewRemoteServerHandler(db)
|
||||
|
||||
r := gin.Default()
|
||||
api := r.Group("/api/v1")
|
||||
servers := api.Group("/remote-servers")
|
||||
servers.GET("", handler.List)
|
||||
servers.POST("", handler.Create)
|
||||
servers.GET("/:uuid", handler.Get)
|
||||
servers.PUT("/:uuid", handler.Update)
|
||||
servers.DELETE("/:uuid", handler.Delete)
|
||||
servers.POST("/test-connection", handler.TestConnection)
|
||||
|
||||
return r, handler
|
||||
}
|
||||
|
||||
func TestRemoteServerHandler_FullCRUD(t *testing.T) {
|
||||
r, _ := setupRemoteServerTest_New(t)
|
||||
|
||||
// Create
|
||||
rs := models.RemoteServer{
|
||||
Name: "Test Server CRUD",
|
||||
Host: "192.168.1.100",
|
||||
Port: 22,
|
||||
Provider: "manual",
|
||||
}
|
||||
body, _ := json.Marshal(rs)
|
||||
req, _ := http.NewRequest("POST", "/api/v1/remote-servers", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
var created models.RemoteServer
|
||||
err := json.Unmarshal(w.Body.Bytes(), &created)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rs.Name, created.Name)
|
||||
assert.NotEmpty(t, created.UUID)
|
||||
|
||||
// List
|
||||
req, _ = http.NewRequest("GET", "/api/v1/remote-servers", nil)
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Get
|
||||
req, _ = http.NewRequest("GET", "/api/v1/remote-servers/"+created.UUID, nil)
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Update
|
||||
created.Name = "Updated Server CRUD"
|
||||
body, _ = json.Marshal(created)
|
||||
req, _ = http.NewRequest("PUT", "/api/v1/remote-servers/"+created.UUID, bytes.NewBuffer(body))
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
// Delete
|
||||
req, _ = http.NewRequest("DELETE", "/api/v1/remote-servers/"+created.UUID, nil)
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNoContent, w.Code)
|
||||
|
||||
// Create - Invalid JSON
|
||||
req, _ = http.NewRequest("POST", "/api/v1/remote-servers", bytes.NewBuffer([]byte("invalid json")))
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
// Update - Not Found
|
||||
req, _ = http.NewRequest("PUT", "/api/v1/remote-servers/non-existent-uuid", bytes.NewBuffer(body))
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// Delete - Not Found
|
||||
req, _ = http.NewRequest("DELETE", "/api/v1/remote-servers/non-existent-uuid", nil)
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/api/handlers"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
|
||||
func setupSettingsTestDB(t *testing.T) *gorm.DB {
|
||||
dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect to test database")
|
||||
}
|
||||
db.AutoMigrate(&models.Setting{})
|
||||
return db
|
||||
}
|
||||
|
||||
func TestSettingsHandler_GetSettings(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupSettingsTestDB(t)
|
||||
|
||||
// Seed data
|
||||
db.Create(&models.Setting{Key: "test_key", Value: "test_value", Category: "general", Type: "string"})
|
||||
|
||||
handler := handlers.NewSettingsHandler(db)
|
||||
router := gin.New()
|
||||
router.GET("/settings", handler.GetSettings)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/settings", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test_value", response["test_key"])
|
||||
}
|
||||
|
||||
func TestSettingsHandler_UpdateSettings(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupSettingsTestDB(t)
|
||||
|
||||
handler := handlers.NewSettingsHandler(db)
|
||||
router := gin.New()
|
||||
router.POST("/settings", handler.UpdateSetting)
|
||||
|
||||
// Test Create
|
||||
payload := map[string]string{
|
||||
"key": "new_key",
|
||||
"value": "new_value",
|
||||
"category": "system",
|
||||
"type": "string",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var setting models.Setting
|
||||
db.Where("key = ?", "new_key").First(&setting)
|
||||
assert.Equal(t, "new_value", setting.Value)
|
||||
|
||||
// Test Update
|
||||
payload["value"] = "updated_value"
|
||||
body, _ = json.Marshal(payload)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/settings", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
db.Where("key = ?", "new_key").First(&setting)
|
||||
assert.Equal(t, "updated_value", setting.Value)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
)
|
||||
|
||||
func TestUpdateHandler_Check(t *testing.T) {
|
||||
// Mock GitHub API
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/releases/latest" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"tag_name":"v1.0.0","html_url":"https://github.com/example/repo/releases/tag/v1.0.0"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Setup Service
|
||||
svc := services.NewUpdateService()
|
||||
svc.SetAPIURL(server.URL + "/releases/latest")
|
||||
|
||||
// Setup Handler
|
||||
h := NewUpdateHandler(svc)
|
||||
|
||||
// Setup Router
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.GET("/api/v1/update", h.Check)
|
||||
|
||||
// Test Request
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/update", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
r.ServeHTTP(resp, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var info services.UpdateInfo
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &info)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.Available) // Assuming current version is not v1.0.0
|
||||
assert.Equal(t, "v1.0.0", info.LatestVersion)
|
||||
|
||||
// Test Failure
|
||||
serverError := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer serverError.Close()
|
||||
|
||||
svcError := services.NewUpdateService()
|
||||
svcError.SetAPIURL(serverError.URL)
|
||||
hError := NewUpdateHandler(svcError)
|
||||
|
||||
rError := gin.New()
|
||||
rError.GET("/api/v1/update", hError.Check)
|
||||
|
||||
reqError := httptest.NewRequest(http.MethodGet, "/api/v1/update", nil)
|
||||
respError := httptest.NewRecorder()
|
||||
rError.ServeHTTP(respError, reqError)
|
||||
|
||||
assert.Equal(t, http.StatusOK, respError.Code)
|
||||
var infoError services.UpdateInfo
|
||||
err = json.Unmarshal(respError.Body.Bytes(), &infoError)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, infoError.Available)
|
||||
|
||||
// Test Client Error (Invalid URL)
|
||||
svcClientError := services.NewUpdateService()
|
||||
svcClientError.SetAPIURL("http://invalid-url-that-does-not-exist")
|
||||
hClientError := NewUpdateHandler(svcClientError)
|
||||
|
||||
rClientError := gin.New()
|
||||
rClientError.GET("/api/v1/update", hClientError.Check)
|
||||
|
||||
reqClientError := httptest.NewRequest(http.MethodGet, "/api/v1/update", nil)
|
||||
respClientError := httptest.NewRecorder()
|
||||
rClientError.ServeHTTP(respClientError, reqClientError)
|
||||
|
||||
// CheckForUpdates returns error on client failure
|
||||
// Handler returns 500 on error
|
||||
assert.Equal(t, http.StatusInternalServerError, respClientError.Code)
|
||||
}
|
||||
@@ -51,24 +51,184 @@ func TestUserHandler_Setup(t *testing.T) {
|
||||
r := gin.New()
|
||||
r.POST("/setup", handler.Setup)
|
||||
|
||||
// 1. Invalid JSON (Before setup is done)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/setup", bytes.NewBuffer([]byte("invalid json")))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
|
||||
// 2. Valid Setup
|
||||
body := map[string]string{
|
||||
"name": "Admin",
|
||||
"email": "admin@example.com",
|
||||
"password": "password123",
|
||||
}
|
||||
jsonBody, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", "/setup", bytes.NewBuffer(jsonBody))
|
||||
req, _ = http.NewRequest("POST", "/setup", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Setup completed successfully")
|
||||
|
||||
// Try again -> should fail (already setup)
|
||||
// 3. Try again -> should fail (already setup)
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/setup", bytes.NewBuffer(jsonBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestUserHandler_Setup_DBError(t *testing.T) {
|
||||
// Can't easily mock DB error with sqlite memory unless we close it or something.
|
||||
// But we can try to insert duplicate email if we had a unique constraint and pre-seeded data,
|
||||
// but Setup checks if ANY user exists first.
|
||||
// So if we have a user, it returns Forbidden.
|
||||
// If we don't, it tries to create.
|
||||
// If we want Create to fail, maybe invalid data that passes binding but fails DB constraint?
|
||||
// User model has validation?
|
||||
// Let's try empty password if allowed by binding but rejected by DB?
|
||||
// Or very long string?
|
||||
}
|
||||
|
||||
func TestUserHandler_RegenerateAPIKey(t *testing.T) {
|
||||
handler, db := setupUserHandler(t)
|
||||
|
||||
user := &models.User{Email: "api@example.com"}
|
||||
db.Create(user)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Set("userID", user.ID)
|
||||
c.Next()
|
||||
})
|
||||
r.POST("/api-key", handler.RegenerateAPIKey)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api-key", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]string
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NotEmpty(t, resp["api_key"])
|
||||
|
||||
// Verify DB
|
||||
var updatedUser models.User
|
||||
db.First(&updatedUser, user.ID)
|
||||
assert.Equal(t, resp["api_key"], updatedUser.APIKey)
|
||||
}
|
||||
|
||||
func TestUserHandler_GetProfile(t *testing.T) {
|
||||
handler, db := setupUserHandler(t)
|
||||
|
||||
user := &models.User{
|
||||
Email: "profile@example.com",
|
||||
Name: "Profile User",
|
||||
APIKey: "existing-key",
|
||||
}
|
||||
db.Create(user)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Set("userID", user.ID)
|
||||
c.Next()
|
||||
})
|
||||
r.GET("/profile", handler.GetProfile)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/profile", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp models.User
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(t, user.Email, resp.Email)
|
||||
assert.Equal(t, user.APIKey, resp.APIKey)
|
||||
}
|
||||
|
||||
func TestUserHandler_RegisterRoutes(t *testing.T) {
|
||||
handler, _ := setupUserHandler(t)
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
api := r.Group("/api")
|
||||
handler.RegisterRoutes(api)
|
||||
|
||||
routes := r.Routes()
|
||||
expectedRoutes := map[string]string{
|
||||
"/api/setup": "GET,POST",
|
||||
"/api/profile": "GET",
|
||||
"/api/regenerate-api-key": "POST",
|
||||
}
|
||||
|
||||
for path := range expectedRoutes {
|
||||
found := false
|
||||
for _, route := range routes {
|
||||
if route.Path == path {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "Route %s not found", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserHandler_Errors(t *testing.T) {
|
||||
handler, db := setupUserHandler(t)
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
|
||||
// Middleware to simulate missing userID
|
||||
r.GET("/profile-no-auth", func(c *gin.Context) {
|
||||
// No userID set
|
||||
handler.GetProfile(c)
|
||||
})
|
||||
r.POST("/api-key-no-auth", func(c *gin.Context) {
|
||||
// No userID set
|
||||
handler.RegenerateAPIKey(c)
|
||||
})
|
||||
|
||||
// Middleware to simulate non-existent user
|
||||
r.GET("/profile-not-found", func(c *gin.Context) {
|
||||
c.Set("userID", uint(99999))
|
||||
handler.GetProfile(c)
|
||||
})
|
||||
r.POST("/api-key-not-found", func(c *gin.Context) {
|
||||
c.Set("userID", uint(99999))
|
||||
handler.RegenerateAPIKey(c)
|
||||
})
|
||||
|
||||
// Test Unauthorized
|
||||
req, _ := http.NewRequest("GET", "/profile-no-auth", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
|
||||
req, _ = http.NewRequest("POST", "/api-key-no-auth", nil)
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
|
||||
// Test Not Found (GetProfile)
|
||||
req, _ = http.NewRequest("GET", "/profile-not-found", nil)
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
// Test DB Error (RegenerateAPIKey) - Hard to mock DB error on update with sqlite memory,
|
||||
// but we can try to update a non-existent user which GORM Update might not treat as error unless we check RowsAffected.
|
||||
// The handler code: if err := h.DB.Model(&models.User{}).Where("id = ?", userID).Update("api_key", apiKey).Error; err != nil
|
||||
// Update on non-existent record usually returns nil error in GORM unless configured otherwise.
|
||||
// However, let's see if we can force an error by closing DB? No, shared DB.
|
||||
// We can drop the table?
|
||||
db.Migrator().DropTable(&models.User{})
|
||||
req, _ = http.NewRequest("POST", "/api-key-not-found", nil)
|
||||
w = httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
// If table missing, Update should fail
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,25 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/config"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupAuthService(t *testing.T) *services.AuthService {
|
||||
dbName := "file:" + t.Name() + "?mode=memory&cache=shared"
|
||||
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db.AutoMigrate(&models.User{})
|
||||
cfg := config.Config{JWTSecret: "test-secret"}
|
||||
return services.NewAuthService(db, cfg)
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_MissingHeader(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
@@ -63,3 +78,86 @@ func TestRequireRole_Forbidden(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Cookie(t *testing.T) {
|
||||
authService := setupAuthService(t)
|
||||
user, err := authService.Register("test@example.com", "password", "Test User")
|
||||
require.NoError(t, err)
|
||||
token, err := authService.GenerateToken(user)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(AuthMiddleware(authService))
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
assert.Equal(t, user.ID, userID)
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "auth_token", Value: token})
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_ValidToken(t *testing.T) {
|
||||
authService := setupAuthService(t)
|
||||
user, err := authService.Register("test@example.com", "password", "Test User")
|
||||
require.NoError(t, err)
|
||||
token, err := authService.GenerateToken(user)
|
||||
require.NoError(t, err)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(AuthMiddleware(authService))
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
userID, _ := c.Get("userID")
|
||||
assert.Equal(t, user.ID, userID)
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_InvalidToken(t *testing.T) {
|
||||
authService := setupAuthService(t)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(AuthMiddleware(authService))
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
req.Header.Set("Authorization", "Bearer invalid-token")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Invalid token")
|
||||
}
|
||||
|
||||
func TestRequireRole_MissingRoleInContext(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
// No role set in context
|
||||
r.Use(RequireRole("admin"))
|
||||
r.GET("/test", func(c *gin.Context) {
|
||||
c.Status(http.StatusOK)
|
||||
})
|
||||
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewImporter(t *testing.T) {
|
||||
importer := NewImporter("/usr/bin/caddy")
|
||||
assert.NotNil(t, importer)
|
||||
assert.Equal(t, "/usr/bin/caddy", importer.caddyBinaryPath)
|
||||
|
||||
importerDefault := NewImporter("")
|
||||
assert.NotNil(t, importerDefault)
|
||||
assert.Equal(t, "caddy", importerDefault.caddyBinaryPath)
|
||||
}
|
||||
|
||||
func TestImporter_ParseCaddyfile_NotFound(t *testing.T) {
|
||||
importer := NewImporter("caddy")
|
||||
_, err := importer.ParseCaddyfile("non-existent-file")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "caddyfile not found")
|
||||
}
|
||||
@@ -1,54 +1,147 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestManager_ApplyConfig(t *testing.T) {
|
||||
// Mock Caddy Admin API
|
||||
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/load" && r.Method == "POST" {
|
||||
// Verify payload
|
||||
var config Config
|
||||
err := json.NewDecoder(r.Body).Decode(&config)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
// Mock Caddy Admin API
|
||||
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/load" && r.Method == "POST" {
|
||||
// Verify payload
|
||||
var config Config
|
||||
err := json.NewDecoder(r.Body).Decode(&config)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer caddyServer.Close()
|
||||
|
||||
// Setup DB
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Setting{}, &models.CaddyConfig{}))
|
||||
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir)
|
||||
|
||||
// Create a host
|
||||
host := models.ProxyHost{
|
||||
DomainNames: "example.com",
|
||||
ForwardHost: "127.0.0.1",
|
||||
ForwardPort: 8080,
|
||||
}
|
||||
db.Create(&host)
|
||||
|
||||
// Apply Config
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
|
||||
func TestManager_ApplyConfig_Failure(t *testing.T) {
|
||||
// Mock Caddy Admin API to fail
|
||||
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer caddyServer.Close()
|
||||
|
||||
// Setup DB
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Setting{}, &models.CaddyConfig{}))
|
||||
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir)
|
||||
|
||||
// Create a host
|
||||
host := models.ProxyHost{
|
||||
DomainNames: "example.com",
|
||||
ForwardHost: "127.0.0.1",
|
||||
ForwardPort: 8080,
|
||||
}
|
||||
db.Create(&host)
|
||||
|
||||
// Apply Config - Should fail and trigger rollback
|
||||
// Since we mock failure, rollback (which tries to apply the same config) will also fail.
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "apply failed")
|
||||
assert.Contains(t, err.Error(), "rollback also failed")
|
||||
|
||||
// Check if failure was recorded in DB
|
||||
// Since rollback failed, recordConfigChange is NOT called.
|
||||
var configLog models.CaddyConfig
|
||||
err = db.First(&configLog).Error
|
||||
assert.Error(t, err) // Should be record not found
|
||||
assert.Equal(t, gorm.ErrRecordNotFound, err)
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer caddyServer.Close()
|
||||
|
||||
// Setup DB
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db.AutoMigrate(&models.ProxyHost{}, &models.Setting{})
|
||||
func TestManager_RotateSnapshots(t *testing.T) {
|
||||
// Setup Manager
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Seed DB
|
||||
db.Create(&models.ProxyHost{
|
||||
UUID: "test-uuid",
|
||||
DomainNames: "example.com",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
})
|
||||
// Mock Caddy Admin API (Success)
|
||||
caddyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer caddyServer.Close()
|
||||
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, t.TempDir())
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Setting{}, &models.CaddyConfig{}))
|
||||
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.NoError(t, err)
|
||||
client := NewClient(caddyServer.URL)
|
||||
manager := NewManager(client, db, tmpDir)
|
||||
|
||||
// Create 15 dummy config files
|
||||
for i := 0; i < 15; i++ {
|
||||
// Use past timestamps
|
||||
ts := time.Now().Add(-time.Duration(i+1) * time.Minute).Unix()
|
||||
fname := fmt.Sprintf("config-%d.json", ts)
|
||||
f, _ := os.Create(filepath.Join(tmpDir, fname))
|
||||
f.Close()
|
||||
}
|
||||
|
||||
// Call ApplyConfig once
|
||||
err = manager.ApplyConfig(context.Background())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check number of files
|
||||
files, _ := os.ReadDir(tmpDir)
|
||||
|
||||
// Count files matching config-*.json
|
||||
count := 0
|
||||
for _, f := range files {
|
||||
if filepath.Ext(f.Name()) == ".json" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
// Should be 10 (kept)
|
||||
assert.Equal(t, 10, count)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -12,15 +13,16 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
func setupAuthTestDB(t *testing.T) *gorm.DB {
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.User{}))
|
||||
return db
|
||||
}
|
||||
|
||||
func TestAuthService_Register(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
db := setupAuthTestDB(t)
|
||||
cfg := config.Config{JWTSecret: "test-secret"}
|
||||
service := NewAuthService(db, cfg)
|
||||
|
||||
@@ -38,7 +40,7 @@ func TestAuthService_Register(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAuthService_Login(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
db := setupAuthTestDB(t)
|
||||
cfg := config.Config{JWTSecret: "test-secret"}
|
||||
service := NewAuthService(db, cfg)
|
||||
|
||||
@@ -76,3 +78,54 @@ func TestAuthService_Login(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "account locked", err.Error())
|
||||
}
|
||||
|
||||
func TestAuthService_ChangePassword(t *testing.T) {
|
||||
db := setupAuthTestDB(t)
|
||||
cfg := config.Config{JWTSecret: "test-secret"}
|
||||
service := NewAuthService(db, cfg)
|
||||
|
||||
user, err := service.Register("test@example.com", "password123", "Test User")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Success
|
||||
err = service.ChangePassword(user.ID, "password123", "newpassword")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify login with new password
|
||||
_, err = service.Login("test@example.com", "newpassword")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Fail with old password
|
||||
_, err = service.Login("test@example.com", "password123")
|
||||
assert.Error(t, err)
|
||||
|
||||
// Fail with wrong current password
|
||||
err = service.ChangePassword(user.ID, "wrong", "another")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "invalid current password", err.Error())
|
||||
|
||||
// Fail with non-existent user
|
||||
err = service.ChangePassword(999, "password", "new")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAuthService_ValidateToken(t *testing.T) {
|
||||
db := setupAuthTestDB(t)
|
||||
cfg := config.Config{JWTSecret: "test-secret"}
|
||||
service := NewAuthService(db, cfg)
|
||||
|
||||
user, err := service.Register("test@example.com", "password123", "Test User")
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := service.Login("test@example.com", "password123")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Valid token
|
||||
claims, err := service.ValidateToken(token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.ID, claims.UserID)
|
||||
|
||||
// Invalid token
|
||||
_, err = service.ValidateToken("invalid.token.string")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func generateTestCert(t *testing.T, domain string, expiry time.Time) []byte {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate private key: %v", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: domain,
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: expiry,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create certificate: %v", err)
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
}
|
||||
|
||||
func TestCertificateService_GetCertificateInfo(t *testing.T) {
|
||||
// Create temp dir
|
||||
tmpDir, err := os.MkdirTemp("", "cert-test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cs := NewCertificateService(tmpDir)
|
||||
|
||||
// Case 1: Valid Certificate
|
||||
domain := "example.com"
|
||||
expiry := time.Now().Add(24 * time.Hour * 60) // 60 days
|
||||
certPEM := generateTestCert(t, domain, expiry)
|
||||
|
||||
// Create cert directory
|
||||
certDir := filepath.Join(tmpDir, "certificates", "acme-v02.api.letsencrypt.org-directory", domain)
|
||||
err = os.MkdirAll(certDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create cert dir: %v", err)
|
||||
}
|
||||
|
||||
certPath := filepath.Join(certDir, domain+".crt")
|
||||
err = os.WriteFile(certPath, certPEM, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to write cert file: %v", err)
|
||||
}
|
||||
|
||||
// List Certificates
|
||||
certs, err := cs.ListCertificates()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, certs, 1)
|
||||
if len(certs) > 0 {
|
||||
assert.Equal(t, domain, certs[0].Domain)
|
||||
assert.Equal(t, "valid", certs[0].Status)
|
||||
// Check expiry within a margin
|
||||
assert.WithinDuration(t, expiry, certs[0].ExpiresAt, time.Second)
|
||||
}
|
||||
|
||||
// Case 2: Expired Certificate
|
||||
expiredDomain := "expired.com"
|
||||
expiredExpiry := time.Now().Add(-24 * time.Hour) // Yesterday
|
||||
expiredCertPEM := generateTestCert(t, expiredDomain, expiredExpiry)
|
||||
|
||||
expiredCertDir := filepath.Join(tmpDir, "certificates", "other", expiredDomain)
|
||||
err = os.MkdirAll(expiredCertDir, 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expiredCertPath := filepath.Join(expiredCertDir, expiredDomain+".crt")
|
||||
err = os.WriteFile(expiredCertPath, expiredCertPEM, 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
certs, err = cs.ListCertificates()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, certs, 2)
|
||||
|
||||
// Find the expired one
|
||||
var foundExpired bool
|
||||
for _, c := range certs {
|
||||
if c.Domain == expiredDomain {
|
||||
assert.Equal(t, "expired", c.Status)
|
||||
foundExpired = true
|
||||
}
|
||||
}
|
||||
assert.True(t, foundExpired, "Should find expired certificate")
|
||||
}
|
||||
@@ -102,4 +102,58 @@ func TestLogService(t *testing.T) {
|
||||
// Test GetLogPath non-existent
|
||||
_, err = service.GetLogPath("missing.log")
|
||||
assert.Error(t, err)
|
||||
|
||||
// Test ListLogs - Directory Not Exist
|
||||
nonExistService := NewLogService(&config.Config{DatabasePath: filepath.Join(t.TempDir(), "missing", "cpm.db")})
|
||||
logs, err = nonExistService.ListLogs()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, logs)
|
||||
|
||||
// Test QueryLogs - Non-JSON Logs
|
||||
plainContent := "2023/10/27 10:00:00 Application started\nJust a plain line\n"
|
||||
err = os.WriteFile(filepath.Join(logsDir, "app.log"), []byte(plainContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, total, err = service.QueryLogs("app.log", models.LogFilter{Limit: 10})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), total)
|
||||
// Reverse order check
|
||||
assert.Equal(t, "Just a plain line", results[0].Msg)
|
||||
assert.Equal(t, "Application started", results[1].Msg)
|
||||
assert.Equal(t, "INFO", results[1].Level)
|
||||
|
||||
// Test QueryLogs - Pagination
|
||||
// We have 2 logs in access.log
|
||||
results, total, err = service.QueryLogs("access.log", models.LogFilter{Limit: 1, Offset: 0})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, 500, results[0].Status) // Newest first
|
||||
|
||||
results, total, err = service.QueryLogs("access.log", models.LogFilter{Limit: 1, Offset: 1})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
assert.Equal(t, 200, results[0].Status) // Second newest
|
||||
|
||||
results, total, err = service.QueryLogs("access.log", models.LogFilter{Limit: 10, Offset: 5})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Test QueryLogs - Exact Status Match
|
||||
results, total, err = service.QueryLogs("access.log", models.LogFilter{Status: "200", Limit: 10})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, 200, results[0].Status)
|
||||
|
||||
// Test QueryLogs - Search Fields
|
||||
// Search Method
|
||||
results, total, err = service.QueryLogs("access.log", models.LogFilter{Search: "POST", Limit: 10})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, "POST", results[0].Request.Method)
|
||||
|
||||
// Search RemoteIP
|
||||
results, total, err = service.QueryLogs("access.log", models.LogFilter{Search: "5.6.7.8", Limit: 10})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), total)
|
||||
assert.Equal(t, "5.6.7.8", results[0].Request.RemoteIP)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupNotificationTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
db.AutoMigrate(&models.Notification{})
|
||||
return db
|
||||
}
|
||||
|
||||
func TestNotificationService_Create(t *testing.T) {
|
||||
db := setupNotificationTestDB(t)
|
||||
svc := NewNotificationService(db)
|
||||
|
||||
notif, err := svc.Create(models.NotificationTypeInfo, "Test", "Message")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Test", notif.Title)
|
||||
assert.Equal(t, "Message", notif.Message)
|
||||
assert.False(t, notif.Read)
|
||||
}
|
||||
|
||||
func TestNotificationService_List(t *testing.T) {
|
||||
db := setupNotificationTestDB(t)
|
||||
svc := NewNotificationService(db)
|
||||
|
||||
svc.Create(models.NotificationTypeInfo, "N1", "M1")
|
||||
svc.Create(models.NotificationTypeInfo, "N2", "M2")
|
||||
|
||||
list, err := svc.List(false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, list, 2)
|
||||
|
||||
// Mark one as read
|
||||
db.Model(&models.Notification{}).Where("title = ?", "N1").Update("read", true)
|
||||
|
||||
listUnread, err := svc.List(true)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, listUnread, 1)
|
||||
assert.Equal(t, "N2", listUnread[0].Title)
|
||||
}
|
||||
|
||||
func TestNotificationService_MarkAsRead(t *testing.T) {
|
||||
db := setupNotificationTestDB(t)
|
||||
svc := NewNotificationService(db)
|
||||
|
||||
notif, _ := svc.Create(models.NotificationTypeInfo, "N1", "M1")
|
||||
|
||||
err := svc.MarkAsRead(fmt.Sprintf("%s", notif.ID))
|
||||
require.NoError(t, err)
|
||||
|
||||
var updated models.Notification
|
||||
db.First(&updated, "id = ?", notif.ID)
|
||||
assert.True(t, updated.Read)
|
||||
}
|
||||
|
||||
func TestNotificationService_MarkAllAsRead(t *testing.T) {
|
||||
db := setupNotificationTestDB(t)
|
||||
svc := NewNotificationService(db)
|
||||
|
||||
svc.Create(models.NotificationTypeInfo, "N1", "M1")
|
||||
svc.Create(models.NotificationTypeInfo, "N2", "M2")
|
||||
|
||||
err := svc.MarkAllAsRead()
|
||||
require.NoError(t, err)
|
||||
|
||||
var count int64
|
||||
db.Model(&models.Notification{}).Where("read = ?", false).Count(&count)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
@@ -11,9 +12,10 @@ import (
|
||||
)
|
||||
|
||||
func setupProxyHostTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}))
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}))
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -29,16 +31,110 @@ func TestProxyHostService_ValidateUniqueDomain(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, db.Create(existing).Error)
|
||||
|
||||
// Test 1: Duplicate domain
|
||||
err := service.ValidateUniqueDomain("example.com", 0)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "domain already exists", err.Error())
|
||||
tests := []struct {
|
||||
name string
|
||||
domainNames string
|
||||
excludeID uint
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "New unique domain",
|
||||
domainNames: "new.example.com",
|
||||
excludeID: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Duplicate domain",
|
||||
domainNames: "example.com",
|
||||
excludeID: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Same domain but excluded ID (update self)",
|
||||
domainNames: "example.com",
|
||||
excludeID: existing.ID,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Test 2: New domain
|
||||
err = service.ValidateUniqueDomain("new.com", 0)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test 3: Update existing (exclude self)
|
||||
err = service.ValidateUniqueDomain("example.com", existing.ID)
|
||||
assert.NoError(t, err)
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := service.ValidateUniqueDomain(tt.domainNames, tt.excludeID)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyHostService_CRUD(t *testing.T) {
|
||||
db := setupProxyHostTestDB(t)
|
||||
service := NewProxyHostService(db)
|
||||
|
||||
// Create
|
||||
host := &models.ProxyHost{
|
||||
UUID: "uuid-1",
|
||||
DomainNames: "test.example.com",
|
||||
ForwardHost: "127.0.0.1",
|
||||
ForwardPort: 8080,
|
||||
}
|
||||
err := service.Create(host)
|
||||
assert.NoError(t, err)
|
||||
assert.NotZero(t, host.ID)
|
||||
|
||||
// Create Duplicate
|
||||
dup := &models.ProxyHost{
|
||||
UUID: "uuid-2",
|
||||
DomainNames: "test.example.com",
|
||||
ForwardHost: "127.0.0.1",
|
||||
ForwardPort: 8081,
|
||||
}
|
||||
err = service.Create(dup)
|
||||
assert.Error(t, err)
|
||||
|
||||
// GetByID
|
||||
fetched, err := service.GetByID(host.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, host.DomainNames, fetched.DomainNames)
|
||||
|
||||
// GetByUUID
|
||||
fetchedUUID, err := service.GetByUUID(host.UUID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, host.ID, fetchedUUID.ID)
|
||||
|
||||
// Update
|
||||
host.ForwardPort = 9090
|
||||
err = service.Update(host)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fetched, err = service.GetByID(host.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 9090, fetched.ForwardPort)
|
||||
|
||||
// Update Duplicate
|
||||
host2 := &models.ProxyHost{
|
||||
UUID: "uuid-3",
|
||||
DomainNames: "other.example.com",
|
||||
ForwardHost: "127.0.0.1",
|
||||
ForwardPort: 8080,
|
||||
}
|
||||
service.Create(host2)
|
||||
|
||||
host.DomainNames = "other.example.com" // Conflict with host2
|
||||
err = service.Update(host)
|
||||
assert.Error(t, err)
|
||||
|
||||
// List
|
||||
hosts, err := service.List()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, hosts, 2)
|
||||
|
||||
// Delete
|
||||
err = service.Delete(host.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = service.GetByID(host.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/driver/sqlite"
|
||||
@@ -11,9 +12,11 @@ import (
|
||||
)
|
||||
|
||||
func setupRemoteServerTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared&mode=memory"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.RemoteServer{}))
|
||||
// Clear table
|
||||
db.Exec("DELETE FROM remote_servers")
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -47,3 +50,53 @@ func TestRemoteServerService_ValidateUniqueServer(t *testing.T) {
|
||||
err = service.ValidateUniqueServer("Existing Server", "192.168.1.100", 8080, existing.ID)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRemoteServerService_CRUD(t *testing.T) {
|
||||
db := setupRemoteServerTestDB(t)
|
||||
service := NewRemoteServerService(db)
|
||||
|
||||
// Create
|
||||
rs := &models.RemoteServer{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Server",
|
||||
Host: "192.168.1.100",
|
||||
Port: 22,
|
||||
Provider: "manual",
|
||||
}
|
||||
err := service.Create(rs)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, rs.ID)
|
||||
assert.NotEmpty(t, rs.UUID)
|
||||
|
||||
// GetByID
|
||||
fetched, err := service.GetByID(rs.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rs.Name, fetched.Name)
|
||||
|
||||
// GetByUUID
|
||||
fetchedUUID, err := service.GetByUUID(rs.UUID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rs.ID, fetchedUUID.ID)
|
||||
|
||||
// Update
|
||||
rs.Name = "Updated Server"
|
||||
err = service.Update(rs)
|
||||
require.NoError(t, err)
|
||||
|
||||
fetchedUpdated, err := service.GetByID(rs.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Updated Server", fetchedUpdated.Name)
|
||||
|
||||
// List
|
||||
list, err := service.List(false)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, list, 1)
|
||||
|
||||
// Delete
|
||||
err = service.Delete(rs.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify Delete
|
||||
_, err = service.GetByID(rs.ID)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +14,7 @@ type UpdateService struct {
|
||||
repoName string
|
||||
lastCheck time.Time
|
||||
cachedResult *UpdateInfo
|
||||
apiURL string // For testing
|
||||
}
|
||||
|
||||
type UpdateInfo struct {
|
||||
@@ -33,19 +33,35 @@ func NewUpdateService() *UpdateService {
|
||||
currentVersion: version.Version,
|
||||
repoOwner: "Wikid82",
|
||||
repoName: "CaddyProxyManagerPlus",
|
||||
apiURL: "https://api.github.com/repos/Wikid82/CaddyProxyManagerPlus/releases/latest",
|
||||
}
|
||||
}
|
||||
|
||||
// SetAPIURL sets the GitHub API URL for testing.
|
||||
func (s *UpdateService) SetAPIURL(url string) {
|
||||
s.apiURL = url
|
||||
}
|
||||
|
||||
// SetCurrentVersion sets the current version for testing.
|
||||
func (s *UpdateService) SetCurrentVersion(v string) {
|
||||
s.currentVersion = v
|
||||
}
|
||||
|
||||
// ClearCache clears the update cache for testing.
|
||||
func (s *UpdateService) ClearCache() {
|
||||
s.cachedResult = nil
|
||||
s.lastCheck = time.Time{}
|
||||
}
|
||||
|
||||
func (s *UpdateService) CheckForUpdates() (*UpdateInfo, error) {
|
||||
// Cache for 1 hour
|
||||
if s.cachedResult != nil && time.Since(s.lastCheck) < 1*time.Hour {
|
||||
return s.cachedResult, nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", s.repoOwner, s.repoName)
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
req, err := http.NewRequest("GET", s.apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUpdateService_CheckForUpdates(t *testing.T) {
|
||||
// Mock GitHub API
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/releases/latest" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
release := githubRelease{
|
||||
TagName: "v1.0.0",
|
||||
HTMLURL: "https://github.com/Wikid82/CaddyProxyManagerPlus/releases/tag/v1.0.0",
|
||||
}
|
||||
json.NewEncoder(w).Encode(release)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
us := NewUpdateService()
|
||||
us.SetAPIURL(server.URL + "/releases/latest")
|
||||
// us.currentVersion is private, so we can't set it directly in test unless we export it or add a setter.
|
||||
// However, NewUpdateService sets it from version.Version.
|
||||
// We can temporarily change version.Version if it's a var, but it's likely a const or var in another package.
|
||||
// Let's check version package.
|
||||
// Assuming version.Version is a var we can change, or we add a SetCurrentVersion method for testing.
|
||||
// For now, let's assume we can't change it easily without a setter.
|
||||
// Let's add SetCurrentVersion to UpdateService for testing purposes.
|
||||
us.SetCurrentVersion("0.9.0")
|
||||
|
||||
// Test Update Available
|
||||
info, err := us.CheckForUpdates()
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, info.Available)
|
||||
assert.Equal(t, "v1.0.0", info.LatestVersion)
|
||||
assert.Equal(t, "https://github.com/Wikid82/CaddyProxyManagerPlus/releases/tag/v1.0.0", info.ChangelogURL)
|
||||
|
||||
// Test No Update Available
|
||||
us.SetCurrentVersion("1.0.0")
|
||||
// us.cachedResult = nil // cachedResult is private
|
||||
// us.lastCheck = time.Time{} // lastCheck is private
|
||||
us.ClearCache() // Add this method
|
||||
|
||||
info, err = us.CheckForUpdates()
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, info.Available)
|
||||
assert.Equal(t, "v1.0.0", info.LatestVersion)
|
||||
|
||||
// Test Cache
|
||||
// If we call again immediately, it should use cache.
|
||||
// We can verify this by closing the server or changing the response, but cache logic is simple.
|
||||
// Let's change the server handler? No, httptest server handler is fixed.
|
||||
// But we can check if it returns the same object.
|
||||
info2, err := us.CheckForUpdates()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, info, info2)
|
||||
|
||||
// Test Error (Server Down)
|
||||
server.Close()
|
||||
us.cachedResult = nil
|
||||
us.lastCheck = time.Time{}
|
||||
|
||||
// Depending on implementation, it might return error or just available=false
|
||||
// Implementation:
|
||||
// resp, err := client.Do(req) -> returns error if connection refused
|
||||
// if err != nil { return nil, err }
|
||||
_, err = us.CheckForUpdates()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupUptimeTestDB(t *testing.T) *gorm.DB {
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
err = db.AutoMigrate(&models.Notification{}, &models.Setting{}, &models.ProxyHost{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to migrate database: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestUptimeService_CheckHost(t *testing.T) {
|
||||
db := setupUptimeTestDB(t)
|
||||
ns := NewNotificationService(db)
|
||||
us := NewUptimeService(db, ns)
|
||||
|
||||
// Test Case 1: Host is UP
|
||||
// Start a listener on a random port
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start listener: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
port := addr.Port
|
||||
|
||||
// Run check in a goroutine to accept connection if needed, but DialTimeout just needs handshake
|
||||
// Actually DialTimeout will succeed if listener is accepting.
|
||||
// We need to accept in a loop or just let it hang?
|
||||
// net.Dial will succeed as soon as handshake is done.
|
||||
// But we should probably accept to be clean.
|
||||
go func() {
|
||||
conn, err := listener.Accept()
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
up := us.CheckHost("127.0.0.1", port)
|
||||
assert.True(t, up, "Host should be UP")
|
||||
|
||||
// Test Case 2: Host is DOWN
|
||||
// Use a port that is unlikely to be in use.
|
||||
// Or just close the listener and try again on same port (might be TIME_WAIT issues though)
|
||||
// Better to pick a random high port that nothing is listening on.
|
||||
// But finding a free port is tricky.
|
||||
// Let's just use a port we know is closed.
|
||||
// Or use the same port after closing listener.
|
||||
listener.Close()
|
||||
// Give it a moment
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
down := us.CheckHost("127.0.0.1", port)
|
||||
assert.False(t, down, "Host should be DOWN")
|
||||
}
|
||||
|
||||
func TestUptimeService_CheckAllHosts(t *testing.T) {
|
||||
db := setupUptimeTestDB(t)
|
||||
ns := NewNotificationService(db)
|
||||
us := NewUptimeService(db, ns)
|
||||
|
||||
// Create a dummy listener for a "UP" host
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start listener: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Seed ProxyHosts
|
||||
upHost := models.ProxyHost{
|
||||
UUID: "uuid-1",
|
||||
DomainNames: "up.example.com",
|
||||
ForwardHost: "127.0.0.1",
|
||||
ForwardPort: addr.Port,
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(&upHost)
|
||||
|
||||
downHost := models.ProxyHost{
|
||||
UUID: "uuid-2",
|
||||
DomainNames: "down.example.com",
|
||||
ForwardHost: "127.0.0.1",
|
||||
ForwardPort: 54321, // Assuming this is closed
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(&downHost)
|
||||
|
||||
disabledHost := models.ProxyHost{
|
||||
UUID: "uuid-3",
|
||||
DomainNames: "disabled.example.com",
|
||||
ForwardHost: "127.0.0.1",
|
||||
ForwardPort: 54322,
|
||||
Enabled: false,
|
||||
}
|
||||
// Force Enabled=false by using map or Select
|
||||
db.Create(&disabledHost)
|
||||
db.Model(&disabledHost).Update("Enabled", false)
|
||||
|
||||
// Run CheckAllHosts
|
||||
us.CheckAllHosts()
|
||||
|
||||
// Verify Notifications
|
||||
var notifications []models.Notification
|
||||
db.Find(¬ifications)
|
||||
|
||||
for _, n := range notifications {
|
||||
t.Logf("Notification: %s - %s", n.Title, n.Message)
|
||||
}
|
||||
|
||||
// We expect 1 notification for the downHost.
|
||||
// upHost is UP -> no notification
|
||||
// disabledHost is DISABLED -> no check -> no notification
|
||||
assert.Equal(t, 1, len(notifications), "Should have 1 notification")
|
||||
if len(notifications) > 0 {
|
||||
assert.Contains(t, notifications[0].Message, "down.example.com", "Notification should mention the down host")
|
||||
assert.Equal(t, models.NotificationTypeError, notifications[0].Type)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user