156 lines
4.9 KiB
Go
156 lines
4.9 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"kra/internal/biz/system"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestComposeStreams(t *testing.T) {
|
|
objects := map[string]string{"a": "hello ", "b": "world"}
|
|
var stored string
|
|
hash, err := composeStreams(context.Background(), []string{"a", "b"}, func(_ context.Context, name string) (io.ReadCloser, error) {
|
|
value, ok := objects[name]
|
|
if !ok {
|
|
return nil, errors.New("missing object")
|
|
}
|
|
return io.NopCloser(strings.NewReader(value)), nil
|
|
}, func(_ context.Context, _ string, reader io.Reader) error {
|
|
value, err := io.ReadAll(reader)
|
|
stored = string(value)
|
|
return err
|
|
}, func(context.Context, string) error { return nil }, "out")
|
|
if err != nil || stored != "hello world" {
|
|
t.Fatalf("compose = %q, %v", stored, err)
|
|
}
|
|
want := md5.Sum([]byte(stored))
|
|
if hash != hex.EncodeToString(want[:]) {
|
|
t.Fatalf("hash = %q", hash)
|
|
}
|
|
}
|
|
|
|
func TestComposeStreamsRemovesPartialDestination(t *testing.T) {
|
|
removed := false
|
|
_, err := composeStreams(context.Background(), []string{"missing"}, func(context.Context, string) (io.ReadCloser, error) {
|
|
return nil, errors.New("open failed")
|
|
}, func(_ context.Context, _ string, reader io.Reader) error {
|
|
_, _ = io.ReadAll(reader)
|
|
return nil
|
|
}, func(context.Context, string) error {
|
|
removed = true
|
|
return nil
|
|
}, "out")
|
|
if err == nil || !removed {
|
|
t.Fatalf("compose error = %v, removed = %v", err, removed)
|
|
}
|
|
}
|
|
|
|
func TestComposeStreamsUnblocksProducerWhenDestinationFails(t *testing.T) {
|
|
putErr := errors.New("destination failed")
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, err := composeStreams(context.Background(), []string{"large"}, func(context.Context, string) (io.ReadCloser, error) {
|
|
return io.NopCloser(strings.NewReader(strings.Repeat("x", 1<<20))), nil
|
|
}, func(context.Context, string, io.Reader) error {
|
|
return putErr
|
|
}, func(context.Context, string) error { return nil }, "out")
|
|
done <- err
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if !errors.Is(err, putErr) {
|
|
t.Fatalf("compose error = %v, want %v", err, putErr)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("compose remained blocked after destination failure")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDeletePrefix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
valid string
|
|
wantErr bool
|
|
}{
|
|
{name: "canonicalizes", value: "/uploads/chunks/", valid: "uploads/chunks"},
|
|
{name: "cleans duplicate separators", value: "uploads//chunks", valid: "uploads/chunks"},
|
|
{name: "rejects empty", value: " / ", wantErr: true},
|
|
{name: "rejects traversal", value: "uploads/../", wantErr: true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := normalizeDeletePrefix(tt.value)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("normalizeDeletePrefix(%q) succeeded with %q", tt.value, got)
|
|
}
|
|
return
|
|
}
|
|
if err != nil || got != tt.valid {
|
|
t.Fatalf("normalizeDeletePrefix(%q) = %q, %v; want %q", tt.value, got, err, tt.valid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBoundedPrefixKeepsDirectoryBoundary(t *testing.T) {
|
|
if got := boundedPrefix("uploads/chunks/1", "uploads/chunks/1/"); got != "uploads/chunks/1/" {
|
|
t.Fatalf("boundedPrefix() = %q", got)
|
|
}
|
|
if got := boundedPrefix("uploads/chunks/1", "uploads/chunks/1"); got != "uploads/chunks/1" {
|
|
t.Fatalf("boundedPrefix() changed ordinary prefix to %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAdvanceDeletePrefixCursor(t *testing.T) {
|
|
if _, err := advanceDeletePrefixCursor("cursor", "cursor", true); err == nil {
|
|
t.Fatal("same cursor should fail")
|
|
}
|
|
if next, err := advanceDeletePrefixCursor("", "next", true); err != nil || next != "next" {
|
|
t.Fatalf("advance cursor = %q, %v", next, err)
|
|
}
|
|
}
|
|
|
|
func TestDeletePrefixViaListDeletesAllPages(t *testing.T) {
|
|
pages := map[string][]*system.StoredFile{
|
|
"": {{Path: "uploads/a"}, {Path: "uploads/b"}},
|
|
"next": {{Path: "uploads/c"}},
|
|
}
|
|
removed := []string{}
|
|
if err := deletePrefixViaList(context.Background(), "uploads", func(_ context.Context, prefix, cursor string, limit int) ([]*system.StoredFile, string, bool, error) {
|
|
if prefix != "uploads/" || limit != 1000 {
|
|
t.Fatalf("list arguments = %q, %q, %d", prefix, cursor, limit)
|
|
}
|
|
items := pages[cursor]
|
|
if cursor == "" {
|
|
return items, "next", true, nil
|
|
}
|
|
return items, "", false, nil
|
|
}, func(_ context.Context, path string) error {
|
|
removed = append(removed, path)
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(removed) != 3 || removed[2] != "uploads/c" {
|
|
t.Fatalf("removed paths = %#v", removed)
|
|
}
|
|
}
|
|
|
|
func TestDeletePrefixViaListRejectsStalledCursor(t *testing.T) {
|
|
err := deletePrefixViaList(context.Background(), "uploads", func(_ context.Context, _, _ string, _ int) ([]*system.StoredFile, string, bool, error) {
|
|
return nil, "", true, nil
|
|
}, func(context.Context, string) error { return nil })
|
|
if err == nil || !strings.Contains(err.Error(), "no progress") {
|
|
t.Fatalf("stalled pagination error = %v", err)
|
|
}
|
|
}
|