improve test coverage for cmd/api 40.5 -> 81#431
improve test coverage for cmd/api 40.5 -> 81#431aaronbrethorst merged 1 commit intoOneBusAway:mainfrom
Conversation
aaronbrethorst
left a comment
There was a problem hiding this comment.
Hey Bikram, thanks for taking the time to improve test coverage for cmd/api — going from 40.5% to 81% is a meaningful jump, and the TestRun_GracefulShutdown test in particular adds genuine value by being the only test that exercises the Run() function directly. Before we can merge this, I will need you to make a couple changes:
Critical
-
os.Exit(1)inside a test function will kill the entire test suite (cmd/api/app_test.go:549-553). Callingos.Exit(1)from test code terminates the entirego testprocess — no subsequent tests run, nodefercleanup executes, and no coverage data is written. This is especially ironic for a PR whose purpose is improving coverage. Replace withrequire.NoError:// Instead of the os.Exit(1) block: jsonConfig, err := appconf.LoadFromFile("../../config.example.json") require.NoError(t, err, "failed to load config.example.json")
Important
-
Unchecked
os.Pipe()error (cmd/api/app_test.go:577). Ifos.Pipe()fails (e.g., file descriptor exhaustion in CI), bothrandwarenil, andos.Stdout = wwould cause a nil-pointer panic insidedumpConfigJSON. Check the error:r, w, err := os.Pipe() require.NoError(t, err, "failed to create OS pipe")
-
Stdout restoration should use
defer(cmd/api/app_test.go:576-583). IfdumpConfigJSONpanics (note: the production code atapp.go:254contains its ownos.Exit(1)on marshal failure),os.Stdoutis never restored. Wrap the restoration in adefer:oldStdout := os.Stdout defer func() { os.Stdout = oldStdout }()
-
Unsafe type assertions will panic instead of producing useful test failures (
cmd/api/app_test.go:597-622). Every bare.(float64),.(string),.(map[string]interface{})assertion will panic with an unhelpful stack trace if the JSON structure changes. Use testify assertions instead, which is also consistent with the rest of the file:// Instead of: if parsed["port"].(float64) != 4000 { t.Fatalf("expected port 4000") } // Use: assert.Equal(t, float64(cfg.Port), parsed["port"]) assert.Equal(t, "development", parsed["env"])
-
Assertions are hardcoded against
config.example.jsonvalues (cmd/api/app_test.go:597-624). If anyone updates the example config (e.g., switches transit agencies), this test breaks for reasons unrelated todumpConfigJSON. Assert against the values you loaded from the config instead of hardcoded strings:assert.Equal(t, float64(cfg.Port), parsed["port"]) assert.Equal(t, gtfsCfg.GtfsURL, staticFeed["url"]) assert.Equal(t, gtfsCfg.GTFSDataPath, parsed["data-path"])
-
No test coverage for auth header redaction — the
dumpConfigJSONfunction contains security-sensitive logic that replaces auth header values with***REDACTED***(app.go:207-209,app.go:234-236). Sinceconfig.example.jsonhas empty auth values, this path is never exercised. Consider adding a test case with populated auth credentials that verifies the secret does not appear in the output.
Fit and Finish
-
Missing trailing newline at end of file (
cmd/api/app_test.go:625). The diff shows\ No newline at end of file. Add a newline after the final}. -
Extra blank line after function signature (
cmd/api/app_test.go:546) and double blank line (cmd/api/app_test.go:595-596). Minor style cleanup. -
Use testify consistently — the test uses raw
if/t.Fatalffor assertions while the rest of the file usesassert.Equal/require.NoError. Switching to testify would make the style consistent. -
Field-by-field struct copy (
cmd/api/app_test.go:560-573) — theGtfsConfigDatatogtfs.Configconversion is duplicated in three places (main.go:67-80,app_test.go:484-494, and this new test). This is an existing issue in the codebase, not something you introduced, but if you're motivated, extracting a helper function incmd/api/app.gowould DRY up all three call sites. (There's an import cycle that prevents putting it inappconf, which is why it's done this way.)
Thanks again, and I look forward to merging this change.
|
Thanks for the reviews @aaronbrethorst . I have made the changes and also added the helper functions. Could you please review these changes. Thanks. Also, what are your thoughts on having a coverage reports GitHub action? |
aaronbrethorst
left a comment
There was a problem hiding this comment.
Hey Bikram, good progress — you've knocked out the critical os.Exit issue, the pipe error handling, the defer for stdout restoration, the trailing newline, and the gtfsConfigFromData helper (which cleans up three call sites nicely). A few items from the previous round still need attention before we can merge:
Important
1. Unsafe type assertions still present (app_test.go:572, 575, 578)
This was item #4 from the last review. Bare type assertions like parsed["gtfs-static-feed"].(map[string]interface{}) will panic with an unhelpful stack trace if the JSON structure changes. Use two-value form or testify:
// Instead of:
staticFeed := parsed["gtfs-static-feed"].(map[string]interface{})
// Use:
staticFeed, ok := parsed["gtfs-static-feed"].(map[string]interface{})
require.True(t, ok, "gtfs-static-feed should be a map")Same for parsed["gtfs-rt-feeds"].([]interface{}) on line 575 and feeds[0].(map[string]interface{}) on line 578.
2. No test for auth header redaction (app.go:227-229, 253-255)
This was item #6 from the last review. The dumpConfigJSON function contains security-sensitive logic that replaces auth header values with ***REDACTED***. Since config.example.json has empty auth values, this code path is never exercised by the test. Add a test case that populates auth credentials and verifies the secret doesn't appear in the output:
func TestDumpConfigJSON_RedactsAuthHeaders(t *testing.T) {
cfg := appconf.Config{Port: 4000, Env: appconf.Development}
gtfsCfg := gtfs.Config{
GtfsURL: "https://example.com/gtfs.zip",
StaticAuthHeaderKey: "Authorization",
StaticAuthHeaderValue: "Bearer secret-token-123",
RealTimeAuthHeaderKey: "X-API-Key",
RealTimeAuthHeaderValue: "my-secret-api-key",
TripUpdatesURL: "https://example.com/trips.pb",
}
// capture stdout, call dumpConfigJSON, parse output...
assert.NotContains(t, output, "secret-token-123")
assert.NotContains(t, output, "my-secret-api-key")
assert.Contains(t, output, "***REDACTED***")
}3. Rebase and squash into a single commit
The branch has 4 commits including a merge commit:
40333d1 Merge remote-tracking branch 'upstream/main'
c05b991 feat: private helper function: config from data
334ab5c feat: use testify instead of if/fataf, use config rather than hardcoded
ec73446 feat: improve test coverage for cmd/api 40.5 -> 81
Please rebase onto main and squash into a single commit. This keeps the git history clean. If you'd like a refresher on rebasing, I wrote a blog post that walks through it step by step: https://www.brethorsting.com/blog/2026/01/git-rebase-for-the-terrified/
Fit and Finish
4. gofmt formatting issues (all 3 files)
Please run go fmt ./cmd/api/... before committing. Specific issues:
app.go:42: Double space inreturn gtfsCfgapp_test.go:565: Trailing tab on blank lineapp_test.go:581: Extra space before comma ingtfsCfg.GTFSDataPath , parsed[...]main.go:68: Trailing tab on blank line
5. Hardcoded "development" assertion (app_test.go:570)
This was part of item #5 from the last review. The env assertion is still hardcoded. Since cfg.Env is an enum, you could derive the expected string the same way dumpConfigJSON does, or simply omit this assertion — the port and URL assertions already confirm the config round-tripped correctly.
Thanks again, and I look forward to merging this change.
|
Sorry, had to rebase and repush all the changes. Incorporated the reviews. |
aaronbrethorst
left a comment
There was a problem hiding this comment.
Hey Bikram, you've made strong progress here -- the auth header redaction test, the safe type assertions, the gtfsConfigFromData helper, and the defer for stdout restoration are all good. A couple of items from the last round still need attention:
Prior Feedback Status
| # | Round | Item | Status |
|---|---|---|---|
| 1 | R1 | os.Exit(1) in test kills the suite |
Fixed -- replaced with require.NoError |
| 2 | R1 | Unchecked os.Pipe() error |
Fixed -- require.NoError(t, err, "Failed to create OS Pipe") |
| 3 | R1 | Stdout restoration with defer |
Fixed -- defer func() { os.Stdout = oldStdout }() |
| 4 | R1/R2 | Unsafe type assertions | Partially fixed -- uses two-value form, but see item #1 below |
| 5 | R1/R2 | Hardcoded config values | Fixed -- assertions use cfg.Port, gtfsCfg.GtfsURL, etc. |
| 6 | R1/R2 | Auth header redaction test | Fixed -- tests realtime auth redaction with ***REDACTED*** check |
| 7 | R1 | Missing trailing newline | Fixed |
| 8 | R1 | Extra blank lines | Fixed |
| 9 | R1 | Use testify consistently | Fixed |
| 10 | R1 | gtfsConfigFromData helper |
Fixed -- cleans up 3 call sites |
| 11 | R2 | gofmt formatting issues |
Fixed -- gofmt -d shows no differences |
| 12 | R2 | Hardcoded "development" assertion |
Fixed -- removed |
| 13 | R2 | Rebase and squash | Not fixed -- see item #2 below |
Blocking
1. Use require.True instead of assert.True for type assertions (app_test.go:577, 580, 583)
The two-value type assertion form from my prior review is correct, but assert.True continues the test on failure. Since the very next line accesses the asserted value (e.g., staticFeed["url"]), a failed assertion means staticFeed is nil, which causes a panic -- the exact problem we were trying to avoid. Use require.True to stop the test gracefully:
// Instead of:
staticFeed, ok := parsed["gtfs-static-feed"].(map[string]interface{})
assert.True(t, ok, "gtfs-static-feed should be a map")
assert.Equal(t, gtfsCfg.GtfsURL, staticFeed["url"]) // panics if ok is false
// Use:
staticFeed, ok := parsed["gtfs-static-feed"].(map[string]interface{})
require.True(t, ok, "gtfs-static-feed should be a map")
assert.Equal(t, gtfsCfg.GtfsURL, staticFeed["url"]) // safe: test stopped if ok was falseSame change needed for feeds (line 580) and rtFeed (line 583).
2. Rebase and squash into a single commit
This was item #3 from my last review. The branch currently has 4 commits:
fad359b feat: improve test coverage for cmd/api 40.5 -> 81
2a6001c feat: use testify instead of if/fataf, use config rather than hardcoded
0cb068a feat: private helper function: config from data
9efbb43 feat: add test for type allocation, auth header redaction, rm develop…
Please squash these into a single commit. If you'd like a refresher, see: https://www.brethorsting.com/blog/2026/01/git-rebase-for-the-terrified/
3. CI checks not running
Only the CLA check is visible -- the lint and test workflows haven't triggered. This may require a maintainer to approve the workflow run for your fork, but please verify that tests pass locally before pushing:
make test && make lintFit and Finish
4. Typo in assertion message (app_test.go:580)
// Before:
assert.True(t, ok, "gtfs-rt-feeds shoulde be an array of maps")
// After:
require.True(t, ok, "gtfs-rt-feeds should be an array of maps")Strengths
- Auth header redaction test: Setting
RealTimeAuthHeaderValueto a known secret, then asserting it's replaced with***REDACTED***is exactly the right approach. This exercises security-sensitive code that was previously untested. gtfsConfigFromDatahelper: Cleans up three call sites and prevents future drift when new fields are added to the GTFS config.TestRun_GracefulShutdown: The only test that exercises theRun()function directly. TheerrCh+time.Afterpattern is clean.- Good use of
deferfor stdout restoration: Protects against panics indumpConfigJSON.
Recommended Action
Fix items #1-4, squash, and push. Once CI is green, this is ready to merge.
aaronbrethorst
left a comment
There was a problem hiding this comment.
Hey Bikram, nice work getting all the prior feedback addressed -- the squashed commit, require.True for safe type assertions, the typo fix, and clean formatting are all spot on. This is ready to merge.
Prior Feedback Status
| # | Round | Item | Status |
|---|---|---|---|
| 1 | R1 | os.Exit(1) in test kills the suite |
Fixed |
| 2 | R1 | Unchecked os.Pipe() error |
Fixed |
| 3 | R1 | Stdout restoration with defer |
Fixed |
| 4 | R1/R2/R3 | Unsafe type assertions | Fixed -- uses two-value form with require.True |
| 5 | R1/R2 | Hardcoded config values | Fixed -- asserts against loaded config values |
| 6 | R1/R2 | Auth header redaction test | Fixed -- realtime auth tested with ***REDACTED*** check |
| 7 | R1 | Missing trailing newline | Fixed |
| 8 | R1 | Extra blank lines | Fixed |
| 9 | R1 | Use testify consistently | Fixed |
| 10 | R1 | gtfsConfigFromData helper |
Fixed -- cleans up 3 call sites |
| 11 | R2 | gofmt formatting issues |
Fixed -- gofmt -d shows no differences |
| 12 | R2 | Hardcoded "development" assertion |
Fixed -- removed |
| 13 | R2/R3 | Rebase and squash | Fixed -- single commit |
| 14 | R3 | require.True instead of assert.True |
Fixed |
| 15 | R3 | Typo "shoulde" | Fixed |
Verification
make test: all packages passmake lint: 0 issuesgofmt -d ./cmd/api/: no formatting differences- Single squashed commit:
0b79701 feat: improve test coverage for cmd/api 40.5 -> 81
Strengths
gtfsConfigFromDatahelper: Consolidates three identical struct constructions into one function. As a bonus, the test version was previously missingStaticAuthHeaderKey,StaticAuthHeaderValue, andEnableGTFSTidy-- using the helper silently fixes that gap.TestRun_GracefulShutdown: Clean test of theRun()function with theerrCh+time.Afterpattern. Lightweight and focused.TestDumpConfigJSON_WithExampleFile: Good coverage of JSON output formatting, config round-tripping, and auth header redaction. Thedeferfor stdout restoration andrequire.Truefor type assertions make this robust.- Persistence: Four rounds of review with steady, thorough improvements each time. Well done.
Add tests for cmd/api to include tests to cover the jsonDumps function and check graceful shutdown as well