Skip to content

Harden tests, deduplicate cert/key file writes, and update CI/linting#20

Merged
madflojo merged 3 commits into
mainfrom
codex/harden-tests-and-enhance-library-safety
Jun 7, 2026
Merged

Harden tests, deduplicate cert/key file writes, and update CI/linting#20
madflojo merged 3 commits into
mainfrom
codex/harden-tests-and-enhance-library-safety

Conversation

@madflojo

@madflojo madflojo commented May 11, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Eliminate race and flakiness in the integration-style TLS tests by removing fixed ports, readiness sleeps, and cross-goroutine error sharing.
  • Reduce duplication and improve robustness of file output helpers while preserving public API and on-disk behavior.
  • Make examples, README, and CI tooling reproducible and up-to-date with current action versions and a pinned linter config.

Description

  • Hardened full-flow TLS tests to bind a listener on 127.0.0.1:0, serve with ServeTLS on the pre-created listener, avoid shared err state, and route client connections to the listener via a custom DialContext while preserving TLS host/SAN validation and IPv6 bracket handling.
  • Replaced readiness time.After sleep with listener-driven serve and a buffered server error channel to synchronize lifecycle and capture serve errors.
  • Deduplicated CA/KeyPair file writes into a shared writePairToFiles helper, introduced fileMode constant (keeps 0640), and corrected key-write error text to report key file failures.
  • Added defensive guards to PEM getters (PrivateKey/PublicKey) and made ConfigureTLSConfig accept nil input safely.
  • Fixed ExampleNewCA to return on intermediate errors and to close response body to avoid nil derefs and errcheck failures.
  • Made permission-negative tests skip when running as root on non-Windows hosts to avoid false negatives.
  • Updated README examples (GenerateCertsToTempFile, proper error checks, TLS config handling).
  • Updated GitHub Actions to newer major versions (setup-go@v5, checkout@v4, codecov@v5) and pinned golangci-lint usage plus added a checked-in .golangci.yml.
  • Minor lint/comment fix: renamed IPAddresses doc to IPNetAddresses to satisfy revive.

Testing

  • Ran go test ./... and all unit tests passed.
  • Ran go vet ./... with no issues.
  • Ran go test -race ./... and the race-sensitive tests now pass.
  • Ran make tests (coverage + race) which completed successfully and reported coverage 81.5%.
  • Ran make lint (golangci-lint) and no issues were reported.

Codex Task

Summary by CodeRabbit

  • Chores

    • Updated CI workflows and lint action versions and pinned linter for more reliable checks and coverage uploads.
  • Refactor

    • Centralized certificate/key file handling, added PEM validation and safer TLS config handling.
  • Documentation

    • Improved README examples for certificate generation and HTTPS setup with clearer error handling and flow.
  • Tests

    • Strengthened tests with platform-aware permission guards, added error-case coverage, dynamic server listeners, and more robust integration flows.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c490780-0485-41c3-a454-37d91c7050b2

📥 Commits

Reviewing files that changed from the base of the PR and between ee7d1d3 and 9b2761a.

📒 Files selected for processing (4)
  • gencerts_test.go
  • kpconfig.go
  • testcerts.go
  • testcerts_test.go
✅ Files skipped from review due to trivial changes (1)
  • gencerts_test.go

📝 Walkthrough

Walkthrough

Bumps CI action versions and golangci-lint; centralizes file-write behavior and nil-safety in testcerts core; modernizes integration tests to use dynamic loopback listeners and ServeTLS with explicit shutdown; updates README examples and a GoDoc comment.

Changes

Testcerts Library Improvements and Infrastructure

Layer / File(s) Summary
CI/CD Infrastructure Updates
.github/workflows/go.yaml, .github/workflows/lint.yml, .golangci.yml
actions/checkout -> v4, actions/setup-go -> v5, codecov/codecov-action -> v5; golangci-lint action -> v8 and linter pinned to v2.1.6; .golangci.yml sets version: "2" and enables misspell, revive.
Testcerts Core Robustness Improvements
testcerts.go
Add fileMode constant, PEM validation error variables, writePairToFiles helper; make CertificateAuthority/KeyPair PrivateKey/PublicKey nil-safe; refactor ToFile/ToTempFile to use helper and adjust key temp-file error returns; allow ConfigureTLSConfig(nil).
Documentation and Method Comments
kpconfig.go, README.md
Fix KeyPairConfig.IPNetAddresses() GoDoc; switch README examples to GenerateCertsToTempFile and precompute tlsConfig, write certs before starting server; remove trailing EOF whitespace.
Platform-Aware Test Skipping
gencerts_test.go
Add runtime import and skip permission-related subtests when running as root on non-Windows platforms.
Integration Test Modernization
testcerts_test.go
TestFullFlow/ExampleNewCA use dynamic loopback listeners (127.0.0.1:0) and server.ServeTLS in a goroutine reporting via serverErrCh; client DialContext targets listener address; requests use http.NewRequest + client.Do with IPv6 bracketing; explicit server close and serve error handling; imports updated (context added, time removed); adds negative file-write and cleanup subtests.

Sequence Diagram

sequenceDiagram
  participant TestFullFlow
  participant LoopbackListener
  participant TLSServer
  participant HTTPClient
  TestFullFlow->>LoopbackListener: create listener on 127.0.0.1:0
  TestFullFlow->>TLSServer: start server.ServeTLS(listener) in goroutine (serverErrCh)
  TestFullFlow->>HTTPClient: build Transport with DialContext -> listener address
  HTTPClient->>LoopbackListener: DialContext connects
  LoopbackListener->>TLSServer: connection handed to ServeTLS
  TLSServer-->>HTTPClient: TLS handshake + response
  TestFullFlow->>TLSServer: server.Close()
  TLSServer-->>TestFullFlow: serverErrCh reports (allow http.ErrServerClosed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through CI and nightly runs,

Nil-guards tucked and file-modes spun,
Loopback listeners hummed in tune,
ServeTLS closed beneath the moon,
Docs and tests now snug as buns! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the three main changes: hardening tests, deduplicating cert/key file writes, and updating CI/linting tooling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/harden-tests-and-enhance-library-safety

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 62.85714% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.85%. Comparing base (51c9e04) to head (9b2761a).

Files with missing lines Patch % Lines
testcerts.go 62.85% 9 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #20      +/-   ##
==========================================
+ Coverage   70.81%   73.85%   +3.03%     
==========================================
  Files           3        3              
  Lines         257      218      -39     
==========================================
- Hits          182      161      -21     
+ Misses         51       30      -21     
- Partials       24       27       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@madflojo madflojo marked this pull request as ready for review May 16, 2026 18:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
testcerts_test.go (1)

617-655: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a pre-bound ephemeral listener in the example.

Line 617 still binds a fixed port, so this example can fail whenever 127.0.0.1:8443 is already occupied. After removing the readiness wait, Line 651 can also race server startup because ListenAndServeTLS performs its own bind and serve asynchronously. Reusing the net.Listen("tcp", "127.0.0.1:0") + ServeTLS pattern from TestFullFlow would make the example deterministic and avoid port-collision flakes.

Proposed fix
-	server := &http.Server{
-		Addr: "127.0.0.1:8443",
+	listener, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		fmt.Printf("Error creating listener - %s", err)
+		return
+	}
+	defer func() {
+		_ = listener.Close()
+	}()
+
+	server := &http.Server{
 		Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
 			if _, writeErr := w.Write([]byte("Hello, World!")); writeErr != nil {
 				fmt.Printf("Error writing response - %s", writeErr)
 			}
 		}),
 		TLSConfig: serverTLSConfig,
 	}
 	defer func() {
 		_ = server.Close()
 	}()
 
 	go func() {
-		// Start HTTP Listener
-		err = server.ListenAndServeTLS(cert.Name(), key.Name())
-		if err != nil && err != http.ErrServerClosed {
-			fmt.Printf("Listener returned error - %s", err)
+		if serveErr := server.ServeTLS(listener, cert.Name(), key.Name()); serveErr != nil && serveErr != http.ErrServerClosed {
+			fmt.Printf("Listener returned error - %s", serveErr)
 		}
 	}()
@@
 	client := &http.Client{
 		Transport: &http.Transport{
 			TLSClientConfig: clientTLSConfig,
+			DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
+				var d net.Dialer
+				return d.DialContext(ctx, network, listener.Addr().String())
+			},
 		},
 	}
 
 	// Make an HTTPS request
