Files

221 lines
8.2 KiB
Go

package server
import (
"context"
"encoding/json"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard"
"github.com/AvengeMedia/dankgo/files"
"github.com/AvengeMedia/dankgo/ipc"
"github.com/stretchr/testify/require"
)
func TestRouteRequestUnavailableManagers(t *testing.T) {
tests := []struct {
method string
err string
}{
{"network.getState", "network manager not initialized"},
{"theme.auto.getState", "theme mode manager not initialized"},
{"wallpaper.getState", "wallpaper manager not initialized"},
{"loginctl.getState", "loginctl manager not initialized"},
{"freedesktop.getState", "freedesktop manager not initialized"},
{"wayland.gamma.getState", "wayland manager not initialized"},
{"bluetooth.getState", "bluetooth manager not initialized"},
{"browser.open", "apppicker manager not initialized"},
{"apppicker.open", "apppicker manager not initialized"},
{"tailscale.getState", "Tailscale not available"},
{"brightness.getState", "brightness manager not initialized"},
{"wlroutput.getState", "wlroutput manager not initialized"},
{"evdev.getState", "evdev manager not initialized"},
{"dbus.subscribe", "dbus manager not initialized"},
{"clipboard.getState", "clipboard manager not initialized"},
{"location.getState", "location manager not initialized"},
{"wellbeing.getState", "wellbeing manager not initialized"},
{"notify.invoke", "notification action manager not initialized"},
{"sysupdate.getState", "sysupdate manager not initialized"},
{"files.list", "files service not initialized"},
}
for _, tt := range tests {
t.Run(tt.method, func(t *testing.T) {
conn := &mockConn{}
RouteRequest(context.Background(), ipc.NewConnWriter(conn), ipc.Request{ID: 17, Method: tt.method})
require.JSONEq(t, `{"id":17,"error":"`+tt.err+`"}`, string(conn.written))
})
}
}
func TestRouteRequestUnknownMethods(t *testing.T) {
for _, method := range []string{"", "missing", "network", "networking.getState", "theme.auto", "matugen.missing", "plugins.missing", "themes.missing", "registries.missing", "mime.missing", "dgop.missing", "lyrics.missing"} {
t.Run(method, func(t *testing.T) {
conn := &mockConn{}
RouteRequest(context.Background(), ipc.NewConnWriter(conn), ipc.Request{ID: 18, Method: method})
require.JSONEq(t, `{"id":18,"error":"unknown method: `+method+`"}`, string(conn.written))
})
}
}
func TestRouteRequestExactMethods(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
conn := &mockConn{}
writer := ipc.NewConnWriter(conn)
RouteRequest(context.Background(), writer, ipc.Request{Method: "ping"})
require.JSONEq(t, `{"result":"pong"}`, string(conn.written))
conn.written = nil
RouteRequest(context.Background(), writer, ipc.Request{ID: 19, Method: "getServerInfo"})
var info ipc.Response[ServerInfo]
require.NoError(t, json.Unmarshal(conn.written, &info))
require.Equal(t, 19, info.ID)
require.NotNil(t, info.Result)
require.Equal(t, getServerInfo(), *info.Result)
conn.written = nil
RouteRequest(context.Background(), writer, ipc.Request{ID: 20, Method: "clipboard.setConfig", Params: map[string]any{"maxHistory": float64(42)}})
require.JSONEq(t, `{"id":20,"result":{"success":true,"message":"config updated"}}`, string(conn.written))
conn.written = nil
RouteRequest(context.Background(), writer, ipc.Request{ID: 21, Method: "clipboard.getConfig"})
var config ipc.Response[clipboard.Config]
require.NoError(t, json.Unmarshal(conn.written, &config))
require.Equal(t, 21, config.ID)
require.NotNil(t, config.Result)
expected := clipboard.DefaultConfig()
expected.MaxHistory = 42
require.Equal(t, expected, *config.Result)
}
func TestRouteRequestBrowserAlias(t *testing.T) {
original := appPickerManager
appPickerManager = apppicker.NewManager()
t.Cleanup(func() {
appPickerManager.Close()
appPickerManager = original
})
events := appPickerManager.Subscribe("route-test")
for _, method := range []string{"browser.open", "apppicker.open"} {
t.Run(method, func(t *testing.T) {
conn := &mockConn{}
RouteRequest(context.Background(), ipc.NewConnWriter(conn), ipc.Request{ID: 22, Method: method, Params: map[string]any{"url": "https://example.com"}})
require.JSONEq(t, `{"id":22,"result":"ok"}`, string(conn.written))
select {
case event := <-events:
require.Equal(t, apppicker.OpenEvent{Target: "https://example.com", RequestType: "url"}, event)
default:
t.Fatal("missing apppicker event")
}
})
}
conn := &mockConn{}
RouteRequest(context.Background(), ipc.NewConnWriter(conn), ipc.Request{ID: 23, Method: "browser.missing"})
require.JSONEq(t, `{"id":23,"error":"unknown method"}`, string(conn.written))
}
func useFilesService(t *testing.T) {
t.Helper()
original := filesService
filesService = files.NewService(&filesEvents, t.TempDir(), nil)
filesService.AttachOnOpen()
t.Cleanup(func() {
filesService.Close()
filesService = original
})
}
func TestRouteRequestFiles(t *testing.T) {
useFilesService(t)
root := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(root, "a.txt"), nil, 0o644))
conn := &mockConn{}
RouteRequest(context.Background(), ipc.NewConnWriter(conn), ipc.Request{ID: 24, Method: "files.list", Params: map[string]any{"path": root}})
var listed ipc.Response[struct {
Path string `json:"path"`
Entries []map[string]any `json:"entries"`
}]
require.NoError(t, json.Unmarshal(conn.written, &listed))
require.Equal(t, 24, listed.ID)
require.NotNil(t, listed.Result)
require.Equal(t, root, listed.Result.Path)
require.Len(t, listed.Result.Entries, 1)
require.Equal(t, "a.txt", listed.Result.Entries[0]["name"])
conn.written = nil
RouteRequest(context.Background(), ipc.NewConnWriter(conn), ipc.Request{ID: 25, Method: "files.missing"})
require.JSONEq(t, `{"id":25,"error":"unknown files method: files.missing","code":"NOTSUPPORTED"}`, string(conn.written))
}
func TestFilesWatchLivesAsLongAsTheConnection(t *testing.T) {
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
useFilesService(t)
s := New()
require.NoError(t, s.Listen())
ctx, cancel := context.WithCancel(context.Background())
served := make(chan error, 1)
go func() { served <- s.ipc.Serve(ctx) }()
t.Cleanup(func() {
cancel()
require.NoError(t, <-served)
})
conn, err := net.Dial("unix", s.SocketPath())
require.NoError(t, err)
defer conn.Close()
require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
decoder := json.NewDecoder(conn)
var caps ipc.Capabilities
require.NoError(t, decoder.Decode(&caps))
require.Contains(t, caps.Capabilities, "files")
require.NoError(t, json.NewEncoder(conn).Encode(ipc.Request{ID: 1, Method: "files.watch", Params: map[string]any{"path": t.TempDir()}}))
var opened ipc.Response[struct {
WatchID string `json:"watchId"`
}]
require.NoError(t, decoder.Decode(&opened))
require.NotNil(t, opened.Result)
watchID := opened.Result.WatchID
require.NotEmpty(t, watchID)
pageWatch := func() string {
mc := &mockConn{}
RouteRequest(context.Background(), ipc.NewConnWriter(mc), ipc.Request{ID: 2, Method: "files.list", Params: map[string]any{"watchId": watchID}})
return string(mc.written)
}
require.Contains(t, pageWatch(), `"watchId":"`+watchID+`"`, "the watch outlives the request that opened it")
require.NoError(t, conn.Close())
require.Eventually(t, func() bool {
return pageWatch() == `{"id":2,"error":"unknown watch: `+watchID+`","code":"EINVAL"}`+"\n"
}, 5*time.Second, 10*time.Millisecond, "closing the connection closes its watches")
}
func BenchmarkRouteRequest(b *testing.B) {
conn := &mockConn{}
writer := ipc.NewConnWriter(conn)
req := ipc.Request{ID: 1, Method: "ping"}
b.ReportAllocs()
for b.Loop() {
conn.written = conn.written[:0]
RouteRequest(context.Background(), writer, req)
}
}
// Routing only: the no-network invariant itself is TestLookupWithoutConsentNeverFetches.
func TestRouteLyricsGetWithoutConsentStaysLocal(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
conn := &mockConn{}
RouteRequest(context.Background(), ipc.NewConnWriter(conn), ipc.Request{ID: 19, Method: "lyrics.get", Params: map[string]any{
"title": "Snake Eater",
"artist": "Cynthia Harrell",
}})
require.Contains(t, string(conn.written), `"found":false`)
}