test(muxterm): unit tests for protocol, token validation, and pool
This commit is contained in:
+25
-159
@@ -13,7 +13,7 @@ func TestNewPool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetEmpty(t *testing.T) {
|
func TestPool_Get_NoSession(t *testing.T) {
|
||||||
p := NewPool()
|
p := NewPool()
|
||||||
s := p.Get("nonexistent")
|
s := p.Get("nonexistent")
|
||||||
if s != nil {
|
if s != nil {
|
||||||
@@ -21,35 +21,37 @@ func TestGetEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsAliveEmpty(t *testing.T) {
|
func TestPool_IsAlive_NoSession(t *testing.T) {
|
||||||
p := NewPool()
|
p := NewPool()
|
||||||
if p.IsAlive("nonexistent") {
|
if p.IsAlive("nonexistent") {
|
||||||
t.Fatal("IsAlive on empty pool should return false")
|
t.Fatal("IsAlive on empty pool should return false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupTmuxSession creates a tmux session for testing. Returns a cleanup function.
|
func TestPool_CloseAll_Empty(t *testing.T) {
|
||||||
func setupTmuxSession(t *testing.T, name string) func() {
|
p := NewPool()
|
||||||
t.Helper()
|
p.CloseAll() // must not panic
|
||||||
// Kill any leftover session with this name
|
}
|
||||||
|
|
||||||
|
func TestPool_Attach_Real(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
name := "test-muxterm"
|
||||||
|
|
||||||
|
// Ensure the tmux session exists.
|
||||||
exec.Command("tmux", "kill-session", "-t", name).Run()
|
exec.Command("tmux", "kill-session", "-t", name).Run()
|
||||||
cmd := exec.Command("tmux", "new-session", "-d", "-s", name, "-x", "80", "-y", "24")
|
cmd := exec.Command("tmux", "new-session", "-d", "-s", name, "-x", "80", "-y", "24")
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
t.Fatalf("failed to create tmux session %q: %v", name, err)
|
t.Fatalf("failed to create tmux session %q: %v", name, err)
|
||||||
}
|
}
|
||||||
return func() {
|
defer exec.Command("tmux", "kill-session", "-t", name).Run()
|
||||||
exec.Command("tmux", "kill-session", "-t", name).Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAttachCreatesSession(t *testing.T) {
|
|
||||||
name := "test-pool-attach"
|
|
||||||
cleanup := setupTmuxSession(t, name)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
p := NewPool()
|
p := NewPool()
|
||||||
defer p.CloseAll()
|
defer p.CloseAll()
|
||||||
|
|
||||||
|
// Attach
|
||||||
sess, err := p.Attach(name, 80, 24)
|
sess, err := p.Attach(name, 80, 24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Attach failed: %v", err)
|
t.Fatalf("Attach failed: %v", err)
|
||||||
@@ -60,165 +62,29 @@ func TestAttachCreatesSession(t *testing.T) {
|
|||||||
if sess.PTY == nil {
|
if sess.PTY == nil {
|
||||||
t.Fatal("Session PTY is nil")
|
t.Fatal("Session PTY is nil")
|
||||||
}
|
}
|
||||||
if sess.Cmd == nil {
|
|
||||||
t.Fatal("Session Cmd is nil")
|
|
||||||
}
|
|
||||||
if sess.Cols != 80 {
|
|
||||||
t.Errorf("Cols = %d, want 80", sess.Cols)
|
|
||||||
}
|
|
||||||
if sess.Rows != 24 {
|
|
||||||
t.Errorf("Rows = %d, want 24", sess.Rows)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetReturnsSession(t *testing.T) {
|
|
||||||
name := "test-pool-get"
|
|
||||||
cleanup := setupTmuxSession(t, name)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
p := NewPool()
|
|
||||||
defer p.CloseAll()
|
|
||||||
|
|
||||||
_, err := p.Attach(name, 80, 24)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Attach failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sess := p.Get(name)
|
|
||||||
if sess == nil {
|
|
||||||
t.Fatal("Get returned nil after Attach")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsAliveAfterAttach(t *testing.T) {
|
|
||||||
name := "test-pool-alive"
|
|
||||||
cleanup := setupTmuxSession(t, name)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
p := NewPool()
|
|
||||||
defer p.CloseAll()
|
|
||||||
|
|
||||||
_, err := p.Attach(name, 80, 24)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Attach failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// IsAlive
|
||||||
if !p.IsAlive(name) {
|
if !p.IsAlive(name) {
|
||||||
t.Fatal("IsAlive should return true after Attach")
|
t.Fatal("IsAlive should return true after Attach")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func TestAttachReusesLiveSession(t *testing.T) {
|
|
||||||
name := "test-pool-reuse"
|
|
||||||
cleanup := setupTmuxSession(t, name)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
p := NewPool()
|
|
||||||
defer p.CloseAll()
|
|
||||||
|
|
||||||
sess1, err := p.Attach(name, 80, 24)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("First Attach failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sess2, err := p.Attach(name, 80, 24)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Second Attach failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sess1 != sess2 {
|
|
||||||
t.Fatal("Attach should reuse existing live session")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResize(t *testing.T) {
|
|
||||||
name := "test-pool-resize"
|
|
||||||
cleanup := setupTmuxSession(t, name)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
p := NewPool()
|
|
||||||
defer p.CloseAll()
|
|
||||||
|
|
||||||
_, err := p.Attach(name, 80, 24)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Attach failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Resize
|
||||||
err = p.Resize(name, 120, 40)
|
err = p.Resize(name, 120, 40)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resize failed: %v", err)
|
t.Fatalf("Resize failed: %v", err)
|
||||||
}
|
}
|
||||||
|
resized := p.Get(name)
|
||||||
sess := p.Get(name)
|
if resized.Cols != 120 {
|
||||||
if sess.Cols != 120 {
|
t.Errorf("After resize Cols = %d, want 120", resized.Cols)
|
||||||
t.Errorf("After resize Cols = %d, want 120", sess.Cols)
|
|
||||||
}
|
}
|
||||||
if sess.Rows != 40 {
|
if resized.Rows != 40 {
|
||||||
t.Errorf("After resize Rows = %d, want 40", sess.Rows)
|
t.Errorf("After resize Rows = %d, want 40", resized.Rows)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResizeNonexistent(t *testing.T) {
|
|
||||||
p := NewPool()
|
|
||||||
err := p.Resize("nonexistent", 80, 24)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Resize on nonexistent session should return error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCloseAll(t *testing.T) {
|
|
||||||
name := "test-pool-closeall"
|
|
||||||
cleanup := setupTmuxSession(t, name)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
p := NewPool()
|
|
||||||
|
|
||||||
_, err := p.Attach(name, 80, 24)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Attach failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
p.CloseAll()
|
p.CloseAll()
|
||||||
|
|
||||||
// Give the background goroutine time to clean up
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
if p.IsAlive(name) {
|
if p.IsAlive(name) {
|
||||||
t.Fatal("IsAlive should return false after CloseAll")
|
t.Fatal("IsAlive should return false after CloseAll")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAttachCleansUpDeadSession(t *testing.T) {
|
|
||||||
name := "test-pool-dead-cleanup"
|
|
||||||
cleanup := setupTmuxSession(t, name)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
p := NewPool()
|
|
||||||
defer p.CloseAll()
|
|
||||||
|
|
||||||
sess1, err := p.Attach(name, 80, 24)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("First Attach failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kill the process to simulate a dead session
|
|
||||||
sess1.Cmd.Process.Kill()
|
|
||||||
sess1.Cmd.Process.Wait()
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Re-create the tmux session since killing the attach killed our handle
|
|
||||||
exec.Command("tmux", "kill-session", "-t", name).Run()
|
|
||||||
setupCmd := exec.Command("tmux", "new-session", "-d", "-s", name, "-x", "80", "-y", "24")
|
|
||||||
if err := setupCmd.Run(); err != nil {
|
|
||||||
t.Fatalf("failed to recreate tmux session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sess2, err := p.Attach(name, 80, 24)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Second Attach after dead session failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sess1 == sess2 {
|
|
||||||
t.Fatal("Attach should create new session when old one is dead")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+41
-134
@@ -5,102 +5,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Client-to-server message type tests ---
|
func TestParseAttachMessage(t *testing.T) {
|
||||||
|
data := []byte(`{"attach": "dev-server"}`)
|
||||||
func TestAttachMsgJSON(t *testing.T) {
|
|
||||||
msg := AttachMsg{Attach: "my-session"}
|
|
||||||
data, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal AttachMsg: %v", err)
|
|
||||||
}
|
|
||||||
var m map[string]interface{}
|
|
||||||
json.Unmarshal(data, &m)
|
|
||||||
if m["attach"] != "my-session" {
|
|
||||||
t.Errorf("expected attach=my-session, got %v", m["attach"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResizeMsgJSON(t *testing.T) {
|
|
||||||
msg := ResizeMsg{Resize: struct {
|
|
||||||
Cols int `json:"cols"`
|
|
||||||
Rows int `json:"rows"`
|
|
||||||
}{Cols: 80, Rows: 24}}
|
|
||||||
data, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal ResizeMsg: %v", err)
|
|
||||||
}
|
|
||||||
var m map[string]interface{}
|
|
||||||
json.Unmarshal(data, &m)
|
|
||||||
resize, ok := m["resize"].(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected resize to be a map, got %T", m["resize"])
|
|
||||||
}
|
|
||||||
if resize["cols"].(float64) != 80 {
|
|
||||||
t.Errorf("expected cols=80, got %v", resize["cols"])
|
|
||||||
}
|
|
||||||
if resize["rows"].(float64) != 24 {
|
|
||||||
t.Errorf("expected rows=24, got %v", resize["rows"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetachMsgJSON(t *testing.T) {
|
|
||||||
msg := DetachMsg{Detach: true}
|
|
||||||
data, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal DetachMsg: %v", err)
|
|
||||||
}
|
|
||||||
var m map[string]interface{}
|
|
||||||
json.Unmarshal(data, &m)
|
|
||||||
if m["detach"] != true {
|
|
||||||
t.Errorf("expected detach=true, got %v", m["detach"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Server-to-client message type tests ---
|
|
||||||
|
|
||||||
func TestAttachedMsgJSON(t *testing.T) {
|
|
||||||
msg := AttachedMsg{Attached: "sess1"}
|
|
||||||
data, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal AttachedMsg: %v", err)
|
|
||||||
}
|
|
||||||
var m map[string]interface{}
|
|
||||||
json.Unmarshal(data, &m)
|
|
||||||
if m["attached"] != "sess1" {
|
|
||||||
t.Errorf("expected attached=sess1, got %v", m["attached"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrorMsgJSON(t *testing.T) {
|
|
||||||
msg := ErrorMsg{Error: "something broke"}
|
|
||||||
data, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal ErrorMsg: %v", err)
|
|
||||||
}
|
|
||||||
var m map[string]interface{}
|
|
||||||
json.Unmarshal(data, &m)
|
|
||||||
if m["error"] != "something broke" {
|
|
||||||
t.Errorf("expected error='something broke', got %v", m["error"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExitedMsgJSON(t *testing.T) {
|
|
||||||
msg := ExitedMsg{Exited: "sess1"}
|
|
||||||
data, err := json.Marshal(msg)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal ExitedMsg: %v", err)
|
|
||||||
}
|
|
||||||
var m map[string]interface{}
|
|
||||||
json.Unmarshal(data, &m)
|
|
||||||
if m["exited"] != "sess1" {
|
|
||||||
t.Errorf("expected exited=sess1, got %v", m["exited"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ParseControlMessage tests ---
|
|
||||||
|
|
||||||
func TestParseControlMessage_Attach(t *testing.T) {
|
|
||||||
data := []byte(`{"attach":"dev"}`)
|
|
||||||
kind, val := ParseControlMessage(data)
|
kind, val := ParseControlMessage(data)
|
||||||
if kind != "attach" {
|
if kind != "attach" {
|
||||||
t.Fatalf("expected kind=attach, got %q", kind)
|
t.Fatalf("expected kind=attach, got %q", kind)
|
||||||
@@ -109,13 +15,13 @@ func TestParseControlMessage_Attach(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("expected *AttachMsg, got %T", val)
|
t.Fatalf("expected *AttachMsg, got %T", val)
|
||||||
}
|
}
|
||||||
if msg.Attach != "dev" {
|
if msg.Attach != "dev-server" {
|
||||||
t.Errorf("expected Attach=dev, got %q", msg.Attach)
|
t.Errorf("expected Attach=%q, got %q", "dev-server", msg.Attach)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseControlMessage_Resize(t *testing.T) {
|
func TestParseResizeMessage(t *testing.T) {
|
||||||
data := []byte(`{"resize":{"cols":120,"rows":40}}`)
|
data := []byte(`{"resize": {"cols": 120, "rows": 40}}`)
|
||||||
kind, val := ParseControlMessage(data)
|
kind, val := ParseControlMessage(data)
|
||||||
if kind != "resize" {
|
if kind != "resize" {
|
||||||
t.Fatalf("expected kind=resize, got %q", kind)
|
t.Fatalf("expected kind=resize, got %q", kind)
|
||||||
@@ -124,13 +30,16 @@ func TestParseControlMessage_Resize(t *testing.T) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("expected *ResizeMsg, got %T", val)
|
t.Fatalf("expected *ResizeMsg, got %T", val)
|
||||||
}
|
}
|
||||||
if msg.Resize.Cols != 120 || msg.Resize.Rows != 40 {
|
if msg.Resize.Cols != 120 {
|
||||||
t.Errorf("expected 120x40, got %dx%d", msg.Resize.Cols, msg.Resize.Rows)
|
t.Errorf("expected Cols=120, got %d", msg.Resize.Cols)
|
||||||
|
}
|
||||||
|
if msg.Resize.Rows != 40 {
|
||||||
|
t.Errorf("expected Rows=40, got %d", msg.Resize.Rows)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseControlMessage_Detach(t *testing.T) {
|
func TestParseDetachMessage(t *testing.T) {
|
||||||
data := []byte(`{"detach":true}`)
|
data := []byte(`{"detach": true}`)
|
||||||
kind, val := ParseControlMessage(data)
|
kind, val := ParseControlMessage(data)
|
||||||
if kind != "detach" {
|
if kind != "detach" {
|
||||||
t.Fatalf("expected kind=detach, got %q", kind)
|
t.Fatalf("expected kind=detach, got %q", kind)
|
||||||
@@ -144,19 +53,8 @@ func TestParseControlMessage_Detach(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseControlMessage_Unknown(t *testing.T) {
|
func TestParseInvalidJSON(t *testing.T) {
|
||||||
data := []byte(`{"foo":"bar"}`)
|
data := []byte(`not json`)
|
||||||
kind, val := ParseControlMessage(data)
|
|
||||||
if kind != "unknown" {
|
|
||||||
t.Errorf("expected kind=unknown, got %q", kind)
|
|
||||||
}
|
|
||||||
if val != nil {
|
|
||||||
t.Errorf("expected nil val for unknown, got %v", val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseControlMessage_Invalid(t *testing.T) {
|
|
||||||
data := []byte(`not json at all`)
|
|
||||||
kind, val := ParseControlMessage(data)
|
kind, val := ParseControlMessage(data)
|
||||||
if kind != "invalid" {
|
if kind != "invalid" {
|
||||||
t.Errorf("expected kind=invalid, got %q", kind)
|
t.Errorf("expected kind=invalid, got %q", kind)
|
||||||
@@ -166,37 +64,46 @@ func TestParseControlMessage_Invalid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Marshal function tests ---
|
func TestParseUnknownMessage(t *testing.T) {
|
||||||
|
data := []byte(`{"foo": "bar"}`)
|
||||||
|
kind, val := ParseControlMessage(data)
|
||||||
|
if kind != "unknown" {
|
||||||
|
t.Errorf("expected kind=unknown, got %q", kind)
|
||||||
|
}
|
||||||
|
if val != nil {
|
||||||
|
t.Errorf("expected nil val for unknown, got %v", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMarshalAttached(t *testing.T) {
|
func TestMarshalAttached(t *testing.T) {
|
||||||
data := MarshalAttached("sess1")
|
data := MarshalAttached("dev-server")
|
||||||
var m map[string]interface{}
|
var msg AttachedMsg
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
t.Fatalf("unmarshal: %v", err)
|
t.Fatalf("unmarshal: %v", err)
|
||||||
}
|
}
|
||||||
if m["attached"] != "sess1" {
|
if msg.Attached != "dev-server" {
|
||||||
t.Errorf("expected attached=sess1, got %v", m["attached"])
|
t.Errorf("expected Attached=%q, got %q", "dev-server", msg.Attached)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalError(t *testing.T) {
|
func TestMarshalError(t *testing.T) {
|
||||||
data := MarshalError("bad thing")
|
data := MarshalError("something broke")
|
||||||
var m map[string]interface{}
|
var msg ErrorMsg
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
t.Fatalf("unmarshal: %v", err)
|
t.Fatalf("unmarshal: %v", err)
|
||||||
}
|
}
|
||||||
if m["error"] != "bad thing" {
|
if msg.Error != "something broke" {
|
||||||
t.Errorf("expected error='bad thing', got %v", m["error"])
|
t.Errorf("expected Error=%q, got %q", "something broke", msg.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarshalExited(t *testing.T) {
|
func TestMarshalExited(t *testing.T) {
|
||||||
data := MarshalExited("sess1")
|
data := MarshalExited("dev-server")
|
||||||
var m map[string]interface{}
|
var msg ExitedMsg
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
if err := json.Unmarshal(data, &msg); err != nil {
|
||||||
t.Fatalf("unmarshal: %v", err)
|
t.Fatalf("unmarshal: %v", err)
|
||||||
}
|
}
|
||||||
if m["exited"] != "sess1" {
|
if msg.Exited != "dev-server" {
|
||||||
t.Errorf("expected exited=sess1, got %v", m["exited"])
|
t.Errorf("expected Exited=%q, got %q", "dev-server", msg.Exited)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-124
@@ -5,13 +5,8 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// makeToken creates a valid HMAC-SHA256 token: sig.timestamp
|
// makeToken creates a valid HMAC-SHA256 token: sig.timestamp
|
||||||
@@ -23,7 +18,7 @@ func makeToken(secret string, ts int64) string {
|
|||||||
return sig + "." + tsStr
|
return sig + "." + tsStr
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateToken_ValidToken(t *testing.T) {
|
func TestValidateToken_Valid(t *testing.T) {
|
||||||
secret := "test-secret"
|
secret := "test-secret"
|
||||||
ts := time.Now().Unix()
|
ts := time.Now().Unix()
|
||||||
token := makeToken(secret, ts)
|
token := makeToken(secret, ts)
|
||||||
@@ -33,139 +28,34 @@ func TestValidateToken_ValidToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateToken_ExpiredToken(t *testing.T) {
|
func TestValidateToken_Expired(t *testing.T) {
|
||||||
secret := "test-secret"
|
secret := "test-secret"
|
||||||
ts := time.Now().Unix() - 60 // 60 seconds ago, TTL is 30
|
ts := time.Now().Unix() - 60 // 60 seconds ago
|
||||||
token := makeToken(secret, ts)
|
token := makeToken(secret, ts)
|
||||||
|
|
||||||
if ValidateToken(token, secret, 30) {
|
if ValidateToken(token, secret, 30) {
|
||||||
t.Error("expected expired token to fail validation")
|
t.Error("expected expired token (60s ago) to be rejected")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateToken_FutureToken_WithinSkew(t *testing.T) {
|
func TestValidateToken_WrongSecret(t *testing.T) {
|
||||||
secret := "test-secret"
|
|
||||||
ts := time.Now().Unix() + 3 // 3 seconds in future, within 5s skew
|
|
||||||
token := makeToken(secret, ts)
|
|
||||||
|
|
||||||
if !ValidateToken(token, secret, 30) {
|
|
||||||
t.Error("expected near-future token within clock skew to pass")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateToken_FutureToken_BeyondSkew(t *testing.T) {
|
|
||||||
secret := "test-secret"
|
|
||||||
ts := time.Now().Unix() + 10 // 10 seconds in future, beyond 5s skew
|
|
||||||
token := makeToken(secret, ts)
|
|
||||||
|
|
||||||
if ValidateToken(token, secret, 30) {
|
|
||||||
t.Error("expected far-future token beyond clock skew to fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateToken_BadSignature(t *testing.T) {
|
|
||||||
ts := time.Now().Unix()
|
ts := time.Now().Unix()
|
||||||
token := makeToken("right-secret", ts)
|
token := makeToken("right-secret", ts)
|
||||||
|
|
||||||
if ValidateToken(token, "wrong-secret", 30) {
|
if ValidateToken(token, "wrong-secret", 30) {
|
||||||
t.Error("expected wrong secret to fail validation")
|
t.Error("expected token signed with wrong secret to be rejected")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateToken_MalformedToken(t *testing.T) {
|
func TestValidateToken_MalformedNoTimestamp(t *testing.T) {
|
||||||
if ValidateToken("not-a-valid-token", "secret", 30) {
|
// No dot separator — cannot split into sig.timestamp
|
||||||
t.Error("expected malformed token (no dot) to fail")
|
if ValidateToken("nodothere", "secret", 30) {
|
||||||
}
|
t.Error("expected malformed token (no dot) to be rejected")
|
||||||
if ValidateToken("abc.def", "secret", 30) {
|
|
||||||
t.Error("expected non-numeric timestamp to fail")
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateToken_EmptyToken(t *testing.T) {
|
||||||
if ValidateToken("", "secret", 30) {
|
if ValidateToken("", "secret", 30) {
|
||||||
t.Error("expected empty token to fail")
|
t.Error("expected empty token to be rejected")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleWebSocket_RejectsNoToken(t *testing.T) {
|
|
||||||
pool := NewPool()
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
HandleWebSocket(pool, "test-secret", w, r)
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
// Try to connect without a token
|
|
||||||
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "?token="
|
|
||||||
_, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected connection to fail without valid token")
|
|
||||||
}
|
|
||||||
if resp != nil && resp.StatusCode != http.StatusForbidden {
|
|
||||||
t.Errorf("expected 403 Forbidden, got %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleWebSocket_RejectsExpiredToken(t *testing.T) {
|
|
||||||
pool := NewPool()
|
|
||||||
secret := "test-secret"
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
HandleWebSocket(pool, secret, w, r)
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
ts := time.Now().Unix() - 60
|
|
||||||
token := makeToken(secret, ts)
|
|
||||||
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "?token=" + token
|
|
||||||
_, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("expected connection to fail with expired token")
|
|
||||||
}
|
|
||||||
if resp != nil && resp.StatusCode != http.StatusForbidden {
|
|
||||||
t.Errorf("expected 403 Forbidden, got %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleWebSocket_AcceptsValidToken(t *testing.T) {
|
|
||||||
pool := NewPool()
|
|
||||||
secret := "test-secret"
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
HandleWebSocket(pool, secret, w, r)
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
ts := time.Now().Unix()
|
|
||||||
token := makeToken(secret, ts)
|
|
||||||
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "?token=" + token
|
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected connection to succeed with valid token, got: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHandleWebSocket_DetachMessage(t *testing.T) {
|
|
||||||
pool := NewPool()
|
|
||||||
secret := "test-secret"
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
HandleWebSocket(pool, secret, w, r)
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
ts := time.Now().Unix()
|
|
||||||
token := makeToken(secret, ts)
|
|
||||||
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "?token=" + token
|
|
||||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected connection to succeed, got: %v", err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
// Send detach - should not crash, just clear state
|
|
||||||
err = conn.WriteMessage(websocket.TextMessage, []byte(`{"detach":true}`))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to send detach message: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSendSIGWINCH_NilProcess(t *testing.T) {
|
|
||||||
// Should not panic with nil Cmd or nil Process
|
|
||||||
sess := &Session{}
|
|
||||||
sendSIGWINCH(sess) // should not panic
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user