diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ffa1e6d7..70d9b7ef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,6 +31,7 @@ repos: language: script files: '\.go$' pass_filenames: false + verbose: true - id: go-vet name: Go Vet entry: bash -c 'cd backend && go vet ./...' diff --git a/backend/internal/api/handlers/domain_handler_test.go b/backend/internal/api/handlers/domain_handler_test.go new file mode 100644 index 00000000..cacdd8d4 --- /dev/null +++ b/backend/internal/api/handlers/domain_handler_test.go @@ -0,0 +1,97 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" +) + +func setupDomainTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) { + t.Helper() + + 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.Domain{})) + + h := NewDomainHandler(db) + r := gin.New() + + // Manually register routes since DomainHandler doesn't have a RegisterRoutes method yet + // or we can just register them here for testing + r.GET("/api/v1/domains", h.List) + r.POST("/api/v1/domains", h.Create) + r.DELETE("/api/v1/domains/:id", h.Delete) + + return r, db +} + +func TestDomainLifecycle(t *testing.T) { + router, _ := setupDomainTestRouter(t) + + // 1. Create Domain + body := `{"name":"example.com"}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/domains", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + require.Equal(t, http.StatusCreated, resp.Code) + + var created models.Domain + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &created)) + require.Equal(t, "example.com", created.Name) + require.NotEmpty(t, created.UUID) + + // 2. List Domains + req = httptest.NewRequest(http.MethodGet, "/api/v1/domains", nil) + resp = httptest.NewRecorder() + router.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + + var list []models.Domain + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &list)) + require.Len(t, list, 1) + require.Equal(t, "example.com", list[0].Name) + + // 3. Delete Domain + req = httptest.NewRequest(http.MethodDelete, "/api/v1/domains/"+created.UUID, nil) + resp = httptest.NewRecorder() + router.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + + // 4. Verify Deletion + req = httptest.NewRequest(http.MethodGet, "/api/v1/domains", nil) + resp = httptest.NewRecorder() + router.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) + + require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &list)) + require.Len(t, list, 0) +} + +func TestDomainErrors(t *testing.T) { + router, _ := setupDomainTestRouter(t) + + // 1. Create Invalid JSON + req := httptest.NewRequest(http.MethodPost, "/api/v1/domains", strings.NewReader(`{invalid}`)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + require.Equal(t, http.StatusBadRequest, resp.Code) + + // 2. Create Missing Name + req = httptest.NewRequest(http.MethodPost, "/api/v1/domains", strings.NewReader(`{}`)) + req.Header.Set("Content-Type", "application/json") + resp = httptest.NewRecorder() + router.ServeHTTP(resp, req) + require.Equal(t, http.StatusBadRequest, resp.Code) +} diff --git a/backend/internal/api/handlers/proxy_host_handler.go b/backend/internal/api/handlers/proxy_host_handler.go index eb8bbf38..54349b1e 100644 --- a/backend/internal/api/handlers/proxy_host_handler.go +++ b/backend/internal/api/handlers/proxy_host_handler.go @@ -30,6 +30,7 @@ func (h *ProxyHostHandler) RegisterRoutes(router *gin.RouterGroup) { router.GET("/proxy-hosts/:uuid", h.Get) router.PUT("/proxy-hosts/:uuid", h.Update) router.DELETE("/proxy-hosts/:uuid", h.Delete) + router.POST("/proxy-hosts/test", h.TestConnection) } // List retrieves all proxy hosts. @@ -119,3 +120,23 @@ func (h *ProxyHostHandler) Delete(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "proxy host deleted"}) } + +// TestConnection checks if the proxy host is reachable. +func (h *ProxyHostHandler) TestConnection(c *gin.Context) { + var req struct { + ForwardHost string `json:"forward_host" binding:"required"` + ForwardPort int `json:"forward_port" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.service.TestConnection(req.ForwardHost, req.ForwardPort); err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Connection successful"}) +} diff --git a/backend/internal/api/handlers/proxy_host_handler_test.go b/backend/internal/api/handlers/proxy_host_handler_test.go index 76fe3f8d..25228db2 100644 --- a/backend/internal/api/handlers/proxy_host_handler_test.go +++ b/backend/internal/api/handlers/proxy_host_handler_test.go @@ -2,6 +2,8 @@ package handlers import ( "encoding/json" + "fmt" + "net" "net/http" "net/http/httptest" "strings" @@ -139,3 +141,40 @@ func TestProxyHostValidation(t *testing.T) { router.ServeHTTP(resp, req) require.Equal(t, http.StatusBadRequest, resp.Code) } + +func TestProxyHostConnection(t *testing.T) { + router, _ := setupTestRouter(t) + + // 1. Test Invalid Input (Missing Host) + body := `{"forward_port": 80}` + req := httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts/test", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + require.Equal(t, http.StatusBadRequest, resp.Code) + + // 2. Test Connection Failure (Unreachable Port) + // Use a reserved port or localhost port that is likely closed + body = `{"forward_host": "localhost", "forward_port": 54321}` + req = httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts/test", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + resp = httptest.NewRecorder() + router.ServeHTTP(resp, req) + // It should return 502 Bad Gateway + require.Equal(t, http.StatusBadGateway, resp.Code) + + // 3. Test Connection Success + // Start a local listener + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + + addr := l.Addr().(*net.TCPAddr) + + body = fmt.Sprintf(`{"forward_host": "%s", "forward_port": %d}`, addr.IP.String(), addr.Port) + req = httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts/test", strings.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + resp = httptest.NewRecorder() + router.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Code) +} diff --git a/backend/internal/api/handlers/settings_handler_test.go b/backend/internal/api/handlers/settings_handler_test.go index c4ef4278..33546089 100644 --- a/backend/internal/api/handlers/settings_handler_test.go +++ b/backend/internal/api/handlers/settings_handler_test.go @@ -91,3 +91,31 @@ func TestSettingsHandler_UpdateSettings(t *testing.T) { db.Where("key = ?", "new_key").First(&setting) assert.Equal(t, "updated_value", setting.Value) } + +func TestSettingsHandler_Errors(t *testing.T) { + gin.SetMode(gin.TestMode) + db := setupSettingsTestDB(t) + + handler := handlers.NewSettingsHandler(db) + router := gin.New() + router.POST("/settings", handler.UpdateSetting) + + // Invalid JSON + req, _ := http.NewRequest("POST", "/settings", bytes.NewBuffer([]byte("invalid"))) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) + + // Missing Key/Value + payload := map[string]string{ + "key": "some_key", + // value missing + } + body, _ := json.Marshal(payload) + req, _ = http.NewRequest("POST", "/settings", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + w = httptest.NewRecorder() + router.ServeHTTP(w, req) + assert.Equal(t, http.StatusBadRequest, w.Code) +} diff --git a/backend/internal/api/handlers/user_handler_test.go b/backend/internal/api/handlers/user_handler_test.go index dc7589a6..d2532ef7 100644 --- a/backend/internal/api/handlers/user_handler_test.go +++ b/backend/internal/api/handlers/user_handler_test.go @@ -9,6 +9,7 @@ import ( "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" @@ -232,3 +233,120 @@ func TestUserHandler_Errors(t *testing.T) { // If table missing, Update should fail assert.Equal(t, http.StatusInternalServerError, w.Code) } + +func TestUserHandler_UpdateProfile(t *testing.T) { + handler, db := setupUserHandler(t) + + // Create user + user := &models.User{ + UUID: uuid.NewString(), + Email: "test@example.com", + Name: "Test User", + APIKey: uuid.NewString(), + } + user.SetPassword("password123") + db.Create(user) + + gin.SetMode(gin.TestMode) + r := gin.New() + r.Use(func(c *gin.Context) { + c.Set("userID", user.ID) + c.Next() + }) + r.PUT("/profile", handler.UpdateProfile) + + // 1. Success - Name only + t.Run("Success Name Only", func(t *testing.T) { + body := map[string]string{ + "name": "Updated Name", + "email": "test@example.com", + } + jsonBody, _ := json.Marshal(body) + req := httptest.NewRequest("PUT", "/profile", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var updatedUser models.User + db.First(&updatedUser, user.ID) + assert.Equal(t, "Updated Name", updatedUser.Name) + }) + + // 2. Success - Email change with password + t.Run("Success Email Change", func(t *testing.T) { + body := map[string]string{ + "name": "Updated Name", + "email": "newemail@example.com", + "current_password": "password123", + } + jsonBody, _ := json.Marshal(body) + req := httptest.NewRequest("PUT", "/profile", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var updatedUser models.User + db.First(&updatedUser, user.ID) + assert.Equal(t, "newemail@example.com", updatedUser.Email) + }) + + // 3. Fail - Email change without password + t.Run("Fail Email Change No Password", func(t *testing.T) { + // Reset email + db.Model(user).Update("email", "test@example.com") + + body := map[string]string{ + "name": "Updated Name", + "email": "another@example.com", + } + jsonBody, _ := json.Marshal(body) + req := httptest.NewRequest("PUT", "/profile", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + }) + + // 4. Fail - Email change wrong password + t.Run("Fail Email Change Wrong Password", func(t *testing.T) { + body := map[string]string{ + "name": "Updated Name", + "email": "another@example.com", + "current_password": "wrongpassword", + } + jsonBody, _ := json.Marshal(body) + req := httptest.NewRequest("PUT", "/profile", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + }) + + // 5. Fail - Email already in use + t.Run("Fail Email In Use", func(t *testing.T) { + // Create another user + otherUser := &models.User{ + UUID: uuid.NewString(), + Email: "other@example.com", + Name: "Other User", + APIKey: uuid.NewString(), + } + db.Create(otherUser) + + body := map[string]string{ + "name": "Updated Name", + "email": "other@example.com", + } + jsonBody, _ := json.Marshal(body) + req := httptest.NewRequest("PUT", "/profile", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusConflict, w.Code) + }) +} diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index 4021131a..79366daa 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -47,3 +47,29 @@ func TestLoad_Defaults(t *testing.T) { assert.Equal(t, "development", cfg.Environment) assert.Equal(t, "8080", cfg.HTTPPort) } + +func TestLoad_Error(t *testing.T) { + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "file") + f, err := os.Create(filePath) + require.NoError(t, err) + f.Close() + + // Case 1: CaddyConfigDir is a file + os.Setenv("CPM_CADDY_CONFIG_DIR", filePath) + // Set other paths to valid locations to isolate the error + os.Setenv("CPM_DB_PATH", filepath.Join(tempDir, "db", "test.db")) + os.Setenv("CPM_IMPORT_DIR", filepath.Join(tempDir, "imports")) + + _, err = Load() + assert.Error(t, err) + assert.Contains(t, err.Error(), "ensure caddy config directory") + + // Case 2: ImportDir is a file + os.Setenv("CPM_CADDY_CONFIG_DIR", filepath.Join(tempDir, "caddy")) + os.Setenv("CPM_IMPORT_DIR", filePath) + + _, err = Load() + assert.Error(t, err) + assert.Contains(t, err.Error(), "ensure import directory") +} diff --git a/backend/internal/database/database_test.go b/backend/internal/database/database_test.go index 67323c74..0b152f6f 100644 --- a/backend/internal/database/database_test.go +++ b/backend/internal/database/database_test.go @@ -1,22 +1,29 @@ package database import ( -"path/filepath" -"testing" + "path/filepath" + "testing" -"github.com/stretchr/testify/assert" + "github.com/stretchr/testify/assert" ) func TestConnect(t *testing.T) { -// Test with memory DB -db, err := Connect("file::memory:?cache=shared") -assert.NoError(t, err) -assert.NotNil(t, db) + // Test with memory DB + db, err := Connect("file::memory:?cache=shared") + assert.NoError(t, err) + assert.NotNil(t, db) -// Test with file DB -tempDir := t.TempDir() -dbPath := filepath.Join(tempDir, "test.db") -db, err = Connect(dbPath) -assert.NoError(t, err) -assert.NotNil(t, db) + // Test with file DB + tempDir := t.TempDir() + dbPath := filepath.Join(tempDir, "test.db") + db, err = Connect(dbPath) + assert.NoError(t, err) + assert.NotNil(t, db) +} + +func TestConnect_Error(t *testing.T) { + // Test with invalid path (directory) + tempDir := t.TempDir() + _, err := Connect(tempDir) + assert.Error(t, err) } diff --git a/backend/internal/models/domain_test.go b/backend/internal/models/domain_test.go new file mode 100644 index 00000000..03f6eae8 --- /dev/null +++ b/backend/internal/models/domain_test.go @@ -0,0 +1,28 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestDomain_BeforeCreate(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + assert.NoError(t, err) + db.AutoMigrate(&Domain{}) + + // Case 1: UUID is empty, should be generated + d1 := &Domain{Name: "example.com"} + err = db.Create(d1).Error + assert.NoError(t, err) + assert.NotEmpty(t, d1.UUID) + + // Case 2: UUID is provided, should be kept + uuid := "123e4567-e89b-12d3-a456-426614174000" + d2 := &Domain{Name: "test.com", UUID: uuid} + err = db.Create(d2).Error + assert.NoError(t, err) + assert.Equal(t, uuid, d2.UUID) +} diff --git a/backend/internal/models/notification_test.go b/backend/internal/models/notification_test.go new file mode 100644 index 00000000..a280a5f4 --- /dev/null +++ b/backend/internal/models/notification_test.go @@ -0,0 +1,28 @@ +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestNotification_BeforeCreate(t *testing.T) { + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) + assert.NoError(t, err) + db.AutoMigrate(&Notification{}) + + // Case 1: ID is empty, should be generated + n1 := &Notification{Title: "Test", Message: "Test Message"} + err = db.Create(n1).Error + assert.NoError(t, err) + assert.NotEmpty(t, n1.ID) + + // Case 2: ID is provided, should be kept + id := "123e4567-e89b-12d3-a456-426614174000" + n2 := &Notification{ID: id, Title: "Test 2", Message: "Test Message 2"} + err = db.Create(n2).Error + assert.NoError(t, err) + assert.Equal(t, id, n2.ID) +} diff --git a/backend/internal/services/proxyhost_service.go b/backend/internal/services/proxyhost_service.go index f2328f73..04bc1069 100644 --- a/backend/internal/services/proxyhost_service.go +++ b/backend/internal/services/proxyhost_service.go @@ -3,6 +3,9 @@ package services import ( "errors" "fmt" + "net" + "strconv" + "time" "gorm.io/gorm" @@ -88,3 +91,19 @@ func (s *ProxyHostService) List() ([]models.ProxyHost, error) { } return hosts, nil } + +// TestConnection attempts to connect to the target host and port. +func (s *ProxyHostService) TestConnection(host string, port int) error { + if host == "" || port <= 0 { + return errors.New("invalid host or port") + } + + target := net.JoinHostPort(host, strconv.Itoa(port)) + conn, err := net.DialTimeout("tcp", target, 3*time.Second) + if err != nil { + return fmt.Errorf("connection failed: %w", err) + } + defer conn.Close() + + return nil +} diff --git a/backend/internal/services/proxyhost_service_test.go b/backend/internal/services/proxyhost_service_test.go index a9868b09..f325c7df 100644 --- a/backend/internal/services/proxyhost_service_test.go +++ b/backend/internal/services/proxyhost_service_test.go @@ -2,6 +2,7 @@ package services import ( "fmt" + "net" "testing" "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" @@ -138,3 +139,31 @@ func TestProxyHostService_CRUD(t *testing.T) { _, err = service.GetByID(host.ID) assert.Error(t, err) } + +func TestProxyHostService_TestConnection(t *testing.T) { + db := setupProxyHostTestDB(t) + service := NewProxyHostService(db) + + // 1. Invalid Input + err := service.TestConnection("", 80) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid host or port") + + err = service.TestConnection("example.com", 0) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid host or port") + + // 2. Connection Failure (Unreachable) + err = service.TestConnection("localhost", 54321) + assert.Error(t, err) + + // 3. Connection Success + // Start a local listener + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + addr := l.Addr().(*net.TCPAddr) + + err = service.TestConnection(addr.IP.String(), addr.Port) + assert.NoError(t, err) +} diff --git a/frontend/src/api/proxyHosts.ts b/frontend/src/api/proxyHosts.ts index 9b4fb65d..40e07d9c 100644 --- a/frontend/src/api/proxyHosts.ts +++ b/frontend/src/api/proxyHosts.ts @@ -50,3 +50,7 @@ export const updateProxyHost = async (uuid: string, host: Partial): P export const deleteProxyHost = async (uuid: string): Promise => { await client.delete(`/proxy-hosts/${uuid}`); }; + +export const testProxyHostConnection = async (host: string, port: number): Promise => { + await client.post('/proxy-hosts/test', { forward_host: host, forward_port: port }); +}; diff --git a/frontend/src/components/ProxyHostForm.tsx b/frontend/src/components/ProxyHostForm.tsx index 6eaa1aff..8fb11377 100644 --- a/frontend/src/components/ProxyHostForm.tsx +++ b/frontend/src/components/ProxyHostForm.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react' -import { CircleHelp, AlertCircle } from 'lucide-react' +import { CircleHelp, AlertCircle, Check, X, Loader2 } from 'lucide-react' import type { ProxyHost } from '../api/proxyHosts' +import { testProxyHostConnection } from '../api/proxyHosts' import { useRemoteServers } from '../hooks/useRemoteServers' import { useDomains } from '../hooks/useDomains' import { useDocker } from '../hooks/useDocker' @@ -39,6 +40,9 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor const [pendingDomain, setPendingDomain] = useState('') const [dontAskAgain, setDontAskAgain] = useState(false) + // Test Connection State + const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle') + useEffect(() => { const stored = localStorage.getItem('cpmp_dont_ask_domain') if (stored === 'true') { @@ -88,6 +92,23 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor localStorage.setItem('cpmp_dont_ask_domain', String(checked)) } + const handleTestConnection = async () => { + if (!formData.forward_host || !formData.forward_port) return + + setTestStatus('testing') + try { + await testProxyHostConnection(formData.forward_host, formData.forward_port) + setTestStatus('success') + // Reset status after 3 seconds + setTimeout(() => setTestStatus('idle'), 3000) + } catch (err) { + console.error("Test connection failed", err) + setTestStatus('error') + // Reset status after 3 seconds + setTimeout(() => setTestStatus('idle'), 3000) + } + } + // Fetch containers based on selected source // If 'local', host is undefined (which defaults to local socket in backend) // If remote UUID, we need to find the server and get its host address? @@ -422,12 +443,30 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor > Cancel + + + diff --git a/scripts/go-test-coverage.sh b/scripts/go-test-coverage.sh index 88a734e4..962037db 100755 --- a/scripts/go-test-coverage.sh +++ b/scripts/go-test-coverage.sh @@ -4,7 +4,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BACKEND_DIR="$ROOT_DIR/backend" COVERAGE_FILE="$BACKEND_DIR/coverage.txt" -MIN_COVERAGE="${CPM_MIN_COVERAGE:-75}" +MIN_COVERAGE="${CPM_MIN_COVERAGE:-80}" # trap 'rm -f "$COVERAGE_FILE"' EXIT