kra-new/internal/integration/storage/compose_test.go

111 lines
3.3 KiB
Go

package storage
import (
"context"
"crypto/md5"
"encoding/hex"
"errors"
"io"
"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 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)
}
}