mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 06:21:11 +00:00 
			
		
		
		
	fix(api): deactivate issue api for disabled or external issue-tracker (#8829)
- When the issue unit is disabled for a repository, don't allow issue related APIs. - Added integration tests. - Resolves #8408 Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8829 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: zokki <zokki.softwareschmiede@gmail.com> Co-committed-by: zokki <zokki.softwareschmiede@gmail.com>
This commit is contained in:
		
					parent
					
						
							
								8f4ebab023
							
						
					
				
			
			
				commit
				
					
						4247c37300
					
				
			
		
					 7 changed files with 252 additions and 75 deletions
				
			
		|  | @ -69,6 +69,7 @@ package v1 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"slices" | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	actions_model "forgejo.org/models/actions" | 	actions_model "forgejo.org/models/actions" | ||||||
|  | @ -468,6 +469,12 @@ func reqAdmin() func(ctx *context.APIContext) { | ||||||
| // reqRepoWriter user should have a permission to write to a repo, or be a site admin | // reqRepoWriter user should have a permission to write to a repo, or be a site admin | ||||||
| func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { | func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { | ||||||
| 	return func(ctx *context.APIContext) { | 	return func(ctx *context.APIContext) { | ||||||
|  | 		if !slices.ContainsFunc(unitTypes, func(unitType unit.Type) bool { | ||||||
|  | 			return ctx.Repo.Repository.UnitEnabled(ctx, unitType) | ||||||
|  | 		}) { | ||||||
|  | 			ctx.NotFound() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 		if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { | 		if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { | ||||||
| 			ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo") | 			ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo") | ||||||
| 			return | 			return | ||||||
|  | @ -487,6 +494,10 @@ func reqRepoBranchWriter(ctx *context.APIContext) { | ||||||
| // reqRepoReader user should have specific read permission or be a repo admin or a site admin | // reqRepoReader user should have specific read permission or be a repo admin or a site admin | ||||||
| func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { | func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { | ||||||
| 	return func(ctx *context.APIContext) { | 	return func(ctx *context.APIContext) { | ||||||
|  | 		if !ctx.Repo.Repository.UnitEnabled(ctx, unitType) { | ||||||
|  | 			ctx.NotFound() | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
| 		if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { | 		if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { | ||||||
| 			ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") | 			ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") | ||||||
| 			return | 			return | ||||||
|  | @ -744,6 +755,26 @@ func mustEnableIssuesOrPulls(ctx *context.APIContext) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func mustEnableLocalIssuesIfIsIssue(ctx *context.APIContext) { | ||||||
|  | 	if ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeIssues) { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if issues_model.IsErrIssueNotExist(err) { | ||||||
|  | 			ctx.NotFound() | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) | ||||||
|  | 		} | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if !issue.IsPull { | ||||||
|  | 		ctx.NotFound() | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func mustEnableWiki(ctx *context.APIContext) { | func mustEnableWiki(ctx *context.APIContext) { | ||||||
| 	if !(ctx.Repo.CanRead(unit.TypeWiki)) { | 	if !(ctx.Repo.CanRead(unit.TypeWiki)) { | ||||||
| 		ctx.NotFound() | 		ctx.NotFound() | ||||||
|  | @ -1426,7 +1457,7 @@ func Routes() *web.Route { | ||||||
| 						m.Group("/comments", func() { | 						m.Group("/comments", func() { | ||||||
| 							m.Combo("").Get(repo.ListIssueComments). | 							m.Combo("").Get(repo.ListIssueComments). | ||||||
| 								Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) | 								Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) | ||||||
| 							m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). | 							m.Combo("/{id}", reqToken(), commentAssignment(":id")).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). | ||||||
| 								Delete(repo.DeleteIssueCommentDeprecated) | 								Delete(repo.DeleteIssueCommentDeprecated) | ||||||
| 						}) | 						}) | ||||||
| 						m.Get("/timeline", repo.ListIssueCommentsAndTimeline) | 						m.Get("/timeline", repo.ListIssueCommentsAndTimeline) | ||||||
|  | @ -1483,7 +1514,7 @@ func Routes() *web.Route { | ||||||
| 								Delete(reqToken(), reqAdmin(), repo.UnpinIssue) | 								Delete(reqToken(), reqAdmin(), repo.UnpinIssue) | ||||||
| 							m.Patch("/{position}", reqToken(), reqAdmin(), repo.MoveIssuePin) | 							m.Patch("/{position}", reqToken(), reqAdmin(), repo.MoveIssuePin) | ||||||
| 						}) | 						}) | ||||||
| 					}) | 					}, mustEnableLocalIssuesIfIsIssue) | ||||||
| 				}, mustEnableIssuesOrPulls) | 				}, mustEnableIssuesOrPulls) | ||||||
| 				m.Group("/labels", func() { | 				m.Group("/labels", func() { | ||||||
| 					m.Combo("").Get(repo.ListLabels). | 					m.Combo("").Get(repo.ListLabels). | ||||||
|  |  | ||||||
|  | @ -6,7 +6,6 @@ package integration | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" |  | ||||||
| 	"mime/multipart" | 	"mime/multipart" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | @ -110,17 +109,11 @@ func TestAPICreateCommentAttachment(t *testing.T) { | ||||||
| 	body := &bytes.Buffer{} | 	body := &bytes.Buffer{} | ||||||
| 
 | 
 | ||||||
| 	// Setup multi-part | 	// Setup multi-part | ||||||
| 	writer := multipart.NewWriter(body) | 	contentType := tests.WriteImageBody(t, buff, filename, body) | ||||||
| 	part, err := writer.CreateFormFile("attachment", filename) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	_, err = io.Copy(part, &buff) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	err = writer.Close() |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 	req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID), body). | 	req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID), body). | ||||||
| 		AddTokenAuth(token). | 		AddTokenAuth(token). | ||||||
| 		SetHeader("Content-Type", writer.FormDataContentType()) | 		SetHeader("Content-Type", contentType) | ||||||
| 	resp := session.MakeRequest(t, req, http.StatusCreated) | 	resp := session.MakeRequest(t, req, http.StatusCreated) | ||||||
| 
 | 
 | ||||||
| 	apiAttachment := new(api.Attachment) | 	apiAttachment := new(api.Attachment) | ||||||
|  | @ -150,16 +143,10 @@ func TestAPICreateCommentAttachmentAutoDate(t *testing.T) { | ||||||
| 		defer tests.PrintCurrentTest(t)() | 		defer tests.PrintCurrentTest(t)() | ||||||
| 
 | 
 | ||||||
| 		// Setup multi-part | 		// Setup multi-part | ||||||
| 		writer := multipart.NewWriter(body) | 		contentType := tests.WriteImageBody(t, buff, filename, body) | ||||||
| 		part, err := writer.CreateFormFile("attachment", filename) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		_, err = io.Copy(part, &buff) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		err = writer.Close() |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 		req := NewRequestWithBody(t, "POST", urlStr, body).AddTokenAuth(token) | 		req := NewRequestWithBody(t, "POST", urlStr, body).AddTokenAuth(token) | ||||||
| 		req.Header.Add("Content-Type", writer.FormDataContentType()) | 		req.Header.Add("Content-Type", contentType) | ||||||
| 		resp := session.MakeRequest(t, req, http.StatusCreated) | 		resp := session.MakeRequest(t, req, http.StatusCreated) | ||||||
| 		apiAttachment := new(api.Attachment) | 		apiAttachment := new(api.Attachment) | ||||||
| 		DecodeJSON(t, resp, &apiAttachment) | 		DecodeJSON(t, resp, &apiAttachment) | ||||||
|  | @ -181,16 +168,10 @@ func TestAPICreateCommentAttachmentAutoDate(t *testing.T) { | ||||||
| 		urlStr += fmt.Sprintf("?updated_at=%s", updatedAt.UTC().Format(time.RFC3339)) | 		urlStr += fmt.Sprintf("?updated_at=%s", updatedAt.UTC().Format(time.RFC3339)) | ||||||
| 
 | 
 | ||||||
| 		// Setup multi-part | 		// Setup multi-part | ||||||
| 		writer := multipart.NewWriter(body) | 		contentType := tests.WriteImageBody(t, buff, filename, body) | ||||||
| 		part, err := writer.CreateFormFile("attachment", filename) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		_, err = io.Copy(part, &buff) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		err = writer.Close() |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 		req := NewRequestWithBody(t, "POST", urlStr, body).AddTokenAuth(token) | 		req := NewRequestWithBody(t, "POST", urlStr, body).AddTokenAuth(token) | ||||||
| 		req.Header.Add("Content-Type", writer.FormDataContentType()) | 		req.Header.Add("Content-Type", contentType) | ||||||
| 		resp := session.MakeRequest(t, req, http.StatusCreated) | 		resp := session.MakeRequest(t, req, http.StatusCreated) | ||||||
| 		apiAttachment := new(api.Attachment) | 		apiAttachment := new(api.Attachment) | ||||||
| 		DecodeJSON(t, resp, &apiAttachment) | 		DecodeJSON(t, resp, &apiAttachment) | ||||||
|  |  | ||||||
|  | @ -6,7 +6,6 @@ package integration | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" |  | ||||||
| 	"mime/multipart" | 	"mime/multipart" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | @ -79,17 +78,11 @@ func TestAPICreateIssueAttachment(t *testing.T) { | ||||||
| 	body := &bytes.Buffer{} | 	body := &bytes.Buffer{} | ||||||
| 
 | 
 | ||||||
| 	// Setup multi-part | 	// Setup multi-part | ||||||
| 	writer := multipart.NewWriter(body) | 	contentType := tests.WriteImageBody(t, buff, filename, body) | ||||||
| 	part, err := writer.CreateFormFile("attachment", filename) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	_, err = io.Copy(part, &buff) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	err = writer.Close() |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 	req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body). | 	req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body). | ||||||
| 		AddTokenAuth(token) | 		AddTokenAuth(token) | ||||||
| 	req.Header.Add("Content-Type", writer.FormDataContentType()) | 	req.Header.Add("Content-Type", contentType) | ||||||
| 	resp := session.MakeRequest(t, req, http.StatusCreated) | 	resp := session.MakeRequest(t, req, http.StatusCreated) | ||||||
| 
 | 
 | ||||||
| 	apiAttachment := new(api.Attachment) | 	apiAttachment := new(api.Attachment) | ||||||
|  | @ -118,16 +111,10 @@ func TestAPICreateIssueAttachmentAutoDate(t *testing.T) { | ||||||
| 		defer tests.PrintCurrentTest(t)() | 		defer tests.PrintCurrentTest(t)() | ||||||
| 
 | 
 | ||||||
| 		// Setup multi-part | 		// Setup multi-part | ||||||
| 		writer := multipart.NewWriter(body) | 		contentType := tests.WriteImageBody(t, buff, filename, body) | ||||||
| 		part, err := writer.CreateFormFile("attachment", filename) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		_, err = io.Copy(part, &buff) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		err = writer.Close() |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 		req := NewRequestWithBody(t, "POST", urlStr, body).AddTokenAuth(token) | 		req := NewRequestWithBody(t, "POST", urlStr, body).AddTokenAuth(token) | ||||||
| 		req.Header.Add("Content-Type", writer.FormDataContentType()) | 		req.Header.Add("Content-Type", contentType) | ||||||
| 		resp := session.MakeRequest(t, req, http.StatusCreated) | 		resp := session.MakeRequest(t, req, http.StatusCreated) | ||||||
| 
 | 
 | ||||||
| 		apiAttachment := new(api.Attachment) | 		apiAttachment := new(api.Attachment) | ||||||
|  | @ -150,16 +137,10 @@ func TestAPICreateIssueAttachmentAutoDate(t *testing.T) { | ||||||
| 		urlStr += fmt.Sprintf("?updated_at=%s", updatedAt.UTC().Format(time.RFC3339)) | 		urlStr += fmt.Sprintf("?updated_at=%s", updatedAt.UTC().Format(time.RFC3339)) | ||||||
| 
 | 
 | ||||||
| 		// Setup multi-part | 		// Setup multi-part | ||||||
| 		writer := multipart.NewWriter(body) | 		contentType := tests.WriteImageBody(t, buff, filename, body) | ||||||
| 		part, err := writer.CreateFormFile("attachment", filename) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		_, err = io.Copy(part, &buff) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		err = writer.Close() |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 		req := NewRequestWithBody(t, "POST", urlStr, body).AddTokenAuth(token) | 		req := NewRequestWithBody(t, "POST", urlStr, body).AddTokenAuth(token) | ||||||
| 		req.Header.Add("Content-Type", writer.FormDataContentType()) | 		req.Header.Add("Content-Type", contentType) | ||||||
| 		resp := session.MakeRequest(t, req, http.StatusCreated) | 		resp := session.MakeRequest(t, req, http.StatusCreated) | ||||||
| 
 | 
 | ||||||
| 		apiAttachment := new(api.Attachment) | 		apiAttachment := new(api.Attachment) | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ | ||||||
| package integration | package integration | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | @ -16,14 +17,18 @@ import ( | ||||||
| 	"forgejo.org/models/db" | 	"forgejo.org/models/db" | ||||||
| 	issues_model "forgejo.org/models/issues" | 	issues_model "forgejo.org/models/issues" | ||||||
| 	repo_model "forgejo.org/models/repo" | 	repo_model "forgejo.org/models/repo" | ||||||
|  | 	"forgejo.org/models/unit" | ||||||
| 	"forgejo.org/models/unittest" | 	"forgejo.org/models/unittest" | ||||||
| 	user_model "forgejo.org/models/user" | 	user_model "forgejo.org/models/user" | ||||||
|  | 	"forgejo.org/modules/optional" | ||||||
| 	"forgejo.org/modules/setting" | 	"forgejo.org/modules/setting" | ||||||
| 	api "forgejo.org/modules/structs" | 	api "forgejo.org/modules/structs" | ||||||
| 	"forgejo.org/tests" | 	"forgejo.org/tests" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/google/uuid" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
|  | 	"xorm.io/xorm/convert" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestAPIListIssues(t *testing.T) { | func TestAPIListIssues(t *testing.T) { | ||||||
|  | @ -620,3 +625,185 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { | ||||||
| 	DecodeJSON(t, resp, &apiIssues) | 	DecodeJSON(t, resp, &apiIssues) | ||||||
| 	assert.Len(t, apiIssues, 2) | 	assert.Len(t, apiIssues, 2) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestAPIInternalAndExternalIssueTracker(t *testing.T) { | ||||||
|  | 	defer tests.PrepareTestEnv(t)() | ||||||
|  | 
 | ||||||
|  | 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) | ||||||
|  | 	otherUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) | ||||||
|  | 	token := getUserToken(t, user.Name, auth_model.AccessTokenScopeAll) | ||||||
|  | 
 | ||||||
|  | 	internalIssueRepo, _, reset := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ | ||||||
|  | 		Name:          optional.Some("internal-issues"), | ||||||
|  | 		EnabledUnits:  optional.Some([]unit.Type{unit.TypeIssues}), | ||||||
|  | 		DisabledUnits: optional.Some([]unit.Type{unit.TypeExternalTracker}), | ||||||
|  | 		UnitConfig: optional.Some(map[unit.Type]convert.Conversion{ | ||||||
|  | 			unit.TypeIssues: &repo_model.IssuesConfig{ | ||||||
|  | 				EnableTimetracker:  true, | ||||||
|  | 				EnableDependencies: true, | ||||||
|  | 			}, | ||||||
|  | 		}), | ||||||
|  | 	}) | ||||||
|  | 	defer reset() | ||||||
|  | 
 | ||||||
|  | 	externalIssueRepo, _, reset := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ | ||||||
|  | 		Name:          optional.Some("external-issues"), | ||||||
|  | 		EnabledUnits:  optional.Some([]unit.Type{unit.TypeExternalTracker}), | ||||||
|  | 		DisabledUnits: optional.Some([]unit.Type{unit.TypeIssues}), | ||||||
|  | 	}) | ||||||
|  | 	defer reset() | ||||||
|  | 
 | ||||||
|  | 	disabledIssueRepo, _, reset := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{ | ||||||
|  | 		Name:          optional.Some("disabled-issues"), | ||||||
|  | 		DisabledUnits: optional.Some([]unit.Type{unit.TypeIssues, unit.TypeExternalTracker}), | ||||||
|  | 	}) | ||||||
|  | 	defer reset() | ||||||
|  | 
 | ||||||
|  | 	runTest := func(t *testing.T, repo *repo_model.Repository, requestAllowed bool) { | ||||||
|  | 		t.Helper() | ||||||
|  | 		getPath := func(path string, args ...any) string { | ||||||
|  | 			suffix := path | ||||||
|  | 			if len(args) > 0 { | ||||||
|  | 				suffix = fmt.Sprintf(path, args...) | ||||||
|  | 			} | ||||||
|  | 			return fmt.Sprintf("/api/v1/repos/%s/%s/issues%s", repo.OwnerName, repo.Name, suffix) | ||||||
|  | 		} | ||||||
|  | 		getStatus := func(allowStatus int) int { | ||||||
|  | 			if requestAllowed { | ||||||
|  | 				return allowStatus | ||||||
|  | 			} | ||||||
|  | 			return http.StatusNotFound | ||||||
|  | 		} | ||||||
|  | 		okStatus := getStatus(http.StatusOK) | ||||||
|  | 		createdStatus := getStatus(http.StatusCreated) | ||||||
|  | 		noContentStatus := getStatus(http.StatusNoContent) | ||||||
|  | 
 | ||||||
|  | 		// setup | ||||||
|  | 		issue := createIssue(t, user, repo, "normal issue", uuid.NewString()) | ||||||
|  | 		deleteIssue := createIssue(t, user, repo, "delete this issue", uuid.NewString()) | ||||||
|  | 		dependencyIssue := createIssue(t, user, repo, "depend on this issue", uuid.NewString()) | ||||||
|  | 		blocksIssue := createIssue(t, user, repo, "depend on this issue", uuid.NewString()) | ||||||
|  | 
 | ||||||
|  | 		// issues | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/")).AddTokenAuth(token), http.StatusOK) | ||||||
|  | 		MakeRequest(t, NewRequestWithValues(t, "POST", getPath("/"), map[string]string{"title": uuid.NewString()}).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithValues(t, "PATCH", getPath("/%d", deleteIssue.Index), map[string]string{"title": uuid.NewString()}).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d", deleteIssue.Index)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 
 | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/pinned")).AddTokenAuth(token), okStatus) | ||||||
|  | 
 | ||||||
|  | 		// comments | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/comments")).AddTokenAuth(token), http.StatusOK) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/comments", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		resp := MakeRequest(t, NewRequestWithValues(t, "POST", getPath("/%d/comments", issue.Index), map[string]string{"body": uuid.NewString()}).AddTokenAuth(token), createdStatus) | ||||||
|  | 		var comment api.Comment | ||||||
|  | 		DecodeJSON(t, resp, &comment) | ||||||
|  | 		resp = MakeRequest(t, NewRequestWithValues(t, "POST", getPath("/%d/comments", issue.Index), map[string]string{"body": uuid.NewString()}).AddTokenAuth(token), createdStatus) | ||||||
|  | 		var commentTwo api.Comment | ||||||
|  | 		DecodeJSON(t, resp, &commentTwo) | ||||||
|  | 		resp = MakeRequest(t, NewRequestWithValues(t, "POST", getPath("/%d/comments", issue.Index), map[string]string{"body": uuid.NewString()}).AddTokenAuth(token), createdStatus) | ||||||
|  | 		var commentThree api.Comment | ||||||
|  | 		DecodeJSON(t, resp, &commentThree) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/comments/%d", commentTwo.ID)).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithValues(t, "PATCH", getPath("/comments/%d", commentTwo.ID), map[string]string{"body": uuid.NewString()}).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/comments/%d", commentTwo.ID)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithValues(t, "PATCH", getPath("/%d/comments/%d", issue.Index, commentThree.ID), map[string]string{"body": uuid.NewString()}).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d/comments/%d", issue.Index, commentThree.ID)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 		// comment-reactions | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/comments/%d/reactions", comment.ID)).AddTokenAuth(token), okStatus) | ||||||
|  | 		reaction := &api.EditReactionOption{Reaction: "+1"} | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "POST", getPath("/comments/%d/reactions", comment.ID), reaction).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "DELETE", getPath("/comments/%d/reactions", comment.ID), reaction).AddTokenAuth(token), okStatus) | ||||||
|  | 		// comment-assets | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/comments/%d/assets", comment.ID)).AddTokenAuth(token), okStatus) | ||||||
|  | 		body := &bytes.Buffer{} | ||||||
|  | 		contentType := tests.WriteImageBody(t, generateImg(), "image.png", body) | ||||||
|  | 		req := NewRequestWithBody(t, "POST", getPath("/comments/%d/assets", comment.ID), bytes.NewReader(body.Bytes())).AddTokenAuth(token) | ||||||
|  | 		req.Header.Add("Content-Type", contentType) | ||||||
|  | 		resp = MakeRequest(t, req, createdStatus) | ||||||
|  | 		var commentAttachment api.Attachment | ||||||
|  | 		DecodeJSON(t, resp, &commentAttachment) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/comments/%d/assets/%d", comment.ID, commentAttachment.ID)).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithValues(t, "PATCH", getPath("/comments/%d/assets/%d", comment.ID, commentAttachment.ID), map[string]string{"name": uuid.NewString()}).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/comments/%d/assets/%d", comment.ID, commentAttachment.ID)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 
 | ||||||
|  | 		// timeline | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/timeline", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 
 | ||||||
|  | 		// labels | ||||||
|  | 		labelName := uuid.NewString() | ||||||
|  | 		labelCreateURL := fmt.Sprintf("/api/v1/repos/%s/%s/labels", repo.OwnerName, repo.Name) | ||||||
|  | 		resp = MakeRequest(t, NewRequestWithValues(t, "POST", labelCreateURL, map[string]string{"name": labelName, "color": "#333333"}).AddTokenAuth(token), http.StatusCreated) | ||||||
|  | 		var label api.Label | ||||||
|  | 		DecodeJSON(t, resp, &label) | ||||||
|  | 
 | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/labels", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "POST", getPath("/%d/labels", issue.Index), api.IssueLabelsOption{Labels: []any{labelName}}).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "PUT", getPath("/%d/labels", issue.Index), api.IssueLabelsOption{Labels: []any{labelName}}).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d/labels", issue.Index)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d/labels/%d", issue.Index, label.ID)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 
 | ||||||
|  | 		// times | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/times", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		resp = MakeRequest(t, NewRequestWithJSON(t, "POST", getPath("/%d/times", issue.Index), api.AddTimeOption{Time: 60}).AddTokenAuth(token), okStatus) | ||||||
|  | 		var trackedTime api.TrackedTime | ||||||
|  | 		DecodeJSON(t, resp, &trackedTime) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d/times", issue.Index)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 		resp = MakeRequest(t, NewRequestWithJSON(t, "POST", getPath("/%d/times", issue.Index), api.AddTimeOption{Time: 75}).AddTokenAuth(token), okStatus) | ||||||
|  | 		DecodeJSON(t, resp, &trackedTime) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d/times/%d", issue.Index, trackedTime.ID)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 
 | ||||||
|  | 		// deadline | ||||||
|  | 		MakeRequest(t, NewRequestWithValues(t, "POST", getPath("/%d/deadline", issue.Index), map[string]string{"due_date": "2022-04-06T00:00:00.000Z"}).AddTokenAuth(token), createdStatus) | ||||||
|  | 
 | ||||||
|  | 		// stopwatch | ||||||
|  | 		MakeRequest(t, NewRequest(t, "POST", getPath("/%d/stopwatch/start", issue.Index)).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "POST", getPath("/%d/stopwatch/stop", issue.Index)).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "POST", getPath("/%d/stopwatch/start", issue.Index)).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d/stopwatch/delete", issue.Index)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 
 | ||||||
|  | 		// subscriptions | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/subscriptions", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/subscriptions/check", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "PUT", getPath("/%d/subscriptions/%s", issue.Index, otherUser.Name)).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d/subscriptions/%s", issue.Index, otherUser.Name)).AddTokenAuth(token), createdStatus) | ||||||
|  | 
 | ||||||
|  | 		// reactions | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/reactions", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "POST", getPath("/%d/reactions", issue.Index), api.EditReactionOption{Reaction: "+1"}).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "DELETE", getPath("/%d/reactions", issue.Index), api.EditReactionOption{Reaction: "+1"}).AddTokenAuth(token), okStatus) | ||||||
|  | 
 | ||||||
|  | 		// assets | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/assets", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		req = NewRequestWithBody(t, "POST", getPath("/%d/assets", issue.Index), bytes.NewReader(body.Bytes())).AddTokenAuth(token) | ||||||
|  | 		req.Header.Add("Content-Type", contentType) | ||||||
|  | 		resp = MakeRequest(t, req, createdStatus) | ||||||
|  | 		var attachment api.Attachment | ||||||
|  | 		DecodeJSON(t, resp, &attachment) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/assets/%d", issue.Index, attachment.ID)).AddTokenAuth(token), okStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithValues(t, "PATCH", getPath("/%d/assets/%d", issue.Index, attachment.ID), map[string]string{"name": uuid.NewString()}).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequest(t, "DELETE", getPath("/%d/assets/%d", issue.Index, attachment.ID)).AddTokenAuth(token), noContentStatus) | ||||||
|  | 
 | ||||||
|  | 		// dependencies | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/dependencies", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		dependencyMeta := api.IssueMeta{Index: dependencyIssue.Index, Owner: dependencyIssue.Repo.OwnerName, Name: dependencyIssue.Repo.Name} | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "POST", getPath("/%d/dependencies", issue.Index), dependencyMeta).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "DELETE", getPath("/%d/dependencies", issue.Index), dependencyMeta).AddTokenAuth(token), createdStatus) | ||||||
|  | 
 | ||||||
|  | 		// blocks | ||||||
|  | 		MakeRequest(t, NewRequest(t, "GET", getPath("/%d/blocks", issue.Index)).AddTokenAuth(token), okStatus) | ||||||
|  | 		blockMeta := api.IssueMeta{Index: blocksIssue.Index, Owner: blocksIssue.Repo.OwnerName, Name: blocksIssue.Repo.Name} | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "POST", getPath("/%d/blocks", issue.Index), blockMeta).AddTokenAuth(token), createdStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "DELETE", getPath("/%d/blocks", issue.Index), blockMeta).AddTokenAuth(token), createdStatus) | ||||||
|  | 
 | ||||||
|  | 		// pin | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "POST", getPath("/%d/pin", issue.Index), blockMeta).AddTokenAuth(token), noContentStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "PATCH", getPath("/%d/pin/1", issue.Index), blockMeta).AddTokenAuth(token), noContentStatus) | ||||||
|  | 		MakeRequest(t, NewRequestWithJSON(t, "DELETE", getPath("/%d/pin", issue.Index), blockMeta).AddTokenAuth(token), noContentStatus) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	runTest(t, internalIssueRepo, true) | ||||||
|  | 	runTest(t, externalIssueRepo, false) | ||||||
|  | 	runTest(t, disabledIssueRepo, false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -6,8 +6,6 @@ package integration | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" |  | ||||||
| 	"mime/multipart" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | @ -319,18 +317,11 @@ func TestAPIUploadAssetRelease(t *testing.T) { | ||||||
| 		defer tests.PrintCurrentTest(t)() | 		defer tests.PrintCurrentTest(t)() | ||||||
| 
 | 
 | ||||||
| 		body := &bytes.Buffer{} | 		body := &bytes.Buffer{} | ||||||
| 
 | 		contentType := tests.WriteImageBody(t, buff, filename, body) | ||||||
| 		writer := multipart.NewWriter(body) |  | ||||||
| 		part, err := writer.CreateFormFile("attachment", filename) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		_, err = io.Copy(part, bytes.NewReader(buff.Bytes())) |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 		err = writer.Close() |  | ||||||
| 		require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(body.Bytes())). | 		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(body.Bytes())). | ||||||
| 			AddTokenAuth(token). | 			AddTokenAuth(token). | ||||||
| 			SetHeader("Content-Type", writer.FormDataContentType()) | 			SetHeader("Content-Type", contentType) | ||||||
| 		resp := MakeRequest(t, req, http.StatusCreated) | 		resp := MakeRequest(t, req, http.StatusCreated) | ||||||
| 
 | 
 | ||||||
