Merge lp:~jtv/gomaasapi/test-upload-file into lp:gomaasapi

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 32
Merged at revision: 33
Proposed branch: lp:~jtv/gomaasapi/test-upload-file
Merge into: lp:gomaasapi
Diff against target: 41 lines (+20/-3)
2 files modified
testservice.go (+1/-3)
testservice_test.go (+19/-0)
To merge this branch: bzr merge lp:~jtv/gomaasapi/test-upload-file
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+151433@code.launchpad.net

Commit message

Test the test service's handling of file uploads, and fix handling of the "filename" parameter.

Description of the change

This was a silly one, but it took me a few days to figure out because I was hitting the problem in another project, when really the gomaasapi test suite should have caught it first.

The handler in the test service that receives a file upload was not receiving the required "filename" parameter. It always used the empty string instead. We did not notice this because originally the handler did not check for the case that the parameter might be missing (and obviously the test suite did not cover this case), and file uploads were not covered by the test suite.

Once I had a test, it was easy to see what went wrong: instead of looking in the http request's Form field (which holds both POST-style and GET-style parameters), the code re-parsed the query part of the raw, unparsed version of the request's URL and looked for a GET-style parameter there. The requesting code actually submits it as a POST parameter. It might have been nice for debugging to pass this particular parameter as part of the URL, but we don't currently have a way to tell the client which parameters to pass as GET and which to pass as POST (and we probably don't want all parameters as GET). There is a test for parameter-passing, but the parameter-passing code that ran here was hidden from the tests by what I call the "Swiss Army Knife" antipattern.

There are a few lessons we can learn from this:

 * Go does not have exceptions. Check your error conditions, or debugging turns into a nightmare.

 * Code changes. You may know that something doesn't fail now, but you still need to check in case somebody breaks it in the future.

 * Distrust third-party input. Check for obvious mistakes.

 * Avoid unnecessary cyclomatic complexity. It hides problems from both tests and humans.

 * Particularly avoid "if" clauses that are there just to reconstruct the intent of the calling code, if the intent was already known and constant at the call site. This is the Swiss Army Knife function: it's both a weak knife and a wobbly screwdriver. Better to accept a bit more code at the call site, and look for other ways to avoid repetition.

 * Unit-testing and integration-testing are both necessary. Integration-testing should cover a basic happy path and error handling, unit-testing can go through the nooks and crannies. But that doesn't work well with the Swiss Army Knife, because it needs to integrate multiple code paths at the call level with multiple code paths at the callee level.

 * If it's not tested, it's broken. Wherever possible, test before implementing so that you can see the test go from failure to success.

 * Fixing bugs as you come across them is disruptive and makes for unpredictable costs. Slower development earlier on could have saved us time in the current stage.

Jeroen

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

Looks good.

[1]

+ filename := r.Form.Get("filename")
  if filename == "" {
   panic("upload has no filename")

Can filename ever be nil? If so, better check for that condition too.

[2]

> Once I had a test, it was easy to see what went wrong: instead of
> looking in the http request's Form field (which holds both
> POST-style and GET-style parameters), the code re-parsed the query

It isn't about the code, but I think it's worthwhile deprecated these
descriptions of parameters.

GET-style parameters are just query string parameters, and can used
with any HTTP method.

POST-style parameters are typically those that are encoded into the
request body in the same way as query string parameters are encoded,
but can be multipart/form-data, in which case the values of these
parameters are actually structured (e.g. they have a content-type).

Many web frameworks call them GET and POST parameters, erroneously I
feel.

Much like Django, hence why it's hard to take seriously. However,
Django then steps up its game another notch, and steals the whole
packet of biscuits by copying PHP without blushing:

> HttpRequest.REQUEST
>
> For convenience, a dictionary-like object that searches POST first,
> then GET. Inspired by PHP’s $_REQUEST.

From https://docs.djangoproject.com/en/dev/ref/request-response/

(Sorry, it's Django and I couldn't resist the chance to twist the
knife.)

I know that you know all this... If you've read this far, what I'm
trying to say is that *we* should stop referring to them as GET and
POST parameters.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> [1]
>
> + filename := r.Form.Get("filename")
> if filename == "" {
> panic("upload has no filename")
>
> Can filename ever be nil? If so, better check for that condition too.

No, but I like your thinking! If Python is a bit like SQL in that any type of variable can be None, Go is more like Oracle (or Django) in that strings can't be.

> [2]
>
> > Once I had a test, it was easy to see what went wrong: instead of
> > looking in the http request's Form field (which holds both
> > POST-style and GET-style parameters), the code re-parsed the query
>
> It isn't about the code, but I think it's worthwhile deprecated these
> descriptions of parameters.

Thanks for explaining this. I've long wondered. This is also why I say "GET-style parameters" but not "GET parameters." Nor do I call them "PUT parameters" just because I happen to be using a different http method. :-)

> I know that you know all this... If you've read this far, what I'm
> trying to say is that *we* should stop referring to them as GET and
> POST parameters.

I'm all for it! What would be the right terms?

Jeroen

Revision history for this message
Gavin Panella (allenap) wrote :

>> I know that you know all this... If you've read this far, what I'm
>> trying to say is that *we* should stop referring to them as GET and
>> POST parameters.
>
> I'm all for it! What would be the right terms?

Ah, good question :)

"Query string parameters" is the least ambiguous thing I can think of
for GET(-style) parameters. The term POST-style isn't very misleading,
though it does hide the different body types that are conventionally
accepted as parameters, and also makes us blind to using other content
types (which is especially pertinent when trying to do ReST). "Request
body parameters" is more accurate I think. Though, again, it is more
of a mouthful.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'testservice.go'
2--- testservice.go 2013-03-04 02:50:16 +0000
3+++ testservice.go 2013-03-04 09:02:22 +0000
4@@ -420,9 +420,7 @@
5 err := r.ParseMultipartForm(10000000)
6 checkError(err)
7
8- values, err := url.ParseQuery(r.URL.RawQuery)
9- checkError(err)
10- filename := values.Get("filename")
11+ filename := r.Form.Get("filename")
12 if filename == "" {
13 panic("upload has no filename")
14 }
15
16=== modified file 'testservice_test.go'
17--- testservice_test.go 2013-03-04 02:50:16 +0000
18+++ testservice_test.go 2013-03-04 09:02:22 +0000
19@@ -420,3 +420,22 @@
20 operations := nodeOperations["mysystemid"]
21 c.Check(operations, DeepEquals, []string{"start"})
22 }
23+
24+func (suite *TestMAASObjectSuite) TestUploadFile(c *C) {
25+ const filename = "myfile.txt"
26+ const fileContent = "uploaded contents"
27+ files := suite.TestMAASObject.GetSubObject("files")
28+ params := url.Values{"filename": {filename}}
29+ filesMap := map[string][]byte{"file": []byte(fileContent)}
30+
31+ // Upload a file.
32+ _, err := files.CallPostFiles("add", params, filesMap)
33+ c.Assert(err, IsNil)
34+
35+ // The file can now be downloaded.
36+ downloadedFile, err := files.CallGet("get", params)
37+ c.Assert(err, IsNil)
38+ bytes, err := downloadedFile.GetBytes()
39+ c.Assert(err, IsNil)
40+ c.Check(string(bytes), Equals, fileContent)
41+}

Subscribers

People subscribed via source and target branches

to all changes: