[v12.0/forgejo] fix(api): deactivate issue api for disabled or external issue-tracker (#9147)
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/8829 - When the issue unit is disabled for a repository, don't allow issue related APIs. - Added integration tests. - Resolves #8408 Co-authored-by: zokki <zokki.softwareschmiede@gmail.com> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9147 Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org> Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
This commit is contained in:
		
					parent
					
						
							
								19d920931d
							
						
					
				
			
			
				commit
				
					
						88e18dea4a
					
				
			
		
					 7 changed files with 252 additions and 75 deletions
				
			
		| 
						 | 
				
			
			@ -69,6 +69,7 @@ package v1
 | 
			
		|||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
func reqRepoWriter(unitTypes ...unit.Type) 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() {
 | 
			
		||||
			ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo")
 | 
			
		||||
			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
 | 
			
		||||
func reqRepoReader(unitType unit.Type) 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() {
 | 
			
		||||
			ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin")
 | 
			
		||||
			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) {
 | 
			
		||||
	if !(ctx.Repo.CanRead(unit.TypeWiki)) {
 | 
			
		||||
		ctx.NotFound()
 | 
			
		||||
| 
						 | 
				
			
			@ -1423,7 +1454,7 @@ func Routes() *web.Route {
 | 
			
		|||
						m.Group("/comments", func() {
 | 
			
		||||
							m.Combo("").Get(repo.ListIssueComments).
 | 
			
		||||
								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)
 | 
			
		||||
						})
 | 
			
		||||
						m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
 | 
			
		||||
| 
						 | 
				
			
			@ -1480,7 +1511,7 @@ func Routes() *web.Route {
 | 
			
		|||
								Delete(reqToken(), reqAdmin(), repo.UnpinIssue)
 | 
			
		||||
							m.Patch("/{position}", reqToken(), reqAdmin(), repo.MoveIssuePin)
 | 
			
		||||
						})
 | 
			
		||||
					})
 | 
			
		||||
					}, mustEnableLocalIssuesIfIsIssue)
 | 
			
		||||
				}, mustEnableIssuesOrPulls)
 | 
			
		||||
				m.Group("/labels", func() {
 | 
			
		||||
					m.Combo("").Get(repo.ListLabels).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ package integration
 | 
			
		|||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"mime/multipart"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"testing"
 | 
			
		||||
| 
						 | 
				
			
			@ -110,17 +109,11 @@ func TestAPICreateCommentAttachment(t *testing.T) {
 | 
			
		|||
	body := &bytes.Buffer{}
 | 
			
		||||
 | 
			
		||||
	// Setup multi-part
 | 
			
		||||
	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)
 | 
			
		||||
	contentType := tests.WriteImageBody(t, buff, filename, 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).
 | 
			
		||||
		SetHeader("Content-Type", writer.FormDataContentType())
 | 
			
		||||
		SetHeader("Content-Type", contentType)
 | 
			
		||||
	resp := session.MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	apiAttachment := new(api.Attachment)
 | 
			
		||||
| 
						 | 
				
			
			@ -150,16 +143,10 @@ func TestAPICreateCommentAttachmentAutoDate(t *testing.T) {
 | 
			
		|||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
		// Setup multi-part
 | 
			
		||||
		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)
 | 
			
		||||
		contentType := tests.WriteImageBody(t, buff, filename, body)
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
		apiAttachment := new(api.Attachment)
 | 
			
		||||
		DecodeJSON(t, resp, &apiAttachment)
 | 
			
		||||
| 
						 | 
				
			
			@ -181,16 +168,10 @@ func TestAPICreateCommentAttachmentAutoDate(t *testing.T) {
 | 
			
		|||
		urlStr += fmt.Sprintf("?updated_at=%s", updatedAt.UTC().Format(time.RFC3339))
 | 
			
		||||
 | 
			
		||||
		// Setup multi-part
 | 
			
		||||
		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)
 | 
			
		||||
		contentType := tests.WriteImageBody(t, buff, filename, body)
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
		apiAttachment := new(api.Attachment)
 | 
			
		||||
		DecodeJSON(t, resp, &apiAttachment)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ package integration
 | 
			
		|||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"mime/multipart"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"testing"
 | 
			
		||||
| 
						 | 
				
			
			@ -79,17 +78,11 @@ func TestAPICreateIssueAttachment(t *testing.T) {
 | 
			
		|||
	body := &bytes.Buffer{}
 | 
			
		||||
 | 
			
		||||
	// Setup multi-part
 | 
			
		||||
	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)
 | 
			
		||||
	contentType := tests.WriteImageBody(t, buff, filename, body)
 | 
			
		||||
 | 
			
		||||
	req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body).
 | 
			
		||||
		AddTokenAuth(token)
 | 
			
		||||
	req.Header.Add("Content-Type", writer.FormDataContentType())
 | 
			
		||||
	req.Header.Add("Content-Type", contentType)
 | 
			
		||||
	resp := session.MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
	apiAttachment := new(api.Attachment)
 | 
			
		||||
| 
						 | 
				
			
			@ -118,16 +111,10 @@ func TestAPICreateIssueAttachmentAutoDate(t *testing.T) {
 | 
			
		|||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
		// Setup multi-part
 | 
			
		||||
		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)
 | 
			
		||||
		contentType := tests.WriteImageBody(t, buff, filename, body)
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
		apiAttachment := new(api.Attachment)
 | 
			
		||||
| 
						 | 
				
			
			@ -150,16 +137,10 @@ func TestAPICreateIssueAttachmentAutoDate(t *testing.T) {
 | 
			
		|||
		urlStr += fmt.Sprintf("?updated_at=%s", updatedAt.UTC().Format(time.RFC3339))
 | 
			
		||||
 | 
			
		||||
		// Setup multi-part
 | 
			
		||||
		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)
 | 
			
		||||
		contentType := tests.WriteImageBody(t, buff, filename, body)
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
		apiAttachment := new(api.Attachment)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,6 +4,7 @@
 | 
			
		|||
package integration
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
| 
						 | 
				
			
			@ -16,14 +17,18 @@ import (
 | 
			
		|||
	"forgejo.org/models/db"
 | 
			
		||||
	issues_model "forgejo.org/models/issues"
 | 
			
		||||
	repo_model "forgejo.org/models/repo"
 | 
			
		||||
	"forgejo.org/models/unit"
 | 
			
		||||
	"forgejo.org/models/unittest"
 | 
			
		||||
	user_model "forgejo.org/models/user"
 | 
			
		||||
	"forgejo.org/modules/optional"
 | 
			
		||||
	"forgejo.org/modules/setting"
 | 
			
		||||
	api "forgejo.org/modules/structs"
 | 
			
		||||
	"forgejo.org/tests"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
	"xorm.io/xorm/convert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAPIListIssues(t *testing.T) {
 | 
			
		||||
| 
						 | 
				
			
			@ -620,3 +625,185 @@ func TestAPISearchIssuesWithLabels(t *testing.T) {
 | 
			
		|||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	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 (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"mime/multipart"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
| 
						 | 
				
			
			@ -319,18 +317,11 @@ func TestAPIUploadAssetRelease(t *testing.T) {
 | 
			
		|||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
 | 
			
		||||
		body := &bytes.Buffer{}
 | 
			
		||||
 | 
			
		||||
		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)
 | 
			
		||||
		contentType := tests.WriteImageBody(t, buff, filename, body)
 | 
			
		||||
 | 
			
		||||
		req := NewRequestWithBody(t, http.MethodPost, assetURL, bytes.NewReader(body.Bytes())).
 | 
			
		||||
			AddTokenAuth(token).
 | 
			
		||||
			SetHeader("Content-Type", writer.FormDataContentType())
 | 
			
		||||
			SetHeader("Content-Type", contentType)
 | 
			
		||||
		resp := MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
		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())).
 | 
			
		||||
			AddTokenAuth(token).
 | 
			
		||||
			SetHeader("Content-Type", writer.FormDataContentType())
 | 
			
		||||
			SetHeader("Content-Type", contentType)
 | 
			
		||||
		resp = MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
		var attachment2 *api.Attachment
 | 
			
		||||
| 
						 | 
				
			
			@ -467,18 +458,11 @@ func TestAPIDuplicateAssetRelease(t *testing.T) {
 | 
			
		|||
	filename := "image.png"
 | 
			
		||||
	buff := generateImg()
 | 
			
		||||
	body := &bytes.Buffer{}
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	contentType := tests.WriteImageBody(t, buff, filename, 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)
 | 
			
		||||
	req.Header.Add("Content-Type", writer.FormDataContentType())
 | 
			
		||||
	req.Header.Add("Content-Type", contentType)
 | 
			
		||||
	MakeRequest(t, req, http.StatusBadRequest)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,8 +36,8 @@ func TestBadges(t *testing.T) {
 | 
			
		|||
			owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
			
		||||
 | 
			
		||||
			repo, _, f := tests.CreateDeclarativeRepo(t, owner, "",
 | 
			
		||||
				[]unit_model.Type{unit_model.TypeActions},
 | 
			
		||||
				[]unit_model.Type{unit_model.TypeIssues, unit_model.TypePullRequests, unit_model.TypeReleases},
 | 
			
		||||
				[]unit_model.Type{unit_model.TypeActions, unit_model.TypeReleases},
 | 
			
		||||
				[]unit_model.Type{unit_model.TypeIssues, unit_model.TypePullRequests},
 | 
			
		||||
				[]*files_service.ChangeRepoFile{
 | 
			
		||||
					{
 | 
			
		||||
						Operation:     "create",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,9 +5,12 @@
 | 
			
		|||
package tests
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"mime/multipart"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
| 
						 | 
				
			
			@ -506,3 +509,13 @@ func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, en
 | 
			
		|||
 | 
			
		||||
	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