1package mcnutils 2 3import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "net/http/httptest" 10 "os" 11 "path/filepath" 12 "testing" 13 14 "github.com/docker/machine/libmachine/log" 15 "github.com/docker/machine/version" 16 "github.com/stretchr/testify/assert" 17) 18 19func TestGetReleaseURL(t *testing.T) { 20 testCases := []struct { 21 apiURL string 22 isoURL string 23 machineVersion string 24 response string 25 }{ 26 {"/repos/org/repo/releases/latest", "/org/repo/releases/download/v0.1/boot2docker.iso", "v0.7.0", `{"tag_name": "v0.1"}`}, 27 28 // Note the difference in this one: It's an RC version. 29 {"/repos/org/repo/releases", "/org/repo/releases/download/v0.2-rc1/boot2docker.iso", "v0.7.0-rc2", `[{"tag_name": "v0.2-rc1"}, {"tag_name": "v0.1"}]`}, 30 31 {"http://dummy.com/boot2docker.iso", "http://dummy.com/boot2docker.iso", "v0.7.0", `{"tag_name": "v0.1"}`}, 32 } 33 34 for _, tt := range testCases { 35 testServer := newTestServer(tt.response) 36 37 // TODO: Modifying this package level variable is not elegant, 38 // but it is effective. Ideally this should be exposed through 39 // an interface. 40 actualMachineVersion := version.Version 41 version.Version = tt.machineVersion 42 b := NewB2dUtils("/tmp/isos") 43 isoURL, err := b.getReleaseURL(testServer.URL + tt.apiURL) 44 45 assert.NoError(t, err) 46 assert.Equal(t, testServer.URL+tt.isoURL, isoURL) 47 version.Version = actualMachineVersion 48 49 testServer.Close() 50 } 51} 52 53func TestGetReleaseURLError(t *testing.T) { 54 // GitHub API error response in case of rate limit 55 ts := newTestServer(`{"message": "API rate limit exceeded for 127.0.0.1.", 56 "documentation_url": "https://developer.github.com/v3/#rate-limiting"}`) 57 defer ts.Close() 58 59 testCases := []struct { 60 apiURL string 61 }{ 62 {ts.URL + "/repos/org/repo/releases/latest"}, 63 {"http://127.0.0.1/repos/org/repo/releases/latest"}, // dummy API URL. cannot connect it. 64 } 65 66 for _, tt := range testCases { 67 b := NewB2dUtils("/tmp/isos") 68 _, err := b.getReleaseURL(tt.apiURL) 69 70 assert.Error(t, err) 71 } 72} 73 74func TestVersion(t *testing.T) { 75 testCases := []string{ 76 "v0.1.0", 77 "v0.2.0-rc1", 78 } 79 80 for _, vers := range testCases { 81 isopath, off, err := newDummyISO("", defaultISOFilename, vers) 82 83 assert.NoError(t, err) 84 85 b := &b2dISO{ 86 commonIsoPath: isopath, 87 volumeIDOffset: off, 88 volumeIDLength: defaultVolumeIDLength, 89 } 90 91 got, err := b.version() 92 93 assert.NoError(t, err) 94 assert.Equal(t, vers, string(got)) 95 removeFileIfExists(isopath) 96 } 97} 98 99func TestDownloadISO(t *testing.T) { 100 testData := "test-download" 101 ts := newTestServer(testData) 102 defer ts.Close() 103 104 filename := "test" 105 106 tmpDir, err := ioutil.TempDir("", "machine-test-") 107 108 assert.NoError(t, err) 109 110 b := NewB2dUtils("/tmp/artifacts") 111 err = b.DownloadISO(tmpDir, filename, ts.URL) 112 113 assert.NoError(t, err) 114 115 data, err := ioutil.ReadFile(filepath.Join(tmpDir, filename)) 116 117 assert.NoError(t, err) 118 assert.Equal(t, testData, string(data)) 119} 120 121func TestGetRequest(t *testing.T) { 122 testCases := []struct { 123 token string 124 want string 125 }{ 126 {"", ""}, 127 {"CATBUG", "token CATBUG"}, 128 } 129 130 for _, tt := range testCases { 131 GithubAPIToken = tt.token 132 133 req, err := getRequest("http://some.github.api") 134 135 assert.NoError(t, err) 136 assert.Equal(t, tt.want, req.Header.Get("Authorization")) 137 } 138} 139 140type MockReadCloser struct { 141 blockLengths []int 142 currentBlock int 143} 144 145func (r *MockReadCloser) Read(p []byte) (n int, err error) { 146 n = r.blockLengths[r.currentBlock] 147 r.currentBlock++ 148 return 149} 150 151func (r *MockReadCloser) Close() error { 152 return nil 153} 154 155func TestReaderWithProgress(t *testing.T) { 156 readCloser := MockReadCloser{blockLengths: []int{5, 45, 50}} 157 output := new(bytes.Buffer) 158 buffer := make([]byte, 100) 159 160 readerWithProgress := ReaderWithProgress{ 161 ReadCloser: &readCloser, 162 out: output, 163 expectedLength: 100, 164 } 165 166 readerWithProgress.Read(buffer) 167 assert.Equal(t, "0%..", output.String()) 168 169 readerWithProgress.Read(buffer) 170 assert.Equal(t, "0%....10%....20%....30%....40%....50%", output.String()) 171 172 readerWithProgress.Read(buffer) 173 assert.Equal(t, "0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%", output.String()) 174 175 readerWithProgress.Close() 176 assert.Equal(t, "0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%\n", output.String()) 177} 178 179type mockReleaseGetter struct { 180 ver string 181 apiErr error 182 verCh chan<- string 183} 184 185func (m *mockReleaseGetter) filename() string { 186 return defaultISOFilename 187} 188 189func (m *mockReleaseGetter) getReleaseTag(apiURL string) (string, error) { 190 return m.ver, m.apiErr 191} 192 193func (m *mockReleaseGetter) getReleaseURL(apiURL string) (string, error) { 194 return "http://127.0.0.1/dummy", m.apiErr 195} 196 197func (m *mockReleaseGetter) download(dir, file, isoURL string) error { 198 path := filepath.Join(dir, file) 199 var err error 200 if _, e := os.Stat(path); os.IsNotExist(e) { 201 err = ioutil.WriteFile(path, dummyISOData(" ", m.ver), 0644) 202 } 203 204 // send a signal of downloading the latest version 205 m.verCh <- m.ver 206 return err 207} 208 209type mockISO struct { 210 isopath string 211 exist bool 212 ver string 213 verCh <-chan string 214} 215 216func (m *mockISO) path() string { 217 return m.isopath 218} 219 220func (m *mockISO) exists() bool { 221 return m.exist 222} 223 224func (m *mockISO) version() (string, error) { 225 select { 226 // receive version of a downloaded iso 227 case ver := <-m.verCh: 228 return ver, nil 229 default: 230 return m.ver, nil 231 } 232} 233 234func TestCopyDefaultISOToMachine(t *testing.T) { 235 apiErr := errors.New("api error") 236 237 testCases := []struct { 238 machineName string 239 create bool 240 localVer string 241 latestVer string 242 apiErr error 243 wantVer string 244 }{ 245 {"none", false, "", "v1.0.0", nil, "v1.0.0"}, // none => downloading 246 {"latest", true, "v1.0.0", "v1.0.0", nil, "v1.0.0"}, // latest iso => as is 247 {"old-badurl", true, "v0.1.0", "", apiErr, "v0.1.0"}, // old iso with bad api => as is 248 {"old", true, "v0.1.0", "v1.0.0", nil, "v1.0.0"}, // old iso => updating 249 } 250 251 var isopath string 252 var err error 253 verCh := make(chan string, 1) 254 for _, tt := range testCases { 255 if tt.create { 256 isopath, _, err = newDummyISO("cache", defaultISOFilename, tt.localVer) 257 } else { 258 if dir, e := ioutil.TempDir("", "machine-test"); e == nil { 259 isopath = filepath.Join(dir, "cache", defaultISOFilename) 260 } 261 } 262 263 // isopath: "$TMPDIR/machine-test-xxxxxx/cache/boot2docker.iso" 264 // tmpDir: "$TMPDIR/machine-test-xxxxxx" 265 imgCachePath := filepath.Dir(isopath) 266 storePath := filepath.Dir(imgCachePath) 267 268 b := &B2dUtils{ 269 releaseGetter: &mockReleaseGetter{ 270 ver: tt.latestVer, 271 apiErr: tt.apiErr, 272 verCh: verCh, 273 }, 274 iso: &mockISO{ 275 isopath: isopath, 276 exist: tt.create, 277 ver: tt.localVer, 278 verCh: verCh, 279 }, 280 storePath: storePath, 281 imgCachePath: imgCachePath, 282 } 283 284 dir := filepath.Join(storePath, "machines", tt.machineName) 285 err = os.MkdirAll(dir, 0700) 286 assert.NoError(t, err, "machine: %s", tt.machineName) 287 288 err = b.CopyIsoToMachineDir("", tt.machineName) 289 assert.NoError(t, err) 290 291 dest := filepath.Join(dir, b.filename()) 292 _, pathErr := os.Stat(dest) 293 294 assert.NoError(t, err, "machine: %s", tt.machineName) 295 assert.True(t, !os.IsNotExist(pathErr), "machine: %s", tt.machineName) 296 297 ver, err := b.version() 298 299 assert.NoError(t, err, "machine: %s", tt.machineName) 300 assert.Equal(t, tt.wantVer, ver, "machine: %s", tt.machineName) 301 302 err = removeFileIfExists(isopath) 303 assert.NoError(t, err, "machine: %s", tt.machineName) 304 } 305} 306 307// newTestServer creates a new httptest.Server that returns respText as a response body. 308func newTestServer(respText string) *httptest.Server { 309 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 310 w.Write([]byte(respText)) 311 })) 312} 313 314// newDummyISO creates a dummy ISO file that contains the given version info, 315// and returns its path and offset value to fetch the version info. 316func newDummyISO(dir, name, version string) (string, int64, error) { 317 tmpDir, err := ioutil.TempDir("", "machine-test-") 318 if err != nil { 319 return "", 0, err 320 } 321 322 tmpDir = filepath.Join(tmpDir, dir) 323 if e := os.MkdirAll(tmpDir, 755); e != nil { 324 return "", 0, err 325 } 326 327 isopath := filepath.Join(tmpDir, name) 328 log.Info("TEST: dummy ISO created at ", isopath) 329 330 // dummy ISO data mimicking the real byte data of a Boot2Docker ISO image 331 padding := " " 332 data := dummyISOData(padding, version) 333 return isopath, int64(len(padding)), ioutil.WriteFile(isopath, data, 0644) 334} 335 336// dummyISOData returns mock data that contains given padding and version. 337func dummyISOData(padding, version string) []byte { 338 return []byte(fmt.Sprintf("%sBoot2Docker-%s ", padding, version)) 339} 340