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
181 changes: 181 additions & 0 deletions b2/bucket_creation_wire_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package b2

import (
"context"
"fmt"
"net/http"
"reflect"
"testing"

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

// Pin the create request independently of a live account's bucket defaults.
// https://www.backblaze.com/apidocs/b2-create-bucket
func TestNewBucketAttributesWire(t *testing.T) {
for _, test := range []struct {
name string
attrs *BucketAttrs
info any
rules any
response string
wantInfo map[string]string
wantRules []LifecycleRule
}{
{
name: "no-attrs",
response: `{"bucketId":"bid","bucketName":"n","bucketType":"allPrivate","bucketInfo":{},"lifecycleRules":[],"revision":1}`,
},
{
name: "only-info", attrs: &BucketAttrs{Info: map[string]string{"this": "that", "other": "thing"}},
info: map[string]string{"this": "that", "other": "thing"},
response: `{"bucketId":"bid","bucketName":"n","bucketType":"allPrivate","bucketInfo":{"this":"that","other":"thing"},"lifecycleRules":[],"revision":1}`,
wantInfo: map[string]string{"this": "that", "other": "thing"},
},
{
name: "only-rules",
attrs: &BucketAttrs{LifecycleRules: []LifecycleRule{
{Prefix: "whee/", DaysHiddenUntilDeleted: 30},
{Prefix: "whoa/", DaysNewUntilHidden: 1},
}},
rules: []map[string]any{
{"fileNamePrefix": "whee/", "daysFromHidingToDeleting": 30},
{"fileNamePrefix": "whoa/", "daysFromUploadingToHiding": 1},
},
response: `{"bucketId":"bid","bucketName":"n","bucketType":"allPrivate","bucketInfo":{},"lifecycleRules":[{"fileNamePrefix":"whee/","daysFromHidingToDeleting":30},{"fileNamePrefix":"whoa/","daysFromUploadingToHiding":1}],"revision":1}`,
wantRules: []LifecycleRule{{Prefix: "whee/", DaysHiddenUntilDeleted: 30}, {Prefix: "whoa/", DaysNewUntilHidden: 1}},
},
{
name: "info-and-rules",
attrs: &BucketAttrs{
Info: map[string]string{"this": "that", "other": "thing"},
LifecycleRules: []LifecycleRule{{Prefix: "whee/", DaysHiddenUntilDeleted: 30}, {Prefix: "whoa/", DaysNewUntilHidden: 1}},
},
info: map[string]string{"this": "that", "other": "thing"},
rules: []map[string]any{
{"fileNamePrefix": "whee/", "daysFromHidingToDeleting": 30},
{"fileNamePrefix": "whoa/", "daysFromUploadingToHiding": 1},
},
response: `{"bucketId":"bid","bucketName":"n","bucketType":"allPrivate","bucketInfo":{"this":"that","other":"thing"},"lifecycleRules":[{"fileNamePrefix":"whee/","daysFromHidingToDeleting":30},{"fileNamePrefix":"whoa/","daysFromUploadingToHiding":1}],"revision":1}`,
wantInfo: map[string]string{"this": "that", "other": "thing"},
wantRules: []LifecycleRule{{Prefix: "whee/", DaysHiddenUntilDeleted: 30}, {Prefix: "whoa/", DaysNewUntilHidden: 1}},
},
} {
t.Run(test.name, func(t *testing.T) {
srv := b2wiretest.New(t)
srv.ReplySequence("b2_list_buckets",
b2wiretest.Response{Status: http.StatusOK, Body: `{"buckets":[]}`},
b2wiretest.Response{Status: http.StatusOK, Body: `{"buckets":[` + test.response + `]}`},
)
srv.Reply("b2_create_bucket", http.StatusOK, test.response)
ctx := context.Background()
client, err := NewClient(ctx, "account", "key", APIBase(srv.URL()))
if err != nil {
t.Fatal(err)
}
bucket, err := client.NewBucket(ctx, "n", test.attrs)
if err != nil {
t.Fatal(err)
}
if bucket.Name() != "n" || bucket.ID() != "bid" {
t.Fatalf("created bucket name/id = %q/%q, want n/bid", bucket.Name(), bucket.ID())
}
creates := srv.Calls("b2_create_bucket")
if len(creates) != 1 {
t.Fatalf("create calls = %d, want 1", len(creates))
}
request := creates[0]
b2wiretest.AssertJSON(t, request, "accountId", "a")
b2wiretest.AssertJSON(t, request, "bucketName", "n")
b2wiretest.AssertJSON(t, request, "bucketType", "allPrivate")
b2wiretest.AssertJSON(t, request, "bucketInfo", test.info)
b2wiretest.AssertJSON(t, request, "lifecycleRules", test.rules)
b2wiretest.AssertNoJSON(t, request, "defaultServerSideEncryption")
attrs, err := bucket.Attrs(ctx)
if err != nil {
t.Fatal(err)
}
if len(attrs.Info) != len(test.wantInfo) {
t.Errorf("Attrs Info has %d entries, want %d", len(attrs.Info), len(test.wantInfo))
}
for key, want := range test.wantInfo {
if got := attrs.Info[key]; got != want {
t.Errorf("Attrs Info[%q] = %q, want %q", key, got, want)
}
}
if len(attrs.LifecycleRules) != len(test.wantRules) {
t.Fatalf("Attrs LifecycleRules = %d rules, want %d", len(attrs.LifecycleRules), len(test.wantRules))
}
for i, want := range test.wantRules {
got := attrs.LifecycleRules[i]
if got.Prefix != want.Prefix {
t.Errorf("Attrs LifecycleRules[%d].Prefix = %q, want %q", i, got.Prefix, want.Prefix)
}
if got.DaysNewUntilHidden != want.DaysNewUntilHidden {
t.Errorf("Attrs LifecycleRules[%d].DaysNewUntilHidden = %d, want %d", i, got.DaysNewUntilHidden, want.DaysNewUntilHidden)
}
if got.DaysHiddenUntilDeleted != want.DaysHiddenUntilDeleted {
t.Errorf("Attrs LifecycleRules[%d].DaysHiddenUntilDeleted = %d, want %d", i, got.DaysHiddenUntilDeleted, want.DaysHiddenUntilDeleted)
}
}
})
}
}

// Omitted SSE must stay omitted; decode the service's answer rather than
// assuming an account default. The null setting also covers older buckets.
// https://www.backblaze.com/apidocs/b2-create-bucket
func TestNewBucketDefaultEncryptionWire(t *testing.T) {
for _, test := range []struct {
name string
attrs *BucketAttrs
service string
want *ServerSideEncryption
}{
{
name: "sse-omitted-encrypted", service: `{"mode":"SSE-B2","algorithm":"AES256"}`,
want: &ServerSideEncryption{Mode: "SSE-B2", Algorithm: "AES256"},
},
{name: "sse-omitted-legacy-unencrypted", service: `{"mode":null,"algorithm":null}`},
{
name: "sse-explicit", attrs: &BucketAttrs{DefaultServerSideEncryption: SSEB2WithAES256()},
service: `{"mode":"SSE-B2","algorithm":"AES256"}`,
want: &ServerSideEncryption{Mode: "SSE-B2", Algorithm: "AES256"},
},
} {
t.Run(test.name, func(t *testing.T) {
srv := b2wiretest.New(t)
bucketJSON := fmt.Sprintf(`{"bucketId":"bid","bucketName":"n","bucketType":"allPrivate","bucketInfo":{},"lifecycleRules":[],"revision":1,"defaultServerSideEncryption":{"isClientAuthorizedToRead":true,"value":%s}}`, test.service)
srv.ReplySequence("b2_list_buckets",
b2wiretest.Response{Body: `{"buckets":[]}`},
b2wiretest.Response{Body: `{"buckets":[` + bucketJSON + `]}`},
)
srv.Reply("b2_create_bucket", http.StatusOK, bucketJSON)
ctx := context.Background()
client, err := NewClient(ctx, "account", "key", APIBase(srv.URL()))
if err != nil {
t.Fatal(err)
}
bucket, err := client.NewBucket(ctx, "n", test.attrs)
if err != nil {
t.Fatal(err)
}
creates := srv.Calls("b2_create_bucket")
if len(creates) != 1 {
t.Fatalf("create calls = %d, want 1", len(creates))
}
if test.attrs == nil {
b2wiretest.AssertNoJSON(t, creates[0], "defaultServerSideEncryption")
} else {
b2wiretest.AssertJSON(t, creates[0], "defaultServerSideEncryption", map[string]string{"mode": "SSE-B2", "algorithm": "AES256"})
}
attrs, err := bucket.Attrs(ctx)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(attrs.DefaultServerSideEncryption, test.want) {
t.Errorf("default encryption = %+v, want %+v", attrs.DefaultServerSideEncryption, test.want)
}
})
}
}
130 changes: 130 additions & 0 deletions b2/empty_delete_wire_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package b2

import (
"context"
"io"
"net/http"
"testing"
"time"

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

// Empty uploads must stay empty in the request, metadata and downloaded bytes.
func TestZeroByteObjectWire(t *testing.T) {
for _, chunkSize := range []int{0, 100000000} {
name := "default-chunk-size"
if chunkSize != 0 {
name = "explicit-chunk-size"
}
t.Run(name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv, bucket := newSHA1WireBucket(t)
const file = `{"fileId":"empty-id","fileName":"empty","action":"upload","contentLength":0,"contentSha1":"da39a3ee5e6b4b0d3255bfef95601890afd80709","contentType":"application/octet-stream","fileInfo":{},"uploadTimestamp":1}`
srv.Reply("upload_file", http.StatusOK, file)
srv.Reply("b2_get_file_info", http.StatusOK, file)
srv.Handle("empty", func(r b2wiretest.Request) b2wiretest.Response {
if r.Method != http.MethodHead {
return downloadResponse(nil, "da39a3ee5e6b4b0d3255bfef95601890afd80709")(r)
}
return b2wiretest.Response{Header: http.Header{
"Content-Length": {"0"},
"X-Bz-File-Id": {"empty-id"},
"X-Bz-Content-Sha1": {"da39a3ee5e6b4b0d3255bfef95601890afd80709"},
}}
})
writer := bucket.Object("empty").NewWriter(ctx)
writer.ChunkSize = chunkSize
if err := writer.Close(); err != nil {
t.Fatal(err)
}
uploads := srv.Calls("upload_file")
if len(uploads) != 1 {
t.Fatalf("upload calls = %d, want 1", len(uploads))
}
if got := uploads[0]; len(got.Body) != 0 || got.Header.Get("Content-Length") != "0" || got.Header.Get("X-Bz-File-Name") != "empty" {
t.Fatalf("empty upload: body=%q headers=%v", got.Body, got.Header)
}
attrs, err := bucket.Object("empty").Attrs(ctx)
if err != nil {
t.Fatal(err)
}
if attrs.Size != 0 {
t.Fatalf("empty object size = %d, want 0", attrs.Size)
}
b2wiretest.AssertJSON(t, srv.Last(t, "b2_get_file_info"), "fileId", "empty-id")
reader := bucket.Object("empty").NewReader(ctx)
defer func() {
if err := reader.Close(); err != nil {
t.Error(err)
}
}()
data, err := io.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
if len(data) != 0 {
t.Fatalf("empty object content = %q, want empty", data)
}
})
}
}

// Delete resolves a version by HEAD, sends its ID and name, then a
// fresh handle must observe that the file is gone.
func TestDeleteWithoutNameWire(t *testing.T) {
ctx := context.Background()
srv := b2wiretest.New(t)
bucket := newListingWireBucket(t, ctx, srv)
deleted := make(chan struct{})
srv.Handle("victim", func(r b2wiretest.Request) b2wiretest.Response {
select {
case <-deleted:
return b2wiretest.Response{Status: http.StatusNotFound,
Body: `{"status":404,"code":"not_found","message":"file not present"}`}
default:
return b2wiretest.Response{Header: http.Header{
"Content-Length": {"7"}, "X-Bz-File-Id": {"victim-id"},
}, Body: "payload"}
}
})
srv.Reply("b2_get_file_info", http.StatusOK, `{"fileId":"victim-id","fileName":"victim","action":"upload","contentLength":7,"contentSha1":"none","contentType":"text/plain","fileInfo":{},"uploadTimestamp":1}`)
srv.Handle("b2_delete_file_version", func(r b2wiretest.Request) b2wiretest.Response {
if r.JSON["fileId"] != "victim-id" || r.JSON["fileName"] != "victim" {
return b2wiretest.Response{Status: http.StatusBadRequest,
Body: `{"status":400,"code":"bad_request","message":"wrong version"}`}
}
select {
case <-deleted:
t.Error("duplicate delete")
default:
close(deleted)
}
return b2wiretest.Response{Body: `{"fileId":"victim-id","fileName":"victim"}`}
})
if _, err := bucket.Object("victim").Attrs(ctx); err != nil {
t.Fatalf("Attrs before Delete: %v", err)
}
if err := bucket.Object("victim").Delete(ctx); err != nil {
t.Fatal(err)
}
if _, err := bucket.Object("victim").Attrs(ctx); !IsNotExist(err) {
t.Errorf("Attrs after Delete = %v, want not-exist", err)
}
deletes := srv.Calls("b2_delete_file_version")
if len(deletes) != 1 {
t.Fatalf("delete calls = %d, want 1", len(deletes))
}
b2wiretest.AssertJSON(t, deletes[0], "fileId", "victim-id")
b2wiretest.AssertJSON(t, deletes[0], "fileName", "victim")
lookups := srv.Calls("victim")
if len(lookups) != 3 {
t.Fatalf("HEAD calls = %d, want 3 (before, resolve for delete, after)", len(lookups))
}
for _, request := range lookups {
if request.Method != http.MethodHead {
t.Errorf("lookup method = %s, want HEAD", request.Method)
}
}
}
Loading
Loading