diff --git a/pkg/display/background_test.go b/pkg/display/background_test.go index 684aebc2..b2c922d3 100644 --- a/pkg/display/background_test.go +++ b/pkg/display/background_test.go @@ -109,3 +109,65 @@ func TestBackground_RegisterBackgroundActions_Ugly(t *testing.T) { assert.Equal(t, "card-01", payload["key"]) assert.Equal(t, map[string]any{"network": "visa", "last4": "4242"}, payload["details"]) } + +func TestBackground_AddSync_Good(t *testing.T) { + r := NewBackgroundRegistry() + source := map[string]any{"tag": "refresh", "kind": "sync"} + record := r.AddSync(source) + + require.NotNil(t, record) + assert.Equal(t, "refresh", record["tag"]) + assert.Equal(t, "sync", record["kind"]) + source["tag"] = "mutated" + assert.Equal(t, "refresh", record["tag"]) +} + +func TestBackground_AddSync_Bad(t *testing.T) { + r := NewBackgroundRegistry() + record := r.AddSync(nil) + + require.NotNil(t, record) + assert.Empty(t, record) +} + +func TestBackground_AddSync_Ugly(t *testing.T) { + r := NewBackgroundRegistry() + first := r.AddSync(map[string]any{"tag": "sync-1"}) + second := r.AddSync(map[string]any{"tag": "sync-2"}) + + require.NotNil(t, first) + require.NotNil(t, second) + assert.Len(t, r.syncRegistrations, 2) +} + +func TestBackground_AddPush_Good(t *testing.T) { + r := NewBackgroundRegistry() + source := map[string]any{"endpoint": "/push/abc", "auth": "core-local"} + record := r.AddPush(source) + + require.NotNil(t, record) + assert.Equal(t, "/push/abc", record["endpoint"]) + assert.Equal(t, "core-local", record["auth"]) + source["endpoint"] = "/push/mutated" + assert.Equal(t, "/push/abc", record["endpoint"]) +} + +func TestBackground_AddPush_Bad(t *testing.T) { + r := NewBackgroundRegistry() + record := r.AddPush(nil) + + require.NotNil(t, record) + assert.Empty(t, record) +} + +func TestBackground_AddPush_Ugly(t *testing.T) { + r := NewBackgroundRegistry() + first := r.AddPush(map[string]any{"endpoint": "/push/abc"}) + second := r.AddPush(map[string]any{"endpoint": "/push/def"}) + + require.NotNil(t, first) + require.NotNil(t, second) + assert.Len(t, r.pushSubscriptions, 2) + assert.Equal(t, "/push/abc", first["endpoint"]) + assert.Equal(t, "/push/def", second["endpoint"]) +} diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 30f93418..93bbd6ec 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -3,6 +3,7 @@ package display import ( "context" "os" + "reflect" "strings" "testing" @@ -392,6 +393,119 @@ func TestHandleWSMessage_SetWindowOpacity_Good(t *testing.T) { assert.InDelta(t, 0.35, info.Opacity, 0.0001) } +func TestDisplay_requireStringField_Good(t *testing.T) { + value, err := requireStringField(map[string]any{"window": "main"}, "window") + + require.NoError(t, err) + assert.Equal(t, "main", value) +} + +func TestDisplay_requireStringField_Bad(t *testing.T) { + value, err := requireStringField(map[string]any{"window": ""}, "window") + + require.Error(t, err) + assert.Empty(t, value) +} + +func TestDisplay_requireStringField_Ugly(t *testing.T) { + value, err := requireStringField(map[string]any{"window": 42}, "window") + + require.Error(t, err) + assert.Empty(t, value) +} + +func TestDisplay_optionsFromMap_Good(t *testing.T) { + opts := optionsFromMap(map[string]any{"alpha": "one", "beta": 2}) + + require.Equal(t, 2, opts.Len()) + got := map[string]any{} + for _, opt := range opts.Items() { + got[opt.Key] = opt.Value + } + assert.True(t, reflect.DeepEqual(map[string]any{"alpha": "one", "beta": 2}, got)) +} + +func TestDisplay_optionsFromMap_Bad(t *testing.T) { + opts := optionsFromMap(nil) + + require.NotNil(t, opts) + assert.Equal(t, 0, opts.Len()) +} + +func TestDisplay_optionsFromMap_Ugly(t *testing.T) { + opts := wsOptions(map[string]any{"nested": map[string]any{"value": "x"}}) + + require.Equal(t, 1, opts.Len()) + item := opts.Items()[0] + assert.Equal(t, "nested", item.Key) + assert.Equal(t, map[string]any{"value": "x"}, item.Value) +} + +func TestDisplay_handleWSMessage_Good(t *testing.T) { + c := newTestConclave(t) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("opacity-win")) + + result := svc.handleWSMessage(WSMessage{ + Action: "window:set-opacity", + Data: map[string]any{ + "name": "opacity-win", + "opacity": 0.55, + }, + }) + require.True(t, result.OK) + + info, err := svc.GetWindowInfo("opacity-win") + require.NoError(t, err) + require.NotNil(t, info) + assert.InDelta(t, 0.55, info.Opacity, 0.0001) +} + +func TestDisplay_handleWSMessage_Bad(t *testing.T) { + svc, _ := newTestDisplayService(t) + result := svc.handleWSMessage(WSMessage{Action: "unknown:action"}) + + require.False(t, result.OK) + assert.Contains(t, result.Value.(error).Error(), "unknown websocket action") +} + +func TestDisplay_handleWSMessage_Ugly(t *testing.T) { + svc, _ := newTestDisplayService(t) + result := svc.handleWSMessage(WSMessage{ + Action: "window:set-opacity", + Data: map[string]any{ + "name": "main", + }, + }) + + require.False(t, result.OK) + assert.Contains(t, result.Value.(error).Error(), "missing required field \"opacity\"") +} + +func TestDisplay_handleTrayAction_Good(t *testing.T) { + platform := window.NewMockPlatform() + c := core.New( + core.WithService(Register(nil)), + core.WithService(window.Register(platform)), + core.WithService(systray.Register(systray.NewMockPlatform())), + core.WithService(menu.Register(menu.NewMockPlatform())), + core.WithServiceLock(), + ) + require.True(t, c.ServiceStartup(context.Background(), nil).OK) + svc := core.MustServiceFor[*Service](c, "display") + _ = svc.OpenWindow(window.WithName("one")) + _ = svc.OpenWindow(window.WithName("two")) + + svc.handleTrayAction("open-desktop") + require.Len(t, platform.Windows, 2) + assert.True(t, platform.Windows[0].IsFocused()) + assert.True(t, platform.Windows[1].IsFocused()) + + svc.handleTrayAction("close-desktop") + assert.False(t, platform.Windows[0].IsVisible()) + assert.False(t, platform.Windows[1].IsVisible()) +} + func TestGetFocusedWindow_Good(t *testing.T) { c := newTestConclave(t) svc := core.MustServiceFor[*Service](c, "display") diff --git a/pkg/display/events_test.go b/pkg/display/events_test.go index 54e729db..5e0d6424 100644 --- a/pkg/display/events_test.go +++ b/pkg/display/events_test.go @@ -166,6 +166,131 @@ func TestWSEventManager_HandleWebSocket_RejectsLoopbackSpoofedOrigin(t *testing. assert.Equal(t, http.StatusForbidden, recorder.Code) } +func TestEvents_trustedWebSocketOrigin_Good(t *testing.T) { + tests := []struct { + name string + req *http.Request + want bool + }{ + { + name: "localhost without origin", + req: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/events", nil) + r.RemoteAddr = "127.0.0.1:12345" + return r + }(), + want: true, + }, + { + name: "local origin", + req: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/events", nil) + r.RemoteAddr = "127.0.0.1:12345" + r.Header.Set("Origin", "http://localhost:8080") + return r + }(), + want: true, + }, + { + name: "file origin", + req: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/events", nil) + r.RemoteAddr = "[::1]:12345" + r.Header.Set("Origin", "file://local") + return r + }(), + want: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, trustedWebSocketOrigin(tc.req)) + }) + } +} + +func TestEvents_trustedWebSocketOrigin_Bad(t *testing.T) { + tests := []struct { + name string + req *http.Request + }{ + { + name: "nil request", + }, + { + name: "wrong path", + req: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/other", nil) + r.RemoteAddr = "127.0.0.1:12345" + return r + }(), + }, + { + name: "remote client", + req: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/events", nil) + r.RemoteAddr = "203.0.113.10:2222" + return r + }(), + }, + { + name: "remote origin", + req: func() *http.Request { + r := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/events", nil) + r.RemoteAddr = "127.0.0.1:12345" + r.Header.Set("Origin", "https://evil.example") + return r + }(), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.False(t, trustedWebSocketOrigin(tc.req)) + }) + } +} + +func TestEvents_trustedWebSocketOrigin_Ugly(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://127.0.0.1/events", nil) + req.RemoteAddr = "127.0.0.1:12345" + req.Header.Set("Origin", "://bad") + + assert.False(t, trustedWebSocketOrigin(req)) + assert.False(t, trustedWebSocketOrigin(&http.Request{})) +} + +func TestEvents_trustedWebSocketHost_Good(t *testing.T) { + assert.True(t, trustedWebSocketHost("localhost")) + assert.True(t, trustedWebSocketHost("127.0.0.1:443")) + assert.True(t, trustedWebSocketHost("[::1]:80")) +} + +func TestEvents_trustedWebSocketHost_Bad(t *testing.T) { + assert.False(t, trustedWebSocketHost("")) + assert.False(t, trustedWebSocketHost("example.com")) +} + +func TestEvents_trustedWebSocketHost_Ugly(t *testing.T) { + assert.False(t, trustedWebSocketHost("not a host")) +} + +func TestEvents_isLoopbackHost_Good(t *testing.T) { + assert.True(t, isLoopbackHost("localhost")) + assert.True(t, isLoopbackHost("127.0.0.1")) + assert.True(t, isLoopbackHost("::1")) +} + +func TestEvents_isLoopbackHost_Bad(t *testing.T) { + assert.False(t, isLoopbackHost("")) + assert.False(t, isLoopbackHost("example.com")) +} + +func TestEvents_isLoopbackHost_Ugly(t *testing.T) { + assert.False(t, isLoopbackHost("203.0.113.10")) +} + func TestWSEventManager_HandleWebSocket_ClosesOnMalformedMessage(t *testing.T) { em := NewWSEventManager() conn, cleanup := dialWSEventManager(t, em) diff --git a/pkg/display/manifest_test.go b/pkg/display/manifest_test.go index 448e4d4b..52a7cf86 100644 --- a/pkg/display/manifest_test.go +++ b/pkg/display/manifest_test.go @@ -170,3 +170,45 @@ func TestManifest_LoadManifestForOrigin_RejectsOversizedFile(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "exceeds") } + +func TestManifest_ManifestBaseDir_Good(t *testing.T) { + assert.Equal(t, "/tmp/app", manifestBaseDir("/tmp/app/.core/view.yaml")) + assert.Equal(t, "/tmp/app/assets", manifestBaseDir("/tmp/app/assets/view.yaml")) +} + +func TestManifest_ManifestBaseDir_Bad(t *testing.T) { + assert.Equal(t, ".", manifestBaseDir(".core/view.yaml")) +} + +func TestManifest_ManifestBaseDir_Ugly(t *testing.T) { + assert.Equal(t, "/", manifestBaseDir("/.core/view.yaml")) +} + +func TestManifest_SafeManifestRelativePath_Good(t *testing.T) { + root := t.TempDir() + target := filepath.Join(root, "preload.js") + require.NoError(t, os.WriteFile(target, []byte("globalThis.ready = true;"), 0o644)) + expected, err := filepath.EvalSymlinks(target) + require.NoError(t, err) + + got, err := safeManifestRelativePath(root, "preload.js", "preload path") + + require.NoError(t, err) + assert.Equal(t, expected, got) +} + +func TestManifest_SafeManifestRelativePath_Bad(t *testing.T) { + root := t.TempDir() + + _, err := safeManifestRelativePath(root, "", "preload path") + require.Error(t, err) + assert.Contains(t, err.Error(), "empty") +} + +func TestManifest_SafeManifestRelativePath_Ugly(t *testing.T) { + root := t.TempDir() + + _, err := safeManifestRelativePath(root, "../escape.js", "preload path") + require.Error(t, err) + assert.Contains(t, err.Error(), "escapes") +} diff --git a/pkg/window/window_test.go b/pkg/window/window_test.go index e7d0de4f..cd6b3f1c 100644 --- a/pkg/window/window_test.go +++ b/pkg/window/window_test.go @@ -2,6 +2,7 @@ package window import ( + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -161,6 +162,34 @@ func TestManager_Remove_Good(t *testing.T) { assert.False(t, ok) } +func TestManager_NewManagerWithDir_Good(t *testing.T) { + dir := t.TempDir() + p := newMockPlatform() + + m := NewManagerWithDir(p, dir) + + require.NotNil(t, m) + assert.Same(t, p, m.Platform()) + assert.Equal(t, dir, m.State().dataDir()) + assert.Equal(t, filepath.Join(dir, "layouts.json"), m.Layout().filePath()) +} + +func TestManager_NewManagerWithDir_Bad(t *testing.T) { + m := NewManagerWithDir(nil, "") + + require.NotNil(t, m) + assert.Nil(t, m.Platform()) + assert.Empty(t, m.State().dataDir()) +} + +func TestManager_NewManagerWithDir_Ugly(t *testing.T) { + dir := filepath.Join(t.TempDir(), "..", "workspace") + m := NewManagerWithDir(nil, dir) + + require.NotNil(t, m) + assert.Equal(t, dir, m.State().dataDir()) +} + // --- Tiling Tests --- func TestTileMode_String_Good(t *testing.T) {