-	rsp, err := client.Get("https://localhost:8443")
+	rsp, err := client.Get("https://localhost")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@testcerts_test.go` around lines 617 - 655, Replace the hard-coded bind and
ListenAndServeTLS usage with a pre-bound ephemeral listener: create a
net.Listen("tcp","127.0.0.1:0") before starting the server, set server.TLSConfig
as before, then start the server using go server.ServeTLS(listener, cert.Name(),
key.Name()) (so the OS picks an available port) and defer server.Close();
finally construct the client URL from listener.Addr().String() (e.g.
"https://"+listener.Addr().String()) instead of "https://localhost:8443" so
client.Get targets the ephemeral port; keep certs.ConfigureTLSConfig and
ca.GenerateTLSConfig logic unchanged.
🧹 Nitpick comments (1)
README.md (1)

49-53: ⚡ Quick win

Avoid writing to outer err from the goroutine in the README example.

The goroutine mutates the outer err, which is a race-prone pattern in sample code. Prefer a local variable inside the goroutine.

Suggested tweak
 go func() {
-	err = http.ListenAndServeTLS("localhost:443", "/tmp/cert", "/tmp/key", someHandler)
-	if err != nil {
+	if serveErr := http.ListenAndServeTLS("localhost:443", "/tmp/cert", "/tmp/key", someHandler); serveErr != nil {
 		// do something
 	}
 }()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 49 - 53, The README example spawns a goroutine that
writes to the outer variable err, causing a race; update the goroutine to use a
local variable (e.g., localErr) for the result of
http.ListenAndServeTLS("localhost:443", "/tmp/cert", "/tmp/key", someHandler)
and handle/log localErr inside the goroutine instead of assigning to the outer
err so no shared mutable state is modified concurrently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@kpconfig.go`:
- Line 55: The doc comment for IPNetAddresses uses the incorrect type casing
"Net.IP"; update the comment to use the correct Go type name "net.IP" so it
matches the actual package/type and Go documentation conventions for the
IPNetAddresses function.

In `@testcerts.go`:
- Around line 229-243: The writePairToFiles function currently writes empty
cert/key files silently; update it to validate inputs before writing: check that
certData and keyData are non-empty and contain valid PEM blocks (e.g., parse
pem.Decode on certData and keyData and ensure returned block != nil), return a
clear error if validation fails, and then proceed to os.WriteFile; also ensure
CertificateAuthority.ToFile (which calls writePairToFiles with PublicKey() and
PrivateKey()) propagates these validation errors so callers don't get silent
success when PEM data is missing or invalid.

---

Outside diff comments:
In `@testcerts_test.go`:
- Around line 617-655: Replace the hard-coded bind and ListenAndServeTLS usage
with a pre-bound ephemeral listener: create a net.Listen("tcp","127.0.0.1:0")
before starting the server, set server.TLSConfig as before, then start the
server using go server.ServeTLS(listener, cert.Name(), key.Name()) (so the OS
picks an available port) and defer server.Close(); finally construct the client
URL from listener.Addr().String() (e.g. "https://"+listener.Addr().String())
instead of "https://localhost:8443" so client.Get targets the ephemeral port;
keep certs.ConfigureTLSConfig and ca.GenerateTLSConfig logic unchanged.

---

Nitpick comments:
In `@README.md`:
- Around line 49-53: The README example spawns a goroutine that writes to the
outer variable err, causing a race; update the goroutine to use a local variable
(e.g., localErr) for the result of http.ListenAndServeTLS("localhost:443",
"/tmp/cert", "/tmp/key", someHandler) and handle/log localErr inside the
goroutine instead of assigning to the outer err so no shared mutable state is
modified concurrently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5175527d-f4a0-4acb-9101-120851336cb9

📥 Commits

Reviewing files that changed from the base of the PR and between 51c9e04 and 2c6d256.

📒 Files selected for processing (8)
  • .github/workflows/go.yaml
  • .github/workflows/lint.yml
  • .golangci.yml
  • README.md
  • gencerts_test.go
  • kpconfig.go
  • testcerts.go
  • testcerts_test.go

Comment thread kpconfig.go Outdated
Comment thread testcerts.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens TLS integration tests against flakiness, deduplicates cert/key file-writing logic, tightens public-API nil safety, refreshes README/example correctness, and updates CI action versions while pinning the linter via a checked-in .golangci.yml.

