# CIIMS Proxy — Test Coverage Fix Plan ## Objective Close all test coverage gaps identified in the codebase analysis. The `internal` package has decent coverage (20 tests) but `cmd/main/main.go` has **zero tests** across 12 functions. This plan adds 49 new tests organized in 4 phases. --- ## Phase 1: `internal` Package — Remaining Gaps (8 tests) ### Objective Cover the few untested paths in the already well-tested `internal` package. ### Implementation Plan - [x] 1.1 `TestNewClient_ZeroTimeout` — Call `NewClient(0)`, assert error returned and message contains "positive". Also test `NewClient(-1)`. **Rationale:** The error path for invalid timeout values is untested. `loadConfig` depends on this to fail fast. - [x] 1.2 `TestNewClient_ValidTimeout` — Call `NewClient(30)`, assert no error and the returned client is non-nil. **Rationale:** Only the success path for `NewClient` is exercised implicitly by other tests; a direct test is cleaner. - [x] 1.3 `TestHTTPStatusError_Truncation` — Create `HTTPStatusError` with a body of 600 'x' characters. Assert `Error()` returns a string ending in `"...` and not exceeding ~520 characters (512 + "ciims returned ...: " prefix). **Rationale:** The truncation logic at `http.go:38-40` is untested and could silently break. - [x] 1.4 `TestHTTPStatusError_EmptyBody` — Create `HTTPStatusError` with empty body. Assert `Error()` returns `"ciims returned "` without a colon. **Rationale:** The empty-body branch at `http.go:41-43` is untested. - [x] 1.5 `TestResponseTooLargeError_Error` — Create `ResponseTooLargeError{Limit: 100}`. Assert `Error()` returns `"ciims response exceeds 100 bytes"`. **Rationale:** Simple error type with no existing direct test. - [x] 1.6 `TestCIIMSResponse_NilReceiver` — Call `(*CIIMSResponse)(nil).IsFault()`, `.ErrorMessage()`, `.Messages()`. Assert they return `false`, `""`, and `nil` respectively without panicking. **Rationale:** The nil-guard branches at `http.go:63,67,74` are untested. - [x] 1.7 `TestClientSend_SOAPFaultWithHTTP500` — Mock server returns HTTP 500 with a SOAP fault body. Assert `err` is `*HTTPStatusError`, `resp.IsFault()` is true, and `resp.ErrorMessage()` returns the fault text. **Rationale:** `handleSendError` in `main.go` has a code path that extracts SOAP faults from `HTTPStatusError.Body` — this scenario must be tested at the transport layer first. - [x] 1.8 `TestXmlElementTexts_MalformedXML` — Call `xmlElementTexts` with truncated XML like `"incomplete"`. Assert it returns gracefully (empty slice) without panicking. **Rationale:** The `decoder.Token()` error path at `codec.go:136-138` is untested. --- ## Phase 2: `cmd/main` — Pure Functions (19 tests) ### Objective Test all functions in `main.go` that have no Gin dependency: configuration loading, URL parsing, environment parsing, and error handling helpers. ### Implementation Plan - [x] 2.1 `TestNormalizeBaseURL_ValidHTTP` — Input `"http://example.com/path/"`, assert returns `"http://example.com/path"` (trailing slash stripped, scheme lowercased). **Rationale:** Happy path for the URL normalization function. - [x] 2.2 `TestNormalizeBaseURL_ValidHTTPS` — Input `"https://EXAMPLE.COM:8443"`, assert host lowercased, scheme lowercased. **Rationale:** Verifies case normalization and port preservation. - [x] 2.3 `TestNormalizeBaseURL_Empty` — Input `""`, assert error contains "must not be empty". **Rationale:** Empty input validation. - [x] 2.4 `TestNormalizeBaseURL_WhitespaceOnly` — Input `" "`, assert error (after TrimSpace, it's empty). **Rationale:** Edge case for whitespace handling. - [x] 2.5 `TestNormalizeBaseURL_InvalidScheme` — Input `"ftp://example.com"`, assert error contains "scheme must be http or https". **Rationale:** Scheme allowlist enforcement. - [x] 2.6 `TestNormalizeBaseURL_MissingHost` — Input `"http:///path"`, assert error contains "host is required". **Rationale:** `url.Parse` on `http:///path` sets Host to empty. - [x] 2.7 `TestNormalizeBaseURL_QueryNotAllowed` — Input `"http://example.com?a=1"`, assert error contains "query and fragment are not allowed". **Rationale:** Query/fragment rejection prevents URL smuggling. - [x] 2.8 `TestNormalizeBaseURL_FragmentNotAllowed` — Input `"http://example.com#section"`, assert error. **Rationale:** Same as above for fragments. - [x] 2.9 `TestNormalizeBaseURL_InvalidURL` — Input `"://bad"`, assert error (from `url.Parse`). **Rationale:** Tests the `url.Parse` error path. - [x] 2.10 `TestParseAllowedServers_Empty` — Input `""`, assert returns nil slice and nil error. **Rationale:** Empty env var is valid (means "no additional servers"). - [x] 2.11 `TestParseAllowedServers_WhitespaceOnly` — Input `" , "`, assert error (whitespace-only entries become empty and are rejected by `normalizeBaseURL`). **Rationale:** Edge case for the split logic. - [x] 2.12 `TestParseAllowedServers_ValidSingle` — Input `"http://other.example.com"`, assert returns `["http://other.example.com"]`. **Rationale:** Happy path for single additional server. - [x] 2.13 `TestParseAllowedServers_ValidMultiple` — Input `"http://a.com,https://b.com:8443"`, assert both are normalized and returned. **Rationale:** Multi-server allowlist parsing. - [x] 2.14 `TestParseAllowedServers_InvalidEntry` — Input `"http://good.com,ftp://bad.com"`, assert error references the bad entry and the env var name. **Rationale:** Error propagation with context. - [x] 2.15 `TestGetIntEnv_Unset` — Unset the env var, assert returns `(0, nil)`. **Rationale:** Default behavior when env var is absent. - [x] 2.16 `TestGetIntEnv_Valid` — Set env var to `"30"`, assert returns `(30, nil)`. **Rationale:** Happy path. - [x] 2.17 `TestGetIntEnv_NotAnInteger` — Set env var to `"abc"`, assert error contains "must be an integer". **Rationale:** Parse error path. - [x] 2.18 `TestGetIntEnv_Negative` — Set env var to `"-5"`, assert error contains "must be positive". **Rationale:** Negative value rejection (new validation in current code). - [x] 2.19 `TestGetIntEnv_Zero` — Set env var to `"0"`, assert error contains "must be positive". **Rationale:** Zero value rejection.]]> --- ## Phase 3: `cmd/main` — Handler-Level Tests (16 tests) ### Objective Test the Gin HTTP handlers end-to-end using `httptest.NewServer` + a mock CIIMS backend. This is the most critical gap. ### Implementation Plan - [x] 3.1 `TestSendSuccess` — POST to `/send` with valid JSON. Mock CIIMS returns `testSendOK`. Assert 200, `{"error":""}`. **Rationale:** Core happy path for the send endpoint. - [x] 3.2 `TestSendSOAPFault` — POST to `/send` with valid JSON. Mock CIIMS returns `testErrMsg`. Assert 500, response contains the fault message. **Rationale:** SOAP fault handling in the handler (was the original nil-pointer bug). - [x] 3.3 `TestSendNetworkError` — POST to `/send` with valid JSON. Mock CIIMS is closed before the request. Assert 500, response contains error. **Rationale:** Network error path through `handleSendError`. - [x] 3.4 `TestHTTP500WithoutSOAPFaultReturnsStatusError` — POST to `/send` with valid JSON. Mock CIIMS returns HTTP 500 with plain text body. Assert 500, response contains the status error. **Rationale:** HTTP error path through `handleSendError` (non-SOAP-fault branch). - [x] 3.5 `TestHTTP500WithSOAPFaultReturnsFaultText` — POST to `/send` with valid JSON. Mock CIIMS returns HTTP 500 with a SOAP fault body. Assert 500, response contains the extracted fault message (not the raw HTTP status). **Rationale:** `handleSendError` has a branch that extracts SOAP faults from `HTTPStatusError.Body` — this is the most complex error path. - [x] 3.6 `TestSendMissingRequiredField` — POST to `/send` with `{"user":"x"}` (missing pass, event, msg). Assert 400. **Rationale:** Gin binding validation. - [x] 3.7 `TestSendOversizedBody` — POST to `/send` with body > 1MB. Assert 413. **Rationale:** `MaxBytesReader` enforcement. - [x] 3.8 `TestSendInvalidJSON` — POST to `/send` with `not json`. Assert 400. **Rationale:** Malformed JSON handling. - [x] 3.9 `TestAllowedRequestURLSucceeds` — POST to `/send` with `{"url":"http://allowed.example.com",...}` where the URL is in `AllowedServers`. Assert 200 (request proxied). **Rationale:** Allowlist pass-through. - [x] 3.10 `TestDisallowedRequestURLReturns400AndDoesNotCallBackend` — POST to `/send` with `{"url":"http://evil.com",...}`. Assert 400, error contains "not allowed", and backend is never called. **Rationale:** Allowlist rejection. - [x] 3.11 `TestReceiveSuccess` — POST to `/receive` with valid JSON. Mock CIIMS returns `testReceiveResp`. Assert 200, response contains messages. **Rationale:** Core happy path for receive. - [x] 3.12 `TestReceiveSOAPFault` — POST to `/receive` with valid JSON. Mock CIIMS returns a SOAP fault. Assert 500. **Rationale:** SOAP fault handling in receive (was previously missing — now fixed but untested). - [x] 3.13 `TestReceiveInvalidCount` — POST to `/receive` with `"count":0`. Assert 400, error contains "between 1 and". Also test count=1001. **Rationale:** Count lower-bound and upper-bound validation. - [x] 3.14 (merged with 3.13) — Already covered by `TestReceiveInvalidCount`. - [x] 3.15 `TestReceiveCountValidBoundary` — POST to `/receive` with `"count":1000`. Assert 200 (boundary value). **Rationale:** Maximum allowed count should succeed. - [x] 3.16 `TestPing` — GET `/ping`. Assert 200, `{"message":"pong"}`. **Rationale:** Health check endpoint. --- ## Phase 4: `cmd/main` — Config & Router Tests (8 tests) ### Objective Test `loadConfig`, `newServer`, and `newRouter` with environment variable manipulation. ### Implementation Plan - [x] 4.1 `TestLoadConfig_Minimal` — Set only `CIIMS_SERVER=http://example.com`, unset others. Assert defaults: Listen=`":9090"`, Timeout=240, AllowedServers contains only the server URL. **Rationale:** Default configuration path. - [x] 4.2 `TestLoadConfig_Full` — Set all env vars. Assert all values are parsed correctly including `AllowedServers`. **Rationale:** Full configuration path. - [x] 4.3 `TestLoadConfigRequiresCIIMSServer` — Set `CIIMS_SERVER=`. Assert error contains "CIIMS_SERVER". **Rationale:** Configuration validation propagates errors. - [x] 4.4 `TestLoadConfigRejectsInvalidCIIMSServer` — Set `CIIMS_SERVER=ftp://bad`. Assert error contains "scheme". **Rationale:** Invalid URL causes load failure. - [x] 4.5 `TestLoadConfigRejectsNonPositiveTimeout` — Set `CIIMS_TIMEOUT=0`. Assert error contains "positive". **Rationale:** Zero timeout rejection propagates through `NewClient`. - [x] 4.6 `TestLoadConfigRejectsInvalidAllowedServer` — Set `CIIMS_ALLOWED_SERVERS=http://bad.example.com?x=1`. Assert error. **Rationale:** Invalid allowed server entry causes load failure. - [x] 4.7 `TestNewServerTimeouts` and `TestNewServerConfig` — Call `newServer(config)`. Assert `ReadHeaderTimeout`, `ReadTimeout`, `IdleTimeout` are set, `Addr` matches config. **Rationale:** Server construction verification. - [x] 4.8 `TestNewRouter_RoutesExist` — Call `newRouter(config)`. Use `httptest.NewServer` with the Gin engine. Assert GET `/ping`, POST `/send`, POST `/receive` all return non-404. **Rationale:** Route registration verification. ### Additional Tests (beyond original plan) - [x] `TestDefaultURLUsesCIIMSServer` — Verifies the service prefix is appended to the default URL. - [x] `TestRequestURLWithQueryReturns400` — Verifies URLs with query params are rejected. - [x] `TestLoadConfigAllowedServers` — Verifies case normalization and multiple allowed servers. - [x] `TestLoadConfig_InvalidTimeout` — Verifies non-integer timeout produces error. --- ## Verification Criteria - All 49 new tests pass with `go test ./... -count=1` - `go vet ./...` reports no issues - Handler tests use `t.Setenv()` to avoid cross-test pollution - Handler tests create a fresh `App` + mock CIIMS `httptest.Server` per sub-test - `internal` tests remain in `package internal` - `cmd/main` tests go in `cmd/main/main_test.go` (package `main`) - No test depends on external network access --- ## Potential Risks and Mitigations 1. **Environment variable pollution between tests** Mitigation: Use `t.Setenv()` which automatically restores the original value after the test. For `loadConfig` tests, set all relevant vars in each test case. 2. **Gin mode pollution (debug vs release logging)** Mitigation: Call `gin.SetMode(gin.TestMode)` in `TestMain` or at the top of each test function. 3. **Port conflicts in parallel handler tests** Mitigation: Use `httptest.NewServer` (which allocates a random port) for both the proxy and the mock CIIMS backend. Do not use `t.Parallel()` for handler tests that share mock servers. 4. **`getIntEnv` tests need `os.Setenv`** Mitigation: Use `t.Setenv()` — this is the standard approach since Go 1.17. 5. **`loadConfig` reads multiple env vars** Mitigation: Set all env vars explicitly in each test case to avoid inheriting values from the test environment. --- ## Alternative Approaches 1. **Table-driven tests for handlers**: Group all send/receive scenarios into `[]struct{name, body, mockResponse, mockStatus, wantStatus, wantBody}` tables. This reduces boilerplate but makes individual test failure messages less clear. **Recommendation**: Use table-driven for `normalizeBaseURL` and `getIntEnv` (pure functions), keep handler tests as individual functions for clarity. 2. **Refactor handlers to accept `http.Handler` interface**: Extract handler logic from Gin to standard `http.Handler` for easier testing. **Recommendation**: Not necessary — `httptest.NewServer` with Gin works fine and tests the actual routing layer. 3. **Use `httptest.NewRecorder` directly on Gin**: Call `router.ServeHTTP(w, req)` instead of `httptest.NewServer`. **Recommendation**: Use `httptest.NewServer` for handler tests because `sendMessage` needs a real TCP connection to the mock CIIMS backend (it calls `client.Send` with a URL).