diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c81ddf06..3845a88f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,7 +41,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4 + uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 with: languages: ${{ matrix.language }} # Use CodeQL config to exclude documented false positives @@ -57,10 +57,10 @@ jobs: cache-dependency-path: backend/go.sum - name: Autobuild - uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4 + uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4 + uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2dbae921..48d73806 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -363,7 +363,7 @@ jobs: - name: Upload Trivy results if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 with: sarif_file: 'trivy-results.sarif' token: ${{ secrets.GITHUB_TOKEN }} @@ -642,7 +642,7 @@ jobs: # Critical Fix #3: SARIF category includes SHA to prevent conflicts - name: Upload SARIF to GitHub Security if: always() - uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 with: sarif_file: grype-results.sarif category: supply-chain-pr-${{ github.event.pull_request.number }}-${{ github.sha }} @@ -793,3 +793,113 @@ jobs: issue_number: context.issue.number, body: body }); + + # ============================================================================ + # E2E Tests (Playwright) for PR Builds + # ============================================================================ + # This job runs end-to-end tests using Playwright against the Docker image + # built for pull requests. It validates the application's functionality from + # the user's perspective before merging. + # + # Dependency Chain: build-and-push → e2e-tests-pr + # ============================================================================ + e2e-tests-pr: + name: E2E Tests (Playwright) + needs: build-and-push + runs-on: ubuntu-latest + timeout-minutes: 15 + if: | + github.event_name == 'pull_request' && + needs.build-and-push.outputs.skip_build != 'true' && + needs.build-and-push.result == 'success' + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + + - name: Download Docker image artifact + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: pr-image-${{ github.event.pull_request.number }} + + - name: Load Docker image + run: | + echo "📦 Loading image from artifact..." + docker load -i charon-pr-image.tar + echo "✅ Image loaded successfully" + + - name: Normalize image name + run: | + IMAGE_NAME=$(echo "${{ github.repository_owner }}/charon" | tr '[:upper:]' '[:lower:]') + echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV + + - name: Verify loaded image + run: | + IMAGE_REF="ghcr.io/${{ env.IMAGE_NAME }}:pr-${{ github.event.pull_request.number }}" + if ! docker image inspect "${IMAGE_REF}" >/dev/null 2>&1; then + echo "❌ ERROR: Image not found: ${IMAGE_REF}" + docker images + exit 1 + fi + echo "✅ Image loaded: ${IMAGE_REF}" + + - name: Start application container + run: | + IMAGE_REF="ghcr.io/${{ env.IMAGE_NAME }}:pr-${{ github.event.pull_request.number }}" + docker run -d --name charon \ + -p 8080:8080 \ + -e CHARON_ENV=development \ + -e CHARON_DEBUG=1 \ + -e CHARON_ENCRYPTION_KEY=test-key-for-ci-only-not-production \ + "${IMAGE_REF}" + + - name: Wait for application health + run: | + echo "Waiting for application to be ready..." + timeout 120 bash -c 'until curl -sf http://localhost:8080/api/v1/health > /dev/null; do + echo "Waiting for health endpoint..." + sleep 2 + done' + echo "✅ Application is ready" + + - name: Setup Node.js + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6 + with: + node-version: lts/* + + - name: Install Playwright dependencies + run: | + npm ci + npx playwright install --with-deps + + - name: Run Playwright E2E tests + env: + PLAYWRIGHT_BASE_URL: http://localhost:8080 + run: npx playwright test + + - name: Stop application container + if: always() + run: docker stop charon && docker rm charon + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: playwright-report-pr-${{ github.event.pull_request.number }} + path: playwright-report/ + retention-days: 7 + + - name: Create E2E Test Summary + if: always() + run: | + echo "## 🎭 E2E Test Results - PR #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Image**: \`ghcr.io/${{ env.IMAGE_NAME }}:pr-${{ github.event.pull_request.number }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Status**: ${{ job.status == 'success' && '✅ All tests passed' || '❌ Tests failed' }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [[ "${{ job.status }}" != "success" ]]; then + echo "📊 [View Test Report](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}#artifacts)" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml.disabled similarity index 74% rename from .github/workflows/playwright.yml rename to .github/workflows/playwright.yml.disabled index c4de1ab2..02ce9cbf 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml.disabled @@ -1,4 +1,14 @@ -name: Playwright Tests +# ============================================================================ +# DISABLED: This workflow has been integrated into docker-build.yml +# ============================================================================ +# Integration date: January 12, 2026 +# Reason: Consolidated E2E testing with Docker build workflow for better +# visibility and to ensure tests run against the actual built image. +# +# See: .github/workflows/docker-build.yml → e2e-tests-pr job +# ============================================================================ + +name: Playwright Tests (DISABLED) on: push: branches: [ main, master ] diff --git a/.github/workflows/security-weekly-rebuild.yml b/.github/workflows/security-weekly-rebuild.yml index a94dbfc7..4d2fd9ea 100644 --- a/.github/workflows/security-weekly-rebuild.yml +++ b/.github/workflows/security-weekly-rebuild.yml @@ -105,7 +105,7 @@ jobs: severity: 'CRITICAL,HIGH,MEDIUM' - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10 with: sarif_file: 'trivy-weekly-results.sarif' 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) +}