Compare commits

...

4 Commits

Author SHA1 Message Date
lukaszraczylo d945e4915d Update go.mod and go.sum (#47) 2026-02-17 03:54:37 +00:00
lukaszraczylo e50f73ec92 chore: add golangci-lint v2 config and fix linter warnings (#46)
- [x] Add golangci-lint v2 configuration with formatters section
- [x] Reorganize linters-settings under linters section
- [x] Replace if-else chains with switch statements for clarity
- [x] Wrap all ignored error returns with `_ = ` pattern
- [x] Add OSC 8 hyperlink helper function for clickable ports
- [x] Add blank line in table styling function
- [x] Remove unnecessary type assertion in test
2026-02-13 18:46:27 +00:00
lukaszraczylo d3c5e5eb36 Update go.mod and go.sum (#45) 2026-02-13 03:57:21 +00:00
lukaszraczylo 34e6fc60da Update go.mod and go.sum (#44) 2026-02-11 04:03:09 +00:00
19 changed files with 83 additions and 65 deletions
+15 -13
View File
@@ -1,29 +1,31 @@
# golangci-lint configuration # golangci-lint configuration
# https://golangci-lint.run/usage/configuration/ # https://golangci-lint.run/usage/configuration/
version: "2"
run: run:
timeout: 5m timeout: 5m
tests: true tests: true
formatters:
enable:
- gofmt
linters: linters:
enable: enable:
- errcheck - errcheck
- gosimple
- govet - govet
- ineffassign - ineffassign
- staticcheck - staticcheck
- unused - unused
- gosec - gosec
- gocritic - gocritic
- gofmt settings:
govet:
linters-settings: enable:
govet: - fieldalignment
enable: gosec:
- fieldalignment excludes:
gosec: - G304 # File path provided as taint input - handled with #nosec comments where needed
excludes: gocritic:
- G304 # File path provided as taint input - handled with #nosec comments where needed disabled-checks:
gocritic: - ifElseChain # Complex conditionals are clearer as if-else than switch true
disabled-checks:
- ifElseChain # Complex conditionals are clearer as if-else than switch true
+3 -2
View File
@@ -347,10 +347,11 @@ func main() {
} }
// Populate headers based on direction // Populate headers based on direction
if entry.Direction == "request" { switch entry.Direction {
case "request":
uiEntry.RequestHeaders = entry.Headers uiEntry.RequestHeaders = entry.Headers
uiEntry.RequestBody = entry.Body uiEntry.RequestBody = entry.Body
} else if entry.Direction == "response" { case "response":
uiEntry.ResponseHeaders = entry.Headers uiEntry.ResponseHeaders = entry.Headers
uiEntry.ResponseBody = entry.Body uiEntry.ResponseBody = entry.Body
} }
+6 -6
View File
@@ -10,21 +10,21 @@ require (
github.com/grandcat/zeroconf v1.0.0 github.com/grandcat/zeroconf v1.0.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.35.0 k8s.io/api v0.35.1
k8s.io/apimachinery v0.35.0 k8s.io/apimachinery v0.35.1
k8s.io/client-go v0.35.0 k8s.io/client-go v0.35.1
k8s.io/klog/v2 v2.130.1 k8s.io/klog/v2 v2.130.1
) )
require ( require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
@@ -81,7 +81,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect
k8s.io/utils v0.0.0-20260108192941-914a6e750570 // indirect k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
+12 -12
View File
@@ -10,8 +10,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk= github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
@@ -24,8 +24,8 @@ github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSg
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -199,18 +199,18 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY=
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
+1 -1
View File
@@ -201,7 +201,7 @@ func (r *Runner) makeRequest(ctx context.Context, cfg Config) (statusCode int, b
if err != nil { if err != nil {
return 0, 0, bytesWritten, err return 0, 0, bytesWritten, err
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
// Read response body to measure bytes // Read response body to measure bytes
respBody, err := io.ReadAll(resp.Body) respBody, err := io.ReadAll(resp.Body)
+1 -1
View File
@@ -423,7 +423,7 @@ func TestWatcher_HandleReload_LoadError(t *testing.T) {
defer watcher.Stop() defer watcher.Stop()
// Delete the config file to cause load error // Delete the config file to cause load error
os.Remove(configPath) _ = os.Remove(configPath)
// Call handleReload directly // Call handleReload directly
watcher.handleReload() watcher.handleReload()
+5 -5
View File
@@ -210,7 +210,7 @@ func TestPortChecker_CheckAvailability_ExcludeMap(t *testing.T) {
// #nosec G102 -- test intentionally binds to all interfaces to match production port checking // #nosec G102 -- test intentionally binds to all interfaces to match production port checking
listener, err := net.Listen("tcp", ":0") listener, err := net.Listen("tcp", ":0")
assert.NoError(t, err, "should create listener") assert.NoError(t, err, "should create listener")
defer listener.Close() defer func() { _ = listener.Close() }()
// Get the port that's now occupied // Get the port that's now occupied
addr := listener.Addr().(*net.TCPAddr) addr := listener.Addr().(*net.TCPAddr)
@@ -236,12 +236,12 @@ func TestPortChecker_CheckAvailability_MultipleSkipPorts(t *testing.T) {
// #nosec G102 -- test intentionally binds to all interfaces to match production port checking // #nosec G102 -- test intentionally binds to all interfaces to match production port checking
listener1, err := net.Listen("tcp", ":0") listener1, err := net.Listen("tcp", ":0")
assert.NoError(t, err) assert.NoError(t, err)
defer listener1.Close() defer func() { _ = listener1.Close() }()
// #nosec G102 -- test intentionally binds to all interfaces to match production port checking // #nosec G102 -- test intentionally binds to all interfaces to match production port checking
listener2, err := net.Listen("tcp", ":0") listener2, err := net.Listen("tcp", ":0")
assert.NoError(t, err) assert.NoError(t, err)
defer listener2.Close() defer func() { _ = listener2.Close() }()
port1 := listener1.Addr().(*net.TCPAddr).Port port1 := listener1.Addr().(*net.TCPAddr).Port
port2 := listener2.Addr().(*net.TCPAddr).Port port2 := listener2.Addr().(*net.TCPAddr).Port
@@ -360,7 +360,7 @@ func TestPortChecker_PortAvailability_Integration(t *testing.T) {
// #nosec G102 -- test intentionally binds to all interfaces to match production port checking // #nosec G102 -- test intentionally binds to all interfaces to match production port checking
listener, err := net.Listen("tcp", ":0") listener, err := net.Listen("tcp", ":0")
assert.NoError(t, err, "should create listener") assert.NoError(t, err, "should create listener")
defer listener.Close() defer func() { _ = listener.Close() }()
// Get the occupied port // Get the occupied port
occupiedPort := listener.Addr().(*net.TCPAddr).Port occupiedPort := listener.Addr().(*net.TCPAddr).Port
@@ -370,7 +370,7 @@ func TestPortChecker_PortAvailability_Integration(t *testing.T) {
assert.False(t, available, "occupied port should not be available") assert.False(t, available, "occupied port should not be available")
// Close the listener // Close the listener
listener.Close() _ = listener.Close()
// The port should now be available (though there might be a brief delay) // The port should now be available (though there might be a brief delay)
// We don't assert this to avoid flakiness in CI environments // We don't assert this to avoid flakiness in CI environments
+1 -1
View File
@@ -439,7 +439,7 @@ func (c *Checker) checkDataTransfer(port int) error {
if err != nil { if err != nil {
return err return err
} }
defer conn.Close() defer func() { _ = conn.Close() }()
// Set a short read deadline to detect hung connections // Set a short read deadline to detect hung connections
// We don't expect to receive data, but we want to verify the connection isn't hung // We don't expect to receive data, but we want to verify the connection isn't hung
+5 -5
View File
@@ -46,7 +46,7 @@ func (s *HealthCheckTestSuite) TearDownTest() {
s.checker.Stop() s.checker.Stop()
} }
if s.listener != nil { if s.listener != nil {
s.listener.Close() _ = s.listener.Close()
} }
} }
@@ -198,17 +198,17 @@ func (s *HealthCheckTestSuite) TestDataTransferMethod() {
case "banner": case "banner":
_, _ = conn.Write([]byte("220 Welcome\r\n")) _, _ = conn.Write([]byte("220 Welcome\r\n"))
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
conn.Close() _ = conn.Close()
case "close": case "close":
conn.Close() _ = conn.Close()
case "silent": case "silent":
// Just keep connection open // Just keep connection open
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
conn.Close() _ = conn.Close()
} }
} }
}() }()
defer testListener.Close() defer func() { _ = testListener.Close() }()
} else { } else {
testPort = 54322 // Unused port testPort = 54322 // Unused port
} }
+3 -3
View File
@@ -20,7 +20,7 @@ func TestNewLogger_OutputModes(t *testing.T) {
t.Run("empty logFile uses io.Discard", func(t *testing.T) { t.Run("empty logFile uses io.Discard", func(t *testing.T) {
l, err := NewLogger("test-forward", "", 1024) l, err := NewLogger("test-forward", "", 1024)
require.NoError(t, err) require.NoError(t, err)
defer l.Close() defer func() { _ = l.Close() }()
assert.Nil(t, l.file) assert.Nil(t, l.file)
assert.Equal(t, io.Discard, l.output) assert.Equal(t, io.Discard, l.output)
@@ -34,7 +34,7 @@ func TestNewLogger_OutputModes(t *testing.T) {
l, err := NewLogger("test-forward", logFile, 2048) l, err := NewLogger("test-forward", logFile, 2048)
require.NoError(t, err) require.NoError(t, err)
defer l.Close() defer func() { _ = l.Close() }()
assert.NotNil(t, l.file) assert.NotNil(t, l.file)
assert.NotEqual(t, io.Discard, l.output) assert.NotEqual(t, io.Discard, l.output)
@@ -58,7 +58,7 @@ func TestNewLogger_OutputModes(t *testing.T) {
err = l.Log(Entry{Direction: "request"}) err = l.Log(Entry{Direction: "request"})
require.NoError(t, err) require.NoError(t, err)
l.Close() _ = l.Close()
// File should have both contents // File should have both contents
data, _ := os.ReadFile(logFile) data, _ := os.ReadFile(logFile)
+2 -2
View File
@@ -160,7 +160,7 @@ func TestNewLogger(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, l) require.NotNil(t, l)
assert.Nil(t, l.file) // No file when using stdout assert.Nil(t, l.file) // No file when using stdout
l.Close() _ = l.Close()
// Test file logger (using temp file) // Test file logger (using temp file)
tmpFile := t.TempDir() + "/test.log" tmpFile := t.TempDir() + "/test.log"
@@ -173,7 +173,7 @@ func TestNewLogger(t *testing.T) {
err = l.Log(Entry{Direction: "request", Method: "GET"}) err = l.Log(Entry{Direction: "request", Method: "GET"})
require.NoError(t, err) require.NoError(t, err)
l.Close() _ = l.Close()
// Verify file has content // Verify file has content
data, err := os.ReadFile(tmpFile) data, err := os.ReadFile(tmpFile)
+3 -3
View File
@@ -98,13 +98,13 @@ func (l *Logger) log(level Level, msg string, fields map[string]interface{}) {
Fields: fields, Fields: fields,
} }
data, _ := json.Marshal(entry) data, _ := json.Marshal(entry)
fmt.Fprintln(l.output, string(data)) _, _ = fmt.Fprintln(l.output, string(data))
} else { } else {
// Text format // Text format
if len(fields) > 0 { if len(fields) > 0 {
fmt.Fprintf(l.output, "[%s] %s %v\n", levelStr, msg, fields) _, _ = fmt.Fprintf(l.output, "[%s] %s %v\n", levelStr, msg, fields)
} else { } else {
fmt.Fprintf(l.output, "[%s] %s\n", levelStr, msg) _, _ = fmt.Fprintf(l.output, "[%s] %s\n", levelStr, msg)
} }
} }
} }
+7 -1
View File
@@ -564,6 +564,11 @@ func (m model) buildTableRows() [][]string {
statusIcon, statusText := m.getStatusIconAndText(id, fwd) statusIcon, statusText := m.getStatusIconAndText(id, fwd)
localPortText := fmt.Sprintf("%d", fwd.LocalPort)
if fwd.Status == "Active" && !m.ui.isForwardDisabled(id) {
localPortText = hyperlink(fmt.Sprintf("http://127.0.0.1:%d", fwd.LocalPort), fmt.Sprintf("%d→", fwd.LocalPort))
}
rows = append(rows, []string{ rows = append(rows, []string{
truncate(fwd.Context, ColumnWidthContext), truncate(fwd.Context, ColumnWidthContext),
truncate(fwd.Namespace, ColumnWidthNamespace), truncate(fwd.Namespace, ColumnWidthNamespace),
@@ -571,7 +576,7 @@ func (m model) buildTableRows() [][]string {
truncate(fwd.Type, ColumnWidthType), truncate(fwd.Type, ColumnWidthType),
truncate(fwd.Resource, ColumnWidthResource), truncate(fwd.Resource, ColumnWidthResource),
fmt.Sprintf("%d", fwd.RemotePort), fmt.Sprintf("%d", fwd.RemotePort),
fmt.Sprintf("%d", fwd.LocalPort), localPortText,
statusIcon + " " + statusText, statusIcon + " " + statusText,
}) })
} }
@@ -642,6 +647,7 @@ func (m model) createTableStyleFunc(colors mainViewColors) func(row, col int) li
return baseStyle.Foreground(colors.errorColor) return baseStyle.Foreground(colors.errorColor)
} }
} }
} }
return baseStyle return baseStyle
+1 -1
View File
@@ -368,7 +368,7 @@ func TestHTTPLogEntry(t *testing.T) {
func TestHTTPLogSubscriberType(t *testing.T) { func TestHTTPLogSubscriberType(t *testing.T) {
// Test that our mock matches the type // Test that our mock matches the type
mock := NewMockHTTPLogSubscriber() mock := NewMockHTTPLogSubscriber()
var subscriber HTTPLogSubscriber = mock.GetSubscriberFunc() subscriber := mock.GetSubscriberFunc()
// Test subscription // Test subscription
callCount := 0 callCount := 0
+4 -3
View File
@@ -856,11 +856,12 @@ func TestModel_Update_ViewModeRouting(t *testing.T) {
ui := NewBubbleTeaUI(nil, "1.0.0") ui := NewBubbleTeaUI(nil, "1.0.0")
ui.mu.Lock() ui.mu.Lock()
ui.viewMode = tt.viewMode ui.viewMode = tt.viewMode
if tt.viewMode == ViewModeAddWizard { switch tt.viewMode {
case ViewModeAddWizard:
ui.addWizard = newAddWizardState() ui.addWizard = newAddWizardState()
} else if tt.viewMode == ViewModeBenchmark { case ViewModeBenchmark:
ui.benchmarkState = newBenchmarkState("id", "alias", 8080) ui.benchmarkState = newBenchmarkState("id", "alias", 8080)
} else if tt.viewMode == ViewModeHTTPLog { case ViewModeHTTPLog:
ui.httpLogState = newHTTPLogState("id", "alias") ui.httpLogState = newHTTPLogState("id", "alias")
} }
ui.mu.Unlock() ui.mu.Unlock()
+7
View File
@@ -187,6 +187,13 @@ func (t *TableUI) Remove(id string) {
delete(t.forwards, id) delete(t.forwards, id)
} }
// hyperlink wraps text in an OSC 8 terminal hyperlink escape sequence.
// Clicking the text opens the URL in terminals that support it (Ghostty, iTerm2,
// Windows Terminal, Kitty, WezTerm, etc.). Unsupported terminals show plain text.
func hyperlink(url, text string) string {
return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text)
}
// truncate truncates a string to maxLen, adding "..." if needed // truncate truncates a string to maxLen, adding "..." if needed
func truncate(s string, maxLen int) string { func truncate(s string, maxLen int) string {
if len(s) <= maxLen { if len(s) <= maxLen {
+4 -3
View File
@@ -661,12 +661,13 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) {
Alias: wizard.alias, Alias: wizard.alias,
} }
if wizard.selectedResourceType == ResourceTypePodPrefix { switch wizard.selectedResourceType {
case ResourceTypePodPrefix:
fwd.Resource = "pod/" + wizard.resourceValue fwd.Resource = "pod/" + wizard.resourceValue
} else if wizard.selectedResourceType == ResourceTypePodSelector { case ResourceTypePodSelector:
fwd.Resource = wizard.resourceValue fwd.Resource = wizard.resourceValue
fwd.Selector = wizard.selector fwd.Selector = wizard.selector
} else if wizard.selectedResourceType == ResourceTypeService { case ResourceTypeService:
fwd.Resource = "service/" + wizard.resourceValue fwd.Resource = "service/" + wizard.resourceValue
} }
+2 -2
View File
@@ -1304,10 +1304,10 @@ func decompressContent(content string, headers map[string]string) string {
if err != nil { if err != nil {
return content // Return original on error return content // Return original on error
} }
defer reader.Close() defer func() { _ = reader.Close() }()
case "deflate": case "deflate":
reader = flate.NewReader(bytes.NewReader(data)) reader = flate.NewReader(bytes.NewReader(data))
defer reader.Close() defer func() { _ = reader.Close() }()
default: default:
// br (brotli), compress, zstd - not in stdlib, return original // br (brotli), compress, zstd - not in stdlib, return original
return content return content
+1 -1
View File
@@ -101,7 +101,7 @@ func (c *Checker) fetchLatestRelease(ctx context.Context) (*ReleaseInfo, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode) return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)