|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874 |
- // Copyright 2014 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
-
- package storage
-
- import (
- "crypto/tls"
- "encoding/json"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "net"
- "net/http"
- "net/http/httptest"
- "net/url"
- "regexp"
- "strings"
- "testing"
- "time"
-
- "cloud.google.com/go/iam"
- "cloud.google.com/go/internal/testutil"
-
- "golang.org/x/net/context"
- "google.golang.org/api/googleapi"
- "google.golang.org/api/iterator"
- "google.golang.org/api/option"
- raw "google.golang.org/api/storage/v1"
- )
-
- func TestHeaderSanitization(t *testing.T) {
- t.Parallel()
- var tests = []struct {
- desc string
- in []string
- want []string
- }{
- {
- desc: "already sanitized headers should not be modified",
- in: []string{"x-goog-header1:true", "x-goog-header2:0"},
- want: []string{"x-goog-header1:true", "x-goog-header2:0"},
- },
- {
- desc: "sanitized headers should be sorted",
- in: []string{"x-goog-header2:0", "x-goog-header1:true"},
- want: []string{"x-goog-header1:true", "x-goog-header2:0"},
- },
- {
- desc: "non-canonical headers should be removed",
- in: []string{"x-goog-header1:true", "x-goog-no-value", "non-canonical-header:not-of-use"},
- want: []string{"x-goog-header1:true"},
- },
- {
- desc: "excluded canonical headers should be removed",
- in: []string{"x-goog-header1:true", "x-goog-encryption-key:my_key", "x-goog-encryption-key-sha256:my_sha256"},
- want: []string{"x-goog-header1:true"},
- },
- {
- desc: "dirty headers should be formatted correctly",
- in: []string{" x-goog-header1 : \textra-spaces ", "X-Goog-Header2:CamelCaseValue"},
- want: []string{"x-goog-header1:extra-spaces", "x-goog-header2:CamelCaseValue"},
- },
- {
- desc: "duplicate headers should be merged",
- in: []string{"x-goog-header1:value1", "X-Goog-Header1:value2"},
- want: []string{"x-goog-header1:value1,value2"},
- },
- }
- for _, test := range tests {
- got := sanitizeHeaders(test.in)
- if !testutil.Equal(got, test.want) {
- t.Errorf("%s: got %v, want %v", test.desc, got, test.want)
- }
- }
- }
-
- func TestSignedURL(t *testing.T) {
- t.Parallel()
- expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
- url, err := SignedURL("bucket-name", "object-name", &SignedURLOptions{
- GoogleAccessID: "xxx@clientid",
- PrivateKey: dummyKey("rsa"),
- Method: "GET",
- MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
- Expires: expires,
- ContentType: "application/json",
- Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
- })
- if err != nil {
- t.Error(err)
- }
- want := "https://storage.googleapis.com/bucket-name/object-name?" +
- "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
- "RfsHlPtbB2JUYjzCgNr2Mi%2BjggdEuL1V7E6N9o6aaqwVLBDuTv3I0%2B9" +
- "x94E6rmmr%2FVgnmZigkIUxX%2Blfl7LgKf30uPGLt0mjKGH2p7r9ey1ONJ" +
- "%2BhVec23FnTRcSgopglvHPuCMWU2oNJE%2F1y8EwWE27baHrG1RhRHbLVF" +
- "bPpLZ9xTRFK20pluIkfHV00JGljB1imqQHXM%2B2XPWqBngLr%2FwqxLN7i" +
- "FcUiqR8xQEOHF%2F2e7fbkTHPNq4TazaLZ8X0eZ3eFdJ55A5QmNi8atlN4W" +
- "5q7Hvs0jcxElG3yqIbx439A995BkspLiAcA%2Fo4%2BxAwEMkGLICdbvakq" +
- "3eEprNCojw%3D%3D"
- if url != want {
- t.Fatalf("Unexpected signed URL; found %v", url)
- }
- }
-
- func TestSignedURL_PEMPrivateKey(t *testing.T) {
- t.Parallel()
- expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
- url, err := SignedURL("bucket-name", "object-name", &SignedURLOptions{
- GoogleAccessID: "xxx@clientid",
- PrivateKey: dummyKey("pem"),
- Method: "GET",
- MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
- Expires: expires,
- ContentType: "application/json",
- Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
- })
- if err != nil {
- t.Error(err)
- }
- want := "https://storage.googleapis.com/bucket-name/object-name?" +
- "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
- "TiyKD%2FgGb6Kh0kkb2iF%2FfF%2BnTx7L0J4YiZua8AcTmnidutePEGIU5" +
- "NULYlrGl6l52gz4zqFb3VFfIRTcPXMdXnnFdMCDhz2QuJBUpsU1Ai9zlyTQ" +
- "dkb6ShG03xz9%2BEXWAUQO4GBybJw%2FULASuv37xA00SwLdkqj8YdyS5II" +
- "1lro%3D"
- if url != want {
- t.Fatalf("Unexpected signed URL; found %v", url)
- }
- }
-
- func TestSignedURL_SignBytes(t *testing.T) {
- t.Parallel()
- expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
- url, err := SignedURL("bucket-name", "object-name", &SignedURLOptions{
- GoogleAccessID: "xxx@clientid",
- SignBytes: func(b []byte) ([]byte, error) {
- return []byte("signed"), nil
- },
- Method: "GET",
- MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
- Expires: expires,
- ContentType: "application/json",
- Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
- })
- if err != nil {
- t.Error(err)
- }
- want := "https://storage.googleapis.com/bucket-name/object-name?" +
- "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
- "c2lnbmVk" // base64('signed') == 'c2lnbmVk'
- if url != want {
- t.Fatalf("Unexpected signed URL\ngot: %q\nwant: %q", url, want)
- }
- }
-
- func TestSignedURL_URLUnsafeObjectName(t *testing.T) {
- t.Parallel()
- expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
- url, err := SignedURL("bucket-name", "object name界", &SignedURLOptions{
- GoogleAccessID: "xxx@clientid",
- PrivateKey: dummyKey("pem"),
- Method: "GET",
- MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
- Expires: expires,
- ContentType: "application/json",
- Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
- })
- if err != nil {
- t.Error(err)
- }
- want := "https://storage.googleapis.com/bucket-name/object%20name%E7%95%8C?" +
- "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=bxVH1%2Bl%2" +
- "BSxpnj3XuqKz6mOFk6M94Y%2B4w85J6FCmJan%2FNhGSpndP6fAw1uLHlOn%2F8xUaY%2F" +
- "SfZ5GzcQ%2BbxOL1WA37yIwZ7xgLYlO%2ByAi3GuqMUmHZiNCai28emODXQ8RtWHvgv6dE" +
- "SQ%2F0KpDMIWW7rYCaUa63UkUyeSQsKhrVqkIA%3D"
- if url != want {
- t.Fatalf("Unexpected signed URL; found %v", url)
- }
- }
-
- func TestSignedURL_MissingOptions(t *testing.T) {
- t.Parallel()
- pk := dummyKey("rsa")
- expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
- var tests = []struct {
- opts *SignedURLOptions
- errMsg string
- }{
- {
- &SignedURLOptions{},
- "missing required GoogleAccessID",
- },
- {
- &SignedURLOptions{GoogleAccessID: "access_id"},
- "exactly one of PrivateKey or SignedBytes must be set",
- },
- {
- &SignedURLOptions{
- GoogleAccessID: "access_id",
- SignBytes: func(b []byte) ([]byte, error) { return b, nil },
- PrivateKey: pk,
- },
- "exactly one of PrivateKey or SignedBytes must be set",
- },
- {
- &SignedURLOptions{
- GoogleAccessID: "access_id",
- PrivateKey: pk,
- },
- "missing required method",
- },
- {
- &SignedURLOptions{
- GoogleAccessID: "access_id",
- SignBytes: func(b []byte) ([]byte, error) { return b, nil },
- },
- "missing required method",
- },
- {
- &SignedURLOptions{
- GoogleAccessID: "access_id",
- PrivateKey: pk,
- Method: "PUT",
- },
- "missing required expires",
- },
- {
- &SignedURLOptions{
- GoogleAccessID: "access_id",
- PrivateKey: pk,
- Method: "PUT",
- Expires: expires,
- MD5: "invalid",
- },
- "invalid MD5 checksum",
- },
- }
- for _, test := range tests {
- _, err := SignedURL("bucket", "name", test.opts)
- if !strings.Contains(err.Error(), test.errMsg) {
- t.Errorf("expected err: %v, found: %v", test.errMsg, err)
- }
- }
- }
-
- func dummyKey(kind string) []byte {
- slurp, err := ioutil.ReadFile(fmt.Sprintf("./testdata/dummy_%s", kind))
- if err != nil {
- log.Fatal(err)
- }
- return slurp
- }
-
- func TestObjectNames(t *testing.T) {
- t.Parallel()
- // Naming requirements: https://cloud.google.com/storage/docs/bucket-naming
- const maxLegalLength = 1024
-
- type testT struct {
- name, want string
- }
- tests := []testT{
- // Embedded characters important in URLs.
- {"foo % bar", "foo%20%25%20bar"},
- {"foo ? bar", "foo%20%3F%20bar"},
- {"foo / bar", "foo%20/%20bar"},
- {"foo %?/ bar", "foo%20%25%3F/%20bar"},
-
- // Non-Roman scripts
- {"타코", "%ED%83%80%EC%BD%94"},
- {"世界", "%E4%B8%96%E7%95%8C"},
-
- // Longest legal name
- {strings.Repeat("a", maxLegalLength), strings.Repeat("a", maxLegalLength)},
-
- // Line terminators besides CR and LF: https://en.wikipedia.org/wiki/Newline#Unicode
- {"foo \u000b bar", "foo%20%0B%20bar"},
- {"foo \u000c bar", "foo%20%0C%20bar"},
- {"foo \u0085 bar", "foo%20%C2%85%20bar"},
- {"foo \u2028 bar", "foo%20%E2%80%A8%20bar"},
- {"foo \u2029 bar", "foo%20%E2%80%A9%20bar"},
-
- // Null byte.
- {"foo \u0000 bar", "foo%20%00%20bar"},
-
- // Non-control characters that are discouraged, but not forbidden, according to the documentation.
- {"foo # bar", "foo%20%23%20bar"},
- {"foo []*? bar", "foo%20%5B%5D%2A%3F%20bar"},
-
- // Angstrom symbol singleton and normalized forms: http://unicode.org/reports/tr15/
- {"foo \u212b bar", "foo%20%E2%84%AB%20bar"},
- {"foo \u0041\u030a bar", "foo%20A%CC%8A%20bar"},
- {"foo \u00c5 bar", "foo%20%C3%85%20bar"},
-
- // Hangul separating jamo: http://www.unicode.org/versions/Unicode7.0.0/ch18.pdf (Table 18-10)
- {"foo \u3131\u314f bar", "foo%20%E3%84%B1%E3%85%8F%20bar"},
- {"foo \u1100\u1161 bar", "foo%20%E1%84%80%E1%85%A1%20bar"},
- {"foo \uac00 bar", "foo%20%EA%B0%80%20bar"},
- }
-
- // C0 control characters not forbidden by the docs.
- var runes []rune
- for r := rune(0x01); r <= rune(0x1f); r++ {
- if r != '\u000a' && r != '\u000d' {
- runes = append(runes, r)
- }
- }
- tests = append(tests, testT{fmt.Sprintf("foo %s bar", string(runes)), "foo%20%01%02%03%04%05%06%07%08%09%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%20bar"})
-
- // C1 control characters, plus DEL.
- runes = nil
- for r := rune(0x7f); r <= rune(0x9f); r++ {
- runes = append(runes, r)
- }
- tests = append(tests, testT{fmt.Sprintf("foo %s bar", string(runes)), "foo%20%7F%C2%80%C2%81%C2%82%C2%83%C2%84%C2%85%C2%86%C2%87%C2%88%C2%89%C2%8A%C2%8B%C2%8C%C2%8D%C2%8E%C2%8F%C2%90%C2%91%C2%92%C2%93%C2%94%C2%95%C2%96%C2%97%C2%98%C2%99%C2%9A%C2%9B%C2%9C%C2%9D%C2%9E%C2%9F%20bar"})
-
- opts := &SignedURLOptions{
- GoogleAccessID: "xxx@clientid",
- PrivateKey: dummyKey("rsa"),
- Method: "GET",
- MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
- Expires: time.Date(2002, time.October, 2, 10, 0, 0, 0, time.UTC),
- ContentType: "application/json",
- Headers: []string{"x-goog-header1", "x-goog-header2"},
- }
-
- for _, test := range tests {
- g, err := SignedURL("bucket-name", test.name, opts)
- if err != nil {
- t.Errorf("SignedURL(%q) err=%v, want nil", test.name, err)
- }
- if w := "/bucket-name/" + test.want; !strings.Contains(g, w) {
- t.Errorf("SignedURL(%q)=%q, want substring %q", test.name, g, w)
- }
- }
- }
-
- func TestCondition(t *testing.T) {
- t.Parallel()
- gotReq := make(chan *http.Request, 1)
- hc, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
- io.Copy(ioutil.Discard, r.Body)
- gotReq <- r
- w.WriteHeader(200)
- })
- defer close()
- ctx := context.Background()
- c, err := NewClient(ctx, option.WithHTTPClient(hc))
- if err != nil {
- t.Fatal(err)
- }
-
- obj := c.Bucket("buck").Object("obj")
- dst := c.Bucket("dstbuck").Object("dst")
- tests := []struct {
- fn func()
- want string
- }{
- {
- func() { obj.Generation(1234).NewReader(ctx) },
- "GET /buck/obj?generation=1234",
- },
- {
- func() { obj.If(Conditions{GenerationMatch: 1234}).NewReader(ctx) },
- "GET /buck/obj?ifGenerationMatch=1234",
- },
- {
- func() { obj.If(Conditions{GenerationNotMatch: 1234}).NewReader(ctx) },
- "GET /buck/obj?ifGenerationNotMatch=1234",
- },
- {
- func() { obj.If(Conditions{MetagenerationMatch: 1234}).NewReader(ctx) },
- "GET /buck/obj?ifMetagenerationMatch=1234",
- },
- {
- func() { obj.If(Conditions{MetagenerationNotMatch: 1234}).NewReader(ctx) },
- "GET /buck/obj?ifMetagenerationNotMatch=1234",
- },
- {
- func() { obj.If(Conditions{MetagenerationNotMatch: 1234}).Attrs(ctx) },
- "GET /storage/v1/b/buck/o/obj?alt=json&ifMetagenerationNotMatch=1234&projection=full",
- },
-
- {
- func() { obj.If(Conditions{MetagenerationMatch: 1234}).Update(ctx, ObjectAttrsToUpdate{}) },
- "PATCH /storage/v1/b/buck/o/obj?alt=json&ifMetagenerationMatch=1234&projection=full",
- },
- {
- func() { obj.Generation(1234).Delete(ctx) },
- "DELETE /storage/v1/b/buck/o/obj?alt=json&generation=1234",
- },
- {
- func() {
- w := obj.If(Conditions{GenerationMatch: 1234}).NewWriter(ctx)
- w.ContentType = "text/plain"
- w.Close()
- },
- "POST /upload/storage/v1/b/buck/o?alt=json&ifGenerationMatch=1234&projection=full&uploadType=multipart",
- },
- {
- func() {
- w := obj.If(Conditions{DoesNotExist: true}).NewWriter(ctx)
- w.ContentType = "text/plain"
- w.Close()
- },
- "POST /upload/storage/v1/b/buck/o?alt=json&ifGenerationMatch=0&projection=full&uploadType=multipart",
- },
- {
- func() {
- dst.If(Conditions{MetagenerationMatch: 5678}).CopierFrom(obj.If(Conditions{GenerationMatch: 1234})).Run(ctx)
- },
- "POST /storage/v1/b/buck/o/obj/rewriteTo/b/dstbuck/o/dst?alt=json&ifMetagenerationMatch=5678&ifSourceGenerationMatch=1234&projection=full",
- },
- }
-
- for i, tt := range tests {
- tt.fn()
- select {
- case r := <-gotReq:
- got := r.Method + " " + r.RequestURI
- if got != tt.want {
- t.Errorf("%d. RequestURI = %q; want %q", i, got, tt.want)
- }
- case <-time.After(5 * time.Second):
- t.Fatalf("%d. timeout", i)
- }
- if err != nil {
- t.Fatal(err)
- }
- }
-
- // Test an error, too:
- err = obj.Generation(1234).NewWriter(ctx).Close()
- if err == nil || !strings.Contains(err.Error(), "NewWriter: generation not supported") {
- t.Errorf("want error about unsupported generation; got %v", err)
- }
- }
-
- func TestConditionErrors(t *testing.T) {
- t.Parallel()
- for _, conds := range []Conditions{
- {GenerationMatch: 0},
- {DoesNotExist: false}, // same as above, actually
- {GenerationMatch: 1, GenerationNotMatch: 2},
- {GenerationNotMatch: 2, DoesNotExist: true},
- {MetagenerationMatch: 1, MetagenerationNotMatch: 2},
- } {
- if err := conds.validate(""); err == nil {
- t.Errorf("%+v: got nil, want error", conds)
- }
- }
- }
-
- // Test object compose.
- func TestObjectCompose(t *testing.T) {
- t.Parallel()
- gotURL := make(chan string, 1)
- gotBody := make(chan []byte, 1)
- hc, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
- body, _ := ioutil.ReadAll(r.Body)
- gotURL <- r.URL.String()
- gotBody <- body
- w.Write([]byte("{}"))
- })
- defer close()
- ctx := context.Background()
- c, err := NewClient(ctx, option.WithHTTPClient(hc))
- if err != nil {
- t.Fatal(err)
- }
-
- testCases := []struct {
- desc string
- dst *ObjectHandle
- srcs []*ObjectHandle
- attrs *ObjectAttrs
- wantReq raw.ComposeRequest
- wantURL string
- wantErr bool
- }{
- {
- desc: "basic case",
- dst: c.Bucket("foo").Object("bar"),
- srcs: []*ObjectHandle{
- c.Bucket("foo").Object("baz"),
- c.Bucket("foo").Object("quux"),
- },
- wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json",
- wantReq: raw.ComposeRequest{
- Destination: &raw.Object{Bucket: "foo"},
- SourceObjects: []*raw.ComposeRequestSourceObjects{
- {Name: "baz"},
- {Name: "quux"},
- },
- },
- },
- {
- desc: "with object attrs",
- dst: c.Bucket("foo").Object("bar"),
- srcs: []*ObjectHandle{
- c.Bucket("foo").Object("baz"),
- c.Bucket("foo").Object("quux"),
- },
- attrs: &ObjectAttrs{
- Name: "not-bar",
- ContentType: "application/json",
- },
- wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json",
- wantReq: raw.ComposeRequest{
- Destination: &raw.Object{
- Bucket: "foo",
- Name: "not-bar",
- ContentType: "application/json",
- },
- SourceObjects: []*raw.ComposeRequestSourceObjects{
- {Name: "baz"},
- {Name: "quux"},
- },
- },
- },
- {
- desc: "with conditions",
- dst: c.Bucket("foo").Object("bar").If(Conditions{
- GenerationMatch: 12,
- MetagenerationMatch: 34,
- }),
- srcs: []*ObjectHandle{
- c.Bucket("foo").Object("baz").Generation(56),
- c.Bucket("foo").Object("quux").If(Conditions{GenerationMatch: 78}),
- },
- wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json&ifGenerationMatch=12&ifMetagenerationMatch=34",
- wantReq: raw.ComposeRequest{
- Destination: &raw.Object{Bucket: "foo"},
- SourceObjects: []*raw.ComposeRequestSourceObjects{
- {
- Name: "baz",
- Generation: 56,
- },
- {
- Name: "quux",
- ObjectPreconditions: &raw.ComposeRequestSourceObjectsObjectPreconditions{
- IfGenerationMatch: 78,
- },
- },
- },
- },
- },
- {
- desc: "no sources",
- dst: c.Bucket("foo").Object("bar"),
- wantErr: true,
- },
- {
- desc: "destination, no bucket",
- dst: c.Bucket("").Object("bar"),
- srcs: []*ObjectHandle{
- c.Bucket("foo").Object("baz"),
- },
- wantErr: true,
- },
- {
- desc: "destination, no object",
- dst: c.Bucket("foo").Object(""),
- srcs: []*ObjectHandle{
- c.Bucket("foo").Object("baz"),
- },
- wantErr: true,
- },
- {
- desc: "source, different bucket",
- dst: c.Bucket("foo").Object("bar"),
- srcs: []*ObjectHandle{
- c.Bucket("otherbucket").Object("baz"),
- },
- wantErr: true,
- },
- {
- desc: "source, no object",
- dst: c.Bucket("foo").Object("bar"),
- srcs: []*ObjectHandle{
- c.Bucket("foo").Object(""),
- },
- wantErr: true,
- },
- {
- desc: "destination, bad condition",
- dst: c.Bucket("foo").Object("bar").Generation(12),
- srcs: []*ObjectHandle{
- c.Bucket("foo").Object("baz"),
- },
- wantErr: true,
- },
- {
- desc: "source, bad condition",
- dst: c.Bucket("foo").Object("bar"),
- srcs: []*ObjectHandle{
- c.Bucket("foo").Object("baz").If(Conditions{MetagenerationMatch: 12}),
- },
- wantErr: true,
- },
- }
-
- for _, tt := range testCases {
- composer := tt.dst.ComposerFrom(tt.srcs...)
- if tt.attrs != nil {
- composer.ObjectAttrs = *tt.attrs
- }
- _, err := composer.Run(ctx)
- if gotErr := err != nil; gotErr != tt.wantErr {
- t.Errorf("%s: got error %v; want err %t", tt.desc, err, tt.wantErr)
- continue
- }
- if tt.wantErr {
- continue
- }
- url, body := <-gotURL, <-gotBody
- if url != tt.wantURL {
- t.Errorf("%s: request URL\ngot %q\nwant %q", tt.desc, url, tt.wantURL)
- }
- var req raw.ComposeRequest
- if err := json.Unmarshal(body, &req); err != nil {
- t.Errorf("%s: json.Unmarshal %v (body %s)", tt.desc, err, body)
- }
- if !testutil.Equal(req, tt.wantReq) {
- // Print to JSON.
- wantReq, _ := json.Marshal(tt.wantReq)
- t.Errorf("%s: request body\ngot %s\nwant %s", tt.desc, body, wantReq)
- }
- }
- }
-
- // Test that ObjectIterator's Next and NextPage methods correctly terminate
- // if there is nothing to iterate over.
- func TestEmptyObjectIterator(t *testing.T) {
- t.Parallel()
- hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
- io.Copy(ioutil.Discard, r.Body)
- fmt.Fprintf(w, "{}")
- })
- defer close()
- ctx := context.Background()
- client, err := NewClient(ctx, option.WithHTTPClient(hClient))
- if err != nil {
- t.Fatal(err)
- }
- it := client.Bucket("b").Objects(ctx, nil)
- _, err = it.Next()
- if err != iterator.Done {
- t.Errorf("got %v, want Done", err)
- }
- }
-
- // Test that BucketIterator's Next method correctly terminates if there is
- // nothing to iterate over.
- func TestEmptyBucketIterator(t *testing.T) {
- t.Parallel()
- hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
- io.Copy(ioutil.Discard, r.Body)
- fmt.Fprintf(w, "{}")
- })
- defer close()
- ctx := context.Background()
- client, err := NewClient(ctx, option.WithHTTPClient(hClient))
- if err != nil {
- t.Fatal(err)
- }
- it := client.Buckets(ctx, "project")
- _, err = it.Next()
- if err != iterator.Done {
- t.Errorf("got %v, want Done", err)
- }
-
- }
-
- func TestCodecUint32(t *testing.T) {
- t.Parallel()
- for _, u := range []uint32{0, 1, 256, 0xFFFFFFFF} {
- s := encodeUint32(u)
- d, err := decodeUint32(s)
- if err != nil {
- t.Fatal(err)
- }
- if d != u {
- t.Errorf("got %d, want input %d", d, u)
- }
- }
- }
-
- func TestBucketAttrs(t *testing.T) {
- for _, c := range []struct {
- attrs BucketAttrs
- raw raw.Bucket
- }{{
- attrs: BucketAttrs{
- Lifecycle: Lifecycle{
- Rules: []LifecycleRule{{
- Action: LifecycleAction{
- Type: SetStorageClassAction,
- StorageClass: "NEARLINE",
- },
- Condition: LifecycleCondition{
- AgeInDays: 10,
- Liveness: Live,
- CreatedBefore: time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
- MatchesStorageClasses: []string{"MULTI_REGIONAL", "REGIONAL", "STANDARD"},
- NumNewerVersions: 3,
- },
- }, {
- Action: LifecycleAction{
- Type: DeleteAction,
- },
- Condition: LifecycleCondition{
- AgeInDays: 30,
- Liveness: Live,
- CreatedBefore: time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
- MatchesStorageClasses: []string{"NEARLINE"},
- NumNewerVersions: 10,
- },
- }, {
- Action: LifecycleAction{
- Type: DeleteAction,
- },
- Condition: LifecycleCondition{
- Liveness: Archived,
- },
- }},
- },
- },
- raw: raw.Bucket{
- Lifecycle: &raw.BucketLifecycle{
- Rule: []*raw.BucketLifecycleRule{{
- Action: &raw.BucketLifecycleRuleAction{
- Type: SetStorageClassAction,
- StorageClass: "NEARLINE",
- },
- Condition: &raw.BucketLifecycleRuleCondition{
- Age: 10,
- IsLive: googleapi.Bool(true),
- CreatedBefore: "2017-01-02",
- MatchesStorageClass: []string{"MULTI_REGIONAL", "REGIONAL", "STANDARD"},
- NumNewerVersions: 3,
- },
- }, {
- Action: &raw.BucketLifecycleRuleAction{
- Type: DeleteAction,
- },
- Condition: &raw.BucketLifecycleRuleCondition{
- Age: 30,
- IsLive: googleapi.Bool(true),
- CreatedBefore: "2017-01-02",
- MatchesStorageClass: []string{"NEARLINE"},
- NumNewerVersions: 10,
- },
- }, {
- Action: &raw.BucketLifecycleRuleAction{
- Type: DeleteAction,
- },
- Condition: &raw.BucketLifecycleRuleCondition{
- IsLive: googleapi.Bool(false),
- },
- }},
- },
- },
- }} {
- if got := c.attrs.toRawBucket(); !testutil.Equal(*got, c.raw) {
- t.Errorf("toRawBucket: got %v, want %v", *got, c.raw)
- }
- }
- }
-
- func TestUserProject(t *testing.T) {
- // Verify that the userProject query param is sent.
- t.Parallel()
- ctx := context.Background()
- gotURL := make(chan *url.URL, 1)
- hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
- io.Copy(ioutil.Discard, r.Body)
- gotURL <- r.URL
- if strings.Contains(r.URL.String(), "/rewriteTo/") {
- res := &raw.RewriteResponse{Done: true}
- bytes, err := res.MarshalJSON()
- if err != nil {
- t.Fatal(err)
- }
- w.Write(bytes)
- } else {
- fmt.Fprintf(w, "{}")
- }
- })
- defer close()
- client, err := NewClient(ctx, option.WithHTTPClient(hClient))
- if err != nil {
- t.Fatal(err)
- }
-
- re := regexp.MustCompile(`\buserProject=p\b`)
- b := client.Bucket("b").UserProject("p")
- o := b.Object("o")
-
- check := func(msg string, f func()) {
- f()
- select {
- case u := <-gotURL:
- if !re.MatchString(u.RawQuery) {
- t.Errorf("%s: query string %q does not contain userProject", msg, u.RawQuery)
- }
- case <-time.After(2 * time.Second):
- t.Errorf("%s: timed out", msg)
- }
- }
-
- check("buckets.delete", func() { b.Delete(ctx) })
- check("buckets.get", func() { b.Attrs(ctx) })
- check("buckets.patch", func() { b.Update(ctx, BucketAttrsToUpdate{}) })
- check("storage.objects.compose", func() { o.ComposerFrom(b.Object("x")).Run(ctx) })
- check("storage.objects.delete", func() { o.Delete(ctx) })
- check("storage.objects.get", func() { o.Attrs(ctx) })
- check("storage.objects.insert", func() { o.NewWriter(ctx).Close() })
- check("storage.objects.list", func() { b.Objects(ctx, nil).Next() })
- check("storage.objects.patch", func() { o.Update(ctx, ObjectAttrsToUpdate{}) })
- check("storage.objects.rewrite", func() { o.CopierFrom(b.Object("x")).Run(ctx) })
- check("storage.objectAccessControls.list", func() { o.ACL().List(ctx) })
- check("storage.objectAccessControls.update", func() { o.ACL().Set(ctx, "", "") })
- check("storage.objectAccessControls.delete", func() { o.ACL().Delete(ctx, "") })
- check("storage.bucketAccessControls.list", func() { b.ACL().List(ctx) })
- check("storage.bucketAccessControls.update", func() { b.ACL().Set(ctx, "", "") })
- check("storage.bucketAccessControls.delete", func() { b.ACL().Delete(ctx, "") })
- check("storage.defaultObjectAccessControls.list",
- func() { b.DefaultObjectACL().List(ctx) })
- check("storage.defaultObjectAccessControls.update",
- func() { b.DefaultObjectACL().Set(ctx, "", "") })
- check("storage.defaultObjectAccessControls.delete",
- func() { b.DefaultObjectACL().Delete(ctx, "") })
- check("buckets.getIamPolicy", func() { b.IAM().Policy(ctx) })
- check("buckets.setIamPolicy", func() {
- p := &iam.Policy{}
- p.Add("m", iam.Owner)
- b.IAM().SetPolicy(ctx, p)
- })
- check("buckets.testIamPermissions", func() { b.IAM().TestPermissions(ctx, nil) })
- check("storage.notifications.insert", func() {
- b.AddNotification(ctx, &Notification{TopicProjectID: "p", TopicID: "t"})
- })
- check("storage.notifications.delete", func() { b.DeleteNotification(ctx, "n") })
- check("storage.notifications.list", func() { b.Notifications(ctx) })
- }
-
- func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*http.Client, func()) {
- ts := httptest.NewTLSServer(http.HandlerFunc(handler))
- tlsConf := &tls.Config{InsecureSkipVerify: true}
- tr := &http.Transport{
- TLSClientConfig: tlsConf,
- DialTLS: func(netw, addr string) (net.Conn, error) {
- return tls.Dial("tcp", ts.Listener.Addr().String(), tlsConf)
- },
- }
- return &http.Client{Transport: tr}, func() {
- tr.CloseIdleConnections()
- ts.Close()
- }
- }
|