mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-10-11 04:27:10 +00:00
* Allow configuring forwards-table columns, order, and width Adds an optional tui.columns section to .kportal.yaml so the visible columns, their order, and their widths can be customized instead of always rendering the hardcoded 8-column layout. Omitting the section keeps the existing default columns/order/widths. Closes #81 * fix(ui): keep the plain table layout and check column widths The plain verbose table must look the same when no tui section is set. It had narrower columns and shorter port headers than before. The default layout is now identical to the old one, and a golden test guards it. A status column that is not last now pads by visible width. Table rules follow the column widths. The UI applies new columns only after a reload succeeds, including the SIGHUP path of the verbose table. The validator rejects a column width above 200. The README describes the tui.columns setting. Refs #81 * fix(httplog): replace the deprecated ReverseProxy Director with Rewrite Staticcheck reports SA1019 because Go 1.26 deprecates Director. The proxy now sets the target in a Rewrite function. Rewrite drops the X-Forwarded-For header that Director sent, so the function calls SetXForwarded to keep that header. A new test checks the target port, the path and the header. * fix(release): drop the deprecated cask url verified key Homebrew warns about the verified parameter of the cask url stanza. The setting is not needed. GoReleaser check now passes without a deprecation warning. The postflight warning remains. GoReleaser generates it from the post install hook, and the upstream fix is not released yet. Refs #79 --------- Co-authored-by: Lukasz Raczylo <[email protected]>
178 lines
4.8 KiB
Go
178 lines
4.8 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/lukaszraczylo/kportal/internal/config"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestNewTableUI tests the constructor.
|
|
func TestNewTableUI(t *testing.T) {
|
|
tui := NewTableUI(false, nil)
|
|
require.NotNil(t, tui)
|
|
assert.NotNil(t, tui.forwards)
|
|
assert.False(t, tui.verbose)
|
|
|
|
tuiVerbose := NewTableUI(true, nil)
|
|
assert.True(t, tuiVerbose.verbose)
|
|
}
|
|
|
|
// TestTableUI_AddForward covers the happy path and resource-parsing branches.
|
|
func TestTableUI_AddForward(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
resource string
|
|
alias string
|
|
expectedType string
|
|
expectedName string
|
|
expectedAlias string
|
|
}{
|
|
{
|
|
name: "pod with prefix",
|
|
resource: "pod/my-app",
|
|
alias: "alias",
|
|
expectedType: "pod",
|
|
expectedName: "my-app",
|
|
expectedAlias: "alias",
|
|
},
|
|
{
|
|
name: "service resource",
|
|
resource: "service/postgres",
|
|
alias: "",
|
|
expectedType: "service",
|
|
expectedName: "postgres",
|
|
expectedAlias: "postgres", // Falls back to resource name
|
|
},
|
|
{
|
|
name: "no type prefix defaults to pod",
|
|
resource: "my-pod",
|
|
alias: "",
|
|
expectedType: "pod",
|
|
expectedName: "my-pod",
|
|
expectedAlias: "my-pod",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tui := NewTableUI(false, nil)
|
|
fwd := &config.Forward{
|
|
Resource: tt.resource,
|
|
Port: 8080,
|
|
LocalPort: 8080,
|
|
Alias: tt.alias,
|
|
}
|
|
tui.AddForward("id-1", fwd)
|
|
|
|
tui.mu.RLock()
|
|
defer tui.mu.RUnlock()
|
|
|
|
require.Len(t, tui.forwards, 1)
|
|
status := tui.forwards["id-1"]
|
|
assert.Equal(t, tt.expectedType, status.Type)
|
|
assert.Equal(t, tt.expectedName, status.Resource)
|
|
assert.Equal(t, tt.expectedAlias, status.Alias)
|
|
assert.Equal(t, "Starting", status.Status)
|
|
assert.Equal(t, 8080, status.RemotePort)
|
|
assert.Equal(t, 8080, status.LocalPort)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTableUI_UpdateStatus verifies status mutation.
|
|
func TestTableUI_UpdateStatus(t *testing.T) {
|
|
tui := NewTableUI(false, nil)
|
|
fwd := &config.Forward{Resource: "pod/app", Port: 80, LocalPort: 8080}
|
|
tui.AddForward("id-1", fwd)
|
|
|
|
tui.UpdateStatus("id-1", "Active")
|
|
|
|
tui.mu.RLock()
|
|
assert.Equal(t, "Active", tui.forwards["id-1"].Status)
|
|
tui.mu.RUnlock()
|
|
|
|
// Updating non-existent ID must not panic.
|
|
tui.UpdateStatus("nonexistent", "Active")
|
|
}
|
|
|
|
// TestTableUI_GetForward covers the lookup path.
|
|
func TestTableUI_GetForward(t *testing.T) {
|
|
tui := NewTableUI(false, nil)
|
|
fwd := &config.Forward{Resource: "pod/app", Port: 80, LocalPort: 8080}
|
|
tui.AddForward("id-1", fwd)
|
|
|
|
got := tui.GetForward("id-1")
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, "app", got.Resource)
|
|
|
|
missing := tui.GetForward("nonexistent")
|
|
assert.Nil(t, missing)
|
|
}
|
|
|
|
// TestTableUI_Remove tests deletion.
|
|
func TestTableUI_Remove(t *testing.T) {
|
|
tui := NewTableUI(false, nil)
|
|
fwd := &config.Forward{Resource: "pod/app", Port: 80, LocalPort: 8080}
|
|
tui.AddForward("id-1", fwd)
|
|
tui.AddForward("id-2", fwd)
|
|
|
|
tui.Remove("id-1")
|
|
|
|
tui.mu.RLock()
|
|
defer tui.mu.RUnlock()
|
|
assert.Len(t, tui.forwards, 1)
|
|
assert.Nil(t, tui.forwards["id-1"])
|
|
assert.NotNil(t, tui.forwards["id-2"])
|
|
}
|
|
|
|
// TestTruncate covers the truncation helper.
|
|
func TestTruncate(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
maxLen int
|
|
}{
|
|
{"hello", "hello", 10},
|
|
{"hello world", "hello...", 8},
|
|
{"hi", "hi", 2},
|
|
{"hi!", "hi", 2}, // maxLen <= 3 branch: no ellipsis
|
|
{"abcd", "abc", 3}, // maxLen <= 3 branch
|
|
{"", "", 5},
|
|
{"café-service", "café-...", 8}, // multibyte: count/slice by rune, no mojibake
|
|
{"日本語ポッド", "日本...", 5}, // CJK runes truncated cleanly
|
|
{"naïve", "naïve", 5}, // exactly maxLen runes (6 bytes) — unchanged
|
|
{"abc", "", 0}, // non-positive maxLen
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(fmt.Sprintf("%s_%d", tt.input, tt.maxLen), func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, truncate(tt.input, tt.maxLen))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestHyperlink verifies the OSC-8 escape sequence is produced.
|
|
func TestHyperlink(t *testing.T) {
|
|
result := hyperlink("http://localhost:8080", "8080→")
|
|
assert.Contains(t, result, "http://localhost:8080")
|
|
assert.Contains(t, result, "8080→")
|
|
// Must contain OSC-8 opener and closer
|
|
assert.Contains(t, result, "\x1b]8;;")
|
|
assert.Contains(t, result, "\x1b\\")
|
|
}
|
|
|
|
// TestFormatStatusWithIndicator covers all status branches.
|
|
func TestFormatStatusWithIndicator(t *testing.T) {
|
|
statuses := []string{"Active", "Starting", "Reconnecting", "Error", "Failed", "Unknown"}
|
|
for _, s := range statuses {
|
|
t.Run(s, func(t *testing.T) {
|
|
result := formatStatusWithIndicator(s)
|
|
// Must contain the original status string.
|
|
assert.Contains(t, result, s)
|
|
})
|
|
}
|
|
}
|