Skip to content

test: close v0.2.0 review coverage gaps#17

Merged
hidetzu merged 2 commits into
mainfrom
feat/close-coverage-gaps
Apr 11, 2026
Merged

test: close v0.2.0 review coverage gaps#17
hidetzu merged 2 commits into
mainfrom
feat/close-coverage-gaps

Conversation

@hidetzu
Copy link
Copy Markdown
Owner

@hidetzu hidetzu commented Apr 11, 2026

Summary

PR 2/4 of the v0.2.0 cleanup pass. Closes every P0/P1 test coverage gap surfaced by the v0.2.0 release review, plus the small test-seam refactor needed to make `internal/app` E2E tests hit the real middleware chain with fake usecases.

No production behavior change beyond:

  • Indirection of `crypto/rand.Reader` through a package-level `randSource` variable in `internal/httpapi/middleware/requestid.go` (necessary for testing the failure-fallback path).
  • Extraction of `app.newWithHandlers` as an internal test seam; `app.New` now delegates to it unchanged.

Everything else is new test code.

What's covered now

1. `internal/logging` (previously 0% coverage)

Test Cases
`TestParseLevel` 10 — every switch branch (debug / info / warn / warning / error), uppercase + mixed case variants, and the defense-in-depth fallback for unknown and empty input
`TestNew_RespectsConfiguredLevel` 4 — debug / info / warn / error, each verifying `slog.Logger.Enabled` returns the expected value for all four standard levels

The `parseLevel` default branch is now unreachable via `Load()` (config.Validate rejects unknown levels since PR 16), but the test stays as defense-in-depth so a future refactor that bypasses `Load` still gets a sane default.

2. `internal/httpapi/middleware` IPv6 and sentinel coverage

Test Cases
`TestClientIP` 11 — XFF single/multi IPv4, XFF single/multi IPv6, leading/trailing whitespace, `RemoteAddr` IPv4 with port, `RemoteAddr` bracketed IPv6 `[2001:db8::1]:1234`, bare IPv6 without port falling through, and empty `RemoteAddr`
`TestNewRequestID_FallsBackToSentinelOnReadFailure` Swaps `randSource` for an `errorReader` and confirms `newRequestID` returns the documented `"00000000000000000000000000"` sentinel

The Fly.io production smoke test already observed real-world IPv6 clients (the `2400:4050:b701:9800:...` address in the smoke-test logs was my own IPv6). These unit tests now guard the XFF extraction path so a refactor cannot silently break IPv6 rate-limit keying.

3. `internal/app` chain integration coverage

Refactor: `app.New` → `newWithHandlers(cfg, logger, analyzeUC, promptUC)`. `New` is now a 1-line wrapper that passes in `usecase.NewAnalyzer()` and `usecase.NewPrompter()`. Tests call `newWithHandlers` directly with `stubAnalyzeUsecase` / `stubPromptUsecase` to exercise the real middleware chain with controllable handler responses.

Stubs: `stubAnalyzeUsecase` supports `block` and `entered` channels for concurrent scenario orchestration. The stub also honors `ctx.Done()` so future timeout tests work without changes.

Test Verifies
`TestChain_AnalyzeEndpointSuccess` POST `/v1/analyze` through the real chain: 200, X-Request-Id set, envelope `{"result": {pull_request, analysis, ...}}` with stubbed `prism.Result` serialized as-is (zero translation layer confirmed end-to-end)
`TestChain_PromptEndpointSuccess` POST `/v1/prompt` with `mode=detailed, language=ja` in body, 200, envelope `{"prompt": "..."}`
`TestChain_ConcurrencyLimitRejectsBeyondCapacity` Cap server at `MaxConcurrentRequests=1`. Fire first request in background (stub parks on gate channel). Fire second request synchronously. Assert 503 + `service_unavailable` error code. Release first request and verify it returns 200. No `time.Sleep` anywhere — pure channel coordination so the race detector sees clean ordering

This closes the P0 "concurrency_limit not E2E tested" gap that PR 6 deliberately deferred.

Not addressed in this PR (deferred to subsequent v0.2.0 cleanup)

  • Shared `writePrismError` helper — `writeAnalyzeUsecaseError` and `writePromptUsecaseError` are still byte-identical; consolidation happens in PR 3.
  • `context.DeadlineExceeded` → `CodeTimeout` mapping fix — currently surfaces as 500 internal_error. I considered adding the timeout E2E test here with `TestChain_TimeoutCancelsSlowHandler`, but decided it would be more useful in PR 3 alongside the fix (otherwise it would just document broken behavior). PR 3 will land the fix and the test together.
  • README + `apiVersion` bump — PR 4.

