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