feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -0,0 +1,142 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"testing"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
)
func TestApproveAllRunsOnPullRequestPage(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
// user2 is the owner of the base repo
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Session := loginUser(t, user2.Name)
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// user4 is the owner of the fork repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
user4Session := loginUser(t, user4.Name)
user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiBaseRepo := createActionsTestRepo(t, user2Token, "approve-all-runs", false)
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
defer doAPIDeleteRepository(user2APICtx)(t)
runner := newMockRunner()
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
// init workflows
wf1TreePath := ".gitea/workflows/pull_1.yml"
wf1FileContent := `name: Pull 1
on: pull_request
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- run: echo unit-test
`
opts1 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent)
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wf1TreePath, opts1)
wf2TreePath := ".gitea/workflows/pull_2.yml"
wf2FileContent := `name: Pull 2
on: pull_request
jobs:
integration-test:
runs-on: ubuntu-latest
steps:
- run: echo integration-test
`
opts2 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wf2TreePath, wf2FileContent)
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wf2TreePath, opts2)
// user4 forks the repo
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
&api.CreateForkOption{
Name: new("approve-all-runs-fork"),
}).AddTokenAuth(user4Token)
resp := MakeRequest(t, req, http.StatusAccepted)
var apiForkRepo api.Repository
DecodeJSON(t, resp, &apiForkRepo)
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
defer doAPIDeleteRepository(user4APICtx)(t)
// user4 creates a pull request from branch "bugfix/user4"
doAPICreateFile(user4APICtx, "user4-fix.txt", &api.CreateFileOptions{
FileOptions: api.FileOptions{
NewBranchName: "bugfix/user4",
Message: "create user4-fix.txt",
Author: api.Identity{
Name: user4.Name,
Email: user4.Email,
},
Committer: api.Identity{
Name: user4.Name,
Email: user4.Email,
},
Dates: api.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte("user4-fix")),
})(t)
apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":bugfix/user4")(t)
assert.NoError(t, err)
// check runs
run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, WorkflowID: "pull_1.yml"})
assert.True(t, run1.NeedApproval)
assert.Equal(t, actions_model.StatusBlocked, run1.Status)
run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, WorkflowID: "pull_2.yml"})
assert.True(t, run2.NeedApproval)
assert.Equal(t, actions_model.StatusBlocked, run2.Status)
// user4 cannot see the approve button
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", baseRepo.OwnerName, baseRepo.Name, apiPull.Index))
resp = user4Session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Zero(t, htmlDoc.doc.Find("#approve-status-checks button.link-action").Length())
// user2 can see the approve button
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", baseRepo.OwnerName, baseRepo.Name, apiPull.Index))
resp = user2Session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
dataURL, exist := htmlDoc.doc.Find("#approve-status-checks button.link-action").Attr("data-url")
assert.True(t, exist)
assert.Equal(t,
fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s",
baseRepo.Link(), apiPull.Head.Sha),
dataURL,
)
// user2 approves all runs
req = NewRequest(t, "POST", dataURL)
user2Session.MakeRequest(t, req, http.StatusOK)
// check runs
run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run1.ID})
assert.False(t, run1.NeedApproval)
assert.Equal(t, user2.ID, run1.ApprovedBy)
assert.Equal(t, actions_model.StatusWaiting, run1.Status)
run2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID})
assert.False(t, run2.NeedApproval)
assert.Equal(t, user2.ID, run2.ApprovedBy)
assert.Equal(t, actions_model.StatusWaiting, run2.Status)
})
}
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
"time"
@@ -121,59 +122,55 @@ jobs:
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent)
createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts)
runIndex := ""
var runID int64
for i := 0; i < len(testCase.outcomes); i++ {
task := runner.fetchTask(t)
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
outcome := testCase.outcomes[jobName]
assert.NotNil(t, outcome)
runner.execTask(t, task, outcome)
runIndex = task.Context.GetFields()["run_number"].GetStringValue()
assert.Equal(t, "1", runIndex)
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
parsedRunIndex, err := strconv.ParseInt(runIndex, 10, 64)
assert.NoError(t, err)
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: apiRepo.ID, Index: parsedRunIndex})
runID = run.ID
}
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
assert.NoError(t, err)
for i := 0; i < len(testCase.outcomes); i++ {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
jobID := jobs[i].ID
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, apiRepo.Name, runID, jobID))
resp := session.MakeRequest(t, req, http.StatusOK)
var listResp actions.ViewResponse
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
assert.NoError(t, err)
assert.Len(t, listResp.State.Run.Jobs, 3)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
}
req := NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
session.MakeRequest(t, req, http.StatusOK)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
session.MakeRequest(t, req, http.StatusOK)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
session.MakeRequest(t, req, http.StatusNotFound)
for i := 0; i < len(testCase.outcomes); i++ {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
jobID := jobs[i].ID
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, apiRepo.Name, runID, jobID))
session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
@@ -62,9 +62,8 @@ jobs:
// run the workflow with os=windows
urlStr := fmt.Sprintf("/%s/%s/actions/run?workflow=%s", user2.Name, repo.Name, "test-inputs-context.yml")
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"ref": "refs/heads/main",
"os": "windows",
"ref": "refs/heads/main",
"os": "windows",
})
session.MakeRequest(t, req, http.StatusSeeOther)
@@ -27,6 +27,7 @@ import (
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJobWithNeeds(t *testing.T) {
@@ -349,6 +350,122 @@ jobs:
})
}
func TestRunnerDisableEnable(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("BasicDisableEnable", func(t *testing.T) {
testData := prepareRunnerDisableEnableTest(t, user2, token, "actions-runner-disable-enable", "mock-runner", "runner-disable-enable")
task1 := testData.runner.fetchTask(t)
require.NotNil(t, task1)
triggerRunnerDisableEnableRun(t, user2, token, testData.repo, "second-push.txt")
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testData.runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
testData.runner.fetchNoTask(t, 2*time.Second)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
task2 := testData.runner.fetchTask(t, 5*time.Second)
require.NotNil(t, task2)
testData.runner.execTask(t, task2, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
})
t.Run("TasksVersionPath", func(t *testing.T) {
testData := prepareRunnerDisableEnableTest(t, user2, token, "actions-runner-version-path", "mock-runner-version-path", "runner-version-path")
var firstVersion int64
var task1 *runnerv1.Task
ddl := time.Now().Add(5 * time.Second)
for time.Now().Before(ddl) {
task1, firstVersion = testData.runner.fetchTaskOnce(t, 0)
if task1 != nil {
break
}
time.Sleep(200 * time.Millisecond)
}
require.NotNil(t, task1, "expected to receive first task")
require.NotZero(t, firstVersion, "response TasksVersion should be set")
// Trigger a second run so there is a pending job after we re-enable the runner
triggerRunnerDisableEnableRun(t, user2, token, testData.repo, "second-push.txt")
time.Sleep(500 * time.Millisecond) // allow workflow run to be created
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testData.runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
// Fetch with the version we had before disable. Server has bumped version on disable,
// so we enter PickTask with a re-loaded runner (disabled) and get no task.
taskAfterDisable, _ := testData.runner.fetchTaskOnce(t, firstVersion)
assert.Nil(t, taskAfterDisable, "disabled runner must not receive a task when sending previous TasksVersion")
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
task2 := testData.runner.fetchTask(t, 5*time.Second)
require.NotNil(t, task2, "after re-enable runner should receive tasks again")
testData.runner.execTask(t, task2, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
})
})
}
type runnerDisableEnableTestData struct {
repo *api.Repository
runner *mockRunner
runnerID int64
}
func prepareRunnerDisableEnableTest(t *testing.T, user *user_model.User, authToken, repoName, runnerName, workflowName string) *runnerDisableEnableTestData {
t.Helper()
apiRepo := createActionsTestRepo(t, authToken, repoName, false)
runner := newMockRunner()
runner.registerAsRepoRunner(t, user.Name, apiRepo.Name, runnerName, []string{"ubuntu-latest"}, false)
wfTreePath := fmt.Sprintf(".gitea/workflows/%s.yml", workflowName)
wfContent := fmt.Sprintf(`name: %s
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo %s
`, workflowName, workflowName)
opts := getWorkflowCreateFileOptions(user, apiRepo.DefaultBranch, "create workflow", wfContent)
createWorkflowFile(t, authToken, user.Name, apiRepo.Name, wfTreePath, opts)
return &runnerDisableEnableTestData{
repo: apiRepo,
runner: runner,
runnerID: getRepoRunnerID(t, authToken, user.Name, apiRepo.Name),
}
}
func triggerRunnerDisableEnableRun(t *testing.T, user *user_model.User, authToken string, repo *api.Repository, treePath string) {
t.Helper()
opts := getWorkflowCreateFileOptions(user, repo.DefaultBranch, "second push", "second run")
createWorkflowFile(t, authToken, user.Name, repo.Name, treePath, opts)
}
func getRepoRunnerID(t *testing.T, authToken, ownerName, repoName string) int64 {
t.Helper()
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners", ownerName, repoName)).AddTokenAuth(authToken)
resp := MakeRequest(t, req, http.StatusOK)
runnerList := api.ActionRunnersResponse{}
DecodeJSON(t, resp, &runnerList)
require.Len(t, runnerList.Entries, 1)
return runnerList.Entries[0].ID
}
func TestActionsGiteaContext(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -0,0 +1,476 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/lfs"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsJobTokenPermissiveAccess(t *testing.T) {
cases := []struct {
name string
isFork bool
ownerPermMode repo_model.ActionsTokenPermissionMode
ownerMaxPerms map[unit_model.Type]perm.AccessMode
repoPermMode repo_model.ActionsTokenPermissionMode
repoMaxPerms map[unit_model.Type]perm.AccessMode
expectGitAccess perm.AccessMode
}{
{
name: "OwnerConfig-Permissive",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
expectGitAccess: perm.AccessModeWrite,
},
{
name: "OwnerConfig-Permissive-CodeNone",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
expectGitAccess: perm.AccessModeNone,
},
{
name: "OwnerConfig-Restricted",
ownerPermMode: repo_model.ActionsTokenPermissionModeRestricted,
expectGitAccess: perm.AccessModeRead,
},
// repo uses its own settings, so owner settings should not affect it
{
name: "SameRepo-Permissive",
ownerPermMode: repo_model.ActionsTokenPermissionModeRestricted,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
expectGitAccess: perm.AccessModeWrite,
},
{
name: "SameRepo-Permissive-CodeNone",
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeRead},
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
repoMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
expectGitAccess: perm.AccessModeNone,
},
{
name: "SameRepo-Restricted",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
expectGitAccess: perm.AccessModeRead,
},
// forks should be always restricted to max read access for code
{
name: "Fork-Permissive",
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
isFork: true,
expectGitAccess: perm.AccessModeRead,
},
{
name: "Fork-Restricted",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
isFork: true,
expectGitAccess: perm.AccessModeRead,
},
{
name: "Fork-Restricted-CodeNone",
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
repoMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
isFork: true,
expectGitAccess: perm.AccessModeNone,
},
}
onGiteaRun(t, func(t *testing.T, u *url.URL) {
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: task.RepoID})
repoActionsUnit := repo.MustGetUnit(t.Context(), unit_model.TypeActions)
repoActionsCfg := repoActionsUnit.ActionsConfig()
ownerActionsCfg, err := actions_model.GetOwnerActionsConfig(t.Context(), repo.OwnerID)
require.NoError(t, err)
_, err = db.GetEngine(t.Context()).ID(task.RepoID).Cols("is_private").Update(&repo_model.Repository{IsPrivate: true})
require.NoError(t, err)
assertRespCodeForSuccess := func(t *testing.T, resp *httptest.ResponseRecorder, succeed bool) {
if succeed {
assert.True(t, 200 <= resp.Code && resp.Code < 300, "Expected success status code, got %d", resp.Code)
} else {
assert.True(t, 400 <= resp.Code && resp.Code < 500, "Expected client error status code, got %d", resp.Code)
}
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
// prepare owner's token permissions settings
ownerActionsCfg.TokenPermissionMode = tt.ownerPermMode
ownerActionsCfg.MaxTokenPermissions = util.Iif(tt.ownerMaxPerms == nil, nil, &repo_model.ActionsTokenPermissions{UnitAccessModes: tt.ownerMaxPerms})
require.NoError(t, actions_model.SetOwnerActionsConfig(t.Context(), repo.OwnerID, ownerActionsCfg))
// prepare repo's token permissions settings
repoActionsCfg.OverrideOwnerConfig = tt.repoPermMode != "" || tt.repoMaxPerms != nil
repoActionsCfg.TokenPermissionMode = tt.repoPermMode
repoActionsCfg.MaxTokenPermissions = util.Iif(tt.repoMaxPerms == nil, nil, &repo_model.ActionsTokenPermissions{UnitAccessModes: tt.repoMaxPerms})
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoActionsUnit))
// prepare task and its token
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
task.IsForkPullRequest = tt.isFork
err := actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status", "is_fork_pull_request")
require.NoError(t, err)
require.NoError(t, task.LoadJob(t.Context()))
require.NoError(t, task.Job.LoadRun(t.Context()))
task.Job.Run.IsForkPullRequest = tt.isFork
require.NoError(t, actions_model.UpdateRun(t.Context(), task.Job.Run, "is_fork_pull_request"))
testURL := *u
testURL.User = url.UserPassword("gitea-actions", task.Token)
t.Run("ReadGitContent", func(t *testing.T) {
testURL.Path = "/user5/repo4.git/HEAD"
resp := MakeRequest(t, NewRequest(t, "GET", testURL.String()), NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess != perm.AccessModeNone)
testURL.Path = "/user5/repo4.git/info/lfs/locks"
req := NewRequest(t, "GET", testURL.String()).SetHeader("Accept", lfs.MediaType)
resp = MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess != perm.AccessModeNone)
})
t.Run("WriteGitContent", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/contents/test-filename", repo.FullName()), &structs.CreateFileOptions{
FileOptions: structs.FileOptions{NewBranchName: "new-branch" + t.Name()},
ContentBase64: base64.StdEncoding.EncodeToString([]byte(`dummy content`)),
}).AddTokenAuth(task.Token)
resp := MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess == perm.AccessModeWrite)
testURL.Path = "/user5/repo4.git/info/lfs/objects/batch"
req = NewRequestWithJSON(t, "POST", testURL.String(), lfs.BatchRequest{Operation: "upload"}).SetHeader("Accept", lfs.MediaType)
resp = MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, tt.expectGitAccess == perm.AccessModeWrite)
})
t.Run("NoOtherPermissions", func(t *testing.T) {
req := NewRequest(t, "DELETE", "/api/v1/repos/"+repo.FullName()).AddTokenAuth(task.Token)
resp := MakeRequest(t, req, NoExpectedStatus)
assertRespCodeForSuccess(t, resp, false)
})
})
}
})
}
func TestActionsCrossRepoAccess(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
// 1. Create Organization
orgName := "org-cross-test"
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &structs.CreateOrgOption{
UserName: orgName,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
owner, err := org_model.GetOrgByName(t.Context(), orgName)
require.NoError(t, err)
// 2. Create Two Repositories in owner
createRepoInOrg := func(name string) int64 {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/repos", orgName), &structs.CreateRepoOption{
Name: name,
AutoInit: true,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
var repo structs.Repository
DecodeJSON(t, resp, &repo)
return repo.ID
}
repoAID := createRepoInOrg("repo-A")
repoBID := createRepoInOrg("repo-B")
// 3. Enable Actions in Repo A (Source) and Repo B (Target)
enableActions := func(repoID int64) {
err := db.Insert(t.Context(), &repo_model.RepoUnit{
RepoID: repoID,
Type: unit_model.TypeActions,
Config: &repo_model.ActionsConfig{
TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive,
},
})
require.NoError(t, err)
}
enableActions(repoAID)
enableActions(repoBID)
// 4. Create Task in Repo A, and use A's token to access B
taskA := createActionTask(t, repoAID, false)
testCtxA := APITestContext{
Session: emptyTestSession(t),
Token: taskA.Token,
Username: orgName,
Reponame: "repo-B",
}
testCtxA.ExpectedCode = http.StatusOK
t.Run("PublicCrossRepoAccess", doAPIGetRepository(testCtxA, func(t *testing.T, r structs.Repository) {
assert.Equal(t, "repo-B", r.Name)
}))
// make repo-B be private
req = NewRequestWithJSON(t, "PATCH", "/api/v1/repos/org-cross-test/repo-B", &structs.EditRepoOption{Private: new(true)}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
testCtxA.ExpectedCode = http.StatusNotFound
t.Run("NoPrivateCrossRepoAccess", doAPIGetRepository(testCtxA, nil))
ownerActionsCfg := actions_model.OwnerActionsConfig{AllowedCrossRepoIDs: []int64{repoBID}}
require.NoError(t, actions_model.SetOwnerActionsConfig(t.Context(), owner.ID, ownerActionsCfg))
testCtxA.ExpectedCode = http.StatusOK
t.Run("AccessToSelectedPrivateRepo", doAPIGetRepository(testCtxA, func(t *testing.T, r structs.Repository) {
assert.Equal(t, "repo-B", r.Name)
}))
t.Run("RepoTransfer", func(t *testing.T) {
ownerActionsCfg, err := actions_model.GetOwnerActionsConfig(t.Context(), owner.ID)
require.NoError(t, err)
assert.Contains(t, ownerActionsCfg.AllowedCrossRepoIDs, repoBID)
// Transfer Repository to user4
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/repo-B/transfer", orgName), &structs.TransferRepoOption{
NewOwner: "user4",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Accept transfer as user4
session4 := loginUser(t, "user4")
token4 := getTokenForLoggedInUser(t, session4, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/repo-B/transfer/accept", orgName)).AddTokenAuth(token4)
MakeRequest(t, req, http.StatusAccepted)
// Verify it is removed from the org's config
ownerActionsCfg, err = actions_model.GetOwnerActionsConfig(t.Context(), owner.ID)
require.NoError(t, err)
assert.NotContains(t, ownerActionsCfg.AllowedCrossRepoIDs, repoBID)
})
})
}
func TestActionsJobTokenPermissions(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("WriteIssue", TestActionsJobTokenPermissionsWriteIssue)
}
func TestActionsJobTokenPermissionsWriteIssue(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
require.Equal(t, repo.ID, task.RepoID)
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{
RepoID: repo.ID,
Type: unit_model.TypeActions,
Config: &repo_model.ActionsConfig{},
}))
repoActionsUnit := repo.MustGetUnit(t.Context(), unit_model.TypeActions)
repoActionsCfg := repoActionsUnit.ActionsConfig()
repoActionsCfg.OverrideOwnerConfig = true
repoActionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModePermissive
repoActionsCfg.MaxTokenPermissions = nil
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoActionsUnit))
require.NoError(t, task.GenerateToken())
task.Status = actions_model.StatusRunning
require.NoError(t, actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status"))
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
labelURL := fmt.Sprintf("/api/v1/repos/%s/%s/labels", user.Name, repo.Name)
req := NewRequestWithJSON(t, "POST", labelURL, &structs.CreateLabelOption{
Name: "task-label",
Color: "0e8a16",
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
label := DecodeJSON(t, resp, &structs.Label{})
issueURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues", user.Name, repo.Name)
req = NewRequestWithJSON(t, "POST", issueURL, &structs.CreateIssueOption{
Title: "issue for actions token label deletion",
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
issue := DecodeJSON(t, resp, &structs.Issue{})
taskToken := task.Token
require.NotEmpty(t, taskToken)
issueLabelsURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", user.Name, repo.Name, issue.Index)
req = NewRequestWithJSON(t, "POST", issueLabelsURL, &structs.IssueLabelsOption{
Labels: []any{label.ID},
}).AddTokenAuth(taskToken)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%d", issueLabelsURL, label.ID)).AddTokenAuth(taskToken)
MakeRequest(t, req, http.StatusNoContent)
}
func createActionTask(t *testing.T, repoID int64, isFork bool) *actions_model.ActionTask {
job := &actions_model.ActionRunJob{
RepoID: repoID,
Status: actions_model.StatusRunning,
IsForkPullRequest: isFork,
JobID: "test_job",
Name: "test_job",
}
require.NoError(t, db.Insert(t.Context(), job))
task := &actions_model.ActionTask{
JobID: job.ID,
RepoID: repoID,
Status: actions_model.StatusRunning,
IsForkPullRequest: isFork,
}
require.NoError(t, task.GenerateToken())
require.NoError(t, db.Insert(t.Context(), task))
return task
}
func TestActionsTokenPermissionsPersistenceWithWorkflow(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
// create repos
repo1 := createActionsTestRepo(t, token, "actions-permission-repo1", false)
repo2 := createActionsTestRepo(t, token, "actions-permission-repo2", true)
// add repo2 to owner-level cross-repo access list
req := NewRequestWithValues(t, "POST", "/user/settings/actions/general", map[string]string{
"cross_repo_add_target": "true",
"cross_repo_add_target_name": repo2.Name,
})
session.MakeRequest(t, req, http.StatusOK)
// create the runner for repo1
runner1 := newMockRunner()
runner1.registerAsRepoRunner(t, user2.Name, repo1.Name, "mock-runner", []string{"ubuntu-latest"}, false)
// set repo1 actions token permission mode to "permissive"
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo1.Name), map[string]string{
"token_permission_mode": "permissive",
"override_owner_config": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// set repo2 actions token permission mode to "restricted", and set max permissions
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeReleases)): "read",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// create a workflow file with "permission" keyword for repo1
wfTreePath := ".gitea/workflows/test_permissions.yml"
wfFileContent := `name: Test Permissions
on:
push:
paths:
- '.gitea/workflows/test_permissions.yml'
jobs:
job-override:
runs-on: ubuntu-latest
permissions:
code: write
steps:
- run: echo "test perms"
`
opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent)
createWorkflowFile(t, token, user2.Name, repo1.Name, wfTreePath, opts)
task1 := runner1.fetchTask(t)
task1Token := task1.Secrets["GITEA_TOKEN"]
require.NotEmpty(t, task1Token)
// should fail: target repo does not allow code access
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo2.Name)).AddTokenAuth(task1Token)
MakeRequest(t, req, http.StatusNotFound)
// set repo2 max permission to "read" so that the actions token can access code
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "read",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeReleases)): "read",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// should succeed: target repo now allows code read access for this token
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo2.Name)).AddTokenAuth(task1Token)
MakeRequest(t, req, http.StatusOK)
// but it should not have write access
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusUnauthorized)
// set repo1&repo2 max permission to "write" so that the actions token can access code
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo1.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "write",
})
session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
"token_permission_mode": "restricted",
"override_owner_config": "true",
"enable_max_permissions": "true",
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "write",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// now task1 has write access to repo1, but still only read access to repo2 (different repo)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo1.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
SetHeader("Accept", lfs.MediaType).
AddBasicAuth("gitea-actions", task1Token)
MakeRequest(t, req, http.StatusUnauthorized)
})
}
@@ -0,0 +1,77 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/url"
"os"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/dbfs"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
actions_module "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/storage"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Regression for https://gitea.com/gitea/runner/issues/950: a runner that
// finalizes a task with no log output sends UpdateLog{Rows:[], NoMore:true}.
// The previous short-circuit on len(Rows)==0 skipped TransferLogs, leaving
// an orphan dbfs_data row. Verify the row is now archived and removed.
func TestActionsLogFinalizeWithoutRows(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user2.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiRepo := createActionsTestRepo(t, token, "actions-finalize-no-rows", false)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
runner := newMockRunner()
runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
const wfTreePath = ".gitea/workflows/finalize-no-rows.yml"
wfFileContent := fmt.Sprintf(`name: finalize-no-rows
on:
push:
paths:
- '%s'
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: noop
`, wfTreePath)
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "trigger", wfFileContent))
task := runner.fetchTask(t)
resp, err := runner.client.runnerServiceClient.UpdateLog(t.Context(), connect.NewRequest(&runnerv1.UpdateLogRequest{
TaskId: task.Id,
Index: 0,
Rows: nil,
NoMore: true,
}))
require.NoError(t, err)
assert.EqualValues(t, 0, resp.Msg.AckIndex)
freshTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
require.True(t, freshTask.LogInStorage, "log_in_storage must flip after empty NoMore=true")
_, err = storage.Actions.Stat(freshTask.LogFilename)
assert.NoError(t, err, "archived log must exist in storage")
_, err = dbfs.Open(t.Context(), actions_module.DBFSPrefix+freshTask.LogFilename)
assert.ErrorIs(t, err, os.ErrNotExist, "DBFS row must be cleaned up after TransferLogs")
})
}
@@ -7,12 +7,10 @@ import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"testing"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
@@ -171,7 +169,7 @@ jobs:
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
// fetch and execute tasks
for jobIndex, outcome := range tc.outcome {
for _, outcome := range tc.outcome {
task := runner.fetchTask(t)
runner.execTask(t, task, outcome)
@@ -183,9 +181,10 @@ jobs:
_, err := storage.Actions.Stat(logFileName)
assert.NoError(t, err)
_, job, run := getTaskAndJobAndRunByTaskID(t, task.Id)
// download task logs and check content
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, repo.Name, runIndex, jobIndex)).
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job.ID)).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
@@ -198,15 +197,8 @@ jobs:
)
}
runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64)
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
assert.NoError(t, err)
assert.Len(t, jobs, len(tc.outcome))
jobID := jobs[jobIndex].ID
// download task logs from API and check content
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)).
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, job.ID)).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
@@ -33,7 +33,7 @@ func TestActionsRerun(t *testing.T) {
wfTreePath := ".gitea/workflows/actions-rerun-workflow-1.yml"
wfFileContent := `name: actions-rerun-workflow-1
on:
on:
push:
paths:
- '.gitea/workflows/actions-rerun-workflow-1.yml'
@@ -54,25 +54,22 @@ jobs:
// fetch and exec job1
job1Task := runner.fetchTask(t)
_, _, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
_, job1, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
runner.execTask(t, job1Task, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
})
// RERUN-FAILURE: the run is not done
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
session.MakeRequest(t, req, http.StatusBadRequest)
// fetch and exec job2
job2Task := runner.fetchTask(t)
_, job2, _ := getTaskAndJobAndRunByTaskID(t, job2Task.Id)
runner.execTask(t, job2Task, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
})
// RERUN-1: rerun the run
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
session.MakeRequest(t, req, http.StatusOK)
// fetch and exec job1
job1TaskR1 := runner.fetchTask(t)
@@ -86,9 +83,7 @@ jobs:
})
// RERUN-2: rerun job1
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 0), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job1.ID))
session.MakeRequest(t, req, http.StatusOK)
// job2 needs job1, so rerunning job1 will also rerun job2
// fetch and exec job1
@@ -103,9 +98,7 @@ jobs:
})
// RERUN-3: rerun job2
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 1), map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job2.ID))
session.MakeRequest(t, req, http.StatusOK)
// only job2 will rerun
// fetch and exec job2
@@ -0,0 +1,300 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
actions_web "code.gitea.io/gitea/routers/web/repo/actions"
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestActionsRoute(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
t.Run("testActionsRouteForIDBasedURL", testActionsRouteForIDBasedURL)
t.Run("testActionsRouteForLegacyIndexBasedURL", testActionsRouteForLegacyIndexBasedURL)
})
}
func testActionsRouteForIDBasedURL(t *testing.T) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Session := loginUser(t, user2.Name)
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
repo1 := createActionsTestRepo(t, user2Token, "actions-route-id-url-1", false)
runner1 := newMockRunner()
runner1.registerAsRepoRunner(t, user2.Name, repo1.Name, "mock-runner", []string{"ubuntu-latest"}, false)
repo2 := createActionsTestRepo(t, user2Token, "actions-route-id-url-2", false)
runner2 := newMockRunner()
runner2.registerAsRepoRunner(t, user2.Name, repo2.Name, "mock-runner", []string{"ubuntu-latest"}, false)
workflowTreePath := ".gitea/workflows/test.yml"
workflowContent := `name: test
on:
push:
paths:
- '.gitea/workflows/test.yml'
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo job1
`
opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+workflowTreePath, workflowContent)
createWorkflowFile(t, user2Token, user2.Name, repo1.Name, workflowTreePath, opts)
createWorkflowFile(t, user2Token, user2.Name, repo2.Name, workflowTreePath, opts)
task1 := runner1.fetchTask(t)
_, job1, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id)
task2 := runner2.fetchTask(t)
_, job2, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id)
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, run1.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, 999999))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// run1 and job1 belong to repo1, success
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job1.ID))
resp := user2Session.MakeRequest(t, req, http.StatusOK)
var viewResp actions_web.ViewResponse
DecodeJSON(t, resp, &viewResp)
assert.Len(t, viewResp.State.Run.Jobs, 1)
assert.Equal(t, job1.ID, viewResp.State.Run.Jobs[0].ID)
// run2 and job2 do not belong to repo1, failure
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job1.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/workflow", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/approve", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/cancel", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/test.txt", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "DELETE", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/test.txt", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// make the tasks complete, then test rerun
runner1.execTask(t, task1, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
})
runner2.execTask(t, task2, &mockTaskOutcome{
result: runnerv1.Result_RESULT_SUCCESS,
})
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo1.Name, run2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run2.ID, job2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run1.ID, job2.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run2.ID, job1.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
}
func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Session := loginUser(t, user2.Name)
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
repo := createActionsTestRepo(t, user2Token, "actions-route-legacy-url", false)
mkRun := func(id, index int64, title, sha string) *actions_model.ActionRun {
return &actions_model.ActionRun{
ID: id,
Index: index,
RepoID: repo.ID,
OwnerID: user2.ID,
Title: title,
WorkflowID: "legacy-route.yml",
TriggerUserID: user2.ID,
Ref: "refs/heads/master",
CommitSHA: sha,
Status: actions_model.StatusWaiting,
}
}
mkJob := func(id, runID int64, name, sha string) *actions_model.ActionRunJob {
return &actions_model.ActionRunJob{
ID: id,
RunID: runID,
RepoID: repo.ID,
OwnerID: user2.ID,
CommitSHA: sha,
Name: name,
Status: actions_model.StatusWaiting,
}
}
// A small ID-based run/job pair that should always resolve directly.
smallIDRun := mkRun(80, 20, "legacy route small id", "aaa001")
smallIDJob := mkJob(170, smallIDRun.ID, "legacy-small-job", smallIDRun.CommitSHA)
// Another small run used to provide a job ID that belongs to a different run.
otherSmallRun := mkRun(90, 30, "legacy route other small", "aaa002")
otherSmallJob := mkJob(180, otherSmallRun.ID, "legacy-other-small-job", otherSmallRun.CommitSHA)
// A large-ID run whose legacy run index should redirect to its ID-based URL.
normalRun := mkRun(1500, 900, "legacy route normal", "aaa003")
normalRunJob := mkJob(1600, normalRun.ID, "legacy-normal-job", normalRun.CommitSHA)
// A run whose index collides with normalRun.ID to exercise summary-page ID-first behavior.
collisionRun := mkRun(2400, 1500, "legacy route collision", "aaa004")
collisionJobIdx0 := mkJob(2600, collisionRun.ID, "legacy-collision-job-1", collisionRun.CommitSHA)
collisionJobIdx1 := mkJob(2601, collisionRun.ID, "legacy-collision-job-2", collisionRun.CommitSHA)
// A run whose job has a smaller ID than the run itself (job_id < run_id)
jobSmallerThanRunRun := mkRun(5000, 5500, "legacy route job before run", "aaa007")
jobSmallerThanRunJob := mkJob(4500, jobSmallerThanRunRun.ID, "legacy-job-before-run-job", jobSmallerThanRunRun.CommitSHA)
// A small ID-based run/job pair that collides with a different legacy run/job index pair.
ambiguousIDRun := mkRun(3, 1, "legacy route ambiguous id", "aaa005")
ambiguousIDJob := mkJob(4, ambiguousIDRun.ID, "legacy-ambiguous-id-job", ambiguousIDRun.CommitSHA)
// The legacy run/job target for the ambiguous /runs/3/jobs/4 URL.
ambiguousLegacyRun := mkRun(1501, ambiguousIDRun.ID, "legacy route ambiguous legacy", "aaa006")
ambiguousLegacyJobIdx0 := mkJob(1601, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-0", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobIdx1 := mkJob(1602, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-1", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobIdx2 := mkJob(1603, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-2", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobIdx3 := mkJob(1604, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-3", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobIdx4 := mkJob(1605, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-4", ambiguousLegacyRun.CommitSHA) // job_index=4
ambiguousLegacyJobIdx5 := mkJob(1606, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-5", ambiguousLegacyRun.CommitSHA)
ambiguousLegacyJobs := []*actions_model.ActionRunJob{
ambiguousLegacyJobIdx0,
ambiguousLegacyJobIdx1,
ambiguousLegacyJobIdx2,
ambiguousLegacyJobIdx3,
ambiguousLegacyJobIdx4,
ambiguousLegacyJobIdx5,
}
targetAmbiguousLegacyJob := ambiguousLegacyJobs[int(ambiguousIDJob.ID)]
insertBeansWithExplicitIDs(t, "action_run",
smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun, jobSmallerThanRunRun,
)
insertBeansWithExplicitIDs(t, "action_run_job",
smallIDJob, otherSmallJob, normalRunJob, ambiguousIDJob, collisionJobIdx0, collisionJobIdx1,
ambiguousLegacyJobIdx0, ambiguousLegacyJobIdx1, ambiguousLegacyJobIdx2, ambiguousLegacyJobIdx3, ambiguousLegacyJobIdx4, ambiguousLegacyJobIdx5,
jobSmallerThanRunJob,
)
t.Run("OnlyRunID", func(t *testing.T) {
// ID-based URLs must be valid
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, smallIDRun.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
})
t.Run("OnlyRunIndex", func(t *testing.T) {
// legacy run index should redirect to the ID-based URL
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.Index))
resp := user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.ID), resp.Header().Get("Location"))
// Best-effort compatibility prefers the run ID when the same number also exists as a legacy run index.
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, collisionRun.Index))
resp = user2Session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), fmt.Sprintf(`data-run-id="%d"`, normalRun.ID)) // because collisionRun.Index == normalRun.ID
// by_index=1 should force the summary page to use the legacy run index interpretation.
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d?by_index=1", user2.Name, repo.Name, collisionRun.Index))
resp = user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, collisionRun.ID), resp.Header().Get("Location"))
})
t.Run("RunIDAndJobID", func(t *testing.T) {
// ID-based URLs must be valid
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, smallIDRun.ID, smallIDJob.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, normalRun.ID, normalRunJob.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
// URL must resolve even when job_id < run_id.
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, jobSmallerThanRunRun.ID, jobSmallerThanRunJob.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
})
t.Run("RunIndexAndJobIndex", func(t *testing.T) {
// /user2/repo2/actions/runs/3/jobs/4 is ambiguous:
// - it may resolve as the ID-based URL for run_id=3/job_id=4,
// - or as the legacy index-based URL for run_index=3/job_index=4 which should redirect to run_id=1501/job_id=1605.
idBasedURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousIDRun.ID, ambiguousIDJob.ID)
indexBasedURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousLegacyRun.Index, 4) // for ambiguousLegacyJobIdx4
assert.Equal(t, idBasedURL, indexBasedURL)
// When both interpretations are valid, prefer the ID-based target by default.
req := NewRequest(t, "GET", indexBasedURL)
user2Session.MakeRequest(t, req, http.StatusOK)
redirectURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousLegacyRun.ID, targetAmbiguousLegacyJob.ID)
// by_index=1 should explicitly force the legacy run/job index interpretation.
req = NewRequest(t, "GET", indexBasedURL+"?by_index=1")
resp := user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, redirectURL, resp.Header().Get("Location"))
// legacy job index 0 should redirect to the first job's ID
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0", user2.Name, repo.Name, collisionRun.Index))
resp = user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, collisionRun.ID, collisionJobIdx0.ID), resp.Header().Get("Location"))
// legacy job index 1 should redirect to the second job's ID
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/1", user2.Name, repo.Name, collisionRun.Index))
resp = user2Session.MakeRequest(t, req, http.StatusFound)
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, collisionRun.ID, collisionJobIdx1.ID), resp.Header().Get("Location"))
})
t.Run("InvalidURLs", func(t *testing.T) {
// the job ID from a different run should not match
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, smallIDRun.ID, otherSmallJob.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// resolve the run by index first and then return not found because the job index is out-of-range
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/2", user2.Name, repo.Name, normalRun.ID))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// an out-of-range job index should return not found
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/2", user2.Name, repo.Name, collisionRun.Index))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// a missing run number should return not found
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, 999999))
user2Session.MakeRequest(t, req, http.StatusNotFound)
// a missing legacy run index should return not found
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0", user2.Name, repo.Name, 999999))
user2Session.MakeRequest(t, req, http.StatusNotFound)
})
}
func insertBeansWithExplicitIDs(t *testing.T, table string, beans ...any) {
t.Helper()
ctx, committer, err := db.TxContext(t.Context())
require.NoError(t, err)
defer committer.Close()
if setting.Database.Type.IsMSSQL() {
_, err = db.Exec(ctx, fmt.Sprintf("SET IDENTITY_INSERT [%s] ON", table))
require.NoError(t, err)
defer func() {
_, err = db.Exec(ctx, fmt.Sprintf("SET IDENTITY_INSERT [%s] OFF", table))
require.NoError(t, err)
}()
}
require.NoError(t, db.Insert(ctx, beans...))
require.NoError(t, committer.Commit())
}
@@ -50,30 +50,46 @@ func TestActionsRunnerModify(t *testing.T) {
doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, description string, expectedStatus int) {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d", baseURL, id), map[string]string{
"_csrf": GetUserCSRFToken(t, sess),
"description": description,
})
sess.MakeRequest(t, req, expectedStatus)
}
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
"_csrf": GetUserCSRFToken(t, sess),
})
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
doDisable := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/update-runner?disabled=true", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
doEnable := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/update-runner?disabled=false", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusNotFound)
doDisable(t, sess, baseURL, id, http.StatusNotFound)
doEnable(t, sess, baseURL, id, http.StatusNotFound)
doDelete(t, sess, baseURL, id, http.StatusNotFound)
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.Empty(t, v.Description)
assert.False(t, v.IsDisabled)
}
assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusSeeOther)
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.Equal(t, "ChangedDescription", v.Description)
doDisable(t, sess, baseURL, id, http.StatusOK)
v = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.True(t, v.IsDisabled)
doEnable(t, sess, baseURL, id, http.StatusOK)
v = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
assert.False(t, v.IsDisabled)
doDelete(t, sess, baseURL, id, http.StatusOK)
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id})
}
@@ -19,6 +19,7 @@ import (
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -83,7 +84,7 @@ func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []strin
func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string, ephemeral bool) {
session := loginUser(t, ownerName)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
req := NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var registrationToken struct {
Token string `json:"token"`
@@ -94,13 +95,13 @@ func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, run
func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task {
task := r.tryFetchTask(t, timeout...)
assert.NotNil(t, task, "failed to fetch a task")
require.NotNil(t, task, "failed to fetch a task")
return task
}
func (r *mockRunner) fetchNoTask(t *testing.T, timeout ...time.Duration) {
task := r.tryFetchTask(t, timeout...)
assert.Nil(t, task, "a task is fetched")
require.Nil(t, task, "a task is fetched")
}
const defaultFetchTaskTimeout = 1 * time.Second
@@ -113,12 +114,8 @@ func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runne
ddl := time.Now().Add(fetchTimeout)
var task *runnerv1.Task
for time.Now().Before(ddl) {
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: 0,
}))
assert.NoError(t, err)
if resp.Msg.Task != nil {
task = resp.Msg.Task
task, _ = r.fetchTaskOnce(t, 0)
if task != nil {
break
}
time.Sleep(200 * time.Millisecond)
@@ -127,6 +124,17 @@ func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runne
return task
}
// fetchTaskOnce performs a single FetchTask request with the given TasksVersion
// and returns the task (if any) and the TasksVersion from the response.
// Used to verify the production path where the runner sends the current version.
func (r *mockRunner) fetchTaskOnce(t *testing.T, tasksVersion int64) (*runnerv1.Task, int64) {
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
TasksVersion: tasksVersion,
}))
require.NoError(t, err)
return resp.Msg.Task, resp.Msg.TasksVersion
}
type mockTaskOutcome struct {
result runnerv1.Result
outputs map[string]string
@@ -19,7 +19,6 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/migration"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
mirror_service "code.gitea.io/gitea/services/mirror"
repo_service "code.gitea.io/gitea/services/repository"
files_service "code.gitea.io/gitea/services/repository/files"
@@ -89,7 +88,9 @@ jobs:
assert.NoError(t, err)
// merge pull request
testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), mergeStyle, false)
testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), MergeOptions{
Style: mergeStyle,
})
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
return pull.MergedCommitID, "@every 2m"
@@ -101,8 +102,8 @@ jobs:
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
// enable manual-merge
doAPIEditRepository(testContext, &api.EditRepoOption{
HasPullRequests: util.ToPointer(true),
AllowManualMerge: util.ToPointer(true),
HasPullRequests: new(true),
AllowManualMerge: new(true),
})(t)
// update workflow file
@@ -168,7 +169,7 @@ func testScheduleUpdateMirrorSync(t *testing.T) {
// enable actions unit for mirror repo
assert.False(t, mirrorRepo.UnitEnabled(t.Context(), unit_model.TypeActions))
doAPIEditRepository(mirrorContext, &api.EditRepoOption{
HasActions: util.ToPointer(true),
HasActions: new(true),
})(t)
actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
@@ -199,11 +200,11 @@ func testScheduleUpdateMirrorSync(t *testing.T) {
func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
doAPIEditRepository(testContext, &api.EditRepoOption{
Archived: util.ToPointer(true),
Archived: new(true),
})(t)
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
doAPIEditRepository(testContext, &api.EditRepoOption{
Archived: util.ToPointer(false),
Archived: new(false),
})(t)
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
assert.NoError(t, err)
@@ -214,11 +215,11 @@ func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
func testScheduleUpdateDisableAndEnableActionsUnit(t *testing.T) {
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
doAPIEditRepository(testContext, &api.EditRepoOption{
HasActions: util.ToPointer(false),
HasActions: new(false),
})(t)
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
doAPIEditRepository(testContext, &api.EditRepoOption{
HasActions: util.ToPointer(true),
HasActions: new(true),
})(t)
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
assert.NoError(t, err)
@@ -0,0 +1,81 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestActionsCollaborativeOwner(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
// user2 is the owner of the private "reusable_workflow" repo
user2Session := loginUser(t, "user2")
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiReusableWorkflowRepo := createActionsTestRepo(t, user2Token, "reusable_workflow", true)
reusableWorkflowRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiReusableWorkflowRepo.ID})
// user4 is the owner of the private caller repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
user4Session := loginUser(t, user4.Name)
user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiCallerRepo := createActionsTestRepo(t, user4Token, "caller_workflow", true)
callerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiCallerRepo.ID})
// create a mock runner for caller
runner := newMockRunner()
runner.registerAsRepoRunner(t, callerRepo.OwnerName, callerRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
// init the workflow for caller
wfTreePath := ".gitea/workflows/test_collaborative_owner.yml"
wfFileContent := `name: Test Collaborative Owner
on: push
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo 'test collaborative owner'
`
opts := getWorkflowCreateFileOptions(user4, callerRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
createWorkflowFile(t, user4Token, callerRepo.OwnerName, callerRepo.Name, wfTreePath, opts)
// fetch the task and get its token
task := runner.fetchTask(t)
taskToken := task.Secrets["GITEA_TOKEN"]
assert.NotEmpty(t, taskToken)
// prepare for clone
dstPath := t.TempDir()
u.Path = fmt.Sprintf("%s/%s.git", "user2", "reusable_workflow")
u.User = url.UserPassword("gitea-actions", taskToken)
// the git clone will fail
doGitCloneFail(u)(t)
// add user10 to the list of collaborative owners
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name), map[string]string{
"collaborative_owner": user4.Name,
})
user2Session.MakeRequest(t, req, http.StatusOK)
// the git clone will be successful
doGitClone(dstPath, u)(t)
// remove user10 from the list of collaborative owners
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name, user4.ID))
user2Session.MakeRequest(t, req, http.StatusOK)
// the git clone will fail
doGitCloneFail(u)(t)
})
}
@@ -23,14 +23,12 @@ import (
user_model "code.gitea.io/gitea/models/user"
actions_module "code.gitea.io/gitea/modules/actions"
"code.gitea.io/gitea/modules/commitstatus"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
@@ -40,6 +38,7 @@ import (
files_service "code.gitea.io/gitea/services/repository/files"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPullRequestTargetEvent(t *testing.T) {
@@ -413,7 +412,7 @@ jobs:
assert.NoError(t, err)
// create a branch
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-create-branch")
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-create-branch")
assert.NoError(t, err)
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: "add workflow",
@@ -439,7 +438,7 @@ jobs:
assert.NotNil(t, run)
// delete the branch
err = repo_service.DeleteBranch(t.Context(), user2, repo, gitRepo, "test-create-branch", nil)
err = repo_service.DeleteBranch(t.Context(), user2, repo, gitRepo, "test-create-branch")
assert.NoError(t, err)
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: "add workflow",
@@ -531,9 +530,7 @@ jobs:
// create a new branch
testBranch := "test-branch"
gitRepo, err := git.OpenRepository(t.Context(), ".")
assert.NoError(t, err)
err = repo_service.CreateNewBranch(t.Context(), user2, repo, gitRepo, "main", testBranch)
err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch)
assert.NoError(t, err)
// create Pull
@@ -695,6 +692,144 @@ func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL,
assert.NoError(t, err)
}
func TestPullRequestReviewCommitStatusEvent(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // reviewer
// create a repo
repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{
Name: "repo-pull-request-review",
Description: "test pull-request-review commit status",
AutoInit: true,
Gitignores: "Go",
License: "MIT",
Readme: "Default",
DefaultBranch: "main",
IsPrivate: false,
})
assert.NoError(t, err)
assert.NotEmpty(t, repo)
// add user4 as collaborator so they can review
ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository)
t.Run("AddUser4AsCollaboratorWithWriteAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeWrite))
// add workflow file that triggers on pull_request_review
addWorkflow, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: ".gitea/workflows/pr-review.yml",
ContentReader: strings.NewReader(`name: test
on:
pull_request_review:
types: [submitted]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo helloworld
`),
},
},
Message: "add workflow",
OldBranch: "main",
NewBranch: "main",
Author: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
assert.NotEmpty(t, addWorkflow)
// create a branch and a PR
testBranch := "test-review-branch"
err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch)
assert.NoError(t, err)
// add a file on the test branch so the PR has changes
addFileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "test.txt",
ContentReader: strings.NewReader("test content"),
},
},
Message: "add test file",
OldBranch: testBranch,
NewBranch: testBranch,
Author: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
assert.NotEmpty(t, addFileResp)
sha := addFileResp.Commit.SHA
pullIssue := &issues_model.Issue{
RepoID: repo.ID,
Title: "A test PR for review",
PosterID: user2.ID,
Poster: user2,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: repo.ID,
BaseRepoID: repo.ID,
HeadBranch: testBranch,
BaseBranch: "main",
HeadRepo: repo,
BaseRepo: repo,
Type: issues_model.PullRequestGitea,
}
prOpts := &pull_service.NewPullRequestOptions{Repo: repo, Issue: pullIssue, PullRequest: pullRequest}
err = pull_service.NewPullRequest(t.Context(), prOpts)
assert.NoError(t, err)
// submit an approval review as user4
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
_, _, err = pull_service.SubmitReview(t.Context(), user4, gitRepo, pullIssue, issues_model.ReviewTypeApprove, "lgtm", sha, nil)
assert.NoError(t, err)
// verify that a commit status was created for the review event
assert.Eventually(t, func() bool {
latestCommitStatuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, sha, db.ListOptionsAll)
assert.NoError(t, err)
if len(latestCommitStatuses) == 0 {
return false
}
if latestCommitStatuses[0].State == commitstatus.CommitStatusPending {
insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context)
return true
}
return false
}, 1*time.Second, 100*time.Millisecond)
})
}
func TestWorkflowDispatchPublicApi(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
@@ -772,6 +907,27 @@ jobs:
CommitSHA: branch.CommitID,
})
assert.NotNil(t, run)
// Now trigger with rundetails
values.Set("return_run_details", "true")
req = NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
runDetails := &api.RunDetails{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(runDetails))
assert.NotEqual(t, 0, runDetails.WorkflowRunID)
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
ID: runDetails.WorkflowRunID,
Title: "add workflow",
RepoID: repo.ID,
Event: "workflow_dispatch",
Ref: "refs/heads/main",
WorkflowID: "dispatch.yml",
CommitSHA: branch.CommitID,
})
assert.NotNil(t, run)
})
}
@@ -1409,7 +1565,7 @@ jobs:
// user4 forks the repo
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
&api.CreateForkOption{
Name: util.ToPointer("close-pull-request-with-path-fork"),
Name: new("close-pull-request-with-path-fork"),
}).AddTokenAuth(user4Token)
resp := MakeRequest(t, req, http.StatusAccepted)
var apiForkRepo api.Repository
@@ -1508,14 +1664,11 @@ jobs:
assert.NotEmpty(t, addWorkflowToBaseResp)
// Get the commit ID of the default branch
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
assert.NoError(t, err)
// create a branch
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-action-run-name-with-variables")
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name-with-variables")
assert.NoError(t, err)
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: user2.LoginName + " is running this workflow",
@@ -1585,14 +1738,11 @@ jobs:
assert.NotEmpty(t, addWorkflowToBaseResp)
// Get the commit ID of the default branch
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
assert.NoError(t, err)
// create a branch
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-action-run-name")
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name")
assert.NoError(t, err)
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
Title: "run name without variables",
@@ -1623,7 +1773,7 @@ func TestPullRequestWithPathsRebase(t *testing.T) {
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir1/dir1.txt", "1")
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir2/dir2.txt", "2")
wfFileContent := `name: ci
on:
on:
pull_request:
paths:
- 'dir1/**'
@@ -1648,12 +1798,10 @@ jobs:
apiPull, err := doAPICreatePullRequest(apiCtx, "user2", repoName, repo.DefaultBranch, "update-dir2")(t)
runner.fetchNoTask(t)
assert.NoError(t, err)
testEditFile(t, session, "user2", repoName, repo.DefaultBranch, "dir1/dir1.txt", "11") // change the file in "dir1"
req := NewRequestWithValues(t, "POST",
fmt.Sprintf("/%s/%s/pulls/%d/update?style=rebase", "user2", repoName, apiPull.Index), // update by rebase
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
})
// change the file in "dir1"
testEditFile(t, session, "user2", repoName, repo.DefaultBranch, "dir1/dir1.txt", "11")
// update by rebase
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/pulls/%d/update?style=rebase", "user2", repoName, apiPull.Index))
session.MakeRequest(t, req, http.StatusSeeOther)
runner.fetchNoTask(t)
})
@@ -50,17 +50,14 @@ func TestActionsVariables(t *testing.T) {
doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, data string, expectedStatus int) {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", baseURL, id), map[string]string{
"_csrf": GetUserCSRFToken(t, sess),
"name": "VAR",
"data": data,
"name": "VAR",
"data": data,
})
sess.MakeRequest(t, req, expectedStatus)
}
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
"_csrf": GetUserCSRFToken(t, sess),
})
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id))
sess.MakeRequest(t, req, expectedStatus)
}
@@ -7,10 +7,14 @@ import (
"net/http"
"testing"
"code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAdminConfig(t *testing.T) {
@@ -20,4 +24,46 @@ func TestAdminConfig(t *testing.T) {
req := NewRequest(t, "GET", "/-/admin/config")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
t.Run("OpenEditorWithApps", func(t *testing.T) {
cfg := setting.Config().Repository.OpenWithEditorApps
editorApps := cfg.Value(t.Context())
assert.Len(t, editorApps, 3)
assert.False(t, cfg.HasValue(t.Context()))
require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[]"}))
config.GetDynGetter().InvalidateCache()
editorApps = cfg.Value(t.Context())
assert.Len(t, editorApps, 3)
assert.False(t, cfg.HasValue(t.Context()))
require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[{}]"}))
config.GetDynGetter().InvalidateCache()
editorApps = cfg.Value(t.Context())
assert.Len(t, editorApps, 1)
assert.True(t, cfg.HasValue(t.Context()))
})
t.Run("InstanceWebBanner", func(t *testing.T) {
banner, rev1, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context())
assert.False(t, has)
assert.Equal(t, setting.WebBannerType{}, banner)
req = NewRequestWithValues(t, "POST", "/-/admin/config", map[string]string{
"key": "instance.web_banner",
"value": `{"DisplayEnabled":true,"ContentMessage":"test-msg","StartTimeUnix":123,"EndTimeUnix":456}`,
})
session.MakeRequest(t, req, http.StatusOK)
banner, rev2, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context())
assert.NotEqual(t, rev1, rev2)
assert.True(t, has)
assert.Equal(t, setting.WebBannerType{
DisplayEnabled: true,
ContentMessage: "test-msg",
StartTimeUnix: 123,
EndTimeUnix: 456,
}, banner)
})
}
@@ -43,18 +43,16 @@ func TestAdminViewUser(t *testing.T) {
func TestAdminEditUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
testSuccessfullEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"})
testSuccessfulEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"})
}
func testSuccessfullEdit(t *testing.T, formData user_model.User) {
func testSuccessfulEdit(t *testing.T, formData user_model.User) {
makeRequest(t, formData, http.StatusSeeOther)
}
func makeRequest(t *testing.T, formData user_model.User, headerCode int) {
session := loginUser(t, "user1")
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", "/-/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit", map[string]string{
"_csrf": csrf,
"user_name": formData.Name,
"login_name": formData.LoginName,
"login_type": "0-0",
@@ -96,10 +94,7 @@ func TestAdminDeleteUser(t *testing.T) {
query = "?purge=true"
}
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query), map[string]string{
"_csrf": csrf,
})
req := NewRequest(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query))
session.MakeRequest(t, req, http.StatusSeeOther)
assertUserDeleted(t, entry.userID)
@@ -6,26 +6,33 @@ package integration
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"fmt"
"io"
"mime"
"net/http"
"strings"
"testing"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/actions"
actions_service "code.gitea.io/gitea/services/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -45,45 +52,168 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
assert.NoError(t, err)
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: 4,
Name: "artifact",
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
table := []struct {
name string
version int32
contentType string
blockID bool
noLength bool
append int
path string
}{
{
name: "artifact",
version: 4,
path: "artifact.zip",
},
{
name: "artifact2",
version: 4,
blockID: true,
},
{
name: "artifact3",
version: 4,
noLength: true,
},
{
name: "artifact4",
version: 4,
blockID: true,
noLength: true,
},
{
name: "artifact5",
version: 7,
blockID: true,
},
{
name: "artifact6",
version: 7,
append: 2,
noLength: true,
},
{
name: "artifact7",
version: 7,
append: 3,
blockID: true,
noLength: true,
},
{
name: "artifact8",
version: 7,
append: 4,
blockID: true,
},
{
name: "artifact9.json",
version: 7,
contentType: "application/json",
},
{
name: "artifact10",
version: 7,
contentType: "application/zip",
path: "artifact10.zip",
},
{
name: "artifact11.zip",
version: 7,
contentType: "application/zip",
path: "artifact11.zip",
},
}
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
for _, entry := range table {
t.Run(entry.name, func(t *testing.T) {
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: entry.version,
Name: entry.name,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil),
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// upload artifact chunk
body := strings.Repeat("A", 1024)
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
MakeRequest(t, req, http.StatusCreated)
h := sha256.New()
t.Logf("Create artifact confirm")
blocks := make([]string, 0, util.Iif(entry.blockID, entry.append+1, 0))
sha := sha256.Sum256([]byte(body))
// get upload url
for i := range entry.append + 1 {
url := uploadResp.SignedUploadUrl
// See https://learn.microsoft.com/en-us/rest/api/storageservices/append-block
// See https://learn.microsoft.com/en-us/rest/api/storageservices/put-block
if entry.blockID {
blockID := base64.RawURLEncoding.EncodeToString(fmt.Append([]byte("SOME_BIG_BLOCK_ID_"), i))
blocks = append(blocks, blockID)
url += "&comp=block&blockid=" + blockID
} else {
url += "&comp=appendBlock"
}
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: "artifact",
Size: 1024,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
// upload artifact chunk
body := strings.Repeat("A", 1024)
_, _ = h.Write([]byte(body))
var bodyReader io.Reader = strings.NewReader(body)
if entry.noLength {
bodyReader = io.MultiReader(bodyReader)
}
req = NewRequestWithBody(t, "PUT", url, bodyReader)
MakeRequest(t, req, http.StatusCreated)
}
if entry.blockID && entry.append > 0 {
// https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// upload artifact blockList
blockList := &actions.BlockList{
Latest: blocks,
}
rawBlockList, err := xml.Marshal(blockList)
assert.NoError(t, err)
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
MakeRequest(t, req, http.StatusCreated)
}
sha := h.Sum(nil)
t.Logf("Create artifact confirm")
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: entry.name,
Size: int64(entry.append+1) * 1024,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha)),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId})
if entry.contentType != "" {
assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType)
} else {
assert.Equal(t, "application/zip", artifact.ContentEncodingOrType)
}
if entry.path != "" {
assert.Equal(t, entry.path, artifact.ArtifactPath)
}
assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status)
assert.Equal(t, int64(entry.append+1)*1024, artifact.FileSize)
assert.Equal(t, int64(entry.append+1)*1024, artifact.FileCompressedSize)
})
}
}
func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
@@ -106,8 +236,7 @@ func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
url := uploadResp.SignedUploadUrl + "&comp=block"
// upload artifact chunk
body := strings.Repeat("B", 1024)
@@ -151,8 +280,7 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
url := uploadResp.SignedUploadUrl + "&comp=block"
// upload artifact chunk
body := strings.Repeat("A", 1024)
@@ -198,9 +326,8 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload urls
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=%2f..%2fmyfile"
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
url := uploadResp.SignedUploadUrl + "&comp=block&blockid=%2f..%2fmyfile"
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// upload artifact chunk
body := strings.Repeat("A", 1024)
@@ -247,63 +374,126 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) {
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
assert.NoError(t, err)
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: 4,
Name: "artifactWithChunksOutOfOrder",
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload urls
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
block1URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block1"
block2URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block2"
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
// upload artifact chunks
bodyb := strings.Repeat("B", 1024)
req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb))
MakeRequest(t, req, http.StatusCreated)
bodya := strings.Repeat("A", 1024)
req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya))
MakeRequest(t, req, http.StatusCreated)
// upload artifact blockList
blockList := &actions.BlockList{
Latest: []string{
"block1",
"block2",
},
table := []struct {
name string
artifactName string
serveDirect bool
contentType string
}{
{name: "Upload-Zip", artifactName: "artifact-v4-upload", contentType: ""},
{name: "Upload-Pdf", artifactName: "report-upload.pdf", contentType: "application/pdf"},
{name: "Upload-Html", artifactName: "report-upload.html", contentType: "application/html"},
{name: "ServeDirect-Zip", artifactName: "artifact-v4-upload-serve-direct", contentType: "", serveDirect: true},
{name: "ServeDirect-Pdf", artifactName: "report-upload-serve-direct.pdf", contentType: "application/pdf", serveDirect: true},
{name: "ServeDirect-Html", artifactName: "report-upload-serve-direct.html", contentType: "application/html", serveDirect: true},
}
rawBlockList, err := xml.Marshal(blockList)
assert.NoError(t, err)
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
MakeRequest(t, req, http.StatusCreated)
t.Logf("Create artifact confirm")
for _, entry := range table {
t.Run(entry.name, func(t *testing.T) {
// Only AzureBlobStorageType supports ServeDirect Uploads
switch setting.Actions.ArtifactStorage.Type {
case setting.AzureBlobStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.serveDirect)()
default:
if entry.serveDirect {
t.Skip()
}
}
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: util.Iif[int32](entry.contentType != "", 7, 4),
Name: entry.artifactName,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil),
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
if !entry.serveDirect {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
}
sha := sha256.Sum256([]byte(bodya + bodyb))
// get upload urls
block1URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block1"))
block2URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block2"))
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: "artifactWithChunksOutOfOrder",
Size: 2048,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
// upload artifact chunks
bodyb := strings.Repeat("B", 1024)
req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
bodya := strings.Repeat("A", 1024)
req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
// upload artifact blockList
blockList := &actions.BlockList{
Latest: []string{
base64.RawURLEncoding.EncodeToString([]byte("block1")),
base64.RawURLEncoding.EncodeToString([]byte("block2")),
},
}
rawBlockList, err := xml.Marshal(blockList)
assert.NoError(t, err)
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
t.Logf("Create artifact confirm")
sha := sha256.Sum256([]byte(bodya + bodyb))
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: entry.artifactName,
Size: 2048,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId})
if entry.contentType != "" {
assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType)
} else {
assert.Equal(t, "application/zip", artifact.ContentEncodingOrType)
}
assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status)
assert.Equal(t, int64(2048), artifact.FileSize)
assert.Equal(t, int64(2048), artifact.FileCompressedSize)
})
}
}
func TestActionsArtifactV4DownloadSingle(t *testing.T) {
@@ -312,33 +502,97 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) {
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
assert.NoError(t, err)
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
NameFilter: wrapperspb.String("artifact-v4-download"),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var listResp actions.ListArtifactsResponse
protojson.Unmarshal(resp.Body.Bytes(), &listResp)
assert.Len(t, listResp.Artifacts, 1)
table := []struct {
Name string
ArtifactName string
FileName string
ServeDirect bool
ContentType string
ContentDisposition string
}{
{Name: "Download-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip"},
{Name: "Download-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf"},
{Name: "Download-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html"},
{Name: "ServeDirect-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip", ServeDirect: true},
{Name: "ServeDirect-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf", ServeDirect: true},
{Name: "ServeDirect-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html", ServeDirect: true},
}
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
Name: "artifact-v4-download",
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.GetSignedArtifactURLResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.NotEmpty(t, finalizeResp.SignedUrl)
for _, entry := range table {
t.Run(entry.Name, func(t *testing.T) {
switch setting.Actions.ArtifactStorage.Type {
case setting.AzureBlobStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.ServeDirect)()
case setting.MinioStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.MinioConfig.ServeDirect, entry.ServeDirect)()
default:
if entry.ServeDirect {
t.Skip()
}
}
req = NewRequest(t, "GET", finalizeResp.SignedUrl)
resp = MakeRequest(t, req, http.StatusOK)
body := strings.Repeat("D", 1024)
assert.Equal(t, body, resp.Body.String())
// list artifacts by name
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
NameFilter: wrapperspb.String(entry.ArtifactName),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var listResp actions.ListArtifactsResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
require.Len(t, listResp.Artifacts, 1)
// list artifacts by id
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
IdFilter: wrapperspb.Int64(listResp.Artifacts[0].DatabaseId),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
assert.Len(t, listResp.Artifacts, 1)
// acquire artifact download url
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
Name: entry.ArtifactName,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.GetSignedArtifactURLResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp))
assert.NotEmpty(t, finalizeResp.SignedUrl)
body := strings.Repeat("D", 1024)
var contentDisposition string
if entry.ServeDirect {
externalReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, finalizeResp.SignedUrl, nil)
require.NoError(t, err)
externalResp, err := http.DefaultClient.Do(externalReq)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, externalResp.StatusCode)
assert.Equal(t, entry.ContentType, externalResp.Header.Get("Content-Type"))
contentDisposition = externalResp.Header.Get("Content-Disposition")
buf := make([]byte, 1024)
n, err := io.ReadAtLeast(externalResp.Body, buf, len(buf))
externalResp.Body.Close()
require.NoError(t, err)
assert.Equal(t, len(buf), n)
assert.Equal(t, body, string(buf))
} else {
req = NewRequest(t, "GET", finalizeResp.SignedUrl)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, entry.ContentType, resp.Header().Get("Content-Type"))
contentDisposition = resp.Header().Get("Content-Disposition")
assert.Equal(t, body, resp.Body.String())
}
disposition, param, err := mime.ParseMediaType(contentDisposition)
require.NoError(t, err)
assert.Equal(t, "inline", disposition)
assert.Equal(t, entry.FileName, param["filename"])
})
}
}
func TestActionsArtifactV4RunDownloadSinglePublicApi(t *testing.T) {
@@ -469,7 +723,7 @@ func TestActionsArtifactV4ListAndGetPublicApi(t *testing.T) {
for _, artifact := range listResp.Entries {
assert.Contains(t, artifact.URL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), artifact.ID))
assert.Contains(t, artifact.ArchiveDownloadURL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), artifact.ID))
req = NewRequestWithBody(t, "GET", listResp.Entries[0].URL, nil).
req = NewRequestWithBody(t, "GET", artifact.URL, nil).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
@@ -0,0 +1,54 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func testActionUserSignIn(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/user").
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp := MakeRequest(t, req, http.StatusOK)
var u api.User
DecodeJSON(t, resp, &u)
assert.Equal(t, "gitea-actions", u.UserName)
}
func testActionUserAccessPublicRepo(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md").
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type"))
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md").
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type"))
}
func testActionUserNoAccessOtherPrivateRepo(t *testing.T) {
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo2/raw/README.md").
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
MakeRequest(t, req, http.StatusNotFound)
}
func TestActionUserAccessPermission(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("ActionUserSignIn", testActionUserSignIn)
t.Run("ActionUserAccessPublicRepo", testActionUserAccessPublicRepo)
t.Run("ActionUserNoAccessOtherPrivateRepo", testActionUserNoAccessOtherPrivateRepo)
}
@@ -6,16 +6,21 @@ package integration
import (
"fmt"
"net/http"
"slices"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIActionsGetWorkflowRun(t *testing.T) {
@@ -26,15 +31,45 @@ func TestAPIActionsGetWorkflowRun(t *testing.T) {
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
t.Run("GetRun", func(t *testing.T) {
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
})
t.Run("GetJobSteps", func(t *testing.T) {
// Insert task steps for task_id 53 (job 198) so the API can return them once the backend loads them
_, err := db.GetEngine(t.Context()).Insert(&actions_model.ActionTaskStep{
Name: "main",
TaskID: 53,
Index: 0,
RepoID: repo.ID,
Status: actions_model.StatusSuccess,
Started: timeutil.TimeStamp(1683636528),
Stopped: timeutil.TimeStamp(1683636626),
})
require.NoError(t, err)
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs", repo.FullName())).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var jobList api.ActionWorkflowJobsResponse
err = json.Unmarshal(resp.Body.Bytes(), &jobList)
require.NoError(t, err)
job198Idx := slices.IndexFunc(jobList.Entries, func(job *api.ActionWorkflowJob) bool { return job.ID == 198 })
require.NotEqual(t, -1, job198Idx, "expected to find job 198 in run 795 jobs list")
job198 := jobList.Entries[job198Idx]
require.NotEmpty(t, job198.Steps, "job must return at least one step when task has steps")
assert.Equal(t, "main", job198.Steps[0].Name, "first step name")
})
}
func TestAPIActionsGetWorkflowJob(t *testing.T) {
@@ -134,3 +169,126 @@ func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository,
assert.Equal(t, expected, findTask1)
assert.Equal(t, expected, findTask2)
}
func TestAPIActionsRerunWorkflowRun(t *testing.T) {
defer prepareTestEnvActionsArtifacts(t)()
t.Run("NotDone", func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/rerun", repo.FullName())).
AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusBadRequest)
})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
t.Run("Success", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
AddTokenAuth(writeToken)
resp := MakeRequest(t, req, http.StatusCreated)
var rerunResp api.ActionWorkflowRun
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
require.NoError(t, err)
assert.Equal(t, int64(795), rerunResp.ID)
assert.Equal(t, "queued", rerunResp.Status)
assert.Equal(t, "c2d72f548424103f01ee1dc02889c1e2bff816b0", rerunResp.HeadSha)
run, err := actions_model.GetRunByRepoAndID(t.Context(), repo.ID, 795)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, run.Status)
assert.Equal(t, timeutil.TimeStamp(0), run.Started)
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
job198, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 198)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, job198.Status)
assert.Equal(t, int64(0), job198.TaskID)
job199, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 199)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
assert.Equal(t, int64(0), job199.TaskID)
})
t.Run("ForbiddenWithoutWriteScope", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
AddTokenAuth(readToken)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("NotFound", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/rerun", repo.FullName())).
AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusNotFound)
})
}
func TestAPIActionsRerunWorkflowJob(t *testing.T) {
defer prepareTestEnvActionsArtifacts(t)()
t.Run("NotDone", func(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/jobs/194/rerun", repo.FullName())).
AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusBadRequest)
})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, user.Name)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
t.Run("Success", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
AddTokenAuth(writeToken)
resp := MakeRequest(t, req, http.StatusCreated)
var rerunResp api.ActionWorkflowJob
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
require.NoError(t, err)
assert.Equal(t, int64(199), rerunResp.ID)
assert.Equal(t, "queued", rerunResp.Status)
run, err := actions_model.GetRunByRepoAndID(t.Context(), repo.ID, 795)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, run.Status)
job198, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 198)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusSuccess, job198.Status)
assert.Equal(t, int64(53), job198.TaskID)
job199, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 199)
require.NoError(t, err)
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
assert.Equal(t, int64(0), job199.TaskID)
})
t.Run("ForbiddenWithoutWriteScope", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
AddTokenAuth(readToken)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("NotFoundJob", func(t *testing.T) {
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/999999/rerun", repo.FullName())).
AddTokenAuth(writeToken)
MakeRequest(t, req, http.StatusNotFound)
})
}
@@ -24,6 +24,12 @@ func TestAPIActionsRunner(t *testing.T) {
t.Run("RepoRunner", testActionsRunnerRepo)
}
func newRunnerUpdateRequest(t *testing.T, url string, disabled bool) *RequestWrapper {
return NewRequestWithJSON(t, http.MethodPatch, url, api.EditActionRunnerOption{
Disabled: &disabled,
})
}
func testActionsRunnerAdmin(t *testing.T) {
defer tests.PrepareTestEnv(t)()
adminUsername := "user1"
@@ -45,22 +51,39 @@ func testActionsRunnerAdmin(t *testing.T) {
require.NotEqual(t, -1, idx)
expectedRunner := runnerList.Entries[idx]
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Name)
assert.False(t, expectedRunner.Disabled)
assert.False(t, expectedRunner.Ephemeral)
assert.Len(t, expectedRunner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
disabledRunner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &disabledRunner)
assert.True(t, disabledRunner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify all returned runners can be requested and deleted
for _, runnerEntry := range runnerList.Entries {
// Verify get the runner by id
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
runnerResp := MakeRequest(t, req, http.StatusOK)
runnerResp = MakeRequest(t, req, http.StatusOK)
runner := api.ActionRunner{}
DecodeJSON(t, runnerResp, &runner)
assert.Equal(t, runnerEntry.Name, runner.Name)
assert.Equal(t, runnerEntry.ID, runner.ID)
assert.Equal(t, runnerEntry.Disabled, runner.Disabled)
assert.Equal(t, runnerEntry.Ephemeral, runner.Ephemeral)
assert.ElementsMatch(t, runnerEntry.Labels, runner.Labels)
@@ -93,6 +116,7 @@ func testActionsRunnerUser(t *testing.T) {
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-user", runnerList.Entries[0].Name)
assert.Equal(t, int64(34346), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Disabled)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
@@ -107,11 +131,26 @@ func testActionsRunnerUser(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-user", runner.Name)
assert.Equal(t, int64(34346), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -136,6 +175,7 @@ func testActionsRunnerOwner(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
assert.Equal(t, int64(34347), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
@@ -165,6 +205,7 @@ func testActionsRunnerOwner(t *testing.T) {
require.NotNil(t, expectedRunner)
assert.Equal(t, "runner_to_be_deleted-org", expectedRunner.Name)
assert.Equal(t, int64(34347), expectedRunner.ID)
assert.False(t, expectedRunner.Disabled)
assert.False(t, expectedRunner.Ephemeral)
assert.Len(t, expectedRunner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
@@ -179,11 +220,26 @@ func testActionsRunnerOwner(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
assert.Equal(t, int64(34347), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -202,6 +258,22 @@ func testActionsRunnerOwner(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("DisableReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("UpdateReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetRepoScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
@@ -244,6 +316,7 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
assert.Equal(t, int64(34348), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
@@ -269,6 +342,7 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Len(t, runnerList.Entries, 1)
assert.Equal(t, "runner_to_be_deleted-repo1", runnerList.Entries[0].Name)
assert.Equal(t, int64(34348), runnerList.Entries[0].ID)
assert.False(t, runnerList.Entries[0].Disabled)
assert.False(t, runnerList.Entries[0].Ephemeral)
assert.Len(t, runnerList.Entries[0].Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
@@ -283,11 +357,26 @@ func testActionsRunnerRepo(t *testing.T) {
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
assert.Equal(t, int64(34348), runner.ID)
assert.False(t, runner.Disabled)
assert.False(t, runner.Ephemeral)
assert.Len(t, runner.Labels, 2)
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
assert.Equal(t, "linux", runner.Labels[1].Name)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
runnerResp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, runnerResp, &runner)
assert.True(t, runner.Disabled)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Verify delete the runner by id
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
@@ -306,6 +395,22 @@ func testActionsRunnerRepo(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("DisableReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348), true).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("UpdateReadScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348), false).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("GetOrganizationScopeForbidden", func(t *testing.T) {
userUsername := "user2"
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
@@ -1,88 +0,0 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/activitypub"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests"
ap "github.com/go-ap/activitypub"
"github.com/stretchr/testify/assert"
)
func TestActivityPubPerson(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
t.Run("ExistingPerson", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
userID := 2
username := "user2"
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/user-id/%v", userID))
resp := MakeRequest(t, req, http.StatusOK)
body := resp.Body.Bytes()
assert.Contains(t, string(body), "@context")
var person ap.Person
err := person.UnmarshalJSON(body)
assert.NoError(t, err)
assert.Equal(t, ap.PersonType, person.Type)
assert.Equal(t, username, person.PreferredUsername.String())
keyID := person.GetID().String()
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v$", userID), keyID)
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v/outbox$", userID), person.Outbox.GetID().String())
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v/inbox$", userID), person.Inbox.GetID().String())
pubKey := person.PublicKey
assert.NotNil(t, pubKey)
publicKeyID := keyID + "#main-key"
assert.Equal(t, pubKey.ID.String(), publicKeyID)
pubKeyPem := pubKey.PublicKeyPem
assert.NotNil(t, pubKeyPem)
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
})
t.Run("MissingPerson", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/api/v1/activitypub/user-id/999999999")
resp := MakeRequest(t, req, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "user does not exist")
})
t.Run("MissingPersonInbox", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
srv := httptest.NewServer(testWebRoutes)
defer srv.Close()
defer test.MockVariableValue(&setting.AppURL, srv.URL+"/")()
username1 := "user1"
ctx := t.Context()
user1, err := user_model.GetUserByName(ctx, username1)
assert.NoError(t, err)
user1url := srv.URL + "/api/v1/activitypub/user-id/1#main-key"
c, err := activitypub.NewClient(t.Context(), user1, user1url)
assert.NoError(t, err)
user2inboxurl := srv.URL + "/api/v1/activitypub/user-id/2/inbox"
// Signed request succeeds
resp, err := c.Post([]byte{}, user2inboxurl)
assert.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
// Unsigned request fails
req := NewRequest(t, "POST", user2inboxurl)
MakeRequest(t, req, http.StatusInternalServerError)
})
}
@@ -4,6 +4,8 @@
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -235,7 +237,40 @@ func TestAPIRenameBranch(t *testing.T) {
MakeRequest(t, req, http.StatusCreated)
resp := testAPIRenameBranch(t, "user2", "user2", "repo1", from, "new-branch-name", http.StatusForbidden)
assert.Contains(t, resp.Body.String(), "Branch is protected by glob-based protection rules.")
assert.Contains(t, resp.Body.String(), "Failed to rename branch due to branch protection rules.")
})
t.Run("RenameBranchToMatchProtectionRulesWithAllowedUser", func(t *testing.T) {
// allow an admin (the owner in this case) to rename a regular branch to one that matches a branch protection rule
repoName := "repo1"
ownerName := "user2"
from := "regular-branch-1"
ctx := NewAPITestContext(t, ownerName, repoName, auth_model.AccessTokenScopeWriteRepository)
testAPICreateBranch(t, ctx.Session, ownerName, repoName, "", from, http.StatusCreated)
// NOTE: The protected/** branch protection rule was created in a previous test, with push enabled.
testAPIRenameBranch(t, ownerName, ownerName, repoName, from, "protected/2", http.StatusNoContent)
})
t.Run("RenameBranchToMatchProtectionRulesWithUnauthorizedUser", func(t *testing.T) {
// don't allow renaming a regular branch to a protected branch if the doer is not in the push whitelist
repoName := "repo1"
ownerName := "user2"
pushWhitelist := []string{ownerName}
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", ownerName, repoName),
&api.BranchProtection{
RuleName: "owner-protected/**",
PushWhitelistUsernames: pushWhitelist,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
from := "regular-branch-2"
ctx := NewAPITestContext(t, ownerName, repoName, auth_model.AccessTokenScopeWriteRepository)
testAPICreateBranch(t, ctx.Session, ownerName, repoName, "", from, http.StatusCreated)
unprivilegedUser := "user40"
resp := testAPIRenameBranch(t, unprivilegedUser, ownerName, repoName, from, "owner-protected/1", http.StatusForbidden)
assert.Contains(t, resp.Body.String(), "Failed to rename branch due to branch protection rules.")
})
t.Run("RenameBranchNormalScenario", func(t *testing.T) {
testAPIRenameBranch(t, "user2", "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent)
@@ -243,6 +278,79 @@ func TestAPIRenameBranch(t *testing.T) {
})
}
func TestAPIUpdateBranchReference(t *testing.T) {
defer tests.PrepareTestEnv(t)()
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
ctx := NewAPITestContext(t, "user2", "update-branch", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
giteaURL.Path = ctx.GitPath()
var defaultBranch string
t.Run("CreateRepo", doAPICreateRepository(ctx, false, func(t *testing.T, repo api.Repository) {
defaultBranch = repo.DefaultBranch
}))
createBranchReq := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/branches", ctx.Username, ctx.Reponame), &api.CreateBranchRepoOption{
BranchName: "feature",
OldRefName: defaultBranch,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, createBranchReq, http.StatusCreated)
var featureInitialCommit string
t.Run("LoadFeatureBranch", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
featureInitialCommit = branch.Commit.ID
assert.NotEmpty(t, featureInitialCommit)
}))
content := base64.StdEncoding.EncodeToString([]byte("branch update test"))
var newCommit string
doAPICreateFile(ctx, "docs/update.txt", &api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: defaultBranch,
NewBranchName: defaultBranch,
Message: "add docs/update.txt",
},
ContentBase64: content,
}, func(t *testing.T, resp api.FileResponse) {
newCommit = resp.Commit.SHA
assert.NotEmpty(t, newCommit)
})(t)
updateReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: newCommit,
OldCommitID: featureInitialCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, updateReq, http.StatusNoContent)
t.Run("FastForwardApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
assert.Equal(t, newCommit, branch.Commit.ID)
}))
staleReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: newCommit,
OldCommitID: featureInitialCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, staleReq, http.StatusUnprocessableEntity)
nonFFReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: featureInitialCommit,
OldCommitID: newCommit,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, nonFFReq, http.StatusUnprocessableEntity)
forceReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
NewCommitID: featureInitialCommit,
OldCommitID: newCommit,
Force: true,
}).AddTokenAuth(ctx.Token)
ctx.Session.MakeRequest(t, forceReq, http.StatusNoContent)
t.Run("ForceApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
assert.Equal(t, featureInitialCommit, branch.Commit.ID)
}))
})
}
func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.RenameBranchRepoOption{
@@ -254,15 +362,20 @@ func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to s
func TestAPIBranchProtection(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Branch protection on branch that not exist
testAPICreateBranchProtection(t, "master/doesnotexist", 1, http.StatusCreated)
// Can create branch protection on branch that not exist
testAPICreateBranchProtection(t, "non-existing/branch", 1, http.StatusCreated)
testAPIGetBranchProtection(t, "non-existing/branch", http.StatusOK)
testAPIDeleteBranchProtection(t, "non-existing/branch", http.StatusNoContent)
// Get branch protection on branch that exist but not branch protection
testAPIGetBranchProtection(t, "master", http.StatusNotFound)
testAPICreateBranchProtection(t, "master", 2, http.StatusCreated)
testAPICreateBranchProtection(t, "master", 1, http.StatusCreated)
// Can only create once
testAPICreateBranchProtection(t, "master", 0, http.StatusForbidden)
testAPICreateBranchProtection(t, "other-branch", 2, http.StatusCreated)
// Can't delete a protected branch
testAPIDeleteBranch(t, "master", http.StatusForbidden)
@@ -294,6 +407,8 @@ func TestAPIBranchProtection(t *testing.T) {
// Test branch deletion
testAPIDeleteBranch(t, "master", http.StatusForbidden)
testAPIDeleteBranch(t, "branch2", http.StatusNoContent)
testAPIDeleteBranch(t, "branch2", http.StatusNotFound) // deleted branch, there is a record in DB with IsDelete=true
testAPIDeleteBranch(t, "no-such-branch", http.StatusNotFound) // non-existing branch, not exist in git or DB
}
func TestAPICreateBranchWithSyncBranches(t *testing.T) {
@@ -303,7 +418,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, branches, 6)
assert.Len(t, branches, 8)
// make a broke repository with no branch on database
_, err = db.DeleteByBean(t.Context(), git_model.Branch{RepoID: 1})
@@ -320,7 +435,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
RepoID: 1,
})
assert.NoError(t, err)
assert.Len(t, branches, 7)
assert.Len(t, branches, 9)
branches, err = db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{
RepoID: 1,
@@ -19,15 +19,35 @@ import (
"github.com/stretchr/testify/assert"
)
func TestCreateForkNoLogin(t *testing.T) {
func TestAPIFork(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("CreateForkNoLogin", testCreateForkNoLogin)
t.Run("CreateForkOrgNoCreatePermission", testCreateForkOrgNoCreatePermission)
t.Run("APIForkListLimitedAndPrivateRepos", testAPIForkListLimitedAndPrivateRepos)
t.Run("GetPrivateReposForks", testGetPrivateReposForks)
}
func testCreateForkNoLogin(t *testing.T) {
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
MakeRequest(t, req, http.StatusUnauthorized)
}
func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testCreateForkOrgNoCreatePermission(t *testing.T) {
user4Sess := loginUser(t, "user4")
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
canCreate, err := org_model.OrgFromUser(org).CanCreateOrgRepo(t.Context(), 4)
assert.NoError(t, err)
assert.False(t, canCreate)
user4Token := getTokenForLoggedInUser(t, user4Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
Organization: &org.Name,
}).AddTokenAuth(user4Token)
MakeRequest(t, req, http.StatusForbidden)
}
func testAPIForkListLimitedAndPrivateRepos(t *testing.T) {
user1Sess := loginUser(t, "user1")
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
@@ -64,10 +84,7 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
resp := MakeRequest(t, req, http.StatusOK)
var forks []*api.Repository
DecodeJSON(t, resp, &forks)
forks := DecodeJSON(t, resp, []*api.Repository{})
assert.Empty(t, forks)
assert.Equal(t, "0", resp.Header().Get("X-Total-Count"))
})
@@ -78,9 +95,7 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
var forks []*api.Repository
DecodeJSON(t, resp, &forks)
forks := DecodeJSON(t, resp, []*api.Repository{})
assert.Len(t, forks, 2)
assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
@@ -88,28 +103,22 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
resp = MakeRequest(t, req, http.StatusOK)
forks = []*api.Repository{}
DecodeJSON(t, resp, &forks)
forks = DecodeJSON(t, resp, []*api.Repository{})
assert.Len(t, forks, 2)
assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
})
}
func TestGetPrivateReposForks(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testGetPrivateReposForks(t *testing.T) {
user1Sess := loginUser(t, "user1")
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // private repository
privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository)
forkedRepoName := "forked-repo"
// create fork from a private repository
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+repo2.FullName()+"/forks", &api.CreateForkOption{
Organization: &privateOrg.Name,
Name: &forkedRepoName,
Name: new("forked-repo"),
}).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusAccepted)
@@ -117,8 +126,7 @@ func TestGetPrivateReposForks(t *testing.T) {
req = NewRequest(t, "GET", "/api/v1/repos/"+repo2.FullName()+"/forks").AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
forks := []*api.Repository{}
DecodeJSON(t, resp, &forks)
forks := DecodeJSON(t, resp, []*api.Repository{})
assert.Len(t, forks, 1)
assert.Equal(t, "1", resp.Header().Get("X-Total-Count"))
assert.Equal(t, "forked-repo", forks[0].Name)
@@ -63,8 +63,8 @@ func TestGPGKeys(t *testing.T) {
t.Run("CreateInvalidGPGKey", func(t *testing.T) {
testCreateInvalidGPGKey(t, tc.makeRequest, tc.token, tc.results[4])
})
t.Run("CreateNoneRegistredEmailGPGKey", func(t *testing.T) {
testCreateNoneRegistredEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[5])
t.Run("CreateNoneRegisteredEmailGPGKey", func(t *testing.T) {
testCreateNoneRegisteredEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[5])
})
t.Run("CreateValidGPGKey", func(t *testing.T) {
testCreateValidGPGKey(t, tc.makeRequest, tc.token, tc.results[6])
@@ -179,7 +179,7 @@ func testCreateInvalidGPGKey(t *testing.T, makeRequest makeRequestFunc, token st
testCreateGPGKey(t, makeRequest, token, expected, "invalid_key")
}
func testCreateNoneRegistredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
func testCreateNoneRegisteredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFmGUygBCACjCNbKvMGgp0fd5vyFW9olE1CLCSyyF9gQN2hSuzmZLuAZF2Kh
@@ -95,9 +95,7 @@ func TestHTTPSigCert(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", "/user/settings/keys", map[string]string{
"_csrf": csrf,
"content": "user1",
"title": "principal",
"type": "principal",
@@ -8,6 +8,7 @@ import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@@ -179,3 +180,19 @@ func TestAPIRepoValidateIssueConfig(t *testing.T) {
assert.NotEmpty(t, issueConfigValidation.Message)
})
}
func TestAPIRepoIssueConfigRequiresCodeUnit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
for _, path := range []string{
fmt.Sprintf("/api/v1/repos/%s/issue_config", repo.FullName()),
fmt.Sprintf("/api/v1/repos/%s/issue_config/validate", repo.FullName()),
} {
req := NewRequest(t, "GET", path).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
}
@@ -29,7 +29,7 @@ func enableRepoDependencies(t *testing.T, repoID int64) {
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypeIssues})
repoUnit.IssuesConfig().EnableDependencies = true
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
assert.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
}
func TestAPICreateIssueDependencyCrossRepoPermission(t *testing.T) {
@@ -44,7 +44,7 @@ func TestAPIIssuesReactions(t *testing.T) {
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
Reaction: "zzz",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
MakeRequest(t, req, http.StatusNoContent)
// Add allowed reaction
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
@@ -55,7 +55,10 @@ func TestAPIIssuesReactions(t *testing.T) {
DecodeJSON(t, resp, &apiNewReaction)
// Add existing reaction
MakeRequest(t, req, http.StatusForbidden)
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
Reaction: "rocket",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Blocked user can't react to comment
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
@@ -111,7 +114,7 @@ func TestAPICommentReactions(t *testing.T) {
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
Reaction: "eyes",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
MakeRequest(t, req, http.StatusNoContent)
t.Run("UnrelatedCommentID", func(t *testing.T) {
// Using the ID of a comment that does not belong to the repository must fail
@@ -142,7 +145,10 @@ func TestAPICommentReactions(t *testing.T) {
DecodeJSON(t, resp, &apiNewReaction)
// Add existing reaction
MakeRequest(t, req, http.StatusForbidden)
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
Reaction: "+1",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
// Get end result of reaction list of issue #1
req = NewRequest(t, "GET", urlStr).
@@ -8,10 +8,12 @@ import (
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
@@ -53,3 +55,19 @@ about: bar
assert.Equal(t, "error occurs when parsing issue template: count=2", resp.Header().Get("X-Gitea-Warning"))
})
}
func TestAPIIssueTemplateRequiresCodeUnit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
issueTemplatesURL := "/api/v1/repos/" + repo.FullName() + "/issue_templates"
languagesURL := "/api/v1/repos/" + repo.FullName() + "/languages"
req := NewRequest(t, "GET", issueTemplatesURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", languagesURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
@@ -19,14 +19,25 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIListIssues(t *testing.T) {
func TestAPIIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("ListIssues", testAPIListIssues)
t.Run("ListIssuesPublicOnly", testAPIListIssuesPublicOnly)
t.Run("SearchIssues", testAPISearchIssues)
t.Run("SearchIssuesWithLabels", testAPISearchIssuesWithLabels)
t.Run("EditIssue", testAPIEditIssue)
t.Run("IssueContentVersion", testAPIIssueContentVersion)
t.Run("CreateIssue", testAPICreateIssue)
t.Run("CreateIssueParallel", testAPICreateIssueParallel)
}
func testAPIListIssues(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -74,9 +85,7 @@ func TestAPIListIssues(t *testing.T) {
}
}
func TestAPIListIssuesPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIListIssuesPublicOnly(t *testing.T) {
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID})
@@ -99,11 +108,10 @@ func TestAPIListIssuesPublicOnly(t *testing.T) {
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
MakeRequest(t, req, http.StatusForbidden)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPICreateIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPICreateIssue(t *testing.T) {
const body, title = "apiTestBody", "apiTestTitle"
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
@@ -141,9 +149,7 @@ func TestAPICreateIssue(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
}
func TestAPICreateIssueParallel(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPICreateIssueParallel(t *testing.T) {
// FIXME: There seems to be a bug in github.com/mattn/go-sqlite3 with sqlite_unlock_notify, when doing concurrent writes to the same database,
// some requests may get stuck in "go-sqlite3.(*SQLiteRows).Next", "go-sqlite3.(*SQLiteStmt).exec" and "go-sqlite3.unlock_notify_wait",
// because the "unlock_notify_wait" never returns and the internal lock never gets releases.
@@ -151,7 +157,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
// The trigger is: a previous test created issues and made the real issue indexer queue start processing, then this test does concurrent writing.
// Adding this "Sleep" makes go-sqlite3 "finish" some internal operations before concurrent writes and then won't get stuck.
// To reproduce: make a new test run these 2 tests enough times:
// > func TestBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } }
// > func testBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } }
// Usually the test gets stuck in fewer than 10 iterations without this "sleep".
time.Sleep(time.Second)
@@ -196,9 +202,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
wg.Wait()
}
func TestAPIEditIssue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIEditIssue(t *testing.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})
@@ -262,11 +266,9 @@ func TestAPIEditIssue(t *testing.T) {
assert.Equal(t, title, issueAfter.Title)
}
func TestAPISearchIssues(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// as this API was used in the frontend, it uses UI page size
expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
func testAPISearchIssues(t *testing.T) {
defer test.MockVariableValue(&setting.API.DefaultPagingNum, 20)()
expectedIssueCount := 20 // 20 is from the fixtures
link, _ := url.Parse("/api/v1/repos/issues/search")
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue)
@@ -361,11 +363,37 @@ func TestAPISearchIssues(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 2)
query = url.Values{"created": {"1"}} // issues created by the auth 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, 5)
query = url.Values{"created": {"1"}, "type": {"pulls"}} // prs created by the auth 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, 3)
query = url.Values{"created_by": {"user2"}} // issues created by the user2
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{"created_by": {"user2"}, "type": {"pulls"}} // prs created by user2
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, 3)
}
func TestAPISearchIssuesWithLabels(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPISearchIssuesWithLabels(t *testing.T) {
// as this API was used in the frontend, it uses UI page size
expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
@@ -420,3 +448,55 @@ func TestAPISearchIssuesWithLabels(t *testing.T) {
DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 2)
}
func testAPIIssueContentVersion(t *testing.T) {
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)
t.Run("ResponseIncludesContentVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
apiIssue := DecodeJSON(t, resp, &api.Issue{})
assert.GreaterOrEqual(t, apiIssue.ContentVersion, 0)
})
t.Run("EditWithCorrectVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
before := DecodeJSON(t, resp, &api.Issue{})
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: new("updated body with correct version"),
ContentVersion: new(before.ContentVersion),
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
after := DecodeJSON(t, resp, &api.Issue{})
assert.Equal(t, "updated body with correct version", after.Body)
assert.Greater(t, after.ContentVersion, before.ContentVersion)
})
t.Run("EditWithWrongVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: new("should fail"),
ContentVersion: new(99999),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusConflict)
})
t.Run("EditWithoutVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
Body: new("edit without version succeeds"),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
})
}
@@ -79,6 +79,12 @@ func TestAPIDeleteTrackedTime(t *testing.T) {
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
// Deletion should be scoped to the issue in the URL
time5 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 5})
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time5.ID).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
time3 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 3})
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID).
AddTokenAuth(token)
@@ -1,35 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestNodeinfo(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
req := NewRequest(t, "GET", "/api/v1/nodeinfo")
resp := MakeRequest(t, req, http.StatusOK)
VerifyJSONSchema(t, resp, "nodeinfo_2.1.json")
var nodeinfo api.NodeInfo
DecodeJSON(t, resp, &nodeinfo)
assert.True(t, nodeinfo.OpenRegistrations)
assert.Equal(t, "gitea", nodeinfo.Software.Name)
assert.Equal(t, 29, nodeinfo.Usage.Users.Total)
assert.Equal(t, 22, nodeinfo.Usage.LocalPosts)
assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
}
@@ -200,7 +200,7 @@ func TestAPINotificationPUT(t *testing.T) {
assert.False(t, apiNL[0].Pinned)
//
// Now nofication ID 2 is the first in the list and is unread.
// Now notification ID 2 is the first in the list and is unread.
//
req = NewRequest(t, "GET", "/api/v1/notifications?all=true").
AddTokenAuth(token)
@@ -212,3 +212,23 @@ func TestAPINotificationPUT(t *testing.T) {
assert.True(t, apiNL[0].Unread)
assert.False(t, apiNL[0].Pinned)
}
func TestAPINotificationPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
token := getUserToken(t, user2.Name, auth_model.AccessTokenScopeReadNotification, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/notifications").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", "/api/v1/notifications/new").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d", thread5.ID)).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
}
@@ -137,34 +137,45 @@ func TestAPIOrgGeneral(t *testing.T) {
})
t.Run("OrgEdit", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "private",
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility)
var apiOrg api.Organization
DecodeJSON(t, resp, &apiOrg)
org3Edit := api.EditOrgOption{
FullName: new("new full name"),
Description: new("new description"),
Website: new("https://org3-new-website.example.com"),
Location: new("new location"),
Visibility: new("limited"),
Email: new("org3-new-email@example.com"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
apiOrg := DecodeJSON(t, resp, &api.Organization{})
assert.Equal(t, "org3", apiOrg.Name)
assert.Equal(t, org.FullName, apiOrg.FullName)
assert.Equal(t, org.Description, apiOrg.Description)
assert.Equal(t, org.Website, apiOrg.Website)
assert.Equal(t, org.Location, apiOrg.Location)
assert.Equal(t, org.Visibility, apiOrg.Visibility)
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
assert.Equal(t, *org3Edit.Description, apiOrg.Description)
assert.Equal(t, *org3Edit.Website, apiOrg.Website)
assert.Equal(t, *org3Edit.Location, apiOrg.Location)
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
assert.Equal(t, *org3Edit.Email, apiOrg.Email)
org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
assert.Equal(t, api.VisibleTypeLimited, org3.Visibility)
// empty email can clear the email, nil fields won't change the settings
req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{
Email: new(""),
}).AddTokenAuth(user1Token)
resp = MakeRequest(t, req, http.StatusOK)
apiOrg = DecodeJSON(t, resp, &api.Organization{})
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
assert.Empty(t, apiOrg.Email)
})
t.Run("OrgEditBadVisibility", func(t *testing.T) {
t.Run("OrgEditInvalidVisibility", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "badvisibility",
Visibility: new("invalid-visibility"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
@@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
arch_module "code.gitea.io/gitea/modules/packages/arch"
"code.gitea.io/gitea/modules/test"
arch_service "code.gitea.io/gitea/services/packages/arch"
"code.gitea.io/gitea/tests"
@@ -78,34 +79,6 @@ license = MIT`)
return buf.Bytes()
}
readIndexContent := func(r io.Reader) (map[string]string, error) {
gzr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
content := make(map[string]string)
tr := tar.NewReader(gzr)
for {
hd, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
buf, err := io.ReadAll(tr)
if err != nil {
return nil, err
}
content[hd.Name] = string(buf)
}
return content, nil
}
compressions := []string{"gz", "xz", "zst"}
repositories := []string{"main", "testing", "with/slash", ""}
@@ -204,7 +177,7 @@ license = MIT`)
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
resp := MakeRequest(t, req, http.StatusOK)
content, err := readIndexContent(resp.Body)
content, err := test.ReadAllTarGzContent(resp.Body)
assert.NoError(t, err)
desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
@@ -256,7 +229,7 @@ license = MIT`)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
resp := MakeRequest(t, req, http.StatusOK)
content, err := readIndexContent(resp.Body)
content, err := test.ReadAllTarGzContent(resp.Body)
assert.NoError(t, err)
desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
@@ -311,7 +284,7 @@ license = MIT`)
req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
resp := MakeRequest(t, req, http.StatusOK)
content, err := readIndexContent(resp.Body)
content, err := test.ReadAllTarGzContent(resp.Body)
assert.NoError(t, err)
assert.Len(t, content, 2)
@@ -326,7 +299,7 @@ license = MIT`)
req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
resp = MakeRequest(t, req, http.StatusOK)
content, err = readIndexContent(resp.Body)
content, err = test.ReadAllTarGzContent(resp.Body)
assert.NoError(t, err)
assert.Len(t, content, 2)
_, has = content["gitea-test-1.0.0/desc"]
@@ -17,6 +17,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
composer_module "code.gitea.io/gitea/modules/packages/composer"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers/api/packages/composer"
"code.gitea.io/gitea/tests"
@@ -27,6 +28,8 @@ func TestPackageComposer(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
otherUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
vendorName := "gitea"
projectName := "composer-package"
@@ -251,5 +254,85 @@ func TestPackageComposer(t *testing.T) {
assert.Equal(t, repo1.HTMLURL(), pkgs[0].Source.URL)
assert.Equal(t, "git", pkgs[0].Source.Type)
assert.Equal(t, packageVersion, pkgs[0].Source.Reference)
// Private repository links remain visible to callers who can access the repository.
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
err = packages.SetRepositoryLink(t.Context(), userPkgs[0].ID, repo2.ID)
assert.NoError(t, err)
req = NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName)).
AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
result = composer.PackageMetadataResponse{}
DecodeJSON(t, resp, &result)
pkgs = result.Packages[packageName]
assert.Len(t, pkgs, 1)
assert.Equal(t, repo2.HTMLURL(), pkgs[0].Source.URL)
assert.Equal(t, "git", pkgs[0].Source.Type)
assert.Equal(t, packageVersion, pkgs[0].Source.Reference)
// Callers without repository access still get the package metadata, but not the private source URL.
req = NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName)).
AddBasicAuth(otherUser.Name)
resp = MakeRequest(t, req, http.StatusOK)
result = composer.PackageMetadataResponse{}
DecodeJSON(t, resp, &result)
pkgs = result.Packages[packageName]
assert.Len(t, pkgs, 1)
assert.Empty(t, pkgs[0].Source.URL)
assert.Empty(t, pkgs[0].Source.Type)
assert.Empty(t, pkgs[0].Source.Reference)
})
t.Run("WebVisibilityBadge", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
listReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages", user.Name)).
AddBasicAuth(user.Name)
listResp := MakeRequest(t, listReq, http.StatusOK)
listDoc := NewHTMLParser(t, listResp.Body)
assert.Equal(t, 0, listDoc.Find(".flex-item-title .ui.basic.label").Length())
viewReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/composer/%s/%s", user.Name, neturl.PathEscape(packageName), neturl.PathEscape(packageVersion))).
AddBasicAuth(user.Name)
viewResp := MakeRequest(t, viewReq, http.StatusOK)
viewDoc := NewHTMLParser(t, viewResp.Body)
assert.Equal(t, 0, viewDoc.Find(".issue-title-header .ui.basic.label").Length())
privatePackageName := privateUser.Name + "/private-composer-package"
privatePackageVersion := "1.0.0"
privateContent := test.WriteZipArchive(map[string]string{
"composer.json": `{
"name": "` + privatePackageName + `",
"description": "Private Package",
"type": "` + packageType + `",
"license": "` + packageLicense + `",
"authors": [
{
"name": "` + packageAuthor + `"
}
]
}`,
}).Bytes()
privateUploadURL := fmt.Sprintf("%sapi/packages/%s/composer?version=%s", setting.AppURL, privateUser.Name, privatePackageVersion)
uploadReq := NewRequestWithBody(t, "PUT", privateUploadURL, bytes.NewReader(privateContent)).
AddBasicAuth(privateUser.Name)
MakeRequest(t, uploadReq, http.StatusCreated)
privateSession := loginUser(t, privateUser.Name)
privateListReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages", privateUser.Name))
privateListResp := privateSession.MakeRequest(t, privateListReq, http.StatusOK)
privateListDoc := NewHTMLParser(t, privateListResp.Body)
assert.Equal(t, 1, privateListDoc.Find(".flex-item-title .ui.basic.label").Length())
assert.Equal(t, "Private", privateListDoc.Find(".flex-item-title .ui.basic.label").First().Text())
privateViewReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/composer/%s/%s", privateUser.Name, neturl.PathEscape(privatePackageName), neturl.PathEscape(privatePackageVersion)))
privateViewResp := privateSession.MakeRequest(t, privateViewReq, http.StatusOK)
privateViewDoc := NewHTMLParser(t, privateViewResp.Body)
assert.Equal(t, 1, privateViewDoc.Find(".issue-title-header .ui.basic.label").Length())
assert.Equal(t, "Private", privateViewDoc.Find(".issue-title-header .ui.basic.label").First().Text())
})
}
@@ -346,15 +346,16 @@ func TestPackageConan(t *testing.T) {
pb, err := packages.GetBlobByID(t.Context(), pf.BlobID)
assert.NoError(t, err)
if pf.Name == conanfileName {
switch pf.Name {
case conanfileName:
assert.True(t, pf.IsLead)
assert.Equal(t, int64(len(buildConanfileContent(name, version1))), pb.Size)
} else if pf.Name == conaninfoName {
case conaninfoName:
assert.False(t, pf.IsLead)
assert.Equal(t, int64(len(contentConaninfo)), pb.Size)
} else {
default:
assert.FailNow(t, "unknown file", "unknown file: %s", pf.Name)
}
}
@@ -88,34 +88,34 @@ func TestPackageContainer(t *testing.T) {
Token string `json:"token"`
}
defaultAuthenticateValues := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`}
wwwAuthenticateForPublic := []string{
`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`,
}
wwwAuthenticateForRequiredSignIn := []string{
`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`,
`Basic realm="Gitea Container Registry"`,
}
t.Run("Anonymous", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", setting.AppURL+"v2")
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate"))
assert.ElementsMatch(t, wwwAuthenticateForPublic, resp.Header().Values("WWW-Authenticate"))
req = NewRequest(t, "GET", setting.AppURL+"v2/token")
resp = MakeRequest(t, req, http.StatusOK)
tokenResponse := &TokenResponse{}
DecodeJSON(t, resp, &tokenResponse)
assert.NotEmpty(t, tokenResponse.Token)
tokenResponse := DecodeJSON(t, resp, &TokenResponse{})
require.NotEmpty(t, tokenResponse.Token)
anonymousToken = "Bearer " + tokenResponse.Token
req = NewRequest(t, "GET", setting.AppURL+"v2").
AddTokenAuth(anonymousToken)
req = NewRequest(t, "GET", setting.AppURL+"v2").AddTokenAuth(anonymousToken)
MakeRequest(t, req, http.StatusOK)
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
req = NewRequest(t, "GET", setting.AppURL+"v2")
MakeRequest(t, req, http.StatusUnauthorized)
resp = MakeRequest(t, req, http.StatusUnauthorized)
assert.ElementsMatch(t, wwwAuthenticateForRequiredSignIn, resp.Header().Values("WWW-Authenticate"))
req = NewRequest(t, "GET", setting.AppURL+"v2/token")
MakeRequest(t, req, http.StatusUnauthorized)
@@ -132,17 +132,13 @@ func TestPackageContainer(t *testing.T) {
req := NewRequest(t, "GET", setting.AppURL+"v2")
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.ElementsMatch(t, wwwAuthenticateForPublic, resp.Header().Values("WWW-Authenticate"))
assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate"))
req = NewRequest(t, "GET", setting.AppURL+"v2/token").
AddBasicAuth(user.Name)
req = NewRequest(t, "GET", setting.AppURL+"v2/token").AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
tokenResponse := &TokenResponse{}
DecodeJSON(t, resp, &tokenResponse)
tokenResponse := DecodeJSON(t, resp, &TokenResponse{})
assert.NotEmpty(t, tokenResponse.Token)
pkgMeta, err := package_service.ParseAuthorizationToken(tokenResponse.Token)
assert.NoError(t, err)
assert.Equal(t, user.ID, pkgMeta.UserID)
@@ -826,7 +822,6 @@ func TestPackageContainer(t *testing.T) {
newOwnerName := "newUsername"
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"name": newOwnerName,
"email": "user2@example.com",
"language": "en-US",
@@ -836,7 +831,6 @@ func TestPackageContainer(t *testing.T) {
t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName))
req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"name": user.Name,
"email": "user2@example.com",
"language": "en-US",
@@ -7,7 +7,9 @@ import (
"bytes"
"fmt"
"io"
"mime"
"net/http"
neturl "net/url"
"testing"
"code.gitea.io/gitea/models/packages"
@@ -140,11 +142,12 @@ func TestPackageGeneric(t *testing.T) {
t.Run("ServeDirect", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
if setting.Packages.Storage.Type == setting.MinioStorageType {
switch setting.Packages.Storage.Type {
case setting.MinioStorageType:
defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, true)()
} else if setting.Packages.Storage.Type == setting.AzureBlobStorageType {
case setting.AzureBlobStorageType:
defer test.MockVariableValue(&setting.Packages.Storage.AzureBlobConfig.ServeDirect, true)()
} else {
default:
t.Skip("Test skipped for non-Minio-storage and non-AzureBlob-storage.")
}
@@ -163,6 +166,7 @@ func TestPackageGeneric(t *testing.T) {
resp2, err := (&http.Client{}).Get(location)
assert.NoError(t, err)
defer resp2.Body.Close()
assert.Equal(t, http.StatusOK, resp2.StatusCode, location)
body, err := io.ReadAll(resp2.Body)
@@ -171,6 +175,27 @@ func TestPackageGeneric(t *testing.T) {
checkDownloadCount(3)
})
t.Run("WebAssetUsesFilename", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeGeneric)
assert.NoError(t, err)
assert.Len(t, pvs, 1)
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
assert.NoError(t, err)
assert.NotEmpty(t, pfs)
req = NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/generic/%s/%s/files/%d", user.Name, neturl.PathEscape(packageName), neturl.PathEscape(packageVersion), pfs[0].ID))
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, content, resp.Body.Bytes())
disposition, params, err := mime.ParseMediaType(resp.Header().Get("Content-Disposition"))
assert.NoError(t, err)
assert.Equal(t, "attachment", disposition)
assert.Equal(t, pfs[0].Name, params["filename"])
})
})
t.Run("Delete", func(t *testing.T) {
@@ -160,7 +160,7 @@ func TestPackageMaven(t *testing.T) {
t.Run("UploadVerifySHA1", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("Missmatch", func(t *testing.T) {
t.Run("Mismatch", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "test", http.StatusBadRequest)
@@ -10,6 +10,7 @@ import (
"encoding/xml"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
neturl "net/url"
@@ -930,4 +931,34 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("UploadMultipartForm", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
packageContent := createPackage(packageName, packageVersion).Bytes()
// Simulate dotnet nuget push which sends multipart/form-data with X-NuGet-ApiKey auth
var body bytes.Buffer
mpw := multipart.NewWriter(&body)
part, err := mpw.CreateFormFile("package", "package.nupkg")
assert.NoError(t, err)
_, err = part.Write(packageContent)
assert.NoError(t, err)
err = mpw.Close()
assert.NoError(t, err)
req := NewRequestWithBody(t, "PUT", url, &body).
SetHeader("Content-Type", mpw.FormDataContentType())
addNuGetAPIKeyHeader(req, writeToken)
MakeRequest(t, req, http.StatusCreated)
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNuGet)
assert.NoError(t, err)
assert.Len(t, pvs, 1)
// Clean up
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNoContent)
})
}
@@ -12,15 +12,16 @@ import (
"io"
"net/http"
"net/http/httptest"
"slices"
"strings"
"testing"
"code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/ProtonMail/go-crypto/openpgp"
@@ -31,14 +32,24 @@ import (
func TestPackageRpm(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
packageName := "gitea-test"
packageVersion := "1.0.2-1"
packageArchitecture := "x86_64"
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
base64RpmPackageContent := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
// To build an RPM package, it needs tools lik "cpio", it's not easy to do in Golang.
// So here we only use pre-built package contents. And to save space, the content is gzipped and base64 encoded.
rpmContentFromGzipBase64 := func(t testing.TB, s string) []byte {
b, err := base64.StdEncoding.DecodeString(s)
require.NoError(t, err)
zr, err := gzip.NewReader(bytes.NewReader(b))
require.NoError(t, err)
content, err := io.ReadAll(zr)
require.NoError(t, err)
return content
}
packageRpmGzipBase64 := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
VNwk2zd2PdvZ9Sxnd3Z3NllNsmF3o6congVFsWFHRWwIImIXfRER0QcRfPBJEXvvBQvWSfZTT0VQ
8TF/MuU33zcz3+zOJGEe73lyuQBRBWKWRzDrEddjuVAkxLMc+lsFUOWfm5bvvReAalWECg/TsivU
dyKa0U61aVnl6wj0Uxe4nc8F92hZiaYE8CO/P0r7/Quegr0c7M/AvoCaGZEIWNGUqMHrhhGROIUT
@@ -66,14 +77,17 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
9tMDyaXb7OAlk5acuPn57ss9mw6Wym0m1Fq2cej7tUt2LL4/b8enXU2fndk+fvv57ndnt55/cQob
7tpp/pEjDS7cGPZ6BY430+7danDq6f42Nw49b9F7zp6BiKpJb9s5P0AYN2+L159cnrur636rx+v1
7ae1K28QbMMcqI8CqwIrgwg9nTOp8Oj9q81plUY7ZuwXN8Vvs8wbAAA=`
rpmPackageContent, err := base64.StdEncoding.DecodeString(base64RpmPackageContent)
assert.NoError(t, err)
zr, err := gzip.NewReader(bytes.NewReader(rpmPackageContent))
assert.NoError(t, err)
packageRpmContent := rpmContentFromGzipBase64(t, packageRpmGzipBase64)
content, err := io.ReadAll(zr)
assert.NoError(t, err)
decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
t.Helper()
zr, err := gzip.NewReader(resp.Body)
assert.NoError(t, err)
assert.NoError(t, xml.NewDecoder(zr).Decode(v))
}
rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name)
@@ -99,7 +113,7 @@ gpgcheck=1
gpgkey=%sapi/packages/%s/rpm/repository.key`,
strings.Join(append([]string{user.LowerName}, groupParts...), "-"),
strings.Join(append([]string{user.Name, setting.AppName}, groupParts...), " - "),
util.URLJoin(setting.AppURL, groupURL),
strings.TrimSuffix(setting.AppURL, "/")+groupURL,
setting.AppURL,
user.Name,
)
@@ -120,10 +134,10 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
t.Run("Upload", func(t *testing.T) {
url := groupURL + "/upload"
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent))
MakeRequest(t, req, http.StatusUnauthorized)
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
@@ -146,9 +160,9 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
pb, err := packages.GetBlobByID(t.Context(), pfs[0].BlobID)
assert.NoError(t, err)
assert.Equal(t, int64(len(content)), pb.Size)
assert.Equal(t, int64(len(packageRpmContent)), pb.Size)
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusConflict)
})
@@ -159,12 +173,12 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
// download the package without the file name
req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture))
resp := MakeRequest(t, req, http.StatusOK)
assert.Equal(t, content, resp.Body.Bytes())
assert.Equal(t, packageRpmContent, resp.Body.Bytes())
// download the package with a file name (it can be anything)
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s/any-file-name", groupURL, packageName, packageVersion, packageArchitecture))
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, content, resp.Body.Bytes())
assert.Equal(t, packageRpmContent, resp.Body.Bytes())
})
t.Run("Repository", func(t *testing.T) {
@@ -248,15 +262,6 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")
})
decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
t.Helper()
zr, err := gzip.NewReader(resp.Body)
assert.NoError(t, err)
assert.NoError(t, xml.NewDecoder(zr).Decode(v))
}
t.Run("primary.xml.gz", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -331,7 +336,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
assert.Equal(t, "sha256", p.Checksum.Type)
assert.Equal(t, "f1d5d2ffcbe4a7568e98b864f40d923ecca084e9b9bcd5977ed6521c46d3fa4c", p.Checksum.Checksum)
assert.Equal(t, "https://gitea.io", p.URL)
assert.EqualValues(t, len(content), p.Size.Package)
assert.EqualValues(t, len(packageRpmContent), p.Size.Package)
assert.EqualValues(t, 13, p.Size.Installed)
assert.EqualValues(t, 272, p.Size.Archive)
assert.Equal(t, fmt.Sprintf("package/%s/%s/%s/%s", packageName, packageVersion, packageArchitecture, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture)), p.Location.Href)
@@ -421,6 +426,417 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
})
})
t.Run("Errata", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
type updateInfo struct {
XMLName xml.Name `xml:"updates"`
Xmlns string `xml:"xmlns,attr"`
Updates []*rpm_module.Update `xml:"update"`
}
errataURL := fmt.Sprintf("%s/package/%s/%s/errata", groupURL, packageName, packageVersion)
advisory := rpm_module.Update{
From: "security@example.com",
Status: "stable",
Type: "security",
Version: "1.0",
ID: "CVE-2023-1234",
Title: "Test Security Update",
Severity: "Important",
Description: "This is a test security update.",
References: []*rpm_module.Reference{
{
Href: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-1234",
ID: "CVE-2023-1234",
Title: "CVE-2023-1234",
Type: "cve",
},
},
PkgList: []*rpm_module.Collection{
{
Short: "el9",
Packages: []*rpm_module.UpdatePackage{
{
Arch: packageArchitecture,
Name: packageName,
Release: "1",
Src: "gitea-test-1.0.2-1.src.rpm",
Version: "1.0.2",
Filename: fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture),
},
},
},
},
}
t.Run("Success", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updates := []*rpm_module.Update{&advisory}
body, err := json.Marshal(updates)
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
url := groupURL + "/repodata"
// Check repomd.xml contains updateinfo
req = NewRequest(t, "GET", url+"/repomd.xml")
resp := MakeRequest(t, req, http.StatusOK)
type testRepoData struct {
Type string `xml:"type,attr"`
}
var repomd struct {
Data []*testRepoData `xml:"data"`
}
err = xml.NewDecoder(resp.Body).Decode(&repomd)
require.NoError(t, err)
found := slices.IndexFunc(repomd.Data, func(s *testRepoData) bool {
return s.Type == "updateinfo"
}) >= 0
assert.True(t, found, "updateinfo not found in repomd.xml")
// Now check updateinfo.xml.gz
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp = MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
assert.Equal(t, "http://linux.duke.edu/metadata/updateinfo", result.Xmlns)
assert.Len(t, result.Updates, 1)
assert.Equal(t, "CVE-2023-1234", result.Updates[0].ID)
assert.NotEmpty(t, result.Updates[0].Issued.Date)
assert.NotEmpty(t, result.Updates[0].Updated.Date)
})
t.Run("InvalidJSON", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithBody(t, "POST", errataURL, strings.NewReader("invalid json")).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusBadRequest)
})
t.Run("NullElementsInJSON", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Send a payload with null inside arrays
payload := `[
{
"id": "CVE-2023-5678",
"from": "security@example.com",
"status": "stable",
"type": "security",
"version": "1.0",
"title": "Test Null Elements",
"severity": "Important",
"description": "Test null elements",
"pkg_list": [
null,
{
"short": "el9",
"packages": [
null,
{
"arch": "x86_64",
"name": "gitea",
"release": "1",
"src": "gitea-1.0.0-1.src.rpm",
"version": "1.0.0",
"filename": "gitea-1.0.0-1.x86_64.rpm"
}
]
}
]
}
]`
req := NewRequestWithBody(t, "POST", errataURL, strings.NewReader(payload)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
// Verify it was stored correctly (skipping nulls)
url := groupURL + "/repodata"
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
// We need to find the new advisory CVE-2023-5678
var newAdvisory *rpm_module.Update
for _, u := range result.Updates {
if u.ID == "CVE-2023-5678" {
newAdvisory = u
break
}
}
assert.NotNil(t, newAdvisory)
assert.Len(t, newAdvisory.PkgList, 1)
assert.Equal(t, "el9", newAdvisory.PkgList[0].Short)
assert.Len(t, newAdvisory.PkgList[0].Packages, 1)
assert.Equal(t, "gitea", newAdvisory.PkgList[0].Packages[0].Name)
})
t.Run("PackageNotFound", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
badURL := fmt.Sprintf("%s/package/%s/non-existent-version/errata", groupURL, packageName)
updates := []*rpm_module.Update{&advisory}
body, err := json.Marshal(updates)
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", badURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("MergeAdvisories", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Upload a second advisory with the same ID but a different package
advisory2 := advisory
advisory2.PkgList = []*rpm_module.Collection{
{
Short: "el9",
Packages: []*rpm_module.UpdatePackage{
{
Arch: packageArchitecture,
Name: "another-package",
Release: "1",
Src: "another-package-1.0.0-1.src.rpm",
Version: "1.0.0",
Filename: "another-package-1.0.0-1.x86_64.rpm",
},
},
},
}
updates := []*rpm_module.Update{&advisory2}
body, err := json.Marshal(updates)
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
// Check updateinfo.xml.gz again
url := groupURL + "/repodata"
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
var targetUpdate *rpm_module.Update
for _, u := range result.Updates {
if u.ID == "CVE-2023-1234" {
targetUpdate = u
break
}
}
assert.NotNil(t, targetUpdate)
// Verify that package lists are merged into the same collection
assert.Len(t, targetUpdate.PkgList, 1)
assert.Len(t, targetUpdate.PkgList[0].Packages, 2)
assert.Equal(t, "another-package", result.Updates[0].PkgList[0].Packages[1].Name)
})
t.Run("NewCollection", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Upload a third advisory with the same ID but a different collection
advisory3 := advisory
advisory3.PkgList = []*rpm_module.Collection{
{
Short: "el8",
Packages: []*rpm_module.UpdatePackage{
{
Arch: packageArchitecture,
Name: packageName,
Release: "1",
Src: "gitea-test-1.0.2-1.src.rpm",
Version: "1.0.2",
Filename: fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture),
},
},
},
}
updates := []*rpm_module.Update{&advisory3}
body, err := json.Marshal(updates)
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
// Check updateinfo.xml.gz again
url := groupURL + "/repodata"
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
var targetUpdate *rpm_module.Update
for _, u := range result.Updates {
if u.ID == "CVE-2023-1234" {
targetUpdate = u
break
}
}
assert.NotNil(t, targetUpdate)
// Verify that we now have 2 collections
assert.Len(t, targetUpdate.PkgList, 2)
// We need to be careful with order, map iteration is random
// Let's check both exist
shorts := []string{targetUpdate.PkgList[0].Short, targetUpdate.PkgList[1].Short}
assert.Contains(t, shorts, "el9")
assert.Contains(t, shorts, "el8")
})
t.Run("Idempotency", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
updates := []*rpm_module.Update{&advisory}
body, err := json.Marshal(updates)
assert.NoError(t, err)
// Post twice
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
// Check updateinfo.xml.gz
url := groupURL + "/repodata"
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
var result updateInfo
decodeGzipXML(t, resp, &result)
var targetUpdate *rpm_module.Update
for _, u := range result.Updates {
if u.ID == "CVE-2023-1234" {
targetUpdate = u
break
}
}
assert.NotNil(t, targetUpdate)
assert.Len(t, targetUpdate.PkgList, 2)
var el9Coll *rpm_module.Collection
for _, coll := range targetUpdate.PkgList {
if coll.Short == "el9" {
el9Coll = coll
break
}
}
assert.NotNil(t, el9Coll)
assert.Len(t, el9Coll.Packages, 2)
})
})
t.Run("NoArch", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
noarchPackageName := "gitea-noarch-test"
noarchPackageVersion := "1.0.0-1"
noarchPackageGzipBase64 := `H4sICKC05mkAA2dpdGVhLW5vYXJjaC10ZXN0LTEuMC4wLTEubm9hcmNoLnJwbQDtmLtrFEEcx+dy` +
`p0ZROYliFMUTLJJiz53HPgbBBwZR0Jgiglo5sztzLt6Lu0uIYmEhFhFUsBUJahd8gHZWKvgnpLES` +
`VIwYo43a6Dm7+zvfhbXsF+ZmP/N7zM5y03wXZt89yyOjbiXqKGHVG6IVnLQ6qt2xcNku2xZG/6wc` +
`WvL70qXbr3PwuAyh4gMz74TnW2YumqJVZl76vQPKrQEeTjn/2swFM6rAb9N61Ezr84sQPwfx9xA/` +
`b8Il6nEpWSgwVz7lrqOZ9oWjqKZSShIQT/k4cHWIAtcJHNsR1OGKSp8w5QcMM+4SiSV1OSY293wi` +
`TAMmmBauL1XoeJ7mXAqhafL6BXzzSd+N2eFP9x8/nHt25u7CBbN49t8/YKZMmTJlypQpU6ZMmTJl` +
`ypTpP1biiXS73Sso8TR+8U1KCOU+mHkXSnyN3HPICc3oh5yeTxL7Jn3A88Brgd8Ab0Q/fJTlZmwC` +
`XgC2gd+h1FfZDbwI9SPAHyB+EPgjxMeAP0O/ceAvED8B/BVYp1xYC1wDXg/nuww8CPvFvlHePG6A` +
`+D3gjcBd4KG0X24d1B9N63Nw3sKxND9XApaQPwQcAm8HVsAMWANz4CrwjpQHrsJ+8D0GnkIcvsfA` +
`C9j/OPBLyL8GPA/xmZj3oj/8OZT4cwij0WStFK+VmiI4JSoKjf8EZdMgevVgRjbqqg1/mEMHxtGR` +
`erupgkhHKkTVqD4xhdLuf27VswLL7VZQbjVrf3mZ9KVX9IZJqkZyaG+j1mypdluF+6KqGhU11R5G` +
`EItXRqKKKf6xNiZOVxsiSW7vF5NqrKV0NDWMqNmfWRixsptYkgx+iV1O/Mn+nldpHSYlq4KCZtRA` +
`lTNRE3E4lDWp6mGjZaUHTWomOtrykdCUKxxgLjTzfKG0J2wqFfeVFjyMzT1CfC255lRRn5HQDT2J` +
`mcDMdQRzeNqL0NBmhEimlTDpnoeV7xGPYSmpZ3vcljQIbYf6SmjJXEwoVT6xpc+k4wkS/7fSC97t` +
`xvcCFbdcTO92X57apoHNSquLs6s3b3s0uHXPpes77+yZnp5eaa7m3PxbJ3YYvwFme3wbyRUAAA==`
noarchRpmPackageContent := rpmContentFromGzipBase64(t, noarchPackageGzipBase64)
// Upload the noarch RPM
req := NewRequestWithBody(t, "PUT", groupURL+"/upload", bytes.NewReader(noarchRpmPackageContent)).AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
// The noarch package should appear in primary.xml when any arch is requested.
// At this point the group contains the existing x86_64 package + this noarch one.
req = NewRequest(t, "GET", groupURL+"/repodata/primary.xml.gz")
resp := MakeRequest(t, req, http.StatusOK)
type PackageSummary struct {
Name string `xml:"name"`
Architecture string `xml:"arch"`
}
type PrimaryMetadata struct {
XMLName xml.Name `xml:"metadata"`
PackageCount int `xml:"packages,attr"`
Packages []PackageSummary `xml:"package"`
}
var primary PrimaryMetadata
decodeGzipXML(t, resp, &primary)
// Both the arch-specific package uploaded earlier and the noarch one must be present.
assert.Equal(t, 2, primary.PackageCount)
assert.Len(t, primary.Packages, 2)
archNames := make([]string, 0, len(primary.Packages))
for _, p := range primary.Packages {
archNames = append(archNames, p.Architecture)
}
assert.Contains(t, archNames, packageArchitecture) // x86_64 from the Upload subtest
assert.Contains(t, archNames, "noarch")
// noarch package must be downloadable regardless of the arch path used.
for _, arch := range []string{"noarch", "x86_64", "aarch64", "my_arch"} {
req = NewRequest(t, "GET", fmt.Sprintf(
"%s/package/%s/%s/%s",
groupURL, noarchPackageName, noarchPackageVersion, arch,
))
MakeRequest(t, req, http.StatusOK)
}
// Clean up: delete via the canonical noarch path.
req = NewRequest(t, "DELETE", fmt.Sprintf(
"%s/package/%s/%s/noarch",
groupURL, noarchPackageName, noarchPackageVersion,
)).AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusNoContent)
// After deletion, the noarch package must no longer be reachable via any arch.
for _, arch := range []string{"noarch", "x86_64", "aarch64"} {
req = NewRequest(t, "GET", fmt.Sprintf(
"%s/package/%s/%s/%s",
groupURL, noarchPackageName, noarchPackageVersion, arch,
))
MakeRequest(t, req, http.StatusNotFound)
}
// The x86_64 package from the Upload subtest must still be present.
req = NewRequest(t, "GET", groupURL+"/repodata/primary.xml.gz")
resp = MakeRequest(t, req, http.StatusOK)
var primaryAfter PrimaryMetadata
decodeGzipXML(t, resp, &primaryAfter)
assert.Equal(t, 1, primaryAfter.PackageCount)
assert.Equal(t, packageArchitecture, primaryAfter.Packages[0].Architecture)
})
t.Run("Delete", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -442,7 +858,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
t.Run("UploadSign", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
url := groupURL + "/upload?sign=true"
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
@@ -18,6 +18,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
swift_module "code.gitea.io/gitea/modules/packages/swift"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
swift_router "code.gitea.io/gitea/routers/api/packages/swift"
"code.gitea.io/gitea/tests"
@@ -35,9 +36,26 @@ func TestPackageSwift(t *testing.T) {
packageID := packageScope + "." + packageName
packageVersion := "1.0.3"
packageVersion2 := "1.0.4"
packageVersion3 := "1.0.5"
packageAuthor := "KN4CK3R"
packageDescription := "Gitea Test Package"
packageRepositoryURL := "https://gitea.io/gitea/gitea"
packageCodeRepositoryURL := "https://gitea.io/gitea/gitea" // this one is not used as a property, it is meta
packageLicenseURL := "https://opensource.org/license/mit"
packageRepositoryURL1 := "https://gitea.io/gitea/repo"
packageRepositoryURLs := []string{packageRepositoryURL1, "https://gitea.io/gitea/repo.git", "ssh://git@gitea.io/gitea/repo.git"}
makePackageMetadataJSON := func(ver string) string {
tmpl := `{
"name":"` + packageName + `",
"version":"%s",
"description":"` + packageDescription + `",
"codeRepository":"` + packageCodeRepositoryURL + `",
"licenseURL":"` + packageLicenseURL + `",
"author":{"givenName":"` + packageAuthor + `"},
"repositoryURLs":["` + strings.Join(packageRepositoryURLs, `","`) + `"]
}`
return fmt.Sprintf(tmpl, ver)
}
contentManifest1 := "// swift-tools-version:5.7\n//\n// Package.swift"
contentManifest2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
@@ -146,7 +164,7 @@ func TestPackageSwift(t *testing.T) {
"Package.swift": contentManifest1,
"Package@swift-5.6.swift": contentManifest2,
}),
`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
makePackageMetadataJSON(packageVersion),
)
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeSwift)
@@ -164,8 +182,8 @@ func TestPackageSwift(t *testing.T) {
assert.Len(t, metadata.Manifests, 2)
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
assert.Len(t, pd.VersionProperties, 1)
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
assert.Len(t, pd.VersionProperties, 3)
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
assert.NoError(t, err)
@@ -234,7 +252,7 @@ func TestPackageSwift(t *testing.T) {
"Package.swift": contentManifest1,
"Package@swift-5.6.swift": contentManifest2,
}),
`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
makePackageMetadataJSON(packageVersion2),
)
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeSwift)
@@ -252,8 +270,8 @@ func TestPackageSwift(t *testing.T) {
assert.Len(t, metadata.Manifests, 2)
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
assert.Len(t, pd.VersionProperties, 1)
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
assert.Len(t, pd.VersionProperties, 3)
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
pfs, err := packages.GetFilesByVersionID(t.Context(), thisPackageVersion.ID)
assert.NoError(t, err)
@@ -318,7 +336,7 @@ func TestPackageSwift(t *testing.T) {
AddBasicAuth(user.Name)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, body, resp.Body.String())
assert.JSONEq(t, body, resp.Body.String())
})
t.Run("PackageVersionMetadata", func(t *testing.T) {
@@ -354,8 +372,11 @@ func TestPackageSwift(t *testing.T) {
assert.Equal(t, packageVersion, result.Metadata.Version)
assert.Equal(t, packageDescription, result.Metadata.Description)
assert.Equal(t, "Swift", result.Metadata.ProgrammingLanguage.Name)
assert.Equal(t, packageLicenseURL, result.Metadata.LicenseURL)
require.NotNil(t, result.Metadata.Author)
assert.Equal(t, packageAuthor, result.Metadata.Author.Name)
assert.Equal(t, packageAuthor, result.Metadata.Author.GivenName)
assert.ElementsMatch(t, packageRepositoryURLs, result.Metadata.RepositoryURLs)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.json", url, packageScope, packageName, packageVersion)).
AddBasicAuth(user.Name)
@@ -364,6 +385,41 @@ func TestPackageSwift(t *testing.T) {
assert.Equal(t, body, resp.Body.String())
})
t.Run("UploadEmptyJSONMetadata", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion3)
var body bytes.Buffer
mpw := multipart.NewWriter(&body)
part, err := mpw.CreateFormFile("source-archive", "source-archive.zip")
require.NoError(t, err)
_, err = io.Copy(part, test.WriteZipArchive(map[string]string{
"Package.swift": contentManifest1,
"Package@swift-5.6.swift": contentManifest2,
}))
require.NoError(t, err)
require.NoError(t, mpw.WriteField("metadata", "{}"))
require.NoError(t, mpw.Close())
req := NewRequestWithBody(t, "PUT", uploadURL, &body).
SetHeader("Content-Type", mpw.FormDataContentType()).
SetHeader("Accept", swift_router.AcceptJSON).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion3)).
AddBasicAuth(user.Name).
SetHeader("Accept", swift_router.AcceptJSON)
resp := MakeRequest(t, req, http.StatusOK)
result := DecodeJSON(t, resp, &swift_router.PackageVersionMetadataResponse{})
assert.Nil(t, result.Metadata.Author)
assert.Empty(t, result.Metadata.RepositoryURLs)
assert.Empty(t, result.Metadata.CodeRepository)
assert.Empty(t, result.Metadata.LicenseURL)
})
t.Run("DownloadManifest", func(t *testing.T) {
manifestURL := fmt.Sprintf("%s/%s/%s/%s/Package.swift", url, packageScope, packageName, packageVersion)
@@ -421,7 +477,7 @@ func TestPackageSwift(t *testing.T) {
req = NewRequest(t, "GET", url+"/identifiers?url=https://unknown.host/")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL).
req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL1).
SetHeader("Accept", swift_router.AcceptJSON)
resp = MakeRequest(t, req, http.StatusOK)
File diff suppressed because one or more lines are too long
@@ -85,6 +85,50 @@ func TestPackageAPI(t *testing.T) {
assert.Equal(t, user.Name, p.Creator.UserName)
})
t.Run("DeleteEntirePackage", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
packageName := "test-package-entire-delete"
for _, version := range []string{"1.0.1", "1.0.2"} {
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user.Name, packageName, version)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1})).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
}
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)).
AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)).
AddTokenAuth(tokenReadPackage)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("DeletePackageVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
packageName := "test-package-version-delete"
for _, version := range []string{"1.0.1", "1.0.2"} {
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user.Name, packageName, version)
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1})).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusCreated)
}
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.1", user.Name, packageName)).
AddTokenAuth(tokenWritePackage)
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.1", user.Name, packageName)).
AddTokenAuth(tokenReadPackage)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.2", user.Name, packageName)).
AddTokenAuth(tokenReadPackage)
MakeRequest(t, req, http.StatusOK)
})
t.Run("ListPackageVersions", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -84,7 +84,7 @@ func TestAPIPrivateServ(t *testing.T) {
assert.Empty(t, results)
// Add reading deploy key
deployKey, err := asymkey_model.AddDeployKey(ctx, 19, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
deployKey, err := asymkey_model.AddDeployKey(ctx, 19 /* repo id */, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
assert.NoError(t, err)
// Can pull from repo we're a deploy key for
@@ -106,17 +106,17 @@ func TestAPIPrivateServ(t *testing.T) {
assert.Empty(t, results)
// Cannot pull from a private repo we're not associated with
results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
assert.Error(t, extra.Error)
assert.Empty(t, results)
// Cannot pull from a public repo we're not associated with
results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
assert.Error(t, extra.Error)
assert.Empty(t, results)
// Add writing deploy key
deployKey, err = asymkey_model.AddDeployKey(ctx, 20, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
deployKey, err = asymkey_model.AddDeployKey(ctx, 20 /* repo id */, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
assert.NoError(t, err)
// Cannot push to a private repo with reading key
@@ -0,0 +1,107 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIUserReposPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/repos").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var repos []api.Repository
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
req = NewRequest(t, "GET", "/api/v1/users/user2/repos").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repos)
assert.NotEmpty(t, repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
func repoNames(repos []api.Repository) []string {
names := make([]string, 0, len(repos))
for _, repo := range repos {
names = append(names, repo.FullName)
}
return names
}
func TestAPIRepoByIDPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/repositories/1").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", "/api/v1/repositories/2").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIActivityFeedsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
req := NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var activities []api.Activity
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
AddTokenAuth(publicToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(orgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assert.NotEmpty(t, activities)
publicOrgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
AddTokenAuth(publicOrgToken)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &activities)
assertPublicActivitiesOnly(t, activities)
}
func assertPublicActivitiesOnly(t *testing.T, activities []api.Activity) {
t.Helper()
for _, activity := range activities {
assert.False(t, activity.IsPrivate)
if activity.Repo != nil {
assert.False(t, activity.Repo.Private)
}
}
}
@@ -15,9 +15,11 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
issue_service "code.gitea.io/gitea/services/issue"
pull_service "code.gitea.io/gitea/services/pull"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -362,6 +364,79 @@ func TestAPIPullReviewRequest(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent)
}
func TestAPIPullReviewCommentResolveEndpoints(t *testing.T) {
defer tests.PrepareTestEnv(t)()
ctx := t.Context()
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
require.NoError(t, pullIssue.LoadAttributes(ctx))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: pullIssue.PosterID})
require.NoError(t, pullIssue.LoadPullRequest(ctx))
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
require.NoError(t, err)
defer gitRepo.Close()
latestCommitID, err := gitRepo.GetRefCommitID(pullIssue.PullRequest.GetGitHeadRefName())
require.NoError(t, err)
codeComment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, pullIssue, 1, "resolve comment", "README.md", false, 0, latestCommitID, nil)
require.NoError(t, err)
require.NotNil(t, codeComment)
session := loginUser(t, doer.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
resolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, codeComment.ID)
unresolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/unresolve", repo.OwnerName, repo.Name, codeComment.ID)
req := NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify comment is resolved
updatedComment, err := issues_model.GetCommentByID(ctx, codeComment.ID)
require.NoError(t, err)
assert.NotZero(t, updatedComment.ResolveDoerID)
assert.Equal(t, doer.ID, updatedComment.ResolveDoerID)
// Resolving again should be idempotent
MakeRequest(t, req, http.StatusNoContent)
req = NewRequest(t, http.MethodPost, unresolveURL).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
// Verify comment is unresolved
updatedComment, err = issues_model.GetCommentByID(ctx, codeComment.ID)
require.NoError(t, err)
assert.Zero(t, updatedComment.ResolveDoerID)
// Unresolving again should be idempotent
MakeRequest(t, req, http.StatusNoContent)
// Non-existing comment ID
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/999999/resolve", repo.OwnerName, repo.Name)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
// Non-code-comment
plainComment, err := issue_service.CreateIssueComment(ctx, doer, repo, pullIssue, "not a review comment", nil)
require.NoError(t, err)
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
// Test permission check: use a user without write access for target repo to test 403 response
unauthorizedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
require.NotEqual(t, pullIssue.PosterID, unauthorizedUser.ID)
unauthorizedSession := loginUser(t, unauthorizedUser.Name)
unauthorizedToken := getTokenForLoggedInUser(t, unauthorizedSession, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
req = NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(unauthorizedToken)
MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(unauthorizedToken)
MakeRequest(t, req, http.StatusForbidden)
}
func TestAPIPullReviewStayDismissed(t *testing.T) {
// This test against issue https://github.com/go-gitea/gitea/issues/28542
// where old reviews surface after a review request got dismissed.
@@ -423,7 +498,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) {
pullIssue.ID, user8.ID, 1, 1, 2, false)
// user8 dismiss review
permUser8, err := access_model.GetUserRepoPermission(t.Context(), pullIssue.Repo, user8)
permUser8, err := access_model.GetIndividualUserRepoPermission(t.Context(), pullIssue.Repo, user8)
assert.NoError(t, err)
_, err = issue_service.ReviewRequest(t.Context(), pullIssue, user8, &permUser8, user8, false)
assert.NoError(t, err)
@@ -270,13 +270,20 @@ func TestAPICreatePullSuccess(t *testing.T) {
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
session := loginUser(t, owner11.Name)
prTitle := "test pull request title " + time.Now().String()
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
Head: owner11.Name + ":master",
Base: "master",
Title: "create a failure pr",
Title: prTitle,
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Also test that AllowMaintainerEdit is true by default, the "false" case is covered by TestAPICreatePullBasePermission
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: prTitle})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: prIssue.ID})
assert.True(t, pr.AllowMaintainerEdit)
MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
}
@@ -290,11 +297,14 @@ func TestAPICreatePullBasePermission(t *testing.T) {
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
session := loginUser(t, user4.Name)
prTitle := "test pull request title " + time.Now().String()
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
opts := &api.CreatePullRequestOption{
Head: repo11.OwnerName + ":master",
Base: "master",
Title: "create a failure pr",
Title: prTitle,
AllowMaintainerEdit: new(false),
}
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusForbidden)
@@ -306,6 +316,11 @@ func TestAPICreatePullBasePermission(t *testing.T) {
// create again
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
// Also test that AllowMaintainerEdit is set to false, the default "true" case is covered by TestAPICreatePullSuccess
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: prTitle})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: prIssue.ID})
assert.False(t, pr.AllowMaintainerEdit)
}
func TestAPICreatePullHeadPermission(t *testing.T) {
@@ -442,9 +457,8 @@ func TestAPIEditPull(t *testing.T) {
Base: "master",
Title: title,
}).AddTokenAuth(token)
apiPull := new(api.PullRequest)
resp := MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, apiPull)
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "master", apiPull.Base.Name)
newTitle := "edit a this pr"
@@ -455,8 +469,9 @@ func TestAPIEditPull(t *testing.T) {
Body: &newBody,
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, apiPull)
apiPull = DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "feature/1", apiPull.Base.Name)
// check comment history
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
err := pull.LoadIssue(t.Context())
@@ -468,6 +483,62 @@ func TestAPIEditPull(t *testing.T) {
Base: "not-exist",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
t.Run("PullContentVersion", func(t *testing.T) {
testAPIPullContentVersion(t, pull.ID)
})
}
func testAPIPullContentVersion(t *testing.T, pullID int64) {
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pullID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pull.BaseRepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner.Name, repo.Name, pull.Index)
t.Run("ResponseIncludesContentVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
assert.GreaterOrEqual(t, apiPull.ContentVersion, 0)
})
t.Run("EditWithCorrectVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
before := DecodeJSON(t, resp, &api.PullRequest{})
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("updated body with correct version"),
ContentVersion: new(before.ContentVersion),
}).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated)
after := DecodeJSON(t, resp, &api.PullRequest{})
assert.Equal(t, "updated body with correct version", after.Body)
assert.Greater(t, after.ContentVersion, before.ContentVersion)
})
t.Run("EditWithWrongVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("should fail"),
ContentVersion: new(99999),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusConflict)
})
t.Run("EditWithoutVersion", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
Body: new("edit without version succeeds"),
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusCreated)
})
}
func doAPIGetPullFiles(ctx APITestContext, pr *api.PullRequest, callback func(*testing.T, []*api.ChangedFile)) func(*testing.T) {
@@ -13,15 +13,15 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
func testAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
// Limit the allowed release types (since by default there is no restriction)
defer test.MockVariableValue(&setting.Repository.Release.AllowedTypes, ".exe")()
defer tests.PrepareTestEnv(t)()
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9})
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
@@ -38,3 +38,34 @@ func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
}
func testAPIDraftReleaseAttachmentAccess(t *testing.T) {
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 13})
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
reader := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
listURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", repoOwner.Name, repo.Name, release.ID)
getURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", repoOwner.Name, repo.Name, release.ID, attachment.ID)
MakeRequest(t, NewRequest(t, "GET", listURL), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", getURL), http.StatusNotFound)
readerToken := getUserToken(t, reader.LowerName, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(readerToken), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(readerToken), http.StatusNotFound)
ownerReadToken := getUserToken(t, repoOwner.LowerName, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(ownerReadToken), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(ownerReadToken), http.StatusNotFound)
ownerToken := getUserToken(t, repoOwner.LowerName, auth_model.AccessTokenScopeWriteRepository)
resp := MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(ownerToken), http.StatusOK)
var attachments []*api.Attachment
DecodeJSON(t, resp, &attachments)
if assert.Len(t, attachments, 1) {
assert.Equal(t, attachment.ID, attachments[0].ID)
}
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(ownerToken), http.StatusOK)
}
@@ -29,12 +29,22 @@ import (
"github.com/stretchr/testify/assert"
)
func TestAPIListReleases(t *testing.T) {
func TestAPIReleaseRead(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("DraftReleaseAttachmentAccess", testAPIDraftReleaseAttachmentAccess)
t.Run("ListReleasesWithWriteToken", testAPIListReleasesWithWriteToken)
t.Run("ListReleasesWithReadToken", testAPIListReleasesWithReadToken)
t.Run("GetDraftRelease", testAPIGetDraftRelease)
t.Run("GetLatestRelease", testAPIGetLatestRelease)
t.Run("GetReleaseByTag", testAPIGetReleaseByTag)
t.Run("GetDraftReleaseByTag", testAPIGetDraftReleaseByTag)
t.Run("EditReleaseAttachmentWithUnallowedFile", testAPIEditReleaseAttachmentWithUnallowedFile) // failed attempt, so it is also a read test
}
func testAPIListReleasesWithWriteToken(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository)
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
@@ -81,6 +91,72 @@ func TestAPIListReleases(t *testing.T) {
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
}
func testAPIListReleasesWithReadToken(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
var apiReleases []*api.Release
DecodeJSON(t, resp, &apiReleases)
if assert.Len(t, apiReleases, 2) {
for _, release := range apiReleases {
switch release.ID {
case 1:
assert.False(t, release.IsDraft)
assert.False(t, release.IsPrerelease)
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/1/assets"), release.UploadURL)
case 5:
assert.False(t, release.IsDraft)
assert.True(t, release.IsPrerelease)
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/5/assets"), release.UploadURL)
default:
assert.NoError(t, fmt.Errorf("unexpected release: %v", release))
}
}
}
// test filter
testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) {
link.RawQuery = query.Encode()
req := NewRequest(t, "GET", link.String())
if auth {
req.AddTokenAuth(token)
}
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiReleases)
assert.Len(t, apiReleases, expectedLength, msgAndArgs)
}
testFilterByLen(false, url.Values{"draft": {"true"}}, 0, "anon should not see drafts")
testFilterByLen(true, url.Values{"draft": {"true"}}, 0, "repo owner with read token should not see drafts")
testFilterByLen(true, url.Values{"draft": {"false"}}, 2, "exclude drafts")
testFilterByLen(true, url.Values{"draft": {"false"}, "pre-release": {"false"}}, 1, "exclude drafts and pre-releases")
testFilterByLen(true, url.Values{"pre-release": {"true"}}, 1, "only get pre-release")
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
}
func testAPIGetDraftRelease(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 4})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
reader := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d", owner.Name, repo.Name, release.ID)
MakeRequest(t, NewRequest(t, "GET", urlStr), http.StatusNotFound)
readerToken := getUserToken(t, reader.LowerName, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "GET", urlStr).AddTokenAuth(readerToken), http.StatusNotFound)
ownerToken := getUserToken(t, owner.LowerName, auth_model.AccessTokenScopeWriteRepository)
resp := MakeRequest(t, NewRequest(t, "GET", urlStr).AddTokenAuth(ownerToken), http.StatusOK)
var apiRelease api.Release
DecodeJSON(t, resp, &apiRelease)
assert.Equal(t, release.Title, apiRelease.Title)
}
func createNewReleaseUsingAPI(t *testing.T, token string, owner *user_model.User, repo *repo_model.Repository, name, target, title, desc string) *api.Release {
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases", owner.Name, repo.Name)
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateReleaseOption{
@@ -230,9 +306,7 @@ func TestAPICreateReleaseGivenInvalidTarget(t *testing.T) {
MakeRequest(t, req, http.StatusNotFound)
}
func TestAPIGetLatestRelease(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIGetLatestRelease(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -245,9 +319,7 @@ func TestAPIGetLatestRelease(t *testing.T) {
assert.Equal(t, "testing-release", release.Title)
}
func TestAPIGetReleaseByTag(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIGetReleaseByTag(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -271,9 +343,7 @@ func TestAPIGetReleaseByTag(t *testing.T) {
assert.NotEmpty(t, err.Message)
}
func TestAPIGetDraftReleaseByTag(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testAPIGetDraftReleaseByTag(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -335,7 +405,7 @@ func TestAPIDeleteReleaseByTagName(t *testing.T) {
func TestAPIUploadAssetRelease(t *testing.T) {
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Attachment.MaxSize, 1)()
defer test.MockVariableValue(&setting.Repository.Release.FileMaxSize, 1)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -352,7 +422,7 @@ func TestAPIUploadAssetRelease(t *testing.T) {
defer tests.PrintCurrentTest(t)()
const filename = "image.png"
performUpload := func(t *testing.T, uploadURL string, buf []byte, expectedStatus int) *httptest.ResponseRecorder {
performUpload := func(t *testing.T, uploadURL string, _ []byte, _ int) *httptest.ResponseRecorder {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", filename)
@@ -29,10 +29,10 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
session := loginUser(t, user1.LowerName)
// public only token should be forbidden
// public-only token cannot see a private repo
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeWriteRepository)
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches", repo3.Name)) // a plain repo
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
@@ -46,7 +46,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
assert.Equal(t, "master", branches[1].Name)
link2, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch", repo3.Name))
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
resp = MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(token), http.StatusOK)
bs, err = io.ReadAll(resp.Body)
@@ -55,7 +55,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
assert.NoError(t, json.Unmarshal(bs, &branch))
assert.Equal(t, "test_branch", branch.Name)
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
req.Header.Add("Content-Type", "application/json")
@@ -81,7 +81,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
link3, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch2", repo3.Name))
MakeRequest(t, NewRequest(t, "DELETE", link3.String()), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(token), http.StatusNoContent)
assert.NoError(t, err)
@@ -121,10 +121,10 @@ func TestAPIRepoBranchesMirror(t *testing.T) {
resp = MakeRequest(t, req, http.StatusForbidden)
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
resp = MakeRequest(t, NewRequest(t, "DELETE", link2.String()).AddTokenAuth(token), http.StatusForbidden)
bs, err = io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
}
@@ -5,46 +5,60 @@ package integration
import (
"net/http"
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPICompareBranches(t *testing.T) {
defer tests.PrepareTestEnv(t)()
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
session2 := loginUser(t, "user2")
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Login as User2.
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
t.Run("CompareBranches", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
t.Run("CompareBranches", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusOK)
apiResp := DecodeJSON(t, resp, &api.Compare{})
assert.Equal(t, 2, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 2)
})
var apiResp *api.Compare
DecodeJSON(t, resp, &apiResp)
t.Run("CompareCommits", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
assert.Equal(t, 2, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 2)
})
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusOK)
apiResp := DecodeJSON(t, resp, &api.Compare{})
assert.Equal(t, 1, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 1)
})
t.Run("CompareCommits", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
t.Run("CompareForkOnlyCommit", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
var apiResp *api.Compare
DecodeJSON(t, resp, &apiResp)
user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
user13Sess := loginUser(t, "user13")
user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository)
assert.Equal(t, 1, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 1)
_, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "new-branch"}, map[string]string{"file.txt": "content"})
require.NoError(t, err)
req := NewRequestf(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:new-branch").AddTokenAuth(user13Token)
resp := MakeRequest(t, req, http.StatusOK)
apiResp := DecodeJSON(t, resp, &api.Compare{})
assert.Equal(t, 1, apiResp.TotalCommits)
assert.Len(t, apiResp.Commits, 1)
})
})
}
@@ -418,5 +418,20 @@ func TestAPIRepoEdit(t *testing.T) {
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo1.Name), &repoEditOption).
AddTokenAuth(token4)
MakeRequest(t, req, http.StatusForbidden)
// Test updating pull request settings without setting has_pull_requests
repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
url = fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
DefaultDeleteBranchAfterMerge: &bTrue,
}).AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &repo)
assert.True(t, repo.DefaultDeleteBranchAfterMerge)
// reset
req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
DefaultDeleteBranchAfterMerge: &bFalse,
}).AddTokenAuth(token2)
_ = MakeRequest(t, req, http.StatusOK)
})
}
@@ -19,7 +19,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"github.com/stretchr/testify/assert"
@@ -53,8 +52,8 @@ func getCreateFileOptions() api.CreateFileOptions {
func normalizeFileContentResponseCommitTime(c *api.ContentsResponse) {
// decoded JSON response may contain different timezone from the one parsed by git commit
// so we need to normalize the time to UTC to make "assert.Equal" pass
c.LastCommitterDate = util.ToPointer(c.LastCommitterDate.UTC())
c.LastAuthorDate = util.ToPointer(c.LastAuthorDate.UTC())
c.LastCommitterDate = new(c.LastCommitterDate.UTC())
c.LastAuthorDate = new(c.LastAuthorDate.UTC())
}
type apiFileResponseInfo struct {
@@ -75,9 +74,9 @@ func getExpectedFileResponseForCreate(info apiFileResponseInfo) *api.FileRespons
Name: path.Base(info.treePath),
Path: info.treePath,
SHA: sha,
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
LastCommitSHA: new(info.lastCommitSHA),
LastCommitterDate: new(info.lastCommitterWhen),
LastAuthorDate: new(info.lastAuthorWhen),
Size: 16,
Type: "file",
Encoding: &encoding,
@@ -20,21 +20,19 @@ import (
func getDeleteFileOptions() *api.DeleteFileOptions {
return &api.DeleteFileOptions{
FileOptionsWithSHA: api.FileOptionsWithSHA{
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "Removing the file new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Jane Doe",
Email: "janedoe@example.com",
},
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "Removing the file new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Jane Doe",
Email: "janedoe@example.com",
},
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
},
}
}
@@ -9,7 +9,6 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -25,8 +24,9 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
// Test with LFS
onGiteaRun(t, func(t *testing.T, u *url.URL) {
createLFSTestRepository(t, "repo-lfs-test")
httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository)
doAPICreateRepository(httpContext, false, func(t *testing.T, repository api.Repository) {
t.Run("repo-lfs-test", func(t *testing.T) {
u.Path = httpContext.GitPath()
dstPath := t.TempDir()
@@ -41,7 +41,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(httpContext.Token)
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
assert.Equal(t, testFileSizeSmall, respLFS.Length)
})
@@ -5,6 +5,7 @@ package integration
import (
"context"
"errors"
"strings"
"testing"
@@ -19,6 +20,9 @@ import (
type createFileInBranchOptions struct {
OldBranch, NewBranch string
CommitMessage string
CommitterName string
CommitterEmail string
}
func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) *api.FilesResponse {
@@ -29,7 +33,17 @@ func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_mode
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) (*api.FilesResponse, error) {
ctx := context.TODO()
opts := &files_service.ChangeRepoFilesOptions{OldBranch: createOpts.OldBranch, NewBranch: createOpts.NewBranch}
opts := &files_service.ChangeRepoFilesOptions{
OldBranch: createOpts.OldBranch,
NewBranch: createOpts.NewBranch,
Message: createOpts.CommitMessage,
}
if createOpts.CommitterName != "" || createOpts.CommitterEmail != "" {
opts.Committer = &files_service.IdentityOptions{
GitUserName: createOpts.CommitterName,
GitUserEmail: createOpts.CommitterEmail,
}
}
for path, content := range files {
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
Operation: "create",
@@ -59,7 +73,7 @@ func deleteFileInBranch(user *user_model.User, repo *repo_model.Repository, tree
func createOrReplaceFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) error {
_, err := deleteFileInBranch(user, repo, treePath, branchName)
if err != nil && !files_service.IsErrRepoFileDoesNotExist(err) {
if err != nil && !errors.Is(err, util.ErrNotExist) {
return err
}
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context"
"github.com/stretchr/testify/assert"
@@ -28,21 +27,19 @@ func getUpdateFileOptions() *api.UpdateFileOptions {
content := "This is updated text"
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
return &api.UpdateFileOptions{
FileOptionsWithSHA: api.FileOptionsWithSHA{
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "My update of new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Anne Doe",
Email: "annedoe@example.com",
},
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "My update of new/file.txt",
Author: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
Committer: api.Identity{
Name: "Anne Doe",
Email: "annedoe@example.com",
},
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
},
ContentBase64: contentEncoded,
}
@@ -61,9 +58,9 @@ func getExpectedFileResponseForUpdate(info apiFileResponseInfo) *api.FileRespons
Name: path.Base(info.treePath),
Path: info.treePath,
SHA: sha,
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
LastCommitSHA: new(info.lastCommitSHA),
LastCommitterDate: new(info.lastCommitterWhen),
LastAuthorDate: new(info.lastAuthorWhen),
Type: "file",
Size: 20,
Encoding: &encoding,
@@ -180,6 +177,15 @@ func TestAPIUpdateFile(t *testing.T) {
assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
assert.Equal(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message)
// Test updating a file without SHA (should create the file)
updateFileOptions = getUpdateFileOptions()
updateFileOptions.SHA = ""
req = NewRequestWithJSON(t, "PUT", "/api/v1/repos/user2/repo1/contents/update-create.txt", &updateFileOptions).AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, &fileResponse)
assert.Equal(t, "08bd14b2e2852529157324de9c226b3364e76136", fileResponse.Content.SHA)
assert.Equal(t, setting.AppURL+"user2/repo1/raw/branch/master/update-create.txt", *fileResponse.Content.DownloadURL)
// Test updating a file and renaming it
updateFileOptions = getUpdateFileOptions()
updateFileOptions.BranchName = repo1.DefaultBranch
@@ -161,9 +161,88 @@ func TestAPIChangeFiles(t *testing.T) {
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
assert.Nil(t, filesResponse.Files[2])
assert.Equal(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
// Test fails creating a file in a branch that already exists without force
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = false
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
assert.Contains(t, resp.Body.String(), `"message":"branch already exists [name: develop]"`)
// Test succeeds creating a file in a branch that already exists with force
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = true
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, &filesResponse)
expectedCreateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/develop/new/file%d.txt", fileID)
expectedCreateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/develop/new/file%d.txt", fileID)
expectedUpdateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/develop/update/file%d.txt", fileID)
expectedUpdateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/develop/update/file%d.txt", fileID)
assert.Equal(t, expectedCreateSHA, filesResponse.Files[0].SHA)
assert.Equal(t, expectedCreateHTMLURL, *filesResponse.Files[0].HTMLURL)
assert.Equal(t, expectedCreateDownloadURL, *filesResponse.Files[0].DownloadURL)
assert.Equal(t, expectedUpdateSHA, filesResponse.Files[1].SHA)
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
assert.Nil(t, filesResponse.Files[2])
assert.Equal(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
// Test fails creating a file in a branch that already exists with force and branch protection enabled
protectionReq := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{
RuleName: "develop",
BranchName: "develop",
Priority: 1,
EnablePush: true,
EnableForcePush: false,
}).AddTokenAuth(token2)
MakeRequest(t, protectionReq, http.StatusCreated)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = true
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusForbidden)
assert.Contains(t, resp.Body.String(), `"message":"branch develop is protected from force push"`)
// Test updating a file and renaming it
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
@@ -17,7 +17,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/stretchr/testify/assert"
@@ -35,9 +34,9 @@ func getExpectedContentsListResponseForContents(ref, refType, lastCommitSHA stri
Name: path.Base(treePath),
Path: treePath,
SHA: sha,
LastCommitSHA: util.ToPointer(lastCommitSHA),
LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastCommitSHA: new(lastCommitSHA),
LastCommitterDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastAuthorDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
Type: "file",
Size: 30,
URL: &selfURL,
@@ -80,7 +79,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
// Make a new branch in repo1
newBranch := "test_branch"
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch)
assert.NoError(t, err)
commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
@@ -18,7 +18,6 @@ import (
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
repo_service "code.gitea.io/gitea/services/repository"
"github.com/stretchr/testify/assert"
@@ -34,17 +33,17 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string)
Name: treePath,
Path: treePath,
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
LastCommitSHA: util.ToPointer(lastCommitSHA),
LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastCommitSHA: new(lastCommitSHA),
LastCommitterDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
LastAuthorDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
Type: "file",
Size: 30,
Encoding: util.ToPointer("base64"),
Content: util.ToPointer("IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"),
Encoding: new("base64"),
Content: new("IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"),
URL: &selfURL,
HTMLURL: &htmlURL,
GitURL: &gitURL,
DownloadURL: util.ToPointer(setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath),
DownloadURL: new(setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath),
Links: &api.FileLinksResponse{
Self: &selfURL,
GitURL: &gitURL,
@@ -61,7 +60,7 @@ func TestAPIGetContents(t *testing.T) {
})
}
func testAPIGetContents(t *testing.T, u *url.URL) {
func testAPIGetContents(t *testing.T, _ *url.URL) {
/*** SETUP ***/
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
@@ -85,7 +84,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
// Make a new branch in repo1
newBranch := "test_branch"
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch)
require.NoError(t, err)
commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
@@ -256,8 +255,8 @@ func testAPIGetContentsExt(t *testing.T) {
assert.Equal(t, "jpeg.jpg", respFile.Name)
assert.Nil(t, respFile.Encoding)
assert.Nil(t, respFile.Content)
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
assert.Equal(t, new(int64(107)), respFile.LfsSize)
assert.Equal(t, new("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
})
t.Run("FileContents", func(t *testing.T) {
// by default, no file content or commit info is returned
@@ -296,7 +295,7 @@ func testAPIGetContentsExt(t *testing.T) {
assert.NotNil(t, respFile.Content)
assert.Nil(t, contentsResponse.FileContents.LastCommitSHA)
assert.Nil(t, contentsResponse.FileContents.LastCommitMessage)
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
assert.Equal(t, new(int64(107)), respFile.LfsSize)
assert.Equal(t, new("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
})
}
@@ -12,10 +12,8 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -30,8 +28,8 @@ func TestAPIGitTags(t *testing.T) {
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
// Set up git config for the tagger
_ = gitcmd.NewCommand("config", "user.name").AddDynamicArguments(user.Name).Run(t.Context(), &gitcmd.RunOpts{Dir: repo.RepoPath()})
_ = gitcmd.NewCommand("config", "user.email").AddDynamicArguments(user.Email).Run(t.Context(), &gitcmd.RunOpts{Dir: repo.RepoPath()})
_ = gitrepo.GitConfigSet(t.Context(), repo, "user.name", user.Name)
_ = gitrepo.GitConfigSet(t.Context(), repo, "user.email", user.Email)
gitRepo, _ := gitrepo.OpenRepository(t.Context(), repo)
defer gitRepo.Close()
@@ -59,7 +57,7 @@ func TestAPIGitTags(t *testing.T) {
assert.Equal(t, aTagMessage+"\n", tag.Message)
assert.Equal(t, user.Name, tag.Tagger.Name)
assert.Equal(t, user.Email, tag.Tagger.Email)
assert.Equal(t, util.URLJoin(repo.APIURL(), "git/tags", aTag.ID.String()), tag.URL)
assert.Equal(t, repo.APIURL()+"/git/tags/"+aTag.ID.String(), tag.URL)
// Should NOT work for lightweight tags
badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s", user.Name, repo.Name, commit.ID.String()).
@@ -34,6 +34,7 @@ func TestAPICreateHook(t *testing.T) {
"url": "http://example.com/",
},
AuthorizationHeader: "Bearer s3cr3t",
Name: " CI notifications ",
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
@@ -41,4 +42,54 @@ func TestAPICreateHook(t *testing.T) {
DecodeJSON(t, resp, &apiHook)
assert.Equal(t, "http://example.com/", apiHook.Config["url"])
assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader)
assert.Equal(t, "CI notifications", apiHook.Name)
newName := "Deploy hook"
patchReq := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d", owner.Name, repo.Name, apiHook.ID), api.EditHookOption{
Name: &newName,
}).AddTokenAuth(token)
patchResp := MakeRequest(t, patchReq, http.StatusOK)
var patched *api.Hook
DecodeJSON(t, patchResp, &patched)
assert.Equal(t, newName, patched.Name)
hooksURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks", owner.Name, repo.Name)
// Create with Name field omitted: Name should be ""
req2 := NewRequestWithJSON(t, "POST", hooksURL, api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": "http://example.com/",
},
}).AddTokenAuth(token)
resp2 := MakeRequest(t, req2, http.StatusCreated)
var created *api.Hook
DecodeJSON(t, resp2, &created)
assert.Empty(t, created.Name)
hookURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d", owner.Name, repo.Name, created.ID)
// PATCH with Name set: existing Name must be updated
setName := "original"
setReq := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{
Name: &setName,
}).AddTokenAuth(token)
MakeRequest(t, setReq, http.StatusOK)
// PATCH without Name field: name must remain "original"
patchReq2 := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{}).AddTokenAuth(token)
patchResp2 := MakeRequest(t, patchReq2, http.StatusOK)
var notCleared *api.Hook
DecodeJSON(t, patchResp2, &notCleared)
assert.Equal(t, "original", notCleared.Name)
// PATCH with Name: "" explicitly: Name should be cleared to ""
clearReq := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{
Name: new(""),
}).AddTokenAuth(token)
clearResp := MakeRequest(t, clearReq, http.StatusOK)
var cleared *api.Hook
DecodeJSON(t, clearResp, &cleared)
assert.Empty(t, cleared.Name)
}
@@ -28,7 +28,6 @@ func TestRepoLanguages(t *testing.T) {
// Save new file to master branch
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": "test.go",
"content": "package main",
@@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPILFSNotStarted(t *testing.T) {
@@ -59,12 +60,12 @@ func TestAPILFSMediaType(t *testing.T) {
MakeRequest(t, req, http.StatusUnsupportedMediaType)
}
func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository {
ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository {
ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", "lfs-"+name+"-repo")
assert.NoError(t, err)
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", repoName)
require.NoError(t, err)
return repo
}
@@ -74,7 +75,7 @@ func TestAPILFSBatch(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "batch")
repo := createLFSTestRepository(t, "lfs-batch-repo")
content := []byte("dummy1")
oid := storeObjectInRepo(t, repo.ID, &content)
@@ -253,7 +254,7 @@ func TestAPILFSBatch(t *testing.T) {
assert.NoError(t, err)
assert.True(t, exist)
repo2 := createLFSTestRepository(t, "batch2")
repo2 := createLFSTestRepository(t, "lfs-batch2-repo")
content := []byte("dummy0")
storeObjectInRepo(t, repo2.ID, &content)
@@ -316,6 +317,7 @@ func TestAPILFSBatch(t *testing.T) {
ul := br.Objects[0].Actions["upload"]
assert.NotNil(t, ul)
assert.NotEmpty(t, ul.Href)
assert.Equal(t, "chunked", ul.Header["Transfer-Encoding"], "git-lfs client needs Transfer-Encoding to do chunked transfer")
assert.Contains(t, br.Objects[0].Actions, "verify")
vl := br.Objects[0].Actions["verify"]
assert.NotNil(t, vl)
@@ -329,7 +331,7 @@ func TestAPILFSUpload(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "upload")
repo := createLFSTestRepository(t, "lfs-upload-repo")
content := []byte("dummy3")
oid := storeObjectInRepo(t, repo.ID, &content)
@@ -433,7 +435,7 @@ func TestAPILFSVerify(t *testing.T) {
setting.LFS.StartServer = true
repo := createLFSTestRepository(t, "verify")
repo := createLFSTestRepository(t, "lfs-verify-repo")
content := []byte("dummy3")
oid := storeObjectInRepo(t, repo.ID, &content)
@@ -43,7 +43,6 @@ func TestAPIRepoLicense(t *testing.T) {
// Save new file to master branch
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": "LICENSE",
"content": testLicenseContent,
@@ -671,6 +671,16 @@ func TestAPIGenerateRepo(t *testing.T) {
templateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 44})
assertGeneratedRepoIsUsable := func(t *testing.T, ownerName string, repo *api.Repository) {
t.Helper()
assert.NotEmpty(t, repo.DefaultBranch)
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s", ownerName, repo.Name, repo.DefaultBranch).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
branch := DecodeJSON(t, resp, &api.Branch{})
assert.Equal(t, repo.DefaultBranch, branch.Name)
}
// user
repo := new(api.Repository)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
@@ -684,6 +694,7 @@ func TestAPIGenerateRepo(t *testing.T) {
DecodeJSON(t, resp, repo)
assert.Equal(t, "new-repo", repo.Name)
assertGeneratedRepoIsUsable(t, user.Name, repo)
// org
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
@@ -697,6 +708,7 @@ func TestAPIGenerateRepo(t *testing.T) {
DecodeJSON(t, resp, repo)
assert.Equal(t, "new-repo", repo.Name)
assertGeneratedRepoIsUsable(t, "org3", repo)
}
func TestAPIRepoGetReviewers(t *testing.T) {
@@ -502,11 +502,12 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
}
unauthorizedLevel := auth_model.Write
if categoryIsRequired {
if minRequiredLevel == auth_model.Read {
switch minRequiredLevel {
case auth_model.Read:
unauthorizedLevel = auth_model.NoAccess
} else if minRequiredLevel == auth_model.Write {
case auth_model.Write:
unauthorizedLevel = auth_model.Read
} else {
default:
assert.FailNow(t, "Invalid test case", "Unknown access token scope level: %v", minRequiredLevel)
}
}
@@ -514,10 +515,10 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
if unauthorizedLevel == auth_model.NoAccess {
continue
}
cateogoryUnauthorizedScopes := auth_model.GetRequiredScopes(
categoryUnauthorizedScopes := auth_model.GetRequiredScopes(
unauthorizedLevel,
category)
unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...)
unauthorizedScopes = append(unauthorizedScopes, categoryUnauthorizedScopes...)
}
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, unauthorizedScopes)
@@ -15,6 +15,21 @@ import (
"github.com/stretchr/testify/assert"
)
func TestPermissionsAPI(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("TokenNeeded", testTokenNeeded)
t.Run("WithOwnerUser", testWithOwnerUser)
t.Run("CanWriteUser", testCanWriteUser)
t.Run("AdminUser", testAdminUser)
t.Run("AdminCanNotCreateRepo", testAdminCanNotCreateRepo)
t.Run("CanReadUser", testCanReadUser)
t.Run("UnknownUser", testUnknownUser)
t.Run("UnknownOrganization", testUnknownOrganization)
t.Run("HiddenMemberPermissionsForbidden", testHiddenMemberPermissionsForbidden)
t.Run("PrivateOrgPermissionsNotFound", testPrivateOrgPermissionsNotFound)
}
type apiUserOrgPermTestCase struct {
LoginUser string
User string
@@ -22,16 +37,12 @@ type apiUserOrgPermTestCase struct {
ExpectedOrganizationPermissions api.OrganizationPermissions
}
func TestTokenNeeded(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testTokenNeeded(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/org6/permissions")
MakeRequest(t, req, http.StatusUnauthorized)
}
func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, auoptc.LoginUser)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser)
@@ -48,7 +59,7 @@ func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanCreateRepository, apiOP.CanCreateRepository)
}
func TestWithOwnerUser(t *testing.T) {
func testWithOwnerUser(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user2",
User: "user2",
@@ -63,7 +74,7 @@ func TestWithOwnerUser(t *testing.T) {
})
}
func TestCanWriteUser(t *testing.T) {
func testCanWriteUser(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user4",
User: "user4",
@@ -78,7 +89,7 @@ func TestCanWriteUser(t *testing.T) {
})
}
func TestAdminUser(t *testing.T) {
func testAdminUser(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user1",
User: "user28",
@@ -93,7 +104,7 @@ func TestAdminUser(t *testing.T) {
})
}
func TestAdminCanNotCreateRepo(t *testing.T) {
func testAdminCanNotCreateRepo(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user1",
User: "user28",
@@ -108,7 +119,7 @@ func TestAdminCanNotCreateRepo(t *testing.T) {
})
}
func TestCanReadUser(t *testing.T) {
func testCanReadUser(t *testing.T) {
sampleTest(t, apiUserOrgPermTestCase{
LoginUser: "user1",
User: "user24",
@@ -123,31 +134,62 @@ func TestCanReadUser(t *testing.T) {
})
}
func TestUnknowUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testUnknownUser(t *testing.T) {
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/users/unknow/orgs/org25/permissions").
req := NewRequest(t, "GET", "/api/v1/users/unknown/orgs/org25/permissions").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusNotFound)
var apiError api.APIError
DecodeJSON(t, resp, &apiError)
assert.Equal(t, "user redirect does not exist [name: unknow]", apiError.Message)
assert.Equal(t, "user redirect does not exist [name: unknown]", apiError.Message)
}
func TestUnknowOrganization(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testUnknownOrganization(t *testing.T) {
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/unknow/permissions").
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/unknown/permissions").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusNotFound)
var apiError api.APIError
DecodeJSON(t, resp, &apiError)
assert.Equal(t, "GetUserByName", apiError.Message)
}
func testHiddenMemberPermissionsForbidden(t *testing.T) {
session := loginUser(t, "user8")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
adminSession := loginUser(t, "user1")
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
adminReq := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
AddTokenAuth(adminToken)
resp := MakeRequest(t, adminReq, http.StatusOK)
var apiOP api.OrganizationPermissions
DecodeJSON(t, resp, &apiOP)
assert.Equal(t, api.OrganizationPermissions{
IsOwner: false,
IsAdmin: false,
CanWrite: true,
CanRead: true,
CanCreateRepository: true,
}, apiOP)
}
func testPrivateOrgPermissionsNotFound(t *testing.T) {
session := loginUser(t, "user8")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
req := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
}
@@ -155,3 +155,44 @@ func TestMyOrgs(t *testing.T) {
},
}, orgs)
}
func TestMyOrgsPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
normalUsername := "user2"
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/orgs").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var orgs []*api.Organization
DecodeJSON(t, resp, &orgs)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
assert.Equal(t, []*api.Organization{
{
ID: 17,
Name: org17.Name,
UserName: org17.Name,
FullName: org17.FullName,
Email: org17.Email,
AvatarURL: org17.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
{
ID: 3,
Name: org3.Name,
UserName: org3.Name,
FullName: org3.FullName,
Email: org3.Email,
AvatarURL: org3.AvatarLink(t.Context()),
Description: "",
Website: "",
Location: "",
Visibility: "public",
},
}, orgs)
}
@@ -0,0 +1,177 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/require"
)
func TestAPIPublicOnlySelfUserRoutes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user31"})
require.True(t, privateUser.Visibility.IsPrivate())
privateSession := loginUser(t, privateUser.Name)
privateReadUserToken := getTokenForLoggedInUser(t, privateSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeReadUser,
)
privateWriteUserToken := getTokenForLoggedInUser(t, privateSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeWriteUser,
)
t.Run("PrivateProfileForbidden", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
})
t.Run("PrivateSensitiveSelfRoutesForbidden", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
hideEmail := true
settingsReq := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
HideEmail: &hideEmail,
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, settingsReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/emails").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
emailReq := NewRequestWithJSON(t, "POST", "/api/v1/user/emails", &api.CreateEmailOption{
Emails: []string{"user31-public-only@example.com"},
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, emailReq, http.StatusForbidden)
keyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", api.CreateKeyOption{
Title: "public-only-private-key",
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, keyReq, http.StatusForbidden)
oauthReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &api.CreateOAuth2ApplicationOptions{
Name: "public-only-private-oauth-app",
RedirectURIs: []string{"https://example.com/callback"},
ConfidentialClient: true,
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, oauthReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_keys").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
gpgKeyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", &api.CreateGPGKeyOption{
ArmoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\ncomment\n-----END PGP PUBLIC KEY BLOCK-----",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, gpgKeyReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_key_token").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
gpgVerifyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_key_verify", &api.VerifyGPGKeyOption{
KeyID: "deadbeef",
Signature: "invalid-signature",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, gpgVerifyReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/actions/variables").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/actions/secrets/PRIVATE_SECRET").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
variableReq := NewRequestWithJSON(t, "POST", "/api/v1/user/actions/variables/PRIVATE_VAR", api.CreateVariableOption{
Value: "private-value",
Description: "must stay private",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, variableReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/hooks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
hookReq := NewRequestWithJSON(t, "POST", "/api/v1/user/hooks", api.CreateHookOption{
Type: "gitea",
Config: api.CreateHookOptionConfig{
"content_type": "json",
"url": "http://example.com/",
},
Name: "public-only-private-hook",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, hookReq, http.StatusForbidden)
avatarReq := NewRequestWithJSON(t, "POST", "/api/v1/user/avatar", &api.UpdateUserAvatarOption{
Image: "aGVsbG8=",
}).AddTokenAuth(privateWriteUserToken)
MakeRequest(t, avatarReq, http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/avatar").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/times").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/stopwatches").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/teams").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/blocks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/blocks/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
})
t.Run("PublicRepoRoutesFilterAndRejectMutations", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
publicSession := loginUser(t, "user2")
fullWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteRepository,
)
publicOnlyReadRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeReadUser,
auth_model.AccessTokenScopeReadRepository,
)
publicOnlyWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
auth_model.AccessTokenScopePublicOnly,
auth_model.AccessTokenScopeWriteUser,
auth_model.AccessTokenScopeWriteRepository,
)
publicRepoName := "public-only-visible-self-repo"
privateRepoName := "public-only-hidden-self-repo"
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: publicRepoName,
Private: false,
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
publicRepo := DecodeJSON(t, resp, &api.Repository{})
require.Equal(t, "user2/"+publicRepoName, publicRepo.FullName)
resp = MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: privateRepoName,
Private: true,
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
privateRepo := DecodeJSON(t, resp, &api.Repository{})
require.Equal(t, "user2/"+privateRepoName, privateRepo.FullName)
MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/"+privateRepoName).AddTokenAuth(publicOnlyReadRepoToken), http.StatusNotFound)
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/repos").AddTokenAuth(publicOnlyReadRepoToken), http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
foundPublicRepo := false
for _, repo := range repos {
require.NotEqual(t, privateRepo.FullName, repo.FullName)
if repo.FullName == publicRepo.FullName {
foundPublicRepo = true
}
}
require.True(t, foundPublicRepo)
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
Name: "public-only-rejected-self-repo",
Private: false,
}).AddTokenAuth(publicOnlyWriteRepoToken), http.StatusForbidden)
})
}
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIStar(t *testing.T) {
@@ -155,3 +156,24 @@ func TestAPIStarDisabled(t *testing.T) {
MakeRequest(t, req, http.StatusForbidden)
})
}
func TestAPIStarPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
req := NewRequest(t, "GET", "/api/v1/user/starred").
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
if assert.Len(t, repos, 1) {
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
req = NewRequest(t, "GET", "/api/v1/users/user2/starred").
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
require.Len(t, repos, 1)
assert.Equal(t, "user5/repo4", repos[0].FullName)
}
@@ -94,3 +94,28 @@ func TestAPIWatch(t *testing.T) {
MakeRequest(t, req, http.StatusNoContent)
})
}
func TestAPIWatchPublicOnly(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
writeRepoToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadUser)
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo1/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo2/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
repos := DecodeJSON(t, resp, []api.Repository{})
for _, r := range repos {
assert.False(t, r.Private, "private repo %s leaked via /user/subscriptions", r.FullName)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user1/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
repos = DecodeJSON(t, resp, []api.Repository{})
for _, r := range repos {
assert.False(t, r.Private, "private repo %s leaked via /users/{username}/subscriptions", r.FullName)
}
assert.NotContains(t, repoNames(repos), "user2/repo2")
}
@@ -14,6 +14,7 @@ import (
"strings"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/test"
@@ -26,6 +27,15 @@ import (
"github.com/stretchr/testify/require"
)
type attachmentScopeCase struct {
name string
url string
readIssueStatus int
readRepoStatus int
publicOnlyIssueStatus int
publicOnlyRepoStatus int
}
func testGeneratePngBytes() []byte {
myImage := image.NewRGBA(image.Rect(0, 0, 32, 32))
var buff bytes.Buffer
@@ -33,15 +43,15 @@ func testGeneratePngBytes() []byte {
return buff.Bytes()
}
func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL, filename string, content []byte, expectedStatus int) string {
return testCreateAttachment(t, session, csrf, repoURL, "issues", filename, content, expectedStatus)
func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string {
return testCreateAttachment(t, session, repoURL, "issues", filename, content, expectedStatus)
}
func testCreateReleaseAttachment(t *testing.T, session *TestSession, csrf, repoURL, filename string, content []byte, expectedStatus int) string {
return testCreateAttachment(t, session, csrf, repoURL, "releases", filename, content, expectedStatus)
func testCreateReleaseAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string {
return testCreateAttachment(t, session, repoURL, "releases", filename, content, expectedStatus)
}
func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string {
func testCreateAttachment(t *testing.T, session *TestSession, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string {
body := &bytes.Buffer{}
// Setup multi-part
@@ -54,7 +64,6 @@ func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, iss
assert.NoError(t, err)
req := NewRequestWithBody(t, "POST", repoURL+"/"+issueOrRelease+"/attachments", body)
req.Header.Add("X-Csrf-Token", csrf)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp := session.MakeRequest(t, req, expectedStatus)
@@ -66,15 +75,13 @@ func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, iss
return obj["uuid"]
}
func testDeleteIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL, uuid string, expectedStatus int) {
func testDeleteIssueAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) {
req := NewRequestWithValues(t, "POST", repoURL+"/issues/attachments/remove", map[string]string{"file": uuid})
req.Header.Add("X-Csrf-Token", csrf)
session.MakeRequest(t, req, expectedStatus)
}
func testDeleteReleaseAttachment(t *testing.T, session *TestSession, csrf, repoURL, uuid string, expectedStatus int) {
func testDeleteReleaseAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) {
req := NewRequestWithValues(t, "POST", repoURL+"/releases/attachments/remove", map[string]string{"file": uuid})
req.Header.Add("X-Csrf-Token", csrf)
session.MakeRequest(t, req, expectedStatus)
}
@@ -100,20 +107,20 @@ func testUploadAttachmentDeleteTemp(t *testing.T) {
defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) {
tmpFileCountDuringUpload = countTmpFile()
})()
_ = testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
_ = testCreateIssueAttachment(t, session, "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory")
assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload")
}
func testCreateAnonymousAttachment(t *testing.T) {
session := emptyTestSession(t)
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
testCreateIssueAttachment(t, session, "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
}
func testCreateUser2IssueAttachment(t *testing.T) {
const repoURL = "user2/repo1"
session := loginUser(t, "user2")
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
uuid := testCreateIssueAttachment(t, session, repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
req := NewRequest(t, "GET", repoURL+"/issues/new")
resp := session.MakeRequest(t, req, http.StatusOK)
@@ -123,7 +130,6 @@ func testCreateUser2IssueAttachment(t *testing.T) {
assert.True(t, exists, "The template has changed")
postData := map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"title": "New Issue With Attachment",
"content": "some content",
"files": uuid,
@@ -187,24 +193,118 @@ func testDeleteAttachmentPermissions(t *testing.T) {
ownerSession := loginUser(t, "user2")
readonlySession := loginUser(t, "user5")
ownerCSRF := GetUserCSRFToken(t, ownerSession)
readonlyCSRF := GetUserCSRFToken(t, readonlySession)
issueFromOwner := testCreateIssueAttachment(t, ownerSession, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, readonlySession, repoURL, issueFromOwner, http.StatusForbidden)
issueFromOwner := testCreateIssueAttachment(t, ownerSession, ownerCSRF, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, readonlySession, readonlyCSRF, repoURL, issueFromOwner, http.StatusForbidden)
issueFromReader := testCreateIssueAttachment(t, readonlySession, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, repoURL, issueFromReader, http.StatusOK)
issueFromReader := testCreateIssueAttachment(t, readonlySession, readonlyCSRF, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, repoURL, issueFromReader, http.StatusOK)
testCreateReleaseAttachment(t, readonlySession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound)
testCreateReleaseAttachment(t, readonlySession, readonlyCSRF, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound)
crossRepoUUID := testCreateIssueAttachment(t, ownerSession, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
testDeleteIssueAttachment(t, ownerSession, repoURL, crossRepoUUID, http.StatusOK)
crossRepoUUID := testCreateIssueAttachment(t, ownerSession, ownerCSRF, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK)
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, repoURL, crossRepoUUID, http.StatusOK)
releaseUUID := testCreateReleaseAttachment(t, ownerSession, ownerCSRF, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK)
testDeleteReleaseAttachment(t, ownerSession, ownerCSRF, repoURL, releaseUUID, http.StatusOK)
releaseUUID := testCreateReleaseAttachment(t, ownerSession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK)
testDeleteReleaseAttachment(t, ownerSession, repoURL, releaseUUID, http.StatusOK)
// test deleting release attachment from another repo
testDeleteReleaseAttachment(t, ownerSession, ownerCSRF, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
testDeleteReleaseAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
}
func TestAttachmentTokenScopes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
for _, uuid := range []string{
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
} {
_, err := storage.Attachments.Save(repo_model.AttachmentRelativePath(uuid), strings.NewReader("hello world"), -1)
require.NoError(t, err)
}
readIssueToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue)
readRepoToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc)
publicOnlyIssueToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
publicOnlyRepoToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
cases := []attachmentScopeCase{
{
name: "GlobalPublicIssueAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusOK,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPublicIssueAttachment",
url: "/user2/repo1/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusOK,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "GlobalPrivateIssueAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPrivateIssueAttachment",
url: "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
readIssueStatus: http.StatusOK,
readRepoStatus: http.StatusForbidden,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "GlobalPublicReleaseAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusOK,
},
{
name: "RepoPublicReleaseAttachment",
url: "/user2/repo1/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusOK,
},
{
name: "GlobalPrivateReleaseAttachment",
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
{
name: "RepoPrivateReleaseAttachment",
url: "/user2/repo2/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
readIssueStatus: http.StatusForbidden,
readRepoStatus: http.StatusOK,
publicOnlyIssueStatus: http.StatusForbidden,
publicOnlyRepoStatus: http.StatusForbidden,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(miscToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(readIssueToken), tc.readIssueStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(readRepoToken), tc.readRepoStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(publicOnlyIssueToken), tc.publicOnlyIssueStatus)
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(publicOnlyRepoToken), tc.publicOnlyRepoStatus)
})
}
}
@@ -125,7 +125,7 @@ type ldapAuthOptions struct {
groupTeamMapRemoval string
}
func (te *ldapTestEnv) buildAuthSourcePayload(csrf string, opts ...ldapAuthOptions) map[string]string {
func (te *ldapTestEnv) buildAuthSourcePayload(opts ...ldapAuthOptions) map[string]string {
opt := util.OptionalArg(opts)
// Modify user filter to test group filter explicitly
userFilter := "(&(objectClass=inetOrgPerson)(memberOf=cn=git,ou=people,dc=planetexpress,dc=com)(uid=%s))"
@@ -134,7 +134,6 @@ func (te *ldapTestEnv) buildAuthSourcePayload(csrf string, opts ...ldapAuthOptio
}
return map[string]string{
"_csrf": csrf,
"type": "2",
"name": "ldap",
"host": te.serverHost,
@@ -164,8 +163,7 @@ func (te *ldapTestEnv) buildAuthSourcePayload(csrf string, opts ...ldapAuthOptio
func (te *ldapTestEnv) addAuthSource(t *testing.T, opts ...ldapAuthOptions) {
session := loginUser(t, "user1")
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", te.buildAuthSourcePayload(csrf, opts...))
req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", te.buildAuthSourcePayload(opts...))
session.MakeRequest(t, req, http.StatusSeeOther)
}
@@ -212,13 +210,12 @@ func TestLDAPAuthChange(t *testing.T) {
req = NewRequest(t, "GET", href)
resp = session.MakeRequest(t, req, http.StatusOK)
doc = NewHTMLParser(t, resp.Body)
csrf := doc.GetCSRF()
host, _ := doc.Find(`input[name="host"]`).Attr("value")
assert.Equal(t, te.serverHost, host)
binddn, _ := doc.Find(`input[name="bind_dn"]`).Attr("value")
assert.Equal(t, "uid=gitea,ou=service,dc=planetexpress,dc=com", binddn)
req = NewRequestWithValues(t, "POST", href, te.buildAuthSourcePayload(csrf, ldapAuthOptions{groupTeamMapRemoval: "off"}))
req = NewRequestWithValues(t, "POST", href, te.buildAuthSourcePayload(ldapAuthOptions{groupTeamMapRemoval: "off"}))
session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequest(t, "GET", href)
@@ -267,8 +264,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
csrf := GetUserCSRFToken(t, session)
payload := te.buildAuthSourcePayload(csrf)
payload := te.buildAuthSourcePayload()
payload["attribute_username"] = ""
req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload)
session.MakeRequest(t, req, http.StatusSeeOther)
@@ -285,7 +281,6 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
for _, u := range te.gitLDAPUsers {
req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
"_csrf": csrf,
"user_name": u.UserName,
"password": u.Password,
})
@@ -512,8 +507,7 @@ func TestLDAPPreventInvalidGroupTeamMap(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
csrf := GetUserCSRFToken(t, session)
payload := te.buildAuthSourcePayload(csrf, ldapAuthOptions{groupTeamMap: `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, groupTeamMapRemoval: "off"})
payload := te.buildAuthSourcePayload(ldapAuthOptions{groupTeamMap: `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`, groupTeamMapRemoval: "off"})
req := NewRequestWithValues(t, "POST", "/-/admin/auths/new", payload)
session.MakeRequest(t, req, http.StatusOK) // StatusOK = failed, StatusSeeOther = ok
}
@@ -61,9 +61,7 @@ func branchAction(t *testing.T, button string) (*HTMLDoc, string) {
t.Skip()
}
req = NewRequestWithValues(t, "POST", link, map[string]string{
"_csrf": htmlDoc.GetCSRF(),
})
req = NewRequest(t, "POST", link)
session.MakeRequest(t, req, http.StatusOK)
url, err := url.Parse(link)
@@ -12,7 +12,7 @@ import (
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
@@ -26,24 +26,20 @@ func TestChangeDefaultBranch(t *testing.T) {
session := loginUser(t, owner.Name)
branchesURL := fmt.Sprintf("/%s/%s/settings/branches", owner.Name, repo.Name)
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", branchesURL, map[string]string{
"_csrf": csrf,
"action": "default_branch",
"branch": "DefaultBranch",
})
session.MakeRequest(t, req, http.StatusSeeOther)
csrf = GetUserCSRFToken(t, session)
req = NewRequestWithValues(t, "POST", branchesURL, map[string]string{
"_csrf": csrf,
"action": "default_branch",
"branch": "does_not_exist",
})
session.MakeRequest(t, req, http.StatusNotFound)
}
func checkDivergence(t *testing.T, session *TestSession, branchesURL, expectedDefaultBranch string, expectedBranchToDivergence map[string]git.DivergeObject) {
func checkDivergence(t *testing.T, session *TestSession, branchesURL, expectedDefaultBranch string, expectedBranchToDivergence map[string]*gitrepo.DivergeObject) {
req := NewRequest(t, "GET", branchesURL)
resp := session.MakeRequest(t, req, http.StatusOK)
@@ -92,7 +88,7 @@ func TestChangeDefaultBranchDivergence(t *testing.T) {
settingsBranchesURL := fmt.Sprintf("/%s/%s/settings/branches", owner.Name, repo.Name)
// check branch divergence before switching default branch
expectedBranchToDivergenceBefore := map[string]git.DivergeObject{
expectedBranchToDivergenceBefore := map[string]*gitrepo.DivergeObject{
"not-signed": {
Ahead: 0,
Behind: 0,
@@ -110,16 +106,14 @@ func TestChangeDefaultBranchDivergence(t *testing.T) {
// switch default branch
newDefaultBranch := "good-sign-not-yet-validated"
csrf := GetUserCSRFToken(t, session)
req := NewRequestWithValues(t, "POST", settingsBranchesURL, map[string]string{
"_csrf": csrf,
"action": "default_branch",
"branch": newDefaultBranch,
})
session.MakeRequest(t, req, http.StatusSeeOther)
// check branch divergence after switching default branch
expectedBranchToDivergenceAfter := map[string]git.DivergeObject{
expectedBranchToDivergenceAfter := map[string]*gitrepo.DivergeObject{
"master": {
Ahead: 1,
Behind: 0,
@@ -10,7 +10,6 @@ import (
"code.gitea.io/gitea/cmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
@@ -38,13 +37,14 @@ func Test_CmdKeys(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// FIXME: this test is not quite right. Each "command run" always re-initializes settings
defer test.MockVariableValue(&cmd.CmdKeys.Before, nil)() // don't re-initialize logger during the test
keysCmd := cmd.NewKeysCommand()
keysCmd.Before = nil // don't re-initialize logger during the test
var stdout, stderr bytes.Buffer
app := &cli.Command{
Writer: &stdout,
ErrWriter: &stderr,
Commands: []*cli.Command{cmd.CmdKeys},
Commands: []*cli.Command{keysCmd},
}
err := app.Run(t.Context(), append([]string{"prog"}, tt.args...))
if tt.wantErr {
@@ -4,19 +4,25 @@
package integration
import (
"bytes"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"testing"
"time"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/test"
repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCompareTag(t *testing.T) {
@@ -124,6 +130,76 @@ func TestCompareBranches(t *testing.T) {
inspectCompare(t, htmlDoc, diffCount, diffChanges)
}
func createUnrelatedBranch(t *testing.T, repo *repo_model.Repository, user *user_model.User, branchName string) {
t.Helper()
repoPath := repo_model.RepoPath(user.Name, repo.Name)
require.NoError(t, gitcmd.NewCommand("read-tree", "--empty").WithDir(repoPath).Run(t.Context()))
stdout, _, err := gitcmd.NewCommand("hash-object", "-w", "--stdin").
WithDir(repoPath).
WithStdinBytes([]byte("Unrelated File")).
RunStdString(t.Context())
require.NoError(t, err)
blobSHA := strings.TrimSpace(stdout)
_, _, err = gitcmd.NewCommand("update-index", "--add", "--replace", "--cacheinfo").
AddDynamicArguments("100644", blobSHA, "unrelated.txt").
WithDir(repoPath).
RunStdString(t.Context())
require.NoError(t, err)
stdout, _, err = gitcmd.NewCommand("write-tree").WithDir(repoPath).RunStdString(t.Context())
require.NoError(t, err)
treeSHA := strings.TrimSpace(stdout)
commitTimeStr := time.Now().Format(time.RFC3339)
doerSig := user.NewGitSig()
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+doerSig.Name,
"GIT_AUTHOR_EMAIL="+doerSig.Email,
"GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_NAME="+doerSig.Name,
"GIT_COMMITTER_EMAIL="+doerSig.Email,
"GIT_COMMITTER_DATE="+commitTimeStr,
)
messageBytes := bytes.NewBufferString("Unrelated\n")
stdout, _, err = gitcmd.NewCommand("commit-tree").AddDynamicArguments(treeSHA).
WithEnv(env).
WithDir(repoPath).
WithStdinBytes(messageBytes.Bytes()).
RunStdString(t.Context())
require.NoError(t, err)
commitSHA := strings.TrimSpace(stdout)
_, _, err = gitcmd.NewCommand("branch").AddDynamicArguments(branchName, commitSHA).
WithDir(repoPath).
RunStdString(t.Context())
require.NoError(t, err)
}
func TestCompareBranchesNoCommonMergeBase(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user2.ID, Name: "repo1"})
createUnrelatedBranch(t, repo1, user2, "unrelated-history")
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo1/compare/master...unrelated-history")
resp := session.MakeRequest(t, req, http.StatusOK)
body := resp.Body.String()
htmlDoc := NewHTMLParser(t, resp.Body)
selection := htmlDoc.doc.Find(".ui.dropdown.select-branch")
assert.Lenf(t, selection.Nodes, 2, "The template has changed")
assert.Contains(t, body, "These branches do not share a common merge base")
assert.Equal(t, 1, htmlDoc.doc.Find(`a.item[href="/user2/repo1/compare/master...unrelated-history"]`).Length())
assert.Equal(t, 1, htmlDoc.doc.Find(`a.item[href="/user2/repo1/compare/master...master"]`).Length())
assert.Equal(t, 0, htmlDoc.doc.Find(".pullrequest-form").Length())
}
func TestCompareCodeExpand(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
@@ -0,0 +1,126 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"time"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func mockSystemConfig[T any](t *testing.T, opt *config.Option[T], v T) func() {
jsonBuf, _ := json.Marshal(v)
old := opt.Value(t.Context())
require.NoError(t, system_model.SetSettings(t.Context(), map[string]string{opt.DynKey(): string(jsonBuf)}))
config.GetDynGetter().InvalidateCache()
return func() {
jsonBuf, _ := json.Marshal(old)
require.NoError(t, system_model.SetSettings(t.Context(), map[string]string{opt.DynKey(): string(jsonBuf)}))
config.GetDynGetter().InvalidateCache()
}
}
func TestInstance(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("WebBanner", func(t *testing.T) {
t.Run("Visibility", func(t *testing.T) {
defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{
DisplayEnabled: true,
ContentMessage: "Planned **upgrade** in progress.",
})()
t.Run("AnonymousUserSeesBanner", func(t *testing.T) {
resp := MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK)
assert.Contains(t, resp.Body.String(), "Planned <strong>upgrade</strong> in progress.")
})
t.Run("NormalUserSeesBanner", func(t *testing.T) {
sess := loginUser(t, "user2")
resp := sess.MakeRequest(t, NewRequest(t, "GET", "/user/settings"), http.StatusOK)
assert.Contains(t, resp.Body.String(), "Planned <strong>upgrade</strong> in progress.")
})
t.Run("AdminSeesBannerWithoutEditHint", func(t *testing.T) {
sess := loginUser(t, "user1")
resp := sess.MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusOK)
assert.Contains(t, resp.Body.String(), "Planned <strong>upgrade</strong> in progress.")
assert.NotContains(t, resp.Body.String(), "Edit this banner")
})
t.Run("APIRequestUnchanged", func(t *testing.T) {
MakeRequest(t, NewRequest(t, "GET", "/api/v1/version"), http.StatusOK)
})
})
t.Run("TimeWindow", func(t *testing.T) {
now := time.Now().Unix()
defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{
DisplayEnabled: true,
ContentMessage: "Future banner",
StartTimeUnix: now + 3600,
EndTimeUnix: now + 7200,
})()
resp := MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK)
assert.NotContains(t, resp.Body.String(), "Future banner")
defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{
DisplayEnabled: true,
ContentMessage: "Expired banner",
StartTimeUnix: now - 7200,
EndTimeUnix: now - 3600,
})()
resp = MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK)
assert.NotContains(t, resp.Body.String(), "Expired banner")
})
})
t.Run("MaintenanceMode", func(t *testing.T) {
defer mockSystemConfig(t, setting.Config().Instance.WebBanner, setting.WebBannerType{
DisplayEnabled: true,
ContentMessage: "MaintenanceModeBanner",
})()
defer mockSystemConfig(t, setting.Config().Instance.MaintenanceMode, setting.MaintenanceModeType{AdminWebAccessOnly: true})()
t.Run("AnonymousUser", func(t *testing.T) {
req := NewRequest(t, "GET", "/")
req.Header.Add("Accept", "text/html")
resp := MakeRequest(t, req, http.StatusServiceUnavailable)
assert.Contains(t, resp.Body.String(), "MaintenanceModeBanner")
assert.Contains(t, resp.Body.String(), `href="/user/login"`) // it must contain the login link
MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusOK)
MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusSeeOther)
MakeRequest(t, NewRequest(t, "GET", "/api/internal/dummy"), http.StatusForbidden)
})
t.Run("AdminLogin", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{"user_name": "user1", "password": userPassword})
resp := MakeRequest(t, req, http.StatusSeeOther)
assert.Equal(t, "/-/admin", resp.Header().Get("Location"))
sess := loginUser(t, "user1")
req = NewRequest(t, "GET", "/")
req.Header.Add("Accept", "text/html")
resp = sess.MakeRequest(t, req, http.StatusServiceUnavailable)
assert.Contains(t, resp.Body.String(), "MaintenanceModeBanner")
resp = sess.MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusSeeOther)
assert.Equal(t, "/-/admin", resp.Header().Get("Location"))
sess.MakeRequest(t, NewRequest(t, "GET", "/-/admin"), http.StatusOK)
})
})
}
@@ -97,9 +97,7 @@ func TestSessionFileCreation(t *testing.T) {
// We're not logged in so there should be no session
assert.False(t, sessionFileExist(t, tmpDir, sessionID))
doc := NewHTMLParser(t, resp.Body)
req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
"_csrf": doc.GetCSRF(),
"user_name": "user2",
"password": userPassword,
})
@@ -1,34 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestCsrfProtection(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// test web form csrf via form
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user.Name)
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
"_csrf": "fake_csrf",
})
resp := session.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
// test web form csrf via header. TODO: should use an UI api to test
req = NewRequest(t, "POST", "/user/settings")
req.Header.Add("X-Csrf-Token", "fake_csrf")
resp = session.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "Invalid CSRF token")
}
@@ -32,11 +32,8 @@ func TestUserDeleteAccount(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user8")
csrf := GetUserCSRFToken(t, session)
urlStr := "/user/settings/account/delete?password=" + userPassword
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
"_csrf": csrf,
})
req := NewRequest(t, "POST", urlStr)
session.MakeRequest(t, req, http.StatusSeeOther)
assertUserDeleted(t, 8)
@@ -47,11 +44,8 @@ func TestUserDeleteAccountStillOwnRepos(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
csrf := GetUserCSRFToken(t, session)
urlStr := "/user/settings/account/delete?password=" + userPassword
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
"_csrf": csrf,
})
req := NewRequest(t, "POST", urlStr)
session.MakeRequest(t, req, http.StatusSeeOther)
// user should not have been deleted, because the user still owns repos
@@ -7,87 +7,205 @@ import (
"net/http"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestDownloadByID(t *testing.T) {
type downloadScopeCase struct {
name string
method string
url string
withScope int
publicOnlyOK bool
}
func TestDownloadRepoContent(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Request raw blob
req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
resp := session.MakeRequest(t, req, http.StatusOK)
t.Run("RawBlob", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
})
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
t.Run("SVGUsesSecureHeaders", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
})
t.Run("MediaBlob", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
})
t.Run("MediaSVGUsesSecureHeaders", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
})
t.Run("MimeTypeMap", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp := session.MakeRequest(t, req, http.StatusOK)
// although the file is a valid XML file, it is served as "text/plain" to avoid site content spamming (the same to "text/html" files)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.MimeTypeMap)()
setting.MimeTypeMap.Enabled = true
setting.MimeTypeMap.Map[".xml"] = "text/xml"
req = NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp = session.MakeRequest(t, req, http.StatusOK)
// respect the mime mapping, and "text/plain" protection isn't used anymore
assert.Equal(t, "text/xml; charset=utf-8", resp.Header().Get("Content-Type"))
assert.Equal(t, "inline; filename=test.xml", resp.Header().Get("Content-Disposition"))
setting.MimeTypeMap.Map[".xml"] = "application/xml"
req = NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp = session.MakeRequest(t, req, http.StatusOK)
// non-text file don't have "charset"
assert.Equal(t, "application/xml", resp.Header().Get("Content-Type"))
})
}
func TestDownloadByIDForSVGUsesSecureHeaders(t *testing.T) {
func TestDownloadRepoContentTokenScopes(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
ownerReadToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc)
publicOnlyToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
// Request raw blob
req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
resp := session.MakeRequest(t, req, http.StatusOK)
cases := []downloadScopeCase{
{
name: "PublicArchiveDownload",
method: http.MethodGet,
url: "/user2/repo1/archive/master.tar.gz",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PrivateArchiveDownload",
method: http.MethodGet,
url: "/user2/repo2/archive/master.tar.gz",
withScope: http.StatusOK,
publicOnlyOK: false,
},
{
name: "PublicArchiveInitiate",
method: http.MethodPost,
url: "/user2/repo1/archive/master.tar.gz",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PrivateArchiveInitiate",
method: http.MethodPost,
url: "/user2/repo2/archive/master.tar.gz",
withScope: http.StatusOK,
publicOnlyOK: false,
},
{
name: "PublicRawBlob",
method: http.MethodGet,
url: "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicRawBranch",
method: http.MethodGet,
url: "/user2/repo1/raw/branch/master/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicRawTag",
method: http.MethodGet,
url: "/user2/repo1/raw/tag/v1.1/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicRawCommit",
method: http.MethodGet,
url: "/user2/repo1/raw/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicMediaBlob",
method: http.MethodGet,
url: "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicMediaBranch",
method: http.MethodGet,
url: "/user2/repo1/media/branch/master/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicMediaTag",
method: http.MethodGet,
url: "/user2/repo1/media/tag/v1.1/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PublicMediaCommit",
method: http.MethodGet,
url: "/user2/repo1/media/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d/README.md",
withScope: http.StatusOK,
publicOnlyOK: true,
},
{
name: "PrivateRawBranch",
method: http.MethodGet,
url: "/user2/repo2/raw/branch/master/test.xml",
withScope: http.StatusOK,
publicOnlyOK: false,
},
{
name: "PrivateRawBlob",
method: http.MethodGet,
url: "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b",
withScope: http.StatusOK,
publicOnlyOK: false,
},
{
name: "PrivateMediaBranch",
method: http.MethodGet,
url: "/user2/repo2/media/branch/master/test.xml",
withScope: http.StatusOK,
publicOnlyOK: false,
},
}
assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
}
func TestDownloadByIDMedia(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Request raw blob
req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
}
func TestDownloadByIDMediaForSVGUsesSecureHeaders(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Request raw blob
req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
}
func TestDownloadRawTextFileWithoutMimeTypeMapping(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
}
func TestDownloadRawTextFileWithMimeTypeMapping(t *testing.T) {
defer tests.PrepareTestEnv(t)()
setting.MimeTypeMap.Map[".xml"] = "text/xml"
setting.MimeTypeMap.Enabled = true
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/xml; charset=utf-8", resp.Header().Get("Content-Type"))
delete(setting.MimeTypeMap.Map, ".xml")
setting.MimeTypeMap.Enabled = false
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(miscToken), http.StatusForbidden)
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(ownerReadToken), tc.withScope)
publicOnlyStatus := http.StatusForbidden
if tc.publicOnlyOK {
publicOnlyStatus = tc.withScope
}
MakeRequest(t, NewRequest(t, tc.method, tc.url).AddTokenAuth(publicOnlyToken), publicOnlyStatus)
})
}
}
@@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
@@ -86,7 +87,6 @@ func testEditorProtectedBranch(t *testing.T) {
session := loginUser(t, "user2")
// Change the "master" branch to "protected"
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"rule_name": "master",
"enable_push": "true",
})
@@ -98,6 +98,42 @@ func testEditorProtectedBranch(t *testing.T) {
resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
// Change "master" branch to mark files under "docs/" as unprotected
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"rule_name": "master",
"protected_file_patterns": "",
"unprotected_file_patterns": "docs/*.md",
"enable_push": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
flashMsg = session.GetCookieFlashMessage()
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/docs/new.md", map[string]string{"tree_path": "docs/new.md", "commit_choice": "direct"})
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), `"redirect":"/user2/repo1/src/branch/master/docs/new.md"`)
// Form's destination (renamed.md) is decided by the pre-receive hook, not the controller.
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_edit/master/docs/new.md", map[string]string{
"content": "renamed via editor",
"commit_choice": "direct",
"tree_path": "docs/renamed.md",
})
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), `"redirect":"/user2/repo1/src/branch/master/docs/renamed.md"`)
// Protected source path: controller rejects up-front regardless of unprotected destination.
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_edit/master/README.md", map[string]string{
"commit_choice": "direct",
"tree_path": "docs/from-readme.md",
})
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_delete/master/README.md", map[string]string{"commit_choice": "direct"})
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
}
func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder {
@@ -105,7 +141,6 @@ func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
form := map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"last_commit": htmlDoc.GetInputValueByName("last_commit"),
}
maps.Copy(form, params)
@@ -149,11 +184,10 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra
func testEditorDiffPreview(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"content": "Hello, World (Edited)\n",
"content": "# repo1 (Edited)",
})
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`)
assert.Contains(t, resp.Body.String(), `<span class="added-code"> (Edited)</span>`)
}
func testEditorPatchFile(t *testing.T) {
@@ -187,7 +221,7 @@ func testEditorWebGitCommitEmail(t *testing.T) {
require.True(t, user.KeepEmailPrivate)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, _ := git.OpenRepository(t.Context(), repo1.RepoPath())
gitRepo, _ := gitrepo.OpenRepository(t.Context(), repo1)
defer gitRepo.Close()
getLastCommit := func(t *testing.T) *git.Commit {
c, err := gitRepo.GetBranchCommit("master")
@@ -199,7 +233,6 @@ func testEditorWebGitCommitEmail(t *testing.T) {
makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder {
lastCommit := getLastCommit(t)
params["_csrf"] = GetUserCSRFToken(t, session)
params["last_commit"] = lastCommit.ID.String()
params["commit_choice"] = "direct"
req := NewRequestWithValues(t, "POST", link, params)
@@ -224,7 +257,6 @@ func testEditorWebGitCommitEmail(t *testing.T) {
uploadForm := multipart.NewWriter(body)
file, _ := uploadForm.CreateFormFile("file", name)
_, _ = io.Copy(file, strings.NewReader(content))
_ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session))
_ = uploadForm.Close()
req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body)
@@ -262,7 +294,7 @@ func testEditorWebGitCommitEmail(t *testing.T) {
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
paramsForKeepPrivate["commit_email"] = ""
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "2+user2@noreply.example.org")
})
t.Run("ChooseEmail", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
@@ -346,7 +378,7 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes")
// fork the repository
req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_fork", branch), map[string]string{"_csrf": GetUserCSRFToken(t, session)})
req = NewRequest(t, "POST", path.Join(owner, repo, "_fork", branch))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.JSONEq(t, `{"redirect":""}`, resp.Body.String())
}
@@ -358,7 +390,6 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
// Archive the repository
req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"repo_name": repo,
"action": "archive",
},
@@ -373,7 +404,6 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
// Unfork the repository
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"repo_name": repo,
"action": "convert_fork",
},
@@ -409,7 +439,6 @@ func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, b
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editRequestForm := map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"last_commit": htmlDoc.GetInputValueByName("last_commit"),
"tree_path": filePath,
"content": "new content in fork",
@@ -32,7 +32,6 @@ import (
func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) {
url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch)
req := NewRequestWithValues(t, "POST", url, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"commit_choice": "direct",
"tree_path": treePath,
"content": content,
@@ -86,7 +85,6 @@ func TestEmptyRepoAddFile(t *testing.T) {
doc := NewHTMLParser(t, resp.Body).Find(`input[name="commit_choice"]`)
assert.Empty(t, doc.AttrOr("checked", "_no_"))
req = NewRequestWithValues(t, "POST", "/user30/empty/_new/"+setting.Repository.DefaultBranch, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"commit_choice": "direct",
"tree_path": "test-file.md",
"content": "newly-added-test-file",
@@ -142,7 +140,6 @@ func TestEmptyRepoUploadFile(t *testing.T) {
body := &bytes.Buffer{}
mpForm := multipart.NewWriter(body)
_ = mpForm.WriteField("_csrf", GetUserCSRFToken(t, session))
file, _ := mpForm.CreateFormFile("file", "uploaded-file.txt")
_, _ = io.Copy(file, strings.NewReader("newly-uploaded-test-file"))
_ = mpForm.Close()
@@ -154,7 +151,6 @@ func TestEmptyRepoUploadFile(t *testing.T) {
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &respMap))
req = NewRequestWithValues(t, "POST", "/user30/empty/_upload/"+setting.Repository.DefaultBranch, map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"commit_choice": "direct",
"files": respMap["uuid"],
"tree_path": "",
@@ -194,14 +194,15 @@ func lfsCommitAndPushTest(t *testing.T, dstPath string, sizes ...int) (pushedFil
t.Run("CommitAndPushLFS", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
prefix := "lfs-data-file-"
err := gitcmd.NewCommand("lfs").AddArguments("install").Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
err := gitcmd.NewCommand("lfs").AddArguments("install").WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
_, _, err = gitcmd.NewCommand("lfs").AddArguments("track").AddDynamicArguments(prefix+"*").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err = gitcmd.NewCommand("lfs").AddArguments("track").AddDynamicArguments(prefix + "*").
WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err)
err = git.AddChanges(t.Context(), dstPath, false, ".gitattributes")
err = gitAddChangesDeprecated(t.Context(), dstPath, false, ".gitattributes")
assert.NoError(t, err)
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: "user2@example.com",
Name: "User Two",
@@ -312,20 +313,20 @@ func lockTest(t *testing.T, repoPath string) {
}
func lockFileTest(t *testing.T, filename, repoPath string) {
_, _, err := gitcmd.NewCommand("lfs").AddArguments("locks").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repoPath})
_, _, err := gitcmd.NewCommand("lfs").AddArguments("locks").WithDir(repoPath).RunStdString(t.Context())
assert.NoError(t, err)
_, _, err = gitcmd.NewCommand("lfs").AddArguments("lock").AddDynamicArguments(filename).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repoPath})
_, _, err = gitcmd.NewCommand("lfs").AddArguments("lock").AddDynamicArguments(filename).WithDir(repoPath).RunStdString(t.Context())
assert.NoError(t, err)
_, _, err = gitcmd.NewCommand("lfs").AddArguments("locks").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repoPath})
_, _, err = gitcmd.NewCommand("lfs").AddArguments("locks").WithDir(repoPath).RunStdString(t.Context())
assert.NoError(t, err)
_, _, err = gitcmd.NewCommand("lfs").AddArguments("unlock").AddDynamicArguments(filename).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repoPath})
_, _, err = gitcmd.NewCommand("lfs").AddArguments("unlock").AddDynamicArguments(filename).WithDir(repoPath).RunStdString(t.Context())
assert.NoError(t, err)
}
func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string {
name, err := generateCommitWithNewData(t.Context(), size, repoPath, "user2@example.com", "User Two", prefix)
assert.NoError(t, err)
_, _, err = gitcmd.NewCommand("push", "origin", "master").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repoPath}) // Push
_, _, err = gitcmd.NewCommand("push", "origin", "master").WithDir(repoPath).RunStdString(t.Context())
assert.NoError(t, err)
return name
}
@@ -346,11 +347,11 @@ func generateCommitWithNewData(ctx context.Context, size int, repoPath, email, f
_ = tmpFile.Close()
// Commit
err = git.AddChanges(ctx, repoPath, false, filepath.Base(tmpFile.Name()))
err = gitAddChangesDeprecated(ctx, repoPath, false, filepath.Base(tmpFile.Name()))
if err != nil {
return "", err
}
err = git.CommitChanges(ctx, repoPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(ctx, repoPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: email,
Name: fullName,
@@ -425,7 +426,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
// Try to force push without force push permissions, which should fail
t.Run("ForcePushWithoutForcePermissions", func(t *testing.T) {
t.Run("CreateDivergentHistory", func(t *testing.T) {
gitcmd.NewCommand("reset", "--hard", "HEAD~1").Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
gitcmd.NewCommand("reset", "--hard", "HEAD~1").WithDir(dstPath).Run(t.Context())
_, err := generateCommitWithNewData(t.Context(), testFileSizeSmall, dstPath, "user2@example.com", "User Two", "branch-data-file-new")
assert.NoError(t, err)
})
@@ -506,10 +507,7 @@ type doProtectBranchOptions struct {
func doProtectBranchExt(ctx APITestContext, ruleName string, opts doProtectBranchOptions) func(t *testing.T) {
// We are going to just use the owner to set the protection.
return func(t *testing.T) {
csrf := GetUserCSRFToken(t, ctx.Session)
formData := map[string]string{
"_csrf": csrf,
"rule_name": ruleName,
"unprotected_file_patterns": opts.UnprotectedFilePatterns,
"protected_file_patterns": opts.ProtectedFilePatterns,
@@ -693,11 +691,7 @@ func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) {
func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testing.T) {
return func(t *testing.T) {
csrf := GetUserCSRFToken(t, ctx.Session)
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)), map[string]string{
"_csrf": csrf,
})
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)))
ctx.Session.MakeRequest(t, req, http.StatusOK)
}
}
@@ -843,10 +837,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0o666)
require.NoError(t, err)
err = git.AddChanges(t.Context(), dstPath, true)
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
assert.NoError(t, err)
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: "user2@example.com",
Name: "user2",
@@ -865,7 +859,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
})
t.Run("Push", func(t *testing.T) {
err := gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic="+headBranch).Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
err := gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master", "-o").
AddDynamicArguments("topic=" + headBranch).
WithDir(dstPath).
Run(t.Context())
require.NoError(t, err)
unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+1)
@@ -883,7 +880,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
assert.Contains(t, "Testing commit 1", prMsg.Body)
assert.Equal(t, commit, prMsg.Head.Sha)
_, _, err = gitcmd.NewCommand("push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/"+headBranch).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err = gitcmd.NewCommand("push", "origin").
AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).
WithDir(dstPath).
RunStdString(t.Context())
require.NoError(t, err)
unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
@@ -909,10 +909,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0o666)
require.NoError(t, err)
err = git.AddChanges(t.Context(), dstPath, true)
err = gitAddChangesDeprecated(t.Context(), dstPath, true)
assert.NoError(t, err)
err = git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
err = gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &git.Signature{
Email: "user2@example.com",
Name: "user2",
@@ -931,7 +931,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
})
t.Run("Push2", func(t *testing.T) {
err := gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic="+headBranch).Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
err := gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master", "-o").
AddDynamicArguments("topic=" + headBranch).
WithDir(dstPath).
Run(t.Context())
require.NoError(t, err)
unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
@@ -941,7 +944,10 @@ func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, headBranch string
assert.False(t, prMsg.HasMerged)
assert.Equal(t, commit, prMsg.Head.Sha)
_, _, err = gitcmd.NewCommand("push", "origin").AddDynamicArguments("HEAD:refs/for/master/test/"+headBranch).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err = gitcmd.NewCommand("push", "origin").
AddDynamicArguments("HEAD:refs/for/master/test/" + headBranch).
WithDir(dstPath).
RunStdString(t.Context())
require.NoError(t, err)
unittest.AssertCount(t, &issues_model.PullRequest{}, pullNum+2)
@@ -58,6 +58,52 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL {
return &u2
}
// gitAddChangesDeprecated marks local changes to be ready for commit.
// Deprecated: use "git fast-import" instead for better performance and more control over the commit creation.
func gitAddChangesDeprecated(ctx context.Context, repoPath string, all bool, files ...string) error {
cmd := gitcmd.NewCommand().AddArguments("add")
if all {
cmd.AddArguments("--all")
}
cmd.AddDashesAndList(files...)
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
return err
}
// CommitChangesOptions the options when a commit created
type gitCommitChangesOptions struct {
Committer *git.Signature
Author *git.Signature
Message string
}
// gitCommitChangesDeprecated commits local changes with given committer, author and message.
// If author is nil, it will be the same as committer.
// Deprecated: use "git fast-import" instead for better performance and more control over the commit creation.
func gitCommitChangesDeprecated(ctx context.Context, repoPath string, opts gitCommitChangesOptions) error {
cmd := gitcmd.NewCommand()
if opts.Committer != nil {
cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name)
cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email)
}
cmd.AddArguments("commit")
if opts.Author == nil {
opts.Author = opts.Committer
}
if opts.Author != nil {
cmd.AddOptionFormat("--author='%s <%s>'", opts.Author.Name, opts.Author.Email)
}
cmd.AddOptionFormat("--message=%s", opts.Message)
_, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
// No stderr but exit status 1 means nothing to commit.
if gitcmd.IsErrorExitCode(err, 1) {
return nil
}
return err
}
func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) {
defer tests.PrepareTestEnv(t, 1)()
s := http.Server{
@@ -123,16 +169,18 @@ func doGitInitTestRepository(dstPath string) func(*testing.T) {
// Init repository in dstPath
assert.NoError(t, git.InitRepository(t.Context(), dstPath, false, git.Sha1ObjectFormat.Name()))
// forcibly set default branch to master
_, _, err := gitcmd.NewCommand("symbolic-ref", "HEAD", git.BranchPrefix+"master").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err := gitcmd.NewCommand("symbolic-ref", "HEAD", git.BranchPrefix+"master").
WithDir(dstPath).
RunStdString(t.Context())
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(dstPath, "README.md"), []byte("# Testing Repository\n\nOriginally created in: "+dstPath), 0o644))
assert.NoError(t, git.AddChanges(t.Context(), dstPath, true))
assert.NoError(t, gitAddChangesDeprecated(t.Context(), dstPath, true))
signature := git.Signature{
Email: "test@example.com",
Name: "test",
When: time.Now(),
}
assert.NoError(t, git.CommitChanges(t.Context(), dstPath, git.CommitChangesOptions{
assert.NoError(t, gitCommitChangesDeprecated(t.Context(), dstPath, gitCommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: "Initial Commit",
@@ -142,21 +190,27 @@ func doGitInitTestRepository(dstPath string) func(*testing.T) {
func doGitAddRemote(dstPath, remoteName string, u *url.URL) func(*testing.T) {
return func(t *testing.T) {
_, _, err := gitcmd.NewCommand("remote", "add").AddDynamicArguments(remoteName, u.String()).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err := gitcmd.NewCommand("remote", "add").AddDynamicArguments(remoteName, u.String()).
WithDir(dstPath).
RunStdString(t.Context())
assert.NoError(t, err)
}
}
func doGitPushTestRepository(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
_, _, err := gitcmd.NewCommand("push", "-u").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err := gitcmd.NewCommand("push", "-u").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).
WithDir(dstPath).
RunStdString(t.Context())
assert.NoError(t, err)
}
}
func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
_, _, err := gitcmd.NewCommand("push").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err := gitcmd.NewCommand("push").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).
WithDir(dstPath).
RunStdString(t.Context())
assert.Error(t, err)
}
}
@@ -173,12 +227,12 @@ func doGitCheckoutWriteFileCommit(opts localGitAddCommitOptions) func(*testing.T
doGitCheckoutBranch(opts.LocalRepoPath, opts.CheckoutBranch)(t)
localFilePath := filepath.Join(opts.LocalRepoPath, opts.TreeFilePath)
require.NoError(t, os.WriteFile(localFilePath, []byte(opts.TreeFileContent), 0o644))
require.NoError(t, git.AddChanges(t.Context(), opts.LocalRepoPath, true))
require.NoError(t, gitAddChangesDeprecated(t.Context(), opts.LocalRepoPath, true))
signature := git.Signature{
Email: "test@test.test",
Name: "test",
}
require.NoError(t, git.CommitChanges(t.Context(), opts.LocalRepoPath, git.CommitChangesOptions{
require.NoError(t, gitCommitChangesDeprecated(t.Context(), opts.LocalRepoPath, gitCommitChangesOptions{
Committer: &signature,
Author: &signature,
Message: fmt.Sprintf("update %s @ %s", opts.TreeFilePath, opts.CheckoutBranch),
@@ -188,28 +242,48 @@ func doGitCheckoutWriteFileCommit(opts localGitAddCommitOptions) func(*testing.T
func doGitCreateBranch(dstPath, branch string) func(*testing.T) {
return func(t *testing.T) {
_, _, err := gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(branch).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err := gitcmd.NewCommand("checkout", "-b").AddDynamicArguments(branch).
WithDir(dstPath).RunStdString(t.Context())
assert.NoError(t, err)
}
}
func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
_, _, err := gitcmd.NewCommand().AddArguments("checkout").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err := gitcmd.NewCommand().AddArguments("checkout").
AddArguments(gitcmd.ToTrustedCmdArgs(args)...).
WithDir(dstPath).
RunStdString(t.Context())
assert.NoError(t, err)
}
}
func doGitMerge(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
_, _, err := gitcmd.NewCommand("merge").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err := gitcmd.NewCommand("merge").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).
WithDir(dstPath).
RunStdString(t.Context())
assert.NoError(t, err)
}
}
func doGitPull(dstPath string, args ...string) func(*testing.T) {
return func(t *testing.T) {
_, _, err := gitcmd.NewCommand().AddArguments("pull").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, _, err := gitcmd.NewCommand().AddArguments("pull").AddArguments(gitcmd.ToTrustedCmdArgs(args)...).
WithDir(dstPath).
RunStdString(t.Context())
assert.NoError(t, err)
}
}
// doGitRemoteArchive runs a git archive command requesting an archive from remote
// and verifies that the command did not error out and returned only normal output
func doGitRemoteArchive(remote string, args ...string) func(*testing.T) {
return func(t *testing.T) {
stdout, stderr, err := gitcmd.NewCommand("archive").AddOptionValues("--remote", remote).AddArguments(gitcmd.ToTrustedCmdArgs(args)...).
RunStdString(t.Context())
require.NoError(t, err)
assert.Empty(t, stderr)
assert.NotEmpty(t, stdout)
}
}
@@ -48,7 +48,9 @@ func TestGitLFSSSH(t *testing.T) {
setting.LFS.AllowPureSSH = true
require.NoError(t, cfg.Save())
_, _, cmdErr := gitcmd.NewCommand("config", "lfs.sshtransfer", "always").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: localRepoForUpload})
_, _, cmdErr := gitcmd.NewCommand("config", "lfs.sshtransfer", "always").
WithDir(localRepoForUpload).
RunStdString(t.Context())
assert.NoError(t, cmdErr)
pushedFiles := lfsCommitAndPushTest(t, localRepoForUpload, 10)
@@ -104,7 +104,8 @@ func TestAgitPullPush(t *testing.T) {
err = gitcmd.NewCommand("push", "origin",
"-o", "title=test-title", "-o", "description=test-description",
"HEAD:refs/for/master/test-agit-push",
).Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
).WithDir(dstPath).
Run(t.Context())
assert.NoError(t, err)
// check pull request exist
@@ -118,20 +119,26 @@ func TestAgitPullPush(t *testing.T) {
assert.NoError(t, err)
// push 2
err = gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-push").Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
err = gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-push").
WithDir(dstPath).
Run(t.Context())
assert.NoError(t, err)
// reset to first commit
err = gitcmd.NewCommand("reset", "--hard", "HEAD~1").Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
err = gitcmd.NewCommand("reset", "--hard", "HEAD~1").WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
// test force push without confirm
_, stderr, err := gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
_, stderr, err := gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-push").
WithDir(dstPath).
RunStdString(t.Context())
assert.Error(t, err)
assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)")
// test force push with confirm
err = gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
err = gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").
WithDir(dstPath).
Run(t.Context())
assert.NoError(t, err)
})
}
@@ -160,7 +167,7 @@ func TestAgitReviewStaleness(t *testing.T) {
err = gitcmd.NewCommand("push", "origin",
"-o", "title=Test agit Review Staleness", "-o", "description=Testing review staleness",
"HEAD:refs/for/master/test-agit-review",
).Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
).WithDir(dstPath).Run(t.Context())
assert.NoError(t, err)
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{
@@ -200,7 +207,9 @@ func TestAgitReviewStaleness(t *testing.T) {
_, err = generateCommitWithNewData(t.Context(), testFileSizeSmall, dstPath, "user2@example.com", "User Two", "updated-")
assert.NoError(t, err)
err = gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-review").Run(t.Context(), &gitcmd.RunOpts{Dir: dstPath})
err = gitcmd.NewCommand("push", "origin", "HEAD:refs/for/master/test-agit-review").
WithDir(dstPath).
Run(t.Context())
assert.NoError(t, err)
// Reload PR to get updated commit ID
@@ -210,9 +210,7 @@ func TestPushPullRefs(t *testing.T) {
doGitClone(dstPath, u)(t)
cmd := gitcmd.NewCommand("push", "--delete", "origin", "refs/pull/2/head")
stdout, stderr, err := cmd.RunStdString(t.Context(), &gitcmd.RunOpts{
Dir: dstPath,
})
stdout, stderr, err := cmd.WithDir(dstPath).RunStdString(t.Context())
assert.Error(t, err)
assert.Empty(t, stdout)
assert.NotContains(t, stderr, "[deleted]", "stderr: %s", stderr)
@@ -9,6 +9,9 @@ import (
"net/url"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
@@ -20,7 +23,9 @@ import (
func TestGitSmartHTTP(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
testGitSmartHTTP(t, u)
testGitSmartHTTPTokenScopes(t)
testRenamedRepoRedirect(t)
testGitArchiveRemote(t, u)
})
}
@@ -79,6 +84,42 @@ func testGitSmartHTTP(t *testing.T, u *url.URL) {
}
}
func testGitSmartHTTPTokenScopes(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2, OwnerName: "user2", Name: "repo2"})
require.True(t, repo.IsPrivate)
session := loginUser(t, "user2")
badToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadNotification)
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadRepository)
t.Run("upload-pack requires read repository scope", func(t *testing.T) {
path := "/user2/repo2/info/refs?service=git-upload-pack"
MakeRequest(t, NewRequest(t, "GET", path).AddBasicAuth(badToken, "x-oauth-basic"), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(badToken), http.StatusForbidden)
resp := MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(readToken), http.StatusOK)
assert.Contains(t, resp.Body.String(), "refs/heads/master")
})
t.Run("receive-pack requires write repository scope", func(t *testing.T) {
path := "/user2/repo2/info/refs?service=git-receive-pack"
MakeRequest(t, NewRequest(t, "GET", path).AddBasicAuth(readToken, "x-oauth-basic"), http.StatusForbidden)
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(readToken), http.StatusForbidden)
resp := MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(writeToken), http.StatusOK)
assert.Contains(t, resp.Body.String(), "refs/heads/master")
})
t.Run("public-only scope rejects private repo", func(t *testing.T) {
path := "/user2/repo2/info/refs?service=git-upload-pack"
MakeRequest(t, NewRequest(t, "GET", path).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
})
}
func testRenamedRepoRedirect(t *testing.T) {
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
@@ -96,3 +137,10 @@ func testRenamedRepoRedirect(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "65f1bf27bc3bf70f64657658635e66094edbcb4d\trefs/tags/v1.1")
}
func testGitArchiveRemote(t *testing.T, u *url.URL) {
u = u.JoinPath("user27/repo49.git")
t.Run("Fetch HEAD archive", doGitRemoteArchive(u.String(), "HEAD"))
t.Run("Fetch HEAD archive subpath", doGitRemoteArchive(u.String(), "HEAD", "test"))
t.Run("list compression options", doGitRemoteArchive(u.String(), "--list"))
}
@@ -9,6 +9,7 @@ import (
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
"net/url"
"os"
"testing"
@@ -255,6 +256,52 @@ func testGitSigning(t *testing.T) {
assert.True(t, branch.Commit.Verification.Verified)
}))
})
setting.Repository.Signing.Merges = []string{"commitssigned"}
setting.Repository.Signing.CRUDActions = []string{"always"}
t.Run("UpdateRebaseSigned", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
testCtx := NewAPITestContext(t, username, "update-rebase-signed", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
var repoID int64
t.Run("GetRepository", doAPIGetRepository(testCtx, func(t *testing.T, repo api.Repository) {
repoID = repo.ID
}))
enableRepoAllowUpdateWithRebase(t, repoID, true)
t.Run("CreateFeatureCommit", crudActionCreateFile(
t, testCtx, user, "master", "feature", "signed-feature.txt"))
pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "feature")(t)
require.NoError(t, err)
content := base64.StdEncoding.EncodeToString([]byte("update base"))
t.Run("UpdateBase", doAPICreateFile(testCtx, "signed-base.txt", &api.CreateFileOptions{
FileOptions: api.FileOptions{
BranchName: "master",
Message: "update base",
Author: api.Identity{
Name: user.FullName,
Email: user.Email,
},
Committer: api.Identity{
Name: user.FullName,
Email: user.Email,
},
},
ContentBase64: content,
}))
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase", testCtx.Username, testCtx.Reponame, pr.Index).
AddTokenAuth(testCtx.Token)
testCtx.Session.MakeRequest(t, req, http.StatusOK)
t.Run("CheckFeatureBranchSigned", doAPIGetBranch(testCtx, "feature", func(t *testing.T, branch api.Branch) {
require.NotNil(t, branch.Commit)
require.NotNil(t, branch.Commit.Verification)
assert.True(t, branch.Commit.Verification.Verified)
}))
})
})
}
@@ -0,0 +1,59 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"time"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestHeatmapEndpoints(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Mock time so fixture actions fall within the heatmap's time window
timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
defer timeutil.MockUnset()
session := loginUser(t, "user2")
t.Run("UserProfile", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/user2/-/heatmap")
resp := session.MakeRequest(t, req, http.StatusOK)
var result map[string]any
DecodeJSON(t, resp, &result)
assert.Contains(t, result, "heatmapData")
assert.Contains(t, result, "totalContributions")
assert.Positive(t, result["totalContributions"])
})
t.Run("OrgDashboard", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/org/org3/dashboard/-/heatmap")
resp := session.MakeRequest(t, req, http.StatusOK)
var result map[string]any
DecodeJSON(t, resp, &result)
assert.Contains(t, result, "heatmapData")
assert.Contains(t, result, "totalContributions")
})
t.Run("OrgTeamDashboard", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/org/org3/dashboard/-/heatmap/team1")
resp := session.MakeRequest(t, req, http.StatusOK)
var result map[string]any
DecodeJSON(t, resp, &result)
assert.Contains(t, result, "heatmapData")
assert.Contains(t, result, "totalContributions")
})
}
@@ -37,11 +37,6 @@ func (doc *HTMLDoc) Find(selector string) *goquery.Selection {
return doc.doc.Find(selector)
}
// GetCSRF for getting CSRF token value from input
func (doc *HTMLDoc) GetCSRF() string {
return doc.GetInputValueByName("_csrf")
}
// AssertHTMLElement check if the element by selector exists or does not exist depending on checkExists
func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) {
sel := doc.doc.Find(selector)
@@ -66,7 +66,14 @@ func TestIncomingEmail(t *testing.T) {
assert.NoError(t, err)
assert.NotEmpty(t, token)
ht, u, p, err := token_service.ExtractToken(t.Context(), token)
ht, u, p, err := token_service.DecodeToken(t.Context(), token)
assert.NoError(t, err)
assert.Equal(t, token_service.ReplyHandlerType, ht)
assert.Equal(t, user.ID, u.ID)
assert.Equal(t, payload, p)
// MTAs may lowercase the local-part of the reply-to address (RFC 5321 §2.4).
ht, u, p, err = token_service.DecodeToken(t.Context(), strings.ToLower(token))
assert.NoError(t, err)
assert.Equal(t, token_service.ReplyHandlerType, ht)
assert.Equal(t, user.ID, u.ID)
@@ -189,7 +196,7 @@ func TestIncomingEmail(t *testing.T) {
assert.NoError(t, err)
msg := sender_service.NewMessageFrom(
strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmail.TokenPlaceholder, token, 1),
strings.Replace(setting.IncomingEmail.ReplyToAddress, setting.IncomingEmailTokenPlaceholder, token, 1),
"",
user.Email,
"",
@@ -1,12 +1,12 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//nolint:forbidigo // use of print functions is allowed in tests
package integration
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"hash"
"hash/fnv"
@@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/testlogger"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware"
@@ -79,14 +80,14 @@ func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder {
}
}
func TestMain(m *testing.M) {
func testMain(m *testing.M) int {
defer log.GetManager().Close()
managerCtx, cancel := context.WithCancel(context.Background())
graceful.InitManager(managerCtx)
defer cancel()
tests.InitTest(true)
tests.InitTest()
testWebRoutes = routers.NormalRoutes()
err := unittest.InitFixtures(
@@ -95,8 +96,7 @@ func TestMain(m *testing.M) {
},
)
if err != nil {
fmt.Printf("Error initializing test database: %v\n", err)
os.Exit(1)
testlogger.Panicf("InitFixtures: %v", err)
}
// FIXME: the console logger is deleted by mistake, so if there is any `log.Fatal`, developers won't see any error message.
@@ -104,15 +104,16 @@ func TestMain(m *testing.M) {
exitCode := m.Run()
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
fmt.Printf("util.RemoveAll: %v\n", err)
os.Exit(1)
log.Error("Failed to remove indexer path: %v", err)
}
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
fmt.Printf("Unable to remove repo indexer: %v\n", err)
os.Exit(1)
log.Error("Failed to remove indexer path: %v", err)
}
return exitCode
}
os.Exit(exitCode)
func TestMain(m *testing.M) {
os.Exit(testMain(m))
}
type TestSession struct {
@@ -225,16 +226,11 @@ func loginUser(t testing.TB, userName string) *TestSession {
func loginUserWithPassword(t testing.TB, userName, password string) *TestSession {
t.Helper()
req := NewRequest(t, "GET", "/user/login")
resp := MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
"_csrf": doc.GetCSRF(),
req := NewRequestWithValues(t, "POST", "/user/login", map[string]string{
"user_name": userName,
"password": password,
})
resp = MakeRequest(t, req, http.StatusSeeOther)
resp := MakeRequest(t, req, http.StatusSeeOther)
ch := http.Header{}
ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
@@ -250,14 +246,13 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
}
// token has to be unique this counter take care of
var tokenCounter int64
var tokenCounter atomic.Int64
// getTokenForLoggedInUser returns a token for a logged-in user.
func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.AccessTokenScope) string {
t.Helper()
urlValues := url.Values{}
urlValues.Add("_csrf", GetUserCSRFToken(t, session))
urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)))
urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", tokenCounter.Add(1)))
for _, scope := range scopes {
urlValues.Add("scope-dummy", string(scope)) // it only needs to start with "scope-" to be accepted
}
@@ -332,9 +327,10 @@ func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *Re
urlStr = "/" + urlStr
}
req, err := http.NewRequest(method, urlStr, body)
assert.NoError(t, err)
req.RequestURI = urlStr
require.NoError(t, err)
if req.URL.User != nil {
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(req.URL.User.String())))
}
return &RequestWrapper{req}
}
@@ -347,6 +343,10 @@ func MakeRequest(t testing.TB, rw *RequestWrapper, expectedStatus int) *httptest
if req.RemoteAddr == "" {
req.RemoteAddr = "test-mock:12345"
}
// Ensure unknown contentLength is seen as -1
if req.Body != nil && req.ContentLength == 0 {
req.ContentLength = -1
}
testWebRoutes.ServeHTTP(recorder, req)
if expectedStatus != NoExpectedStatus {
if expectedStatus != recorder.Code {
@@ -410,11 +410,13 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
}
}
func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) {
func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) {
t.Helper()
decoder := json.NewDecoder(resp.Body)
require.NoError(t, decoder.Decode(v))
// FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names
decoder := json.NewDecoderCaseInsensitive(resp.Body)
require.NoError(t, decoder.Decode(&v))
return v
}
func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) {
@@ -435,20 +437,3 @@ func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile
assert.Empty(t, result.Errors())
assert.True(t, result.Valid())
}
// GetUserCSRFToken returns CSRF token for current user
func GetUserCSRFToken(t testing.TB, session *TestSession) string {
t.Helper()
cookie := session.GetSiteCookie("_csrf")
require.NotEmpty(t, cookie)
return cookie
}
// GetUserCSRFToken returns CSRF token for anonymous user (not logged in)
func GetAnonymousCSRFToken(t testing.TB, session *TestSession) string {
t.Helper()
resp := session.MakeRequest(t, NewRequest(t, "GET", "/user/login"), http.StatusOK)
csrfToken := NewHTMLParser(t, resp.Body).GetCSRF()
require.NotEmpty(t, csrfToken)
return csrfToken
}

Some files were not shown because too many files have changed in this diff Show More