package localstorage import ( "context" "encoding/base64" "encoding/json" "os" "path/filepath" "strings" "testing" "pilotvault/apiserver/internal/plugins" ) func TestDescriptor(t *testing.T) { p := &Plugin{} d := p.Descriptor() if d.Name != "localstorage" { t.Fatalf("name = %q, want localstorage", d.Name) } if d.Kind != plugins.KindBuiltin { t.Fatalf("kind = %q, want builtin", d.Kind) } if d.Category != plugins.CategoryDrivesLocal { t.Fatalf("category = %q, want %q", d.Category, plugins.CategoryDrivesLocal) } if len(d.Capabilities) == 0 { t.Fatal("expected capabilities") } } // TestRegistered confirms the plugin registered itself with the shared registry // via init(), so the manager will surface it. func TestRegistered(t *testing.T) { m := plugins.NewManager(t.TempDir() + "/plugins.json") if _, ok := m.Get("localstorage"); !ok { t.Fatal("localstorage not registered in the plugin manager") } } // TestResolveConfinement verifies that traversal, absolute-looking, and // backslash paths all stay under the base directory. func TestResolveConfinement(t *testing.T) { base := t.TempDir() p := &Plugin{basePath: base} absBase, _ := filepath.Abs(base) contained := []string{"a/b.txt", "/etc/passwd", "../../../etc/passwd", "a\\b", "./x", ""} for _, in := range contained { got, err := p.resolve(in) if err != nil { t.Fatalf("resolve(%q) errored: %v", in, err) } if got != absBase && !strings.HasPrefix(got, absBase+string(os.PathSeparator)) { t.Errorf("resolve(%q) = %q escaped base %q", in, got, absBase) } } } func TestResolveNoBase(t *testing.T) { p := &Plugin{} if _, err := p.resolve("x"); err == nil { t.Fatal("expected error when base path unset") } } func TestHealthCheckMissing(t *testing.T) { p := &Plugin{} // Base path unset -> down. if h := p.HealthCheck(context.Background()); h.Status != plugins.StatusDown { t.Errorf("unset base: status = %q, want down", h.Status) } // Nonexistent path without createMissing -> down. _ = p.Init(context.Background(), map[string]string{"basePath": filepath.Join(t.TempDir(), "nope")}) if h := p.HealthCheck(context.Background()); h.Status != plugins.StatusDown { t.Errorf("missing base: status = %q, want down (detail=%q)", h.Status, h.Detail) } } func TestHealthCheckCreateMissing(t *testing.T) { dir := filepath.Join(t.TempDir(), "created") p := &Plugin{} _ = p.Init(context.Background(), map[string]string{"basePath": dir, "createMissing": "true"}) h := p.HealthCheck(context.Background()) if h.Status != plugins.StatusOK { t.Fatalf("status = %q, want ok (detail=%q)", h.Status, h.Detail) } if fi, err := os.Stat(dir); err != nil || !fi.IsDir() { t.Fatalf("base path was not created: %v", err) } } // TestRoundTrip exercises upload -> list -> download -> delete end to end. func TestRoundTrip(t *testing.T) { base := t.TempDir() p := &Plugin{} _ = p.Init(context.Background(), map[string]string{"basePath": base, "createMissing": "true"}) payload := []byte("hello pilotvault") up, _ := json.Marshal(writeParams{Path: "sub/dir/file.txt", ContentBase64: base64.StdEncoding.EncodeToString(payload)}) if _, err := p.Invoke(context.Background(), "upload", up); err != nil { t.Fatalf("upload: %v", err) } dl, _ := json.Marshal(pathParams{Path: "sub/dir/file.txt"}) raw, err := p.Invoke(context.Background(), "download", dl) if err != nil { t.Fatalf("download: %v", err) } var got struct { ContentBase64 string `json:"contentBase64"` } _ = json.Unmarshal(raw, &got) if decoded, _ := base64.StdEncoding.DecodeString(got.ContentBase64); string(decoded) != string(payload) { t.Fatalf("download content = %q, want %q", decoded, payload) } if _, err := p.Invoke(context.Background(), "delete", dl); err != nil { t.Fatalf("delete: %v", err) } if _, err := os.Stat(filepath.Join(base, "sub", "dir", "file.txt")); !os.IsNotExist(err) { t.Fatalf("file still present after delete: %v", err) } } func TestReadOnlyRejectsWrites(t *testing.T) { base := t.TempDir() p := &Plugin{} _ = p.Init(context.Background(), map[string]string{"basePath": base, "readOnly": "true"}) up, _ := json.Marshal(writeParams{Path: "x.txt", ContentBase64: ""}) if _, err := p.Invoke(context.Background(), "upload", up); err == nil { t.Error("upload should be rejected in read-only mode") } del, _ := json.Marshal(pathParams{Path: "x.txt"}) if _, err := p.Invoke(context.Background(), "delete", del); err == nil { t.Error("delete should be rejected in read-only mode") } }