package storage import ( "bytes" "context" "io" "net/url" "strconv" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newTestStore(t *testing.T) *LocalFSStore { t.Helper() s, err := NewLocalFSStore(t.TempDir(), "http://localhost:3001/storage", "test-secret") require.NoError(t, err) return s } func TestLocalFSStore_PutGetStat(t *testing.T) { ctx := context.Background() s := newTestStore(t) payload := []byte("hello-visiona") key := "models/user-1/m1.nef" require.NoError(t, s.Put(ctx, key, bytes.NewReader(payload), int64(len(payload)), nil)) // Stat info, err := s.Stat(ctx, key) require.NoError(t, err) assert.Equal(t, int64(len(payload)), info.Size) assert.Equal(t, key, info.Key) // Get rc, obj, err := s.Get(ctx, key) require.NoError(t, err) defer rc.Close() got, err := io.ReadAll(rc) require.NoError(t, err) assert.Equal(t, payload, got) assert.Equal(t, int64(len(payload)), obj.Size) } func TestLocalFSStore_Exists(t *testing.T) { ctx := context.Background() s := newTestStore(t) ok, err := s.Exists(ctx, "nope.txt") require.NoError(t, err) assert.False(t, ok, "不存在應回 (false, nil)") require.NoError(t, s.Put(ctx, "a.txt", strings.NewReader("x"), 1, nil)) ok, err = s.Exists(ctx, "a.txt") require.NoError(t, err) assert.True(t, ok) } func TestLocalFSStore_Get_NotFound(t *testing.T) { s := newTestStore(t) _, _, err := s.Get(context.Background(), "missing.txt") assert.ErrorIs(t, err, ErrNotFound) } func TestLocalFSStore_Stat_NotFound(t *testing.T) { s := newTestStore(t) _, err := s.Stat(context.Background(), "missing.txt") assert.ErrorIs(t, err, ErrNotFound) } func TestLocalFSStore_Delete(t *testing.T) { ctx := context.Background() s := newTestStore(t) require.NoError(t, s.Put(ctx, "tmp.txt", strings.NewReader("x"), 1, nil)) require.NoError(t, s.Delete(ctx, "tmp.txt")) ok, _ := s.Exists(ctx, "tmp.txt") assert.False(t, ok) // 刪除不存在的 key 不應回錯 assert.NoError(t, s.Delete(ctx, "never.txt")) } func TestLocalFSStore_List(t *testing.T) { ctx := context.Background() s := newTestStore(t) require.NoError(t, s.Put(ctx, "models/u1/a.nef", strings.NewReader("A"), 1, nil)) require.NoError(t, s.Put(ctx, "models/u1/b.nef", strings.NewReader("B"), 1, nil)) require.NoError(t, s.Put(ctx, "models/u2/c.nef", strings.NewReader("C"), 1, nil)) listU1, err := s.List(ctx, "models/u1") require.NoError(t, err) assert.Len(t, listU1, 2) listAll, err := s.List(ctx, "") require.NoError(t, err) assert.Len(t, listAll, 3) listEmpty, err := s.List(ctx, "not-exist-prefix") require.NoError(t, err) assert.Empty(t, listEmpty) } func TestLocalFSStore_PathTraversal_Rejected(t *testing.T) { ctx := context.Background() s := newTestStore(t) // 嘗試逃出 root err := s.Put(ctx, "../../etc/passwd", strings.NewReader("pwned"), 5, nil) assert.ErrorIs(t, err, ErrInvalidKey) _, _, err = s.Get(ctx, "../secret.txt") assert.ErrorIs(t, err, ErrInvalidKey) _, err = s.Stat(ctx, "../secret.txt") assert.ErrorIs(t, err, ErrInvalidKey) // 空 key err = s.Put(ctx, "", strings.NewReader("x"), 1, nil) assert.ErrorIs(t, err, ErrInvalidKey) } func TestLocalFSStore_PresignedGetURL_AndVerify(t *testing.T) { ctx := context.Background() s := newTestStore(t) key := "models/u1/m.nef" require.NoError(t, s.Put(ctx, key, strings.NewReader("X"), 1, nil)) u, err := s.PresignedGetURL(ctx, key, 5*time.Minute) require.NoError(t, err) assert.Contains(t, u, "http://localhost:3001/storage/") assert.Contains(t, u, "expires=") assert.Contains(t, u, "signature=") // 解析並驗證 parsed, err := url.Parse(u) require.NoError(t, err) expires, err := strconv.ParseInt(parsed.Query().Get("expires"), 10, 64) require.NoError(t, err) sig := parsed.Query().Get("signature") assert.NoError(t, s.VerifySignature("GET", key, expires, sig)) // 簽名錯誤 assert.ErrorIs(t, s.VerifySignature("GET", key, expires, "tampered"), ErrInvalidSignature) // 已過期 assert.ErrorIs(t, s.VerifySignature("GET", key, time.Now().Add(-1*time.Hour).Unix(), sig), ErrInvalidSignature) // method 不符 assert.ErrorIs(t, s.VerifySignature("PUT", key, expires, sig), ErrInvalidSignature) } func TestLocalFSStore_PresignedPutURL(t *testing.T) { s := newTestStore(t) u, err := s.PresignedPutURL(context.Background(), "models/u1/new.nef", 10*time.Minute) require.NoError(t, err) assert.Contains(t, u, "mode=put") } func TestNewLocalFSStore_EmptyRoot(t *testing.T) { _, err := NewLocalFSStore("", "", "") assert.Error(t, err) }