Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions base/create_bucket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import (
"reflect"
"strings"
"testing"
"time"

"github.com/Backblaze/blazer/internal/b2types"
"github.com/Backblaze/blazer/internal/b2wiretest"
)

const corsAndLockBucketJSON = `{
Expand Down Expand Up @@ -155,6 +157,104 @@ func TestCreateBucketOmitsUnsetCORSAndFileLock(t *testing.T) {
}
}

func TestCreateBucketNormalizesTypeAndEncryption(t *testing.T) {
for _, tc := range []struct {
name, bucketType, wantType string
sse *b2types.ServerSideEncryption
wantSSE any
}{
{name: "public", bucketType: "allPublic", wantType: "allPublic"},
{name: "private", bucketType: "allPrivate", wantType: "allPrivate"},
{name: "unknown type", bucketType: "unknown", wantType: "allPrivate"},
{name: "empty encryption", wantType: "allPrivate", sse: &b2types.ServerSideEncryption{}},
{name: "SSE-B2", wantType: "allPrivate", sse: &b2types.ServerSideEncryption{Mode: "SSE-B2", Algorithm: "AES256"},
wantSSE: map[string]any{"mode": "SSE-B2", "algorithm": "AES256"}},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
b, sent, closeSrv := bucketFixture(t, plainBucketJSON)
defer closeSrv()
if _, err := b.CreateBucket(ctx, "name", tc.bucketType, nil, nil, tc.sse, nil, false, nil); err != nil {
t.Fatal(err)
}
if got := (*sent)["bucketType"]; got != tc.wantType {
t.Errorf("request bucketType = %#v, want %q", got, tc.wantType)
}
got, present := (*sent)["defaultServerSideEncryption"]
if !reflect.DeepEqual(got, tc.wantSSE) || present != (tc.wantSSE != nil) {
t.Errorf("request defaultServerSideEncryption = %#v (present=%t), want %#v", got, present, tc.wantSSE)
}
})
}
}

func TestCreateBucketReadsEachReplicationRole(t *testing.T) {
for _, tc := range []struct {
name, value string
want *b2types.ReplicationConfiguration
}{
{"source only", `{"asReplicationSource":{"sourceApplicationKeyId":"source-key","replicationRules":[]},"asReplicationDestination":null}`,
&b2types.ReplicationConfiguration{AsReplicationSource: &b2types.AsReplicationSource{KeyID: "source-key", ReplicationRules: []b2types.ReplicationRules{}}}},
{"destination only", `{"asReplicationSource":null,"asReplicationDestination":{"sourceToDestinationKeyMapping":{"source-key":"destination-key"}}}`,
&b2types.ReplicationConfiguration{AsReplicationDestination: &b2types.AsReplicationDestination{SourceToDestinationKeyMapping: map[string]string{"source-key": "destination-key"}}}},
{"neither role", `{"asReplicationSource":null,"asReplicationDestination":null}`, nil},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
response := fmt.Sprintf(`{"bucketId":"bid","replicationConfiguration":{"isClientAuthorizedToRead":true,"value":%s}}`, tc.value)
b, _, closeSrv := bucketFixture(t, response)
defer closeSrv()
bucket, err := b.CreateBucket(ctx, "name", "allPrivate", nil, nil, nil, nil, false, nil)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(bucket.ReplicationConfiguration, tc.want) {
t.Errorf("replication configuration = %#v, want %#v", bucket.ReplicationConfiguration, tc.want)
}
})
}
}

func TestUpdateBucketDistinguishesUnsetEmptyAndPopulatedFields(t *testing.T) {
for _, tc := range []struct {
name string
info map[string]string
rules []LifecycleRule
wantInfo any
wantRules any
}{
{name: "unset"},
{name: "clear", info: map[string]string{}, rules: []LifecycleRule{}, wantInfo: map[string]any{}, wantRules: []any{}},
{name: "set", info: map[string]string{"owner": "app"}, rules: []LifecycleRule{{Prefix: "logs/", DaysNewUntilHidden: 7, DaysHiddenUntilDeleted: 30}},
wantInfo: map[string]any{"owner": "app"},
wantRules: []any{map[string]any{"fileNamePrefix": "logs/", "daysFromUploadingToHiding": float64(7), "daysFromHidingToDeleting": float64(30)}}},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv := b2wiretest.New(t)
srv.Reply("b2_update_bucket", http.StatusOK, `{"bucketId":"bid","bucketName":"name","bucketType":"allPrivate","bucketInfo":{},"lifecycleRules":[],"revision":2,"fileLockConfiguration":{"isClientAuthorizedToRead":true,"value":{"isFileLockEnabled":false}},"replicationConfiguration":{"isClientAuthorizedToRead":true,"value":null}}`)
root, err := AuthorizeAccount(ctx, "account-id", "application-key", SetAPIBase(srv.URL()))
if err != nil {
t.Fatal(err)
}
bucket := &Bucket{ID: "bid", b2: root, Info: tc.info, LifecycleRules: tc.rules}
if _, err := bucket.Update(ctx); err != nil {
t.Fatal(err)
}
request := srv.Last(t, "b2_update_bucket")
for field, want := range map[string]any{"bucketInfo": tc.wantInfo, "lifecycleRules": tc.wantRules} {
got, present := request.JSON[field]
if present != (want != nil) || !reflect.DeepEqual(got, want) {
t.Errorf("request %s = %#v (present=%t), want %#v", field, got, present, want)
}
}
})
}
}

func TestCreateBucketSendsAndReadsReplicationConfiguration(t *testing.T) {
b, sent, closeSrv := bucketFixture(t, replicationBucketJSON)
defer closeSrv()
Expand Down
82 changes: 82 additions & 0 deletions base/large_file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package base

import (
"context"
"net/http"
"reflect"
"strings"
"testing"
"time"

"github.com/Backblaze/blazer/internal/b2wiretest"
)

func TestStartLargeFileReturnsRequestResult(t *testing.T) {
for _, tc := range []struct {
name, response string
status int
}{
{"success", `{"fileId":"large-id"}`, http.StatusOK},
{"failure", `{"status":400,"code":"bad_request","message":"start rejected"}`, http.StatusBadRequest},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv := b2wiretest.New(t)
srv.Reply("b2_start_large_file", tc.status, tc.response)
root, err := AuthorizeAccount(ctx, "account-id", "application-key", SetAPIBase(srv.URL()))
if err != nil {
t.Fatal(err)
}
large, err := (&Bucket{ID: "bid", b2: root}).StartLargeFile(ctx, "large", "text/plain", nil)
if tc.status == http.StatusBadRequest {
code, _ := Code(err)
if large != nil || code != 400 || err == nil || !strings.Contains(err.Error(), "start rejected") {
t.Fatalf("StartLargeFile = (%v, %v), status %d; want nil and start rejected (400)", large, err, code)
}
} else if err != nil || large == nil || large.ID != "large-id" {
t.Fatalf("StartLargeFile = (%v, %v), want large-id and nil", large, err)
}
b2wiretest.AssertJSON(t, srv.Last(t, "b2_start_large_file"), "bucketId", "bid")
})
}
}

func TestUploadPartPreservesExplicitAndTrailingSHA1(t *testing.T) {
for _, tc := range []struct {
name, body, checksum string
size int
}{
{"explicit", "abc", "a9993e364706816aba3e25717850c26c9cd0d89d", 3},
{"trailing", "abca9993e364706816aba3e25717850c26c9cd0d89d", "hex_digits_at_end", 43},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv := b2wiretest.New(t)
srv.Reply("b2_finish_large_file", http.StatusOK, `{"fileId":"large-id","fileName":"large","action":"upload"}`)
root, err := AuthorizeAccount(ctx, "account-id", "application-key", SetAPIBase(srv.URL()))
if err != nil {
t.Fatal(err)
}
large := (&File{ID: "large-id", b2: root}).AsLargeFile()
chunk, err := large.GetUploadPartURL(ctx)
if err != nil {
t.Fatal(err)
}
if n, err := chunk.UploadPart(ctx, strings.NewReader(tc.body), tc.checksum, tc.size, 1); err != nil || n != tc.size {
t.Fatalf("UploadPart = (%d, %v), want %d and nil", n, err, tc.size)
}
request := srv.Last(t, "upload_part")
if string(request.Body) != tc.body || request.Header.Get("X-Bz-Content-Sha1") != tc.checksum {
t.Errorf("upload body=%q checksum=%q, want %q and %q", request.Body, request.Header.Get("X-Bz-Content-Sha1"), tc.body, tc.checksum)
}
if _, err := large.FinishLargeFile(ctx); err != nil {
t.Fatal(err)
}
if got := srv.Last(t, "b2_finish_large_file").JSON["partSha1Array"]; !reflect.DeepEqual(got, []any{"a9993e364706816aba3e25717850c26c9cd0d89d"}) {
t.Errorf("finish partSha1Array = %#v, want [abc SHA1]", got)
}
})
}
}
20 changes: 11 additions & 9 deletions base/mapping_edges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
Expand All @@ -43,21 +42,24 @@ func TestUserAgentOptionReachesTheWire(t *testing.T) {

for _, options := range [][]AuthOption{
{SetAPIBase(srv.URL), UserAgent(customUserAgent)},
{SetAPIBase(srv.URL), UserAgent(customUserAgent), UserAgent("outer/2.0")},
{SetAPIBase(srv.URL)},
} {
if _, err := AuthorizeAccount(context.Background(), "account-id", "application-key", options...); err != nil {
t.Fatal(err)
}
}
if len(userAgents) != 2 {
t.Fatalf("authorization requests = %d, want 2", len(userAgents))
if len(userAgents) != 3 {
t.Fatalf("authorization requests = %d, want 3", len(userAgents))
}
prefix := customUserAgent + " "
if !strings.HasPrefix(userAgents[0], prefix) || len(userAgents[0]) <= len(prefix) {
t.Errorf("custom User-Agent = %q, want prefix %q followed by the default agent", userAgents[0], prefix)
}
if strings.Contains(userAgents[1], customUserAgent) {
t.Errorf("default User-Agent = %q, want no %q", userAgents[1], customUserAgent)
for i, want := range []string{
"acme-app/1.2 " + DefaultUserAgent,
"outer/2.0 acme-app/1.2 " + DefaultUserAgent,
DefaultUserAgent,
} {
if userAgents[i] != want {
t.Errorf("User-Agent[%d] = %q, want %q", i, userAgents[i], want)
}
}
}

Expand Down
75 changes: 75 additions & 0 deletions base/range_response_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package base

import (
"context"
"errors"
"io"
"net/http"
"strings"
"testing"
"time"
)

func TestValidateRangeResponse(t *testing.T) {
Expand All @@ -15,13 +19,16 @@ func TestValidateRangeResponse(t *testing.T) {
wantErr bool
}{
{name: "200 whole file", status: 200, offset: 0, size: 6, length: "6"},
{name: "200 empty file", status: 200, offset: 0, size: 1, length: "0"},
{name: "200 shorter than request", status: 200, offset: 0, size: 8, length: "6"},
{name: "200 open ended", status: 200, offset: 0, size: -1, length: "6"},
{name: "200 overlong", status: 200, offset: 0, size: 2, length: "6", wantErr: true},
{name: "200 at nonzero offset", status: 200, offset: 2, size: 2, length: "2", wantErr: true},
{name: "200 chunked", status: 200, offset: 0, size: 6, wantErr: true},
{name: "200 invalid length", status: 200, offset: 0, size: 6, length: "invalid", wantErr: true},
{name: "206 matching range", status: 206, offset: 2, size: 2, length: "2", span: "bytes 2-3/6"},
{name: "206 starts at zero", status: 206, offset: 0, size: 2, length: "2", span: "bytes 0-1/6"},
{name: "206 single byte", status: 206, offset: 2, size: 1, length: "1", span: "bytes 2-2/6"},
{name: "206 stops at total", status: 206, offset: 4, size: 8, length: "2", span: "bytes 4-5/6"},
{name: "206 open ended", status: 206, offset: 2, size: 0, length: "4", span: "bytes 2-5/6"},
{name: "206 wrong start", status: 206, offset: 2, size: 2, length: "2", span: "bytes 1-2/6", wantErr: true},
Expand Down Expand Up @@ -53,3 +60,71 @@ func TestValidateRangeResponse(t *testing.T) {
})
}
}

func TestDownloadsValidateRangesAndLimitBodies(t *testing.T) {
for _, tc := range []struct {
name, length, span, body, wantBody, requestRange string
status int
offset, size int64
wantInvalidRange bool
}{
{name: "whole file", status: 200, length: "6", body: "abcdef", wantBody: "abcdef"},
{name: "bounded partial", status: 206, offset: 1, size: 2, requestRange: "bytes=1-2", length: "2", span: "bytes 1-2/6", body: "bcEXTRA", wantBody: "bc"},
{name: "open ended partial", status: 206, offset: 1, requestRange: "bytes=1-", length: "5", span: "bytes 1-5/6", body: "bcdef", wantBody: "bcdef"},
{name: "overlong 200", status: 200, size: 2, requestRange: "bytes=0-1", length: "6", body: "abcdef", wantInvalidRange: true},
{name: "overlong 206", status: 206, offset: 1, size: 2, requestRange: "bytes=1-2", length: "3", span: "bytes 1-2/6", body: "bcd", wantInvalidRange: true},
} {
for _, byID := range []bool{false, true} {
method := "by name"
if byID {
method = "by ID"
}
t.Run(tc.name+"/"+method, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
body := &closeTrackingBody{Reader: strings.NewReader(tc.body)}
transport := authSnapshotTransport(func(req *http.Request) (*http.Response, error) {
if got := req.Header.Get("Range"); got != tc.requestRange {
t.Errorf("request Range = %q, want %q", got, tc.requestRange)
}
// A stub can expose extra body bytes despite Content-Length,
// independently testing the SDK's body limit and Close forwarding.
return &http.Response{StatusCode: tc.status, Header: http.Header{
"Content-Length": {tc.length}, "Content-Range": {tc.span},
}, Body: body, Request: req}, nil
})
bucket := &Bucket{Name: "bucket", b2: &B2{downloadURI: "https://example.invalid", opts: &b2Options{transport: transport}}}
var reader *FileReader
var err error
if byID {
reader, err = bucket.DownloadFileByID(ctx, "id", tc.offset, tc.size, false)
} else {
reader, err = bucket.DownloadFileByName(ctx, "object", tc.offset, tc.size, false)
}
if reader != nil {
defer func() {
if err := reader.Close(); err != nil {
t.Error(err)
}
}()
}
if tc.wantInvalidRange {
if reader != nil || !errors.Is(err, ErrInvalidRangeResponse) || !body.closed {
t.Fatalf("download = (%v, %v), closed=%t; want invalid range and closed body", reader, err, body.closed)
}
return
}
if err != nil || reader == nil {
t.Fatalf("download = (%v, %v), want reader and nil", reader, err)
}
got, readErr := io.ReadAll(reader)
if readErr != nil || string(got) != tc.wantBody {
t.Errorf("downloaded body = %q, error=%v; want %q and nil", got, readErr, tc.wantBody)
}
if err := reader.Close(); err != nil || !body.closed {
t.Errorf("Close = %v, underlying closed=%t; want nil and true", err, body.closed)
}
})
}
}
}
36 changes: 27 additions & 9 deletions base/ssec_wire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,15 +190,33 @@ func TestSSECWireShapes(t *testing.T) {
}

func TestSSEB2AndSSECConflictIsRejected(t *testing.T) {
conflict := &b2types.ServerSideEncryption{
Mode: "SSE-B2",
Algorithm: "AES256",
CustomerKey: "secret",
CustomerKeyMD5: "digest",
}
err := addServerSideEncryptionHeaders(map[string]string{}, conflict)
if err == nil || !strings.Contains(err.Error(), "SSE-B2 and SSE-C") {
t.Fatalf("conflict error = %v, want a clear SSE-B2/SSE-C conflict", err)
for _, tc := range []struct {
name, key, digest string
wantConflict bool
}{
{"both customer fields", "secret", "digest", true},
{"customer key only", "secret", "", true},
{"customer digest only", "", "digest", true},
{"no customer fields", "", "", false},
} {
t.Run(tc.name, func(t *testing.T) {
headers := map[string]string{}
err := addServerSideEncryptionHeaders(headers, &b2types.ServerSideEncryption{
Mode: "SSE-B2", Algorithm: "AES256", CustomerKey: tc.key, CustomerKeyMD5: tc.digest,
})
if tc.wantConflict {
if err == nil || !strings.Contains(err.Error(), "SSE-B2 and SSE-C") {
t.Fatalf("conflict error = %v, want a clear SSE-B2/SSE-C conflict", err)
}
if len(headers) != 0 {
t.Errorf("rejected encryption changed headers: %v", headers)
}
return
}
if err != nil || len(headers) != 1 || headers["X-Bz-Server-Side-Encryption"] != "AES256" {
t.Fatalf("SSE-B2 without customer fields: headers=%v error=%v, want AES256 and nil", headers, err)
}
})
}
}

Expand Down
Loading