| OLD | NEW |
| (Empty) |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 package cipd | |
| 6 | |
| 7 import ( | |
| 8 "bytes" | |
| 9 "fmt" | |
| 10 "io/ioutil" | |
| 11 "net/http" | |
| 12 "net/url" | |
| 13 "testing" | |
| 14 "time" | |
| 15 | |
| 16 "infra/libs/logging" | |
| 17 | |
| 18 . "github.com/smartystreets/goconvey/convey" | |
| 19 ) | |
| 20 | |
| 21 func TestUploadToCAS(t *testing.T) { | |
| 22 Convey("Given a mocked service and clock", t, func() { | |
| 23 mockClock(time.Now()) | |
| 24 mockResumableUpload() | |
| 25 | |
| 26 Convey("UploadToCAS full flow", func() { | |
| 27 mockRemoteServiceWithExpectations([]expectedHTTPCall{ | |
| 28 { | |
| 29 Method: "POST", | |
| 30 Path: "/_ah/api/cas/v1/upload/SHA1/abc
", | |
| 31 Reply: `{"status":"SUCCESS","upload_ses
sion_id":"12345","upload_url":"http://localhost"}`, | |
| 32 }, | |
| 33 { | |
| 34 Method: "POST", | |
| 35 Path: "/_ah/api/cas/v1/finalize/12345"
, | |
| 36 Reply: `{"status":"VERIFYING"}`, | |
| 37 }, | |
| 38 { | |
| 39 Method: "POST", | |
| 40 Path: "/_ah/api/cas/v1/finalize/12345"
, | |
| 41 Reply: `{"status":"PUBLISHED"}`, | |
| 42 }, | |
| 43 }) | |
| 44 err := UploadToCAS(UploadToCASOptions{SHA1: "abc"}) | |
| 45 So(err, ShouldBeNil) | |
| 46 }) | |
| 47 | |
| 48 Convey("UploadToCAS timeout", func() { | |
| 49 // Append a bunch of "still verifying" responses at the
end. | |
| 50 calls := []expectedHTTPCall{ | |
| 51 { | |
| 52 Method: "POST", | |
| 53 Path: "/_ah/api/cas/v1/upload/SHA1/abc
", | |
| 54 Reply: `{"status":"SUCCESS","upload_ses
sion_id":"12345","upload_url":"http://localhost"}`, | |
| 55 }, | |
| 56 } | |
| 57 for i := 0; i < 19; i++ { | |
| 58 calls = append(calls, expectedHTTPCall{ | |
| 59 Method: "POST", | |
| 60 Path: "/_ah/api/cas/v1/finalize/12345"
, | |
| 61 Reply: `{"status":"VERIFYING"}`, | |
| 62 }) | |
| 63 } | |
| 64 mockRemoteServiceWithExpectations(calls) | |
| 65 err := UploadToCAS(UploadToCASOptions{SHA1: "abc"}) | |
| 66 So(err, ShouldEqual, ErrFinalizationTimeout) | |
| 67 }) | |
| 68 }) | |
| 69 } | |
| 70 | |
| 71 func TestRegisterInstance(t *testing.T) { | |
| 72 Convey("Mocking remote service", t, func() { | |
| 73 mockClock(time.Now()) | |
| 74 mockResumableUpload() | |
| 75 | |
| 76 // Build an empty package to be uploaded. | |
| 77 out := bytes.Buffer{} | |
| 78 err := BuildInstance(BuildInstanceOptions{ | |
| 79 Input: []File{}, | |
| 80 Output: &out, | |
| 81 PackageName: "testing", | |
| 82 }) | |
| 83 So(err, ShouldBeNil) | |
| 84 | |
| 85 // Open it for reading. | |
| 86 inst, err := OpenInstance(bytes.NewReader(out.Bytes()), "") | |
| 87 So(err, ShouldBeNil) | |
| 88 Reset(func() { inst.Close() }) | |
| 89 | |
| 90 Convey("RegisterInstance full flow", func() { | |
| 91 mockRemoteServiceWithExpectations([]expectedHTTPCall{ | |
| 92 { | |
| 93 Method: "POST", | |
| 94 Path: "/_ah/api/repo/v1/instance", | |
| 95 Query: url.Values{ | |
| 96 "instance_id": []string{inst.In
stanceID()}, | |
| 97 "package_name": []string{inst.Pa
ckageName()}, | |
| 98 }, | |
| 99 Reply: `{ | |
| 100 "status": "UPLOAD_FIRST", | |
| 101 "upload_session_id": "12345", | |
| 102 "upload_url": "http://localhost" | |
| 103 }`, | |
| 104 }, | |
| 105 { | |
| 106 Method: "POST", | |
| 107 Path: "/_ah/api/cas/v1/finalize/12345"
, | |
| 108 Reply: `{"status":"PUBLISHED"}`, | |
| 109 }, | |
| 110 { | |
| 111 Method: "POST", | |
| 112 Path: "/_ah/api/repo/v1/instance", | |
| 113 Query: url.Values{ | |
| 114 "instance_id": []string{inst.In
stanceID()}, | |
| 115 "package_name": []string{inst.Pa
ckageName()}, | |
| 116 }, | |
| 117 Reply: `{ | |
| 118 "status": "REGISTERED", | |
| 119 "instance": { | |
| 120 "registered_by": "user:a
@example.com", | |
| 121 "registered_ts": "0" | |
| 122 } | |
| 123 }`, | |
| 124 }, | |
| 125 }) | |
| 126 err = RegisterInstance(RegisterInstanceOptions{PackageIn
stance: inst}) | |
| 127 So(err, ShouldBeNil) | |
| 128 }) | |
| 129 | |
| 130 Convey("RegisterInstance already registered", func() { | |
| 131 mockRemoteServiceWithExpectations([]expectedHTTPCall{ | |
| 132 { | |
| 133 Method: "POST", | |
| 134 Path: "/_ah/api/repo/v1/instance", | |
| 135 Query: url.Values{ | |
| 136 "instance_id": []string{inst.In
stanceID()}, | |
| 137 "package_name": []string{inst.Pa
ckageName()}, | |
| 138 }, | |
| 139 Reply: `{ | |
| 140 "status": "ALREADY_REGISTERED", | |
| 141 "instance": { | |
| 142 "registered_by": "user:a
@example.com", | |
| 143 "registered_ts": "0" | |
| 144 } | |
| 145 }`, | |
| 146 }, | |
| 147 }) | |
| 148 err = RegisterInstance(RegisterInstanceOptions{PackageIn
stance: inst}) | |
| 149 So(err, ShouldBeNil) | |
| 150 }) | |
| 151 }) | |
| 152 } | |
| 153 | |
| 154 func TestResumableUpload(t *testing.T) { | |
| 155 Convey("Resumable upload full flow", t, func(c C) { | |
| 156 mockClock(time.Now()) | |
| 157 | |
| 158 dataToUpload := "0123456789abcdef" | |
| 159 totalLen := len(dataToUpload) | |
| 160 uploaded := bytes.NewBuffer(nil) | |
| 161 errors := 0 | |
| 162 | |
| 163 server, client := mockServerWithHandler("/", func(w http.Respons
eWriter, r *http.Request) { | |
| 164 c.So(r.URL.Path, ShouldEqual, "/upl") | |
| 165 c.So(r.Method, ShouldEqual, "PUT") | |
| 166 | |
| 167 rangeHeader := r.Header.Get("Content-Range") | |
| 168 body, err := ioutil.ReadAll(r.Body) | |
| 169 c.So(err, ShouldBeNil) | |
| 170 | |
| 171 // Insert a bunch of consecutive transient errors in the
middle. | |
| 172 cur := uploaded.Len() | |
| 173 if cur > totalLen/2 && errors < 3 { | |
| 174 errors++ | |
| 175 w.WriteHeader(500) | |
| 176 return | |
| 177 } | |
| 178 | |
| 179 // Request for uploaded offset. | |
| 180 if len(body) == 0 { | |
| 181 c.So(rangeHeader, ShouldEqual, fmt.Sprintf("byte
s */%d", totalLen)) | |
| 182 if cur == totalLen { | |
| 183 w.WriteHeader(200) | |
| 184 return | |
| 185 } | |
| 186 if cur != 0 { | |
| 187 w.Header().Set("Range", fmt.Sprintf("byt
es=0-%d", cur-1)) | |
| 188 } | |
| 189 w.WriteHeader(308) | |
| 190 return | |
| 191 } | |
| 192 | |
| 193 // Uploading next chunk. | |
| 194 c.So(rangeHeader, ShouldEqual, fmt.Sprintf("bytes %d-%d/
%d", cur, cur+len(body)-1, totalLen)) | |
| 195 _, err = uploaded.Write(body) | |
| 196 c.So(err, ShouldBeNil) | |
| 197 if uploaded.Len() == totalLen { | |
| 198 w.WriteHeader(200) | |
| 199 } else { | |
| 200 w.WriteHeader(308) | |
| 201 } | |
| 202 }) | |
| 203 | |
| 204 err := resumableUpload(server.URL+"/upl", 3, UploadToCASOptions{ | |
| 205 SHA1: "abc", | |
| 206 Data: bytes.NewReader([]byte(dataToUpload)), | |
| 207 UploadOptions: UploadOptions{ | |
| 208 Client: client, | |
| 209 Log: logging.DefaultLogger, | |
| 210 }, | |
| 211 }) | |
| 212 So(err, ShouldBeNil) | |
| 213 So(uploaded.Bytes(), ShouldResemble, []byte(dataToUpload)) | |
| 214 }) | |
| 215 } | |
| 216 | |
| 217 func TestAttachTagsWhenReady(t *testing.T) { | |
| 218 Convey("Mocking clock", t, func() { | |
| 219 mockClock(time.Now()) | |
| 220 | |
| 221 Convey("attachTagsWhenReady works", func() { | |
| 222 remote := mockRemoteServiceWithExpectations([]expectedHT
TPCall{ | |
| 223 { | |
| 224 Method: "POST", | |
| 225 Path: "/_ah/api/repo/v1/tags", | |
| 226 Query: url.Values{ | |
| 227 "instance_id": []string{"aaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, | |
| 228 "package_name": []string{"pkgnam
e"}, | |
| 229 }, | |
| 230 Reply: `{"status": "PROCESSING_NOT_FINIS
HED_YET"}`, | |
| 231 }, | |
| 232 { | |
| 233 Method: "POST", | |
| 234 Path: "/_ah/api/repo/v1/tags", | |
| 235 Query: url.Values{ | |
| 236 "instance_id": []string{"aaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, | |
| 237 "package_name": []string{"pkgnam
e"}, | |
| 238 }, | |
| 239 Reply: `{"status": "SUCCESS"}`, | |
| 240 }, | |
| 241 }) | |
| 242 err := attachTagsWhenReady( | |
| 243 remote, "pkgname", "aaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaa", | |
| 244 []string{"tag1:value1"}, logging.DefaultLogger) | |
| 245 So(err, ShouldBeNil) | |
| 246 }) | |
| 247 | |
| 248 Convey("attachTagsWhenReady timeout", func() { | |
| 249 calls := []expectedHTTPCall{} | |
| 250 for i := 0; i < 20; i++ { | |
| 251 calls = append(calls, expectedHTTPCall{ | |
| 252 Method: "POST", | |
| 253 Path: "/_ah/api/repo/v1/tags", | |
| 254 Query: url.Values{ | |
| 255 "instance_id": []string{"aaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, | |
| 256 "package_name": []string{"pkgnam
e"}, | |
| 257 }, | |
| 258 Reply: `{"status": "PROCESSING_NOT_FINIS
HED_YET"}`, | |
| 259 }) | |
| 260 } | |
| 261 remote := mockRemoteServiceWithExpectations(calls) | |
| 262 err := attachTagsWhenReady( | |
| 263 remote, "pkgname", "aaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaa", | |
| 264 []string{"tag1:value1"}, logging.DefaultLogger) | |
| 265 So(err, ShouldEqual, ErrAttachTagsTimeout) | |
| 266 }) | |
| 267 }) | |
| 268 } | |
| 269 | |
| 270 func mockResumableUpload() { | |
| 271 prev := resumableUpload | |
| 272 resumableUpload = func(string, int64, UploadToCASOptions) error { | |
| 273 return nil | |
| 274 } | |
| 275 Reset(func() { resumableUpload = prev }) | |
| 276 } | |
| OLD | NEW |