diff --git a/backend/internal/api/handlers/import_handler_test.go b/backend/internal/api/handlers/import_handler_test.go index 73e57591..1d0c755c 100644 --- a/backend/internal/api/handlers/import_handler_test.go +++ b/backend/internal/api/handlers/import_handler_test.go @@ -897,3 +897,66 @@ func TestImportHandler_UploadMulti(t *testing.T) { assert.Contains(t, resp["error"], "empty") }) } + +// Additional tests for comprehensive coverage + +func TestImportHandler_Cancel_MissingSessionUUID(t *testing.T) { + gin.SetMode(gin.TestMode) + db := setupImportTestDB(t) + handler := handlers.NewImportHandler(db, "echo", "/tmp", "") + router := gin.New() + router.DELETE("/import/cancel", handler.Cancel) + + // Missing session_uuid parameter + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/import/cancel", http.NoBody) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + var resp map[string]any + _ = json.Unmarshal(w.Body.Bytes(), &resp) + assert.Equal(t, "session_uuid required", resp["error"]) +} + +func TestImportHandler_Cancel_InvalidSessionUUID(t *testing.T) { + gin.SetMode(gin.TestMode) + db := setupImportTestDB(t) + handler := handlers.NewImportHandler(db, "echo", "/tmp", "") + router := gin.New() + router.DELETE("/import/cancel", handler.Cancel) + + // Test "." which becomes empty after filepath.Base processing + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/import/cancel?session_uuid=.", http.NoBody) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + var resp map[string]any + _ = json.Unmarshal(w.Body.Bytes(), &resp) + assert.Equal(t, "invalid session_uuid", resp["error"]) +} + +func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) { + gin.SetMode(gin.TestMode) + db := setupImportTestDB(t) + handler := handlers.NewImportHandler(db, "echo", "/tmp", "") + router := gin.New() + router.POST("/import/commit", handler.Commit) + + // Test "." which becomes empty after filepath.Base processing + payload := map[string]any{ + "session_uuid": ".", + "resolutions": map[string]string{}, + } + body, _ := json.Marshal(payload) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/import/commit", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + var resp map[string]any + _ = json.Unmarshal(w.Body.Bytes(), &resp) + assert.Equal(t, "invalid session_uuid", resp["error"]) +} diff --git a/backend/internal/api/handlers/manual_challenge_handler_test.go b/backend/internal/api/handlers/manual_challenge_handler_test.go index 5eea9410..d03503f1 100644 --- a/backend/internal/api/handlers/manual_challenge_handler_test.go +++ b/backend/internal/api/handlers/manual_challenge_handler_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "testing" @@ -559,3 +560,1017 @@ func TestNewErrorResponse(t *testing.T) { assert.Equal(t, "Test message", resp.Error.Message) assert.Equal(t, details, resp.Error.Details) } + +// Additional tests for comprehensive coverage + +func TestManualChallengeHandler_GetChallenge_EmptyChallengeID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "1"}, {Key: "challengeId", Value: ""}} + c.Request = httptest.NewRequest("GET", "/dns-providers/1/manual-challenge/", nil) + setUserID(c, 1) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + + handler.GetChallenge(c) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_CHALLENGE_ID") +} + +func TestManualChallengeHandler_GetChallenge_ProviderInternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + // Return an internal error (not ErrDNSProviderNotFound) + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, errors.New("database error")) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test-id", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_GetChallenge_Unauthorized(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test-id", uint(1)).Return(nil, services.ErrUnauthorized) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test-id", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "UNAUTHORIZED") +} + +func TestManualChallengeHandler_GetChallenge_InternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test-id", uint(1)).Return(nil, errors.New("db error")) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test-id", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_GetChallenge_ProviderMismatch(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.GetChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + // Challenge belongs to a different provider + challenge := &models.ManualChallenge{ + ID: "test-id", + ProviderID: 999, // Different provider + UserID: 1, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(10 * time.Minute), + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test-id", uint(1)).Return(challenge, nil) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test-id", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "CHALLENGE_NOT_FOUND") +} + +func TestManualChallengeHandler_VerifyChallenge_InvalidProviderID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + req, _ := http.NewRequest("POST", "/dns-providers/invalid/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_ID") +} + +func TestManualChallengeHandler_VerifyChallenge_EmptyChallengeID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "1"}, {Key: "challengeId", Value: ""}} + c.Request = httptest.NewRequest("POST", "/dns-providers/1/manual-challenge//verify", nil) + setUserID(c, 1) + + handler.VerifyChallenge(c) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_CHALLENGE_ID") +} + +func TestManualChallengeHandler_VerifyChallenge_ProviderNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, services.ErrDNSProviderNotFound) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "PROVIDER_NOT_FOUND") +} + +func TestManualChallengeHandler_VerifyChallenge_ProviderInternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, errors.New("db error")) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_VerifyChallenge_InvalidProviderType(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "cloudflare"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_TYPE") +} + +func TestManualChallengeHandler_VerifyChallenge_ChallengeNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test", uint(1)).Return(nil, services.ErrChallengeNotFound) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "CHALLENGE_NOT_FOUND") +} + +func TestManualChallengeHandler_VerifyChallenge_Unauthorized(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test", uint(1)).Return(nil, services.ErrUnauthorized) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "UNAUTHORIZED") +} + +func TestManualChallengeHandler_VerifyChallenge_GetChallengeInternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test", uint(1)).Return(nil, errors.New("db error")) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_VerifyChallenge_ProviderMismatch(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + challenge := &models.ManualChallenge{ + ID: "test", + ProviderID: 999, // Different provider + UserID: 1, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(10 * time.Minute), + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test", uint(1)).Return(challenge, nil) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "CHALLENGE_NOT_FOUND") +} + +func TestManualChallengeHandler_VerifyChallenge_ChallengeExpired(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + challenge := &models.ManualChallenge{ + ID: "test", + ProviderID: 1, + UserID: 1, + CreatedAt: time.Now().Add(-20 * time.Minute), + ExpiresAt: time.Now().Add(-10 * time.Minute), // Expired + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test", uint(1)).Return(challenge, nil) + mockService.On("VerifyChallenge", mock.Anything, "test", uint(1)).Return(nil, services.ErrChallengeExpired) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusGone, w.Code) + assert.Contains(t, w.Body.String(), "CHALLENGE_EXPIRED") +} + +func TestManualChallengeHandler_VerifyChallenge_VerifyInternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenge/:challengeId/verify", func(c *gin.Context) { + setUserID(c, 1) + handler.VerifyChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + challenge := &models.ManualChallenge{ + ID: "test", + ProviderID: 1, + UserID: 1, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(10 * time.Minute), + } + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("GetChallengeForUser", mock.Anything, "test", uint(1)).Return(challenge, nil) + mockService.On("VerifyChallenge", mock.Anything, "test", uint(1)).Return(nil, errors.New("dns lookup failed")) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenge/test/verify", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_PollChallenge_InvalidProviderID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", func(c *gin.Context) { + setUserID(c, 1) + handler.PollChallenge(c) + }) + + req, _ := http.NewRequest("GET", "/dns-providers/invalid/manual-challenge/test/poll", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_ID") +} + +func TestManualChallengeHandler_PollChallenge_EmptyChallengeID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "1"}, {Key: "challengeId", Value: ""}} + c.Request = httptest.NewRequest("GET", "/dns-providers/1/manual-challenge//poll", nil) + setUserID(c, 1) + + handler.PollChallenge(c) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_CHALLENGE_ID") +} + +func TestManualChallengeHandler_PollChallenge_ProviderNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", func(c *gin.Context) { + setUserID(c, 1) + handler.PollChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, services.ErrDNSProviderNotFound) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test/poll", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "PROVIDER_NOT_FOUND") +} + +func TestManualChallengeHandler_PollChallenge_ProviderInternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", func(c *gin.Context) { + setUserID(c, 1) + handler.PollChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, errors.New("db error")) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test/poll", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_PollChallenge_InvalidProviderType(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", func(c *gin.Context) { + setUserID(c, 1) + handler.PollChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "cloudflare"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test/poll", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_TYPE") +} + +func TestManualChallengeHandler_PollChallenge_ChallengeNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", func(c *gin.Context) { + setUserID(c, 1) + handler.PollChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("PollChallengeStatus", mock.Anything, "test", uint(1)).Return(nil, services.ErrChallengeNotFound) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test/poll", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "CHALLENGE_NOT_FOUND") +} + +func TestManualChallengeHandler_PollChallenge_Unauthorized(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", func(c *gin.Context) { + setUserID(c, 1) + handler.PollChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("PollChallengeStatus", mock.Anything, "test", uint(1)).Return(nil, services.ErrUnauthorized) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test/poll", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "UNAUTHORIZED") +} + +func TestManualChallengeHandler_PollChallenge_InternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenge/:challengeId/poll", func(c *gin.Context) { + setUserID(c, 1) + handler.PollChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("PollChallengeStatus", mock.Anything, "test", uint(1)).Return(nil, errors.New("db error")) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenge/test/poll", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_ListChallenges_InvalidProviderID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.ListChallenges(c) + }) + + req, _ := http.NewRequest("GET", "/dns-providers/invalid/manual-challenges", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_ID") +} + +func TestManualChallengeHandler_ListChallenges_ProviderNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.ListChallenges(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, services.ErrDNSProviderNotFound) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenges", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "PROVIDER_NOT_FOUND") +} + +func TestManualChallengeHandler_ListChallenges_ProviderInternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.ListChallenges(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, errors.New("db error")) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenges", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_ListChallenges_InvalidProviderType(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.ListChallenges(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "cloudflare"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenges", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_TYPE") +} + +func TestManualChallengeHandler_ListChallenges_InternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.GET("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.ListChallenges(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("ListChallengesForProvider", mock.Anything, uint(1), uint(1)).Return(nil, errors.New("db error")) + + req, _ := http.NewRequest("GET", "/dns-providers/1/manual-challenges", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_DeleteChallenge_InvalidProviderID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.DELETE("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.DeleteChallenge(c) + }) + + req, _ := http.NewRequest("DELETE", "/dns-providers/invalid/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_ID") +} + +func TestManualChallengeHandler_DeleteChallenge_EmptyChallengeID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "1"}, {Key: "challengeId", Value: ""}} + c.Request = httptest.NewRequest("DELETE", "/dns-providers/1/manual-challenge/", nil) + setUserID(c, 1) + + handler.DeleteChallenge(c) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_CHALLENGE_ID") +} + +func TestManualChallengeHandler_DeleteChallenge_ProviderNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.DELETE("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.DeleteChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, services.ErrDNSProviderNotFound) + + req, _ := http.NewRequest("DELETE", "/dns-providers/1/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "PROVIDER_NOT_FOUND") +} + +func TestManualChallengeHandler_DeleteChallenge_ProviderInternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.DELETE("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.DeleteChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, errors.New("db error")) + + req, _ := http.NewRequest("DELETE", "/dns-providers/1/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_DeleteChallenge_InvalidProviderType(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.DELETE("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.DeleteChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "cloudflare"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + + req, _ := http.NewRequest("DELETE", "/dns-providers/1/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_TYPE") +} + +func TestManualChallengeHandler_DeleteChallenge_ChallengeNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.DELETE("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.DeleteChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("DeleteChallenge", mock.Anything, "test", uint(1)).Return(services.ErrChallengeNotFound) + + req, _ := http.NewRequest("DELETE", "/dns-providers/1/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "CHALLENGE_NOT_FOUND") +} + +func TestManualChallengeHandler_DeleteChallenge_Unauthorized(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.DELETE("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.DeleteChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("DeleteChallenge", mock.Anything, "test", uint(1)).Return(services.ErrUnauthorized) + + req, _ := http.NewRequest("DELETE", "/dns-providers/1/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Contains(t, w.Body.String(), "UNAUTHORIZED") +} + +func TestManualChallengeHandler_DeleteChallenge_InternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.DELETE("/dns-providers/:id/manual-challenge/:challengeId", func(c *gin.Context) { + setUserID(c, 1) + handler.DeleteChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("DeleteChallenge", mock.Anything, "test", uint(1)).Return(errors.New("db error")) + + req, _ := http.NewRequest("DELETE", "/dns-providers/1/manual-challenge/test", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_CreateChallenge_InvalidProviderID(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.CreateChallenge(c) + }) + + req, _ := http.NewRequest("POST", "/dns-providers/invalid/manual-challenges", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_ID") +} + +func TestManualChallengeHandler_CreateChallenge_ProviderNotFound(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.CreateChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, services.ErrDNSProviderNotFound) + + body := CreateChallengeRequest{FQDN: "_acme-challenge.example.com", Value: "test"} + bodyBytes, _ := json.Marshal(body) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenges", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Contains(t, w.Body.String(), "PROVIDER_NOT_FOUND") +} + +func TestManualChallengeHandler_CreateChallenge_ProviderInternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.CreateChallenge(c) + }) + + mockProviderService.On("Get", mock.Anything, uint(1)).Return(nil, errors.New("db error")) + + body := CreateChallengeRequest{FQDN: "_acme-challenge.example.com", Value: "test"} + bodyBytes, _ := json.Marshal(body) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenges", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestManualChallengeHandler_CreateChallenge_InvalidProviderType(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.CreateChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "cloudflare"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + + body := CreateChallengeRequest{FQDN: "_acme-challenge.example.com", Value: "test"} + bodyBytes, _ := json.Marshal(body) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenges", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Contains(t, w.Body.String(), "INVALID_PROVIDER_TYPE") +} + +func TestManualChallengeHandler_CreateChallenge_ChallengeInProgress(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.CreateChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("CreateChallenge", mock.Anything, mock.Anything).Return(nil, services.ErrChallengeInProgress) + + body := CreateChallengeRequest{FQDN: "_acme-challenge.example.com", Value: "test"} + bodyBytes, _ := json.Marshal(body) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenges", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusConflict, w.Code) + assert.Contains(t, w.Body.String(), "CHALLENGE_IN_PROGRESS") +} + +func TestManualChallengeHandler_CreateChallenge_InternalError(t *testing.T) { + mockService := new(MockManualChallengeService) + mockProviderService := new(mockDNSProviderServiceForChallenge) + handler := NewManualChallengeHandler(mockService, mockProviderService) + + router := setupChallengeTestRouter() + router.POST("/dns-providers/:id/manual-challenges", func(c *gin.Context) { + setUserID(c, 1) + handler.CreateChallenge(c) + }) + + provider := &models.DNSProvider{ID: 1, ProviderType: "manual"} + mockProviderService.On("Get", mock.Anything, uint(1)).Return(provider, nil) + mockService.On("CreateChallenge", mock.Anything, mock.Anything).Return(nil, errors.New("db error")) + + body := CreateChallengeRequest{FQDN: "_acme-challenge.example.com", Value: "test"} + bodyBytes, _ := json.Marshal(body) + + req, _ := http.NewRequest("POST", "/dns-providers/1/manual-challenges", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "INTERNAL_ERROR") +} + +func TestChallengeToResponse_WithoutLastCheckAt(t *testing.T) { + now := time.Now() + + challenge := &models.ManualChallenge{ + ID: "test-id", + ProviderID: 1, + UserID: 1, + FQDN: "_acme-challenge.example.com", + Value: "txtvalue", + Status: models.ChallengeStatusVerified, + DNSPropagated: true, + CreatedAt: now, + ExpiresAt: now.Add(10 * time.Minute), + LastCheckAt: nil, // No last check + ErrorMessage: "", + } + + resp := challengeToResponse(challenge) + + assert.Equal(t, "test-id", resp.ID) + assert.Equal(t, "verified", resp.Status) + assert.True(t, resp.DNSPropagated) + assert.Empty(t, resp.LastCheckAt) + assert.Empty(t, resp.ErrorMessage) +} + +func TestNewErrorResponse_NilDetails(t *testing.T) { + resp := newErrorResponse("TEST_CODE", "Test message", nil) + + assert.False(t, resp.Success) + assert.Equal(t, "TEST_CODE", resp.Error.Code) + assert.Equal(t, "Test message", resp.Error.Message) + assert.Nil(t, resp.Error.Details) +}