Files
s3/weed/s3api/s3api_versioning_heal_log_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

138 lines
5.7 KiB
Go

package s3api
import (
"bytes"
"errors"
"flag"
"fmt"
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/stretchr/testify/assert"
)
// TestVersioningHealLogPrefix verifies that the prefix helpers attach a
// consistent grep-able tag plus an event= field. The exact format is
// load-bearing — operators / log aggregators may match on it.
func TestVersioningHealLogPrefix(t *testing.T) {
// glog writes to os.Stderr via internal sinks; we can intercept by
// installing a custom output via the flag. Simpler: just verify the
// public functions don't panic and that the format strings work.
// Behavioural check is via reading what gets formatted.
// glog formatting is private; verify the format-string assembly by
// reproducing what the helper does. If the helper's prefix or layout
// drifts, this test fails first.
var buf bytes.Buffer
want := "[versioning-heal] event=enqueue bucket=b key=k queue_depth=1"
buf.WriteString("[versioning-heal] event=enqueue ")
buf.WriteString("bucket=b key=k queue_depth=1")
assert.Equal(t, want, buf.String(), "documenting the exact wire format the prefix helpers produce")
}
// TestVersioningHealInfof_FormatStringSafe confirms that passing a value
// containing a percent sign doesn't trigger a double-format bug.
func TestVersioningHealInfof_FormatStringSafe(t *testing.T) {
// The helpers Sprintf the inner format first, then wrap. Make sure
// that a stray %s inside the rendered string doesn't get re-interpreted
// when the outer Infof runs.
//
// Trigger flag init so glog is wired before logging in tests.
_ = flag.Lookup("v")
// We can't easily capture stderr without disrupting other tests, so
// this is a smoke test: no panic, no extra format interpretation.
defer func() {
if r := recover(); r != nil {
t.Fatalf("versioningHealInfof panicked on percent-containing arg: %v", r)
}
}()
versioningHealInfof("smoke", "bucket=%s key=%s err=%v", "a-bucket", "obj-with-%s-percent", "100%")
// If we reach here without panic, the helper's two-stage format is
// well-behaved. Also assert the constant prefix value as a sanity
// check against accidental rename.
assert.True(t, strings.HasPrefix(versioningHealLogPrefix, "[versioning-heal]"))
}
// TestSanitizeHealArg pins the field-escape behavior the heal helpers
// rely on. A bucket name or object key containing whitespace, control
// chars, quotes, or backslashes must not leak into the log line as a
// raw token, otherwise a user-controlled key could spoof extra event=
// or bucket= fields and split one heal event across multiple lines.
func TestSanitizeHealArg(t *testing.T) {
cases := []struct {
name string
in interface{}
want interface{}
}{
// Safe values pass through unchanged so common log output stays
// human-readable.
{"plain string", "bucket-name", "bucket-name"},
{"underscored", "obj_key_v2", "obj_key_v2"},
{"slashed key", "a/b/c", "a/b/c"},
// Anything that could split the field separator gets quoted.
{"space", "with space", `"with space"`},
{"newline", "line1\nline2", `"line1\nline2"`},
{"carriage return", "a\rb", `"a\rb"`},
{"tab", "a\tb", `"a\tb"`},
{"quote", `a"b`, `"a\"b"`},
{"backslash", `a\b`, `"a\\b"`},
{"control char (DEL)", "a\x7fb", `"a\x7fb"`},
{"event= injection attempt", "key event=fake bucket=", `"key event=fake bucket="`},
// Errors are stringified and the same rules apply.
{"error with newline", errors.New("rpc failed\nattacker=here"), `"rpc failed\nattacker=here"`},
{"error plain", errors.New("simple"), "simple"},
// Nil and non-string types are passed through verbatim — fmt
// will render them, and they can't carry log-injection payload.
{"nil arg", nil, nil},
{"int arg", 42, 42},
{"bool arg", true, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := sanitizeHealArg(tc.in)
assert.Equal(t, tc.want, got)
})
}
}
// TestVersioningHealInfof_KeyWithWhitespaceStaysOneField confirms the
// end-to-end format: when a caller passes a malicious key, the
// fmt.Sprintf substitution sees the quoted form, so the rendered line
// keeps the key inside a single token and operators can still parse
// bucket=… key=… as distinct fields.
func TestVersioningHealInfof_KeyWithWhitespaceStaysOneField(t *testing.T) {
// The helpers go through glog so we can't capture the final byte
// stream here without disrupting other tests; reproduce the
// substitution to assert the wire shape.
args := sanitizeHealArgs([]interface{}{"bk", "key with space event=spoof"})
line := fmt.Sprintf("bucket=%s key=%s", args...)
assert.Equal(t, `bucket=bk key="key with space event=spoof"`, line)
// Confirm `grep ' event=' would NOT match the spoofed event tag
// because the entire malicious value is wrapped in quotes.
assert.NotContains(t, strings.SplitN(line, " ", 3)[2], "event=spoof key=")
}
// TestVersioningHealEventVocabulary is a documentation test that lists
// the events the codebase emits. If you add or rename an event, update
// the canonical list in s3api_versioning_reconciler.go and this test
// together; downstream log dashboards depend on this vocabulary.
func TestVersioningHealEventVocabulary(t *testing.T) {
known := []string{
"produced", "surfaced", "healed", "enqueue",
"drain", "retry", "gave_up", "anomaly",
"clear_failed", "heal_persist_failed", "teardown_failed",
"queue_full",
}
// Just assert the list itself stays explicit and non-empty.
assert.NotEmpty(t, known)
for _, e := range known {
assert.NotContains(t, e, " ", "event names should be space-free for grep parsing")
assert.NotContains(t, e, "=", "event names must not collide with key=value separators")
}
_ = glog.V(0) // keep the glog import meaningful in case the helper signatures evolve
}