mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
db3c1e011f
- MediaStore: use full UUID to prevent ref collisions, preserve and expose metadata via ResolveWithMeta, include underlying OS errors - Agent loop: populate MediaPart Type/Filename/ContentType from MediaStore metadata so channels can dispatch media correctly - SplitMessage: fix byte-vs-rune index mixup in code block header parsing, remove dead candidateStr variable - Pico auth: restrict query-param token behind AllowTokenQuery config flag (default false) to prevent token leakage via logs/referer - HandleMessage: replace context.TODO with caller-propagated ctx, log PublishInbound failures instead of silently discarding - Gateway shutdown: use fresh 15s timeout context for StopAll so graceful shutdown is not short-circuited by the cancelled parent ctx
224 lines
5.7 KiB
Go
224 lines
5.7 KiB
Go
package media
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func createTempFile(t *testing.T, dir, name string) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte("test content"), 0o644); err != nil {
|
|
t.Fatalf("failed to create temp file: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestStoreAndResolve(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := NewFileMediaStore()
|
|
|
|
path := createTempFile(t, dir, "photo.jpg")
|
|
|
|
ref, err := store.Store(path, MediaMeta{Filename: "photo.jpg", Source: "telegram"}, "scope1")
|
|
if err != nil {
|
|
t.Fatalf("Store failed: %v", err)
|
|
}
|
|
|
|
if !strings.HasPrefix(ref, "media://") {
|
|
t.Errorf("ref should start with media://, got %q", ref)
|
|
}
|
|
|
|
resolved, err := store.Resolve(ref)
|
|
if err != nil {
|
|
t.Fatalf("Resolve failed: %v", err)
|
|
}
|
|
if resolved != path {
|
|
t.Errorf("Resolve returned %q, want %q", resolved, path)
|
|
}
|
|
}
|
|
|
|
func TestReleaseAll(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := NewFileMediaStore()
|
|
|
|
paths := make([]string, 3)
|
|
refs := make([]string, 3)
|
|
for i := 0; i < 3; i++ {
|
|
paths[i] = createTempFile(t, dir, strings.Repeat("a", i+1)+".jpg")
|
|
var err error
|
|
refs[i], err = store.Store(paths[i], MediaMeta{Source: "test"}, "scope1")
|
|
if err != nil {
|
|
t.Fatalf("Store failed: %v", err)
|
|
}
|
|
}
|
|
|
|
if err := store.ReleaseAll("scope1"); err != nil {
|
|
t.Fatalf("ReleaseAll failed: %v", err)
|
|
}
|
|
|
|
// Files should be deleted
|
|
for _, p := range paths {
|
|
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
|
t.Errorf("file %q should have been deleted", p)
|
|
}
|
|
}
|
|
|
|
// Refs should be unresolvable
|
|
for _, ref := range refs {
|
|
if _, err := store.Resolve(ref); err == nil {
|
|
t.Errorf("Resolve(%q) should fail after ReleaseAll", ref)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMultiScopeIsolation(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := NewFileMediaStore()
|
|
|
|
pathA := createTempFile(t, dir, "fileA.jpg")
|
|
pathB := createTempFile(t, dir, "fileB.jpg")
|
|
|
|
refA, _ := store.Store(pathA, MediaMeta{Source: "test"}, "scopeA")
|
|
refB, _ := store.Store(pathB, MediaMeta{Source: "test"}, "scopeB")
|
|
|
|
// Release only scopeA
|
|
if err := store.ReleaseAll("scopeA"); err != nil {
|
|
t.Fatalf("ReleaseAll(scopeA) failed: %v", err)
|
|
}
|
|
|
|
// scopeA file should be gone
|
|
if _, err := os.Stat(pathA); !os.IsNotExist(err) {
|
|
t.Error("file A should have been deleted")
|
|
}
|
|
if _, err := store.Resolve(refA); err == nil {
|
|
t.Error("refA should be unresolvable after release")
|
|
}
|
|
|
|
// scopeB file should still exist
|
|
if _, err := os.Stat(pathB); err != nil {
|
|
t.Error("file B should still exist")
|
|
}
|
|
resolved, err := store.Resolve(refB)
|
|
if err != nil {
|
|
t.Fatalf("refB should still resolve: %v", err)
|
|
}
|
|
if resolved != pathB {
|
|
t.Errorf("resolved %q, want %q", resolved, pathB)
|
|
}
|
|
}
|
|
|
|
func TestReleaseAllIdempotent(t *testing.T) {
|
|
store := NewFileMediaStore()
|
|
|
|
// ReleaseAll on non-existent scope should not error
|
|
if err := store.ReleaseAll("nonexistent"); err != nil {
|
|
t.Fatalf("ReleaseAll on empty scope should not error: %v", err)
|
|
}
|
|
|
|
// Create and release, then release again
|
|
dir := t.TempDir()
|
|
path := createTempFile(t, dir, "file.jpg")
|
|
_, _ = store.Store(path, MediaMeta{Source: "test"}, "scope1")
|
|
|
|
if err := store.ReleaseAll("scope1"); err != nil {
|
|
t.Fatalf("first ReleaseAll failed: %v", err)
|
|
}
|
|
if err := store.ReleaseAll("scope1"); err != nil {
|
|
t.Fatalf("second ReleaseAll should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStoreNonexistentFile(t *testing.T) {
|
|
store := NewFileMediaStore()
|
|
|
|
_, err := store.Store("/nonexistent/path/file.jpg", MediaMeta{Source: "test"}, "scope1")
|
|
if err == nil {
|
|
t.Error("Store should fail for nonexistent file")
|
|
}
|
|
// Error message should include the underlying os error, not just "file does not exist"
|
|
if !strings.Contains(err.Error(), "no such file or directory") {
|
|
t.Errorf("Error should contain OS error detail, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestResolveWithMeta(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := NewFileMediaStore()
|
|
|
|
path := createTempFile(t, dir, "image.png")
|
|
meta := MediaMeta{
|
|
Filename: "image.png",
|
|
ContentType: "image/png",
|
|
Source: "telegram",
|
|
}
|
|
|
|
ref, err := store.Store(path, meta, "scope1")
|
|
if err != nil {
|
|
t.Fatalf("Store failed: %v", err)
|
|
}
|
|
|
|
resolvedPath, resolvedMeta, err := store.ResolveWithMeta(ref)
|
|
if err != nil {
|
|
t.Fatalf("ResolveWithMeta failed: %v", err)
|
|
}
|
|
if resolvedPath != path {
|
|
t.Errorf("ResolveWithMeta path = %q, want %q", resolvedPath, path)
|
|
}
|
|
if resolvedMeta.Filename != meta.Filename {
|
|
t.Errorf("ResolveWithMeta Filename = %q, want %q", resolvedMeta.Filename, meta.Filename)
|
|
}
|
|
if resolvedMeta.ContentType != meta.ContentType {
|
|
t.Errorf("ResolveWithMeta ContentType = %q, want %q", resolvedMeta.ContentType, meta.ContentType)
|
|
}
|
|
if resolvedMeta.Source != meta.Source {
|
|
t.Errorf("ResolveWithMeta Source = %q, want %q", resolvedMeta.Source, meta.Source)
|
|
}
|
|
|
|
// Unknown ref should fail
|
|
_, _, err = store.ResolveWithMeta("media://nonexistent")
|
|
if err == nil {
|
|
t.Error("ResolveWithMeta should fail for unknown ref")
|
|
}
|
|
}
|
|
|
|
func TestConcurrentSafety(t *testing.T) {
|
|
dir := t.TempDir()
|
|
store := NewFileMediaStore()
|
|
|
|
const goroutines = 20
|
|
const filesPerGoroutine = 5
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
|
|
for g := 0; g < goroutines; g++ {
|
|
go func(gIdx int) {
|
|
defer wg.Done()
|
|
scope := strings.Repeat("s", gIdx+1)
|
|
|
|
for i := 0; i < filesPerGoroutine; i++ {
|
|
path := createTempFile(t, dir, strings.Repeat("f", gIdx*filesPerGoroutine+i+1)+".tmp")
|
|
ref, err := store.Store(path, MediaMeta{Source: "test"}, scope)
|
|
if err != nil {
|
|
t.Errorf("Store failed: %v", err)
|
|
return
|
|
}
|
|
|
|
if _, err := store.Resolve(ref); err != nil {
|
|
t.Errorf("Resolve failed: %v", err)
|
|
}
|
|
}
|
|
|
|
if err := store.ReleaseAll(scope); err != nil {
|
|
t.Errorf("ReleaseAll failed: %v", err)
|
|
}
|
|
}(g)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|