Skip to content

[test] Add tests for proxy.handleWithDIFC uncovered branches#3059

Merged
lpcox merged 1 commit intomainfrom
test-coverage-handler-difc-cbae97d36109cae1
Apr 3, 2026
Merged

[test] Add tests for proxy.handleWithDIFC uncovered branches#3059
lpcox merged 1 commit intomainfrom
test-coverage-handler-difc-cbae97d36109cae1

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot commented Apr 2, 2026

Test Coverage Improvement: handleWithDIFC

Function Analyzed

  • Package: internal/proxy
  • Function: handleWithDIFC
  • File: internal/proxy/handler.go (lines 116–302)
  • Complexity: High — 6-phase DIFC pipeline with 20+ conditional branches

Why This Function?

handleWithDIFC is the core enforcement function of the proxy server. It implements the full DIFC (Decentralized Information Flow Control) pipeline. Despite its complexity, all 6 existing tests used guard.NewNoopGuard(), which always returns nil from LabelResponse. This means only the "no fine-grained labels + coarse allowed" path was tested — leaving the vast majority of branches uncovered.

What Was Added

internal/proxy/handler_difc_test.go — 12 new tests covering every previously untested branch:

Test Branch Covered
TestHandleWithDIFC_LabelResourceError Phase 1: LabelResource error → 502
TestHandleWithDIFC_WriteOperationBlocked Phase 2: Write blocked by coarse check → 403
TestHandleWithDIFC_LabelResponseError_CoarseAllowed Phase 4: LabelResponse error, coarse allowed → original response
TestHandleWithDIFC_LabelResponseError_CoarseBlocked Phase 4: LabelResponse error, coarse blocked → empty response
TestHandleWithDIFC_NoFineGrainedLabels_CoarseBlocked Phase 5: nil fine-grained + coarse blocked → empty response
TestHandleWithDIFC_StrictMode_FiltersBlock Phase 5: strict mode + filtered items → 403
TestHandleWithDIFC_Collection_FilterMode_ItemsFiltered Phase 5: filter mode collection → only accessible items returned
TestHandleWithDIFC_GraphQL_Collection_NoItemsFiltered Phase 5: GraphQL + collection, no filtering → original body
TestHandleWithDIFC_GraphQL_Collection_ItemsFiltered Phase 5: GraphQL + collection, filtered → rebuilt response
TestHandleWithDIFC_SimpleLabeledData_NonGraphQL Phase 5: SimpleLabeledData (REST) → ToResult path
TestHandleWithDIFC_SimpleLabeledData_GraphQL Phase 5: SimpleLabeledData (GraphQL) → original body
TestHandleWithDIFC_PropagateMode_AccumulatesLabels Phase 6: propagate mode → agent labels accumulated

Key Infrastructure

  • stubGuard: A configurable guard.Guard test double that allows tests to control what LabelResource and LabelResponse return, enabling precise branch targeting.
  • newTestServerWithStub / newTestServerWithPrivateAgent: Server factory helpers that set up servers with specific guards and enforcement modes.
  • privateResource() / publicResource(): Factory helpers for creating labeled resources to trigger coarse check denials.

Notes

  • Tests are in package proxy (same package), following the project convention.
  • All tests use testify (require/assert) as per AGENTS.md conventions.
  • The test file follows the existing handler_test.go patterns (mockUpstream, proxyHandler, httptest).
  • Tests could not be executed in the CI environment where this PR was generated due to unavailable Go module cache; the tests are syntactically correct (verified with gofmt) and logically correct (verified against the handler implementation).

Generated by Test Coverage Improver

Generated by Test Coverage Improver ·

Adds handler_difc_test.go with 12 comprehensive tests covering
the previously untested branches of the handleWithDIFC DIFC
pipeline in internal/proxy/handler.go:

- Phase 1: LabelResource error → 502
- Phase 2: write operation blocked by coarse check → 403
- Phase 4: LabelResponse error with coarse allowed → original response
- Phase 4: LabelResponse error with coarse blocked → empty response
- Phase 5: no fine-grained labels + coarse blocked → empty response
- Phase 5: strict mode with filtered items → 403
- Phase 5: filter mode with filtered collection items → accessible items returned
- Phase 5: GraphQL + collection + no items filtered → original body preserved
- Phase 5: GraphQL + collection + filtered items → rebuilt response
- Phase 5: SimpleLabeledData (non-GraphQL) → ToResult
- Phase 5: SimpleLabeledData (GraphQL) → original body preserved
- Phase 6: propagate mode accumulates secrecy labels on agent

Introduces stubGuard, a configurable test double implementing
guard.Guard that allows controlling LabelResource and LabelResponse
return values to exercise each code path.

All existing tests used guard.NewNoopGuard() which always returns
nil from LabelResponse, limiting coverage to the happy path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@lpcox lpcox marked this pull request as ready for review April 3, 2026 04:54
Copilot AI review requested due to automatic review settings April 3, 2026 04:54
@lpcox lpcox merged commit 34dcb46 into main Apr 3, 2026
3 checks passed
@lpcox lpcox deleted the test-coverage-handler-difc-cbae97d36109cae1 branch April 3, 2026 04:54
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds targeted unit tests to increase branch coverage of the DIFC enforcement pipeline in internal/proxy by exercising previously untested error/fallback and filtering behaviors in handleWithDIFC.

Changes:

  • Introduces a configurable stubGuard to deterministically control LabelResource / LabelResponse outcomes.
  • Adds 12 new tests covering LabelResource/LabelResponse failure paths, strict vs filter mode behavior, GraphQL collection rebuild behavior, and propagate-mode label accumulation.
  • Adds small test helpers for building servers with specific enforcement modes and agent default labels.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +115 to +133
func TestHandleWithDIFC_WriteOperationBlocked(t *testing.T) {
// The agent carries a private secrecy tag; the resource (public) has no secrecy.
// For a WRITE: agent secrecy must be a subset of resource secrecy.
// Agent has "private:test-org/test-repo", resource is empty → write is denied.
upstream := mockUpstream(t, http.StatusOK, map[string]interface{}{"id": 1})
defer upstream.Close()

g := &stubGuard{
labelResourceResult: publicResource(),
labelResourceOp: difc.OperationWrite,
}
s := newTestServerWithPrivateAgent(t, upstream.URL, g, difc.EnforcementFilter)
h := &proxyHandler{server: s}

req := httptest.NewRequest(http.MethodPost, "/repos/org/repo/issues",
bytes.NewBufferString(`{"title":"new"}`))
w := httptest.NewRecorder()
h.handleWithDIFC(w, req, "/repos/org/repo/issues", "create_issue",
map[string]interface{}{"owner": "org", "repo": "repo"}, nil)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestHandleWithDIFC_WriteOperationBlocked drives the write-block branch by calling handleWithDIFC with a REST POST. In the actual request path, ServeHTTP bypasses handleWithDIFC for non-GraphQL write methods (it calls passthrough), so this scenario is unreachable and can give misleading coverage. Consider rewriting this test to exercise the same Phase 2 write-deny behavior through a GraphQL POST /graphql that the guard labels as OperationWrite (e.g., a mutation), or otherwise route through ServeHTTP in a way that matches production control flow.

Copilot uses AI. Check for mistakes.
Comment on lines +528 to +529
for i, t := range tags {
result[i] = string(t)
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In tagsAsStrings, the loop variable name t is easy to confuse with the conventional t *testing.T identifier used throughout tests. Renaming it to something like tag would make the helper clearer and reduce cognitive overhead when reading failures.

Suggested change
for i, t := range tags {
result[i] = string(t)
for i, tag := range tags {
result[i] = string(tag)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants