storage_test.go

  1package plugin
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"reflect"
  8	"runtime"
  9	"strings"
 10	"sync"
 11	"testing"
 12)
 13
 14// setTestHome makes t.TempDir() the effective home directory for the duration
 15// of the test on both Unix and Windows. Go's os.UserHomeDir() reads $HOME on
 16// Unix but %USERPROFILE% on Windows, so we set both.
 17func setTestHome(t *testing.T) string {
 18	t.Helper()
 19	dir := t.TempDir()
 20	t.Setenv("HOME", dir)
 21	if runtime.GOOS == "windows" {
 22		t.Setenv("USERPROFILE", dir)
 23	}
 24	return dir
 25}
 26
 27func TestPluginStoreSetGet(t *testing.T) {
 28	setTestHome(t)
 29
 30	store, err := newPluginStore("test_plugin")
 31	if err != nil {
 32		t.Fatal(err)
 33	}
 34
 35	if err := store.Set("token", "abc123"); err != nil {
 36		t.Fatal(err)
 37	}
 38
 39	got, ok := store.Get("token")
 40	if !ok {
 41		t.Fatal("expected stored key")
 42	}
 43	if got != "abc123" {
 44		t.Fatalf("expected abc123, got %q", got)
 45	}
 46}
 47
 48func TestPluginStoreDelete(t *testing.T) {
 49	setTestHome(t)
 50
 51	store, err := newPluginStore("test_plugin")
 52	if err != nil {
 53		t.Fatal(err)
 54	}
 55	if err := store.Set("token", "abc123"); err != nil {
 56		t.Fatal(err)
 57	}
 58	if err := store.Delete("token"); err != nil {
 59		t.Fatal(err)
 60	}
 61
 62	if got, ok := store.Get("token"); ok {
 63		t.Fatalf("expected key to be deleted, got %q", got)
 64	}
 65}
 66
 67func TestPluginStoreKeys(t *testing.T) {
 68	setTestHome(t)
 69
 70	store, err := newPluginStore("test_plugin")
 71	if err != nil {
 72		t.Fatal(err)
 73	}
 74	if err := store.Set("a", "1"); err != nil {
 75		t.Fatal(err)
 76	}
 77	if err := store.Set("b", "2"); err != nil {
 78		t.Fatal(err)
 79	}
 80
 81	got := map[string]bool{}
 82	for _, key := range store.Keys() {
 83		got[key] = true
 84	}
 85
 86	want := map[string]bool{"a": true, "b": true}
 87	if !reflect.DeepEqual(got, want) {
 88		t.Fatalf("expected keys %v, got %v", want, got)
 89	}
 90}
 91
 92func TestPluginStoreKeysSortedOrder(t *testing.T) {
 93	setTestHome(t)
 94
 95	store, err := newPluginStore("test_plugin")
 96	if err != nil {
 97		t.Fatal(err)
 98	}
 99	// Insert in non-sorted order so map iteration order won't accidentally
100	// produce the expected result.
101	for _, k := range []string{"c", "a", "b", "z", "m"} {
102		if err := store.Set(k, k); err != nil {
103			t.Fatal(err)
104		}
105	}
106
107	got := store.Keys()
108	want := []string{"a", "b", "c", "m", "z"}
109	if !reflect.DeepEqual(got, want) {
110		t.Fatalf("expected sorted keys %v, got %v", want, got)
111	}
112}
113
114func TestPluginStoreKeysEmpty(t *testing.T) {
115	setTestHome(t)
116
117	store, err := newPluginStore("test_plugin")
118	if err != nil {
119		t.Fatal(err)
120	}
121
122	if keys := store.Keys(); len(keys) != 0 {
123		t.Fatalf("expected no keys, got %v", keys)
124	}
125}
126
127func TestPluginStoreConcurrentSets(t *testing.T) {
128	setTestHome(t)
129
130	store, err := newPluginStore("test_plugin")
131	if err != nil {
132		t.Fatal(err)
133	}
134
135	var wg sync.WaitGroup
136	for i := 0; i < 20; i++ {
137		wg.Add(1)
138		go func(i int) {
139			defer wg.Done()
140			if err := store.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i)); err != nil {
141				t.Errorf("set key%d: %v", i, err)
142			}
143		}(i)
144	}
145	wg.Wait()
146
147	for i := 0; i < 20; i++ {
148		key := fmt.Sprintf("key%d", i)
149		want := fmt.Sprintf("value%d", i)
150		got, ok := store.Get(key)
151		if !ok {
152			t.Fatalf("expected %s to be stored", key)
153		}
154		if got != want {
155			t.Fatalf("expected %s, got %q", want, got)
156		}
157	}
158}
159
160func TestPluginStorePersistence(t *testing.T) {
161	setTestHome(t)
162
163	store, err := newPluginStore("test_plugin")
164	if err != nil {
165		t.Fatal(err)
166	}
167	if err := store.Set("token", "abc123"); err != nil {
168		t.Fatal(err)
169	}
170
171	reloaded, err := newPluginStore("test_plugin")
172	if err != nil {
173		t.Fatal(err)
174	}
175
176	got, ok := reloaded.Get("token")
177	if !ok {
178		t.Fatal("expected persisted key")
179	}
180	if got != "abc123" {
181		t.Fatalf("expected abc123, got %q", got)
182	}
183}
184
185func TestPluginStoreFileMode(t *testing.T) {
186	setTestHome(t)
187
188	store, err := newPluginStore("test_plugin")
189	if err != nil {
190		t.Fatal(err)
191	}
192	if err := store.Set("token", "abc123"); err != nil {
193		t.Fatal(err)
194	}
195
196	info, err := os.Stat(store.path)
197	if err != nil {
198		t.Fatal(err)
199	}
200	if runtime.GOOS != "windows" {
201		if got := info.Mode().Perm(); got != 0o600 {
202			t.Fatalf("expected mode 0600, got %o", got)
203		}
204	}
205}
206
207func TestPluginStoreFileModeAfterOverwrite(t *testing.T) {
208	setTestHome(t)
209
210	store, err := newPluginStore("test_plugin")
211	if err != nil {
212		t.Fatal(err)
213	}
214	if err := store.Set("token", "abc123"); err != nil {
215		t.Fatal(err)
216	}
217	if err := os.Chmod(store.path, 0o666); err != nil {
218		t.Fatal(err)
219	}
220	if err := store.Set("token", "def456"); err != nil {
221		t.Fatal(err)
222	}
223
224	info, err := os.Stat(store.path)
225	if err != nil {
226		t.Fatal(err)
227	}
228	if runtime.GOOS != "windows" {
229		if got := info.Mode().Perm(); got != 0o600 {
230			t.Fatalf("expected mode 0600 after overwrite, got %o", got)
231		}
232	}
233}
234
235func TestNewPluginStoreRejectsInvalidPluginName(t *testing.T) {
236	setTestHome(t)
237
238	for _, name := range []string{"", ".", "..", "../etc", "foo/bar", `foo\bar`, "foo.bar"} {
239		t.Run(name, func(t *testing.T) {
240			if _, err := newPluginStore(name); err == nil {
241				t.Fatal("expected invalid plugin name error")
242			}
243		})
244	}
245}
246
247func TestLuaStoreInitErrorPropagates(t *testing.T) {
248	home := setTestHome(t)
249
250	dir := filepath.Join(home, ".config", "matcha", "plugins", "test_plugin")
251	if err := os.MkdirAll(dir, 0o700); err != nil {
252		t.Fatal(err)
253	}
254	if err := os.WriteFile(filepath.Join(dir, "data.json"), []byte("{"), 0o600); err != nil {
255		t.Fatal(err)
256	}
257
258	m := newTestManager()
259	defer m.Close()
260	m.currentPlugin = "test_plugin"
261
262	err := m.state.DoString(`
263		local matcha = require("matcha")
264		matcha.store_get("token")
265	`)
266	if err == nil {
267		t.Fatal("expected store_get to fail on store init error")
268	}
269	if !strings.Contains(err.Error(), "store_get:") {
270		t.Fatalf("expected store_get error, got %v", err)
271	}
272}