diff --git a/weed/s3api/s3lifecycle/dispatcher/pipeline_test.go b/weed/s3api/s3lifecycle/dispatcher/pipeline_test.go index 36c7f3628..1020ea519 100644 --- a/weed/s3api/s3lifecycle/dispatcher/pipeline_test.go +++ b/weed/s3api/s3lifecycle/dispatcher/pipeline_test.go @@ -2,6 +2,7 @@ package dispatcher import ( "context" + "strings" "testing" "time" @@ -82,3 +83,59 @@ func TestPipelineRunRequiresDependencies(t *testing.T) { t.Fatal("expected error for empty Pipeline") } } + +// stubFilerClient satisfies filer_pb.SeaweedFilerClient just enough to +// pass the nil-check in Pipeline.Run; methods would panic if called, +// but the validation tests below all return before any RPC. +type stubFilerClient struct { + filer_pb.SeaweedFilerClient +} + +// fullPipeline assembles a Pipeline whose dependencies all pass the +// nil-check, so individual tests can knock out one piece at a time +// to exercise specific validation branches. +func fullPipeline() *Pipeline { + return &Pipeline{ + Engine: engine.New(), + Persister: reader.NewInMemoryPersister(), + Client: &fakeClient{}, + FilerClient: &stubFilerClient{}, + BucketsPath: "/buckets", + ShardID: 0, + } +} + +func TestPipelineRunValidation(t *testing.T) { + // Each case mutates one piece of a fullPipeline() and asserts the + // expected error fragment. Per-dependency cases pin that the nil + // check exercises every required field individually; the Buckets- + // Path case asserts the distinct error message; the shard cases + // pin the half-open [0, ShardCount) range and that any one bad + // entry refuses the whole multi-shard run. + cases := []struct { + name string + mutate func(*Pipeline) + wantErr string + }{ + {"missing Engine", func(p *Pipeline) { p.Engine = nil }, "missing required dependency"}, + {"missing Persister", func(p *Pipeline) { p.Persister = nil }, "missing required dependency"}, + {"missing Client", func(p *Pipeline) { p.Client = nil }, "missing required dependency"}, + {"missing FilerClient", func(p *Pipeline) { p.FilerClient = nil }, "missing required dependency"}, + {"missing BucketsPath", func(p *Pipeline) { p.BucketsPath = "" }, "BucketsPath required"}, + {"negative ShardID", func(p *Pipeline) { p.ShardID = -1 }, "out of"}, + {"ShardID at boundary", func(p *Pipeline) { p.ShardID = s3lifecycle.ShardCount }, "out of"}, + {"multi-shard out of range", func(p *Pipeline) { + p.Shards = []int{0, 1, s3lifecycle.ShardCount + 1} + }, "out of"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + p := fullPipeline() + tc.mutate(p) + err := p.Run(context.Background()) + if err == nil || !strings.Contains(err.Error(), tc.wantErr) { + t.Fatalf("expected error containing %q, got %v", tc.wantErr, err) + } + }) + } +}