package api import ( "archive/zip" "bytes" "io" "mime" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "testing" "git.misaka.ren/m1saka/dsh-web/internal/store" ) func TestWorkspaceDownloadFilename(t *testing.T) { a, sid := runtimeTestAPI(t, func(http.ResponseWriter, *http.Request) { t.Error("unexpected Docker call") }) if err := a.s.AddInstance(store.Instance{ID: "one", UID: "owner"}); err != nil { t.Fatal(err) } home := filepath.Join(a.root, "workspaces/owner/instances/one/home") checkFilename := func(t *testing.T, w *httptest.ResponseRecorder, want string) { t.Helper() header := w.Header().Get("Content-Disposition") mediaType, params, err := mime.ParseMediaType(header) if err != nil { t.Fatalf("parse Content-Disposition %q: %v", header, err) } if mediaType != "attachment" || params["filename"] != want { t.Fatalf("Content-Disposition = %q, want attachment with filename %q", header, want) } } const content = "0123456789\nunchanged 下载 content\x00\xff" for _, name := range []string{"report.txt", "中文文件.txt", "report with spaces.txt", `report";100%.txt`, "README"} { t.Run(name, func(t *testing.T) { p := "parent/nested/" + name file := filepath.Join(home, filepath.FromSlash(p)) if err := os.MkdirAll(filepath.Dir(file), 0750); err != nil { t.Fatal(err) } if err := os.WriteFile(file, []byte(content), 0600); err != nil { t.Fatal(err) } for _, partial := range []bool{false, true} { r := runtimeRequest(http.MethodGet, "/api/ws/download?instance=one&path="+url.QueryEscape(p), "", sid) wantStatus, wantBody := http.StatusOK, content if partial { r.Header.Set("Range", "bytes=2-6") wantStatus, wantBody = http.StatusPartialContent, content[2:7] } w := httptest.NewRecorder() a.workspace(w, r) if w.Code != wantStatus { t.Fatalf("partial=%v: status = %d, want %d: %s", partial, w.Code, wantStatus, w.Body.String()) } checkFilename(t, w, name) if w.Body.String() != wantBody { t.Errorf("partial=%v: body = %q, want %q", partial, w.Body.String(), wantBody) } } }) } t.Run("directory zip", func(t *testing.T) { const name = `资料 ";100%` p := "parent/" + name dir := filepath.Join(home, filepath.FromSlash(p)) if err := os.MkdirAll(dir, 0750); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "entry.txt"), []byte(content), 0600); err != nil { t.Fatal(err) } w := httptest.NewRecorder() a.workspace(w, runtimeRequest(http.MethodGet, "/api/ws/download?instance=one&path="+url.QueryEscape(p), "", sid)) if w.Code != http.StatusOK { t.Fatalf("status = %d: %s", w.Code, w.Body.String()) } checkFilename(t, w, name+".zip") archive, err := zip.NewReader(bytes.NewReader(w.Body.Bytes()), int64(w.Body.Len())) if err != nil { t.Fatal(err) } if len(archive.File) != 1 || archive.File[0].Name != "entry.txt" { t.Fatalf("unexpected archive entries: %v", archive.File) } r, err := archive.File[0].Open() if err != nil { t.Fatal(err) } defer r.Close() got, err := io.ReadAll(r) if err != nil { t.Fatal(err) } if string(got) != content { t.Errorf("archive body = %q, want %q", got, content) } }) }