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.
 
 
 

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