mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-16 16:15:23 -04:00
988b54515e
* feat(tailscale): add connect/disconnect/exit-node/LAN-access backend The Tailscale backend previously exposed only read-only status (tailscale.getStatus, tailscale.refresh). This adds write actions through the existing tailscale.com/client/local integration: - tailscale.connect / tailscale.disconnect (EditPrefs WantRunning) - tailscale.setExitNode (EditPrefs ExitNodeID; empty id clears it and any legacy ExitNodeIP, mirroring `tailscale set --exit-node`) - tailscale.setAllowLanAccess (EditPrefs ExitNodeAllowLANAccess) The manager's client interface gains GetPrefs/EditPrefs; fetchState merges ExitNodeAllowLANAccess from prefs, and Peer exposes ExitNodeOption so the UI can list exit-node-capable peers. * feat(tailscale): expose the new actions in TailscaleService Adds connectTailscale/disconnectTailscale, setExitNode/clearExitNode and setAllowLanAccess wrappers, plus derived exitNodeOptions/currentExitNode and the exitNodeAllowLanAccess state. Write-action errors surface via ToastService. * feat(tailscale): add connection, exit-node and LAN-access controls to the widget The control-center widget toggle was a no-op. It now connects/disconnects, and the detail panel gains a connection status row with a connect/disconnect button, an exit-node picker and a LAN-access toggle.
156 lines
4.1 KiB
Go
156 lines
4.1 KiB
Go
package tailscale
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnstate"
|
|
)
|
|
|
|
type mockConn struct {
|
|
*bytes.Buffer
|
|
}
|
|
|
|
func (m *mockConn) Close() error { return nil }
|
|
func (m *mockConn) LocalAddr() net.Addr { return nil }
|
|
func (m *mockConn) RemoteAddr() net.Addr { return nil }
|
|
func (m *mockConn) SetDeadline(t time.Time) error { return nil }
|
|
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
func handlerTestManager() *Manager {
|
|
client := &mockClient{
|
|
watchFn: func(ctx context.Context, mask ipn.NotifyWatchOpt) (ipnBusWatcher, error) {
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
},
|
|
statusFn: func(ctx context.Context) (*ipnstate.Status, error) {
|
|
return runningStatus(), nil
|
|
},
|
|
}
|
|
m := newManager(client)
|
|
m.RefreshState()
|
|
return m
|
|
}
|
|
|
|
func TestHandleGetStatus(t *testing.T) {
|
|
m := handlerTestManager()
|
|
defer m.Close()
|
|
|
|
buf := &bytes.Buffer{}
|
|
conn := &mockConn{Buffer: buf}
|
|
|
|
req := models.Request{ID: 1, Method: "tailscale.getStatus"}
|
|
handleGetStatus(conn, req, m)
|
|
|
|
var resp models.Response[TailscaleState]
|
|
err := json.NewDecoder(buf).Decode(&resp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, resp.ID)
|
|
assert.NotNil(t, resp.Result)
|
|
assert.True(t, resp.Result.Connected)
|
|
assert.Equal(t, "cachyos", resp.Result.Self.Hostname)
|
|
}
|
|
|
|
func TestHandleRefresh(t *testing.T) {
|
|
m := handlerTestManager()
|
|
defer m.Close()
|
|
|
|
buf := &bytes.Buffer{}
|
|
conn := &mockConn{Buffer: buf}
|
|
|
|
req := models.Request{ID: 1, Method: "tailscale.refresh"}
|
|
handleRefresh(conn, req, m)
|
|
|
|
var resp models.Response[models.SuccessResult]
|
|
err := json.NewDecoder(buf).Decode(&resp)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, resp.ID)
|
|
assert.NotNil(t, resp.Result)
|
|
assert.True(t, resp.Result.Success)
|
|
}
|
|
|
|
func TestHandleActions(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
method string
|
|
params map[string]any
|
|
}{
|
|
{"connect", "tailscale.connect", nil},
|
|
{"disconnect", "tailscale.disconnect", nil},
|
|
{"setExitNode", "tailscale.setExitNode", map[string]any{"id": "nABC123"}},
|
|
{"clearExitNode", "tailscale.setExitNode", map[string]any{"id": ""}},
|
|
{"setAllowLanAccess", "tailscale.setAllowLanAccess", map[string]any{"enabled": true}},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
m := handlerTestManager()
|
|
defer m.Close()
|
|
|
|
buf := &bytes.Buffer{}
|
|
conn := &mockConn{Buffer: buf}
|
|
|
|
req := models.Request{ID: 1, Method: tc.method, Params: tc.params}
|
|
HandleRequest(conn, req, m)
|
|
|
|
var resp models.Response[models.SuccessResult]
|
|
require.NoError(t, json.NewDecoder(buf).Decode(&resp))
|
|
assert.Equal(t, 1, resp.ID)
|
|
assert.Empty(t, resp.Error)
|
|
require.NotNil(t, resp.Result)
|
|
assert.True(t, resp.Result.Success)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleAction_BackendError(t *testing.T) {
|
|
client := &mockClient{
|
|
watchFn: blockingWatch,
|
|
statusFn: func(ctx context.Context) (*ipnstate.Status, error) { return runningStatus(), nil },
|
|
editPrefsFn: func(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
|
return nil, fmt.Errorf("backend rejected edit")
|
|
},
|
|
}
|
|
m := newManager(client)
|
|
defer m.Close()
|
|
|
|
buf := &bytes.Buffer{}
|
|
conn := &mockConn{Buffer: buf}
|
|
|
|
req := models.Request{ID: 1, Method: "tailscale.connect"}
|
|
HandleRequest(conn, req, m)
|
|
|
|
var resp models.Response[models.SuccessResult]
|
|
require.NoError(t, json.NewDecoder(buf).Decode(&resp))
|
|
assert.Nil(t, resp.Result)
|
|
assert.Contains(t, resp.Error, "backend rejected edit")
|
|
}
|
|
|
|
func TestHandleRequest_UnknownMethod(t *testing.T) {
|
|
m := handlerTestManager()
|
|
defer m.Close()
|
|
|
|
buf := &bytes.Buffer{}
|
|
conn := &mockConn{Buffer: buf}
|
|
|
|
req := models.Request{ID: 1, Method: "tailscale.unknownMethod"}
|
|
HandleRequest(conn, req, m)
|
|
|
|
var resp models.Response[any]
|
|
err := json.NewDecoder(buf).Decode(&resp)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, resp.Result)
|
|
assert.NotEmpty(t, resp.Error)
|
|
assert.Contains(t, resp.Error, "unknown method")
|
|
}
|