Local checks

  • `go vet ./...` clean
  • `go test ./... -race -count=1` passes (new totals: 86 top-level tests, 210 total test runs including subtests)
  • `golangci-lint run` 0 issues (both `gofmt` and `revive` are clean)
  • `go mod tidy` clean (no new dependencies)

v0.2.0 cleanup context

# PR Status
1 config boundary validation merged
2 close coverage gaps (this PR) open
3 shared writePrismError + timeout error mapping blocked on 2
4 README + apiVersion bump + v0.2.0 tag blocked on 1, 2, 3

Intended to be squash-merged.

hidetzu added 2 commits April 12, 2026 08:25
Address the remaining QA findings from the v0.2.0 release review
that were flagged as P0/P1 and deferred to a follow-up pass. No
production behavior change in this PR beyond a small, test-only
refactor in internal/app and a randSource injection point in
internal/httpapi/middleware.

Coverage added:

1. internal/logging/logging_test.go (new)
   - TestParseLevel: table-driven, 10 cases covering every branch
     of parseLevel including the defense-in-depth default that
     config.Validate now renders unreachable via Load() but which
     stays in the code for future refactor safety.
   - TestNew_RespectsConfiguredLevel: 4 cases verifying the level
     wired from Config is actually honored by the returned slog
     logger (Enabled(Debug/Info/Warn/Error) per level).

2. internal/httpapi/middleware/clientip_test.go (new)
   - TestClientIP: 11 cases covering both the X-Forwarded-For
     path and the RemoteAddr fallback. IPv6 edge cases are the
     primary motivation: XFF single/multi IPv6, RemoteAddr
     bracketed IPv6, and bare IPv6 without port that falls
     through to the raw r.RemoteAddr. The Fly.io production
     smoke test already observed real-world IPv6 clients; these
     unit tests now guard the invariant so a refactor does not
     silently break rate_limit keying for IPv6 users.

3. internal/httpapi/middleware/requestid.go / requestid_test.go
   - Tiny refactor: the crypto/rand.Reader call is indirected
     through a package-level randSource io.Reader variable so a
     test can swap it out. rand.Reader's declared type is
     io.Reader, so type inference keeps the declaration minimal
     (var randSource = rand.Reader).
   - New TestNewRequestID_FallsBackToSentinelOnReadFailure swaps
     randSource for an errorReader and verifies newRequestID
     returns the documented "00000000000000000000000000"
     sentinel instead of panicking or returning an empty string.
     This closes a P1 from the review — the failure path was
     previously uncovered because crypto/rand.Reader essentially
     never fails on Linux.

4. internal/app/app.go / app_chain_test.go
   - Refactor: extract newWithHandlers as an internal test seam.
     New() now delegates to it with production usecases
     (usecase.NewAnalyzer / usecase.NewPrompter). Middleware
     chain, routing, and server setup are unchanged.
   - Add stubAnalyzeUsecase and stubPromptUsecase implementing
     the handler-side interfaces so tests can substitute fakes
     without reaching pkg/prism.
   - TestChain_AnalyzeEndpointSuccess: full POST /v1/analyze
     through the real middleware chain with a canned prism.Result.
     Verifies status 200, X-Request-Id header, and the expected
     {"result": {pull_request, analysis}} envelope.
   - TestChain_PromptEndpointSuccess: parallel test for
     /v1/prompt with explicit mode/language in the request body
     and a canned prompt string response.
   - TestChain_ConcurrencyLimitRejectsBeyondCapacity: cap the
     server at 1 in-flight request, hold the first in the stub
     via a release channel, and verify the second request gets
     503 service_unavailable via the concurrency_limit
     middleware. This closes the P0 E2E gap from PR 6 which
     shipped without this case.

Remaining deferred items from the review (to be picked up by
subsequent v0.2.0 cleanup PRs):
  - Shared writePrismError helper to dedupe
    writeAnalyzeUsecaseError / writePromptUsecaseError
  - Map context.DeadlineExceeded to response.CodeTimeout in the
    handler error mapping (currently surfaces as 500), plus an
    E2E timeout integration test once the fix lands.
  - README + apiVersion bump + v0.2.0 tag
Address the PR 17 review nit: the "UC" abbreviation in
analyzeUC / promptUC was cute but inconsistent with Go
convention (stdlib and most Go codebases spell out acronyms in
parameter names unless they are well-known like URL/HTTP/ID).
Use analyzeUsecase / promptUsecase for consistency with the
handler package's AnalyzeUsecase / PromptUsecase interface
names.

No behavior change; the symbol is a package-private helper
parameter so the rename is purely cosmetic.
@hidetzu hidetzu merged commit ce74b42 into main Apr 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant