You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

875 lines
26 KiB

  1. // Copyright 2014 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package storage
  15. import (
  16. "crypto/tls"
  17. "encoding/json"
  18. "fmt"
  19. "io"
  20. "io/ioutil"
  21. "log"
  22. "net"
  23. "net/http"
  24. "net/http/httptest"
  25. "net/url"
  26. "regexp"
  27. "strings"
  28. "testing"
  29. "time"
  30. "cloud.google.com/go/iam"
  31. "cloud.google.com/go/internal/testutil"
  32. "golang.org/x/net/context"
  33. "google.golang.org/api/googleapi"
  34. "google.golang.org/api/iterator"
  35. "google.golang.org/api/option"
  36. raw "google.golang.org/api/storage/v1"
  37. )
  38. func TestHeaderSanitization(t *testing.T) {
  39. t.Parallel()
  40. var tests = []struct {
  41. desc string
  42. in []string
  43. want []string
  44. }{
  45. {
  46. desc: "already sanitized headers should not be modified",
  47. in: []string{"x-goog-header1:true", "x-goog-header2:0"},
  48. want: []string{"x-goog-header1:true", "x-goog-header2:0"},
  49. },
  50. {
  51. desc: "sanitized headers should be sorted",
  52. in: []string{"x-goog-header2:0", "x-goog-header1:true"},
  53. want: []string{"x-goog-header1:true", "x-goog-header2:0"},
  54. },
  55. {
  56. desc: "non-canonical headers should be removed",
  57. in: []string{"x-goog-header1:true", "x-goog-no-value", "non-canonical-header:not-of-use"},
  58. want: []string{"x-goog-header1:true"},
  59. },
  60. {
  61. desc: "excluded canonical headers should be removed",
  62. in: []string{"x-goog-header1:true", "x-goog-encryption-key:my_key", "x-goog-encryption-key-sha256:my_sha256"},
  63. want: []string{"x-goog-header1:true"},
  64. },
  65. {
  66. desc: "dirty headers should be formatted correctly",
  67. in: []string{" x-goog-header1 : \textra-spaces ", "X-Goog-Header2:CamelCaseValue"},
  68. want: []string{"x-goog-header1:extra-spaces", "x-goog-header2:CamelCaseValue"},
  69. },
  70. {
  71. desc: "duplicate headers should be merged",
  72. in: []string{"x-goog-header1:value1", "X-Goog-Header1:value2"},
  73. want: []string{"x-goog-header1:value1,value2"},
  74. },
  75. }
  76. for _, test := range tests {
  77. got := sanitizeHeaders(test.in)
  78. if !testutil.Equal(got, test.want) {
  79. t.Errorf("%s: got %v, want %v", test.desc, got, test.want)
  80. }
  81. }
  82. }
  83. func TestSignedURL(t *testing.T) {
  84. t.Parallel()
  85. expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
  86. url, err := SignedURL("bucket-name", "object-name", &SignedURLOptions{
  87. GoogleAccessID: "xxx@clientid",
  88. PrivateKey: dummyKey("rsa"),
  89. Method: "GET",
  90. MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
  91. Expires: expires,
  92. ContentType: "application/json",
  93. Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
  94. })
  95. if err != nil {
  96. t.Error(err)
  97. }
  98. want := "https://storage.googleapis.com/bucket-name/object-name?" +
  99. "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
  100. "RfsHlPtbB2JUYjzCgNr2Mi%2BjggdEuL1V7E6N9o6aaqwVLBDuTv3I0%2B9" +
  101. "x94E6rmmr%2FVgnmZigkIUxX%2Blfl7LgKf30uPGLt0mjKGH2p7r9ey1ONJ" +
  102. "%2BhVec23FnTRcSgopglvHPuCMWU2oNJE%2F1y8EwWE27baHrG1RhRHbLVF" +
  103. "bPpLZ9xTRFK20pluIkfHV00JGljB1imqQHXM%2B2XPWqBngLr%2FwqxLN7i" +
  104. "FcUiqR8xQEOHF%2F2e7fbkTHPNq4TazaLZ8X0eZ3eFdJ55A5QmNi8atlN4W" +
  105. "5q7Hvs0jcxElG3yqIbx439A995BkspLiAcA%2Fo4%2BxAwEMkGLICdbvakq" +
  106. "3eEprNCojw%3D%3D"
  107. if url != want {
  108. t.Fatalf("Unexpected signed URL; found %v", url)
  109. }
  110. }
  111. func TestSignedURL_PEMPrivateKey(t *testing.T) {
  112. t.Parallel()
  113. expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
  114. url, err := SignedURL("bucket-name", "object-name", &SignedURLOptions{
  115. GoogleAccessID: "xxx@clientid",
  116. PrivateKey: dummyKey("pem"),
  117. Method: "GET",
  118. MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
  119. Expires: expires,
  120. ContentType: "application/json",
  121. Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
  122. })
  123. if err != nil {
  124. t.Error(err)
  125. }
  126. want := "https://storage.googleapis.com/bucket-name/object-name?" +
  127. "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
  128. "TiyKD%2FgGb6Kh0kkb2iF%2FfF%2BnTx7L0J4YiZua8AcTmnidutePEGIU5" +
  129. "NULYlrGl6l52gz4zqFb3VFfIRTcPXMdXnnFdMCDhz2QuJBUpsU1Ai9zlyTQ" +
  130. "dkb6ShG03xz9%2BEXWAUQO4GBybJw%2FULASuv37xA00SwLdkqj8YdyS5II" +
  131. "1lro%3D"
  132. if url != want {
  133. t.Fatalf("Unexpected signed URL; found %v", url)
  134. }
  135. }
  136. func TestSignedURL_SignBytes(t *testing.T) {
  137. t.Parallel()
  138. expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
  139. url, err := SignedURL("bucket-name", "object-name", &SignedURLOptions{
  140. GoogleAccessID: "xxx@clientid",
  141. SignBytes: func(b []byte) ([]byte, error) {
  142. return []byte("signed"), nil
  143. },
  144. Method: "GET",
  145. MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
  146. Expires: expires,
  147. ContentType: "application/json",
  148. Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
  149. })
  150. if err != nil {
  151. t.Error(err)
  152. }
  153. want := "https://storage.googleapis.com/bucket-name/object-name?" +
  154. "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=" +
  155. "c2lnbmVk" // base64('signed') == 'c2lnbmVk'
  156. if url != want {
  157. t.Fatalf("Unexpected signed URL\ngot: %q\nwant: %q", url, want)
  158. }
  159. }
  160. func TestSignedURL_URLUnsafeObjectName(t *testing.T) {
  161. t.Parallel()
  162. expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
  163. url, err := SignedURL("bucket-name", "object name界", &SignedURLOptions{
  164. GoogleAccessID: "xxx@clientid",
  165. PrivateKey: dummyKey("pem"),
  166. Method: "GET",
  167. MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
  168. Expires: expires,
  169. ContentType: "application/json",
  170. Headers: []string{"x-goog-header1:true", "x-goog-header2:false"},
  171. })
  172. if err != nil {
  173. t.Error(err)
  174. }
  175. want := "https://storage.googleapis.com/bucket-name/object%20name%E7%95%8C?" +
  176. "Expires=1033570800&GoogleAccessId=xxx%40clientid&Signature=bxVH1%2Bl%2" +
  177. "BSxpnj3XuqKz6mOFk6M94Y%2B4w85J6FCmJan%2FNhGSpndP6fAw1uLHlOn%2F8xUaY%2F" +
  178. "SfZ5GzcQ%2BbxOL1WA37yIwZ7xgLYlO%2ByAi3GuqMUmHZiNCai28emODXQ8RtWHvgv6dE" +
  179. "SQ%2F0KpDMIWW7rYCaUa63UkUyeSQsKhrVqkIA%3D"
  180. if url != want {
  181. t.Fatalf("Unexpected signed URL; found %v", url)
  182. }
  183. }
  184. func TestSignedURL_MissingOptions(t *testing.T) {
  185. t.Parallel()
  186. pk := dummyKey("rsa")
  187. expires, _ := time.Parse(time.RFC3339, "2002-10-02T10:00:00-05:00")
  188. var tests = []struct {
  189. opts *SignedURLOptions
  190. errMsg string
  191. }{
  192. {
  193. &SignedURLOptions{},
  194. "missing required GoogleAccessID",
  195. },
  196. {
  197. &SignedURLOptions{GoogleAccessID: "access_id"},
  198. "exactly one of PrivateKey or SignedBytes must be set",
  199. },
  200. {
  201. &SignedURLOptions{
  202. GoogleAccessID: "access_id",
  203. SignBytes: func(b []byte) ([]byte, error) { return b, nil },
  204. PrivateKey: pk,
  205. },
  206. "exactly one of PrivateKey or SignedBytes must be set",
  207. },
  208. {
  209. &SignedURLOptions{
  210. GoogleAccessID: "access_id",
  211. PrivateKey: pk,
  212. },
  213. "missing required method",
  214. },
  215. {
  216. &SignedURLOptions{
  217. GoogleAccessID: "access_id",
  218. SignBytes: func(b []byte) ([]byte, error) { return b, nil },
  219. },
  220. "missing required method",
  221. },
  222. {
  223. &SignedURLOptions{
  224. GoogleAccessID: "access_id",
  225. PrivateKey: pk,
  226. Method: "PUT",
  227. },
  228. "missing required expires",
  229. },
  230. {
  231. &SignedURLOptions{
  232. GoogleAccessID: "access_id",
  233. PrivateKey: pk,
  234. Method: "PUT",
  235. Expires: expires,
  236. MD5: "invalid",
  237. },
  238. "invalid MD5 checksum",
  239. },
  240. }
  241. for _, test := range tests {
  242. _, err := SignedURL("bucket", "name", test.opts)
  243. if !strings.Contains(err.Error(), test.errMsg) {
  244. t.Errorf("expected err: %v, found: %v", test.errMsg, err)
  245. }
  246. }
  247. }
  248. func dummyKey(kind string) []byte {
  249. slurp, err := ioutil.ReadFile(fmt.Sprintf("./testdata/dummy_%s", kind))
  250. if err != nil {
  251. log.Fatal(err)
  252. }
  253. return slurp
  254. }
  255. func TestObjectNames(t *testing.T) {
  256. t.Parallel()
  257. // Naming requirements: https://cloud.google.com/storage/docs/bucket-naming
  258. const maxLegalLength = 1024
  259. type testT struct {
  260. name, want string
  261. }
  262. tests := []testT{
  263. // Embedded characters important in URLs.
  264. {"foo % bar", "foo%20%25%20bar"},
  265. {"foo ? bar", "foo%20%3F%20bar"},
  266. {"foo / bar", "foo%20/%20bar"},
  267. {"foo %?/ bar", "foo%20%25%3F/%20bar"},
  268. // Non-Roman scripts
  269. {"타코", "%ED%83%80%EC%BD%94"},
  270. {"世界", "%E4%B8%96%E7%95%8C"},
  271. // Longest legal name
  272. {strings.Repeat("a", maxLegalLength), strings.Repeat("a", maxLegalLength)},
  273. // Line terminators besides CR and LF: https://en.wikipedia.org/wiki/Newline#Unicode
  274. {"foo \u000b bar", "foo%20%0B%20bar"},
  275. {"foo \u000c bar", "foo%20%0C%20bar"},
  276. {"foo \u0085 bar", "foo%20%C2%85%20bar"},
  277. {"foo \u2028 bar", "foo%20%E2%80%A8%20bar"},
  278. {"foo \u2029 bar", "foo%20%E2%80%A9%20bar"},
  279. // Null byte.
  280. {"foo \u0000 bar", "foo%20%00%20bar"},
  281. // Non-control characters that are discouraged, but not forbidden, according to the documentation.
  282. {"foo # bar", "foo%20%23%20bar"},
  283. {"foo []*? bar", "foo%20%5B%5D%2A%3F%20bar"},
  284. // Angstrom symbol singleton and normalized forms: http://unicode.org/reports/tr15/
  285. {"foo \u212b bar", "foo%20%E2%84%AB%20bar"},
  286. {"foo \u0041\u030a bar", "foo%20A%CC%8A%20bar"},
  287. {"foo \u00c5 bar", "foo%20%C3%85%20bar"},
  288. // Hangul separating jamo: http://www.unicode.org/versions/Unicode7.0.0/ch18.pdf (Table 18-10)
  289. {"foo \u3131\u314f bar", "foo%20%E3%84%B1%E3%85%8F%20bar"},
  290. {"foo \u1100\u1161 bar", "foo%20%E1%84%80%E1%85%A1%20bar"},
  291. {"foo \uac00 bar", "foo%20%EA%B0%80%20bar"},
  292. }
  293. // C0 control characters not forbidden by the docs.
  294. var runes []rune
  295. for r := rune(0x01); r <= rune(0x1f); r++ {
  296. if r != '\u000a' && r != '\u000d' {
  297. runes = append(runes, r)
  298. }
  299. }
  300. 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"})
  301. // C1 control characters, plus DEL.
  302. runes = nil
  303. for r := rune(0x7f); r <= rune(0x9f); r++ {
  304. runes = append(runes, r)
  305. }
  306. 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"})
  307. opts := &SignedURLOptions{
  308. GoogleAccessID: "xxx@clientid",
  309. PrivateKey: dummyKey("rsa"),
  310. Method: "GET",
  311. MD5: "ICy5YqxZB1uWSwcVLSNLcA==",
  312. Expires: time.Date(2002, time.October, 2, 10, 0, 0, 0, time.UTC),
  313. ContentType: "application/json",
  314. Headers: []string{"x-goog-header1", "x-goog-header2"},
  315. }
  316. for _, test := range tests {
  317. g, err := SignedURL("bucket-name", test.name, opts)
  318. if err != nil {
  319. t.Errorf("SignedURL(%q) err=%v, want nil", test.name, err)
  320. }
  321. if w := "/bucket-name/" + test.want; !strings.Contains(g, w) {
  322. t.Errorf("SignedURL(%q)=%q, want substring %q", test.name, g, w)
  323. }
  324. }
  325. }
  326. func TestCondition(t *testing.T) {
  327. t.Parallel()
  328. gotReq := make(chan *http.Request, 1)
  329. hc, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
  330. io.Copy(ioutil.Discard, r.Body)
  331. gotReq <- r
  332. w.WriteHeader(200)
  333. })
  334. defer close()
  335. ctx := context.Background()
  336. c, err := NewClient(ctx, option.WithHTTPClient(hc))
  337. if err != nil {
  338. t.Fatal(err)
  339. }
  340. obj := c.Bucket("buck").Object("obj")
  341. dst := c.Bucket("dstbuck").Object("dst")
  342. tests := []struct {
  343. fn func()
  344. want string
  345. }{
  346. {
  347. func() { obj.Generation(1234).NewReader(ctx) },
  348. "GET /buck/obj?generation=1234",
  349. },
  350. {
  351. func() { obj.If(Conditions{GenerationMatch: 1234}).NewReader(ctx) },
  352. "GET /buck/obj?ifGenerationMatch=1234",
  353. },
  354. {
  355. func() { obj.If(Conditions{GenerationNotMatch: 1234}).NewReader(ctx) },
  356. "GET /buck/obj?ifGenerationNotMatch=1234",
  357. },
  358. {
  359. func() { obj.If(Conditions{MetagenerationMatch: 1234}).NewReader(ctx) },
  360. "GET /buck/obj?ifMetagenerationMatch=1234",
  361. },
  362. {
  363. func() { obj.If(Conditions{MetagenerationNotMatch: 1234}).NewReader(ctx) },
  364. "GET /buck/obj?ifMetagenerationNotMatch=1234",
  365. },
  366. {
  367. func() { obj.If(Conditions{MetagenerationNotMatch: 1234}).Attrs(ctx) },
  368. "GET /storage/v1/b/buck/o/obj?alt=json&ifMetagenerationNotMatch=1234&projection=full",
  369. },
  370. {
  371. func() { obj.If(Conditions{MetagenerationMatch: 1234}).Update(ctx, ObjectAttrsToUpdate{}) },
  372. "PATCH /storage/v1/b/buck/o/obj?alt=json&ifMetagenerationMatch=1234&projection=full",
  373. },
  374. {
  375. func() { obj.Generation(1234).Delete(ctx) },
  376. "DELETE /storage/v1/b/buck/o/obj?alt=json&generation=1234",
  377. },
  378. {
  379. func() {
  380. w := obj.If(Conditions{GenerationMatch: 1234}).NewWriter(ctx)
  381. w.ContentType = "text/plain"
  382. w.Close()
  383. },
  384. "POST /upload/storage/v1/b/buck/o?alt=json&ifGenerationMatch=1234&projection=full&uploadType=multipart",
  385. },
  386. {
  387. func() {
  388. w := obj.If(Conditions{DoesNotExist: true}).NewWriter(ctx)
  389. w.ContentType = "text/plain"
  390. w.Close()
  391. },
  392. "POST /upload/storage/v1/b/buck/o?alt=json&ifGenerationMatch=0&projection=full&uploadType=multipart",
  393. },
  394. {
  395. func() {
  396. dst.If(Conditions{MetagenerationMatch: 5678}).CopierFrom(obj.If(Conditions{GenerationMatch: 1234})).Run(ctx)
  397. },
  398. "POST /storage/v1/b/buck/o/obj/rewriteTo/b/dstbuck/o/dst?alt=json&ifMetagenerationMatch=5678&ifSourceGenerationMatch=1234&projection=full",
  399. },
  400. }
  401. for i, tt := range tests {
  402. tt.fn()
  403. select {
  404. case r := <-gotReq:
  405. got := r.Method + " " + r.RequestURI
  406. if got != tt.want {
  407. t.Errorf("%d. RequestURI = %q; want %q", i, got, tt.want)
  408. }
  409. case <-time.After(5 * time.Second):
  410. t.Fatalf("%d. timeout", i)
  411. }
  412. if err != nil {
  413. t.Fatal(err)
  414. }
  415. }
  416. // Test an error, too:
  417. err = obj.Generation(1234).NewWriter(ctx).Close()
  418. if err == nil || !strings.Contains(err.Error(), "NewWriter: generation not supported") {
  419. t.Errorf("want error about unsupported generation; got %v", err)
  420. }
  421. }
  422. func TestConditionErrors(t *testing.T) {
  423. t.Parallel()
  424. for _, conds := range []Conditions{
  425. {GenerationMatch: 0},
  426. {DoesNotExist: false}, // same as above, actually
  427. {GenerationMatch: 1, GenerationNotMatch: 2},
  428. {GenerationNotMatch: 2, DoesNotExist: true},
  429. {MetagenerationMatch: 1, MetagenerationNotMatch: 2},
  430. } {
  431. if err := conds.validate(""); err == nil {
  432. t.Errorf("%+v: got nil, want error", conds)
  433. }
  434. }
  435. }
  436. // Test object compose.
  437. func TestObjectCompose(t *testing.T) {
  438. t.Parallel()
  439. gotURL := make(chan string, 1)
  440. gotBody := make(chan []byte, 1)
  441. hc, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
  442. body, _ := ioutil.ReadAll(r.Body)
  443. gotURL <- r.URL.String()
  444. gotBody <- body
  445. w.Write([]byte("{}"))
  446. })
  447. defer close()
  448. ctx := context.Background()
  449. c, err := NewClient(ctx, option.WithHTTPClient(hc))
  450. if err != nil {
  451. t.Fatal(err)
  452. }
  453. testCases := []struct {
  454. desc string
  455. dst *ObjectHandle
  456. srcs []*ObjectHandle
  457. attrs *ObjectAttrs
  458. wantReq raw.ComposeRequest
  459. wantURL string
  460. wantErr bool
  461. }{
  462. {
  463. desc: "basic case",
  464. dst: c.Bucket("foo").Object("bar"),
  465. srcs: []*ObjectHandle{
  466. c.Bucket("foo").Object("baz"),
  467. c.Bucket("foo").Object("quux"),
  468. },
  469. wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json",
  470. wantReq: raw.ComposeRequest{
  471. Destination: &raw.Object{Bucket: "foo"},
  472. SourceObjects: []*raw.ComposeRequestSourceObjects{
  473. {Name: "baz"},
  474. {Name: "quux"},
  475. },
  476. },
  477. },
  478. {
  479. desc: "with object attrs",
  480. dst: c.Bucket("foo").Object("bar"),
  481. srcs: []*ObjectHandle{
  482. c.Bucket("foo").Object("baz"),
  483. c.Bucket("foo").Object("quux"),
  484. },
  485. attrs: &ObjectAttrs{
  486. Name: "not-bar",
  487. ContentType: "application/json",
  488. },
  489. wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json",
  490. wantReq: raw.ComposeRequest{
  491. Destination: &raw.Object{
  492. Bucket: "foo",
  493. Name: "not-bar",
  494. ContentType: "application/json",
  495. },
  496. SourceObjects: []*raw.ComposeRequestSourceObjects{
  497. {Name: "baz"},
  498. {Name: "quux"},
  499. },
  500. },
  501. },
  502. {
  503. desc: "with conditions",
  504. dst: c.Bucket("foo").Object("bar").If(Conditions{
  505. GenerationMatch: 12,
  506. MetagenerationMatch: 34,
  507. }),
  508. srcs: []*ObjectHandle{
  509. c.Bucket("foo").Object("baz").Generation(56),
  510. c.Bucket("foo").Object("quux").If(Conditions{GenerationMatch: 78}),
  511. },
  512. wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json&ifGenerationMatch=12&ifMetagenerationMatch=34",
  513. wantReq: raw.ComposeRequest{
  514. Destination: &raw.Object{Bucket: "foo"},
  515. SourceObjects: []*raw.ComposeRequestSourceObjects{
  516. {
  517. Name: "baz",
  518. Generation: 56,
  519. },
  520. {
  521. Name: "quux",
  522. ObjectPreconditions: &raw.ComposeRequestSourceObjectsObjectPreconditions{
  523. IfGenerationMatch: 78,
  524. },
  525. },
  526. },
  527. },
  528. },
  529. {
  530. desc: "no sources",
  531. dst: c.Bucket("foo").Object("bar"),
  532. wantErr: true,
  533. },
  534. {
  535. desc: "destination, no bucket",
  536. dst: c.Bucket("").Object("bar"),
  537. srcs: []*ObjectHandle{
  538. c.Bucket("foo").Object("baz"),
  539. },
  540. wantErr: true,
  541. },
  542. {
  543. desc: "destination, no object",
  544. dst: c.Bucket("foo").Object(""),
  545. srcs: []*ObjectHandle{
  546. c.Bucket("foo").Object("baz"),
  547. },
  548. wantErr: true,
  549. },
  550. {
  551. desc: "source, different bucket",
  552. dst: c.Bucket("foo").Object("bar"),
  553. srcs: []*ObjectHandle{
  554. c.Bucket("otherbucket").Object("baz"),
  555. },
  556. wantErr: true,
  557. },
  558. {
  559. desc: "source, no object",
  560. dst: c.Bucket("foo").Object("bar"),
  561. srcs: []*ObjectHandle{
  562. c.Bucket("foo").Object(""),
  563. },
  564. wantErr: true,
  565. },
  566. {
  567. desc: "destination, bad condition",
  568. dst: c.Bucket("foo").Object("bar").Generation(12),
  569. srcs: []*ObjectHandle{
  570. c.Bucket("foo").Object("baz"),
  571. },
  572. wantErr: true,
  573. },
  574. {
  575. desc: "source, bad condition",
  576. dst: c.Bucket("foo").Object("bar"),
  577. srcs: []*ObjectHandle{
  578. c.Bucket("foo").Object("baz").If(Conditions{MetagenerationMatch: 12}),
  579. },
  580. wantErr: true,
  581. },
  582. }
  583. for _, tt := range testCases {
  584. composer := tt.dst.ComposerFrom(tt.srcs...)
  585. if tt.attrs != nil {
  586. composer.ObjectAttrs = *tt.attrs
  587. }
  588. _, err := composer.Run(ctx)
  589. if gotErr := err != nil; gotErr != tt.wantErr {
  590. t.Errorf("%s: got error %v; want err %t", tt.desc, err, tt.wantErr)
  591. continue
  592. }
  593. if tt.wantErr {
  594. continue
  595. }
  596. url, body := <-gotURL, <-gotBody
  597. if url != tt.wantURL {
  598. t.Errorf("%s: request URL\ngot %q\nwant %q", tt.desc, url, tt.wantURL)
  599. }
  600. var req raw.ComposeRequest
  601. if err := json.Unmarshal(body, &req); err != nil {
  602. t.Errorf("%s: json.Unmarshal %v (body %s)", tt.desc, err, body)
  603. }
  604. if !testutil.Equal(req, tt.wantReq) {
  605. // Print to JSON.
  606. wantReq, _ := json.Marshal(tt.wantReq)
  607. t.Errorf("%s: request body\ngot %s\nwant %s", tt.desc, body, wantReq)
  608. }
  609. }
  610. }
  611. // Test that ObjectIterator's Next and NextPage methods correctly terminate
  612. // if there is nothing to iterate over.
  613. func TestEmptyObjectIterator(t *testing.T) {
  614. t.Parallel()
  615. hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
  616. io.Copy(ioutil.Discard, r.Body)
  617. fmt.Fprintf(w, "{}")
  618. })
  619. defer close()
  620. ctx := context.Background()
  621. client, err := NewClient(ctx, option.WithHTTPClient(hClient))
  622. if err != nil {
  623. t.Fatal(err)
  624. }
  625. it := client.Bucket("b").Objects(ctx, nil)
  626. _, err = it.Next()
  627. if err != iterator.Done {
  628. t.Errorf("got %v, want Done", err)
  629. }
  630. }
  631. // Test that BucketIterator's Next method correctly terminates if there is
  632. // nothing to iterate over.
  633. func TestEmptyBucketIterator(t *testing.T) {
  634. t.Parallel()
  635. hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
  636. io.Copy(ioutil.Discard, r.Body)
  637. fmt.Fprintf(w, "{}")
  638. })
  639. defer close()
  640. ctx := context.Background()
  641. client, err := NewClient(ctx, option.WithHTTPClient(hClient))
  642. if err != nil {
  643. t.Fatal(err)
  644. }
  645. it := client.Buckets(ctx, "project")
  646. _, err = it.Next()
  647. if err != iterator.Done {
  648. t.Errorf("got %v, want Done", err)
  649. }
  650. }
  651. func TestCodecUint32(t *testing.T) {
  652. t.Parallel()
  653. for _, u := range []uint32{0, 1, 256, 0xFFFFFFFF} {
  654. s := encodeUint32(u)
  655. d, err := decodeUint32(s)
  656. if err != nil {
  657. t.Fatal(err)
  658. }
  659. if d != u {
  660. t.Errorf("got %d, want input %d", d, u)
  661. }
  662. }
  663. }
  664. func TestBucketAttrs(t *testing.T) {
  665. for _, c := range []struct {
  666. attrs BucketAttrs
  667. raw raw.Bucket
  668. }{{
  669. attrs: BucketAttrs{
  670. Lifecycle: Lifecycle{
  671. Rules: []LifecycleRule{{
  672. Action: LifecycleAction{
  673. Type: SetStorageClassAction,
  674. StorageClass: "NEARLINE",
  675. },
  676. Condition: LifecycleCondition{
  677. AgeInDays: 10,
  678. Liveness: Live,
  679. CreatedBefore: time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
  680. MatchesStorageClasses: []string{"MULTI_REGIONAL", "REGIONAL", "STANDARD"},
  681. NumNewerVersions: 3,
  682. },
  683. }, {
  684. Action: LifecycleAction{
  685. Type: DeleteAction,
  686. },
  687. Condition: LifecycleCondition{
  688. AgeInDays: 30,
  689. Liveness: Live,
  690. CreatedBefore: time.Date(2017, 1, 2, 3, 4, 5, 6, time.UTC),
  691. MatchesStorageClasses: []string{"NEARLINE"},
  692. NumNewerVersions: 10,
  693. },
  694. }, {
  695. Action: LifecycleAction{
  696. Type: DeleteAction,
  697. },
  698. Condition: LifecycleCondition{
  699. Liveness: Archived,
  700. },
  701. }},
  702. },
  703. },
  704. raw: raw.Bucket{
  705. Lifecycle: &raw.BucketLifecycle{
  706. Rule: []*raw.BucketLifecycleRule{{
  707. Action: &raw.BucketLifecycleRuleAction{
  708. Type: SetStorageClassAction,
  709. StorageClass: "NEARLINE",
  710. },
  711. Condition: &raw.BucketLifecycleRuleCondition{
  712. Age: 10,
  713. IsLive: googleapi.Bool(true),
  714. CreatedBefore: "2017-01-02",
  715. MatchesStorageClass: []string{"MULTI_REGIONAL", "REGIONAL", "STANDARD"},
  716. NumNewerVersions: 3,
  717. },
  718. }, {
  719. Action: &raw.BucketLifecycleRuleAction{
  720. Type: DeleteAction,
  721. },
  722. Condition: &raw.BucketLifecycleRuleCondition{
  723. Age: 30,
  724. IsLive: googleapi.Bool(true),
  725. CreatedBefore: "2017-01-02",
  726. MatchesStorageClass: []string{"NEARLINE"},
  727. NumNewerVersions: 10,
  728. },
  729. }, {
  730. Action: &raw.BucketLifecycleRuleAction{
  731. Type: DeleteAction,
  732. },
  733. Condition: &raw.BucketLifecycleRuleCondition{
  734. IsLive: googleapi.Bool(false),
  735. },
  736. }},
  737. },
  738. },
  739. }} {
  740. if got := c.attrs.toRawBucket(); !testutil.Equal(*got, c.raw) {
  741. t.Errorf("toRawBucket: got %v, want %v", *got, c.raw)
  742. }
  743. }
  744. }
  745. func TestUserProject(t *testing.T) {
  746. // Verify that the userProject query param is sent.
  747. t.Parallel()
  748. ctx := context.Background()
  749. gotURL := make(chan *url.URL, 1)
  750. hClient, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
  751. io.Copy(ioutil.Discard, r.Body)
  752. gotURL <- r.URL
  753. if strings.Contains(r.URL.String(), "/rewriteTo/") {
  754. res := &raw.RewriteResponse{Done: true}
  755. bytes, err := res.MarshalJSON()
  756. if err != nil {
  757. t.Fatal(err)
  758. }
  759. w.Write(bytes)
  760. } else {
  761. fmt.Fprintf(w, "{}")
  762. }
  763. })
  764. defer close()
  765. client, err := NewClient(ctx, option.WithHTTPClient(hClient))
  766. if err != nil {
  767. t.Fatal(err)
  768. }
  769. re := regexp.MustCompile(`\buserProject=p\b`)
  770. b := client.Bucket("b").UserProject("p")
  771. o := b.Object("o")
  772. check := func(msg string, f func()) {
  773. f()
  774. select {
  775. case u := <-gotURL:
  776. if !re.MatchString(u.RawQuery) {
  777. t.Errorf("%s: query string %q does not contain userProject", msg, u.RawQuery)
  778. }
  779. case <-time.After(2 * time.Second):
  780. t.Errorf("%s: timed out", msg)
  781. }
  782. }
  783. check("buckets.delete", func() { b.Delete(ctx) })
  784. check("buckets.get", func() { b.Attrs(ctx) })
  785. check("buckets.patch", func() { b.Update(ctx, BucketAttrsToUpdate{}) })
  786. check("storage.objects.compose", func() { o.ComposerFrom(b.Object("x")).Run(ctx) })
  787. check("storage.objects.delete", func() { o.Delete(ctx) })
  788. check("storage.objects.get", func() { o.Attrs(ctx) })
  789. check("storage.objects.insert", func() { o.NewWriter(ctx).Close() })
  790. check("storage.objects.list", func() { b.Objects(ctx, nil).Next() })
  791. check("storage.objects.patch", func() { o.Update(ctx, ObjectAttrsToUpdate{}) })
  792. check("storage.objects.rewrite", func() { o.CopierFrom(b.Object("x")).Run(ctx) })
  793. check("storage.objectAccessControls.list", func() { o.ACL().List(ctx) })
  794. check("storage.objectAccessControls.update", func() { o.ACL().Set(ctx, "", "") })
  795. check("storage.objectAccessControls.delete", func() { o.ACL().Delete(ctx, "") })
  796. check("storage.bucketAccessControls.list", func() { b.ACL().List(ctx) })
  797. check("storage.bucketAccessControls.update", func() { b.ACL().Set(ctx, "", "") })
  798. check("storage.bucketAccessControls.delete", func() { b.ACL().Delete(ctx, "") })
  799. check("storage.defaultObjectAccessControls.list",
  800. func() { b.DefaultObjectACL().List(ctx) })
  801. check("storage.defaultObjectAccessControls.update",
  802. func() { b.DefaultObjectACL().Set(ctx, "", "") })
  803. check("storage.defaultObjectAccessControls.delete",
  804. func() { b.DefaultObjectACL().Delete(ctx, "") })
  805. check("buckets.getIamPolicy", func() { b.IAM().Policy(ctx) })
  806. check("buckets.setIamPolicy", func() {
  807. p := &iam.Policy{}
  808. p.Add("m", iam.Owner)
  809. b.IAM().SetPolicy(ctx, p)
  810. })
  811. check("buckets.testIamPermissions", func() { b.IAM().TestPermissions(ctx, nil) })
  812. check("storage.notifications.insert", func() {
  813. b.AddNotification(ctx, &Notification{TopicProjectID: "p", TopicID: "t"})
  814. })
  815. check("storage.notifications.delete", func() { b.DeleteNotification(ctx, "n") })
  816. check("storage.notifications.list", func() { b.Notifications(ctx) })
  817. }
  818. func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*http.Client, func()) {
  819. ts := httptest.NewTLSServer(http.HandlerFunc(handler))
  820. tlsConf := &tls.Config{InsecureSkipVerify: true}
  821. tr := &http.Transport{
  822. TLSClientConfig: tlsConf,
  823. DialTLS: func(netw, addr string) (net.Conn, error) {
  824. return tls.Dial("tcp", ts.Listener.Addr().String(), tlsConf)
  825. },
  826. }
  827. return &http.Client{Transport: tr}, func() {
  828. tr.CloseIdleConnections()
  829. ts.Close()
  830. }
  831. }