fix: enforce validation for empty domain names in proxy host updates and update related tests

This commit is contained in:
GitHub Actions
2026-02-15 18:31:33 +00:00
parent 3d614dd8e2
commit f4fafde161
6 changed files with 328 additions and 263 deletions

View File

@@ -112,8 +112,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=

View File

@@ -295,6 +295,38 @@ func TestProxyHostUpdate_WAFDisabled(t *testing.T) {
assert.True(t, updated.WAFDisabled)
}
func TestProxyHostUpdate_RejectsEmptyDomainNamesAndPreservesOriginal(t *testing.T) {
t.Parallel()
router, db := setupUpdateTestRouter(t)
host := models.ProxyHost{
UUID: uuid.NewString(),
Name: "Validation Test Host",
DomainNames: "original.example.com",
ForwardScheme: "http",
ForwardHost: "localhost",
ForwardPort: 8080,
Enabled: true,
}
require.NoError(t, db.Create(&host).Error)
updateBody := map[string]any{
"domain_names": "",
}
body, _ := json.Marshal(updateBody)
req := httptest.NewRequest(http.MethodPut, "/api/v1/proxy-hosts/"+host.UUID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
router.ServeHTTP(resp, req)
require.Equal(t, http.StatusBadRequest, resp.Code)
var updated models.ProxyHost
require.NoError(t, db.First(&updated, "uuid = ?", host.UUID).Error)
assert.Equal(t, "original.example.com", updated.DomainNames)
}
// TestProxyHostUpdate_SecurityHeaderProfileID_NegativeFloat tests that a negative float64
// for security_header_profile_id returns a 400 Bad Request.
func TestProxyHostUpdate_SecurityHeaderProfileID_NegativeFloat(t *testing.T) {

View File

@@ -80,6 +80,10 @@ func (s *ProxyHostService) ValidateHostname(host string) error {
}
func (s *ProxyHostService) validateProxyHost(host *models.ProxyHost) error {
if strings.TrimSpace(host.DomainNames) == "" {
return errors.New("domain names is required")
}
if host.ForwardHost == "" {
return errors.New("forward host is required")
}

View File

@@ -93,3 +93,44 @@ func TestProxyHostService_ForwardHostValidation(t *testing.T) {
})
}
}
func TestProxyHostService_DomainNamesRequired(t *testing.T) {
db := setupProxyHostTestDB(t)
service := NewProxyHostService(db)
t.Run("create rejects empty domain names", func(t *testing.T) {
host := &models.ProxyHost{
UUID: "create-empty-domain",
DomainNames: "",
ForwardHost: "localhost",
ForwardPort: 8080,
ForwardScheme: "http",
}
err := service.Create(host)
assert.Error(t, err)
assert.Contains(t, err.Error(), "domain names is required")
})
t.Run("update rejects whitespace-only domain names", func(t *testing.T) {
host := &models.ProxyHost{
UUID: "update-empty-domain",
DomainNames: "valid.example.com",
ForwardHost: "localhost",
ForwardPort: 8080,
ForwardScheme: "http",
}
err := service.Create(host)
assert.NoError(t, err)
host.DomainNames = " "
err = service.Update(host)
assert.Error(t, err)
assert.Contains(t, err.Error(), "domain names is required")
persisted, getErr := service.GetByID(host.ID)
assert.NoError(t, getErr)
assert.Equal(t, "valid.example.com", persisted.DomainNames)
})
}

View File

