Merge lp:~jtv/gwacl/storage-urls into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 113
Merged at revision: 102
Proposed branch: lp:~jtv/gwacl/storage-urls
Merge into: lp:gwacl
Diff against target: 576 lines (+187/-190)
6 files modified
storage_base.go (+44/-53)
storage_base_test.go (+54/-82)
storage_test.go (+5/-4)
utils.go (+21/-20)
utils_test.go (+60/-26)
x509session.go (+3/-5)
To merge this branch: bzr merge lp:~jtv/gwacl/storage-urls
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Gavin Panella Pending
Review via email: mp+161562@code.launchpad.net

This proposal supersedes a proposal from 2013-04-29.

Commit message

Factor out some repeated composition of storage URLs.

Description of the change

This increases the testing code a bit, but simplifies run-of-the-mill code nicely and eliminates some error-prone repetition. I also moved the addURLQueryParam() helper into utils.go, where it probably belongs now. Actually addURLQueryParam() is now a simple wrapper around addURLQueryParams() — I didn't bother testing them separately given how thin the wrapping is. Maybe I should drop the singular one now.

Much of the added complication in tests is an absolutely revolting property of url.URL: it does not maintain a consistent sorting of URL parameters. It's arbitrary. So the tests that happily assumed a fixed ordering, now that we're setting them "properly" through the standard library, start failing randomly. Different subset every time. The tests need to do looser text matching on URLs, and separate matching on the query.

With this change, interpolateURL() is barely used any more — just two cases, with one interpolation argument each. And I'm not even sure the one in x509Session.composeURL() is really a good idea. So unless we find new use for interpolateURL(), it might be worth dropping.

Jeroen

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal

I think addURLQueryParams() is good.

However, I don't think get{Account,Container,File}URL and the
resulting changes to storage_base.go are very useful. Despite the lack
of precedent, the syntax used with interpolateURL() is quite obvious
and readable, though I am surely biased. There's more code now, it
obfuscates what's happening, and I suspect YAGNI too.

I think most of the benefit of this branch could be obtained with:

  const STORAGE_URL_TEMPLATE = "http://___.blob.core.windows.net"

  s/"http://___.blob.core.windows.net"/STORAGE_URL_TEMPLATE + "/g

I'm torn. On the one hand I could well be biased because I introduced
interpolateURL(). On the other hand, I have little love for any Go
code, mine or otherwise. I vote approve, but please don't land this
unless you're really sure that it improves gwacl, regardless of any
other factor.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal

Hypothetically, if get{Account,Container,File}URL each constructed their URL from scratch, repeating part of the URL between the three but not between call sites, would that assuage your concerns?

Revision history for this message
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal

> Hypothetically, if get{Account,Container,File}URL each constructed
> their URL from scratch, repeating part of the URL between the three
> but not between call sites, would that assuage your concerns?

Bizarrely enough, even though I like addURLQueryParam(s), it's its use
that adds the most noise. There's the map[string][]string{...}
construction and then the err != nil bits, both of which are almost
entirely noise, and distracting from the intention of the code.

There are two changes I might make:

1. Don't use a map[noise noise noise}, create a new struct to hold
   query parameters and pass them using a variadic arg. This might
   also help with the ordering issue you mentioned.

   -- or --

   Use a url.Values instead of a map[noise}. It won't help with the
   ordering though, if this is where the problem lies.

2. Panic instead of returning an error? I'm not saying anything new,
   but returning errors means loads of err != nil noise, and panicking
   is awkward to deal with because defer/recover is about as pleasant
   as rubbing fire ants into one's squishy bits. The choice is not
   clear.

   However, if the URL can't be parsed in the situations we have in
   gwacl *at this point in time* (as you say, the URL is tightly
   controlled), I would say that's a programmatic error rather than a
   runtime error, and so panicking seems appropriate.

Something like:

  type queryParam struct {
      name string
      value string
  }

  func addURLQueryParams(
      originalURL string, params ...queryParam) string {
      parsedURL, err := url.Parse(originalURL)
      if err != nil {
          panic(err)
      }
      query := parsedURL.Query()
      for param := range params {
          query.Add(param.name, param.value)
      }
      parsedURL.RawQuery = query.Encode()
      return parsedURL.String()
  }

  url := "http://example.com"
  addURLQueryParams(
      url, queryParam{"fred", "foo"}, queryParam("fred", "bar"})
  c.Assert(url, Equals, "http://example.com?fred=foo&fred=bar")

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal

About interpolateURL, I think fmt.Sprintf("http://%s.foo/bar//", url.QueryEscape(account)) isn't so bad for just a few cases in the codebase. Not particularly hard to read.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal

> 1. Don't use a map[noise noise noise}, create a new struct to hold
> query parameters and pass them using a variadic arg. This might
> also help with the ordering issue you mentioned.

Good one. I'll do that. It also unifies the two functions.

> 2. Panic instead of returning an error? I'm not saying anything new,
> but returning errors means loads of err != nil noise, and panicking
> is awkward to deal with because defer/recover is about as pleasant
> as rubbing fire ants into one's squishy bits. The choice is not
> clear.
>
> However, if the URL can't be parsed in the situations we have in
> gwacl *at this point in time* (as you say, the URL is tightly
> controlled), I would say that's a programmatic error rather than a
> runtime error, and so panicking seems appropriate.

I toyed with the idea, but didn't have the courage to do it. But if I have an accomplice, that makes me feel safer.

Attempts to write clean code feel so futile in Go... In any other language, if I see the same 4 or 5 lines repeated all over the place, that's intolerable so I extract a function. But in Go, calling the function is 4 or 5 lines of code. Maybe 6 if it means I have to pre-define a variable to avoid shadowing. Or I can compress it a bit if I don't mind making the code harder to read — but I do.

Jeroen

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote : Posted in a previous version of this proposal

Okay, I removed interpolateURL and that simplified a few things (like over-interpolation bugs we had to watch out for). I also whittled down the redundancy in the get*URL() tests. The branch now removes about as many lines as it adds. Since I'm more or less manning the fort alone this week, I'm going to land this and then we can argue over any remaining details later.

Revision history for this message
Julian Edwards (julian-edwards) wrote : Posted in a previous version of this proposal

On 29/04/13 22:57, Gavin Panella wrote:
> Review: Approve
>
> I think addURLQueryParams() is good.
>
> However, I don't think get{Account,Container,File}URL and the
> resulting changes to storage_base.go are very useful. Despite the lack
> of precedent, the syntax used with interpolateURL() is quite obvious
> and readable, though I am surely biased. There's more code now, it
> obfuscates what's happening, and I suspect YAGNI too.
>
> I think most of the benefit of this branch could be obtained with:
>
> const STORAGE_URL_TEMPLATE = "http://___.blob.core.windows.net"
>
> s/"http://___.blob.core.windows.net"/STORAGE_URL_TEMPLATE + "/g
>
> I'm torn. On the one hand I could well be biased because I introduced
> interpolateURL(). On the other hand, I have little love for any Go
> code, mine or otherwise. I vote approve, but please don't land this
> unless you're really sure that it improves gwacl, regardless of any
> other factor.
>

FWIW, that URL needs to be configurable at some point. There is a
development simulator service which we may want to use if anyone dares
run up Windows.

Revision history for this message
Julian Edwards (julian-edwards) wrote : Posted in a previous version of this proposal
Revision history for this message
Julian Edwards (julian-edwards) wrote : Posted in a previous version of this proposal
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

This has already been approved in a previous incarnation of the MP, but I'm resubmitting it because Tarmac is refusing to land it. The previous merge proposal had a prerequisite branch, which has already landed.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'storage_base.go'
2--- storage_base.go 2013-04-30 08:58:11 +0000
3+++ storage_base.go 2013-04-30 09:36:29 +0000
4@@ -265,18 +265,23 @@
5 return resp, nil
6 }
7
8-// addURLQueryParam adds a query parameter to a URL (and escapes as needed).
9-func addURLQueryParam(originalURL, key, value string) (string, error) {
10- parsedURL, err := url.Parse(originalURL)
11- if err != nil {
12- // Not much we can add beyond that the URL doesn't parse. Leave it
13- // to the caller to provide context.
14- return "", err
15- }
16- query := parsedURL.Query()
17- query.Add(key, value)
18- parsedURL.RawQuery = query.Encode()
19- return parsedURL.String(), nil
20+// getAccountURL returns the base URL for the context's storage account.
21+// (The result ends in a slash.)
22+func (context *StorageContext) getAccountURL() string {
23+ escapedAccount := url.QueryEscape(context.Account)
24+ return fmt.Sprintf("http://%s.blob.core.windows.net/", escapedAccount)
25+}
26+
27+// getContainerURL returns the URL for a given storage container.
28+// (The result does not end in a slash.)
29+func (context *StorageContext) getContainerURL(container string) string {
30+ return context.getAccountURL() + url.QueryEscape(container)
31+}
32+
33+// getFileURL returns the URL for a given file in the given container.
34+// (The result does not end in a slash.)
35+func (context *StorageContext) getFileURL(container, filename string) string {
36+ return context.getContainerURL(container) + "/" + url.QueryEscape(filename)
37 }
38
39 // getListContainersBatch calls the "List Containers" operation on the storage
40@@ -285,16 +290,10 @@
41 // subsequent calls to get additional batches of the same result, pass the
42 // NextMarker from the previous call's result.
43 func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, error) {
44- var err error
45- uri := interpolateURL("http://___.blob.core.windows.net/?comp=list", context.Account)
46+ uri := context.getAccountURL()
47+ uri = addURLQueryParams(uri, "comp", "list")
48 if marker != "" {
49- // Add the marker argument. Do it after interpolation, in case the
50- // marker string might contain the interpolation's placeholder
51- // sequence.
52- uri, err = addURLQueryParam(uri, "marker", marker)
53- if err != nil {
54- return nil, fmt.Errorf("malformed storage account URL '%s': %v", uri, err)
55- }
56+ uri = addURLQueryParams(uri, "marker", marker)
57 }
58 req, err := http.NewRequest("GET", uri, nil)
59 if err != nil {
60@@ -349,18 +348,13 @@
61 // subsequent calls to get additional batches of the same result, pass the
62 // NextMarker from the previous call's result.
63 func (context *StorageContext) getListBlobsBatch(container, marker string) (*BlobEnumerationResults, error) {
64- var err error
65- uri := interpolateURL(
66- "http://___.blob.core.windows.net/___?restype=container&comp=list",
67- context.Account, container)
68+ uri := context.getContainerURL(container)
69+ uri = addURLQueryParams(
70+ uri,
71+ "restype", "container",
72+ "comp", "list")
73 if marker != "" {
74- // Add the marker argument. Do it after interpolation, in case the
75- // marker string might contain the interpolation's placeholder
76- // sequence.
77- uri, err = addURLQueryParam(uri, "marker", marker)
78- if err != nil {
79- return nil, fmt.Errorf("malformed storage account URL '%s': %v", uri, err)
80- }
81+ uri = addURLQueryParams(uri, "marker", marker)
82 }
83 req, err := http.NewRequest("GET", uri, nil)
84 if err != nil {
85@@ -408,9 +402,8 @@
86 // Send a request to the storage service to create a new container. If the
87 // request fails, error is non-nil.
88 func (context *StorageContext) CreateContainer(container string) error {
89- uri := interpolateURL(
90- "http://___.blob.core.windows.net/___?restype=container",
91- context.Account, container)
92+ uri := context.getContainerURL(container)
93+ uri = addURLQueryParams(uri, "restype", "container")
94 req, err := http.NewRequest("PUT", uri, nil)
95 if err != nil {
96 return err
97@@ -427,9 +420,7 @@
98 // do the uploading, it just makes an empty file.
99 func (context *StorageContext) PutBlob(container, filename string) error {
100 // TODO: Add blobtype header.
101- uri := interpolateURL(
102- "http://___.blob.core.windows.net/___/___",
103- context.Account, container, filename)
104+ uri := context.getFileURL(container, filename)
105 req, err := http.NewRequest("PUT", uri, nil)
106 if err != nil {
107 return err
108@@ -445,9 +436,12 @@
109 // Send a request to fetch the list of blocks that have been uploaded as part
110 // of a block blob.
111 func (context *StorageContext) GetBlockList(container, filename string) (*GetBlockList, error) {
112- uri := interpolateURL(
113- "http://___.blob.core.windows.net/___/___?comp=blocklist&blocklisttype=all",
114- context.Account, container, filename)
115+ uri := context.getFileURL(container, filename)
116+ uri = addURLQueryParams(
117+ uri,
118+ "comp", "blocklist",
119+ "blocklisttype", "all")
120+
121 req, err := http.NewRequest("GET", uri, nil)
122 if err != nil {
123 return nil, err
124@@ -466,9 +460,11 @@
125 // data block to upload.
126 func (context *StorageContext) PutBlock(container, filename, id string, data io.Reader) error {
127 base64_id := base64.StdEncoding.EncodeToString([]byte(id))
128- uri := interpolateURL(
129- "http://___.blob.core.windows.net/___/___?comp=block&blockid=___",
130- context.Account, container, filename, base64_id)
131+ uri := context.getFileURL(container, filename)
132+ uri = addURLQueryParams(
133+ uri,
134+ "comp", "block",
135+ "blockid", base64_id)
136 req, err := http.NewRequest("PUT", uri, data)
137 if err != nil {
138 return err
139@@ -486,9 +482,8 @@
140
141 // Send a request to piece together blocks into a list that specifies a blob.
142 func (context *StorageContext) PutBlockList(container, filename string, blocklist *BlockList) error {
143- uri := interpolateURL(
144- "http://___.blob.core.windows.net/___/___?comp=blocklist",
145- context.Account, container, filename)
146+ uri := context.getFileURL(container, filename)
147+ uri = addURLQueryParams(uri, "comp", "blocklist")
148 data, err := blocklist.Serialize()
149 if err != nil {
150 return err
151@@ -511,9 +506,7 @@
152
153 // Delete the specified blob from the given container.
154 func (context *StorageContext) DeleteBlob(container, filename string) error {
155- uri := interpolateURL(
156- "http://___.blob.core.windows.net/___/___",
157- context.Account, container, filename)
158+ uri := context.getFileURL(container, filename)
159 req, err := http.NewRequest("DELETE", uri, nil)
160 if err != nil {
161 return err
162@@ -532,9 +525,7 @@
163
164 // Get the specified blob from the given container.
165 func (context *StorageContext) GetBlob(container, filename string) (io.ReadCloser, error) {
166- uri := interpolateURL(
167- "http://___.blob.core.windows.net/___/___",
168- context.Account, container, filename)
169+ uri := context.getFileURL(container, filename)
170 req, err := http.NewRequest("GET", uri, nil)
171 if err != nil {
172 return nil, err
173
174=== modified file 'storage_base_test.go'
175--- storage_base_test.go 2013-04-29 08:06:24 +0000
176+++ storage_base_test.go 2013-04-30 09:36:29 +0000
177@@ -246,6 +246,42 @@
178
179 var _ = Suite(&TestStorageContext{})
180
181+// makeNastyURLUnfriendlyString returns a string that really needs escaping
182+// before it can be included in a URL.
183+func makeNastyURLUnfriendlyString() string {
184+ return MakeRandomString(3) + "?&" + MakeRandomString(3) + "$%"
185+}
186+
187+func (suite *TestStorageContext) TestGetAccountURL(c *C) {
188+ account := makeNastyURLUnfriendlyString()
189+ context := StorageContext{Account: account}
190+ c.Check(
191+ context.getAccountURL(),
192+ Equals,
193+ "http://"+url.QueryEscape(account)+".blob.core.windows.net/")
194+}
195+
196+func (suite *TestStorageContext) TestGetContainerURL(c *C) {
197+ account := makeNastyURLUnfriendlyString()
198+ container := makeNastyURLUnfriendlyString()
199+ context := StorageContext{Account: account}
200+ c.Check(
201+ context.getContainerURL(container),
202+ Equals,
203+ "http://"+url.QueryEscape(account)+".blob.core.windows.net/"+url.QueryEscape(container))
204+}
205+
206+func (suite *TestStorageContext) TestGetFileURL(c *C) {
207+ account := makeNastyURLUnfriendlyString()
208+ container := makeNastyURLUnfriendlyString()
209+ file := makeNastyURLUnfriendlyString()
210+ context := StorageContext{Account: account}
211+ c.Check(
212+ context.getFileURL(container, file),
213+ Equals,
214+ "http://"+url.QueryEscape(account)+".blob.core.windows.net/"+url.QueryEscape(container)+"/"+url.QueryEscape(file))
215+}
216+
217 func (suite *TestStorageContext) TestGetClientReturnsDefaultClient(c *C) {
218 context := &StorageContext{client: nil}
219 c.Assert(context.getClient(), Equals, http.DefaultClient)
220@@ -266,48 +302,6 @@
221 }
222 }
223
224-type TestAddURLQueryParam struct{}
225-
226-var _ = Suite(&TestAddURLQueryParam{})
227-
228-func (*TestAddURLQueryParam) TestAddURLQueryParamUsesBaseURL(c *C) {
229- baseURL := "http://example.com"
230-
231- extendedURL, err := addURLQueryParam(baseURL, "key", "value")
232- c.Assert(err, IsNil)
233-
234- parsedURL, err := url.Parse(extendedURL)
235- c.Assert(err, IsNil)
236- c.Check(parsedURL.Scheme, Equals, "http")
237- c.Check(parsedURL.Host, Equals, "example.com")
238-}
239-
240-func (suite *TestAddURLQueryParam) TestEscapesParam(c *C) {
241- key := "key&key"
242- value := "value%value"
243-
244- uri, err := addURLQueryParam("http://example.com", key, value)
245- c.Assert(err, IsNil)
246-
247- parsedURL, err := url.Parse(uri)
248- c.Assert(err, IsNil)
249- c.Check(parsedURL.Query()[key], DeepEquals, []string{value})
250-}
251-
252-// The parameter may safely contain the placeholder that we happen to use
253-// for URL interpolation.
254-func (suite *TestAddURLQueryParam) TestDoesNotInterpolateParam(c *C) {
255- key := "key" + interpolationPlaceholder + "key"
256- value := "value" + interpolationPlaceholder + "value"
257-
258- uri, err := addURLQueryParam("http://example.com", key, value)
259- c.Assert(err, IsNil)
260-
261- parsedURL, err := url.Parse(uri)
262- c.Assert(err, IsNil)
263- c.Check(parsedURL.Query()[key], DeepEquals, []string{value})
264-}
265-
266 type TestListContainers struct{}
267
268 var _ = Suite(&TestListContainers{})
269@@ -479,27 +473,6 @@
270 c.Check(values["marker"], DeepEquals, []string{"x&y"})
271 }
272
273-// The marker string may safely contain the placeholder that we happen to use
274-// for URL interpolation.
275-func (suite *TestListContainers) TestGetListContainersBatchDoesNotInterpolateMarker(c *C) {
276- transport := TestTransport2{}
277- transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil)
278- context := makeStorageContext()
279- context.client = &http.Client{Transport: &transport}
280- marker := "x" + interpolationPlaceholder + "y"
281-
282- // An error about the http response is fine. What matters is that (1) we
283- // don't fail while putting together the URL, and (2) we get the right URL.
284- _, err := context.getListContainersBatch(marker)
285- c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
286- c.Assert(transport.ExchangeCount, Equals, 1)
287-
288- query := transport.Exchanges[0].Request.URL.RawQuery
289- values, err := url.ParseQuery(query)
290- c.Assert(err, IsNil)
291- c.Check(values["marker"], DeepEquals, []string{marker})
292-}
293-
294 type TestListBlobs struct{}
295
296 var _ = Suite(&TestListBlobs{})
297@@ -561,9 +534,11 @@
298 context.client = &http.Client{Transport: transport}
299 results, err := context.ListBlobs("container")
300 c.Assert(err, IsNil)
301- c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf(
302- "http://%s.blob.core.windows.net/container?restype=container&comp=list",
303- context.Account))
304+ c.Check(transport.Request.URL.String(), Matches, context.getContainerURL("container")+"?.*")
305+ c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{
306+ "restype": {"container"},
307+ "comp": {"list"},
308+ })
309 c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "")
310 c.Assert(results, NotNil)
311 }
312@@ -746,9 +721,7 @@
313 context.client = &http.Client{Transport: transport}
314 err := context.PutBlob("container", "blobname")
315 c.Assert(err, IsNil)
316- c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf(
317- "http://%s.blob.core.windows.net/container/blobname",
318- context.Account))
319+ c.Check(transport.Request.URL.String(), Equals, context.getFileURL("container", "blobname"))
320 c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "")
321 }
322
323@@ -799,10 +772,11 @@
324
325 // The blockid should have been base64 encoded and url escaped.
326 base64_id := base64.StdEncoding.EncodeToString([]byte(blockid))
327- escaped_id := url.QueryEscape(base64_id)
328- c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf(
329- "http://%s.blob.core.windows.net/container/blobname?comp=block&blockid=%s",
330- context.Account, escaped_id))
331+ c.Check(transport.Request.URL.String(), Matches, context.getFileURL("container", "blobname")+"?.*")
332+ c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{
333+ "comp": {"block"},
334+ "blockid": {base64_id},
335+ })
336 c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "")
337
338 data, err := ioutil.ReadAll(transport.Request.Body)
339@@ -936,9 +910,11 @@
340 context.client = &http.Client{Transport: transport}
341 results, err := context.GetBlockList("container", "myfilename")
342 c.Assert(err, IsNil)
343- c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf(
344- "http://%s.blob.core.windows.net/container/myfilename?comp=blocklist&blocklisttype=all",
345- context.Account))
346+ c.Check(transport.Request.URL.String(), Matches, context.getFileURL("container", "myfilename")+"?.*")
347+ c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{
348+ "comp": {"blocklist"},
349+ "blocklisttype": {"all"},
350+ })
351 c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "")
352 c.Assert(results, NotNil)
353 }
354@@ -982,9 +958,7 @@
355 c.Assert(err, IsNil)
356
357 c.Check(transport.Request.Method, Equals, "DELETE")
358- c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf(
359- "http://%s.blob.core.windows.net/container/blobname",
360- context.Account))
361+ c.Check(transport.Request.URL.String(), Equals, context.getFileURL("container", "blobname"))
362 c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "")
363 c.Check(transport.Request.Body, IsNil)
364 }
365@@ -1036,9 +1010,7 @@
366 defer reader.Close()
367
368 c.Check(transport.Request.Method, Equals, "GET")
369- c.Check(transport.Request.URL.String(), Equals, fmt.Sprintf(
370- "http://%s.blob.core.windows.net/container/blobname",
371- context.Account))
372+ c.Check(transport.Request.URL.String(), Equals, context.getFileURL("container", "blobname"))
373 c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "")
374
375 data, err := ioutil.ReadAll(reader)
376
377=== modified file 'storage_test.go'
378--- storage_test.go 2013-04-24 10:08:54 +0000
379+++ storage_test.go 2013-04-30 09:36:29 +0000
380@@ -67,10 +67,11 @@
381
382 func assertBlockSent(
383 c *C, context *StorageContext, data []byte, blockID string, exchange *TestTransport2Exchange) {
384- c.Check(exchange.Request.URL.String(), Equals, fmt.Sprintf(
385- "http://%s.blob.core.windows.net/MyContainer/MyFile"+
386- "?comp=block&blockid=%s",
387- context.Account, url.QueryEscape(blockID)))
388+ c.Check(exchange.Request.URL.String(), Matches, context.getFileURL("MyContainer", "MyFile")+"?.*")
389+ c.Check(exchange.Request.URL.Query(), DeepEquals, url.Values{
390+ "comp": {"block"},
391+ "blockid": {blockID},
392+ })
393 body, err := ioutil.ReadAll(exchange.Request.Body)
394 c.Check(err, IsNil)
395 c.Check(body, DeepEquals, data)
396
397=== modified file 'utils.go'
398--- utils.go 2013-04-26 03:53:37 +0000
399+++ utils.go 2013-04-30 09:36:29 +0000
400@@ -6,7 +6,6 @@
401 import (
402 "fmt"
403 "net/url"
404- "strings"
405 )
406
407 // checkPathComponents checks that none of the passed components contains any
408@@ -23,23 +22,25 @@
409 }
410 }
411
412-// We use ___ (three underscores) as a placeholder for variables that are to be
413-// interpolated into a URL.
414-const interpolationPlaceholder = "___"
415-
416-// interpolateURL replaces occurrences of the interpolation placeolder with the
417-// components specified. Additionally, the components are percent-encoded
418-// (using net.url.QueryEscape) before being interpolated.
419-func interpolateURL(urlstring string, components ...string) string {
420- components_count := len(components)
421- replacement_count := strings.Count(urlstring, interpolationPlaceholder)
422- if components_count != replacement_count {
423- panic(fmt.Errorf("%d slot(s) but given %d replacement(s)",
424- replacement_count, components_count))
425- }
426- for _, component := range components {
427- component = url.QueryEscape(component)
428- urlstring = strings.Replace(urlstring, interpolationPlaceholder, component, 1)
429- }
430- return urlstring
431+// addURLQueryParams adds query parameters to a URL (and escapes as needed).
432+// Parameters are URL, [key, value, [key, value, [...]]].
433+// The original URL must be correct, i.e. it should parse without error.
434+func addURLQueryParams(originalURL string, params ...string) string {
435+ if len(params)%2 != 0 {
436+ panic(fmt.Errorf("got %d parameter argument(s), instead of matched key/value pairs", len(params)))
437+ }
438+ parsedURL, err := url.Parse(originalURL)
439+ if err != nil {
440+ panic(err)
441+ }
442+ query := parsedURL.Query()
443+
444+ for index := 0; index < len(params); index += 2 {
445+ key := params[index]
446+ value := params[index+1]
447+ query.Add(key, value)
448+ }
449+
450+ parsedURL.RawQuery = query.Encode()
451+ return parsedURL.String()
452 }
453
454=== modified file 'utils_test.go'
455--- utils_test.go 2013-04-12 15:03:26 +0000
456+++ utils_test.go 2013-04-30 09:36:29 +0000
457@@ -5,7 +5,7 @@
458
459 import (
460 . "launchpad.net/gocheck"
461- "regexp"
462+ "net/url"
463 )
464
465 type UtilsSuite struct{}
466@@ -25,29 +25,63 @@
467 PanicMatches, "'[.][.]' is not allowed")
468 }
469
470-func (suite *UtilsSuite) TestInterpolateURL(c *C) {
471- // Interpolation with nothing is fine.
472- c.Check(
473- interpolateURL("http://example.com"),
474- Equals, "http://example.com")
475- // Interpolation with left-over arguments causes a panic.
476- c.Check(
477- func() { interpolateURL("http://example.com", "fred") }, PanicMatches,
478- regexp.QuoteMeta("0 slot(s) but given 1 replacement(s)"))
479- // Three underscores denotes where replacement should happen.
480- c.Check(
481- interpolateURL("http://___.example.com", "fred"),
482- Equals, "http://fred.example.com")
483- // Multiple replacements are fine.
484- c.Check(
485- interpolateURL("http://___.example.com/___", "fred", "bob"),
486- Equals, "http://fred.example.com/bob")
487- // If there are too few replacements it panics.
488- c.Check(
489- func() { interpolateURL("http://example.com/___") }, PanicMatches,
490- regexp.QuoteMeta("1 slot(s) but given 0 replacement(s)"))
491- // Replacements are percent-encoded.
492- c.Check(
493- interpolateURL("http://example.com/___", "foo^bar/baz"),
494- Equals, "http://example.com/foo%5Ebar%2Fbaz")
495+type TestAddURLQueryParams struct{}
496+
497+var _ = Suite(&TestAddURLQueryParams{})
498+
499+func (*TestAddURLQueryParams) TestUsesBaseURL(c *C) {
500+ baseURL := "http://example.com"
501+
502+ extendedURL := addURLQueryParams(baseURL, "key", "value")
503+
504+ parsedURL, err := url.Parse(extendedURL)
505+ c.Assert(err, IsNil)
506+ c.Check(parsedURL.Scheme, Equals, "http")
507+ c.Check(parsedURL.Host, Equals, "example.com")
508+}
509+
510+func (suite *TestAddURLQueryParams) TestEscapesParams(c *C) {
511+ key := "key&key"
512+ value := "value%value"
513+
514+ uri := addURLQueryParams("http://example.com", key, value)
515+
516+ parsedURL, err := url.Parse(uri)
517+ c.Assert(err, IsNil)
518+ c.Check(parsedURL.Query()[key], DeepEquals, []string{value})
519+}
520+
521+func (suite *TestAddURLQueryParams) TestAddsToExistingParams(c *C) {
522+ uri := addURLQueryParams("http://example.com?a=one", "b", "two")
523+
524+ parsedURL, err := url.Parse(uri)
525+ c.Assert(err, IsNil)
526+ c.Check(parsedURL.Query(), DeepEquals, url.Values{
527+ "a": {"one"},
528+ "b": {"two"},
529+ })
530+}
531+
532+func (suite *TestAddURLQueryParams) TestAppendsRepeatedParams(c *C) {
533+ uri := addURLQueryParams("http://example.com?foo=bar", "foo", "bar")
534+ c.Check(uri, Equals, "http://example.com?foo=bar&foo=bar")
535+}
536+
537+func (suite *TestAddURLQueryParams) TestAddsMultipleParams(c *C) {
538+ uri := addURLQueryParams("http://example.com", "one", "un", "two", "deux")
539+ parsedURL, err := url.Parse(uri)
540+ c.Assert(err, IsNil)
541+ c.Check(parsedURL.Query(), DeepEquals, url.Values{
542+ "one": {"un"},
543+ "two": {"deux"},
544+ })
545+}
546+
547+func (suite *TestAddURLQueryParams) TestRejectsOddNumberOfParams(c *C) {
548+ defer func() {
549+ err := recover()
550+ c.Check(err, ErrorMatches, ".*got 1 parameter.*")
551+ }()
552+ addURLQueryParams("http://example.com", "key")
553+ c.Assert("This should have panicked", Equals, "But it didn't.")
554 }
555
556=== modified file 'x509session.go'
557--- x509session.go 2013-04-12 09:26:52 +0000
558+++ x509session.go 2013-04-30 09:36:29 +0000
559@@ -34,14 +34,12 @@
560 if err != nil {
561 panic(err)
562 }
563- path = fmt.Sprintf("___/%s", path)
564- path_url, err := url.Parse(path)
565+ escapedID := url.QueryEscape(session.subscriptionId)
566+ path_url, err := url.Parse(escapedID + "/" + path)
567 if err != nil {
568 panic(err)
569 }
570- return interpolateURL(
571- azure_url.ResolveReference(path_url).String(),
572- session.subscriptionId)
573+ return azure_url.ResolveReference(path_url).String()
574 }
575
576 // _X509Dispatcher is the function used to dispatch requests. We call the

Subscribers

People subscribed via source and target branches