Files
tunnel-please/session/interaction/interaction_test.go
bagas b9ec59efb6
SonarQube Scan / SonarQube Trigger (push) Successful in 3m53s
test(interaction): add unit tests for interaction behavior
2026-01-27 15:39:45 +07:00

2311 lines
58 KiB
Go

package interaction
import (
"context"
"errors"
"io"
"net"
"testing"
"time"
"tunnel_pls/types"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"golang.org/x/crypto/ssh"
)
type MockRandom struct {
mock.Mock
}
func (m *MockRandom) String(length int) (string, error) {
args := m.Called(length)
return args.String(0), args.Error(1)
}
type MockConfig struct {
mock.Mock
}
func (m *MockConfig) Domain() string { return m.Called().String(0) }
func (m *MockConfig) SSHPort() string { return m.Called().String(0) }
func (m *MockConfig) HTTPPort() string { return m.Called().String(0) }
func (m *MockConfig) HTTPSPort() string { return m.Called().String(0) }
func (m *MockConfig) TLSEnabled() bool { return m.Called().Bool(0) }
func (m *MockConfig) TLSRedirect() bool { return m.Called().Bool(0) }
func (m *MockConfig) ACMEEmail() string { return m.Called().String(0) }
func (m *MockConfig) CFAPIToken() string { return m.Called().String(0) }
func (m *MockConfig) ACMEStaging() bool { return m.Called().Bool(0) }
func (m *MockConfig) AllowedPortsStart() uint16 { return uint16(m.Called().Int(0)) }
func (m *MockConfig) AllowedPortsEnd() uint16 { return uint16(m.Called().Int(0)) }
func (m *MockConfig) BufferSize() int { return m.Called().Int(0) }
func (m *MockConfig) HeaderSize() int { return m.Called().Int(0) }
func (m *MockConfig) PprofEnabled() bool { return m.Called().Bool(0) }
func (m *MockConfig) PprofPort() string { return m.Called().String(0) }
func (m *MockConfig) Mode() types.ServerMode { return m.Called().Get(0).(types.ServerMode) }
func (m *MockConfig) GRPCAddress() string { return m.Called().String(0) }
func (m *MockConfig) GRPCPort() string { return m.Called().String(0) }
func (m *MockConfig) NodeToken() string { return m.Called().String(0) }
func (m *MockConfig) TLSStoragePath() string { return m.Called().String(0) }
func (m *MockConfig) KeyLoc() string { return m.Called().String(0) }
type MockSlug struct {
mock.Mock
}
func (ms *MockSlug) Set(slug string) { ms.Called(slug) }
func (ms *MockSlug) String() string { return ms.Called().String(0) }
type MockForwarder struct {
mock.Mock
}
func (m *MockForwarder) CreateForwardedTCPIPPayload(origin net.Addr) []byte {
args := m.Called(origin)
return args.Get(0).([]byte)
}
func (m *MockForwarder) HandleConnection(dst io.ReadWriter, src ssh.Channel) {
m.Called(dst, src)
}
func (m *MockForwarder) Close() error {
args := m.Called()
return args.Error(0)
}
func (m *MockForwarder) TunnelType() types.TunnelType {
args := m.Called()
return args.Get(0).(types.TunnelType)
}
func (m *MockForwarder) ForwardedPort() uint16 {
args := m.Called()
return args.Get(0).(uint16)
}
func (m *MockForwarder) SetType(tunnelType types.TunnelType) {
m.Called(tunnelType)
}
func (m *MockForwarder) SetForwardedPort(port uint16) {
m.Called(port)
}
func (m *MockForwarder) SetListener(listener net.Listener) {
m.Called(listener)
}
func (m *MockForwarder) Listener() net.Listener {
args := m.Called()
return args.Get(0).(net.Listener)
}
func (m *MockForwarder) OpenForwardedChannel(ctx context.Context, origin net.Addr) (ssh.Channel, <-chan *ssh.Request, error) {
args := m.Called(ctx, origin)
if args.Get(0) == nil {
return nil, nil, args.Error(2)
}
return args.Get(0).(ssh.Channel), args.Get(1).(<-chan *ssh.Request), args.Error(2)
}
type MockSessionRegistry struct {
mock.Mock
}
func (m *MockSessionRegistry) Update(user string, oldKey, newKey types.SessionKey) error {
args := m.Called(user, oldKey, newKey)
return args.Error(0)
}
type MockChannel struct {
mock.Mock
data []byte
}
func (m *MockChannel) Read(b []byte) (n int, err error) {
args := m.Called(b)
return args.Int(0), args.Error(1)
}
func (m *MockChannel) Write(b []byte) (n int, err error) {
m.data = append(m.data, b...)
args := m.Called(b)
return args.Int(0), args.Error(1)
}
func (m *MockChannel) Close() error {
args := m.Called()
return args.Error(0)
}
func (m *MockChannel) CloseWrite() error {
args := m.Called()
return args.Error(0)
}
func (m *MockChannel) SendRequest(name string, wantReply bool, payload []byte) (bool, error) {
args := m.Called(name, wantReply, payload)
return args.Bool(0), args.Error(1)
}
func (m *MockChannel) Stderr() io.ReadWriter {
args := m.Called()
if args.Get(0) == nil {
return nil
}
return args.Get(0).(io.ReadWriter)
}
type MockCloser struct {
mock.Mock
}
func (m *MockCloser) Close() error { return m.Called().Error(0) }
func TestNew(t *testing.T) {
tests := []struct {
name string
user string
}{
{
name: "creates interaction with default mode",
user: "testuser",
},
{
name: "creates interaction for different user",
user: "anotheruser",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, tt.user, mockCloser.Close)
assert.NotNil(t, mockInteraction)
})
}
}
func TestInteraction_SetMode(t *testing.T) {
tests := []struct {
name string
mode types.InteractiveMode
}{
{
name: "set headless mode",
mode: types.InteractiveModeHEADLESS,
},
{
name: "set interactive mode",
mode: types.InteractiveModeINTERACTIVE,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
mockInteraction.SetMode(tt.mode)
assert.Equal(t, tt.mode, mockInteraction.Mode())
})
}
}
func TestInteraction_Mode(t *testing.T) {
tests := []struct {
name string
setMode types.InteractiveMode
expected types.InteractiveMode
}{
{
name: "mode returns set value",
setMode: types.InteractiveModeINTERACTIVE,
expected: types.InteractiveModeINTERACTIVE,
},
{
name: "mode returns headless value",
setMode: types.InteractiveModeHEADLESS,
expected: types.InteractiveModeHEADLESS,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
mockInteraction.SetMode(tt.setMode)
assert.Equal(t, tt.expected, mockInteraction.Mode())
})
}
}
func TestInteraction_Send(t *testing.T) {
tests := []struct {
name string
message string
setupChannel bool
channelReturn int
channelError error
wantError bool
}{
{
name: "send message successfully",
message: "test message",
setupChannel: true,
channelReturn: 12,
channelError: nil,
wantError: false,
},
{
name: "send message with channel error",
message: "test message",
setupChannel: true,
channelReturn: 0,
channelError: errors.New("channel write error"),
wantError: true,
},
{
name: "send message without channel",
message: "test message",
setupChannel: false,
wantError: false,
},
{
name: "send empty message",
message: "",
setupChannel: true,
channelReturn: 0,
channelError: nil,
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
if tt.setupChannel {
mockChannel := &MockChannel{}
mockChannel.On("Write", []byte(tt.message)).Return(tt.channelReturn, tt.channelError)
mockInteraction.SetChannel(mockChannel)
}
err := mockInteraction.Send(tt.message)
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestInteraction_SetWH(t *testing.T) {
tests := []struct {
name string
width int
height int
}{
{
name: "set large window size",
width: 100,
height: 50,
},
{
name: "set medium window size",
width: 80,
height: 24,
},
{
name: "set small window size",
width: 20,
height: 10,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
mockInteraction.SetWH(tt.width, tt.height)
})
}
}
func TestInteraction_SetChannel(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
mockChannel := &MockChannel{}
mockInteraction.SetChannel(mockChannel)
mockChannel.On("Write", []byte("test")).Return(4, nil)
err := mockInteraction.Send("test")
assert.NoError(t, err)
}
func TestInteraction_Redraw(t *testing.T) {
tests := []struct {
name string
description string
}{
{
name: "redraw interaction",
description: "should not panic when calling redraw",
},
{
name: "redraw multiple times",
description: "should handle multiple redraws",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
mockInteraction.Redraw()
})
}
}
func TestInteraction_Start(t *testing.T) {
tests := []struct {
name string
mode types.InteractiveMode
tlsEnabled bool
tunnelType types.TunnelType
port uint16
}{
{
name: "start in headless mode - should return immediately",
mode: types.InteractiveModeHEADLESS,
tlsEnabled: false,
tunnelType: types.TunnelTypeHTTP,
port: 8080,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
mockInteraction.SetMode(tt.mode)
mockConfig.On("Domain").Return("tunnl.live")
mockConfig.On("TLSEnabled").Return(tt.tlsEnabled)
mockForwarder.On("TunnelType").Return(tt.tunnelType)
mockForwarder.On("ForwardedPort").Return(tt.port)
mockInteraction.Start()
})
}
}
func TestModel_Update(t *testing.T) {
tests := []struct {
name string
msg tea.Msg
showingComingSoon bool
editingSlug bool
showingCommands bool
width int
height int
expectedWidth int
expectedHeight int
expectedQuit bool
}{
{
name: "tick message clears coming soon",
msg: tickMsg{},
showingComingSoon: true,
editingSlug: false,
showingCommands: false,
expectedQuit: false,
},
{
name: "window size message - large screen",
msg: tea.WindowSizeMsg{Width: 100, Height: 50},
expectedWidth: 100,
expectedHeight: 50,
expectedQuit: false,
},
{
name: "window size message - small screen",
msg: tea.WindowSizeMsg{Width: 60, Height: 20},
expectedWidth: 60,
expectedHeight: 20,
expectedQuit: false,
},
{
name: "quit message",
msg: tea.QuitMsg{},
expectedQuit: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
m := &model{
randomizer: mockRandom,
domain: "tunnl.live",
protocol: "http",
tunnelType: types.TunnelTypeHTTP,
port: 8080,
commandList: list.New([]list.Item{}, list.NewDefaultDelegate(), 80, 20),
interaction: mockInteraction.(*interaction),
showingComingSoon: tt.showingComingSoon,
editingSlug: tt.editingSlug,
showingCommands: tt.showingCommands,
width: tt.width,
height: tt.height,
keymap: keymap{
quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
command: key.NewBinding(
key.WithKeys("c"),
key.WithHelp("c", "commands"),
),
random: key.NewBinding(
key.WithKeys("ctrl+r"),
key.WithHelp("ctrl+r", "random"),
),
},
}
result, _ := m.Update(tt.msg)
updatedModel := result.(*model)
if tt.expectedQuit {
assert.True(t, updatedModel.quitting)
}
if windowMsg, ok := tt.msg.(tea.WindowSizeMsg); ok {
assert.Equal(t, windowMsg.Width, updatedModel.width)
assert.Equal(t, windowMsg.Height, updatedModel.height)
}
if _, ok := tt.msg.(tickMsg); ok && tt.showingComingSoon {
assert.False(t, updatedModel.showingComingSoon)
}
})
}
}
func TestModel_View(t *testing.T) {
tests := []struct {
name string
quitting bool
showingComingSoon bool
editingSlug bool
showingCommands bool
expectedEmpty bool
}{
{
name: "quitting returns empty string",
quitting: true,
expectedEmpty: true,
},
{
name: "showing coming soon view",
showingComingSoon: true,
expectedEmpty: false,
},
{
name: "editing slug view",
editingSlug: true,
expectedEmpty: false,
},
{
name: "showing commands view",
showingCommands: true,
expectedEmpty: false,
},
{
name: "dashboard view (default)",
expectedEmpty: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
mockSlug.On("String").Return("test-slug")
m := &model{
randomizer: mockRandom,
domain: "tunnl.live",
protocol: "http",
tunnelType: types.TunnelTypeHTTP,
port: 8080,
commandList: list.New([]list.Item{}, list.NewDefaultDelegate(), 80, 20),
interaction: mockInteraction.(*interaction),
quitting: tt.quitting,
showingComingSoon: tt.showingComingSoon,
editingSlug: tt.editingSlug,
showingCommands: tt.showingCommands,
keymap: keymap{
quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
command: key.NewBinding(
key.WithKeys("c"),
key.WithHelp("c", "commands"),
),
random: key.NewBinding(
key.WithKeys("ctrl+r"),
key.WithHelp("ctrl+r", "random"),
),
},
}
view := m.View()
if tt.expectedEmpty {
assert.Empty(t, view)
} else {
assert.NotEmpty(t, view)
}
})
}
}
func TestInteraction_Integration(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeCallCount := 0
closeFunc := func() error {
closeCallCount++
return nil
}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
assert.NotNil(t, mockInteraction)
mockInteraction.SetMode(types.InteractiveModeINTERACTIVE)
assert.Equal(t, types.InteractiveModeINTERACTIVE, mockInteraction.Mode())
mockChannel := &MockChannel{}
mockInteraction.SetChannel(mockChannel)
mockChannel.On("Write", []byte("hello")).Return(5, nil)
err := mockInteraction.Send("hello")
assert.NoError(t, err)
mockChannel.AssertExpectations(t)
mockInteraction.SetWH(80, 24)
mockInteraction.Redraw()
}
func TestModel_Update_KeyMessages(t *testing.T) {
tests := []struct {
name string
key tea.KeyMsg
showingComingSoon bool
editingSlug bool
showingCommands bool
description string
}{
{
name: "key press while showing coming soon",
key: tea.KeyMsg{Type: tea.KeyEnter},
showingComingSoon: true,
description: "should call comingSoonUpdate",
},
{
name: "key press while editing slug",
key: tea.KeyMsg{Type: tea.KeyEnter},
editingSlug: true,
description: "should call slugUpdate",
},
{
name: "key press while showing commands",
key: tea.KeyMsg{Type: tea.KeyEnter},
showingCommands: true,
description: "should call commandsUpdate",
},
{
name: "key press in dashboard view",
key: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'c'}},
description: "should call dashboardUpdate",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "user", mockCloser.Close)
mockSlug.On("String").Return("test-slug").Maybe()
mockSessionRegistry.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
m := &model{
randomizer: mockRandom,
domain: "tunnl.live",
protocol: "http",
tunnelType: types.TunnelTypeHTTP,
port: 8080,
commandList: list.New([]list.Item{}, list.NewDefaultDelegate(), 80, 20),
interaction: mockInteraction.(*interaction),
showingComingSoon: tt.showingComingSoon,
editingSlug: tt.editingSlug,
showingCommands: tt.showingCommands,
keymap: keymap{
quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
command: key.NewBinding(
key.WithKeys("c"),
key.WithHelp("c", "commands"),
),
random: key.NewBinding(
key.WithKeys("ctrl+r"),
key.WithHelp("ctrl+r", "random"),
),
},
}
result, _ := m.Update(tt.key)
assert.NotNil(t, result)
})
}
}
func TestModel_SlugUpdate(t *testing.T) {
tests := []struct {
name string
tunnelType types.TunnelType
keyMsg tea.KeyMsg
inputValue string
setupMocks func(*MockSessionRegistry, *MockSlug, *MockRandom)
expectedEdit bool
expectedError string
shouldSetValue bool
}{
{
name: "escape key cancels editing",
tunnelType: types.TunnelTypeHTTP,
keyMsg: tea.KeyMsg{Type: tea.KeyEsc},
setupMocks: func(msr *MockSessionRegistry, ms *MockSlug, mr *MockRandom) {},
expectedEdit: false,
},
{
name: "ctrl+c cancels editing",
tunnelType: types.TunnelTypeHTTP,
keyMsg: tea.KeyMsg{Type: tea.KeyCtrlC},
setupMocks: func(msr *MockSessionRegistry, ms *MockSlug, mr *MockRandom) {},
expectedEdit: false,
},
{
name: "enter key saves valid slug",
tunnelType: types.TunnelTypeHTTP,
keyMsg: tea.KeyMsg{Type: tea.KeyEnter},
inputValue: "my-custom-slug",
setupMocks: func(msr *MockSessionRegistry, ms *MockSlug, mr *MockRandom) {
ms.On("String").Return("old-slug")
msr.On("Update", "testuser",
types.SessionKey{Id: "old-slug", Type: types.TunnelTypeHTTP},
types.SessionKey{Id: "my-custom-slug", Type: types.TunnelTypeHTTP},
).Return(nil)
},
expectedEdit: false,
expectedError: "",
},
{
name: "enter key with error shows error message",
tunnelType: types.TunnelTypeHTTP,
keyMsg: tea.KeyMsg{Type: tea.KeyEnter},
inputValue: "invalid",
setupMocks: func(msr *MockSessionRegistry, ms *MockSlug, mr *MockRandom) {
ms.On("String").Return("old-slug")
msr.On("Update", "testuser",
types.SessionKey{Id: "old-slug", Type: types.TunnelTypeHTTP},
types.SessionKey{Id: "invalid", Type: types.TunnelTypeHTTP},
).Return(assert.AnError)
},
expectedEdit: true,
expectedError: assert.AnError.Error(),
},
{
name: "ctrl+r generates random slug",
tunnelType: types.TunnelTypeHTTP,
keyMsg: tea.KeyMsg{Type: tea.KeyCtrlR},
setupMocks: func(msr *MockSessionRegistry, ms *MockSlug, mr *MockRandom) {
mr.On("String", 20).Return("random-generated-slug", nil)
},
expectedEdit: true,
shouldSetValue: true,
},
{
name: "ctrl+r with error does nothing",
tunnelType: types.TunnelTypeHTTP,
keyMsg: tea.KeyMsg{Type: tea.KeyCtrlR},
setupMocks: func(msr *MockSessionRegistry, ms *MockSlug, mr *MockRandom) {
mr.On("String", 20).Return("", assert.AnError)
},
expectedEdit: true,
},
{
name: "regular key updates input",
tunnelType: types.TunnelTypeHTTP,
keyMsg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}},
setupMocks: func(msr *MockSessionRegistry, ms *MockSlug, mr *MockRandom) {},
expectedEdit: true,
},
{
name: "tcp tunnel exits editing immediately",
tunnelType: types.TunnelTypeTCP,
keyMsg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'a'}},
setupMocks: func(msr *MockSessionRegistry, ms *MockSlug, mr *MockRandom) {},
expectedEdit: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
ti := textinput.New()
ti.SetValue(tt.inputValue)
m := &model{
randomizer: mockRandom,
domain: "tunnl.live",
protocol: "http",
tunnelType: tt.tunnelType,
port: 8080,
commandList: list.New([]list.Item{}, list.NewDefaultDelegate(), 80, 20),
slugInput: ti,
editingSlug: true,
interaction: mockInteraction.(*interaction),
keymap: keymap{
quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
command: key.NewBinding(
key.WithKeys("c"),
key.WithHelp("c", "commands"),
),
random: key.NewBinding(
key.WithKeys("ctrl+r"),
key.WithHelp("ctrl+r", "random"),
),
},
}
tt.setupMocks(mockSessionRegistry, mockSlug, mockRandom)
result, _ := m.slugUpdate(tt.keyMsg)
resultModel := result.(*model)
assert.Equal(t, tt.expectedEdit, resultModel.editingSlug)
if tt.expectedError != "" {
assert.Equal(t, tt.expectedError, resultModel.slugError)
} else if !tt.expectedEdit {
assert.Equal(t, "", resultModel.slugError)
}
mockSessionRegistry.AssertExpectations(t)
mockSlug.AssertExpectations(t)
mockRandom.AssertExpectations(t)
})
}
}
func TestModel_SlugView(t *testing.T) {
tests := []struct {
name string
width int
tunnelType types.TunnelType
slugError string
contains string
}{
{
name: "http tunnel - large screen",
width: 100,
tunnelType: types.TunnelTypeHTTP,
contains: "Subdomain",
},
{
name: "http tunnel - small screen",
width: 50,
tunnelType: types.TunnelTypeHTTP,
contains: "Subdomain",
},
{
name: "http tunnel - tiny screen",
width: 30,
tunnelType: types.TunnelTypeHTTP,
contains: "Subdomain",
},
{
name: "http tunnel with error",
width: 100,
tunnelType: types.TunnelTypeHTTP,
slugError: "Slug already exists",
contains: "Slug already exists",
},
{
name: "tcp tunnel - large screen",
width: 100,
tunnelType: types.TunnelTypeTCP,
contains: "TCP",
},
{
name: "tcp tunnel - small screen",
width: 50,
tunnelType: types.TunnelTypeTCP,
contains: "TCP",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
ti := textinput.New()
ti.SetValue("test-slug")
m := &model{
randomizer: mockRandom,
domain: "tunnl.live",
protocol: "http",
tunnelType: tt.tunnelType,
port: 8080,
slugInput: ti,
slugError: tt.slugError,
interaction: mockInteraction.(*interaction),
width: tt.width,
}
view := m.slugView()
assert.NotEmpty(t, view)
assert.Contains(t, view, tt.contains)
})
}
}
func TestModel_ComingSoonUpdate(t *testing.T) {
tests := []struct {
name string
keyMsg tea.KeyMsg
}{
{
name: "any key dismisses coming soon",
keyMsg: tea.KeyMsg{Type: tea.KeyEnter},
},
{
name: "escape key dismisses",
keyMsg: tea.KeyMsg{Type: tea.KeyEsc},
},
{
name: "space key dismisses",
keyMsg: tea.KeyMsg{Type: tea.KeySpace},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
m := &model{
interaction: mockInteraction.(*interaction),
showingComingSoon: true,
}
result, _ := m.comingSoonUpdate(tt.keyMsg)
resultModel := result.(*model)
assert.False(t, resultModel.showingComingSoon)
})
}
}
func TestModel_ComingSoonView(t *testing.T) {
tests := []struct {
name string
width int
}{
{
name: "large screen",
width: 100,
},
{
name: "medium screen",
width: 60,
},
{
name: "small screen",
width: 50,
},
{
name: "tiny screen",
width: 30,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
m := &model{
interaction: mockInteraction.(*interaction),
width: tt.width,
}
view := m.comingSoonView()
assert.NotEmpty(t, view)
assert.Contains(t, view, "Coming")
})
}
}
func TestModel_CommandsUpdate(t *testing.T) {
tests := []struct {
name string
keyMsg tea.KeyMsg
selectedItem list.Item
expectCommands bool
expectEditSlug bool
expectComingSoon bool
}{
{
name: "escape key closes commands",
keyMsg: tea.KeyMsg{Type: tea.KeyEsc},
expectCommands: false,
},
{
name: "q key closes commands",
keyMsg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}},
expectCommands: false,
},
{
name: "enter on slug command starts editing",
keyMsg: tea.KeyMsg{Type: tea.KeyEnter},
selectedItem: commandItem{name: "slug", desc: "Set custom subdomain"},
expectCommands: false,
expectEditSlug: true,
},
{
name: "enter on tunnel-type shows coming soon",
keyMsg: tea.KeyMsg{Type: tea.KeyEnter},
selectedItem: commandItem{name: "tunnel-type", desc: "Change tunnel type"},
expectCommands: false,
expectComingSoon: true,
},
{
name: "arrow key navigates list",
keyMsg: tea.KeyMsg{Type: tea.KeyDown},
expectCommands: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
mockSlug.On("String").Return("current-slug").Maybe()
items := []list.Item{
commandItem{name: "slug", desc: "Set custom subdomain"},
commandItem{name: "tunnel-type", desc: "Change tunnel type"},
}
delegate := list.NewDefaultDelegate()
commandList := list.New(items, delegate, 80, 20)
if tt.selectedItem != nil {
for i, item := range items {
if item.(commandItem).name == tt.selectedItem.(commandItem).name {
commandList.Select(i)
break
}
}
}
ti := textinput.New()
m := &model{
randomizer: mockRandom,
interaction: mockInteraction.(*interaction),
showingCommands: true,
commandList: commandList,
slugInput: ti,
keymap: keymap{
quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
command: key.NewBinding(
key.WithKeys("c"),
key.WithHelp("c", "commands"),
),
random: key.NewBinding(
key.WithKeys("ctrl+r"),
key.WithHelp("ctrl+r", "random"),
),
},
}
result, _ := m.commandsUpdate(tt.keyMsg)
resultModel := result.(*model)
assert.Equal(t, tt.expectCommands, resultModel.showingCommands)
if tt.expectEditSlug {
assert.True(t, resultModel.editingSlug)
}
if tt.expectComingSoon {
assert.True(t, resultModel.showingComingSoon)
}
})
}
}
func TestModel_CommandsView(t *testing.T) {
tests := []struct {
name string
width int
}{
{
name: "large screen",
width: 100,
},
{
name: "medium screen",
width: 60,
},
{
name: "small screen",
width: 50,
},
{
name: "tiny screen",
width: 30,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
items := []list.Item{
commandItem{name: "slug", desc: "Set custom subdomain"},
commandItem{name: "tunnel-type", desc: "Change tunnel type"},
}
delegate := list.NewDefaultDelegate()
commandList := list.New(items, delegate, 80, 20)
m := &model{
interaction: mockInteraction.(*interaction),
commandList: commandList,
width: tt.width,
}
view := m.commandsView()
assert.NotEmpty(t, view)
assert.Contains(t, view, "Commands")
})
}
}
func TestModel_DashboardUpdate(t *testing.T) {
tests := []struct {
name string
keyMsg tea.KeyMsg
expectQuit bool
expectCommands bool
}{
{
name: "q key quits",
keyMsg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'q'}},
expectQuit: true,
},
{
name: "ctrl+c quits",
keyMsg: tea.KeyMsg{Type: tea.KeyCtrlC},
expectQuit: true,
},
{
name: "c key opens commands",
keyMsg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'c'}},
expectCommands: true,
},
{
name: "other keys do nothing",
keyMsg: tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'x'}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
m := &model{
interaction: mockInteraction.(*interaction),
keymap: keymap{
quit: key.NewBinding(
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q", "quit"),
),
command: key.NewBinding(
key.WithKeys("c"),
key.WithHelp("c", "commands"),
),
random: key.NewBinding(
key.WithKeys("ctrl+r"),
key.WithHelp("ctrl+r", "random"),
),
},
}
result, _ := m.dashboardUpdate(tt.keyMsg)
resultModel := result.(*model)
if tt.expectQuit {
assert.True(t, resultModel.quitting)
}
if tt.expectCommands {
assert.True(t, resultModel.showingCommands)
}
})
}
}
func TestModel_DashboardView(t *testing.T) {
tests := []struct {
name string
width int
tunnelType types.TunnelType
protocol string
port uint16
contains string
}{
{
name: "http tunnel - large screen",
width: 100,
tunnelType: types.TunnelTypeHTTP,
protocol: "http",
contains: "http",
},
{
name: "https tunnel - large screen",
width: 100,
tunnelType: types.TunnelTypeHTTP,
protocol: "https",
contains: "https",
},
{
name: "tcp tunnel - large screen",
width: 100,
tunnelType: types.TunnelTypeTCP,
port: 8080,
contains: "tcp",
},
{
name: "http tunnel - medium screen",
width: 70,
tunnelType: types.TunnelTypeHTTP,
protocol: "http",
contains: "http",
},
{
name: "http tunnel - small screen",
width: 50,
tunnelType: types.TunnelTypeHTTP,
protocol: "http",
contains: "http",
},
{
name: "http tunnel - tiny screen",
width: 30,
tunnelType: types.TunnelTypeHTTP,
protocol: "http",
contains: "http",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
mockSlug.On("String").Return("test-slug")
m := &model{
randomizer: mockRandom,
domain: "tunnl.live",
protocol: tt.protocol,
tunnelType: tt.tunnelType,
port: tt.port,
interaction: mockInteraction.(*interaction),
width: tt.width,
}
view := m.dashboardView()
assert.NotEmpty(t, view)
assert.Contains(t, view, tt.contains)
})
}
}
func TestGetResponsiveWidth(t *testing.T) {
tests := []struct {
name string
screenWidth int
padding int
minWidth int
maxWidth int
expected int
}{
{
name: "screen wider than max",
screenWidth: 100,
padding: 10,
minWidth: 20,
maxWidth: 60,
expected: 60,
},
{
name: "screen narrower than min",
screenWidth: 30,
padding: 10,
minWidth: 40,
maxWidth: 80,
expected: 40,
},
{
name: "screen within range",
screenWidth: 70,
padding: 10,
minWidth: 20,
maxWidth: 80,
expected: 60,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getResponsiveWidth(tt.screenWidth, tt.padding, tt.minWidth, tt.maxWidth)
assert.Equal(t, tt.expected, result)
})
}
}
func TestShouldUseCompactLayout(t *testing.T) {
tests := []struct {
name string
width int
threshold int
expected bool
}{
{
name: "width below threshold",
width: 50,
threshold: 60,
expected: true,
},
{
name: "width at threshold",
width: 60,
threshold: 60,
expected: false,
},
{
name: "width above threshold",
width: 70,
threshold: 60,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := shouldUseCompactLayout(tt.width, tt.threshold)
assert.Equal(t, tt.expected, result)
})
}
}
func TestTruncateString(t *testing.T) {
tests := []struct {
name string
input string
maxLength int
expected string
}{
{
name: "string shorter than max",
input: "short",
maxLength: 10,
expected: "short",
},
{
name: "string equal to max",
input: "exactly10c",
maxLength: 10,
expected: "exactly10c",
},
{
name: "string longer than max",
input: "this is a very long string",
maxLength: 10,
expected: "this is...",
},
{
name: "very short max length",
input: "hello",
maxLength: 3,
expected: "hel",
},
{
name: "max length less than 4",
input: "hello",
maxLength: 2,
expected: "he",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := truncateString(tt.input, tt.maxLength)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBuildURL(t *testing.T) {
tests := []struct {
name string
protocol string
subdomain string
domain string
expected string
}{
{
name: "http url",
protocol: "http",
subdomain: "test",
domain: "tunnl.live",
expected: "http://test.tunnl.live",
},
{
name: "https url",
protocol: "https",
subdomain: "api",
domain: "myapp.io",
expected: "https://api.myapp.io",
},
{
name: "custom subdomain",
protocol: "http",
subdomain: "my-custom-slug",
domain: "tunnl.live",
expected: "http://my-custom-slug.tunnl.live",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildURL(tt.protocol, tt.subdomain, tt.domain)
assert.Equal(t, tt.expected, result)
})
}
}
func TestTickCmd(t *testing.T) {
tests := []struct {
name string
duration time.Duration
}{
{
name: "5 second tick",
duration: 5 * time.Second,
},
{
name: "1 second tick",
duration: 1 * time.Second,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tickCmd(tt.duration)
assert.NotNil(t, cmd)
})
}
}
func TestGetPaddingValue(t *testing.T) {
tests := []struct {
name string
isVeryCompact bool
isCompact bool
expected int
}{
{
name: "very compact layout",
isVeryCompact: true,
isCompact: false,
expected: 1,
},
{
name: "compact layout",
isVeryCompact: false,
isCompact: true,
expected: 1,
},
{
name: "normal layout",
isVeryCompact: false,
isCompact: false,
expected: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getPaddingValue(tt.isVeryCompact, tt.isCompact)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetMarginValue(t *testing.T) {
tests := []struct {
name string
isCompact bool
compactValue int
normalValue int
expected int
}{
{
name: "compact layout",
isCompact: true,
compactValue: 1,
normalValue: 2,
expected: 1,
},
{
name: "normal layout",
isCompact: false,
compactValue: 1,
normalValue: 2,
expected: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getMarginValue(tt.isCompact, tt.compactValue, tt.normalValue)
assert.Equal(t, tt.expected, result)
})
}
}
func TestCommandItem(t *testing.T) {
tests := []struct {
name string
item commandItem
wantName string
wantDesc string
}{
{
name: "slug command",
item: commandItem{name: "slug", desc: "Set custom subdomain"},
wantName: "slug",
wantDesc: "Set custom subdomain",
},
{
name: "tunnel-type command",
item: commandItem{name: "tunnel-type", desc: "Change tunnel type"},
wantName: "tunnel-type",
wantDesc: "Change tunnel type",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.wantName, tt.item.FilterValue())
assert.Equal(t, tt.wantName, tt.item.Title())
assert.Equal(t, tt.wantDesc, tt.item.Description())
})
}
}
func TestModel_GetTunnelURL(t *testing.T) {
tests := []struct {
name string
tunnelType types.TunnelType
protocol string
slug string
domain string
port uint16
expected string
}{
{
name: "http tunnel",
tunnelType: types.TunnelTypeHTTP,
protocol: "http",
slug: "my-app",
domain: "tunnl.live",
expected: "http://my-app.tunnl.live",
},
{
name: "https tunnel",
tunnelType: types.TunnelTypeHTTP,
protocol: "https",
slug: "secure-app",
domain: "tunnl.live",
expected: "https://secure-app.tunnl.live",
},
{
name: "tcp tunnel",
tunnelType: types.TunnelTypeTCP,
domain: "tunnl.live",
port: 8080,
expected: "tcp://tunnl.live:8080",
},
{
name: "tcp tunnel with different port",
tunnelType: types.TunnelTypeTCP,
domain: "tunnl.live",
port: 3306,
expected: "tcp://tunnl.live:3306",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
mockSlug.On("String").Return(tt.slug).Maybe()
m := &model{
domain: tt.domain,
protocol: tt.protocol,
tunnelType: tt.tunnelType,
port: tt.port,
interaction: mockInteraction.(*interaction),
}
result := m.getTunnelURL()
assert.Equal(t, tt.expected, result)
})
}
}
func TestModel_Init(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
mockCloser := &MockCloser{}
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", mockCloser.Close)
m := &model{
interaction: mockInteraction.(*interaction),
}
cmd := m.Init()
assert.NotNil(t, cmd)
}
func TestInteraction_Start_Interactive(t *testing.T) {
tests := []struct {
name string
tlsEnabled bool
tunnelType types.TunnelType
port uint16
domain string
}{
{
name: "interactive mode with http",
tlsEnabled: false,
tunnelType: types.TunnelTypeHTTP,
port: 8080,
domain: "tunnl.live",
},
{
name: "interactive mode with https",
tlsEnabled: true,
tunnelType: types.TunnelTypeHTTP,
port: 8443,
domain: "secure.tunnl.live",
},
{
name: "interactive mode with tcp",
tlsEnabled: false,
tunnelType: types.TunnelTypeTCP,
port: 3306,
domain: "db.tunnl.live",
},
{
name: "interactive mode with tcp and tls enabled",
tlsEnabled: true,
tunnelType: types.TunnelTypeTCP,
port: 5432,
domain: "postgres.tunnl.live",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeCallCount := 0
closeFunc := func() error {
closeCallCount++
return nil
}
mockConfig.On("Domain").Return(tt.domain)
mockConfig.On("TLSEnabled").Return(tt.tlsEnabled)
mockForwarder.On("TunnelType").Return(tt.tunnelType)
mockForwarder.On("ForwardedPort").Return(tt.port)
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
mockInteraction.SetMode(types.InteractiveModeINTERACTIVE)
mockChannel := &MockChannel{}
mockChannel.On("Read", mock.Anything).Return(0, assert.AnError).Maybe()
mockChannel.On("Write", mock.Anything).Return(0, nil).Maybe()
mockInteraction.SetChannel(mockChannel)
done := make(chan bool, 1)
go func() {
mockInteraction.Start()
done <- true
}()
time.Sleep(50 * time.Millisecond)
i := mockInteraction.(*interaction)
i.Stop()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("Start() did not complete in time")
}
assert.Equal(t, 1, closeCallCount, "close function should be called once")
mockConfig.AssertExpectations(t)
mockForwarder.AssertExpectations(t)
})
}
}
func TestInteraction_Start_ProtocolSelection(t *testing.T) {
tests := []struct {
name string
tlsEnabled bool
expectedProto string
}{
{
name: "http when TLS disabled",
tlsEnabled: false,
expectedProto: "http",
},
{
name: "https when TLS enabled",
tlsEnabled: true,
expectedProto: "https",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockConfig.On("Domain").Return("tunnl.live")
mockConfig.On("TLSEnabled").Return(tt.tlsEnabled)
mockForwarder.On("TunnelType").Return(types.TunnelTypeHTTP)
mockForwarder.On("ForwardedPort").Return(uint16(8080))
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
mockInteraction.SetMode(types.InteractiveModeINTERACTIVE)
mockChannel := &MockChannel{}
mockChannel.On("Read", mock.Anything).Return(0, assert.AnError).Maybe()
mockChannel.On("Write", mock.Anything).Return(0, nil).Maybe()
mockInteraction.SetChannel(mockChannel)
go func() {
mockInteraction.Start()
}()
time.Sleep(50 * time.Millisecond)
i := mockInteraction.(*interaction)
if i.program != nil {
assert.NotNil(t, i.program, "program should be initialized")
}
i.Stop()
mockConfig.AssertExpectations(t)
mockForwarder.AssertExpectations(t)
})
}
}
func TestInteraction_Stop(t *testing.T) {
tests := []struct {
name string
setupProgram bool
description string
}{
{
name: "stop with active program",
setupProgram: true,
description: "should kill program and set to nil",
},
{
name: "stop without program",
setupProgram: false,
description: "should not panic when program is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
i := mockInteraction.(*interaction)
if tt.setupProgram {
mockConfig.On("Domain").Return("tunnl.live")
mockConfig.On("TLSEnabled").Return(false)
mockForwarder.On("TunnelType").Return(types.TunnelTypeHTTP)
mockForwarder.On("ForwardedPort").Return(uint16(8080))
mockInteraction.SetMode(types.InteractiveModeINTERACTIVE)
mockChannel := &MockChannel{}
mockChannel.On("Read", mock.Anything).Return(0, assert.AnError).Maybe()
mockChannel.On("Write", mock.Anything).Return(0, nil).Maybe()
mockInteraction.SetChannel(mockChannel)
go func() {
mockInteraction.Start()
}()
time.Sleep(50 * time.Millisecond)
}
assert.NotPanics(t, func() {
i.Stop()
})
assert.Nil(t, i.program)
select {
case <-i.ctx.Done():
case <-time.After(100 * time.Millisecond):
t.Fatal("context should be cancelled after Stop()")
}
})
}
}
func TestInteraction_Start_CommandListSetup(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockConfig.On("Domain").Return("tunnl.live")
mockConfig.On("TLSEnabled").Return(false)
mockForwarder.On("TunnelType").Return(types.TunnelTypeHTTP)
mockForwarder.On("ForwardedPort").Return(uint16(8080))
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
mockInteraction.SetMode(types.InteractiveModeINTERACTIVE)
mockChannel := &MockChannel{}
mockChannel.On("Read", mock.Anything).Return(0, nil)
mockChannel.On("Write", mock.Anything).Return(0, nil)
mockInteraction.SetChannel(mockChannel)
go func() {
mockInteraction.Start()
}()
time.Sleep(50 * time.Millisecond)
i := mockInteraction.(*interaction)
assert.NotNil(t, i.program, "program should be initialized")
i.Stop()
}
func TestInteraction_Start_TextInputSetup(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockConfig.On("Domain").Return("tunnl.live")
mockConfig.On("TLSEnabled").Return(false)
mockForwarder.On("TunnelType").Return(types.TunnelTypeHTTP)
mockForwarder.On("ForwardedPort").Return(uint16(8080))
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
mockInteraction.SetMode(types.InteractiveModeINTERACTIVE)
mockChannel := &MockChannel{}
mockChannel.On("Read", mock.Anything).Return(0, assert.AnError).Maybe()
mockChannel.On("Write", mock.Anything).Return(0, nil).Maybe()
mockInteraction.SetChannel(mockChannel)
go func() {
mockInteraction.Start()
}()
time.Sleep(50 * time.Millisecond)
i := mockInteraction.(*interaction)
i.Stop()
mockConfig.AssertExpectations(t)
mockForwarder.AssertExpectations(t)
}
func TestInteraction_Start_CleanupOnExit(t *testing.T) {
tests := []struct {
name string
closeFunc CloseFunc
expectCloseCalled bool
}{
{
name: "cleanup calls close function",
closeFunc: func() error {
return nil
},
expectCloseCalled: true,
},
{
name: "cleanup with nil close function",
closeFunc: nil,
expectCloseCalled: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeCallCount := 0
var closeFunc CloseFunc
if tt.closeFunc != nil {
closeFunc = func() error {
closeCallCount++
return tt.closeFunc()
}
}
mockConfig.On("Domain").Return("tunnl.live")
mockConfig.On("TLSEnabled").Return(false)
mockForwarder.On("TunnelType").Return(types.TunnelTypeHTTP)
mockForwarder.On("ForwardedPort").Return(uint16(8080))
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
mockInteraction.SetMode(types.InteractiveModeINTERACTIVE)
mockChannel := &MockChannel{}
mockChannel.On("Read", mock.Anything).Return(0, assert.AnError).Maybe()
mockChannel.On("Write", mock.Anything).Return(0, nil).Maybe()
mockInteraction.SetChannel(mockChannel)
done := make(chan bool, 1)
go func() {
mockInteraction.Start()
done <- true
}()
time.Sleep(50 * time.Millisecond)
i := mockInteraction.(*interaction)
i.Stop()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("Start() did not complete")
}
if tt.expectCloseCalled {
assert.Equal(t, 1, closeCallCount, "close function should be called")
} else {
assert.Equal(t, 0, closeCallCount, "close function should not be called when nil")
}
})
}
}
func TestInteraction_Start_WithDifferentChannels(t *testing.T) {
tests := []struct {
name string
setupChannel bool
}{
{
name: "start with channel set",
setupChannel: true,
},
{
name: "start with nil channel",
setupChannel: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockConfig.On("Domain").Return("tunnl.live")
mockConfig.On("TLSEnabled").Return(false)
mockForwarder.On("TunnelType").Return(types.TunnelTypeHTTP)
mockForwarder.On("ForwardedPort").Return(uint16(8080))
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
mockInteraction.SetMode(types.InteractiveModeINTERACTIVE)
if tt.setupChannel {
mockChannel := &MockChannel{}
mockChannel.On("Read", mock.Anything).Return(0, assert.AnError).Maybe()
mockChannel.On("Write", mock.Anything).Return(0, nil).Maybe()
mockInteraction.SetChannel(mockChannel)
}
go func() {
mockInteraction.Start()
}()
time.Sleep(50 * time.Millisecond)
i := mockInteraction.(*interaction)
i.Stop()
mockConfig.AssertExpectations(t)
mockForwarder.AssertExpectations(t)
})
}
}
func TestInteraction_Stop_ContextCancellation(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
i := mockInteraction.(*interaction)
select {
case <-i.ctx.Done():
t.Fatal("context should not be cancelled initially")
default:
}
i.Stop()
select {
case <-i.ctx.Done():
case <-time.After(100 * time.Millisecond):
t.Fatal("context should be cancelled after Stop()")
}
assert.NotPanics(t, func() {
i.Stop()
})
}
func TestInteraction_Stop_MultipleCallsSafe(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
i := mockInteraction.(*interaction)
assert.NotPanics(t, func() {
i.Stop()
i.Stop()
i.Stop()
})
assert.Nil(t, i.program)
}
func TestInteraction_Start_HeadlessMode_NoOp(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
mockInteraction.SetMode(types.InteractiveModeHEADLESS)
done := make(chan bool, 1)
go func() {
mockInteraction.Start()
done <- true
}()
select {
case <-done:
case <-time.After(100 * time.Millisecond):
t.Fatal("headless mode should return immediately")
}
i := mockInteraction.(*interaction)
assert.Nil(t, i.program, "program should not be created in headless mode")
mockConfig.AssertNotCalled(t, "Domain")
mockConfig.AssertNotCalled(t, "TLSEnabled")
mockForwarder.AssertNotCalled(t, "TunnelType")
mockForwarder.AssertNotCalled(t, "ForwardedPort")
}
func TestInteraction_New_ContextInitialization(t *testing.T) {
mockRandom := &MockRandom{}
mockConfig := &MockConfig{}
mockSlug := &MockSlug{}
mockForwarder := &MockForwarder{}
mockSessionRegistry := &MockSessionRegistry{}
closeFunc := func() error { return nil }
mockSlug.On("String").Return("test-slug")
mockInteraction := New(mockRandom, mockConfig, mockSlug, mockForwarder, mockSessionRegistry, "testuser", closeFunc)
i := mockInteraction.(*interaction)
assert.NotNil(t, i.ctx, "context should be initialized")
assert.NotNil(t, i.cancel, "cancel function should be initialized")
select {
case <-i.ctx.Done():
t.Fatal("context should not be cancelled initially")
default:
}
}