Files
s3/weed/s3api/s3lifecycle/engine/match_test.go
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

195 lines
7.8 KiB
Go

package engine
import (
"reflect"
"sort"
"testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
)
// activeAll returns a PriorStates map that activates every action of the
// given canonical rules under bucket.
func activeAll(bucket string, rules []*s3lifecycle.Rule) map[s3lifecycle.ActionKey]PriorState {
out := map[s3lifecycle.ActionKey]PriorState{}
for _, r := range rules {
rh := s3lifecycle.RuleHash(r)
for _, k := range s3lifecycle.RuleActionKinds(r) {
out[s3lifecycle.ActionKey{Bucket: bucket, RuleHash: rh, ActionKind: k}] = PriorState{BootstrapComplete: true}
}
}
return out
}
func sortedKeys(keys []s3lifecycle.ActionKey) []s3lifecycle.ActionKey {
out := append([]s3lifecycle.ActionKey(nil), keys...)
sort.Slice(out, func(i, j int) bool {
return out[i].ActionKind < out[j].ActionKind
})
return out
}
func TestMatchOriginalWrite_DelayGroupRoutes(t *testing.T) {
r30 := ruleExpDays("a", "x/", 30)
r60 := ruleExpDays("b", "x/", 60)
rules := []*s3lifecycle.Rule{r30, r60}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: rules}}, CompileOptions{PriorStates: activeAll("bk", rules)})
ev := &Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "x/o"}
got30 := snap.MatchOriginalWrite(ev, s3lifecycle.DaysToDuration(30))
if len(got30) != 1 || got30[0].ActionKind != s3lifecycle.ActionKindExpirationDays {
t.Fatalf("30d sweep should match r30, got %v", got30)
}
got60 := snap.MatchOriginalWrite(ev, s3lifecycle.DaysToDuration(60))
if len(got60) != 1 {
t.Fatalf("60d sweep should match r60, got %v", got60)
}
}
func TestMatchOriginalWrite_PrefixFilter(t *testing.T) {
r := ruleExpDays("a", "logs/", 30)
e := New()
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{r}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{r})})
if got := snap.MatchOriginalWrite(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "data/x"}, s3lifecycle.DaysToDuration(30)); len(got) != 0 {
t.Fatalf("non-matching prefix should reject, got %v", got)
}
if got := snap.MatchOriginalWrite(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "logs/x"}, s3lifecycle.DaysToDuration(30)); len(got) != 1 {
t.Fatalf("matching prefix should fire, got %v", got)
}
}
func TestMatchOriginalWrite_MarkActiveBecomesRoutable(t *testing.T) {
// pending_bootstrap actions are indexed but inactive: MatchOriginalWrite
// returns nothing. After MarkActive flips engineState, the same key is
// routable in subsequent matches without a recompile.
r := ruleExpDays("a", "x/", 30)
e := New()
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{r}}}, CompileOptions{})
ev := &Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "x/o"}
if got := snap.MatchOriginalWrite(ev, s3lifecycle.DaysToDuration(30)); len(got) != 0 {
t.Fatalf("inactive action should not match, got %v", got)
}
snap.MarkActive(s3lifecycle.ActionKey{Bucket: "bk", RuleHash: s3lifecycle.RuleHash(r), ActionKind: s3lifecycle.ActionKindExpirationDays})
if got := snap.MatchOriginalWrite(ev, s3lifecycle.DaysToDuration(30)); len(got) != 1 {
t.Fatalf("post-markActive should be routable, got %v", got)
}
}
func TestMatchOriginalWrite_AbortMPUOnlyOnMPUInit(t *testing.T) {
r := &s3lifecycle.Rule{
ID: "mpu",
Status: s3lifecycle.StatusEnabled,
Prefix: ".uploads/",
AbortMPUDaysAfterInitiation: 7,
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{r}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{r})})
// Non-MPU event under .uploads/ prefix: filtered out by shape gating.
got := snap.MatchOriginalWrite(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: ".uploads/u1/", IsMPUInit: false}, s3lifecycle.DaysToDuration(7))
if len(got) != 0 {
t.Fatalf("non-MPU event should be filtered, got %v", got)
}
got = snap.MatchOriginalWrite(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: ".uploads/u1/", IsMPUInit: true}, s3lifecycle.DaysToDuration(7))
if len(got) != 1 {
t.Fatalf("MPU init should fire, got %v", got)
}
}
func TestMatchPredicateChange_OnlyTagSensitiveActions(t *testing.T) {
rTag := &s3lifecycle.Rule{
ID: "tag",
Status: s3lifecycle.StatusEnabled,
Prefix: "x/",
ExpirationDays: 30,
FilterTags: map[string]string{"env": "prod"},
}
rPlain := ruleExpDays("plain", "x/", 30)
rules := []*s3lifecycle.Rule{rTag, rPlain}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: rules}}, CompileOptions{PriorStates: activeAll("bk", rules)})
// Predicate-change event: should match only the predicate-sensitive rule.
ev := &Event{Shape: EventShapePredicateChange, Bucket: "bk", Path: "x/o", Tags: map[string]string{"env": "prod"}}
got := snap.MatchPredicateChange(ev)
if len(got) != 1 {
t.Fatalf("only the tag-sensitive rule should match, got %v", got)
}
rh := s3lifecycle.RuleHash(rTag)
if got[0].RuleHash != rh {
t.Fatalf("expected match on tag-rule")
}
// Wrong shape: never matches.
if got := snap.MatchPredicateChange(&Event{Shape: EventShapeOriginalWrite, Bucket: "bk", Path: "x/o"}); len(got) != 0 {
t.Fatalf("wrong shape should be empty, got %v", got)
}
}
func TestMatchPath_BootstrapWalkerSeesAllActiveActions(t *testing.T) {
// One rule with three actions (90d Expiration, 30d NoncurrentDays,
// 7d AbortMPU). MatchPath returns every active action whose filter
// matches the path; the bootstrap walker iterates these and calls
// EvaluateAction per kind for the entry.
rule := &s3lifecycle.Rule{
ID: "multi",
Status: s3lifecycle.StatusEnabled,
Prefix: "x/",
ExpirationDays: 90,
NoncurrentVersionExpirationDays: 30,
AbortMPUDaysAfterInitiation: 7,
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{rule})})
got := snap.MatchPath("bk", "x/obj", nil)
if len(got) != 3 {
t.Fatalf("want all 3 active actions, got %v", got)
}
wantKinds := []s3lifecycle.ActionKind{
s3lifecycle.ActionKindExpirationDays,
s3lifecycle.ActionKindNoncurrentDays,
s3lifecycle.ActionKindAbortMPU,
}
gotKinds := []s3lifecycle.ActionKind{}
for _, k := range sortedKeys(got) {
gotKinds = append(gotKinds, k.ActionKind)
}
sort.Slice(wantKinds, func(i, j int) bool { return wantKinds[i] < wantKinds[j] })
if !reflect.DeepEqual(gotKinds, wantKinds) {
t.Fatalf("want kinds %v, got %v", wantKinds, gotKinds)
}
}
func TestMatchPath_PrefixMismatchExcludes(t *testing.T) {
rule := ruleExpDays("a", "logs/", 30)
e := New()
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{rule})})
if got := snap.MatchPath("bk", "data/x", nil); len(got) != 0 {
t.Fatalf("prefix mismatch should reject, got %v", got)
}
}
func TestMatchPath_FilterRejectsByTag(t *testing.T) {
rule := &s3lifecycle.Rule{
ID: "tag",
Status: s3lifecycle.StatusEnabled,
Prefix: "x/",
ExpirationDays: 30,
FilterTags: map[string]string{"env": "prod"},
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "bk", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: activeAll("bk", []*s3lifecycle.Rule{rule})})
// With ev passed, tag mismatch rejects.
ev := &Event{Bucket: "bk", Path: "x/obj", Tags: map[string]string{"env": "dev"}}
if got := snap.MatchPath("bk", "x/obj", ev); len(got) != 0 {
t.Fatalf("tag mismatch should reject, got %v", got)
}
// With ev nil (caller will fetch live state): prefix-only, returns the action.
if got := snap.MatchPath("bk", "x/obj", nil); len(got) != 1 {
t.Fatalf("ev=nil prefix-only path should return action, got %v", got)
}
}