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) } }