Add browser inbox receiver
This commit is contained in:
+21
-7
@@ -30,6 +30,11 @@ import (
|
||||
"github.com/verstak/verstak-desktop/internal/shell/debug"
|
||||
)
|
||||
|
||||
var newSyncClient = syncsvc.NewClient
|
||||
var emitFrontendEvent = runtime.EventsEmit
|
||||
|
||||
const pluginEventRuntimeName = "verstak:plugin-event"
|
||||
|
||||
// App is the main application struct exposed to the Wails frontend.
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
@@ -974,8 +979,8 @@ func (a *App) PublishPluginEvent(pluginID, eventName string, payload map[string]
|
||||
return ""
|
||||
}
|
||||
|
||||
// SubscribePluginEvent validates subscribe permission for a bundled frontend plugin.
|
||||
// Actual bundled event dispatch is handled by the frontend plugin host event bus.
|
||||
// SubscribePluginEvent validates subscribe permission and bridges backend events
|
||||
// into the bundled frontend plugin host.
|
||||
func (a *App) SubscribePluginEvent(pluginID, eventName string) string {
|
||||
if _, err := a.requirePluginAccess(pluginID, "events.subscribe"); err != nil {
|
||||
return err.Error()
|
||||
@@ -983,6 +988,15 @@ func (a *App) SubscribePluginEvent(pluginID, eventName string) string {
|
||||
if eventName == "" {
|
||||
return "event name is empty"
|
||||
}
|
||||
if a.eventBus != nil {
|
||||
a.eventBus.Subscribe(eventName, func(event events.Event) {
|
||||
emitFrontendEvent(a.ctx, pluginEventRuntimeName, map[string]interface{}{
|
||||
"name": event.Name,
|
||||
"timestamp": event.Timestamp,
|
||||
"payload": event.Payload,
|
||||
})
|
||||
})
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1513,7 +1527,7 @@ func (a *App) syncStatus() (*SyncStatusDTO, error) {
|
||||
dto.UnpushedOps = len(unpushed)
|
||||
|
||||
if deviceToken != "" {
|
||||
client := syncsvc.NewClient(serverURL, "", "", vaultPath)
|
||||
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||
client.DeviceToken = deviceToken
|
||||
if cfg.Sync.DeviceID != "" {
|
||||
client.DeviceID = cfg.Sync.DeviceID
|
||||
@@ -1570,7 +1584,7 @@ func (a *App) syncConfigure(serverURL, username, password string) error {
|
||||
if hostname == "" {
|
||||
hostname = "unknown"
|
||||
}
|
||||
client := syncsvc.NewClient(serverURL, "", "", vaultPath)
|
||||
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||
deviceID, deviceToken, err := client.PairDevice(serverURL, username, password, hostname, "verstak-desktop/v2")
|
||||
if err != nil {
|
||||
return fmt.Errorf("pair: %w", err)
|
||||
@@ -1613,7 +1627,7 @@ func (a *App) syncDisconnect() error {
|
||||
cfg := a.appSettings.Get()
|
||||
|
||||
if deviceToken != "" {
|
||||
client := syncsvc.NewClient(cfg.Sync.ServerURL, "", "", vaultPath)
|
||||
client := newSyncClient(cfg.Sync.ServerURL, "", "", vaultPath)
|
||||
client.DeviceToken = deviceToken
|
||||
_ = client.RevokeCurrent()
|
||||
}
|
||||
@@ -1647,7 +1661,7 @@ func (a *App) syncTestConnection(serverURL, username, password string) error {
|
||||
if vaultPath == "" {
|
||||
vaultPath = "/tmp"
|
||||
}
|
||||
client := syncsvc.NewClient(serverURL, "", "", vaultPath)
|
||||
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||
return client.TestAuth(serverURL, username, password)
|
||||
}
|
||||
|
||||
@@ -1703,7 +1717,7 @@ func (a *App) syncNow() (map[string]interface{}, error) {
|
||||
deviceID = cfg.Sync.DeviceID
|
||||
}
|
||||
|
||||
client := syncsvc.NewClient(serverURL, apiKey, deviceID, vaultPath)
|
||||
client := newSyncClient(serverURL, apiKey, deviceID, vaultPath)
|
||||
client.DeviceToken = deviceToken
|
||||
|
||||
unpushed, err := a.syncSvc.GetUnpushedOps()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -21,6 +23,20 @@ import (
|
||||
"github.com/verstak/verstak-desktop/internal/core/workspace"
|
||||
)
|
||||
|
||||
func newLocalHTTPTestServer(t *testing.T, handler http.Handler) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen local test server: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewUnstartedServer(handler)
|
||||
server.Listener = listener
|
||||
server.Start()
|
||||
return server
|
||||
}
|
||||
|
||||
// newTestApp creates an App with a mocked plugin list for testing.
|
||||
func newTestApp(tmpRoot string) *App {
|
||||
return &App{
|
||||
@@ -738,7 +754,7 @@ func TestSyncNowPushesLocalOpsAndAppliesPulledFileOps(t *testing.T) {
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
var pushedDeviceID string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newLocalHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer device-token" {
|
||||
http.Error(w, "missing auth", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -1291,3 +1307,55 @@ func TestPluginBridgeCapabilitiesCommandsAndEventsAreChecked(t *testing.T) {
|
||||
t.Fatal("expected command permission/ownership error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscribePluginEventRegistersBackendEventBridge(t *testing.T) {
|
||||
app := newBridgeTestApp(t)
|
||||
emitted := make(chan map[string]interface{}, 1)
|
||||
originalEmit := emitFrontendEvent
|
||||
emitFrontendEvent = func(_ context.Context, eventName string, data ...interface{}) {
|
||||
if eventName != pluginEventRuntimeName {
|
||||
t.Errorf("eventName = %q, want %q", eventName, pluginEventRuntimeName)
|
||||
}
|
||||
if len(data) != 1 {
|
||||
t.Errorf("data length = %d, want 1", len(data))
|
||||
return
|
||||
}
|
||||
payload, ok := data[0].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Errorf("data[0] type = %T, want map[string]interface{}", data[0])
|
||||
return
|
||||
}
|
||||
emitted <- payload
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
emitFrontendEvent = originalEmit
|
||||
})
|
||||
|
||||
if errStr := app.SubscribePluginEvent("bridge.plugin", "browser.capture.page"); errStr != "" {
|
||||
t.Fatalf("SubscribePluginEvent: %s", errStr)
|
||||
}
|
||||
if !app.eventBus.HasSubscribers("browser.capture.page") {
|
||||
t.Fatal("expected backend event bus subscriber")
|
||||
}
|
||||
|
||||
app.eventBus.Publish(events.Event{
|
||||
Name: "browser.capture.page",
|
||||
Timestamp: "2026-06-27T00:00:00.000Z",
|
||||
Payload: map[string]interface{}{"url": "https://example.com"},
|
||||
})
|
||||
|
||||
event := <-emitted
|
||||
if event["name"] != "browser.capture.page" {
|
||||
t.Fatalf("event name = %v, want browser.capture.page", event["name"])
|
||||
}
|
||||
if event["timestamp"] != "2026-06-27T00:00:00.000Z" {
|
||||
t.Fatalf("event timestamp = %v, want documented timestamp", event["timestamp"])
|
||||
}
|
||||
payload, ok := event["payload"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("event payload type = %T, want map[string]interface{}", event["payload"])
|
||||
}
|
||||
if payload["url"] != "https://example.com" {
|
||||
t.Fatalf("payload url = %v, want https://example.com", payload["url"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user