329 lines
8.7 KiB
Go
329 lines
8.7 KiB
Go
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: <dir>/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)
|
|
}
|
|
})
|
|
}
|
|
}
|