go-toast

Send toast notifications in Windows
Log | Files | Refs | README | LICENSE

commit 2fb0180c4771388cb0976302e365074fdb4dbecc
parent 24528066e08feab41da3d31468079ab29778add9
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Tue, 21 Mar 2023 16:25:29 +0800

bind: test registry manipulation

Ensure the proper keys are set correctly.

This commit inserts test hooks into the registry code.

Instead of trying to mock the object oriented nature of
the registry package api, I've wrapped it in a simple
function that opens and set the registry value in one go.

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>

Diffstat:
Minternal/bind/bind_test.go | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/bind/registry.go | 78++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
2 files changed, 139 insertions(+), 32 deletions(-)

diff --git a/internal/bind/bind_test.go b/internal/bind/bind_test.go @@ -2,6 +2,8 @@ package bind import ( "fmt" + "path/filepath" + "reflect" "testing" ) @@ -75,4 +77,95 @@ func TestSetAppData(t *testing.T) { t.Fatalf("want=%v, got %v", appData, input) } }) + + t.Run("expect registry keys", func(t *testing.T) { + + t.Run("error on empty ID", func(t *testing.T) { + setAppData = setAppDataImpl + + record := map[string]string{} + + // Capture what would be written out to the registry. + writeStringValue = func(path, name, value string) error { + record[filepath.Join(path, name)] = value + return nil + } + + input := AppData{ + ExePath: "test.exe", + } + + if err := SetAppData(input); err == nil { + t.Fatalf("expected error, got nil") + } + + if len(record) > 0 { + t.Fatalf("wanted empty map, got value: %v", record) + } + }) + + t.Run("minimal keys", func(t *testing.T) { + setAppData = setAppDataImpl + + record := map[string]string{} + + // Capture what would be written out to the registry. + writeStringValue = func(path, name, value string) error { + record[filepath.Join(path, name)] = value + return nil + } + + input := AppData{ + ID: "test-id", + } + + if err := SetAppData(input); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expect := map[string]string{ + filepath.Join(appKeyRoot, input.ID, "CustomActivator"): GUID_ImplNotificationActivationCallback.String(), + filepath.Join(appKeyRoot, input.ID, "DisplayName"): input.ID, + } + + if !reflect.DeepEqual(expect, record) { + t.Fatalf("\nwant=%v \ngot =%v\n", expect, record) + } + }) + + t.Run("all keys", func(t *testing.T) { + setAppData = setAppDataImpl + + record := map[string]string{} + + // Capture what would be written out to the registry. + writeStringValue = func(path, name, value string) error { + record[filepath.Join(path, name)] = value + return nil + } + + input := AppData{ + ID: "test-id", + IconPath: "path/to/icon.ico", + IconBackgroundColor: "#FFFFFF", + ExePath: "path/to/exe", + } + + if err := SetAppData(input); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expect := map[string]string{ + filepath.Join(appKeyRoot, input.ID, "CustomActivator"): GUID_ImplNotificationActivationCallback.String(), + filepath.Join(appKeyRoot, input.ID, "DisplayName"): input.ID, + filepath.Join(appKeyRoot, input.ID, "IconUri"): input.IconPath, + filepath.Join(appKeyRoot, input.ID, "IconBackgroundColor"): input.IconBackgroundColor, + filepath.Join(activationKey): input.ExePath, + } + + if !reflect.DeepEqual(expect, record) { + t.Fatalf("\nwant=%v \ngot =%v\n", expect, record) + } + }) + }) } diff --git a/internal/bind/registry.go b/internal/bind/registry.go @@ -4,7 +4,6 @@ package bind import ( - "errors" "fmt" "path/filepath" "sync" @@ -60,57 +59,72 @@ func SetAppData(data AppData) (err error) { return nil } -// Override for testing. -var setAppData = func(data AppData) error { +// Overridden in testing. +var ( + writeStringValue = writeStringValueImpl + setAppData = setAppDataImpl +) + +var ( + // appKeyRoot is the root path for app metadata. + appKeyRoot = filepath.Join("SOFTWARE", "Classes", "AppUserModelId") + // activationKey is the root path to the activation executable. + activationKey = filepath.Join("SOFTWARE", "Classes", "CLSID", GUID_ImplNotificationActivationCallback.String(), "LocalServer32") +) + +// The Windows registry package uses empty string for the "(Default)" key. +const registryDefaultKey string = "" + +func setAppDataImpl(data AppData) error { if data.ID == "" { return fmt.Errorf("empty app ID") } - appIDKey, _, err := registry.CreateKey(registry.CURRENT_USER, filepath.Join("SOFTWARE", "Classes", "AppUserModelId", data.ID), registry.SET_VALUE) - if err != nil { - return fmt.Errorf("opening registry: %w", err) - } - defer func() { - err = errors.Join(err, appIDKey.Close()) - }() - if err := appIDKey.SetStringValue("DisplayName", data.ID); err != nil { - return fmt.Errorf("setting DisplayName: %w", err) + + appKey := filepath.Join(appKeyRoot, data.ID) + + if err := writeStringValue(appKey, "DisplayName", data.ID); err != nil { + return err } + // CustomActivator teaches Window what COM class to use as the callback when // a toast notification is activated. - if err := appIDKey.SetStringValue("CustomActivator", GUID_ImplNotificationActivationCallback.String()); err != nil { - return fmt.Errorf("setting CustomActivator: %w", err) + if err := writeStringValue(appKey, "CustomActivator", GUID_ImplNotificationActivationCallback.String()); err != nil { + return err } + if data.IconPath != "" { - if err := appIDKey.SetStringValue("IconUri", data.IconPath); err != nil { - return fmt.Errorf("setting IconUri: %w", err) + if err := writeStringValue(appKey, "IconUri", data.IconPath); err != nil { + return err } - if err := appIDKey.SetStringValue("IconBackgroundColor", data.IconBackgroundColor); err != nil { - return fmt.Errorf("setting IconBackgroundColor: %w", err) + } + + if data.IconBackgroundColor != "" { + if err := writeStringValue(appKey, "IconBackgroundColor", data.IconBackgroundColor); err != nil { + return err } } + if data.ExePath != "" { - if err := setActivationExecutable(data.ExePath); err != nil { + if err := writeStringValue(activationKey, registryDefaultKey, data.ExePath); err != nil { return fmt.Errorf("setting activation executable: %w", err) } } + return nil } -// The Windows registry package uses empty string for the "(Default)" key. -const registryDefaultKey string = "" - -// setActivationExecutable registers the given executable path with the CLSID. -// Windows will invoke this executable for cold starts, eg when the application is not running. -func setActivationExecutable(exe string) error { - clsidKey, _, err := registry.CreateKey(registry.CURRENT_USER, filepath.Join("SOFTWARE", "Classes", "CLSID", GUID_ImplNotificationActivationCallback.String(), "LocalServer32"), registry.SET_VALUE) +// writeStringValue writes a string value to the path, where name is the subkey and +// value is the literal value. +func writeStringValueImpl(path, name, value string) error { + key, _, err := registry.CreateKey(registry.CURRENT_USER, path, registry.SET_VALUE) if err != nil { - return fmt.Errorf("setting the exe path for LocalServer reponse: %w", err) + return fmt.Errorf("opening registry key: %s: %w", path, err) } - defer func() { - err = errors.Join(err, clsidKey.Close()) - }() - if err := clsidKey.SetStringValue(registryDefaultKey, exe); err != nil { - return fmt.Errorf("seting LocalServer32: %w", err) + if err := key.SetStringValue(name, value); err != nil { + return fmt.Errorf("setting string value: (%s) %s=%s: %w", path, name, value, err) + } + if err := key.Close(); err != nil { + return fmt.Errorf("closing key: %s: %w", path, err) } return nil }