| 		var attachment *api.Attachment | 		var attachment *api.Attachment | ||||||
|  | @ -341,7 +332,7 @@ func TestAPIUploadAssetRelease(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 		req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=test-asset", bytes.NewReader(body.Bytes())). | 		req = NewRequestWithBody(t, http.MethodPost, assetURL+"?name=test-asset", bytes.NewReader(body.Bytes())). | ||||||
| 			AddTokenAuth(token). | 			AddTokenAuth(token). | ||||||
| 			SetHeader("Content-Type", writer.FormDataContentType()) | 			SetHeader("Content-Type", contentType) | ||||||
| 		resp = MakeRequest(t, req, http.StatusCreated) | 		resp = MakeRequest(t, req, http.StatusCreated) | ||||||
| 
 | 
 | ||||||
| 		var attachment2 *api.Attachment | 		var attachment2 *api.Attachment | ||||||
|  | @ -467,18 +458,11 @@ func TestAPIDuplicateAssetRelease(t *testing.T) { | ||||||
| 	filename := "image.png" | 	filename := "image.png" | ||||||
| 	buff := generateImg() | 	buff := generateImg() | ||||||
| 	body := &bytes.Buffer{} | 	body := &bytes.Buffer{} | ||||||
| 
 | 	contentType := tests.WriteImageBody(t, buff, filename, body) | ||||||
| 	writer := multipart.NewWriter(body) |  | ||||||
| 	part, err := writer.CreateFormFile("attachment", filename) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	_, err = io.Copy(part, &buff) |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 	err = writer.Close() |  | ||||||
| 	require.NoError(t, err) |  | ||||||
| 
 | 
 | ||||||
| 	req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&external_url=https%%3A%%2F%%2Fforgejo.org%%2F", owner.Name, repo.Name, r.ID), body). | 	req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&external_url=https%%3A%%2F%%2Fforgejo.org%%2F", owner.Name, repo.Name, r.ID), body). | ||||||
| 		AddTokenAuth(token) | 		AddTokenAuth(token) | ||||||
| 	req.Header.Add("Content-Type", writer.FormDataContentType()) | 	req.Header.Add("Content-Type", contentType) | ||||||
| 	MakeRequest(t, req, http.StatusBadRequest) | 	MakeRequest(t, req, http.StatusBadRequest) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -36,8 +36,8 @@ func TestBadges(t *testing.T) { | ||||||
| 			owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | 			owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||||
| 
 | 
 | ||||||
| 			repo, _, f := tests.CreateDeclarativeRepo(t, owner, "", | 			repo, _, f := tests.CreateDeclarativeRepo(t, owner, "", | ||||||
| 				[]unit_model.Type{unit_model.TypeActions}, | 				[]unit_model.Type{unit_model.TypeActions, unit_model.TypeReleases}, | ||||||
| 				[]unit_model.Type{unit_model.TypeIssues, unit_model.TypePullRequests, unit_model.TypeReleases}, | 				[]unit_model.Type{unit_model.TypeIssues, unit_model.TypePullRequests}, | ||||||
| 				[]*files_service.ChangeRepoFile{ | 				[]*files_service.ChangeRepoFile{ | ||||||
| 					{ | 					{ | ||||||
| 						Operation:     "create", | 						Operation:     "create", | ||||||
|  |  | ||||||
|  | @ -5,9 +5,12 @@ | ||||||
| package tests | package tests | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
| 	"database/sql" | 	"database/sql" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"mime/multipart" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path" | 	"path" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | @ -524,3 +527,13 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en | ||||||
| 
 | 
 | ||||||
| 	return CreateDeclarativeRepoWithOptions(t, owner, opts) | 	return CreateDeclarativeRepoWithOptions(t, owner, opts) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func WriteImageBody(t *testing.T, buff bytes.Buffer, filename string, body *bytes.Buffer) string { | ||||||
|  | 	writer := multipart.NewWriter(body) | ||||||
|  | 	defer writer.Close() | ||||||
|  | 	part, err := writer.CreateFormFile("attachment", filename) | ||||||
|  | 	require.NoError(t, err) | ||||||
|  | 	_, err = io.Copy(part, &buff) | ||||||
|  | 	require.NoError(t, err) | ||||||
|  | 	return writer.FormDataContentType() | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue