gui/pkg/notification/service_test.go
Claude 9bd0e7b025
Some checks failed
Security Scan / security (push) Failing after 27s
refactor: remove banned imports from test files
- window/persistence_test.go: path/filepath → core.JoinPath
- display/display_test.go: path/filepath → core.JoinPath/PathDir
- browser/service_test.go: errors → core.NewError
- notification/service_test.go: errors → core.NewError

Zero banned imports in ALL gui files — production AND test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:53:40 +01:00

260 lines
8.3 KiB
Go

// pkg/notification/service_test.go
package notification
import (
"context"
core "dappco.re/go/core"
"testing"
"forge.lthn.ai/core/gui/pkg/dialog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockPlatform struct {
sendErr error
permGranted bool
permErr error
revokeErr error
revokeCalled bool
lastOpts NotificationOptions
sendCalled bool
}
func (m *mockPlatform) Send(opts NotificationOptions) error {
m.sendCalled = true
m.lastOpts = opts
return m.sendErr
}
func (m *mockPlatform) RequestPermission() (bool, error) { return m.permGranted, m.permErr }
func (m *mockPlatform) CheckPermission() (bool, error) { return m.permGranted, m.permErr }
func (m *mockPlatform) RevokePermission() error {
m.revokeCalled = true
return m.revokeErr
}
// mockDialogPlatform tracks whether MessageDialog was called (for fallback test).
type mockDialogPlatform struct {
messageCalled bool
lastMsgOpts dialog.MessageDialogOptions
}
func (m *mockDialogPlatform) OpenFile(opts dialog.OpenFileOptions) ([]string, error) { return nil, nil }
func (m *mockDialogPlatform) SaveFile(opts dialog.SaveFileOptions) (string, error) { return "", nil }
func (m *mockDialogPlatform) OpenDirectory(opts dialog.OpenDirectoryOptions) (string, error) {
return "", nil
}
func (m *mockDialogPlatform) MessageDialog(opts dialog.MessageDialogOptions) (string, error) {
m.messageCalled = true
m.lastMsgOpts = opts
return "OK", nil
}
func newTestService(t *testing.T) (*mockPlatform, *core.Core) {
t.Helper()
mock := &mockPlatform{permGranted: true}
c := core.New(
core.WithService(Register(mock)),
core.WithServiceLock(),
)
require.True(t, c.ServiceStartup(context.Background(), nil).OK)
return mock, c
}
func taskRun(c *core.Core, name string, task any) core.Result {
return c.Action(name).Run(context.Background(), core.NewOptions(
core.Option{Key: "task", Value: task},
))
}
func TestRegister_Good(t *testing.T) {
_, c := newTestService(t)
svc := core.MustServiceFor[*Service](c, "notification")
assert.NotNil(t, svc)
}
func TestTaskSend_Good(t *testing.T) {
mock, c := newTestService(t)
r := taskRun(c, "notification.send", TaskSend{
Options: NotificationOptions{Title: "Test", Message: "Hello"},
})
require.True(t, r.OK)
assert.True(t, mock.sendCalled)
assert.Equal(t, "Test", mock.lastOpts.Title)
}
func TestTaskSend_Fallback_Good(t *testing.T) {
// Platform fails -> falls back to dialog via IPC
mockNotify := &mockPlatform{sendErr: core.NewError("no permission")}
mockDlg := &mockDialogPlatform{}
c := core.New(
core.WithService(dialog.Register(mockDlg)),
core.WithService(Register(mockNotify)),
core.WithServiceLock(),
)
require.True(t, c.ServiceStartup(context.Background(), nil).OK)
r := taskRun(c, "notification.send", TaskSend{
Options: NotificationOptions{Title: "Warn", Message: "Oops", Severity: SeverityWarning},
})
assert.True(t, r.OK) // fallback succeeds even though platform failed
assert.True(t, mockDlg.messageCalled)
assert.Equal(t, dialog.DialogWarning, mockDlg.lastMsgOpts.Type)
}
func TestQueryPermission_Good(t *testing.T) {
_, c := newTestService(t)
r := c.QUERY(QueryPermission{})
require.True(t, r.OK)
status := r.Value.(PermissionStatus)
assert.True(t, status.Granted)
}
func TestTaskRequestPermission_Good(t *testing.T) {
_, c := newTestService(t)
r := c.Action("notification.requestPermission").Run(context.Background(), core.NewOptions())
require.True(t, r.OK)
assert.Equal(t, true, r.Value)
}
func TestTaskSend_Bad(t *testing.T) {
c := core.New(core.WithServiceLock())
r := c.Action("notification.send").Run(context.Background(), core.NewOptions())
assert.False(t, r.OK)
}
// --- TaskRevokePermission ---
func TestTaskRevokePermission_Good(t *testing.T) {
mock, c := newTestService(t)
r := c.Action("notification.revokePermission").Run(context.Background(), core.NewOptions())
require.True(t, r.OK)
assert.True(t, mock.revokeCalled)
}
func TestTaskRevokePermission_Bad(t *testing.T) {
mock, c := newTestService(t)
mock.revokeErr = core.NewError("cannot revoke")
r := c.Action("notification.revokePermission").Run(context.Background(), core.NewOptions())
assert.False(t, r.OK)
}
func TestTaskRevokePermission_Ugly(t *testing.T) {
// No service registered — action is not registered
c := core.New(core.WithServiceLock())
r := c.Action("notification.revokePermission").Run(context.Background(), core.NewOptions())
assert.False(t, r.OK)
}
// --- TaskRegisterCategory ---
func TestTaskRegisterCategory_Good(t *testing.T) {
_, c := newTestService(t)
category := NotificationCategory{
ID: "message",
Actions: []NotificationAction{
{ID: "reply", Title: "Reply"},
{ID: "delete", Title: "Delete", Destructive: true},
},
}
r := taskRun(c, "notification.registerCategory", TaskRegisterCategory{Category: category})
require.True(t, r.OK)
svc := core.MustServiceFor[*Service](c, "notification")
stored, ok := svc.categories["message"]
require.True(t, ok)
assert.Equal(t, 2, len(stored.Actions))
assert.Equal(t, "reply", stored.Actions[0].ID)
assert.True(t, stored.Actions[1].Destructive)
}
func TestTaskRegisterCategory_Bad(t *testing.T) {
// No service registered — action is not registered
c := core.New(core.WithServiceLock())
r := taskRun(c, "notification.registerCategory", TaskRegisterCategory{Category: NotificationCategory{ID: "x"}})
assert.False(t, r.OK)
}
func TestTaskRegisterCategory_Ugly(t *testing.T) {
// Re-registering a category replaces the previous one
_, c := newTestService(t)
first := NotificationCategory{ID: "chat", Actions: []NotificationAction{{ID: "a", Title: "A"}}}
second := NotificationCategory{ID: "chat", Actions: []NotificationAction{{ID: "b", Title: "B"}, {ID: "c", Title: "C"}}}
require.True(t, taskRun(c, "notification.registerCategory", TaskRegisterCategory{Category: first}).OK)
require.True(t, taskRun(c, "notification.registerCategory", TaskRegisterCategory{Category: second}).OK)
svc := core.MustServiceFor[*Service](c, "notification")
assert.Equal(t, 2, len(svc.categories["chat"].Actions))
assert.Equal(t, "b", svc.categories["chat"].Actions[0].ID)
}
// --- NotificationOptions with Actions ---
func TestTaskSend_WithActions_Good(t *testing.T) {
mock, c := newTestService(t)
options := NotificationOptions{
Title: "Team Chat",
Message: "New message from Alice",
CategoryID: "message",
Actions: []NotificationAction{
{ID: "reply", Title: "Reply"},
{ID: "dismiss", Title: "Dismiss"},
},
}
r := taskRun(c, "notification.send", TaskSend{Options: options})
require.True(t, r.OK)
assert.Equal(t, "message", mock.lastOpts.CategoryID)
assert.Equal(t, 2, len(mock.lastOpts.Actions))
}
// --- ActionNotificationActionTriggered ---
func TestActionNotificationActionTriggered_Good(t *testing.T) {
// ActionNotificationActionTriggered is broadcast by external code; confirm it can be received
_, c := newTestService(t)
var received *ActionNotificationActionTriggered
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if a, ok := msg.(ActionNotificationActionTriggered); ok {
received = &a
}
return core.Result{OK: true}
})
_ = c.ACTION(ActionNotificationActionTriggered{NotificationID: "n1", ActionID: "reply"})
require.NotNil(t, received)
assert.Equal(t, "n1", received.NotificationID)
assert.Equal(t, "reply", received.ActionID)
}
func TestActionNotificationDismissed_Good(t *testing.T) {
_, c := newTestService(t)
var received *ActionNotificationDismissed
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if a, ok := msg.(ActionNotificationDismissed); ok {
received = &a
}
return core.Result{OK: true}
})
_ = c.ACTION(ActionNotificationDismissed{ID: "n2"})
require.NotNil(t, received)
assert.Equal(t, "n2", received.ID)
}
func TestQueryPermission_Bad(t *testing.T) {
// No service — QUERY returns handled=false
c := core.New(core.WithServiceLock())
r := c.QUERY(QueryPermission{})
assert.False(t, r.OK)
}
func TestQueryPermission_Ugly(t *testing.T) {
// Platform returns error — QUERY returns OK=false (framework does not propagate Value for failed queries)
mock := &mockPlatform{permErr: core.NewError("platform error")}
c := core.New(
core.WithService(Register(mock)),
core.WithServiceLock(),
)
require.True(t, c.ServiceStartup(context.Background(), nil).OK)
r := c.QUERY(QueryPermission{})
assert.False(t, r.OK)
}