Files
yms-daemon/internal/filestore/store_test.go
T
2026-08-17 10:10:14 +08:00

153 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package filestore
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"os"
"path/filepath"
"testing"
)
// TestStoreCommitsAndReusesImmutableFile 验证 Commit 首次写入会创建文件且 Reused 为 false
// 第二次写入相同身份时按幂等返回 Reused 为 true 且路径一致。
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)
}
}
// TestStoreInspectsCommittedIdentityWithoutMutation 验证 Inspect 能校验已提交文件的身份、
// 正确处理不存在的文件或目录,并对身份不符返回 ErrDestinationConflict。
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)
}
}
// TestStoreRejectsMismatchAndNeverPublishesInvalidFile 验证身份不符时 Commit 返回错误,
// 且目标路径上不会留下任何已发布的文件。
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)
}
}
// TestStoreRejectsNilSource 验证 Commit 传入 nil source 时会被拒绝。
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")
}
}
// TestStoreRejectsExistingDifferentContentAndPathEscape 验证目标已存在不同内容时返回
// ErrDestinationConflict,且通过 ../ 越界的相对路径会被拒绝。
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")
}
}
// TestStoreRejectsSymlinkedParentOutsideRoot 验证父目录为指向根目录之外的符号链接时,
// Commit 会被拒绝,防止文件通过符号链接写出存储根目录。
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")
}
}
// identityOf 根据 content 计算其 SHA-256 摘要并返回对应的 Identity,供测试构造期望身份。
func identityOf(content []byte) Identity {
digest := sha256.Sum256(content)
return Identity{Size: int64(len(content)), SHA256: hex.EncodeToString(digest[:])}
}