package pluginmgr import ( "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "time" ) // redirectTransport rewrites every request's Host to point at target. type redirectTransport struct { target *url.URL rt http.RoundTripper } func (t redirectTransport) RoundTrip(req *http.Request) (*http.Response, error) { clone := req.Clone(req.Context()) clone.URL.Scheme = t.target.Scheme clone.URL.Host = t.target.Host clone.Host = t.target.Host return t.rt.RoundTrip(clone) } func installTestClient(t *testing.T, serverURL string) { t.Helper() u, err := url.Parse(serverURL) if err != nil { t.Fatalf("parse: %v", err) } old := httpClient httpClient = &http.Client{ Timeout: 5 * time.Second, Transport: redirectTransport{target: u, rt: http.DefaultTransport}, } t.Cleanup(func() { httpClient = old }) } func withTempHome(t *testing.T) string { t.Helper() home := t.TempDir() t.Setenv("HOME", home) return home } func TestScanPluginsEmptyDir(t *testing.T) { dir := t.TempDir() plugins, err := scanPlugins(dir) if err != nil { t.Fatalf("scanPlugins: %v", err) } if len(plugins) != 0 { t.Errorf("expected 0 plugins in empty dir, got %d", len(plugins)) } } func TestScanPluginsSingleFile(t *testing.T) { dir := t.TempDir() src := `plugin.register({ name = "hello", version = "1.2", description = "says hi", type = "visualizer", }) ` if err := os.WriteFile(filepath.Join(dir, "hello.lua"), []byte(src), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } plugins, err := scanPlugins(dir) if err != nil { t.Fatalf("scanPlugins: %v", err) } if len(plugins) != 1 { t.Fatalf("got %d plugins, want 1", len(plugins)) } got := plugins[0] if got.name != "hello" { t.Errorf("name = %q, want hello", got.name) } if got.version != "1.2" { t.Errorf("version = %q, want 1.2", got.version) } if got.description != "says hi" { t.Errorf("description = %q, want 'says hi'", got.description) } if got.typ != "visualizer" { t.Errorf("typ = %q, want visualizer", got.typ) } if got.file != "hello.lua" { t.Errorf("file = %q, want hello.lua", got.file) } } func TestScanPluginsFallsBackToFilename(t *testing.T) { dir := t.TempDir() // Plugin without register() call — name should default to filename. if err := os.WriteFile(filepath.Join(dir, "nameless.lua"), []byte(`-- nothing`), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } plugins, err := scanPlugins(dir) if err != nil { t.Fatalf("scanPlugins: %v", err) } if len(plugins) != 1 { t.Fatalf("got %d plugins, want 1", len(plugins)) } if plugins[0].name != "nameless" { t.Errorf("name = %q, want 'nameless' (filename fallback)", plugins[0].name) } } func TestScanPluginsDirectoryEntry(t *testing.T) { dir := t.TempDir() // Create a directory plugin: /myplug/init.lua sub := filepath.Join(dir, "myplug") if err := os.MkdirAll(sub, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } if err := os.WriteFile(filepath.Join(sub, "init.lua"), []byte(`plugin.register({ name = "myplug", version = "0.1" })`), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } plugins, err := scanPlugins(dir) if err != nil { t.Fatalf("scanPlugins: %v", err) } if len(plugins) != 1 { t.Fatalf("got %d plugins, want 1", len(plugins)) } if plugins[0].file != "myplug/" { t.Errorf("file = %q, want 'myplug/'", plugins[0].file) } } func TestScanPluginsIgnoresNonLua(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("nothing"), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } plugins, err := scanPlugins(dir) if err != nil { t.Fatalf("scanPlugins: %v", err) } if len(plugins) != 0 { t.Errorf("non-lua file should be ignored, got %+v", plugins) } } func TestListNoPlugins(t *testing.T) { withTempHome(t) if err := List(); err != nil { t.Errorf("List on empty dir should not error, got %v", err) } } func TestListPluginsShowsInstalled(t *testing.T) { home := withTempHome(t) plugDir := filepath.Join(home, ".config", "cliamp", "plugins") if err := os.MkdirAll(plugDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } if err := os.WriteFile(filepath.Join(plugDir, "x.lua"), []byte(`plugin.register({ name = "x", version = "1.0" })`), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } // List writes to stdout — we just verify it doesn't error. if err := List(); err != nil { t.Errorf("List: %v", err) } } func TestRemoveFile(t *testing.T) { home := withTempHome(t) plugDir := filepath.Join(home, ".config", "cliamp", "plugins") if err := os.MkdirAll(plugDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } path := filepath.Join(plugDir, "foo.lua") if err := os.WriteFile(path, []byte("x"), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } if err := Remove("foo"); err != nil { t.Fatalf("Remove: %v", err) } if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("plugin should be removed, stat err=%v", err) } } func TestRemoveDirectory(t *testing.T) { home := withTempHome(t) plugDir := filepath.Join(home, ".config", "cliamp", "plugins") nested := filepath.Join(plugDir, "bar", "init.lua") if err := os.MkdirAll(filepath.Dir(nested), 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } if err := os.WriteFile(nested, []byte("x"), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } if err := Remove("bar"); err != nil { t.Fatalf("Remove: %v", err) } if _, err := os.Stat(filepath.Dir(nested)); !os.IsNotExist(err) { t.Errorf("plugin dir should be removed, stat err=%v", err) } } func TestRemoveMissing(t *testing.T) { withTempHome(t) err := Remove("ghost") if err == nil { t.Error("Remove non-existent plugin should error") } if !strings.Contains(err.Error(), "not found") { t.Errorf("error = %q, want to mention 'not found'", err.Error()) } } func TestInstallFromRawURL(t *testing.T) { withTempHome(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "/example.lua") { t.Errorf("unexpected path %q", r.URL.Path) } _, _ = w.Write([]byte(`plugin.register({ name = "example", version = "1" })`)) })) defer srv.Close() installTestClient(t, srv.URL) if err := Install(srv.URL+"/example.lua", true); err != nil { t.Fatalf("Install: %v", err) } // Verify the installed file exists. home, _ := os.UserHomeDir() dest := filepath.Join(home, ".config", "cliamp", "plugins", "example.lua") if _, err := os.Stat(dest); err != nil { t.Errorf("installed plugin missing: %v", err) } } func TestInstallAlreadyExists(t *testing.T) { home := withTempHome(t) plugDir := filepath.Join(home, ".config", "cliamp", "plugins") if err := os.MkdirAll(plugDir, 0o755); err != nil { t.Fatalf("MkdirAll: %v", err) } existing := filepath.Join(plugDir, "mypl.lua") if err := os.WriteFile(existing, []byte("x"), 0o644); err != nil { t.Fatalf("WriteFile: %v", err) } srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`-- ok`)) })) defer srv.Close() installTestClient(t, srv.URL) err := Install(srv.URL+"/mypl.lua", true) if err == nil { t.Fatal("Install over existing plugin should error") } if !strings.Contains(err.Error(), "already exists") { t.Errorf("error = %q, want to mention 'already exists'", err.Error()) } } func TestInstallAllURLsFail(t *testing.T) { withTempHome(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "not found", http.StatusNotFound) })) defer srv.Close() installTestClient(t, srv.URL) err := Install(srv.URL+"/nonexistent.lua", true) if err == nil { t.Error("Install with all failing URLs should error") } } func TestInstallTooLarge(t *testing.T) { withTempHome(t) big := strings.Repeat("x", maxPluginSize+1) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(big)) })) defer srv.Close() installTestClient(t, srv.URL) err := Install(srv.URL+"/huge.lua", true) if err == nil { t.Error("Install of oversized plugin should fail") } } func TestDownloadErrors(t *testing.T) { tests := []struct { name string handler http.HandlerFunc wantErr string }{ {"404", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "missing", http.StatusNotFound) }, "HTTP"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { srv := httptest.NewServer(tt.handler) defer srv.Close() installTestClient(t, srv.URL) _, err := download(srv.URL + "/x.lua") if err == nil { t.Fatal("download should have errored") } if !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("error = %q, want to contain %q", err.Error(), tt.wantErr) } }) } }