mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-03-25 21:33:04 -04:00
Repository-specific personal access tokens will allow a user's access tokens to be restricted to accessing zero-or-more specific repositories. Currently they can be configured as "All", or "Public only", and this project will add a third configuration option allowing specific repositories. This PR is part of a series (#11311), and builds on the infrastructure work in #11434. In this PR, repository-specific access tokens are implemented on the universal permission checks performed by the API middleware, affecting ~182 API endpoints that perform permission checks based upon repositories referenced in their API path (eg. `/v1/api/repos/{owner}/{repo}/...`). **Breaking change:** API access with a public-only access token would previously return a `403 Forbidden` error when attempting to access a private repository where the repository is on the API path. As part of incorporating the public-only logic into the centralized permission check, these APIs will now return `404 Not Found` instead, consistent with how repository-specific access tokens, and other permissions checks, are implemented in order to reduce the risk of data probing through error messages. For larger context on the usage and future incoming work, the description of #11311 can be referenced. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes (can be removed for JavaScript changes) - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - As there is no end-user accessibility to create repo-specific access tokens, this functionality will not be accessible to end-users yet. But the breaking change in error APIs for public-only access tokens will be visible to end-users. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Breaking features - [PR](https://codeberg.org/forgejo/forgejo/pulls/11437): <!--number 11437 --><!--line 0 --><!--description aW1wbGVtZW50IHJlcG8tc3BlY2lmaWMgYWNjZXNzIHRva2VucyBicm9hZGx5IGZvciB1bml2ZXJzYWwgQVBJIHBlcm1pc3Npb24gY2hlY2tzLiAgKipCcmVha2luZzoqKiBBUEkgYWNjZXNzIHdpdGggYSBwdWJsaWMtb25seSBhY2Nlc3MgdG9rZW4gd291bGQgcHJldmlvdXNseSByZXR1cm4gYSBgNDAzIEZvcmJpZGRlbmAgZXJyb3Igd2hlbiBhdHRlbXB0aW5nIHRvIGFjY2VzcyBhIHByaXZhdGUgcmVwb3NpdG9yeSB3aGVyZSB0aGUgcmVwb3NpdG9yeSBpcyBvbiB0aGUgQVBJIHBhdGguICBBcyBwYXJ0IG9mIGluY29ycG9yYXRpbmcgdGhlIHB1YmxpYy1vbmx5IGxvZ2ljIGludG8gdGhlIGNlbnRyYWxpemVkIHBlcm1pc3Npb24gY2hlY2ssIHRoZXNlIEFQSXMgd2lsbCBub3cgcmV0dXJuIGA0MDQgTm90IEZvdW5kYCBpbnN0ZWFkLCBjb25zaXN0ZW50IHdpdGggaG93IG1vc3QgcGVybWlzc2lvbiBjaGVja3MgYXJlIGltcGxlbWVudGVkIGluIG9yZGVyIHRvIHJlZHVjZSB0aGUgcmlzayBvZiBkYXRhIHByb2JpbmcgdGhyb3VnaCBlcnJvciBtZXNzYWdlcy4=-->implement repo-specific access tokens broadly for universal API permission checks. **Breaking:** API access with a public-only access token would previously return a `403 Forbidden` error when attempting to access a private repository where the repository is on the API path. As part of incorporating the public-only logic into the centralized permission check, these APIs will now return `404 Not Found` instead, consistent with how most permission checks are implemented in order to reduce the risk of data probing through error messages.<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11437 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
888 lines
39 KiB
Go
888 lines
39 KiB
Go
// Copyright 2017 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
auth_model "forgejo.org/models/auth"
|
|
"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) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
|
|
session := loginUser(t, owner.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
|
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name))
|
|
|
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode()
|
|
resp := MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
var apiIssues []*api.Issue
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}))
|
|
for _, apiIssue := range apiIssues {
|
|
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: apiIssue.ID, RepoID: repo.ID})
|
|
}
|
|
|
|
// test milestone filter
|
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "type": {"all"}, "milestones": {"ignore,milestone1,3,4"}}.Encode()
|
|
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 2) {
|
|
assert.EqualValues(t, 3, apiIssues[0].Milestone.ID)
|
|
assert.EqualValues(t, 1, apiIssues[1].Milestone.ID)
|
|
}
|
|
|
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "created_by": {"user2"}}.Encode()
|
|
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 1) {
|
|
assert.EqualValues(t, 5, apiIssues[0].ID)
|
|
}
|
|
|
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "assigned_by": {"user1"}}.Encode()
|
|
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 1) {
|
|
assert.EqualValues(t, 1, apiIssues[0].ID)
|
|
}
|
|
|
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "mentioned_by": {"user4"}}.Encode()
|
|
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 1) {
|
|
assert.EqualValues(t, 1, apiIssues[0].ID)
|
|
}
|
|
|
|
t.Run("Sort", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
link.RawQuery = url.Values{"token": {token}, "sort": {"oldest"}}.Encode()
|
|
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 4) {
|
|
assert.EqualValues(t, 1, apiIssues[0].ID)
|
|
assert.EqualValues(t, 2, apiIssues[1].ID)
|
|
assert.EqualValues(t, 3, apiIssues[2].ID)
|
|
assert.EqualValues(t, 11, apiIssues[3].ID)
|
|
}
|
|
|
|
link.RawQuery = url.Values{"token": {token}, "sort": {"newest"}}.Encode()
|
|
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 4) {
|
|
assert.EqualValues(t, 11, apiIssues[0].ID)
|
|
assert.EqualValues(t, 3, apiIssues[1].ID)
|
|
assert.EqualValues(t, 2, apiIssues[2].ID)
|
|
assert.EqualValues(t, 1, apiIssues[3].ID)
|
|
}
|
|
|
|
link.RawQuery = url.Values{"token": {token}, "sort": {"recentupdate"}}.Encode()
|
|
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 4) {
|
|
assert.EqualValues(t, 11, apiIssues[0].ID)
|
|
assert.EqualValues(t, 1, apiIssues[1].ID)
|
|
assert.EqualValues(t, 2, apiIssues[2].ID)
|
|
assert.EqualValues(t, 3, apiIssues[3].ID)
|
|
}
|
|
|
|
link.RawQuery = url.Values{"token": {token}, "sort": {"leastupdate"}}.Encode()
|
|
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 4) {
|
|
assert.EqualValues(t, 3, apiIssues[0].ID)
|
|
assert.EqualValues(t, 2, apiIssues[1].ID)
|
|
assert.EqualValues(t, 1, apiIssues[2].ID)
|
|
assert.EqualValues(t, 11, apiIssues[3].ID)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPIListIssuesWithLabels(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6, RepoID: repo.ID})
|
|
orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 4, OrgID: owner.ID})
|
|
|
|
session := loginUser(t, "user1")
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopeWriteIssue)
|
|
|
|
addLabelsURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index)
|
|
req := NewRequestWithJSON(t, "POST", addLabelsURL, &api.IssueLabelsOption{Labels: []any{orgLabel.Name}}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name))
|
|
link.RawQuery = url.Values{"state": {"all"}, "labels": {orgLabel.Name}}.Encode()
|
|
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
var apiIssues []*api.Issue
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
if assert.Len(t, apiIssues, 1) {
|
|
assert.Equal(t, issue.ID, apiIssues[0].ID)
|
|
}
|
|
}
|
|
|
|
func TestAPIListIssuesPublicOnly(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
owner1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID})
|
|
|
|
session := loginUser(t, owner1.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
|
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner1.Name, repo1.Name))
|
|
link.RawQuery = url.Values{"state": {"all"}}.Encode()
|
|
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
|
owner2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo2.OwnerID})
|
|
|
|
session = loginUser(t, owner2.Name)
|
|
token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
|
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner2.Name, repo2.Name))
|
|
link.RawQuery = url.Values{"state": {"all"}}.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
}
|
|
|
|
func TestAPICreateIssue(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
const body, title = "apiTestBody", "apiTestTitle"
|
|
|
|
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
|
beforeNumIssues := repoBefore.NumIssues(t.Context())
|
|
beforeNumClosedIssues := repoBefore.NumClosedIssues(t.Context())
|
|
|
|
session := loginUser(t, owner.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name)
|
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
|
|
Body: body,
|
|
Title: title,
|
|
Assignee: owner.Name,
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
var apiIssue api.Issue
|
|
DecodeJSON(t, resp, &apiIssue)
|
|
assert.Equal(t, body, apiIssue.Body)
|
|
assert.Equal(t, title, apiIssue.Title)
|
|
|
|
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
|
RepoID: repoBefore.ID,
|
|
AssigneeID: owner.ID,
|
|
Content: body,
|
|
Title: title,
|
|
})
|
|
|
|
repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
assert.Equal(t, beforeNumIssues+1, repoAfter.NumIssues(t.Context()))
|
|
assert.Equal(t, beforeNumClosedIssues, repoAfter.NumClosedIssues(t.Context()))
|
|
}
|
|
|
|
func TestAPICreateIssueParallel(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
const body, title = "apiTestBody", "apiTestTitle"
|
|
|
|
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
|
|
|
session := loginUser(t, owner.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all", owner.Name, repoBefore.Name)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func(parentT *testing.T, i int) {
|
|
parentT.Run(fmt.Sprintf("ParallelCreateIssue_%d", i), func(t *testing.T) {
|
|
newTitle := title + strconv.Itoa(i)
|
|
newBody := body + strconv.Itoa(i)
|
|
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
|
|
Body: newBody,
|
|
Title: newTitle,
|
|
Assignee: owner.Name,
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
var apiIssue api.Issue
|
|
DecodeJSON(t, resp, &apiIssue)
|
|
assert.Equal(t, newBody, apiIssue.Body)
|
|
assert.Equal(t, newTitle, apiIssue.Title)
|
|
|
|
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
|
RepoID: repoBefore.ID,
|
|
AssigneeID: owner.ID,
|
|
Content: newBody,
|
|
Title: newTitle,
|
|
})
|
|
|
|
wg.Done()
|
|
})
|
|
}(t, i)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestAPIEditIssue(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
|
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
|
require.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
|
|
assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
|
|
assert.Equal(t, api.StateOpen, issueBefore.State())
|
|
beforeNumClosedIssues := repoBefore.NumClosedIssues(t.Context())
|
|
|
|
session := loginUser(t, owner.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
|
|
|
// update values of issue
|
|
issueState := "closed"
|
|
removeDeadline := true
|
|
milestone := int64(4)
|
|
body := "new content!"
|
|
title := "new title from api set"
|
|
|
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
|
|
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
|
State: &issueState,
|
|
RemoveDeadline: &removeDeadline,
|
|
Milestone: &milestone,
|
|
Body: &body,
|
|
Title: title,
|
|
|
|
// ToDo change more
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
var apiIssue api.Issue
|
|
DecodeJSON(t, resp, &apiIssue)
|
|
|
|
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
|
repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
|
|
|
// check comment history
|
|
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: issueAfter.ID, OldTitle: issueBefore.Title, NewTitle: title})
|
|
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: issueAfter.ID, ContentText: body, IsFirstCreated: false})
|
|
|
|
// check deleted user
|
|
assert.Equal(t, int64(500), issueAfter.PosterID)
|
|
require.NoError(t, issueAfter.LoadAttributes(db.DefaultContext))
|
|
assert.Equal(t, int64(-1), issueAfter.PosterID)
|
|
assert.Equal(t, int64(-1), issueBefore.PosterID)
|
|
assert.Equal(t, int64(-1), apiIssue.Poster.ID)
|
|
|
|
// check repo change
|
|
assert.Equal(t, beforeNumClosedIssues+1, repoAfter.NumClosedIssues(t.Context()))
|
|
|
|
// API response
|
|
assert.Equal(t, api.StateClosed, apiIssue.State)
|
|
assert.Equal(t, milestone, apiIssue.Milestone.ID)
|
|
assert.Equal(t, body, apiIssue.Body)
|
|
assert.Nil(t, apiIssue.Deadline)
|
|
assert.Equal(t, title, apiIssue.Title)
|
|
|
|
// in database
|
|
assert.Equal(t, api.StateClosed, issueAfter.State())
|
|
assert.Equal(t, milestone, issueAfter.MilestoneID)
|
|
assert.Equal(t, int64(0), int64(issueAfter.DeadlineUnix))
|
|
assert.Equal(t, body, issueAfter.Content)
|
|
assert.Equal(t, title, issueAfter.Title)
|
|
|
|
// verify the idempotency of state, milestone, body and title changes
|
|
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
|
State: &issueState,
|
|
Milestone: &milestone,
|
|
Body: &body,
|
|
Title: title,
|
|
}).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusCreated)
|
|
var apiIssueIdempotent api.Issue
|
|
DecodeJSON(t, resp, &apiIssueIdempotent)
|
|
assert.Equal(t, apiIssue.State, apiIssueIdempotent.State)
|
|
assert.Equal(t, apiIssue.Milestone.Title, apiIssueIdempotent.Milestone.Title)
|
|
assert.Equal(t, apiIssue.Body, apiIssueIdempotent.Body)
|
|
assert.Equal(t, apiIssue.Title, apiIssueIdempotent.Title)
|
|
}
|
|
|
|
func TestAPIEditIssueAutoDate(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13})
|
|
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
|
require.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
|
|
|
|
t.Run("WithAutoDate", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
// User2 is not owner, but can update the 'public' issue with auto date
|
|
session := loginUser(t, "user2")
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
|
|
|
|
body := "new content!"
|
|
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
|
Body: &body,
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
var apiIssue api.Issue
|
|
DecodeJSON(t, resp, &apiIssue)
|
|
|
|
// the execution of the API call supposedly lasted less than one minute
|
|
updatedSince := time.Since(apiIssue.Updated)
|
|
assert.LessOrEqual(t, updatedSince, time.Minute)
|
|
|
|
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
|
|
updatedSince = time.Since(issueAfter.UpdatedUnix.AsTime())
|
|
assert.LessOrEqual(t, updatedSince, time.Minute)
|
|
})
|
|
|
|
t.Run("WithUpdateDate", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
// User1 is admin, and so can update the issue without auto date
|
|
session := loginUser(t, "user1")
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
|
|
|
|
body := "new content, with updated time"
|
|
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
|
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
|
Body: &body,
|
|
Updated: &updatedAt,
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusCreated)
|
|
var apiIssue api.Issue
|
|
DecodeJSON(t, resp, &apiIssue)
|
|
|
|
// dates are converted into the same tz, in order to compare them
|
|
utcTZ, _ := time.LoadLocation("UTC")
|
|
assert.Equal(t, updatedAt.In(utcTZ), apiIssue.Updated.In(utcTZ))
|
|
|
|
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueBefore.ID})
|
|
assert.Equal(t, updatedAt.In(utcTZ), issueAfter.UpdatedUnix.AsTime().In(utcTZ))
|
|
})
|
|
|
|
t.Run("WithoutPermission", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
// User2 is not owner nor admin, and so can't update the issue without auto date
|
|
session := loginUser(t, "user2")
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
|
|
|
|
body := "new content, with updated time"
|
|
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
|
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
|
Body: &body,
|
|
Updated: &updatedAt,
|
|
}).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusForbidden)
|
|
var apiError api.APIError
|
|
DecodeJSON(t, resp, &apiError)
|
|
|
|
assert.Equal(t, "user needs to have admin or owner right", apiError.Message)
|
|
})
|
|
}
|
|
|
|
func TestAPIEditIssueMilestoneAutoDate(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
|
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
|
|
|
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
|
require.NoError(t, issueBefore.LoadAttributes(db.DefaultContext))
|
|
|
|
session := loginUser(t, owner.Name)
|
|
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
|
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
|
|
|
|
t.Run("WithAutoDate", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
milestone := int64(1)
|
|
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
|
Milestone: &milestone,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
unittest.FlushAsyncCalcs(t)
|
|
|
|
// the execution of the API call supposedly lasted less than one minute
|
|
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
|
|
updatedSince := time.Since(milestoneAfter.UpdatedUnix.AsTime())
|
|
assert.LessOrEqual(t, updatedSince, time.Minute)
|
|
})
|
|
|
|
t.Run("WithPostUpdateDate", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
// Note: the updated_unix field of the test Milestones is set to NULL
|
|
// Hence, any date is higher than the Milestone's updated date
|
|
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
|
milestone := int64(2)
|
|
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
|
Milestone: &milestone,
|
|
Updated: &updatedAt,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
unittest.FlushAsyncCalcs(t)
|
|
|
|
// the milestone date should be set to 'updatedAt'
|
|
// dates are converted into the same tz, in order to compare them
|
|
utcTZ, _ := time.LoadLocation("UTC")
|
|
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
|
|
assert.Equal(t, updatedAt.In(utcTZ), milestoneAfter.UpdatedUnix.AsTime().In(utcTZ))
|
|
})
|
|
|
|
t.Run("WithPastUpdateDate", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
// Note: This Milestone's updated_unix has been set to Now() by the first subtest
|
|
milestone := int64(1)
|
|
milestoneBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
|
|
|
|
updatedAt := time.Now().Add(-time.Hour).Truncate(time.Second)
|
|
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
|
Milestone: &milestone,
|
|
Updated: &updatedAt,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
unittest.FlushAsyncCalcs(t)
|
|
|
|
// the milestone date should not change
|
|
// dates are converted into the same tz, in order to compare them
|
|
utcTZ, _ := time.LoadLocation("UTC")
|
|
milestoneAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: milestone})
|
|
assert.Equal(t, milestoneAfter.UpdatedUnix.AsTime().In(utcTZ), milestoneBefore.UpdatedUnix.AsTime().In(utcTZ))
|
|
})
|
|
}
|
|
|
|
func TestAPISearchIssues(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
// as this API was used in the frontend, it uses UI page size
|
|
expectedIssueCount := 20 // from the fixtures
|
|
if expectedIssueCount > setting.UI.IssuePagingNum {
|
|
expectedIssueCount = setting.UI.IssuePagingNum
|
|
}
|
|
|
|
link, _ := url.Parse("/api/v1/repos/issues/search")
|
|
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue)
|
|
query := url.Values{}
|
|
var apiIssues []*api.Issue
|
|
|
|
link.RawQuery = query.Encode()
|
|
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, expectedIssueCount)
|
|
|
|
publicOnlyToken := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 16) // 16 public issues
|
|
|
|
since := "2000-01-01T00:50:01+00:00" // 946687801
|
|
before := time.Unix(999307200, 0).Format(time.RFC3339)
|
|
query.Add("since", since)
|
|
query.Add("before", before)
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 11)
|
|
query.Del("since")
|
|
query.Del("before")
|
|
|
|
query.Add("state", "closed")
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 2)
|
|
|
|
query.Set("state", "all")
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Equal(t, "23", resp.Header().Get("X-Total-Count"))
|
|
assert.Len(t, apiIssues, 20)
|
|
|
|
query.Add("limit", "10")
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Equal(t, "23", resp.Header().Get("X-Total-Count"))
|
|
assert.Len(t, apiIssues, 10)
|
|
|
|
query = url.Values{"assigned": {"true"}, "state": {"all"}}
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 2)
|
|
|
|
query = url.Values{"milestones": {"milestone1"}, "state": {"all"}}
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 1)
|
|
|
|
query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}}
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 2)
|
|
|
|
query = url.Values{"owner": {"user2"}} // user
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 9)
|
|
|
|
query = url.Values{"owner": {"org3"}} // organization
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 5)
|
|
|
|
query = url.Values{"owner": {"org3"}, "team": {"team1"}} // organization + team
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 2)
|
|
}
|
|
|
|
func TestAPISearchIssuesWithLabels(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
// as this API was used in the frontend, it uses UI page size
|
|
expectedIssueCount := 20 // from the fixtures
|
|
if expectedIssueCount > setting.UI.IssuePagingNum {
|
|
expectedIssueCount = setting.UI.IssuePagingNum
|
|
}
|
|
|
|
link, _ := url.Parse("/api/v1/repos/issues/search")
|
|
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue)
|
|
query := url.Values{}
|
|
var apiIssues []*api.Issue
|
|
|
|
link.RawQuery = query.Encode()
|
|
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, expectedIssueCount)
|
|
|
|
query.Add("labels", "label1")
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 2)
|
|
|
|
// multiple labels
|
|
query.Set("labels", "label1,label2")
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 2)
|
|
|
|
// an org label
|
|
query.Set("labels", "orglabel4")
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 1)
|
|
|
|
// org and repo label
|
|
query.Set("labels", "label2,orglabel4")
|
|
query.Add("state", "all")
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &apiIssues)
|
|
assert.Len(t, apiIssues, 2)
|
|
|
|
// org and repo label which share the same issue
|
|
query.Set("labels", "label1,orglabel4")
|
|
link.RawQuery = query.Encode()
|
|
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
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)
|
|
}
|
|
|
|
func TestAPIIssueDependencyPermissions(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
actingUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
|
token := getUserToken(t, actingUser.Name, auth_model.AccessTokenScopeAll)
|
|
|
|
actingUserRepo, _, reset := tests.CreateDeclarativeRepoWithOptions(t, actingUser, tests.DeclarativeRepoOptions{})
|
|
defer reset()
|
|
actingUserIssue := createIssue(t, actingUser, actingUserRepo, "source issue", "some content")
|
|
|
|
otherUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
|
otherUserRepo, _, reset := tests.CreateDeclarativeRepoWithOptions(t, otherUser, tests.DeclarativeRepoOptions{
|
|
IsPrivate: optional.Some(true),
|
|
})
|
|
defer reset()
|
|
otherUserIssue := createIssue(t, otherUser, otherUserRepo, "target issue", "some content")
|
|
|
|
apiEndpoint := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", actingUserRepo.OwnerName, actingUserRepo.Name, actingUserIssue.Index)
|
|
req := NewRequest(t, "GET", apiEndpoint).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
var blockingIssues []*api.Issue
|
|
DecodeJSON(t, resp, &blockingIssues)
|
|
require.Empty(t, blockingIssues)
|
|
|
|
req = NewRequestWithJSON(t, "POST", apiEndpoint, api.IssueMeta{
|
|
Owner: otherUserRepo.OwnerName,
|
|
Name: otherUserRepo.Name,
|
|
Index: otherUserIssue.Index,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNotFound) // as otherUserRepo is a private repo we can't link a dependency to it
|
|
|
|
req = NewRequest(t, "GET", apiEndpoint).AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
blockingIssues = []*api.Issue{} // reset
|
|
DecodeJSON(t, resp, &blockingIssues)
|
|
require.Empty(t, blockingIssues)
|
|
|
|
req = NewRequestWithJSON(t, "DELETE", apiEndpoint, api.IssueMeta{
|
|
Owner: otherUserRepo.OwnerName,
|
|
Name: otherUserRepo.Name,
|
|
Index: otherUserIssue.Index,
|
|
}).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNotFound) // as otherUserRepo is a private repo we can't link a dependency to it
|
|
}
|