initial commit
This commit is contained in:
128
internal/server/freedesktop/actions.go
Normal file
128
internal/server/freedesktop/actions.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func (m *Manager) SetIconFile(iconPath string) error {
|
||||
if !m.state.Accounts.Available || m.accountsObj == nil {
|
||||
return fmt.Errorf("accounts service not available")
|
||||
}
|
||||
|
||||
err := m.accountsObj.Call(dbusAccountsUserInterface+".SetIconFile", 0, iconPath).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set icon file: %w", err)
|
||||
}
|
||||
|
||||
m.updateAccountsState()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetRealName(name string) error {
|
||||
if !m.state.Accounts.Available || m.accountsObj == nil {
|
||||
return fmt.Errorf("accounts service not available")
|
||||
}
|
||||
|
||||
err := m.accountsObj.Call(dbusAccountsUserInterface+".SetRealName", 0, name).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set real name: %w", err)
|
||||
}
|
||||
|
||||
m.updateAccountsState()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetEmail(email string) error {
|
||||
if !m.state.Accounts.Available || m.accountsObj == nil {
|
||||
return fmt.Errorf("accounts service not available")
|
||||
}
|
||||
|
||||
err := m.accountsObj.Call(dbusAccountsUserInterface+".SetEmail", 0, email).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set email: %w", err)
|
||||
}
|
||||
|
||||
m.updateAccountsState()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetLanguage(language string) error {
|
||||
if !m.state.Accounts.Available || m.accountsObj == nil {
|
||||
return fmt.Errorf("accounts service not available")
|
||||
}
|
||||
|
||||
err := m.accountsObj.Call(dbusAccountsUserInterface+".SetLanguage", 0, language).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set language: %w", err)
|
||||
}
|
||||
|
||||
m.updateAccountsState()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetLocation(location string) error {
|
||||
if !m.state.Accounts.Available || m.accountsObj == nil {
|
||||
return fmt.Errorf("accounts service not available")
|
||||
}
|
||||
|
||||
err := m.accountsObj.Call(dbusAccountsUserInterface+".SetLocation", 0, location).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set location: %w", err)
|
||||
}
|
||||
|
||||
m.updateAccountsState()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetUserIconFile(username string) (string, error) {
|
||||
if m.systemConn == nil {
|
||||
return "", fmt.Errorf("accounts service not available")
|
||||
}
|
||||
|
||||
accountsManager := m.systemConn.Object(dbusAccountsDest, dbus.ObjectPath(dbusAccountsPath))
|
||||
|
||||
var userPath dbus.ObjectPath
|
||||
err := accountsManager.Call(dbusAccountsInterface+".FindUserByName", 0, username).Store(&userPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
userObj := m.systemConn.Object(dbusAccountsDest, userPath)
|
||||
variant, err := userObj.GetProperty(dbusAccountsUserInterface + ".IconFile")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var iconFile string
|
||||
if err := variant.Store(&iconFile); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return iconFile, nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetIconTheme(iconTheme string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
check := exec.CommandContext(ctx, "gsettings", "writable", "org.gnome.desktop.interface", "icon-theme")
|
||||
if err := check.Run(); err == nil {
|
||||
cmd := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.desktop.interface", "icon-theme", iconTheme)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("gsettings set failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
checkDconf := exec.CommandContext(ctx, "dconf", "write", "/org/gnome/desktop/interface/icon-theme", fmt.Sprintf("'%s'", iconTheme))
|
||||
if err := checkDconf.Run(); err != nil {
|
||||
return fmt.Errorf("both gsettings and dconf unavailable or failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
145
internal/server/freedesktop/actions_test.go
Normal file
145
internal/server/freedesktop/actions_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestManager_SetIconFile(t *testing.T) {
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := manager.SetIconFile("/path/to/icon.png")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accounts service not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_SetRealName(t *testing.T) {
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := manager.SetRealName("New Name")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accounts service not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_SetEmail(t *testing.T) {
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := manager.SetEmail("test@example.com")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accounts service not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_SetLanguage(t *testing.T) {
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := manager.SetLanguage("en_US.UTF-8")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accounts service not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_SetLocation(t *testing.T) {
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := manager.SetLocation("Test Location")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accounts service not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_GetUserIconFile(t *testing.T) {
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
iconFile, err := manager.GetUserIconFile("testuser")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accounts service not available")
|
||||
assert.Empty(t, iconFile)
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_UpdateAccountsState(t *testing.T) {
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := manager.updateAccountsState()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "accounts service not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_UpdateSettingsState(t *testing.T) {
|
||||
t.Run("settings not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Settings: SettingsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
err := manager.updateSettingsState()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "settings portal not available")
|
||||
})
|
||||
}
|
||||
14
internal/server/freedesktop/constants.go
Normal file
14
internal/server/freedesktop/constants.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package freedesktop
|
||||
|
||||
const (
|
||||
dbusAccountsDest = "org.freedesktop.Accounts"
|
||||
dbusAccountsPath = "/org/freedesktop/Accounts"
|
||||
dbusAccountsInterface = "org.freedesktop.Accounts"
|
||||
dbusAccountsUserInterface = "org.freedesktop.Accounts.User"
|
||||
|
||||
dbusPortalDest = "org.freedesktop.portal.Desktop"
|
||||
dbusPortalPath = "/org/freedesktop/portal/desktop"
|
||||
dbusPortalSettingsInterface = "org.freedesktop.portal.Settings"
|
||||
|
||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||
)
|
||||
166
internal/server/freedesktop/handlers.go
Normal file
166
internal/server/freedesktop/handlers.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/danklinux/internal/server/models"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
ID int `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params map[string]interface{} `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type SuccessResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "freedesktop.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
case "freedesktop.accounts.setIconFile":
|
||||
handleSetIconFile(conn, req, manager)
|
||||
case "freedesktop.accounts.setRealName":
|
||||
handleSetRealName(conn, req, manager)
|
||||
case "freedesktop.accounts.setEmail":
|
||||
handleSetEmail(conn, req, manager)
|
||||
case "freedesktop.accounts.setLanguage":
|
||||
handleSetLanguage(conn, req, manager)
|
||||
case "freedesktop.accounts.setLocation":
|
||||
handleSetLocation(conn, req, manager)
|
||||
case "freedesktop.accounts.getUserIconFile":
|
||||
handleGetUserIconFile(conn, req, manager)
|
||||
case "freedesktop.settings.getColorScheme":
|
||||
handleGetColorScheme(conn, req, manager)
|
||||
case "freedesktop.settings.setIconTheme":
|
||||
handleSetIconTheme(conn, req, manager)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req Request, manager *Manager) {
|
||||
state := manager.GetState()
|
||||
models.Respond(conn, req.ID, state)
|
||||
}
|
||||
|
||||
func handleSetIconFile(conn net.Conn, req Request, manager *Manager) {
|
||||
iconPath, ok := req.Params["path"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'path' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetIconFile(iconPath); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "icon file set"})
|
||||
}
|
||||
|
||||
func handleSetRealName(conn net.Conn, req Request, manager *Manager) {
|
||||
name, ok := req.Params["name"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'name' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetRealName(name); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "real name set"})
|
||||
}
|
||||
|
||||
func handleSetEmail(conn net.Conn, req Request, manager *Manager) {
|
||||
email, ok := req.Params["email"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'email' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetEmail(email); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "email set"})
|
||||
}
|
||||
|
||||
func handleSetLanguage(conn net.Conn, req Request, manager *Manager) {
|
||||
language, ok := req.Params["language"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'language' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetLanguage(language); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "language set"})
|
||||
}
|
||||
|
||||
func handleSetLocation(conn net.Conn, req Request, manager *Manager) {
|
||||
location, ok := req.Params["location"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'location' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetLocation(location); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "location set"})
|
||||
}
|
||||
|
||||
func handleGetUserIconFile(conn net.Conn, req Request, manager *Manager) {
|
||||
username, ok := req.Params["username"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'username' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
iconFile, err := manager.GetUserIconFile(username)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Value: iconFile})
|
||||
}
|
||||
|
||||
func handleGetColorScheme(conn net.Conn, req Request, manager *Manager) {
|
||||
if err := manager.updateSettingsState(); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
state := manager.GetState()
|
||||
models.Respond(conn, req.ID, map[string]uint32{"colorScheme": state.Settings.ColorScheme})
|
||||
}
|
||||
|
||||
func handleSetIconTheme(conn net.Conn, req Request, manager *Manager) {
|
||||
iconTheme, ok := req.Params["iconTheme"].(string)
|
||||
if !ok {
|
||||
models.RespondError(conn, req.ID, "missing or invalid 'iconTheme' parameter")
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.SetIconTheme(iconTheme); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, SuccessResult{Success: true, Message: "icon theme set"})
|
||||
}
|
||||
581
internal/server/freedesktop/handlers_test.go
Normal file
581
internal/server/freedesktop/handlers_test.go
Normal file
@@ -0,0 +1,581 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
mockdbus "github.com/AvengeMedia/danklinux/internal/mocks/github.com/godbus/dbus/v5"
|
||||
"github.com/AvengeMedia/danklinux/internal/server/models"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockNetConn struct {
|
||||
net.Conn
|
||||
readBuf *bytes.Buffer
|
||||
writeBuf *bytes.Buffer
|
||||
closed bool
|
||||
}
|
||||
|
||||
func newMockNetConn() *mockNetConn {
|
||||
return &mockNetConn{
|
||||
readBuf: &bytes.Buffer{},
|
||||
writeBuf: &bytes.Buffer{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockNetConn) Read(b []byte) (n int, err error) {
|
||||
return m.readBuf.Read(b)
|
||||
}
|
||||
|
||||
func (m *mockNetConn) Write(b []byte) (n int, err error) {
|
||||
return m.writeBuf.Write(b)
|
||||
}
|
||||
|
||||
func (m *mockNetConn) Close() error {
|
||||
m.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockGetAllAccountsProperties() *dbus.Call {
|
||||
props := map[string]dbus.Variant{
|
||||
"IconFile": dbus.MakeVariant("/path/to/icon.png"),
|
||||
"RealName": dbus.MakeVariant("Test"),
|
||||
"UserName": dbus.MakeVariant("test"),
|
||||
"AccountType": dbus.MakeVariant(int32(0)),
|
||||
"HomeDirectory": dbus.MakeVariant("/home/test"),
|
||||
"Shell": dbus.MakeVariant("/bin/bash"),
|
||||
"Email": dbus.MakeVariant(""),
|
||||
"Language": dbus.MakeVariant(""),
|
||||
"Location": dbus.MakeVariant(""),
|
||||
"Locked": dbus.MakeVariant(false),
|
||||
"PasswordMode": dbus.MakeVariant(int32(1)),
|
||||
}
|
||||
return &dbus.Call{Err: nil, Body: []interface{}{props}}
|
||||
}
|
||||
|
||||
func TestRespondError_Freedesktop(t *testing.T) {
|
||||
conn := newMockNetConn()
|
||||
models.RespondError(conn, 123, "test error")
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Equal(t, "test error", resp.Error)
|
||||
assert.Nil(t, resp.Result)
|
||||
}
|
||||
|
||||
func TestRespond_Freedesktop(t *testing.T) {
|
||||
conn := newMockNetConn()
|
||||
result := SuccessResult{Success: true, Message: "test"}
|
||||
models.Respond(conn, 123, result)
|
||||
|
||||
var resp models.Response[SuccessResult]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Success)
|
||||
assert.Equal(t, "test", resp.Result.Message)
|
||||
}
|
||||
|
||||
func TestHandleGetState(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
UserName: "testuser",
|
||||
RealName: "Test User",
|
||||
UID: 1000,
|
||||
},
|
||||
Settings: SettingsState{
|
||||
Available: true,
|
||||
ColorScheme: 1,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{ID: 123, Method: "freedesktop.getState"}
|
||||
|
||||
handleGetState(conn, req, manager)
|
||||
|
||||
var resp models.Response[FreedeskState]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Accounts.Available)
|
||||
assert.Equal(t, "testuser", resp.Result.Accounts.UserName)
|
||||
assert.True(t, resp.Result.Settings.Available)
|
||||
assert.Equal(t, uint32(1), resp.Result.Settings.ColorScheme)
|
||||
}
|
||||
|
||||
func TestHandleSetIconFile(t *testing.T) {
|
||||
t.Run("missing path parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setIconFile",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleSetIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'path' parameter")
|
||||
})
|
||||
|
||||
t.Run("successful set icon file", func(t *testing.T) {
|
||||
mockAccountsObj := mockdbus.NewMockBusObject(t)
|
||||
mockCall := &dbus.Call{Err: nil}
|
||||
mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetIconFile", dbus.Flags(0), "/path/to/icon.png").Return(mockCall)
|
||||
mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties())
|
||||
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
accountsObj: mockAccountsObj,
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setIconFile",
|
||||
Params: map[string]interface{}{
|
||||
"path": "/path/to/icon.png",
|
||||
},
|
||||
}
|
||||
|
||||
handleSetIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[SuccessResult]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Success)
|
||||
assert.Equal(t, "icon file set", resp.Result.Message)
|
||||
})
|
||||
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setIconFile",
|
||||
Params: map[string]interface{}{
|
||||
"path": "/path/to/icon.png",
|
||||
},
|
||||
}
|
||||
|
||||
handleSetIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[SuccessResult]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "accounts service not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleSetRealName(t *testing.T) {
|
||||
t.Run("missing name parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setRealName",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleSetRealName(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'name' parameter")
|
||||
})
|
||||
|
||||
t.Run("successful set real name", func(t *testing.T) {
|
||||
mockAccountsObj := mockdbus.NewMockBusObject(t)
|
||||
mockCall := &dbus.Call{Err: nil}
|
||||
mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetRealName", dbus.Flags(0), "New Name").Return(mockCall)
|
||||
mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties())
|
||||
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
accountsObj: mockAccountsObj,
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setRealName",
|
||||
Params: map[string]interface{}{
|
||||
"name": "New Name",
|
||||
},
|
||||
}
|
||||
|
||||
handleSetRealName(conn, req, manager)
|
||||
|
||||
var resp models.Response[SuccessResult]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Success)
|
||||
assert.Equal(t, "real name set", resp.Result.Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleSetEmail(t *testing.T) {
|
||||
t.Run("missing email parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setEmail",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleSetEmail(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'email' parameter")
|
||||
})
|
||||
|
||||
t.Run("successful set email", func(t *testing.T) {
|
||||
mockAccountsObj := mockdbus.NewMockBusObject(t)
|
||||
mockCall := &dbus.Call{Err: nil}
|
||||
mockAccountsObj.EXPECT().Call("org.freedesktop.Accounts.User.SetEmail", dbus.Flags(0), "test@example.com").Return(mockCall)
|
||||
mockAccountsObj.EXPECT().CallWithContext(mock.Anything, "org.freedesktop.DBus.Properties.GetAll", dbus.Flags(0), "org.freedesktop.Accounts.User").Return(mockGetAllAccountsProperties())
|
||||
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
accountsObj: mockAccountsObj,
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setEmail",
|
||||
Params: map[string]interface{}{
|
||||
"email": "test@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
handleSetEmail(conn, req, manager)
|
||||
|
||||
var resp models.Response[SuccessResult]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Success)
|
||||
assert.Equal(t, "email set", resp.Result.Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleSetLanguage(t *testing.T) {
|
||||
t.Run("missing language parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setLanguage",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleSetLanguage(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'language' parameter")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleSetLocation(t *testing.T) {
|
||||
t.Run("missing location parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.setLocation",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleSetLocation(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'location' parameter")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleGetUserIconFile(t *testing.T) {
|
||||
t.Run("missing username parameter", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.getUserIconFile",
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
handleGetUserIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "missing or invalid 'username' parameter")
|
||||
})
|
||||
|
||||
t.Run("accounts not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.accounts.getUserIconFile",
|
||||
Params: map[string]interface{}{
|
||||
"username": "testuser",
|
||||
},
|
||||
}
|
||||
|
||||
handleGetUserIconFile(conn, req, manager)
|
||||
|
||||
var resp models.Response[SuccessResult]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "accounts service not available")
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleGetColorScheme(t *testing.T) {
|
||||
t.Run("settings not available", func(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Settings: SettingsState{
|
||||
Available: false,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
|
||||
|
||||
handleGetColorScheme(conn, req, manager)
|
||||
|
||||
var resp models.Response[map[string]uint32]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "settings portal not available")
|
||||
})
|
||||
|
||||
t.Run("successful get color scheme", func(t *testing.T) {
|
||||
mockSettingsObj := mockdbus.NewMockBusObject(t)
|
||||
mockCall := &dbus.Call{
|
||||
Err: nil,
|
||||
Body: []interface{}{dbus.MakeVariant(uint32(1))},
|
||||
}
|
||||
mockSettingsObj.EXPECT().Call("org.freedesktop.portal.Settings.ReadOne", dbus.Flags(0), "org.freedesktop.appearance", "color-scheme").Return(mockCall)
|
||||
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Settings: SettingsState{
|
||||
Available: true,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
settingsObj: mockSettingsObj,
|
||||
}
|
||||
|
||||
conn := newMockNetConn()
|
||||
req := Request{ID: 123, Method: "freedesktop.settings.getColorScheme"}
|
||||
|
||||
handleGetColorScheme(conn, req, manager)
|
||||
|
||||
var resp models.Response[map[string]uint32]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
require.NotNil(t, resp.Result)
|
||||
assert.Equal(t, uint32(1), (*resp.Result)["colorScheme"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleRequest(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
UserName: "testuser",
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
t.Run("unknown method", func(t *testing.T) {
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.unknown",
|
||||
}
|
||||
|
||||
HandleRequest(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Contains(t, resp.Error, "unknown method")
|
||||
})
|
||||
|
||||
t.Run("valid method - getState", func(t *testing.T) {
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: "freedesktop.getState",
|
||||
}
|
||||
|
||||
HandleRequest(conn, req, manager)
|
||||
|
||||
var resp models.Response[FreedeskState]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
assert.Empty(t, resp.Error)
|
||||
})
|
||||
|
||||
t.Run("all method routes", func(t *testing.T) {
|
||||
tests := []string{
|
||||
"freedesktop.accounts.setIconFile",
|
||||
"freedesktop.accounts.setRealName",
|
||||
"freedesktop.accounts.setEmail",
|
||||
"freedesktop.accounts.setLanguage",
|
||||
"freedesktop.accounts.setLocation",
|
||||
"freedesktop.accounts.getUserIconFile",
|
||||
"freedesktop.settings.getColorScheme",
|
||||
}
|
||||
|
||||
for _, method := range tests {
|
||||
conn := newMockNetConn()
|
||||
req := Request{
|
||||
ID: 123,
|
||||
Method: method,
|
||||
Params: map[string]interface{}{},
|
||||
}
|
||||
|
||||
HandleRequest(conn, req, manager)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(conn.writeBuf).Decode(&resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 123, resp.ID)
|
||||
// Will have errors due to missing params or service unavailable
|
||||
// but the method routing should work
|
||||
}
|
||||
})
|
||||
}
|
||||
251
internal/server/freedesktop/manager.go
Normal file
251
internal/server/freedesktop/manager.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
systemConn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||
}
|
||||
|
||||
sessionConn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
sessionConn = nil
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{},
|
||||
Settings: SettingsState{},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
systemConn: systemConn,
|
||||
sessionConn: sessionConn,
|
||||
currentUID: uint64(os.Getuid()),
|
||||
subscribers: make(map[string]chan FreedeskState),
|
||||
subMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
m.initializeAccounts()
|
||||
m.initializeSettings()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) initializeAccounts() error {
|
||||
accountsManager := m.systemConn.Object(dbusAccountsDest, dbus.ObjectPath(dbusAccountsPath))
|
||||
|
||||
var userPath dbus.ObjectPath
|
||||
err := accountsManager.Call(dbusAccountsInterface+".FindUserById", 0, int64(m.currentUID)).Store(&userPath)
|
||||
if err != nil {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Accounts.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
m.accountsObj = m.systemConn.Object(dbusAccountsDest, userPath)
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Accounts.Available = true
|
||||
m.state.Accounts.UserPath = string(userPath)
|
||||
m.state.Accounts.UID = m.currentUID
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
if err := m.updateAccountsState(); err != nil {
|
||||
return fmt.Errorf("failed to update accounts state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) initializeSettings() error {
|
||||
if m.sessionConn == nil {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Settings.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return fmt.Errorf("no session bus connection")
|
||||
}
|
||||
|
||||
m.settingsObj = m.sessionConn.Object(dbusPortalDest, dbus.ObjectPath(dbusPortalPath))
|
||||
|
||||
var variant dbus.Variant
|
||||
err := m.settingsObj.Call(dbusPortalSettingsInterface+".ReadOne", 0, "org.freedesktop.appearance", "color-scheme").Store(&variant)
|
||||
if err != nil {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Settings.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Settings.Available = true
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
if err := m.updateSettingsState(); err != nil {
|
||||
return fmt.Errorf("failed to update settings state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) updateAccountsState() error {
|
||||
if !m.state.Accounts.Available || m.accountsObj == nil {
|
||||
return fmt.Errorf("accounts service not available")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
props, err := m.getAccountProperties(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
if v, ok := props["IconFile"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.IconFile = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["RealName"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.RealName = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["UserName"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.UserName = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["AccountType"]; ok {
|
||||
if val, ok := v.Value().(int32); ok {
|
||||
m.state.Accounts.AccountType = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["HomeDirectory"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.HomeDirectory = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Shell"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Shell = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Email"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Email = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Language"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Language = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Location"]; ok {
|
||||
if val, ok := v.Value().(string); ok {
|
||||
m.state.Accounts.Location = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["Locked"]; ok {
|
||||
if val, ok := v.Value().(bool); ok {
|
||||
m.state.Accounts.Locked = val
|
||||
}
|
||||
}
|
||||
if v, ok := props["PasswordMode"]; ok {
|
||||
if val, ok := v.Value().(int32); ok {
|
||||
m.state.Accounts.PasswordMode = val
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) updateSettingsState() error {
|
||||
if !m.state.Settings.Available || m.settingsObj == nil {
|
||||
return fmt.Errorf("settings portal not available")
|
||||
}
|
||||
|
||||
var variant dbus.Variant
|
||||
err := m.settingsObj.Call(dbusPortalSettingsInterface+".ReadOne", 0, "org.freedesktop.appearance", "color-scheme").Store(&variant)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if colorScheme, ok := variant.Value().(uint32); ok {
|
||||
m.stateMutex.Lock()
|
||||
m.state.Settings.ColorScheme = colorScheme
|
||||
m.stateMutex.Unlock()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) getAccountProperties(ctx context.Context) (map[string]dbus.Variant, error) {
|
||||
var props map[string]dbus.Variant
|
||||
err := m.accountsObj.CallWithContext(ctx, dbusPropsInterface+".GetAll", 0, dbusAccountsUserInterface).Store(&props)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return props, nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() FreedeskState {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
return *m.state
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan FreedeskState {
|
||||
ch := make(chan FreedeskState, 64)
|
||||
m.subMutex.Lock()
|
||||
m.subscribers[id] = ch
|
||||
m.subMutex.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
m.subMutex.Lock()
|
||||
if ch, ok := m.subscribers[id]; ok {
|
||||
close(ch)
|
||||
delete(m.subscribers, id)
|
||||
}
|
||||
m.subMutex.Unlock()
|
||||
}
|
||||
|
||||
func (m *Manager) NotifySubscribers() {
|
||||
m.subMutex.RLock()
|
||||
defer m.subMutex.RUnlock()
|
||||
|
||||
state := m.GetState()
|
||||
for _, ch := range m.subscribers {
|
||||
select {
|
||||
case ch <- state:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
m.subMutex.Lock()
|
||||
for id, ch := range m.subscribers {
|
||||
close(ch)
|
||||
delete(m.subscribers, id)
|
||||
}
|
||||
m.subMutex.Unlock()
|
||||
|
||||
if m.systemConn != nil {
|
||||
m.systemConn.Close()
|
||||
}
|
||||
if m.sessionConn != nil {
|
||||
m.sessionConn.Close()
|
||||
}
|
||||
}
|
||||
143
internal/server/freedesktop/manager_test.go
Normal file
143
internal/server/freedesktop/manager_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestManager_GetState(t *testing.T) {
|
||||
state := &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
UserName: "testuser",
|
||||
RealName: "Test User",
|
||||
UID: 1000,
|
||||
},
|
||||
Settings: SettingsState{
|
||||
Available: true,
|
||||
ColorScheme: 1,
|
||||
},
|
||||
}
|
||||
|
||||
manager := &Manager{
|
||||
state: state,
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
result := manager.GetState()
|
||||
assert.True(t, result.Accounts.Available)
|
||||
assert.Equal(t, "testuser", result.Accounts.UserName)
|
||||
assert.Equal(t, "Test User", result.Accounts.RealName)
|
||||
assert.Equal(t, uint64(1000), result.Accounts.UID)
|
||||
assert.True(t, result.Settings.Available)
|
||||
assert.Equal(t, uint32(1), result.Settings.ColorScheme)
|
||||
}
|
||||
|
||||
func TestManager_GetState_ThreadSafe(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
UserName: "testuser",
|
||||
},
|
||||
Settings: SettingsState{
|
||||
Available: true,
|
||||
ColorScheme: 1,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
state := manager.GetState()
|
||||
assert.True(t, state.Accounts.Available)
|
||||
assert.Equal(t, "testuser", state.Accounts.UserName)
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_Close(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
systemConn: nil,
|
||||
sessionConn: nil,
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
manager.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
t.Run("attempts to create manager", func(t *testing.T) {
|
||||
manager, err := NewManager()
|
||||
if err != nil {
|
||||
assert.Nil(t, manager)
|
||||
} else {
|
||||
assert.NotNil(t, manager)
|
||||
assert.NotNil(t, manager.state)
|
||||
assert.NotNil(t, manager.systemConn)
|
||||
|
||||
manager.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_GetState_EmptyState(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
result := manager.GetState()
|
||||
assert.False(t, result.Accounts.Available)
|
||||
assert.Empty(t, result.Accounts.UserName)
|
||||
assert.False(t, result.Settings.Available)
|
||||
assert.Equal(t, uint32(0), result.Settings.ColorScheme)
|
||||
}
|
||||
|
||||
func TestManager_AccountsState_Modification(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
UserName: "testuser",
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
state := manager.GetState()
|
||||
state.Accounts.UserName = "modifieduser"
|
||||
|
||||
original := manager.GetState()
|
||||
assert.Equal(t, "testuser", original.Accounts.UserName)
|
||||
}
|
||||
|
||||
func TestManager_SettingsState_Modification(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Settings: SettingsState{
|
||||
Available: true,
|
||||
ColorScheme: 0,
|
||||
},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
state := manager.GetState()
|
||||
state.Settings.ColorScheme = 1
|
||||
|
||||
original := manager.GetState()
|
||||
assert.Equal(t, uint32(0), original.Settings.ColorScheme)
|
||||
}
|
||||
46
internal/server/freedesktop/types.go
Normal file
46
internal/server/freedesktop/types.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type AccountsState struct {
|
||||
Available bool `json:"available"`
|
||||
UserPath string `json:"userPath"`
|
||||
IconFile string `json:"iconFile"`
|
||||
RealName string `json:"realName"`
|
||||
UserName string `json:"userName"`
|
||||
AccountType int32 `json:"accountType"`
|
||||
HomeDirectory string `json:"homeDirectory"`
|
||||
Shell string `json:"shell"`
|
||||
Email string `json:"email"`
|
||||
Language string `json:"language"`
|
||||
Location string `json:"location"`
|
||||
Locked bool `json:"locked"`
|
||||
PasswordMode int32 `json:"passwordMode"`
|
||||
UID uint64 `json:"uid"`
|
||||
}
|
||||
|
||||
type SettingsState struct {
|
||||
Available bool `json:"available"`
|
||||
ColorScheme uint32 `json:"colorScheme"`
|
||||
}
|
||||
|
||||
type FreedeskState struct {
|
||||
Accounts AccountsState `json:"accounts"`
|
||||
Settings SettingsState `json:"settings"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
state *FreedeskState
|
||||
stateMutex sync.RWMutex
|
||||
systemConn *dbus.Conn
|
||||
sessionConn *dbus.Conn
|
||||
accountsObj dbus.BusObject
|
||||
settingsObj dbus.BusObject
|
||||
currentUID uint64
|
||||
subscribers map[string]chan FreedeskState
|
||||
subMutex sync.RWMutex
|
||||
}
|
||||
70
internal/server/freedesktop/types_test.go
Normal file
70
internal/server/freedesktop/types_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAccountsState_Struct(t *testing.T) {
|
||||
state := AccountsState{
|
||||
Available: true,
|
||||
UserPath: "/org/freedesktop/Accounts/User1000",
|
||||
RealName: "Test User",
|
||||
UserName: "testuser",
|
||||
Locked: false,
|
||||
UID: 1000,
|
||||
}
|
||||
|
||||
assert.True(t, state.Available)
|
||||
assert.Equal(t, "/org/freedesktop/Accounts/User1000", state.UserPath)
|
||||
assert.Equal(t, "Test User", state.RealName)
|
||||
assert.Equal(t, "testuser", state.UserName)
|
||||
assert.Equal(t, uint64(1000), state.UID)
|
||||
assert.False(t, state.Locked)
|
||||
}
|
||||
|
||||
func TestSettingsState_Struct(t *testing.T) {
|
||||
state := SettingsState{
|
||||
Available: true,
|
||||
ColorScheme: 1, // Dark mode
|
||||
}
|
||||
|
||||
assert.True(t, state.Available)
|
||||
assert.Equal(t, uint32(1), state.ColorScheme)
|
||||
}
|
||||
|
||||
func TestFreedeskState_Struct(t *testing.T) {
|
||||
state := FreedeskState{
|
||||
Accounts: AccountsState{
|
||||
Available: true,
|
||||
UserName: "testuser",
|
||||
UID: 1000,
|
||||
},
|
||||
Settings: SettingsState{
|
||||
Available: true,
|
||||
ColorScheme: 0, // Light mode
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, state.Accounts.Available)
|
||||
assert.Equal(t, "testuser", state.Accounts.UserName)
|
||||
assert.True(t, state.Settings.Available)
|
||||
assert.Equal(t, uint32(0), state.Settings.ColorScheme)
|
||||
}
|
||||
|
||||
func TestAccountsState_DefaultValues(t *testing.T) {
|
||||
state := AccountsState{}
|
||||
|
||||
assert.False(t, state.Available)
|
||||
assert.Empty(t, state.UserPath)
|
||||
assert.Empty(t, state.UserName)
|
||||
assert.Equal(t, uint64(0), state.UID)
|
||||
}
|
||||
|
||||
func TestSettingsState_DefaultValues(t *testing.T) {
|
||||
state := SettingsState{}
|
||||
|
||||
assert.False(t, state.Available)
|
||||
assert.Equal(t, uint32(0), state.ColorScheme)
|
||||
}
|
||||
Reference in New Issue
Block a user