Changes:

  • Test hardening: replace fixed port 0.0.0.0:8443 + 3s sleep with an ephemeral 127.0.0.1:0 listener, ServeTLS, buffered server error channel, and a custom DialContext (with IPv6 bracket handling) for client requests.
  • API/refactor: extract writePairToFiles + fileMode constant, fix the key-file error message, add nil guards on PrivateKey/PublicKey, and let ConfigureTLSConfig accept a nil *tls.Config.
  • CI/docs: bump setup-go@v5, checkout@v4, codecov@v5, pin golangci-lint-action@v8 to v2.1.6 with a new .golangci.yml, rename IPAddresses doc to IPNetAddresses, and fix README's GenerateCertsToTempFile name + ConfigureTLSConfig usage and example error handling.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
testcerts.go Adds fileMode constant, dedupes file writes via writePairToFiles, adds nil guards on PEM getters, and makes ConfigureTLSConfig tolerate a nil tls.Config.
testcerts_test.go Removes fixed listen address / readiness sleep, uses an ephemeral listener with ServeTLS, IPv6-aware URL host, server error channel, and cleans up the now-misnamed example.
README.md Corrects function name, error-check, and ConfigureTLSConfig multi-return usage in the example.
kpconfig.go Renames doc comment for IPNetAddresses to satisfy revive.
gencerts_test.go Skips permission negative tests when running as root on non-Windows hosts.
.golangci.yml New pinned linter config enabling misspell and revive with v2 format.
.github/workflows/lint.yml Bumps checkout to v4 and golangci-lint-action to v8 pinned to v2.1.6.
.github/workflows/go.yaml Bumps setup-go to v5, checkout to v4, and codecov-action to v5.

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

Comment thread testcerts_test.go Outdated
Comment on lines 623 to 645
@@ -630,7 +645,11 @@
rsp, err := client.Get("https://localhost:8443")
Comment thread testcerts_test.go
Comment on lines 623 to 629
go func() {
// Start HTTP Listener
err = server.ListenAndServeTLS(cert.Name(), key.Name())
if err != nil && err != http.ErrServerClosed {
fmt.Printf("Listener returned error - %s", err)
}
}()
Comment thread testcerts_test.go Outdated
Comment on lines +645 to +652
rsp, err := client.Get("https://localhost:8443")
if err != nil {
fmt.Printf("Client returned error - %s", err)
return
}
defer func() {
_ = rsp.Body.Close()
}()
Comment thread testcerts.go
Comment on lines +229 to 237
func writePairToFiles(certData []byte, certFile string, keyData []byte, keyFile string) error {
if err := os.WriteFile(certFile, certData, fileMode); err != nil {
return fmt.Errorf("unable to create certificate file - %w", err)
}

// Write Key
err = os.WriteFile(keyFile, ca.PrivateKey(), 0640)
if err != nil {
return fmt.Errorf("unable to create certificate file - %w", err)
if err := os.WriteFile(keyFile, keyData, fileMode); err != nil {
return fmt.Errorf("unable to create key file - %w", err)
}

return nil
}
Comment thread testcerts.go Outdated
Comment on lines +352 to +357
// ConfigureTLSConfig will configure the tls.Config with the KeyPair certificate and private key.
// The returned tls.Config can be used for a server or client.
func (kp *KeyPair) ConfigureTLSConfig(tlsConfig *tls.Config) (*tls.Config, error) {
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
Comment thread testcerts.go
Comment on lines 354 to 358
func (kp *KeyPair) ConfigureTLSConfig(tlsConfig *tls.Config) (*tls.Config, error) {
if tlsConfig == nil {
tlsConfig = &tls.Config{}
}
cert, err := tls.X509KeyPair(kp.PublicKey(), kp.PrivateKey())
Comment thread gencerts_test.go
@@ -109,6 +110,9 @@ func TestGeneratingCertsToFile(t *testing.T) {
})

t.Run("Testing the unhappy path for insufficient permissions", func(t *testing.T) {
Comment thread kpconfig.go Outdated
}

// IPAddresses returns a list of IP addresses in Net.IP format.
// IPNetAddresses returns a list of IP addresses in Net.IP format.
Validate cert/key PEM before writing files, clean up orphaned certs on key write failure, and make ExampleNewCA avoid listener races.
@madflojo

madflojo commented Jun 7, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@madflojo madflojo merged commit 208b98f into main Jun 7, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants