2026-08-15 02:30:36 +08:00
|
|
|
|
package filestore
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// TestStoreCommitsAndReusesImmutableFile 验证 Commit 首次写入会创建文件且 Reused 为 false,
|
|
|
|
|
|
// 第二次写入相同身份时按幂等返回 Reused 为 true 且路径一致。
|
2026-08-15 02:30:36 +08:00
|
|
|
|
func TestStoreCommitsAndReusesImmutableFile(t *testing.T) {
|
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
store, err := New(t.TempDir())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create file store: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
content := []byte("signed update package")
|
|
|
|
|
|
identity := identityOf(content)
|
|
|
|
|
|
committed, err := store.Commit("transactions/one/package.zip", bytes.NewReader(content), identity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("commit file: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if committed.Reused {
|
|
|
|
|
|
t.Fatal("new file reported as reused")
|
|
|
|
|
|
}
|
|
|
|
|
|
actual, err := os.ReadFile(committed.Path)
|
|
|
|
|
|
if err != nil || !bytes.Equal(actual, content) {
|
|
|
|
|
|
t.Fatalf("read committed file: content=%q err=%v", actual, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
reused, err := store.Commit("transactions/one/package.zip", bytes.NewReader(content), identity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("reuse committed file: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if !reused.Reused || reused.Path != committed.Path {
|
|
|
|
|
|
t.Fatalf("unexpected reused file: %+v", reused)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// TestStoreInspectsCommittedIdentityWithoutMutation 验证 Inspect 能校验已提交文件的身份、
|
|
|
|
|
|
// 正确处理不存在的文件或目录,并对身份不符返回 ErrDestinationConflict。
|
2026-08-16 01:27:30 +08:00
|
|
|
|
func TestStoreInspectsCommittedIdentityWithoutMutation(t *testing.T) {
|
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
store, err := New(t.TempDir())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create file store: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
content := []byte("immutable backend jar")
|
|
|
|
|
|
identity := identityOf(content)
|
|
|
|
|
|
if _, err := store.Commit("releases/backend.jar", bytes.NewReader(content), identity); err != nil {
|
|
|
|
|
|
t.Fatalf("commit file: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
inspected, found, err := store.Inspect("releases/backend.jar", identity)
|
|
|
|
|
|
if err != nil || !found || inspected.Path == "" || !inspected.Reused {
|
|
|
|
|
|
t.Fatalf("inspect committed file: file=%+v found=%t err=%v", inspected, found, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
_, found, err = store.Inspect("releases/missing.jar", identity)
|
|
|
|
|
|
if err != nil || found {
|
|
|
|
|
|
t.Fatalf("inspect missing file: found=%t err=%v", found, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
_, found, err = store.Inspect("missing/directory/backend.jar", identity)
|
|
|
|
|
|
if err != nil || found {
|
|
|
|
|
|
t.Fatalf("inspect file below missing directory: found=%t err=%v", found, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
_, _, err = store.Inspect("releases/backend.jar", identityOf([]byte("different")))
|
|
|
|
|
|
if !errors.Is(err, ErrDestinationConflict) {
|
|
|
|
|
|
t.Fatalf("expected immutable identity conflict, got %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// TestStoreRejectsMismatchAndNeverPublishesInvalidFile 验证身份不符时 Commit 返回错误,
|
|
|
|
|
|
// 且目标路径上不会留下任何已发布的文件。
|
2026-08-15 02:30:36 +08:00
|
|
|
|
func TestStoreRejectsMismatchAndNeverPublishesInvalidFile(t *testing.T) {
|
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
|
store, err := New(root)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create file store: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
content := []byte("actual")
|
|
|
|
|
|
expected := identityOf([]byte("different"))
|
|
|
|
|
|
expected.Size = int64(len(content))
|
|
|
|
|
|
_, err = store.Commit("package.zip", bytes.NewReader(content), expected)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
t.Fatal("SHA-256 mismatch was accepted")
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, statErr := os.Stat(filepath.Join(root, "package.zip")); !errors.Is(statErr, os.ErrNotExist) {
|
|
|
|
|
|
t.Fatalf("invalid destination was published: %v", statErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// TestStoreRejectsNilSource 验证 Commit 传入 nil source 时会被拒绝。
|
2026-08-15 02:30:36 +08:00
|
|
|
|
func TestStoreRejectsNilSource(t *testing.T) {
|
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
store, err := New(t.TempDir())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create file store: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := store.Commit("package.zip", nil, identityOf(nil)); err == nil {
|
|
|
|
|
|
t.Fatal("nil file source was accepted")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// TestStoreRejectsExistingDifferentContentAndPathEscape 验证目标已存在不同内容时返回
|
|
|
|
|
|
// ErrDestinationConflict,且通过 ../ 越界的相对路径会被拒绝。
|
2026-08-15 02:30:36 +08:00
|
|
|
|
func TestStoreRejectsExistingDifferentContentAndPathEscape(t *testing.T) {
|
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
|
store, err := New(root)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create file store: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := os.WriteFile(filepath.Join(root, "package.zip"), []byte("old"), 0o640); err != nil {
|
|
|
|
|
|
t.Fatalf("seed destination: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
_, err = store.Commit("package.zip", bytes.NewReader([]byte("new")), identityOf([]byte("new")))
|
|
|
|
|
|
if !errors.Is(err, ErrDestinationConflict) {
|
|
|
|
|
|
t.Fatalf("expected destination conflict, got %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
_, err = store.Commit("../outside", bytes.NewReader(nil), identityOf(nil))
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
t.Fatal("path traversal was accepted")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// TestStoreRejectsSymlinkedParentOutsideRoot 验证父目录为指向根目录之外的符号链接时,
|
|
|
|
|
|
// Commit 会被拒绝,防止文件通过符号链接写出存储根目录。
|
2026-08-15 02:30:36 +08:00
|
|
|
|
func TestStoreRejectsSymlinkedParentOutsideRoot(t *testing.T) {
|
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
root := t.TempDir()
|
|
|
|
|
|
outside := t.TempDir()
|
|
|
|
|
|
if err := os.Symlink(outside, filepath.Join(root, "linked")); err != nil {
|
|
|
|
|
|
t.Fatalf("create parent symlink: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
store, err := New(root)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("create file store: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
_, err = store.Commit("linked/package.zip", bytes.NewReader(nil), identityOf(nil))
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
t.Fatal("symlinked parent outside root was accepted")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 10:10:14 +08:00
|
|
|
|
// identityOf 根据 content 计算其 SHA-256 摘要并返回对应的 Identity,供测试构造期望身份。
|
2026-08-15 02:30:36 +08:00
|
|
|
|
func identityOf(content []byte) Identity {
|
|
|
|
|
|
digest := sha256.Sum256(content)
|
|
|
|
|
|
return Identity{Size: int64(len(content)), SHA256: hex.EncodeToString(digest[:])}
|
|
|
|
|
|
}
|