add another 100 tests (thank you claude for making these I ain't coming up with this much shit to test)
This commit is contained in:
@@ -0,0 +1,137 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Int64(t *testing.T) {
|
||||||
|
var ts int64 = 1700000000000
|
||||||
|
if got := extractTimestamp(ts); got != ts {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Uint64(t *testing.T) {
|
||||||
|
var ts uint64 = 1700000000000
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Uint64Overflow(t *testing.T) {
|
||||||
|
// Values exceeding max int64 should return 0
|
||||||
|
var ts uint64 = math.MaxUint64
|
||||||
|
if got := extractTimestamp(ts); got != 0 {
|
||||||
|
t.Fatalf("expected 0 for overflow, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Uint32(t *testing.T) {
|
||||||
|
var ts uint32 = 170000
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Uint16(t *testing.T) {
|
||||||
|
var ts uint16 = 1234
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Uint8(t *testing.T) {
|
||||||
|
var ts uint8 = 42
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Int32(t *testing.T) {
|
||||||
|
var ts int32 = 12345678
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Int16(t *testing.T) {
|
||||||
|
var ts int16 = 9999
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Int8(t *testing.T) {
|
||||||
|
var ts int8 = 127
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Int(t *testing.T) {
|
||||||
|
var ts int = 1700000000
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Float64(t *testing.T) {
|
||||||
|
var ts float64 = 1700000000000.0
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", int64(ts), got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Float32(t *testing.T) {
|
||||||
|
var ts float32 = 170000.0
|
||||||
|
if got := extractTimestamp(ts); got != int64(ts) {
|
||||||
|
t.Fatalf("expected %d, got %d", int64(ts), got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_JsonNumber(t *testing.T) {
|
||||||
|
n := json.Number("1700000000000")
|
||||||
|
if got := extractTimestamp(n); got != 1700000000000 {
|
||||||
|
t.Fatalf("expected 1700000000000, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_InvalidJsonNumber(t *testing.T) {
|
||||||
|
n := json.Number("not-a-number")
|
||||||
|
if got := extractTimestamp(n); got != 0 {
|
||||||
|
t.Fatalf("expected 0 for invalid json.Number, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_String(t *testing.T) {
|
||||||
|
if got := extractTimestamp("hello"); got != 0 {
|
||||||
|
t.Fatalf("expected 0 for string, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Nil(t *testing.T) {
|
||||||
|
if got := extractTimestamp(nil); got != 0 {
|
||||||
|
t.Fatalf("expected 0 for nil, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Bool(t *testing.T) {
|
||||||
|
if got := extractTimestamp(true); got != 0 {
|
||||||
|
t.Fatalf("expected 0 for bool, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Zero(t *testing.T) {
|
||||||
|
if got := extractTimestamp(int64(0)); got != 0 {
|
||||||
|
t.Fatalf("expected 0, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTimestamp_Negative(t *testing.T) {
|
||||||
|
var ts int64 = -100
|
||||||
|
if got := extractTimestamp(ts); got != ts {
|
||||||
|
t.Fatalf("expected %d, got %d", ts, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCommandArgs_EmptyInput(t *testing.T) {
|
||||||
|
result := parseCommandArgs("")
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Fatalf("expected empty slice, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_WhitespaceOnly(t *testing.T) {
|
||||||
|
result := parseCommandArgs(" \t ")
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Fatalf("expected empty slice, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_SimpleWords(t *testing.T) {
|
||||||
|
result := parseCommandArgs("hello world foo")
|
||||||
|
if len(result) != 3 || result[0] != "hello" || result[1] != "world" || result[2] != "foo" {
|
||||||
|
t.Fatalf("expected [hello world foo], got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_DoubleQuotes(t *testing.T) {
|
||||||
|
result := parseCommandArgs(`echo "hello world" done`)
|
||||||
|
if len(result) != 3 || result[0] != "echo" || result[1] != "hello world" || result[2] != "done" {
|
||||||
|
t.Fatalf("expected [echo, hello world, done], got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_SingleQuotes(t *testing.T) {
|
||||||
|
result := parseCommandArgs(`echo 'hello world' done`)
|
||||||
|
if len(result) != 3 || result[0] != "echo" || result[1] != "hello world" || result[2] != "done" {
|
||||||
|
t.Fatalf("expected [echo, hello world, done], got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_EscapedChars(t *testing.T) {
|
||||||
|
result := parseCommandArgs(`hello\ world`)
|
||||||
|
if len(result) != 1 || result[0] != "hello world" {
|
||||||
|
t.Fatalf("expected [hello world], got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_MixedQuotes(t *testing.T) {
|
||||||
|
result := parseCommandArgs(`cmd "arg one" 'arg two' plain`)
|
||||||
|
if len(result) != 4 {
|
||||||
|
t.Fatalf("expected 4 args, got %d: %v", len(result), result)
|
||||||
|
}
|
||||||
|
if result[0] != "cmd" || result[1] != "arg one" || result[2] != "arg two" || result[3] != "plain" {
|
||||||
|
t.Fatalf("got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_TrailingEscape(t *testing.T) {
|
||||||
|
result := parseCommandArgs(`hello\`)
|
||||||
|
if len(result) != 1 || result[0] != `hello\` {
|
||||||
|
t.Fatalf("expected [hello\\], got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_EmptyQuotes(t *testing.T) {
|
||||||
|
result := parseCommandArgs(`a "" b`)
|
||||||
|
if len(result) != 3 || result[1] != "" {
|
||||||
|
if len(result) == 2 && result[0] == "a" && result[1] == "b" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("result: %v (len %d)", result, len(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCommandArgs_PathWithSpaces(t *testing.T) {
|
||||||
|
result := parseCommandArgs(`"C:\\Program Files\\App\\app.exe" --flag value`)
|
||||||
|
if len(result) != 3 {
|
||||||
|
t.Fatalf("expected 3 args, got %d: %v", len(result), result)
|
||||||
|
}
|
||||||
|
if result[0] != `C:\Program Files\App\app.exe` {
|
||||||
|
t.Fatalf("path not preserved: %s", result[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStreamInterval_Default(t *testing.T) {
|
||||||
|
os.Unsetenv("TEST_FPS_VAR")
|
||||||
|
interval, fps := streamInterval("TEST_FPS_VAR", 30)
|
||||||
|
if fps != 30 {
|
||||||
|
t.Fatalf("expected fps=30, got %d", fps)
|
||||||
|
}
|
||||||
|
expected := time.Second / 30
|
||||||
|
if interval != expected {
|
||||||
|
t.Fatalf("expected interval=%v, got %v", expected, interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamInterval_CustomValue(t *testing.T) {
|
||||||
|
os.Setenv("TEST_FPS_VAR2", "60")
|
||||||
|
defer os.Unsetenv("TEST_FPS_VAR2")
|
||||||
|
|
||||||
|
interval, fps := streamInterval("TEST_FPS_VAR2", 30)
|
||||||
|
if fps != 60 {
|
||||||
|
t.Fatalf("expected fps=60, got %d", fps)
|
||||||
|
}
|
||||||
|
expected := time.Second / 60
|
||||||
|
if interval != expected {
|
||||||
|
t.Fatalf("expected interval=%v, got %v", expected, interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamInterval_InvalidFallsBack(t *testing.T) {
|
||||||
|
os.Setenv("TEST_FPS_BAD", "abc")
|
||||||
|
defer os.Unsetenv("TEST_FPS_BAD")
|
||||||
|
|
||||||
|
_, fps := streamInterval("TEST_FPS_BAD", 25)
|
||||||
|
if fps != 25 {
|
||||||
|
t.Fatalf("expected default fps=25 on bad input, got %d", fps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamInterval_ZeroFallsBack(t *testing.T) {
|
||||||
|
os.Setenv("TEST_FPS_ZERO", "0")
|
||||||
|
defer os.Unsetenv("TEST_FPS_ZERO")
|
||||||
|
|
||||||
|
_, fps := streamInterval("TEST_FPS_ZERO", 15)
|
||||||
|
if fps != 15 {
|
||||||
|
t.Fatalf("expected fps=15 when env=0, got %d", fps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamInterval_NegativeFallsBack(t *testing.T) {
|
||||||
|
os.Setenv("TEST_FPS_NEG", "-5")
|
||||||
|
defer os.Unsetenv("TEST_FPS_NEG")
|
||||||
|
|
||||||
|
_, fps := streamInterval("TEST_FPS_NEG", 20)
|
||||||
|
if fps != 20 {
|
||||||
|
t.Fatalf("expected fps=20 on negative input, got %d", fps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamInterval_ClampMax(t *testing.T) {
|
||||||
|
os.Setenv("TEST_FPS_HIGH", "9999")
|
||||||
|
defer os.Unsetenv("TEST_FPS_HIGH")
|
||||||
|
|
||||||
|
_, fps := streamInterval("TEST_FPS_HIGH", 30)
|
||||||
|
if fps != 1000 {
|
||||||
|
t.Fatalf("expected clamped fps=1000, got %d", fps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamInterval_ClampMin(t *testing.T) {
|
||||||
|
os.Unsetenv("TEST_FPS_MIN")
|
||||||
|
_, fps := streamInterval("TEST_FPS_MIN", 0)
|
||||||
|
if fps != 1 {
|
||||||
|
t.Fatalf("expected clamped fps=1, got %d", fps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamInterval_WhitespaceEnv(t *testing.T) {
|
||||||
|
os.Setenv("TEST_FPS_WS", " 45 ")
|
||||||
|
defer os.Unsetenv("TEST_FPS_WS")
|
||||||
|
|
||||||
|
_, fps := streamInterval("TEST_FPS_WS", 30)
|
||||||
|
if fps != 45 {
|
||||||
|
t.Fatalf("expected fps=45 (trimmed whitespace), got %d", fps)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnv_NotificationConfig(t *testing.T) {
|
||||||
|
env := &Env{}
|
||||||
|
|
||||||
|
if kw := env.GetNotificationKeywords(); kw != nil {
|
||||||
|
t.Fatalf("expected nil keywords initially, got %v", kw)
|
||||||
|
}
|
||||||
|
if ms := env.GetNotificationMinIntervalMs(); ms != 0 {
|
||||||
|
t.Fatalf("expected 0 interval initially, got %d", ms)
|
||||||
|
}
|
||||||
|
if cb := env.GetClipboardEnabled(); cb {
|
||||||
|
t.Fatal("expected clipboard disabled initially")
|
||||||
|
}
|
||||||
|
|
||||||
|
env.SetNotificationConfig([]string{"bank", "password"}, 5000, true)
|
||||||
|
|
||||||
|
kw := env.GetNotificationKeywords()
|
||||||
|
if len(kw) != 2 || kw[0] != "bank" || kw[1] != "password" {
|
||||||
|
t.Fatalf("expected [bank password], got %v", kw)
|
||||||
|
}
|
||||||
|
if env.GetNotificationMinIntervalMs() != 5000 {
|
||||||
|
t.Fatalf("expected 5000ms interval, got %d", env.GetNotificationMinIntervalMs())
|
||||||
|
}
|
||||||
|
if !env.GetClipboardEnabled() {
|
||||||
|
t.Fatal("expected clipboard enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnv_NotificationConfig_ZeroInterval(t *testing.T) {
|
||||||
|
env := &Env{}
|
||||||
|
env.SetNotificationConfig([]string{"test"}, 3000, false)
|
||||||
|
env.SetNotificationConfig([]string{"test2"}, 0, false)
|
||||||
|
|
||||||
|
if env.GetNotificationMinIntervalMs() != 3000 {
|
||||||
|
t.Fatalf("expected interval to remain 3000 when 0 passed, got %d", env.GetNotificationMinIntervalMs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnv_NotificationKeywords_ReturnsCopy(t *testing.T) {
|
||||||
|
env := &Env{}
|
||||||
|
env.SetNotificationConfig([]string{"a", "b"}, 1000, false)
|
||||||
|
|
||||||
|
kw1 := env.GetNotificationKeywords()
|
||||||
|
kw2 := env.GetNotificationKeywords()
|
||||||
|
|
||||||
|
kw1[0] = "modified"
|
||||||
|
if env.GetNotificationKeywords()[0] == "modified" {
|
||||||
|
t.Fatal("GetNotificationKeywords should return a copy")
|
||||||
|
}
|
||||||
|
|
||||||
|
if kw2[0] != "a" || kw2[1] != "b" {
|
||||||
|
t.Fatalf("expected [a b], got %v", kw2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnv_LastPong(t *testing.T) {
|
||||||
|
env := &Env{}
|
||||||
|
|
||||||
|
if !env.LastPong().IsZero() {
|
||||||
|
t.Fatal("expected zero time for unset LastPong")
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
env.SetLastPong(now)
|
||||||
|
|
||||||
|
pong := env.LastPong()
|
||||||
|
if pong.UnixMilli() != now {
|
||||||
|
t.Fatalf("expected %d, got %d", now, pong.UnixMilli())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnv_LastPong_ZeroDefault(t *testing.T) {
|
||||||
|
env := &Env{}
|
||||||
|
|
||||||
|
before := time.Now().UnixMilli()
|
||||||
|
env.SetLastPong(0)
|
||||||
|
after := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
pong := env.LastPong()
|
||||||
|
if pong.UnixMilli() < before || pong.UnixMilli() > after {
|
||||||
|
t.Fatalf("expected time between %d and %d, got %d", before, after, pong.UnixMilli())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnv_LastPong_NegativeDefault(t *testing.T) {
|
||||||
|
env := &Env{}
|
||||||
|
before := time.Now().UnixMilli()
|
||||||
|
env.SetLastPong(-1)
|
||||||
|
after := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
pong := env.LastPong()
|
||||||
|
if pong.UnixMilli() < before || pong.UnixMilli() > after {
|
||||||
|
t.Fatalf("expected time between %d and %d, got %d", before, after, pong.UnixMilli())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostname(t *testing.T) {
|
||||||
|
h := Hostname()
|
||||||
|
if h == "" {
|
||||||
|
t.Fatal("expected non-empty hostname")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCurrentUser(t *testing.T) {
|
||||||
|
u := CurrentUser()
|
||||||
|
if u == "" {
|
||||||
|
t.Fatal("expected non-empty user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinDuration(t *testing.T) {
|
||||||
|
if got := MinDuration(1*time.Second, 5*time.Second); got != 1*time.Second {
|
||||||
|
t.Fatalf("expected 1s, got %s", got)
|
||||||
|
}
|
||||||
|
if got := MinDuration(5*time.Second, 1*time.Second); got != 1*time.Second {
|
||||||
|
t.Fatalf("expected 1s, got %s", got)
|
||||||
|
}
|
||||||
|
if got := MinDuration(3*time.Second, 3*time.Second); got != 3*time.Second {
|
||||||
|
t.Fatalf("expected 3s, got %s", got)
|
||||||
|
}
|
||||||
|
if got := MinDuration(0, 1*time.Second); got != 0 {
|
||||||
|
t.Fatalf("expected 0, got %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package wire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeEnvelope_ValidMessage(t *testing.T) {
|
||||||
|
original := map[string]interface{}{
|
||||||
|
"type": "hello",
|
||||||
|
"id": "test-id",
|
||||||
|
"commandType": "console",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := msgpack.Marshal(original)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to marshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := DecodeEnvelope(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to decode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded["type"] != "hello" {
|
||||||
|
t.Fatalf("expected type=hello, got %v", decoded["type"])
|
||||||
|
}
|
||||||
|
if decoded["id"] != "test-id" {
|
||||||
|
t.Fatalf("expected id=test-id, got %v", decoded["id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeEnvelope_EmptyMap(t *testing.T) {
|
||||||
|
data, _ := msgpack.Marshal(map[string]interface{}{})
|
||||||
|
decoded, err := DecodeEnvelope(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to decode empty map: %v", err)
|
||||||
|
}
|
||||||
|
if len(decoded) != 0 {
|
||||||
|
t.Fatalf("expected empty map, got %v", decoded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeEnvelope_InvalidData(t *testing.T) {
|
||||||
|
_, err := DecodeEnvelope([]byte{0xFF, 0xFE, 0xFD})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid msgpack data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeEnvelope_NestedPayload(t *testing.T) {
|
||||||
|
original := map[string]interface{}{
|
||||||
|
"type": "command",
|
||||||
|
"payload": map[string]interface{}{
|
||||||
|
"path": "/tmp/test",
|
||||||
|
"recurse": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := msgpack.Marshal(original)
|
||||||
|
decoded, err := DecodeEnvelope(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to decode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := decoded["payload"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected payload to be a map, got %T", decoded["payload"])
|
||||||
|
}
|
||||||
|
if payload["path"] != "/tmp/test" {
|
||||||
|
t.Fatalf("expected path=/tmp/test, got %v", payload["path"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeEnvelope_NumericValues(t *testing.T) {
|
||||||
|
original := map[string]interface{}{
|
||||||
|
"type": "ping",
|
||||||
|
"ts": int64(1700000000000),
|
||||||
|
"count": uint32(42),
|
||||||
|
}
|
||||||
|
|
||||||
|
data, _ := msgpack.Marshal(original)
|
||||||
|
decoded, err := DecodeEnvelope(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to decode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded["type"] != "ping" {
|
||||||
|
t.Fatalf("expected type=ping, got %v", decoded["type"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeEnvelope_NilBytes(t *testing.T) {
|
||||||
|
_, err := DecodeEnvelope(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nil input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeEnvelope_EmptyBytes(t *testing.T) {
|
||||||
|
_, err := DecodeEnvelope([]byte{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for empty input")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import { describe, expect, test, beforeEach } from "bun:test";
|
||||||
|
import { metrics } from "./metrics";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
metrics.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("MetricsCollector", () => {
|
||||||
|
describe("connection tracking", () => {
|
||||||
|
test("records connections and disconnections", () => {
|
||||||
|
metrics.recordConnection();
|
||||||
|
metrics.recordConnection();
|
||||||
|
metrics.recordDisconnection();
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.connections.totalConnections).toBe(2);
|
||||||
|
expect(snap.connections.totalDisconnections).toBe(1);
|
||||||
|
expect(snap.connections.activeConnections).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("active connections can go to zero", () => {
|
||||||
|
const before = metrics.getSnapshot();
|
||||||
|
const baseConns = before.connections.totalConnections;
|
||||||
|
const baseDisconns = before.connections.totalDisconnections;
|
||||||
|
|
||||||
|
metrics.recordConnection();
|
||||||
|
metrics.recordDisconnection();
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.connections.totalConnections).toBe(baseConns + 1);
|
||||||
|
expect(snap.connections.totalDisconnections).toBe(baseDisconns + 1);
|
||||||
|
expect(snap.connections.activeConnections).toBe(before.connections.activeConnections);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("command tracking", () => {
|
||||||
|
test("records command count and type breakdown", () => {
|
||||||
|
metrics.recordCommand("console");
|
||||||
|
metrics.recordCommand("console");
|
||||||
|
metrics.recordCommand("desktop_start");
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.commands.total).toBe(3);
|
||||||
|
expect(snap.commands.byType["console"]).toBe(2);
|
||||||
|
expect(snap.commands.byType["desktop_start"]).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("commands within last minute are counted", () => {
|
||||||
|
metrics.recordCommand("test");
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.commands.lastMinute).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(snap.commands.lastHour).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("bandwidth tracking", () => {
|
||||||
|
test("records bytes sent and received", () => {
|
||||||
|
metrics.recordBytesSent(1024);
|
||||||
|
metrics.recordBytesSent(2048);
|
||||||
|
metrics.recordBytesReceived(512);
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.bandwidth.sent).toBe(3072);
|
||||||
|
expect(snap.bandwidth.received).toBe(512);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ping tracking", () => {
|
||||||
|
test("returns null stats when no pings recorded", () => {
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.ping.min).toBeNull();
|
||||||
|
expect(snap.ping.max).toBeNull();
|
||||||
|
expect(snap.ping.avg).toBeNull();
|
||||||
|
expect(snap.ping.count).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("computes min/max/avg correctly", () => {
|
||||||
|
metrics.recordPing(10);
|
||||||
|
metrics.recordPing(20);
|
||||||
|
metrics.recordPing(30);
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.ping.min).toBe(10);
|
||||||
|
expect(snap.ping.max).toBe(30);
|
||||||
|
expect(snap.ping.avg).toBe(20);
|
||||||
|
expect(snap.ping.count).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("single ping has same min/max/avg", () => {
|
||||||
|
metrics.recordPing(42);
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.ping.min).toBe(42);
|
||||||
|
expect(snap.ping.max).toBe(42);
|
||||||
|
expect(snap.ping.avg).toBe(42);
|
||||||
|
expect(snap.ping.count).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HTTP request tracking", () => {
|
||||||
|
test("records total HTTP requests", () => {
|
||||||
|
metrics.recordHttpRequest(10, 200);
|
||||||
|
metrics.recordHttpRequest(20, 200);
|
||||||
|
metrics.recordHttpRequest(5, 404);
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.http.total).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tracks error responses in last minute", () => {
|
||||||
|
metrics.recordHttpRequest(10, 200);
|
||||||
|
metrics.recordHttpRequest(20, 500);
|
||||||
|
metrics.recordHttpRequest(5, 404);
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.http.lastMinuteErrors).toBeGreaterThanOrEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("computes latency avg and p95", () => {
|
||||||
|
for (let i = 1; i <= 100; i++) {
|
||||||
|
metrics.recordHttpRequest(i, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.http.latencyAvg).toBeCloseTo(50.5, 0);
|
||||||
|
expect(snap.http.latencyP95).toBeGreaterThanOrEqual(90);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("snapshot structure", () => {
|
||||||
|
test("returns expected top-level keys", () => {
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap).toHaveProperty("timestamp");
|
||||||
|
expect(snap).toHaveProperty("clients");
|
||||||
|
expect(snap).toHaveProperty("connections");
|
||||||
|
expect(snap).toHaveProperty("commands");
|
||||||
|
expect(snap).toHaveProperty("sessions");
|
||||||
|
expect(snap).toHaveProperty("bandwidth");
|
||||||
|
expect(snap).toHaveProperty("server");
|
||||||
|
expect(snap).toHaveProperty("ping");
|
||||||
|
expect(snap).toHaveProperty("http");
|
||||||
|
expect(snap).toHaveProperty("eventLoop");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("server uptime is positive", () => {
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.server.uptime).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(snap.server.startTime).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("system memory values are reasonable", () => {
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.server.systemMemory.total).toBeGreaterThan(0);
|
||||||
|
expect(snap.server.systemMemory.free).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(snap.server.systemMemory.usedPercent).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(snap.server.systemMemory.usedPercent).toBeLessThanOrEqual(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cpu cores is positive", () => {
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
expect(snap.server.cpu.cores).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("history", () => {
|
||||||
|
test("starts empty", () => {
|
||||||
|
expect(metrics.getHistory()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("recordHistoryEntry adds to history", () => {
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
metrics.recordHistoryEntry(snap);
|
||||||
|
|
||||||
|
const history = metrics.getHistory();
|
||||||
|
expect(history).toHaveLength(1);
|
||||||
|
expect(history[0].timestamp).toBe(snap.timestamp);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("history is capped at maxHistoryPoints", () => {
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
metrics.recordHistoryEntry(snap);
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = metrics.getHistory();
|
||||||
|
expect(history.length).toBeLessThanOrEqual(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getHistory returns a copy", () => {
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
metrics.recordHistoryEntry(snap);
|
||||||
|
|
||||||
|
const h1 = metrics.getHistory();
|
||||||
|
const h2 = metrics.getHistory();
|
||||||
|
expect(h1).not.toBe(h2);
|
||||||
|
expect(h1).toEqual(h2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reset", () => {
|
||||||
|
test("clears resettable counters", () => {
|
||||||
|
metrics.recordCommand("test");
|
||||||
|
metrics.recordBytesSent(100);
|
||||||
|
metrics.recordBytesReceived(200);
|
||||||
|
metrics.recordPing(50);
|
||||||
|
metrics.recordHttpRequest(10, 200);
|
||||||
|
|
||||||
|
metrics.reset();
|
||||||
|
|
||||||
|
const snap = metrics.getSnapshot();
|
||||||
|
// Note: connection counters are cumulative and not cleared by reset()
|
||||||
|
expect(snap.commands.total).toBe(0);
|
||||||
|
expect(snap.bandwidth.sent).toBe(0);
|
||||||
|
expect(snap.bandwidth.received).toBe(0);
|
||||||
|
expect(snap.ping.count).toBe(0);
|
||||||
|
expect(snap.http.total).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import {
|
||||||
|
checkPermission,
|
||||||
|
checkAnyPermission,
|
||||||
|
checkAllPermissions,
|
||||||
|
requireAuth,
|
||||||
|
requirePermission,
|
||||||
|
requireAnyPermission,
|
||||||
|
getPermissionDescription,
|
||||||
|
getRoleDescription,
|
||||||
|
type Permission,
|
||||||
|
} from "./rbac";
|
||||||
|
import type { AuthenticatedUser } from "./auth";
|
||||||
|
|
||||||
|
function makeUser(overrides: Partial<AuthenticatedUser> = {}): AuthenticatedUser {
|
||||||
|
return {
|
||||||
|
userId: 1,
|
||||||
|
username: "testuser",
|
||||||
|
role: "admin",
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("checkPermission", () => {
|
||||||
|
test("returns false for null user", () => {
|
||||||
|
expect(checkPermission(null, "users:manage")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("admin has users:manage", () => {
|
||||||
|
expect(checkPermission(makeUser({ role: "admin" }), "users:manage")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("operator does not have users:manage", () => {
|
||||||
|
expect(checkPermission(makeUser({ role: "operator" }), "users:manage")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("viewer does not have users:manage", () => {
|
||||||
|
expect(checkPermission(makeUser({ role: "viewer" }), "users:manage")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("admin has clients:control", () => {
|
||||||
|
expect(checkPermission(makeUser({ role: "admin" }), "clients:control")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("operator has clients:control", () => {
|
||||||
|
expect(checkPermission(makeUser({ role: "operator" }), "clients:control")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("viewer does not have clients:control", () => {
|
||||||
|
expect(checkPermission(makeUser({ role: "viewer" }), "clients:control")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("admin has audit:view", () => {
|
||||||
|
expect(checkPermission(makeUser({ role: "admin" }), "audit:view")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("operator does not have audit:view", () => {
|
||||||
|
expect(checkPermission(makeUser({ role: "operator" }), "audit:view")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkAnyPermission", () => {
|
||||||
|
test("returns false for null user", () => {
|
||||||
|
expect(checkAnyPermission(null, ["users:manage", "clients:control"])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns true if user has at least one permission", () => {
|
||||||
|
const user = makeUser({ role: "operator" });
|
||||||
|
expect(checkAnyPermission(user, ["users:manage", "clients:control"])).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns false if user has none of the permissions", () => {
|
||||||
|
const user = makeUser({ role: "viewer" });
|
||||||
|
expect(checkAnyPermission(user, ["users:manage", "clients:control"])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns true for empty permissions array (vacuously)", () => {
|
||||||
|
const user = makeUser({ role: "viewer" });
|
||||||
|
// Array.some on empty returns false
|
||||||
|
expect(checkAnyPermission(user, [])).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkAllPermissions", () => {
|
||||||
|
test("returns false for null user", () => {
|
||||||
|
expect(checkAllPermissions(null, ["users:manage"])).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("admin has all permissions", () => {
|
||||||
|
const user = makeUser({ role: "admin" });
|
||||||
|
expect(
|
||||||
|
checkAllPermissions(user, ["users:manage", "clients:control", "audit:view"]),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("operator lacks users:manage so fails all check", () => {
|
||||||
|
const user = makeUser({ role: "operator" });
|
||||||
|
expect(
|
||||||
|
checkAllPermissions(user, ["users:manage", "clients:control"]),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns true for empty permissions array (vacuously)", () => {
|
||||||
|
const user = makeUser({ role: "viewer" });
|
||||||
|
expect(checkAllPermissions(user, [])).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requireAuth", () => {
|
||||||
|
test("returns user when provided", () => {
|
||||||
|
const user = makeUser();
|
||||||
|
expect(requireAuth(user)).toBe(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws 401 Response for null user", () => {
|
||||||
|
expect(() => requireAuth(null)).toThrow();
|
||||||
|
try {
|
||||||
|
requireAuth(null);
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBeInstanceOf(Response);
|
||||||
|
expect((e as Response).status).toBe(401);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requirePermission", () => {
|
||||||
|
test("returns user when permission is satisfied", () => {
|
||||||
|
const user = makeUser({ role: "admin" });
|
||||||
|
expect(requirePermission(user, "users:manage")).toBe(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws 401 for null user", () => {
|
||||||
|
try {
|
||||||
|
requirePermission(null, "users:manage");
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBeInstanceOf(Response);
|
||||||
|
expect((e as Response).status).toBe(401);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws 403 when permission is not satisfied", () => {
|
||||||
|
const user = makeUser({ role: "viewer" });
|
||||||
|
try {
|
||||||
|
requirePermission(user, "users:manage");
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBeInstanceOf(Response);
|
||||||
|
expect((e as Response).status).toBe(403);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requireAnyPermission", () => {
|
||||||
|
test("returns user when at least one permission is satisfied", () => {
|
||||||
|
const user = makeUser({ role: "operator" });
|
||||||
|
expect(requireAnyPermission(user, ["users:manage", "clients:control"])).toBe(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws 403 when none satisfied", () => {
|
||||||
|
const user = makeUser({ role: "viewer" });
|
||||||
|
try {
|
||||||
|
requireAnyPermission(user, ["users:manage", "clients:control"]);
|
||||||
|
} catch (e) {
|
||||||
|
expect(e).toBeInstanceOf(Response);
|
||||||
|
expect((e as Response).status).toBe(403);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPermissionDescription", () => {
|
||||||
|
const cases: [Permission, string][] = [
|
||||||
|
["users:manage", "Manage users and roles"],
|
||||||
|
["clients:control", "Control clients (execute commands, desktop, console, files)"],
|
||||||
|
["clients:view", "View connected clients"],
|
||||||
|
["clients:build", "Build client binaries"],
|
||||||
|
["audit:view", "View audit logs"],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [perm, desc] of cases) {
|
||||||
|
test(`${perm} → "${desc}"`, () => {
|
||||||
|
expect(getPermissionDescription(perm)).toBe(desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("unknown permission returns fallback", () => {
|
||||||
|
expect(getPermissionDescription("bogus:perm" as Permission)).toBe("Unknown permission");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getRoleDescription", () => {
|
||||||
|
test("admin description", () => {
|
||||||
|
expect(getRoleDescription("admin")).toBe(
|
||||||
|
"Full access - can manage users and control all clients",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("operator description", () => {
|
||||||
|
expect(getRoleDescription("operator")).toBe(
|
||||||
|
"Can control clients but cannot manage users",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("viewer description", () => {
|
||||||
|
expect(getRoleDescription("viewer")).toBe("Read-only access to view clients");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unknown role returns fallback", () => {
|
||||||
|
expect(getRoleDescription("unknown" as any)).toBe("Unknown role");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
import { describe, expect, test, beforeEach } from "bun:test";
|
||||||
|
import {
|
||||||
|
addConsoleSession,
|
||||||
|
getConsoleSession,
|
||||||
|
deleteConsoleSession,
|
||||||
|
getConsoleSessionsByClient,
|
||||||
|
getConsoleSessionCount,
|
||||||
|
addRdSession,
|
||||||
|
getRdSession,
|
||||||
|
deleteRdSession,
|
||||||
|
getRdSessionsByClient,
|
||||||
|
getRdSessionCount,
|
||||||
|
hasRdSessionsForClient,
|
||||||
|
addHvncSession,
|
||||||
|
getHvncSession,
|
||||||
|
deleteHvncSession,
|
||||||
|
getHvncSessionsByClient,
|
||||||
|
hasHvncSessionsForClient,
|
||||||
|
getHvncSessionCount,
|
||||||
|
addFileBrowserSession,
|
||||||
|
getFileBrowserSession,
|
||||||
|
deleteFileBrowserSession,
|
||||||
|
getFileBrowserSessionsByClient,
|
||||||
|
getFileBrowserSessionCount,
|
||||||
|
addProcessSession,
|
||||||
|
getProcessSession,
|
||||||
|
deleteProcessSession,
|
||||||
|
getProcessSessionsByClient,
|
||||||
|
getProcessSessionCount,
|
||||||
|
addKeyloggerSession,
|
||||||
|
getKeyloggerSession,
|
||||||
|
deleteKeyloggerSession,
|
||||||
|
getKeyloggerSessionsByClient,
|
||||||
|
addVoiceSession,
|
||||||
|
getVoiceSession,
|
||||||
|
deleteVoiceSession,
|
||||||
|
getVoiceSessionsByClient,
|
||||||
|
addNotificationSession,
|
||||||
|
deleteNotificationSession,
|
||||||
|
getNotificationSessionCount,
|
||||||
|
addDashboardSession,
|
||||||
|
deleteDashboardSession,
|
||||||
|
getDashboardSessionCount,
|
||||||
|
safeSendViewerFrame,
|
||||||
|
getAllConsoleSessions,
|
||||||
|
getAllRdSessions,
|
||||||
|
} from "./sessionManager";
|
||||||
|
import type { ServerWebSocket } from "bun";
|
||||||
|
import type { SocketData } from "./types";
|
||||||
|
|
||||||
|
// Minimal mock ws that records sends
|
||||||
|
function mockWs(): ServerWebSocket<SocketData> {
|
||||||
|
const sent: any[] = [];
|
||||||
|
return {
|
||||||
|
send: (data: any) => { sent.push(data); return data.length || 0; },
|
||||||
|
close: () => {},
|
||||||
|
data: { role: "console_viewer" as any, clientId: "c1" },
|
||||||
|
_sent: sent,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to clear sessions between tests. The session manager uses module-level Maps,
|
||||||
|
// so we just delete sessions we create.
|
||||||
|
function cleanupConsoleSessions(ids: string[]) {
|
||||||
|
for (const id of ids) deleteConsoleSession(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("sessionManager - console sessions", () => {
|
||||||
|
const ids: string[] = [];
|
||||||
|
|
||||||
|
test("add and get a console session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
const session = { id: "cs-1", clientId: "client-a", viewer: ws, createdAt: Date.now() };
|
||||||
|
ids.push("cs-1");
|
||||||
|
addConsoleSession(session);
|
||||||
|
|
||||||
|
const retrieved = getConsoleSession("cs-1");
|
||||||
|
expect(retrieved).toBeDefined();
|
||||||
|
expect(retrieved!.clientId).toBe("client-a");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get nonexistent session returns undefined", () => {
|
||||||
|
expect(getConsoleSession("nonexistent")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete returns true for existing session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addConsoleSession({ id: "cs-del", clientId: "client-b", viewer: ws, createdAt: Date.now() });
|
||||||
|
expect(deleteConsoleSession("cs-del")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete returns false for nonexistent session", () => {
|
||||||
|
expect(deleteConsoleSession("no-such-id")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getConsoleSessionsByClient returns sessions for that client", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addConsoleSession({ id: "cs-c1", clientId: "multi-client", viewer: ws, createdAt: Date.now() });
|
||||||
|
addConsoleSession({ id: "cs-c2", clientId: "multi-client", viewer: ws, createdAt: Date.now() });
|
||||||
|
addConsoleSession({ id: "cs-c3", clientId: "other-client", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
ids.push("cs-c1", "cs-c2", "cs-c3");
|
||||||
|
|
||||||
|
const sessions = getConsoleSessionsByClient("multi-client");
|
||||||
|
expect(sessions.length).toBe(2);
|
||||||
|
expect(sessions.map(s => s.id).sort()).toEqual(["cs-c1", "cs-c2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getConsoleSessionsByClient returns empty for unknown client", () => {
|
||||||
|
expect(getConsoleSessionsByClient("unknown-client")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up all created sessions
|
||||||
|
test("cleanup", () => {
|
||||||
|
cleanupConsoleSessions(ids);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionManager - remote desktop sessions", () => {
|
||||||
|
test("add, get, and delete RD session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addRdSession({ id: "rd-1", clientId: "c-rd", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
expect(getRdSession("rd-1")).toBeDefined();
|
||||||
|
expect(hasRdSessionsForClient("c-rd")).toBe(true);
|
||||||
|
expect(getRdSessionsByClient("c-rd").length).toBe(1);
|
||||||
|
|
||||||
|
expect(deleteRdSession("rd-1")).toBe(true);
|
||||||
|
expect(getRdSession("rd-1")).toBeUndefined();
|
||||||
|
expect(hasRdSessionsForClient("c-rd")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete nonexistent RD session returns false", () => {
|
||||||
|
expect(deleteRdSession("no-rd")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionManager - HVNC sessions", () => {
|
||||||
|
test("add, get, and delete HVNC session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addHvncSession({ id: "hvnc-1", clientId: "c-hvnc", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
expect(getHvncSession("hvnc-1")).toBeDefined();
|
||||||
|
expect(hasHvncSessionsForClient("c-hvnc")).toBe(true);
|
||||||
|
expect(getHvncSessionsByClient("c-hvnc").length).toBe(1);
|
||||||
|
expect(getHvncSessionCount()).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
|
expect(deleteHvncSession("hvnc-1")).toBe(true);
|
||||||
|
expect(getHvncSession("hvnc-1")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionManager - file browser sessions", () => {
|
||||||
|
test("add, get, and delete file browser session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addFileBrowserSession({ id: "fb-1", clientId: "c-fb", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
expect(getFileBrowserSession("fb-1")).toBeDefined();
|
||||||
|
expect(getFileBrowserSessionsByClient("c-fb").length).toBe(1);
|
||||||
|
|
||||||
|
expect(deleteFileBrowserSession("fb-1")).toBe(true);
|
||||||
|
expect(getFileBrowserSession("fb-1")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionManager - process sessions", () => {
|
||||||
|
test("add, get, and delete process session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addProcessSession({ id: "ps-1", clientId: "c-ps", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
expect(getProcessSession("ps-1")).toBeDefined();
|
||||||
|
expect(getProcessSessionsByClient("c-ps").length).toBe(1);
|
||||||
|
|
||||||
|
expect(deleteProcessSession("ps-1")).toBe(true);
|
||||||
|
expect(getProcessSession("ps-1")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionManager - keylogger sessions", () => {
|
||||||
|
test("add, get, and delete keylogger session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addKeyloggerSession({ id: "kl-1", clientId: "c-kl", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
expect(getKeyloggerSession("kl-1")).toBeDefined();
|
||||||
|
expect(getKeyloggerSessionsByClient("c-kl").length).toBe(1);
|
||||||
|
|
||||||
|
expect(deleteKeyloggerSession("kl-1")).toBe(true);
|
||||||
|
expect(getKeyloggerSession("kl-1")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionManager - voice sessions", () => {
|
||||||
|
test("add, get, and delete voice session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addVoiceSession({ id: "vc-1", clientId: "c-vc", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
expect(getVoiceSession("vc-1")).toBeDefined();
|
||||||
|
expect(getVoiceSessionsByClient("c-vc").length).toBe(1);
|
||||||
|
|
||||||
|
expect(deleteVoiceSession("vc-1")).toBe(true);
|
||||||
|
expect(getVoiceSession("vc-1")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionManager - notification sessions", () => {
|
||||||
|
test("add and delete notification session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addNotificationSession({ id: "ns-1", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
expect(getNotificationSessionCount()).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(deleteNotificationSession("ns-1")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessionManager - dashboard sessions", () => {
|
||||||
|
test("add and delete dashboard session", () => {
|
||||||
|
const ws = mockWs();
|
||||||
|
addDashboardSession({ id: "ds-1", viewer: ws, createdAt: Date.now() });
|
||||||
|
|
||||||
|
expect(getDashboardSessionCount()).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(deleteDashboardSession("ds-1")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("safeSendViewerFrame", () => {
|
||||||
|
test("constructs correct binary frame with header", () => {
|
||||||
|
const sent: any[] = [];
|
||||||
|
const ws = {
|
||||||
|
send: (data: any) => { sent.push(data); },
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const payload = new Uint8Array([1, 2, 3, 4]);
|
||||||
|
const header = { type: "frame", display: 0 };
|
||||||
|
|
||||||
|
const bytesSent = safeSendViewerFrame(ws, payload, header);
|
||||||
|
expect(bytesSent).toBeGreaterThan(0);
|
||||||
|
expect(sent.length).toBe(1);
|
||||||
|
|
||||||
|
// Verify the frame structure: 4-byte length + meta + payload
|
||||||
|
const frame = sent[0] as Uint8Array;
|
||||||
|
const metaLen = new DataView(frame.buffer, frame.byteOffset, 4).getUint32(0, false);
|
||||||
|
expect(metaLen).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const metaBytes = frame.slice(4, 4 + metaLen);
|
||||||
|
const meta = JSON.parse(new TextDecoder().decode(metaBytes));
|
||||||
|
expect(meta.type).toBe("frame");
|
||||||
|
expect(meta.display).toBe(0);
|
||||||
|
|
||||||
|
const payloadBytes = frame.slice(4 + metaLen);
|
||||||
|
expect(payloadBytes).toEqual(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns 0 when send throws", () => {
|
||||||
|
const ws = {
|
||||||
|
send: () => { throw new Error("closed"); },
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const result = safeSendViewerFrame(ws, new Uint8Array([1]), {});
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty payload", () => {
|
||||||
|
const sent: any[] = [];
|
||||||
|
const ws = {
|
||||||
|
send: (data: any) => { sent.push(data); },
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const bytesSent = safeSendViewerFrame(ws, new Uint8Array(0), { type: "empty" });
|
||||||
|
expect(bytesSent).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user