package mcnutils import ( "bytes" "errors" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/docker/machine/libmachine/log" "github.com/stretchr/testify/assert" ) func TestGetReleaseURL(t *testing.T) { ts := newTestServer(`{"tag_name": "v0.1"}`) defer ts.Close() testCases := []struct { apiURL string isoURL string }{ {ts.URL + "/repos/org/repo/releases/latest", ts.URL + "/org/repo/releases/download/v0.1/boot2docker.iso"}, {"http://dummy.com/boot2docker.iso", "http://dummy.com/boot2docker.iso"}, } for _, tt := range testCases { b := NewB2dUtils("/tmp/isos") isoURL, err := b.getReleaseURL(tt.apiURL) assert.NoError(t, err) assert.Equal(t, isoURL, tt.isoURL) } } func TestGetReleaseURLError(t *testing.T) { // GitHub API error response in case of rate limit ts := newTestServer(`{"message": "API rate limit exceeded for 127.0.0.1.", "documentation_url": "https://developer.github.com/v3/#rate-limiting"}`) defer ts.Close() testCases := []struct { apiURL string }{ {ts.URL + "/repos/org/repo/releases/latest"}, {"http://127.0.0.1/repos/org/repo/releases/latest"}, // dummy API URL. cannot connect it. } for _, tt := range testCases { b := NewB2dUtils("/tmp/isos") _, err := b.getReleaseURL(tt.apiURL) assert.Error(t, err) } } func TestVersion(t *testing.T) { want := "v0.1.0" isopath, off, err := newDummyISO("", defaultISOFilename, want) defer removeFileIfExists(isopath) assert.NoError(t, err) b := &b2dISO{ commonIsoPath: isopath, volumeIDOffset: off, volumeIDLength: defaultVolumeIDLength, } got, err := b.version() assert.NoError(t, err) assert.Equal(t, want, string(got)) } func TestDownloadISO(t *testing.T) { testData := "test-download" ts := newTestServer(testData) defer ts.Close() filename := "test" tmpDir, err := ioutil.TempDir("", "machine-test-") assert.NoError(t, err) b := NewB2dUtils("/tmp/artifacts") err = b.DownloadISO(tmpDir, filename, ts.URL) assert.NoError(t, err) data, err := ioutil.ReadFile(filepath.Join(tmpDir, filename)) assert.NoError(t, err) assert.Equal(t, testData, string(data)) } func TestGetRequest(t *testing.T) { testCases := []struct { token string want string }{ {"", ""}, {"CATBUG", "token CATBUG"}, } for _, tt := range testCases { GithubAPIToken = tt.token req, err := getRequest("http://some.github.api") assert.NoError(t, err) assert.Equal(t, tt.want, req.Header.Get("Authorization")) } } type MockReadCloser struct { blockLengths []int currentBlock int } func (r *MockReadCloser) Read(p []byte) (n int, err error) { n = r.blockLengths[r.currentBlock] r.currentBlock++ return } func (r *MockReadCloser) Close() error { return nil } func TestReaderWithProgress(t *testing.T) { readCloser := MockReadCloser{blockLengths: []int{5, 45, 50}} output := new(bytes.Buffer) buffer := make([]byte, 100) readerWithProgress := ReaderWithProgress{ ReadCloser: &readCloser, out: output, expectedLength: 100, } readerWithProgress.Read(buffer) assert.Equal(t, "0%..", output.String()) readerWithProgress.Read(buffer) assert.Equal(t, "0%....10%....20%....30%....40%....50%", output.String()) readerWithProgress.Read(buffer) assert.Equal(t, "0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%", output.String()) readerWithProgress.Close() assert.Equal(t, "0%....10%....20%....30%....40%....50%....60%....70%....80%....90%....100%\n", output.String()) } type mockReleaseGetter struct { ver string apiErr error verCh chan<- string } func (m *mockReleaseGetter) filename() string { return defaultISOFilename } func (m *mockReleaseGetter) getReleaseTag(apiURL string) (string, error) { return m.ver, m.apiErr } func (m *mockReleaseGetter) getReleaseURL(apiURL string) (string, error) { return "http://127.0.0.1/dummy", m.apiErr } func (m *mockReleaseGetter) download(dir, file, isoURL string) error { path := filepath.Join(dir, file) var err error if _, e := os.Stat(path); os.IsNotExist(e) { err = ioutil.WriteFile(path, dummyISOData(" ", m.ver), 0644) } // send a signal of downloading the latest version m.verCh <- m.ver return err } type mockISO struct { isopath string exist bool ver string verCh <-chan string } func (m *mockISO) path() string { return m.isopath } func (m *mockISO) exists() bool { return m.exist } func (m *mockISO) version() (string, error) { select { // receive version of a downloaded iso case ver := <-m.verCh: return ver, nil default: return m.ver, nil } } func TestCopyDefaultISOToMachine(t *testing.T) { apiErr := errors.New("api error") testCases := []struct { machineName string create bool localVer string latestVer string apiErr error wantVer string }{ {"none", false, "", "v1.0.0", nil, "v1.0.0"}, // none => downloading {"latest", true, "v1.0.0", "v1.0.0", nil, "v1.0.0"}, // latest iso => as is {"old-badurl", true, "v0.1.0", "", apiErr, "v0.1.0"}, // old iso with bad api => as is {"old", true, "v0.1.0", "v1.0.0", nil, "v1.0.0"}, // old iso => updating } var isopath string var err error verCh := make(chan string, 1) for _, tt := range testCases { if tt.create { isopath, _, err = newDummyISO("cache", defaultISOFilename, tt.localVer) } else { if dir, e := ioutil.TempDir("", "machine-test"); e == nil { isopath = filepath.Join(dir, "cache", defaultISOFilename) } } // isopath: "$TMPDIR/machine-test-xxxxxx/cache/boot2docker.iso" // tmpDir: "$TMPDIR/machine-test-xxxxxx" imgCachePath := filepath.Dir(isopath) storePath := filepath.Dir(imgCachePath) b := &B2dUtils{ releaseGetter: &mockReleaseGetter{ ver: tt.latestVer, apiErr: tt.apiErr, verCh: verCh, }, iso: &mockISO{ isopath: isopath, exist: tt.create, ver: tt.localVer, verCh: verCh, }, storePath: storePath, imgCachePath: imgCachePath, } dir := filepath.Join(storePath, "machines", tt.machineName) err = os.MkdirAll(dir, 0700) assert.NoError(t, err, "machine: %s", tt.machineName) err = b.CopyIsoToMachineDir("", tt.machineName) assert.NoError(t, err) dest := filepath.Join(dir, b.filename()) _, pathErr := os.Stat(dest) assert.NoError(t, err, "machine: %s", tt.machineName) assert.True(t, !os.IsNotExist(pathErr), "machine: %s", tt.machineName) ver, err := b.version() assert.NoError(t, err, "machine: %s", tt.machineName) assert.Equal(t, tt.wantVer, ver, "machine: %s", tt.machineName) err = removeFileIfExists(isopath) assert.NoError(t, err, "machine: %s", tt.machineName) } } // newTestServer creates a new httptest.Server that returns respText as a response body. func newTestServer(respText string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(respText)) })) } // newDummyISO creates a dummy ISO file that contains the given version info, // and returns its path and offset value to fetch the version info. func newDummyISO(dir, name, version string) (string, int64, error) { tmpDir, err := ioutil.TempDir("", "machine-test-") if err != nil { return "", 0, err } tmpDir = filepath.Join(tmpDir, dir) if e := os.MkdirAll(tmpDir, 755); e != nil { return "", 0, err } isopath := filepath.Join(tmpDir, name) log.Info("TEST: dummy ISO created at ", isopath) // dummy ISO data mimicking the real byte data of a Boot2Docker ISO image padding := " " data := dummyISOData(padding, version) return isopath, int64(len(padding)), ioutil.WriteFile(isopath, data, 0644) } // dummyISOData returns mock data that contains given padding and version. func dummyISOData(padding, version string) []byte { return []byte(fmt.Sprintf("%sBoot2Docker-%s ", padding, version)) }