26.9(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeEngine 可配置健康检查结果的桩引擎。
|
||||
type fakeEngine struct{ failHealth bool }
|
||||
|
||||
func (f *fakeEngine) SaveFile(ctx context.Context, r io.Reader, savePath string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (f *fakeEngine) DeleteFile(ctx context.Context, savePath string) error { return nil }
|
||||
func (f *fakeEngine) Open(ctx context.Context, savePath string, rng *Range) (*Download, error) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
func (f *fakeEngine) Stat(ctx context.Context, savePath string) (*FileMeta, error) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
func (f *fakeEngine) SaveChunk(ctx context.Context, uploadID string, chunkIndex int, r io.Reader, savePath string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (f *fakeEngine) MergeChunks(ctx context.Context, uploadID string, total int, verifyHash func(index int) (string, error), savePath string) (int64, string, error) {
|
||||
return 0, "", nil
|
||||
}
|
||||
func (f *fakeEngine) CleanChunks(ctx context.Context, uploadID string, savePath string) error {
|
||||
return nil
|
||||
}
|
||||
func (f *fakeEngine) FileExists(ctx context.Context, savePath string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (f *fakeEngine) HeadMeta(ctx context.Context, savePath string, headBytes int64) (*FileMeta, []byte, error) {
|
||||
return nil, nil, ErrNotSupported
|
||||
}
|
||||
func (f *fakeEngine) PresignGetURL(ctx context.Context, savePath string, expires int64) (string, error) {
|
||||
return "", ErrNotSupported
|
||||
}
|
||||
func (f *fakeEngine) PresignPutURL(ctx context.Context, savePath string, expires int64) (string, error) {
|
||||
return "", ErrNotSupported
|
||||
}
|
||||
func (f *fakeEngine) HealthCheck(ctx context.Context) error {
|
||||
if f.failHealth {
|
||||
return ErrUnavailable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newTestManager 构造测试用 Manager:local 健康引擎起步;s3/webdav 由计数器控制健康。
|
||||
func newTestManager(s3Fail *atomic.Bool) *Manager {
|
||||
build := func(name string) (Storage, error) {
|
||||
switch name {
|
||||
case "local":
|
||||
return &fakeEngine{}, nil
|
||||
case "s3":
|
||||
return &fakeEngine{failHealth: s3Fail.Load()}, nil
|
||||
case "webdav":
|
||||
return &fakeEngine{}, nil
|
||||
}
|
||||
return nil, errors.New("unknown")
|
||||
}
|
||||
return NewManager("local", &fakeEngine{}, build)
|
||||
}
|
||||
|
||||
// TestSwitchSuccessAndCurrentName 切换成功后当前引擎名与实例更新。
|
||||
func TestSwitchSuccessAndCurrentName(t *testing.T) {
|
||||
var s3Fail atomic.Bool
|
||||
m := newTestManager(&s3Fail)
|
||||
if m.CurrentName() != "local" {
|
||||
t.Fatalf("初始引擎应为 local,得到 %s", m.CurrentName())
|
||||
}
|
||||
if _, err := m.Switch("s3"); err != nil {
|
||||
t.Fatalf("Switch(s3) 失败: %v", err)
|
||||
}
|
||||
if m.CurrentName() != "s3" {
|
||||
t.Fatalf("切换后引擎应为 s3,得到 %s", m.CurrentName())
|
||||
}
|
||||
if _, err := m.Switch("webdav"); err != nil {
|
||||
t.Fatalf("Switch(webdav) 失败: %v", err)
|
||||
}
|
||||
if m.CurrentName() != "webdav" {
|
||||
t.Fatalf("切换后引擎应为 webdav,得到 %s", m.CurrentName())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSwitchFailureKeepsCurrent 健康检查失败时保持原引擎(v3 核心语义)。
|
||||
func TestSwitchFailureKeepsCurrent(t *testing.T) {
|
||||
var s3Fail atomic.Bool
|
||||
s3Fail.Store(true) // s3 不健康
|
||||
m := newTestManager(&s3Fail)
|
||||
if _, err := m.Switch("s3"); err == nil {
|
||||
t.Fatal("s3 不健康时 Switch 应失败")
|
||||
}
|
||||
if m.CurrentName() != "local" {
|
||||
t.Fatalf("切换失败后应保持 local,得到 %s", m.CurrentName())
|
||||
}
|
||||
// 恢复健康后可切换成功
|
||||
s3Fail.Store(false)
|
||||
if _, err := m.Switch("s3"); err != nil {
|
||||
t.Fatalf("恢复健康后 Switch(s3) 应成功: %v", err)
|
||||
}
|
||||
if m.CurrentName() != "s3" {
|
||||
t.Fatalf("恢复后引擎应为 s3,得到 %s", m.CurrentName())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSwitchInvalidName 非法引擎名拒绝。
|
||||
func TestSwitchInvalidName(t *testing.T) {
|
||||
m := newTestManager(&atomic.Bool{})
|
||||
if _, err := m.Switch("ftp"); err == nil || !strings.Contains(err.Error(), "未知存储引擎") {
|
||||
t.Fatalf("非法引擎名应报未知存储引擎,得到 %v", err)
|
||||
}
|
||||
if !ValidEngine("local") || ValidEngine("ftp") {
|
||||
t.Fatal("ValidEngine 判定错误")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEngineOfCacheAndInvalidate EngineOf 缓存命中 + Invalidate 后重建(参数生效路径)。
|
||||
func TestEngineOfCacheAndInvalidate(t *testing.T) {
|
||||
var builds atomic.Int64
|
||||
build := func(name string) (Storage, error) {
|
||||
builds.Add(1)
|
||||
return &fakeEngine{failHealth: false}, nil
|
||||
}
|
||||
m := NewManager("local", &fakeEngine{}, build)
|
||||
|
||||
s1, err := m.EngineOf("s3")
|
||||
if err != nil {
|
||||
t.Fatalf("EngineOf(s3): %v", err)
|
||||
}
|
||||
s2, err := m.EngineOf("s3")
|
||||
if err != nil {
|
||||
t.Fatalf("EngineOf(s3) second: %v", err)
|
||||
}
|
||||
if s1 != s2 {
|
||||
t.Fatal("EngineOf 应命中缓存返回同一实例")
|
||||
}
|
||||
if n := builds.Load(); n != 1 {
|
||||
t.Fatalf("应只构建 1 次,实际 %d", n)
|
||||
}
|
||||
|
||||
// Invalidate 后下次取重建新实例
|
||||
m.Invalidate("s3")
|
||||
s3, err := m.EngineOf("s3")
|
||||
if err != nil {
|
||||
t.Fatalf("EngineOf(s3) after invalidate: %v", err)
|
||||
}
|
||||
if s3 == s1 {
|
||||
t.Fatal("Invalidate 后应返回重建的新实例")
|
||||
}
|
||||
if n := builds.Load(); n != 2 {
|
||||
t.Fatalf("Invalidate 后应再构建 1 次,实际累计 %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvalidateCurrentNoop Invalidate 当前引擎不生效(热路径实例保持)。
|
||||
func TestInvalidateCurrentNoop(t *testing.T) {
|
||||
m := newTestManager(&atomic.Bool{})
|
||||
cur := m.Current()
|
||||
m.Invalidate("local") // 当前引擎:应为 no-op
|
||||
if m.Current() != cur {
|
||||
t.Fatal("Invalidate 当前引擎不应替换实例")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSwitchSameNameNoop 同名 Switch 幂等。
|
||||
func TestSwitchSameNameNoop(t *testing.T) {
|
||||
m := newTestManager(&atomic.Bool{})
|
||||
s, err := m.Switch("local")
|
||||
if err != nil {
|
||||
t.Fatalf("Switch(local) 同名应成功: %v", err)
|
||||
}
|
||||
if s != m.Current() {
|
||||
t.Fatal("同名 Switch 应返回当前实例")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDelegateToCurrent 保存/读取类操作委托当前引擎(切换后指向新引擎)。
|
||||
func TestDelegateToCurrent(t *testing.T) {
|
||||
var s3Fail atomic.Bool
|
||||
m := newTestManager(&s3Fail)
|
||||
ctx := context.Background()
|
||||
// local 引擎 HealthCheck 健康
|
||||
if err := m.HealthCheck(ctx); err != nil {
|
||||
t.Fatalf("委托 HealthCheck(local): %v", err)
|
||||
}
|
||||
if _, err := m.Switch("s3"); err != nil {
|
||||
t.Fatalf("Switch(s3): %v", err)
|
||||
}
|
||||
if err := m.HealthCheck(ctx); err != nil {
|
||||
t.Fatalf("委托 HealthCheck(s3): %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user