1package jsonmessage // import "github.com/docker/docker/pkg/jsonmessage"
2
3import (
4	"bytes"
5	"fmt"
6	"os"
7	"strings"
8	"testing"
9	"time"
10
11	"github.com/moby/term"
12	"gotest.tools/v3/assert"
13	is "gotest.tools/v3/assert/cmp"
14)
15
16func TestError(t *testing.T) {
17	je := JSONError{404, "Not found"}
18	assert.Assert(t, is.Error(&je, "Not found"))
19}
20
21func TestProgressString(t *testing.T) {
22	type expected struct {
23		short string
24		long  string
25	}
26
27	shortAndLong := func(short, long string) expected {
28		return expected{short: short, long: long}
29	}
30
31	start := time.Date(2017, 12, 3, 15, 10, 1, 0, time.UTC)
32	timeAfter := func(delta time.Duration) func() time.Time {
33		return func() time.Time {
34			return start.Add(delta)
35		}
36	}
37
38	var testcases = []struct {
39		name     string
40		progress JSONProgress
41		expected expected
42	}{
43		{
44			name: "no progress",
45		},
46		{
47			name:     "progress 1",
48			progress: JSONProgress{Current: 1},
49			expected: shortAndLong("      1B", "      1B"),
50		},
51		{
52			name: "some progress with a start time",
53			progress: JSONProgress{
54				Current: 20,
55				Total:   100,
56				Start:   start.Unix(),
57				nowFunc: timeAfter(time.Second),
58			},
59			expected: shortAndLong(
60				"     20B/100B 4s",
61				"[==========>                                        ]      20B/100B 4s",
62			),
63		},
64		{
65			name:     "some progress without a start time",
66			progress: JSONProgress{Current: 50, Total: 100},
67			expected: shortAndLong(
68				"     50B/100B",
69				"[=========================>                         ]      50B/100B",
70			),
71		},
72		{
73			name:     "current more than total is not negative gh#7136",
74			progress: JSONProgress{Current: 50, Total: 40},
75			expected: shortAndLong(
76				"     50B",
77				"[==================================================>]      50B",
78			),
79		},
80		{
81			name:     "with units",
82			progress: JSONProgress{Current: 50, Total: 100, Units: "units"},
83			expected: shortAndLong(
84				"50/100 units",
85				"[=========================>                         ] 50/100 units",
86			),
87		},
88		{
89			name:     "current more than total with units is not negative ",
90			progress: JSONProgress{Current: 50, Total: 40, Units: "units"},
91			expected: shortAndLong(
92				"50 units",
93				"[==================================================>] 50 units",
94			),
95		},
96		{
97			name:     "hide counts",
98			progress: JSONProgress{Current: 50, Total: 100, HideCounts: true},
99			expected: shortAndLong(
100				"",
101				"[=========================>                         ] ",
102			),
103		},
104	}
105
106	for _, testcase := range testcases {
107		t.Run(testcase.name, func(t *testing.T) {
108			testcase.progress.winSize = 100
109			assert.Equal(t, testcase.progress.String(), testcase.expected.short)
110
111			testcase.progress.winSize = 200
112			assert.Equal(t, testcase.progress.String(), testcase.expected.long)
113		})
114	}
115}
116
117func TestJSONMessageDisplay(t *testing.T) {
118	now := time.Now()
119	messages := map[JSONMessage][]string{
120		// Empty
121		{}: {"\n", "\n"},
122		// Status
123		{
124			Status: "status",
125		}: {
126			"status\n",
127			"status\n",
128		},
129		// General
130		{
131			Time:   now.Unix(),
132			ID:     "ID",
133			From:   "From",
134			Status: "status",
135		}: {
136			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(RFC3339NanoFixed)),
137			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(now.Unix(), 0).Format(RFC3339NanoFixed)),
138		},
139		// General, with nano precision time
140		{
141			TimeNano: now.UnixNano(),
142			ID:       "ID",
143			From:     "From",
144			Status:   "status",
145		}: {
146			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
147			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
148		},
149		// General, with both times Nano is preferred
150		{
151			Time:     now.Unix(),
152			TimeNano: now.UnixNano(),
153			ID:       "ID",
154			From:     "From",
155			Status:   "status",
156		}: {
157			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
158			fmt.Sprintf("%v ID: (from From) status\n", time.Unix(0, now.UnixNano()).Format(RFC3339NanoFixed)),
159		},
160		// Stream over status
161		{
162			Status: "status",
163			Stream: "stream",
164		}: {
165			"stream",
166			"stream",
167		},
168		// With progress message
169		{
170			Status:          "status",
171			ProgressMessage: "progressMessage",
172		}: {
173			"status progressMessage",
174			"status progressMessage",
175		},
176		// With progress, stream empty
177		{
178			Status:   "status",
179			Stream:   "",
180			Progress: &JSONProgress{Current: 1},
181		}: {
182			"",
183			fmt.Sprintf("%c[2K\rstatus       1B\r", 27),
184		},
185	}
186
187	// The tests :)
188	for jsonMessage, expectedMessages := range messages {
189		// Without terminal
190		data := bytes.NewBuffer([]byte{})
191		if err := jsonMessage.Display(data, false); err != nil {
192			t.Fatal(err)
193		}
194		if data.String() != expectedMessages[0] {
195			t.Fatalf("Expected %q,got %q", expectedMessages[0], data.String())
196		}
197		// With terminal
198		data = bytes.NewBuffer([]byte{})
199		if err := jsonMessage.Display(data, true); err != nil {
200			t.Fatal(err)
201		}
202		if data.String() != expectedMessages[1] {
203			t.Fatalf("\nExpected %q\n     got %q", expectedMessages[1], data.String())
204		}
205	}
206}
207
208// Test JSONMessage with an Error. It will return an error with the text as error, not the meaning of the HTTP code.
209func TestJSONMessageDisplayWithJSONError(t *testing.T) {
210	data := bytes.NewBuffer([]byte{})
211	jsonMessage := JSONMessage{Error: &JSONError{404, "Can't find it"}}
212
213	err := jsonMessage.Display(data, true)
214	if err == nil || err.Error() != "Can't find it" {
215		t.Fatalf("Expected a JSONError 404, got %q", err)
216	}
217
218	jsonMessage = JSONMessage{Error: &JSONError{401, "Anything"}}
219	err = jsonMessage.Display(data, true)
220	assert.Check(t, is.Error(err, "authentication is required"))
221}
222
223func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) {
224	var (
225		inFd uintptr
226	)
227	data := bytes.NewBuffer([]byte{})
228	reader := strings.NewReader("This is not a 'valid' JSON []")
229	inFd, _ = term.GetFdInfo(reader)
230
231	exp := "invalid character "
232	if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err == nil || !strings.HasPrefix(err.Error(), exp) {
233		t.Fatalf("Expected error (%s...), got %q", exp, err)
234	}
235}
236
237func TestDisplayJSONMessagesStream(t *testing.T) {
238	var (
239		inFd uintptr
240	)
241
242	messages := map[string][]string{
243		// empty string
244		"": {
245			"",
246			""},
247		// Without progress & ID
248		"{ \"status\": \"status\" }": {
249			"status\n",
250			"status\n",
251		},
252		// Without progress, with ID
253		"{ \"id\": \"ID\",\"status\": \"status\" }": {
254			"ID: status\n",
255			fmt.Sprintf("ID: status\n"),
256		},
257		// With progress
258		"{ \"id\": \"ID\", \"status\": \"status\", \"progress\": \"ProgressMessage\" }": {
259			"ID: status ProgressMessage",
260			fmt.Sprintf("\n%c[%dAID: status ProgressMessage%c[%dB", 27, 1, 27, 1),
261		},
262		// With progressDetail
263		"{ \"id\": \"ID\", \"status\": \"status\", \"progressDetail\": { \"Current\": 1} }": {
264			"", // progressbar is disabled in non-terminal
265			fmt.Sprintf("\n%c[%dA%c[2K\rID: status       1B\r%c[%dB", 27, 1, 27, 27, 1),
266		},
267	}
268
269	// Use $TERM which is unlikely to exist, forcing DisplayJSONMessageStream to
270	// (hopefully) use &noTermInfo.
271	origTerm := os.Getenv("TERM")
272	os.Setenv("TERM", "xyzzy-non-existent-terminfo")
273
274	for jsonMessage, expectedMessages := range messages {
275		data := bytes.NewBuffer([]byte{})
276		reader := strings.NewReader(jsonMessage)
277		inFd, _ = term.GetFdInfo(reader)
278
279		// Without terminal
280		if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err != nil {
281			t.Fatal(err)
282		}
283		if data.String() != expectedMessages[0] {
284			t.Fatalf("Expected an %q, got %q", expectedMessages[0], data.String())
285		}
286
287		// With terminal
288		data = bytes.NewBuffer([]byte{})
289		reader = strings.NewReader(jsonMessage)
290		if err := DisplayJSONMessagesStream(reader, data, inFd, true, nil); err != nil {
291			t.Fatal(err)
292		}
293		if data.String() != expectedMessages[1] {
294			t.Fatalf("\nExpected %q\n     got %q", expectedMessages[1], data.String())
295		}
296	}
297	os.Setenv("TERM", origTerm)
298
299}
300