|
1 | 1 | package cmd |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "archive/tar" |
4 | 5 | "bytes" |
5 | 6 | "context" |
6 | 7 | "errors" |
7 | 8 | "fmt" |
8 | 9 | "io" |
9 | 10 | "net/http" |
10 | 11 | "os" |
| 12 | + "path/filepath" |
11 | 13 | "strings" |
12 | 14 | "testing" |
13 | 15 | "time" |
14 | 16 |
|
15 | 17 | "github.com/kernel/kernel-go-sdk" |
16 | 18 | "github.com/kernel/kernel-go-sdk/option" |
17 | 19 | "github.com/kernel/kernel-go-sdk/packages/pagination" |
| 20 | + "github.com/klauspost/compress/zstd" |
18 | 21 | "github.com/pterm/pterm" |
19 | 22 | "github.com/stretchr/testify/assert" |
20 | 23 | ) |
@@ -224,86 +227,92 @@ func TestProfilesDelete_SkipConfirm(t *testing.T) { |
224 | 227 | assert.Contains(t, buf.String(), "Deleted profile: a") |
225 | 228 | } |
226 | 229 |
|
227 | | -func TestProfilesDownload_MissingOutput(t *testing.T) { |
228 | | - buf := captureProfilesOutput(t) |
229 | | - fake := &FakeProfilesService{DownloadFunc: func(ctx context.Context, idOrName string, opts ...option.RequestOption) (*http.Response, error) { |
230 | | - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("content")), Header: http.Header{}}, nil |
231 | | - }} |
| 230 | +// makeProfileArchive builds a zstd-compressed tar archive from a map of file |
| 231 | +// paths to contents, for use in download tests. |
| 232 | +func makeProfileArchive(t *testing.T, files map[string]string) []byte { |
| 233 | + t.Helper() |
| 234 | + var buf bytes.Buffer |
| 235 | + zw, err := zstd.NewWriter(&buf) |
| 236 | + assert.NoError(t, err) |
| 237 | + tw := tar.NewWriter(zw) |
| 238 | + for name, content := range files { |
| 239 | + hdr := &tar.Header{Name: name, Mode: 0o644, Size: int64(len(content)), Typeflag: tar.TypeReg} |
| 240 | + assert.NoError(t, tw.WriteHeader(hdr)) |
| 241 | + _, err := tw.Write([]byte(content)) |
| 242 | + assert.NoError(t, err) |
| 243 | + } |
| 244 | + assert.NoError(t, tw.Close()) |
| 245 | + assert.NoError(t, zw.Close()) |
| 246 | + return buf.Bytes() |
| 247 | +} |
| 248 | + |
| 249 | +func TestProfilesDownload_MissingTo(t *testing.T) { |
| 250 | + fake := &FakeProfilesService{} |
232 | 251 | p := ProfilesCmd{profiles: fake} |
233 | | - _ = p.Download(context.Background(), ProfilesDownloadInput{Identifier: "p1", Output: "", Pretty: false}) |
234 | | - assert.Contains(t, buf.String(), "Missing --to output file path") |
| 252 | + err := p.Download(context.Background(), ProfilesDownloadInput{Identifier: "p1", To: ""}) |
| 253 | + assert.Error(t, err) |
| 254 | + assert.Contains(t, err.Error(), "missing required --to") |
235 | 255 | } |
236 | 256 |
|
237 | | -func TestProfilesDownload_RawSuccess(t *testing.T) { |
| 257 | +func TestProfilesDownload_ExtractSuccess(t *testing.T) { |
238 | 258 | buf := captureProfilesOutput(t) |
239 | | - f, err := os.CreateTemp("", "profile-*.zip") |
| 259 | + dir, err := os.MkdirTemp("", "profile-*") |
240 | 260 | assert.NoError(t, err) |
241 | | - name := f.Name() |
242 | | - _ = f.Close() |
243 | | - defer os.Remove(name) |
| 261 | + defer os.RemoveAll(dir) |
244 | 262 |
|
245 | | - content := "hello" |
| 263 | + archive := makeProfileArchive(t, map[string]string{ |
| 264 | + "Default/Preferences": "{\"k\":1}", |
| 265 | + "Local State": "local", |
| 266 | + }) |
246 | 267 | fake := &FakeProfilesService{DownloadFunc: func(ctx context.Context, idOrName string, opts ...option.RequestOption) (*http.Response, error) { |
247 | | - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(content)), Header: http.Header{}}, nil |
| 268 | + return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(archive)), Header: http.Header{}}, nil |
248 | 269 | }} |
249 | 270 | p := ProfilesCmd{profiles: fake} |
250 | | - _ = p.Download(context.Background(), ProfilesDownloadInput{Identifier: "p1", Output: name, Pretty: false}) |
251 | | - |
252 | | - b, readErr := os.ReadFile(name) |
253 | | - assert.NoError(t, readErr) |
254 | | - assert.Equal(t, content, string(b)) |
255 | | - assert.Contains(t, buf.String(), "Saved profile to "+name) |
256 | | -} |
257 | | - |
258 | | -func TestProfilesDownload_PrettySuccess(t *testing.T) { |
259 | | - f, err := os.CreateTemp("", "profile-*.json") |
| 271 | + err = p.Download(context.Background(), ProfilesDownloadInput{Identifier: "p1", To: dir}) |
260 | 272 | assert.NoError(t, err) |
261 | | - name := f.Name() |
262 | | - _ = f.Close() |
263 | | - defer os.Remove(name) |
264 | 273 |
|
265 | | - jsonBody := "{\"a\":1}" |
266 | | - fake := &FakeProfilesService{DownloadFunc: func(ctx context.Context, idOrName string, opts ...option.RequestOption) (*http.Response, error) { |
267 | | - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(jsonBody)), Header: http.Header{}}, nil |
268 | | - }} |
269 | | - p := ProfilesCmd{profiles: fake} |
270 | | - _ = p.Download(context.Background(), ProfilesDownloadInput{Identifier: "p1", Output: name, Pretty: true}) |
| 274 | + b, readErr := os.ReadFile(filepath.Join(dir, "Default", "Preferences")) |
| 275 | + assert.NoError(t, readErr) |
| 276 | + assert.Equal(t, "{\"k\":1}", string(b)) |
271 | 277 |
|
272 | | - b, readErr := os.ReadFile(name) |
| 278 | + b2, readErr := os.ReadFile(filepath.Join(dir, "Local State")) |
273 | 279 | assert.NoError(t, readErr) |
274 | | - out := string(b) |
275 | | - assert.Contains(t, out, "\n") |
276 | | - assert.Contains(t, out, "\"a\": 1") |
| 280 | + assert.Equal(t, "local", string(b2)) |
| 281 | + |
| 282 | + assert.Contains(t, buf.String(), "Extracted profile 'p1' to "+dir) |
277 | 283 | } |
278 | 284 |
|
279 | | -func TestProfilesDownload_PrettyEmptyBody(t *testing.T) { |
| 285 | +func TestProfilesDownload_202NoData(t *testing.T) { |
280 | 286 | buf := captureProfilesOutput(t) |
281 | | - f, err := os.CreateTemp("", "profile-*.json") |
| 287 | + dir, err := os.MkdirTemp("", "profile-*") |
282 | 288 | assert.NoError(t, err) |
283 | | - name := f.Name() |
284 | | - _ = f.Close() |
285 | | - defer os.Remove(name) |
| 289 | + defer os.RemoveAll(dir) |
286 | 290 |
|
287 | 291 | fake := &FakeProfilesService{DownloadFunc: func(ctx context.Context, idOrName string, opts ...option.RequestOption) (*http.Response, error) { |
288 | | - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("")), Header: http.Header{}}, nil |
| 292 | + return &http.Response{StatusCode: http.StatusAccepted, Body: io.NopCloser(strings.NewReader("")), Header: http.Header{}}, nil |
289 | 293 | }} |
290 | 294 | p := ProfilesCmd{profiles: fake} |
291 | | - _ = p.Download(context.Background(), ProfilesDownloadInput{Identifier: "p1", Output: name, Pretty: true}) |
292 | | - assert.Contains(t, buf.String(), "Empty response body") |
| 295 | + err = p.Download(context.Background(), ProfilesDownloadInput{Identifier: "fresh", To: dir}) |
| 296 | + assert.NoError(t, err) |
| 297 | + assert.Contains(t, buf.String(), "no saved data yet") |
| 298 | + |
| 299 | + entries, _ := os.ReadDir(dir) |
| 300 | + assert.Empty(t, entries) |
293 | 301 | } |
294 | 302 |
|
295 | | -func TestProfilesDownload_PrettyInvalidJSON(t *testing.T) { |
296 | | - buf := captureProfilesOutput(t) |
297 | | - f, err := os.CreateTemp("", "profile-*.json") |
| 303 | +func TestProfilesDownload_PathTraversalRejected(t *testing.T) { |
| 304 | + dir, err := os.MkdirTemp("", "profile-*") |
298 | 305 | assert.NoError(t, err) |
299 | | - name := f.Name() |
300 | | - _ = f.Close() |
301 | | - defer os.Remove(name) |
| 306 | + defer os.RemoveAll(dir) |
302 | 307 |
|
| 308 | + archive := makeProfileArchive(t, map[string]string{ |
| 309 | + "../escape": "nope", |
| 310 | + }) |
303 | 311 | fake := &FakeProfilesService{DownloadFunc: func(ctx context.Context, idOrName string, opts ...option.RequestOption) (*http.Response, error) { |
304 | | - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("not json")), Header: http.Header{}}, nil |
| 312 | + return &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(archive)), Header: http.Header{}}, nil |
305 | 313 | }} |
306 | 314 | p := ProfilesCmd{profiles: fake} |
307 | | - _ = p.Download(context.Background(), ProfilesDownloadInput{Identifier: "p1", Output: name, Pretty: true}) |
308 | | - assert.Contains(t, buf.String(), "Failed to pretty-print JSON") |
| 315 | + err = p.Download(context.Background(), ProfilesDownloadInput{Identifier: "p1", To: dir}) |
| 316 | + assert.Error(t, err) |
| 317 | + assert.Contains(t, err.Error(), "illegal entry path") |
309 | 318 | } |
0 commit comments