@@ -1,352 +1,341 @@
## DNS Providers E2E Failure Recovery Spec
## Data Consistency Failure Remediation Spec
Date: 2026-02-15
Owner: Planning Agent
Target file: docs/plans/current_spec.md
Scope: Single failure only
---
## 1) Introduction
This specification addresses two failing Playwright Firefox tests in tests/core/domain-dns-management.spec.ts:
This specification addresses only the failing Playwright assertion:
1. DNS Providers - list providers after API seed
2. DNS Providers - delete provider via API and verify removal
- `[firefox] tests/core/data-consistency.spec.ts:327 Failed transaction prevents partial data updates`
Observed failures are deterministic across retries and happen before deletion assertions. The plan below focuses on root-cause correction with minimal API request overhead and stable UI semantics.
Failure signal:
- Expected: original domain remains unchanged after invalid update attempt.
- Received: empty string (`''`) for domain.
Primary objective:
- Make DNS provider cards discoverable and testable again without regressing Manual DNS Challenge behavior.
Anchor from provided failure report:
- Stack trace lines 369-372 (poll/assertion block in this scenario).
Secondary objective:
- Reduce unnecessary DNS page requests while preserving UX and accessibility behavior.
Current file anchor (same logical block in workspace revision):
- `tests/core/data-consistency.spec.ts` lines around 379-382:
- `return proxy.domain_names || proxy.domainNames || '';`
- message: `Expected proxy ... domain to remain unchanged after failed update`
Objective:
- Define a deterministic, minimal-request remediation plan that resolves this failure at root cause.
- Keep changes narrowly scoped to this scenario.
---
## 2) Research Findings
### 2.1 Confirmed failing assertions
### 2.1 Test logic findings
From targeted run:
- tests/core/domain-dns-management.spec.ts:122
- waitForResourceInUI fails after 15s for namespaced provider name.
- tests/core/domain-dns-management.spec.ts:166
- expect(providerCard).toBeVisible() fails, element not found.
File inspected:
- `tests/core/data-consistency.spec.ts`
Both fail before provider deletion verification; both retries fail identically.
Relevant behavior:
- Creates a proxy via `POST /api/v1/proxy-hosts`.
- Sends invalid update payload `domain_names: ''` via `PUT /api/v1/proxy-hosts/:uuid`.
- Accepts status `[200, 400, 422]`.
- Asserts domain remains unchanged afterward.
### 2.2 Root-cause path (entry -> transform -> render)
Observation:
- The test currently permits `200` for the invalid update step while still expecting unchanged data, which weakens contract clarity.
Entry path:
- Test fixture creates providers via TestDataManager.createDNSProvider in tests/utils/TestDataManager.ts.
- Backend accepts and persists via DNSProviderHandler.Create in backend/internal/api/handlers/dns_provider_handler.go.
### 2.2 API behavior findings
Transform path:
- DNSProviders page fetches provider list through useDNSProviders -> getDNSProviders.
- File chain:
- frontend/src/pages/DNSProviders.tsx
- frontend/src/hooks/useDNSProviders.ts
- frontend/src/api/dnsProviders.ts
Files/functions inspected:
- `backend/internal/api/handlers/proxy_host_handler.go`
- `func (h *ProxyHostHandler) Update(c *gin.Context)`
- `backend/internal/services/proxyhost_service.go`
- `func (s *ProxyHostService) Update(host *models.ProxyHost) error`
- `func (s *ProxyHostService) ValidateUniqueDomain(domainNames string, excludeID uint) error`
- `func (s *ProxyHostService) validateProxyHost(host *models.ProxyHost) error`
- `backend/internal/models/proxy_host.go`
- `DomainNames string 'gorm:"not null;index"'`
Render path:
- In DNSProviders.tsx, showManualChallenge is derived from manualChallenge state.
- loadManualChallenge calls getChallenge(providerId, active).
- On any error, loadManualChallenge currently sets a fallback challenge object.
- Because fallback always sets manualChallenge, showManualChallenge becomes true.
- Provider cards grid is gated by !showManualChallenge and providers.length > 0.
- Result: provider cards are suppressed even when providers exist.
Key observations:
1. Handler `Update` applies payload field directly:
- if `domain_names` present and string, assigns `host.DomainNames = v` even when `v == ""`.
2. Service validation does not reject empty `domain_names`:
- `ValidateUniqueDomain` checks duplicates only.
- `validateProxyHost` validates `forward_host`, not `domain_names`.
3. Model uses `not null`, which allows empty string in SQLite.
Persistence path validation:
- Backend list/delete routes are present and correct when encryption key is configured:
- protected.GET /dns-providers
- protected.DELETE /dns-providers/:id
in backend/internal/api/routes/routes.go.
- DNS provider service List/Delete implementations do not explain the UI invisibility.
Likely direct cause of observed `''`:
- Empty domain is accepted and persisted.
### 2.3 Additional architectural observations
### 2.3 Transaction semantics findings
- Manual challenge endpoint GET /dns-providers/:id/manual-challenge/:challengeId returns 404 when challenge is missing.
- Frontend passes challengeId active as a synthetic ID, but backend has no explicit active alias route.
- Manual challenge UI has comprehensive component tests in frontend/src/components/__tests__/ManualDNSChallenge.test.tsx.
- DNS providers page has no dedicated unit test for coexistence rules between provider cards and manual challenge panel.
Files/functions inspected:
- `backend/internal/database/database.go`
- `gorm.Config{ SkipDefaultTransaction: true }`
- `backend/internal/api/handlers/proxy_host_handler.go`
- `Update` calls `service.Update(host)` then `caddyManager.ApplyConfig(...)`
### 2.4 Confidence score
Key observations:
1. No explicit transaction wraps validation + update + config-apply in `Update`.
2. If persistence succeeds but Caddy apply fails, request may fail after DB mutation (no rollback).
3. Scenario label says “Failed transaction”, but code path is not transactional in this route.
Confidence: 93% (high)
### 2.4 Frontend contract context (for API/UI parity)
Files/components inspected:
- `frontend/src/components/ProxyHostForm.tsx`
- Domain input has `required` on client-side form.
- `frontend/src/api/proxyHosts.ts`
- `updateProxyHost(uuid, host)` forwards payload directly.
- `frontend/src/pages/ProxyHosts.tsx`
Observation:
- UI form prevents blank input for normal interactions, but API path still allows blank when called directly (as in test).
### 2.5 Confidence
Confidence score: 95%
Rationale:
- Failure output, snapshots, and rendering condition align.
- Issue reproduces across retries.
- Backend CRUD path appears healthy; failure is in frontend display gating.
- Failure output and inspected code paths align directly with persisted empty domain.
- Missing domain validation and non-transactional update semantics are explicit in code.
---
## 3) Technical Specifications
## 3) Root Cause Hypothesis (Primary)
## 3.1 EARS requirements
Primary root cause:
- Backend update contract allows `domain_names: ''` and persists it.
- WHEN the DNS Providers page loads and no active manual challenge exists, THE SYSTEM SHALL render provider cards from GET /api/v1/dns-providers.
- WHEN provider cards exist, THE SYSTEM SHALL keep them visible regardless of manual challenge fetch failures.
- WHEN stabilizing the current failure, THE SYSTEM SHALL apply frontend root-cause fixes before any E2E test edits.
- IF the same E2E assertion still fails after the frontend fix is applied, THEN THE SYSTEM SHALL permit minimal, targeted test adjustments.
- IF manual challenge retrieval returns not found, THEN THE SYSTEM SHALL treat it as no active challenge and SHALL NOT inject fallback challenge content.
- WHEN a person explicitly requests Manual DNS Challenge view, THE SYSTEM SHALL fetch/display challenge data and SHALL preserve keyboard/screen-reader semantics.
- WHEN a provider is created via API seed in tests, THE SYSTEM SHALL expose it in UI heading text within the provider card grid.
- WHEN a provider is deleted via API and page is refreshed, THE SYSTEM SHALL remove the matching card from the grid.
Contributing factors:
- Test allows `200` on invalid update attempt, reducing strictness of expected failure contract.
- Route semantics are not transactional despite scenario wording.
## 3.2 Request-minimization strategy (least amount of requests)
Current behavior issues:
- DNS page performs challenge fetch eagerly and converts errors into fallback UI state.
Planned behavior:
- One baseline request on page load: GET /api/v1/dns-providers.
- Zero manual challenge requests on initial load unless manual challenge panel is opened explicitly.
- Optional follow-up requests only on user action:
- GET /api/v1/dns-providers/:id/manual-challenge/active (new optional endpoint), or
- GET /api/v1/dns-providers/:id/manual-challenge/:challengeId only if challengeId is known.
Net effect:
- Fewer initial requests and deterministic provider list rendering.
## 3.3 API and handler design options
Option A (preferred for least backend change):
- Frontend-only behavior correction.
- In frontend/src/pages/DNSProviders.tsx:
- Remove synthetic fallback challenge injection from catch block in loadManualChallenge.
- Track manual panel visibility separately from challenge data.
- Only call loadManualChallenge from manual action button or explicit deep-link flow.
Option B (clean API contract enhancement):
- Add explicit active challenge endpoint:
- GET /api/v1/dns-providers/:id/manual-challenge/active
- Handler addition in backend/internal/api/handlers/manual_challenge_handler.go:
- GetActiveChallenge(c *gin.Context)
- Service addition in backend/internal/services/manual_challenge_service.go:
- GetLatestActiveChallengeForProvider(ctx, providerID, userID)
- Route registration in backend/internal/api/routes/routes.go.
Recommendation:
- Implement Option A first (fastest unblock).
- Option B can follow if active-challenge UX remains core and reused broadly.
## 3.4 Component-level design changes
Primary component:
- frontend/src/pages/DNSProviders.tsx
Current critical symbols:
- loadManualChallenge
- showManualChallenge
- manualChallenge
- manualProviderId
Planned symbol responsibilities:
- isManualPanelOpen (new): controls panel visibility as explicit UI state.
- manualChallenge: nullable real challenge only (no synthetic fallback).
- loadManualChallenge(providerId):
- on 404/not-found, set manualChallenge = null and keep provider cards visible.
- on transport/server errors, surface toast warning without replacing page mode.
Render rules:
- Provider grid visibility should not be blocked by challenge lookup failure.
- Manual panel visibility must be driven by `isManualPanelOpen`, not by presence/absence of `manualChallenge`.
- `manualChallenge` data existence must not implicitly switch overall page mode.
Accessibility continuity:
- Preserve existing button names and aria labeling in ManualDNSChallenge.
- Ensure focus flow remains predictable when opening/closing panel.
## 3.5 Test strategy updates
E2E tests to stabilize:
- tests/core/domain-dns-management.spec.ts
E2E test-edit guard:
- Do not edit failing E2E tests during initial fix.
- Only after frontend fix attempt, if the same failure still reproduces deterministically, allow minimal assertion/wait adjustments limited to the failing steps.
Related E2E tests for regression guard:
- tests/dns-provider-crud.spec.ts
- tests/manual-dns-provider.spec.ts
- tests/dns-provider-types.spec.ts
Frontend unit test additions (new):
- frontend/src/pages/__tests__/DNSProviders.test.tsx (new file)
- Case 1: providers render when manual challenge fetch returns 404.
- Case 2: manual panel opens only after explicit button action.
- Case 3: provider grid remains visible after manual challenge fetch error.
Backend unit tests (only if Option B implemented):
- backend/internal/api/handlers/manual_challenge_handler_test.go
- backend/internal/services/manual_challenge_service_test.go
Non-cause (for this failure):
- UI rendering logic is not primary here; final assertion reads API detail directly.
---
## 4) Implementation Plan (Phased, minimal-request first)
## 4) EARS Requirements (Focused)
## Phase 1 - Reproduction and baseline lock
- WHEN a proxy host update request includes `domain_names` as an empty string, THE SYSTEM SHALL reject the request with validation error (`400` or `422`).
- WHEN validation rejects that request, THE SYSTEM SHALL NOT persist any change to `domain_names`.
- WHEN the invalid update is attempted in the failing scenario, THE SYSTEM SHALL preserve the previously stored domain value.
- IF update processing fails after persistence-related steps, THEN THE SYSTEM SHALL provide deterministic behavior that prevents partial state for this field path.
- WHEN this failure is fixed, THE SYSTEM SHALL pass the targeted Playwright scenario deterministically across repeated runs.
### Decision - 2026-02-15
**Decision**: For this single flaky-failure fix scope, the Supervisor blocker on post-persistence failure behavior is closed by enforcing pre-persistence validation rejection for empty `domain_names` and treating this as the required failure path for the targeted scenario.
**Context**: The failing scenario (`Failed transaction prevents partial data updates`) currently reproduces because `domain_names: ''` is accepted and persisted. The route does not implement an explicit transaction boundary for persistence + post-persistence Caddy apply, so broad rollback semantics are a separate concern.
**Options**:
- **Option A (selected):** Enforce validation rejection before persistence for empty-domain updates in this scope.
- **Option B (deferred):** Implement broader transactional rollback hardening for post-persistence failures in the update flow.
**Rationale**: The reported flaky failure is fully addressed by preventing invalid writes before persistence, which directly satisfies the scenarios failure expectation with the smallest safe change surface. Expanding into transaction rollback hardening would increase scope and coupling for this fix and is not required to close the current blocker.
**Impact**: The targeted failure path is deterministic for this issue. Broader post-persistence rollback guarantees remain unchanged and are explicitly deferred as follow-up technical hardening.
**Review**: Reassess deferred rollback hardening in a dedicated follow-up item after this flaky-failure fix lands and stabilizes.
### Supervisor Blocker Closure Note
- **Status**: Closed for this issue scope.
- **Closure basis**: Enforced validation rejection path (empty domain rejected pre-persistence) is the approved failure mechanism for this scenario.
- **Deferred follow-up**: Broader transaction rollback hardening for post-persistence failures is out-of-scope for this fix and tracked as subsequent hardening work.
---
## 5) Remediation Plan (Phased)
### Phase 1: Contract lock and deterministic repro
Goal:
- Lock failing behavior and ensure deterministic reproduction path.
- Reproduce only this failure and lock expected contract.
Actions:
1. Run targeted failing tests only in tests/core/domain-dns-management.spec.ts.
2. Capture failure screenshots, traces, and assertions.
3. Capture request timeline for /api/v1/dns-providers and manual challenge endpoints.
1. Run targeted Playwright case only:
- `tests/core/data-consistency.spec.ts` test: `Failed transaction prevents partial data updates`.
2. Capture request/response for:
- `PUT /api/v1/proxy-hosts/:uuid` invalid payload.
- `GET /api/v1/proxy-hosts/:uuid` verification read.
3. Record whether update returns `200` and whether persisted value is empty.
Expected output:
- Baseline artifact bundle confirming pre-fix failure.
Validation output:
- Baseline artifact proving failure prior to fix.
## Phase 2 - Frontend display mode correction (least requests)
### Phase 2: Backend validation fix (primary path)
Goal:
- Ensure provider list renders independently from manual challenge fetch.
- Enforce non-empty domain update contract at API boundary/service.
Files:
- frontend/src/pages/DNSProviders.tsx
Files/functions to update (implementation phase reference):
- `backend/internal/api/handlers/proxy_host_handler.go`
- `Update`
- `backend/internal/services/proxyhost_service.go`
- `Update` and/or validation helper path
Actions:
1. Decouple panel visibility from challenge fetch side effects.
2. Remove synthetic fallback challenge creation on fetch error.
3. Make manual challenge fetch opt-in (button-driven).
4. Keep provider cards visible by default after provider list fetch.
Design constraints:
1. Reject empty/whitespace-only `domain_names` when provided in update payload.
2. Preserve partial-update behavior for omitted fields.
3. Keep current response schema; return clear validation error.
Expected output:
- The two failing tests can locate seeded provider cards.
Validation tests to add/update:
- `backend/internal/api/handlers/proxy_host_handler_update_test.go`
- Add case: empty `domain_names` returns `400/422` and DB value unchanged.
- `backend/internal/services/proxyhost_service_validation_test.go`
- Add case: domain validation rejects empty string on update path.
## Phase 3 - Test hardening and contract coverage
### Phase 3: Transaction semantics hardening decision (narrow)
Goal:
- Prevent regressions in DNS page state machine.
- Resolve mismatch between scenario intent (“failed transaction”) and current route behavior.
Files:
- tests/core/domain-dns-management.spec.ts (assertion timing refinements only if needed)
- frontend/src/pages/__tests__/DNSProviders.test.tsx (new)
Decision gate:
- For this specific failure, validation fix is mandatory and sufficient to close the current Supervisor blocker.
- Broader transactional rollback hardening is explicitly deferred follow-up work and is not part of this issue scope.
Actions:
1. Add unit tests for no-active-challenge behavior.
2. Verify manual challenge visibility toggle logic.
3. Keep Playwright assertions role-based and deterministic.
4. Do not modify existing failing E2E assertions in this phase unless post-fix deterministic failure persists.
Files/functions to inspect for conditional hardening:
- `backend/internal/api/handlers/proxy_host_handler.go`
- `Update` sequence (`service.Update` then `caddyManager.ApplyConfig`).
- `backend/internal/database/database.go`
- `SkipDefaultTransaction` implications.
Expected output:
- Stable UI contract for providers + manual challenge coexistence.
Deferred hardening follow-up (out-of-scope for this fix):
- Evaluate wrapping update path in an explicit transaction for persistence + post-persistence operations, or introduce compensating rollback strategy on downstream failure where feasible.
- Produce a dedicated implementation spec for rollback hardening to avoid coupling with this flaky-failure patch.
- Record explicit risk acceptance for this issue: post-persistence failure rollback behavior remains unchanged until follow-up lands.
## Phase 4 - Backend/API enhancement (deferred by default)
### Phase 4: Test contract tightening (only after product fix)
Goal:
- Normalize active challenge retrieval contract, reduce 404 control-flow usage.
- Align E2E assertion with backend contract.
Scope rule:
- Out of scope for this spec by default.
- Enter this phase only if the frontend-only fix fails to resolve the two target failures after deterministic re-runs.
File:
- `tests/core/data-consistency.spec.ts`
Files:
- backend/internal/api/handlers/manual_challenge_handler.go
- backend/internal/services/manual_challenge_service.go
- backend/internal/api/routes/routes.go
- backend/internal/api/handlers/manual_challenge_handler_test.go
- backend/internal/services/manual_challenge_service_test.go
Allowed change (conditional):
- Narrow accepted status list for invalid update from `[200, 400, 422]` to failure-only status set, but only after backend fix is in place and confirmed.
Actions:
1. Add explicit active challenge endpoint.
2. Return 204 or structured null payload for no-active state.
3. Update frontend manual flow to consume explicit endpoint.
Not allowed:
- Masking failure by weakening final unchanged-data assertion.
Expected output:
- Cleaner API semantics and fewer error-as-control-flow branches.
## Phase 5 - Validation and CI gates
### Phase 5: Deterministic validation sequence (minimal requests)
Goal:
- Validate functionality, patch coverage, and security checks.
- Confirm fix with low-noise, repeatable verification.
Actions:
1. Run exactly the two failing Firefox tests in tests/core/domain-dns-management.spec.ts.
2. Repeat the same two tests to confirm stability (deterministic pass/fail behavior).
3. Run one focused manual DNS regression: tests/manual-dns-provider.spec.ts.
4. Broaden beyond this set only if failures remain unresolved after steps 1-3.
5. Run additional coverage/security gates per repo policy only after deterministic validation set is complete.
Sequence:
1. Run backend targeted tests only (new/updated handler + service tests).
2. Run single Playwright failing scenario.
3. Re-run the same scenario 2 additional times.
Expected output:
- Green E2E for failing scenarios, no regressions in DNS/manual challenge suites.
Deterministic pass criterion:
- 3/3 pass for the targeted scenario.
Request minimization guidance for scenario validation:
- Keep one create request, one invalid update request, one detail read request per attempt.
- Avoid unrelated navigation or suite-wide runs until this scenario is stable.
### Phase 6: Coverage gate and Codecov patch triage closure (mandatory)
Goal:
- Close CI coverage blocker for this flaky-failure fix with explicit, auditable evidence.
Required actions:
1. Run focused coverage on only modified backend/frontend test targets for this failure fix.
2. Open Codecov Patch view for the PR/commit and copy exact uncovered or partial ranges.
3. Record ranges in this plan using exact file + line spans from Patch view (no paraphrase).
4. Add targeted tests that execute each missing/partial modified line.
5. Re-run coverage and confirm Patch view reports 100% coverage for modified lines.
Patch-line triage record (must be filled during execution):
- Source: Codecov Patch view
- Missing ranges: `<file>#Lx-Ly`, `<file>#Lx`, ...
- Partial ranges: `<file>#Lx-Ly`, `<file>#Lx`, ...
- Targeted tests added: `<test file>::<test name>` mapped to each range above
- Closure evidence: Patch view status = 100% for modified lines
Hard gate:
- Do not mark this spec complete while any missing/partial modified range remains open in Codecov Patch view.
---
## 5) Risk and Edge Case Matrix
## 6) Guardrails: When test edits are allowed
1. Risk: Manual challenge panel no longer appears automatically for valid active challenges.
- Mitigation: explicit open action plus optional active endpoint check.
### Test edits are NOT allowed when:
1. Backend currently accepts invalid domain payload (`''`) or persists it.
2. No backend validation test exists for empty-domain rejection.
3. Root-cause behavior is unfixed in handler/service.
2. Risk: Existing manual-dns-provider tests assume automatic panel visibility.
- Mitigation: update tests to trigger panel intentionally via Manual DNS Challenge button.
Required action in this state:
- Fix backend/frontend product behavior first (backend for this failure).
3. Risk: Provider card heading selector changes break tests.
- Mitigation: keep DNSProviderCard title semantics stable in frontend/src/components/DNSProviderCard.tsx.
### Test edits are allowed only when ALL are true:
1. Backend rejects empty `domain_names` deterministically.
2. Backend unit tests verify unchanged persisted value on invalid update.
3. Targeted Playwright scenario passes without weakening final assertion.
4. Risk: Encryption key not configured in environment suppresses DNS routes.
- Mitigation: confirm CHARON_ENCRYPTION_KEY presence in test runtime before asserting provider flows.
Allowed scope of test edits:
- Tighten status-code expectation and reduce nondeterministic waits.
- No assertion downgrades that hide data-integrity regressions.
---
## 6) File-by-File Change Map
## 7) Exact Files/Functions/Components to Inspect During Execution
Planned edits (high likelihood):
- frontend/src/pages/DNSProviders.tsx
- frontend/src/pages/__tests__/DNSProviders.test.tsx (new)
- tests/core/domain-dns-management.spec.ts (small sync updates only if required)
Primary:
- `tests/core/data-consistency.spec.ts`
- test `Failed transaction prevents partial data updates`
- `backend/internal/api/handlers/proxy_host_handler.go`
- `Update`
- `backend/internal/services/proxyhost_service.go`
- `Update`, `ValidateUniqueDomain`, `validateProxyHost`
- `backend/internal/models/proxy_host.go`
- `DomainNames` field constraints
Conditional edits (if API enhancement chosen):
- backend/internal/api/handlers/manual_challenge_handler.go
- backend/internal/services/manual_challenge_service.go
- backend/internal/api/routes/routes.go
- backend/internal/api/handlers/manual_challenge_handler_test.go
- backend/internal/services/manual_challenge_service_test.go
No expected edits for root fix:
- backend/internal/api/handlers/dns_provider_handler.go
- backend/internal/services/dns_provider_service.go
Supporting:
- `backend/internal/api/handlers/proxy_host_handler_update_test.go`
- `backend/internal/services/proxyhost_service_validation_test.go`
- `backend/internal/database/database.go`
- `frontend/src/components/ProxyHostForm.tsx`
- `frontend/src/api/proxyHosts.ts`
- `frontend/src/pages/ProxyHosts.tsx`
---
## 7) Deferred Scope Notes
## 8) Acceptance Criteria (Failure-Scoped)
- Unrelated root-level configuration file review is deferred and removed from this spec scope.
- Any .gitignore, codecov.yml, .dockerignore, or Dockerfile review must be handled in a separate dedicated plan.
1. The targeted Playwright scenario no longer returns empty domain after invalid update attempt.
2. Invalid payload `domain_names: ''` is rejected by backend (`400` or `422`).
3. Persisted domain value remains original after rejection.
4. Targeted backend tests cover both rejection and no-partial-update behavior.
5. Deterministic validation passes (3 repeated targeted Playwright runs).
6. Coverage gate compliance is demonstrated for this fix scope (project threshold met and Patch view validated).
7. Codecov Patch view shows 100% patch coverage for all modified lines in this fix.
8. Exact missing/partial patch ranges (if any appeared) are captured and closed via targeted tests before completion.
9. No unrelated feature/test changes are introduced.
---
## 8) Acceptance Criteria
## 9) Out-of-Scope (Strict)
Functional:
- The two previously failing tests in tests/core/domain-dns-management.spec.ts pass in Firefox:
- DNS Providers - list providers after API seed
- DNS Providers - delete provider via API and verify removal
Behavioral:
- DNS provider cards are visible after API seed on /dns/providers.
- Provider card visibility no longer depends on fallback manual challenge state.
- Manual challenge panel appears only under explicit trigger or real active challenge state.
Quality:
- No regression in tests/manual-dns-provider.spec.ts and tests/dns-provider-crud.spec.ts.
- Added or updated unit tests cover DNSProviders page state transitions.
- Validation sequence follows minimal deterministic order: two failing Firefox tests, repeat run, then one manual DNS regression test.
- No backend/API enhancement work is executed unless frontend fix attempt fails and failure still reproduces.
Coverage and gate alignment:
- Patch coverage remains 100% for modified lines.
- Existing project coverage thresholds remain satisfied.
- DNS provider flows
- Manual DNS challenge flows
- Bulk security header update transaction path (except reference for transaction pattern only)
- Broad transaction rollback hardening for post-persistence failures in proxy-host update flow (deferred follow-up)
- Broad Playwright suite stabilization unrelated to this single failure
---
## 9) Handoff to Supervisor
## 10) Codecov Patch-Line Closure Protocol (Execution Checklist)
Supervisor review focus:
1. Verify root-cause alignment: UI state gating vs backend CRUD.
2. Confirm minimal-request architecture in DNSProviders page flow.
3. Confirm manual panel state is explicit UI state and not inferred from challenge object presence.
4. Confirm no hidden regressions in manual challenge UX and accessibility semantics.
5. Approve frontend-first execution, with backend/API phase only if frontend fix fails.
This checklist is required for this flaky failure only and must be completed before handoff.
1. Capture exact Patch view gaps:
- Copy each missing/partial modified range exactly as displayed in Codecov Patch view.
2. Document triage table in execution notes:
- `Range` | `Why not covered` | `Targeted test` | `Status`.
3. Implement only targeted tests needed to cover listed ranges.
4. Re-run focused coverage and refresh Codecov Patch view.
5. Repeat triage until all listed ranges are closed and Patch coverage is 100%.
Mandatory closure condition:
- Every modified line in this fix is green in Codecov Patch view with no remaining missing/partial ranges.

View File

@@ -358,7 +358,8 @@ test.describe('Data Consistency', () => {
}
);
expect([200, 400, 422]).toContain(response.status());
expect(response.ok()).toBe(false);
expect([400, 422]).toContain(response.status());
});
await test.step('Verify original data unchanged', async () => {