Files
PilotVault/API Server/internal/plugins/builtin/localstorage/localstorage_test.go
T
tajniak81andClaude Opus 4.8 afc6952eda Initial commit: PilotVault multi-service project
Add API Server (Go/PocketBase), Web App (Go BFF + Vue), Fly App
(Flutter/DJI MSDK), Adobe Plugin, and Docker/Docker AIO deployment
configs. Design assets and build artifacts are gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:43:33 +02:00

140 lines
4.4 KiB
Go

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")
}
}