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>
140 lines
4.4 KiB
Go
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")
|
|
}
|
|
}
|