diff --git a/server/api/archive.go b/server/api/archive.go index 0528cb940..21a6dc849 100644 --- a/server/api/archive.go +++ b/server/api/archive.go @@ -4,6 +4,7 @@ package api import ( + "errors" "fmt" "net/http" "time" @@ -156,9 +157,18 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { return } + if a.app.GetConfig().MaxFileSize > 0 { + r.Body = http.MaxBytesReader(w, r.Body, a.app.GetConfig().MaxFileSize) + } + file, handle, err := r.FormFile(UploadFormFileKey) if err != nil { - fmt.Fprintf(w, "%v", err) + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + a.errorResponse(w, r, model.ErrRequestEntityTooLarge) + return + } + a.errorResponse(w, r, model.NewErrBadRequest(err.Error())) return } defer file.Close() diff --git a/server/api/archive_test.go b/server/api/archive_test.go new file mode 100644 index 000000000..6a24849ba --- /dev/null +++ b/server/api/archive_test.go @@ -0,0 +1,133 @@ +// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api + +import ( + "bytes" + "context" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/golang/mock/gomock" + "github.com/gorilla/mux" + "github.com/mattermost/mattermost-plugin-boards/server/app" + "github.com/mattermost/mattermost-plugin-boards/server/auth" + "github.com/mattermost/mattermost-plugin-boards/server/model" + "github.com/mattermost/mattermost-plugin-boards/server/services/config" + "github.com/mattermost/mattermost-plugin-boards/server/services/metrics" + "github.com/mattermost/mattermost-plugin-boards/server/services/permissions/mmpermissions" + mmpermissionsMocks "github.com/mattermost/mattermost-plugin-boards/server/services/permissions/mmpermissions/mocks" + permissionsMocks "github.com/mattermost/mattermost-plugin-boards/server/services/permissions/mocks" + "github.com/mattermost/mattermost-plugin-boards/server/services/store/mockstore" + "github.com/mattermost/mattermost-plugin-boards/server/services/webhook" + "github.com/mattermost/mattermost-plugin-boards/server/ws" + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/shared/mlog" + "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks" +) + +func setupArchiveImportAPI(t *testing.T, maxFileSize int64) (*API, func()) { + t.Helper() + + ctrl := gomock.NewController(t) + cfg := config.Configuration{MaxFileSize: maxFileSize} + store := mockstore.NewMockStore(ctrl) + filesBackend := &mocks.FileBackend{} + authService := auth.New(&cfg, store, nil) + logger, _ := mlog.NewLogger() + wsserver := ws.NewServer(authService, logger, store) + webhookClient := webhook.NewClient(&cfg, logger) + metricsService := metrics.NewMetrics(metrics.InstanceInfo{}) + + permStore := permissionsMocks.NewMockStore(ctrl) + pluginAPI := mmpermissionsMocks.NewMockAPI(ctrl) + pluginAPI.EXPECT().HasPermissionToTeam(gomock.Any(), gomock.Any(), model.PermissionViewTeam).Return(true) + store.EXPECT().GetUserByID(gomock.Any()).Return(&model.User{ID: "user", IsGuest: false}, nil) + permissions := mmpermissions.New(permStore, pluginAPI, mlog.CreateConsoleTestLogger(t)) + + testApp := app.New(&cfg, wsserver, app.Services{ + Auth: authService, + Store: store, + FilesBackend: filesBackend, + Webhook: webhookClient, + Metrics: metricsService, + Logger: logger, + SkipTemplateInit: true, + Permissions: permissions, + }) + + api := NewAPI(testApp, "", "", permissions, mlog.CreateConsoleTestLogger(t), nil) + + tearDown := func() { + testApp.Shutdown() + if logger != nil { + _ = logger.Shutdown() + } + } + + return api, tearDown +} + +func archiveImportRequest(t *testing.T, teamID string, body io.Reader, contentType string) *http.Request { + t.Helper() + + req := httptest.NewRequest(http.MethodPost, "/teams/"+teamID+"/archive/import", body) + req = mux.SetURLVars(req, map[string]string{"teamID": teamID}) + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + session := &model.Session{UserID: "user"} + ctx := context.WithValue(req.Context(), sessionContextKey, session) + return req.WithContext(ctx) +} + +func TestHandleArchiveImportFormErrors(t *testing.T) { + const teamID = "abcdefghijklmnopqrstuvwxyz" + + t.Run("returns 413 for oversized multipart body", func(t *testing.T) { + api, tearDown := setupArchiveImportAPI(t, 1) + defer tearDown() + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + part, err := writer.CreateFormFile(UploadFormFileKey, "archive.boardarchive") + require.NoError(t, err) + _, err = part.Write([]byte("too-large")) + require.NoError(t, err) + require.NoError(t, writer.Close()) + + req := archiveImportRequest(t, teamID, &body, writer.FormDataContentType()) + w := httptest.NewRecorder() + + api.handleArchiveImport(w, req) + + res := w.Result() + defer res.Body.Close() + require.Equal(t, http.StatusRequestEntityTooLarge, res.StatusCode) + require.Equal(t, "application/json", res.Header.Get("Content-Type")) + b, readErr := io.ReadAll(res.Body) + require.NoError(t, readErr) + require.Contains(t, string(b), "entity too large") + }) + + t.Run("returns 400 for malformed upload", func(t *testing.T) { + api, tearDown := setupArchiveImportAPI(t, 0) + defer tearDown() + + req := archiveImportRequest(t, teamID, bytes.NewReader([]byte("not-a-multipart-body")), "text/plain") + w := httptest.NewRecorder() + + api.handleArchiveImport(w, req) + + res := w.Result() + defer res.Body.Close() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + require.Equal(t, "application/json", res.Header.Get("Content-Type")) + }) +} diff --git a/server/app/import.go b/server/app/import.go index 177da2b0a..9b3518f4e 100644 --- a/server/app/import.go +++ b/server/app/import.go @@ -68,9 +68,11 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { dir, filename := filepath.Split(hdr.Name) dir = path.Clean(dir) + maxEntry := a.effectiveArchiveEntryMaxSize() + switch filename { case "version.json": - ver, errVer := parseVersionFile(zr) + ver, errVer := parseVersionFile(zr, maxEntry) if errVer != nil { return errVer } @@ -92,12 +94,28 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { mlog.String("dir", dir), mlog.String("filename", filename), ) + if err := discardLimited(zr, maxEntry); err != nil { + return fmt.Errorf("cannot skip orphan file %s: %w", filename, err) + } continue } - newFileName, err := a.SaveFile(zr, opt.TeamID, board.ID, filename, board.IsTemplate) + fileReader := newLimitedReader(zr, maxEntry) + newFileName, err := a.SaveFile(fileReader, opt.TeamID, board.ID, filename, board.IsTemplate) if err != nil { return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err) } + if limitedReaderExceeded(fileReader) { + filePath, pathErr := getDestinationFilePath(board.IsTemplate, opt.TeamID, board.ID, newFileName) + if pathErr == nil { + if removeErr := a.filesBackend.RemoveFile(filePath); removeErr != nil { + a.logger.Warn("failed to remove oversized import file", + mlog.String("path", filePath), + mlog.Err(removeErr), + ) + } + } + return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, errSizeLimitExceeded) + } fileMap[filename] = newFileName a.logger.Debug("import archive file", @@ -472,11 +490,33 @@ func arrayMapsValue(m map[string]interface{}, key string) ([]map[string]interfac return arr, true } -func parseVersionFile(r io.Reader) (int, error) { - file, err := io.ReadAll(r) +func newLimitedReader(r io.Reader, limit int64) *io.LimitedReader { + return &io.LimitedReader{R: r, N: limit + 1} +} + +func limitedReaderExceeded(lr *io.LimitedReader) bool { + return lr.N <= 0 +} + +func (a *App) effectiveArchiveEntryMaxSize() int64 { + maxEntry := int64(importMaxFileSize) + if a.config != nil { + if cfgMax := a.config.MaxFileSize; cfgMax > 0 && cfgMax < maxEntry { + maxEntry = cfgMax + } + } + return maxEntry +} + +func parseVersionFile(r io.Reader, maxSize int64) (int, error) { + lr := newLimitedReader(r, maxSize) + file, err := io.ReadAll(lr) if err != nil { return 0, fmt.Errorf("cannot read version.json: %w", err) } + if limitedReaderExceeded(lr) { + return 0, fmt.Errorf("cannot read version.json: %w", errSizeLimitExceeded) + } var header model.ArchiveHeader if err := json.Unmarshal(file, &header); err != nil { @@ -484,3 +524,14 @@ func parseVersionFile(r io.Reader) (int, error) { } return header.Version, nil } + +func discardLimited(r io.Reader, limit int64) error { + lr := newLimitedReader(r, limit) + if _, err := io.Copy(io.Discard, lr); err != nil { + return err + } + if limitedReaderExceeded(lr) { + return errSizeLimitExceeded + } + return nil +} diff --git a/server/app/import_test.go b/server/app/import_test.go index 672312405..c410f4b39 100644 --- a/server/app/import_test.go +++ b/server/app/import_test.go @@ -4,13 +4,17 @@ package app import ( + "archive/zip" "bytes" + "io" + "strings" "testing" - "github.com/mattermost/mattermost-plugin-boards/server/utils" - "github.com/golang/mock/gomock" "github.com/mattermost/mattermost-plugin-boards/server/model" + "github.com/mattermost/mattermost-plugin-boards/server/utils" + "github.com/mattermost/mattermost/server/public/plugin/plugintest/mock" + "github.com/mattermost/mattermost/server/v8/platform/shared/filestore/mocks" "github.com/stretchr/testify/require" ) @@ -324,6 +328,195 @@ func TestApp_ImportArchive(t *testing.T) { }) } +func TestEffectiveArchiveEntryMaxSize(t *testing.T) { + th, tearDown := SetupTestHelper(t) + defer tearDown() + + t.Run("returns importMaxFileSize when config is unset or larger", func(t *testing.T) { + th.App.config.MaxFileSize = 0 + require.Equal(t, int64(importMaxFileSize), th.App.effectiveArchiveEntryMaxSize()) + + th.App.config.MaxFileSize = int64(importMaxFileSize) * 2 + require.Equal(t, int64(importMaxFileSize), th.App.effectiveArchiveEntryMaxSize()) + }) + + t.Run("honors configured MaxFileSize when stricter", func(t *testing.T) { + th.App.config.MaxFileSize = 1024 + require.Equal(t, int64(1024), th.App.effectiveArchiveEntryMaxSize()) + }) +} + +func TestParseVersionFile(t *testing.T) { + t.Run("valid version", func(t *testing.T) { + ver, err := parseVersionFile(strings.NewReader(`{"version":2}`), importMaxFileSize) + require.NoError(t, err) + require.Equal(t, 2, ver) + }) + + t.Run("size limit exceeded", func(t *testing.T) { + payload := []byte(`{"version":2,"padding":"` + strings.Repeat("x", int(importMaxFileSize)) + `"}`) + _, err := parseVersionFile(bytes.NewReader(payload), importMaxFileSize) + require.Error(t, err) + require.ErrorIs(t, err, errSizeLimitExceeded) + }) +} + +func TestImportArchiveVersionFileSizeLimit(t *testing.T) { + t.Run("valid archive with version.json only", func(t *testing.T) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + w, err := zw.Create("version.json") + require.NoError(t, err) + _, err = w.Write([]byte(`{"version":2}`)) + require.NoError(t, err) + require.NoError(t, zw.Close()) + + th, tearDown := SetupTestHelper(t) + defer tearDown() + + err = th.App.ImportArchive(bytes.NewReader(buf.Bytes()), model.ImportArchiveOptions{ + TeamID: "test-team", + ModifiedBy: "user", + }) + require.NoError(t, err) + }) + + t.Run("rejects oversized version.json", func(t *testing.T) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + w, err := zw.Create("version.json") + require.NoError(t, err) + payload := []byte(`{"version":2,"padding":"` + strings.Repeat("x", int(importMaxFileSize)) + `"}`) + _, err = w.Write(payload) + require.NoError(t, err) + require.NoError(t, zw.Close()) + + th, tearDown := SetupTestHelper(t) + defer tearDown() + + err = th.App.ImportArchive(bytes.NewReader(buf.Bytes()), model.ImportArchiveOptions{ + TeamID: "test-team", + ModifiedBy: "user", + }) + require.Error(t, err) + require.ErrorIs(t, err, errSizeLimitExceeded) + }) +} + +func TestImportArchiveImageFileSizeLimit(t *testing.T) { + const ( + boardDir = "testboard" + teamID = "abcdefghijklmnopqrstuvwxyz" + ) + minimalBoardJSONL := `{"type":"board","data":{"id":"bfoi6yy6pa3yzika53spj7pq9ee","teamId":"` + teamID + `","createdBy":"user","modifiedBy":"user","type":"P","title":"Test","createAt":1,"updateAt":1}} +{"type":"block","data":{"id":"ckpc3b1dp3pbw7bqntfryy9jbzo","parentId":"bfoi6yy6pa3yzika53spj7pq9ee","createdBy":"user","modifiedBy":"user","schema":1,"type":"card","title":"Test","fields":{},"createAt":1,"updateAt":1,"boardId":"bfoi6yy6pa3yzika53spj7pq9ee"}} +` + + t.Run("rejects oversized image file and removes partial save", func(t *testing.T) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + writeZipEntry(t, zw, "version.json", []byte(`{"version":2}`)) + writeZipEntry(t, zw, boardDir+"/board.jsonl", []byte(minimalBoardJSONL)) + writeZipEntry(t, zw, boardDir+"/image.jpg", bytes.Repeat([]byte("x"), int(importMaxFileSize)+1)) + require.NoError(t, zw.Close()) + + th, tearDown := SetupTestHelper(t) + defer tearDown() + + board := &model.Board{ + ID: "bfoi6yy6pa3yzika53spj7pq9ee", + TeamID: teamID, + Title: "Test", + } + block := &model.Block{ + ID: "ckpc3b1dp3pbw7bqntfryy9jbzo", + ParentID: board.ID, + Type: model.TypeCard, + BoardID: board.ID, + } + babs := &model.BoardsAndBlocks{ + Boards: []*model.Board{board}, + Blocks: []*model.Block{block}, + } + + th.API.EXPECT().HasPermissionToTeam("user", teamID, model.PermissionCreatePrivateChannel).Return(true).AnyTimes() + th.API.EXPECT().HasPermissionToTeam("user", teamID, model.PermissionCreatePublicChannel).Return(true).AnyTimes() + + th.Store.EXPECT().CreateBoardsAndBlocks(gomock.AssignableToTypeOf(&model.BoardsAndBlocks{}), "user").Return(babs, nil) + th.Store.EXPECT().GetMembersForBoard(board.ID).AnyTimes().Return([]*model.BoardMember{}, nil) + th.Store.EXPECT().GetBoard(board.ID).AnyTimes().Return(board, nil) + th.Store.EXPECT().GetMemberForBoard(board.ID, "user").AnyTimes().Return(&model.BoardMember{ + BoardID: board.ID, + UserID: "user", + }, nil) + th.Store.EXPECT().GetUserCategoryBoards("user", teamID).Return([]model.CategoryBoards{ + { + Category: model.Category{ + Type: "default", + Name: "Boards", + ID: "boards_category_id", + }, + }, + }, nil) + th.Store.EXPECT().GetUserCategoryBoards("user", teamID) + th.Store.EXPECT().CreateCategory(utils.Anything).Return(nil) + th.Store.EXPECT().GetCategory(utils.Anything).Return(&model.Category{ + ID: "boards_category_id", + Name: "Boards", + }, nil) + th.Store.EXPECT().GetBoardsForUserAndTeam("user", teamID, false).Return([]*model.Board{}, nil) + th.Store.EXPECT().GetMembersForUser("user").Return([]*model.BoardMember{}, nil) + th.Store.EXPECT().AddUpdateCategoryBoard("user", utils.Anything, utils.Anything).Return(nil) + th.Store.EXPECT().SaveFileInfo(gomock.Any()).Return(nil) + + mockedFileBackend := &mocks.FileBackend{} + th.App.filesBackend = mockedFileBackend + writeFileFunc := func(reader io.Reader, path string) int64 { + n, _ := io.Copy(io.Discard, reader) + return n + } + writeFileErrorFunc := func(reader io.Reader, filePath string) error { return nil } + mockedFileBackend.On("WriteFile", mock.Anything, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) + mockedFileBackend.On("RemoveFile", mock.Anything).Return(nil) + + err := th.App.ImportArchive(bytes.NewReader(buf.Bytes()), model.ImportArchiveOptions{ + TeamID: teamID, + ModifiedBy: "user", + }) + require.Error(t, err) + require.ErrorIs(t, err, errSizeLimitExceeded) + mockedFileBackend.AssertCalled(t, "RemoveFile", mock.Anything) + }) +} + +func TestImportArchiveOrphanFileSizeLimit(t *testing.T) { + t.Run("rejects oversized orphan file", func(t *testing.T) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + writeZipEntry(t, zw, "version.json", []byte(`{"version":2}`)) + writeZipEntry(t, zw, "orphan/image.jpg", bytes.Repeat([]byte("x"), int(importMaxFileSize)+1)) + require.NoError(t, zw.Close()) + + th, tearDown := SetupTestHelper(t) + defer tearDown() + + err := th.App.ImportArchive(bytes.NewReader(buf.Bytes()), model.ImportArchiveOptions{ + TeamID: "test-team", + ModifiedBy: "user", + }) + require.Error(t, err) + require.ErrorIs(t, err, errSizeLimitExceeded) + }) +} + +func writeZipEntry(t *testing.T, zw *zip.Writer, name string, content []byte) { + t.Helper() + w, err := zw.Create(name) + require.NoError(t, err) + _, err = w.Write(content) + require.NoError(t, err) +} + //nolint:lll const openBoardArchive = `{"type":"board","data":{"id":"bfoi6yy6pa3yzika53spj7pq9ee","teamId":"wsmqbtwb5jb35jb3mtp85c8a9h","createdBy":"f1tydgc697fcbp8ampr6881jea","modifiedBy":"f1tydgc697fcbp8ampr6881jea","type":"O","minimumRole":"","title":"Open Import Test","createAt":1672750481591,"updateAt":1672750481591}} {"type":"block","data":{"id":"ckpc3b1dp3pbw7bqntfryy9jbzo","parentId":"bjaqxtbyqz3bu7pgyddpgpms74a","createdBy":"f1tydgc697fcbp8ampr6881jea","modifiedBy":"f1tydgc697fcbp8ampr6881jea","schema":1,"type":"card","title":"Test","fields":{"contentOrder":[],"icon":"","isTemplate":false,"properties":{}},"createAt":1672750481612,"updateAt":1672845003530,"deleteAt":0,"boardId":"bfoi6yy6pa3yzika53spj7pq9ee"}} diff --git a/server/client/client.go b/server/client/client.go index bd3d5d7df..c66d9b399 100644 --- a/server/client/client.go +++ b/server/client/client.go @@ -97,6 +97,27 @@ func (c *Client) DoAPIPost(url, data string) (*http.Response, error) { return c.DoAPIRequest(http.MethodPost, c.APIURL+url, data, "") } +func (c *Client) DoAPIPostMultipart(url, fieldName, fileName string, data []byte) (*http.Response, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile(fieldName, fileName) + if err != nil { + return nil, err + } + if _, err = part.Write(data); err != nil { + return nil, err + } + if err = writer.Close(); err != nil { + return nil, err + } + + opt := func(r *http.Request) { + r.Header.Set("Content-Type", writer.FormDataContentType()) + } + + return c.doAPIRequestReader(http.MethodPost, c.APIURL+url, body, "", opt) +} + func (c *Client) DoAPIPatch(url, data string) (*http.Response, error) { return c.DoAPIRequest(http.MethodPatch, c.APIURL+url, data, "") } diff --git a/server/integrationtests/import_archive_test.go b/server/integrationtests/import_archive_test.go index 45e34a56f..71b219294 100644 --- a/server/integrationtests/import_archive_test.go +++ b/server/integrationtests/import_archive_test.go @@ -9,6 +9,7 @@ import ( "bytes" "encoding/json" "io" + "net/http" "strings" "testing" @@ -18,6 +19,70 @@ import ( "github.com/stretchr/testify/require" ) +func minimalImportArchiveZip(t *testing.T) []byte { + t.Helper() + + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + w, err := zw.Create("version.json") + require.NoError(t, err) + _, err = w.Write([]byte(`{"version":2}`)) + require.NoError(t, err) + require.NoError(t, zw.Close()) + return buf.Bytes() +} + +func TestImportArchiveAPIErrors(t *testing.T) { + t.Run("rejects oversized upload with 413 and JSON error body", func(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + + clients := setupClients(th) + th.Client = clients.TeamMember + teamID := mmModel.NewId() + + config := th.Server.App().GetConfig() + origMaxFileSize := config.MaxFileSize + defer func() { + config.MaxFileSize = origMaxFileSize + th.Server.App().SetConfig(config) + }() + + config.MaxFileSize = 1 + th.Server.App().SetConfig(config) + + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + w, err := zw.Create("version.json") + require.NoError(t, err) + _, err = w.Write([]byte(`{"version":2}`)) + require.NoError(t, err) + require.NoError(t, zw.Close()) + + resp := th.Client.ImportArchive(teamID, bytes.NewReader(buf.Bytes())) + th.CheckRequestEntityTooLarge(resp) + require.Equal(t, "application/json", resp.Header.Get("Content-Type")) + }) + + t.Run("returns 400 with JSON error body for malformed upload", func(t *testing.T) { + th := SetupTestHelperPluginMode(t) + defer th.TearDown() + + clients := setupClients(th) + th.Client = clients.TeamMember + teamID := mmModel.NewId() + + url := th.Client.GetTeamRoute(teamID) + "/archive/import" + r, err := th.Client.DoAPIPost(url, "not-a-multipart-body") + require.NotNil(t, r) + defer r.Body.Close() + + require.Equal(t, http.StatusBadRequest, r.StatusCode) + require.Equal(t, "application/json", r.Header.Get("Content-Type")) + _ = err // client returns RequestReaderError for non-2xx responses + }) +} + func TestImportArchiveStripsGuestSchemeAdmin(t *testing.T) { t.Run("import archive with guest member schemeAdmin true", func(t *testing.T) { th := SetupTestHelperPluginMode(t) diff --git a/server/integrationtests/permissions_test.go b/server/integrationtests/permissions_test.go index 82c6bdcb8..23e1e1199 100644 --- a/server/integrationtests/permissions_test.go +++ b/server/integrationtests/permissions_test.go @@ -13,6 +13,7 @@ import ( "strings" "testing" + "github.com/mattermost/mattermost-plugin-boards/server/api" "github.com/mattermost/mattermost-plugin-boards/server/model" "github.com/mattermost/mattermost-plugin-boards/server/utils" mmModel "github.com/mattermost/mattermost/server/public/model" @@ -277,7 +278,11 @@ func runTestCases(t *testing.T, ttCases []TestCase, testData TestData, clients C response, err = reqClient.DoAPIGet(url, "") defer response.Body.Close() case methodPost: - response, err = reqClient.DoAPIPost(url, body) + if strings.Contains(url, "/archive/import") && tc.expectedStatusCode >= 200 && tc.expectedStatusCode < 300 { + response, err = reqClient.DoAPIPostMultipart(url, api.UploadFormFileKey, "archive.boardarchive", minimalImportArchiveZip(t)) + } else { + response, err = reqClient.DoAPIPost(url, body) + } defer response.Body.Close() case methodPatch: response, err = reqClient.DoAPIPatch(url, body) @@ -2831,11 +2836,11 @@ func TestPermissionsBoardArchiveImport(t *testing.T) { ttCases := []TestCase{ {"/teams/test-team/archive/import", methodPost, "", userAnon, http.StatusUnauthorized, 0}, {"/teams/test-team/archive/import", methodPost, "", userNoTeamMember, http.StatusForbidden, 1}, - {"/teams/test-team/archive/import", methodPost, "", userTeamMember, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userViewer, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userCommenter, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userEditor, http.StatusOK, 1}, - {"/teams/test-team/archive/import", methodPost, "", userAdmin, http.StatusOK, 1}, + {"/teams/test-team/archive/import", methodPost, "", userTeamMember, http.StatusOK, 0}, + {"/teams/test-team/archive/import", methodPost, "", userViewer, http.StatusOK, 0}, + {"/teams/test-team/archive/import", methodPost, "", userCommenter, http.StatusOK, 0}, + {"/teams/test-team/archive/import", methodPost, "", userEditor, http.StatusOK, 0}, + {"/teams/test-team/archive/import", methodPost, "", userAdmin, http.StatusOK, 0}, {"/teams/test-team/archive/import", methodPost, "", userGuest, http.StatusForbidden, 0}, }