初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# Integration tests
|
||||
|
||||
Integration tests can be run with command `make test-integration`.
|
||||
Environment variable `GITEA_TEST_DATABASE` can be used to specify the database type for testing.
|
||||
|
||||
If you encounter some errors like mismatched database version, SSH push errors, etc.,
|
||||
you can try to perform a clean build by: `make clean build`.
|
||||
|
||||
## Run sqlite integration tests
|
||||
|
||||
Start tests directly (empty `GITEA_TEST_DATABASE` defaults to sqlite):
|
||||
|
||||
```
|
||||
make test-integration
|
||||
```
|
||||
|
||||
## Run MySQL integration tests
|
||||
|
||||
Set up a MySQL database inside docker:
|
||||
|
||||
```
|
||||
docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest #(just ctrl-c to stop db and clean the container)
|
||||
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --rm --name elasticsearch elasticsearch:7.6.0 #(in a second terminal, just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
|
||||
Start tests based on the database container:
|
||||
|
||||
```
|
||||
GITEA_TEST_DATABASE=mysql TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-integration
|
||||
```
|
||||
|
||||
## Run pgsql integration tests
|
||||
|
||||
Set up a pgsql database inside docker:
|
||||
|
||||
```
|
||||
docker run -e "POSTGRES_DB=test" -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=postgres" -p 5432:5432 --rm --name pgsql postgres:latest #(just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
|
||||
Set up minio inside docker:
|
||||
|
||||
```
|
||||
docker run --rm -p 9000:9000 -e MINIO_ROOT_USER=123456 -e MINIO_ROOT_PASSWORD=12345678 --name minio bitnamilegacy/minio:2023.8.31
|
||||
```
|
||||
|
||||
Start tests based on the database container:
|
||||
|
||||
```
|
||||
GITEA_TEST_DATABASE=pgsql TEST_MINIO_ENDPOINT=localhost:9000 TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=postgres TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-integration
|
||||
```
|
||||
|
||||
## Run mssql integration tests
|
||||
|
||||
Set up a mssql database inside docker:
|
||||
|
||||
```
|
||||
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
|
||||
Start tests based on the database container:
|
||||
|
||||
```
|
||||
GITEA_TEST_DATABASE=mssql TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-integration
|
||||
```
|
||||
|
||||
## Running individual tests
|
||||
|
||||
Example command to run GPG test:
|
||||
|
||||
```
|
||||
GITEA_TEST_DATABASE=... make test-integration#GPG
|
||||
```
|
||||
|
||||
## Run Gitea Actions tests via local act_runner
|
||||
|
||||
### Run all jobs
|
||||
|
||||
```
|
||||
act_runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest
|
||||
```
|
||||
|
||||
Warning: This file defines many jobs, so it will be resource-intensive and therefore not recommended.
|
||||
|
||||
### Run single job
|
||||
|
||||
```SHELL
|
||||
act_runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest -j <job_name>
|
||||
```
|
||||
|
||||
You can list all job names via:
|
||||
|
||||
```SHELL
|
||||
act_runner exec -W ./.github/workflows/pull-db-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest -l
|
||||
```
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
{"id":1,"name":"test_repo","full_name":"gitea/test_repo","owner":{"id":1,"login":"gitea","username":"gitea"},"private":false,"html_url":"https://gitea.com/gitea/test_repo","clone_url":"https://gitea.com/gitea/test_repo.git","default_branch":"master","description":"test repo for migration"}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[{"id":1553,"body":"TESTSET for gitea2gitea migration\n","user":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z"},{"id":1554,"body":"Oh!\n","user":{"id":-1,"login":"Ghost","full_name":"","email":"","avatar_url":"","username":"Ghost"},"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z"}]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[{"user":{"id":-1,"login":"Ghost","full_name":"","email":"","avatar_url":"","username":"Ghost"},"content":"gitea"},{"user":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"content":"laugh"}]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[{"number":1,"title":"issue1","body":"","state":"open","user":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"labels":[],"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z","is_locked":false},{"number":2,"title":"issue2","body":"","state":"open","user":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"labels":[],"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z","is_locked":false},{"number":3,"title":"issue3","body":"","state":"open","user":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"labels":[],"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z","is_locked":false},{"number":4,"title":"what is this repo about?","body":"","state":"closed","user":{"id":-1,"login":"Ghost","full_name":"","email":"","avatar_url":"","username":"Ghost"},"labels":[{"id":2,"name":"Question","color":"#d876e3"}],"milestone":{"id":1,"title":"V1"},"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z","closed_at":"2020-01-01T00:00:00Z","is_locked":true},{"number":5,"title":"issue5","body":"","state":"open","user":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"labels":[],"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z","is_locked":false},{"number":6,"title":"issue6","body":"","state":"open","user":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"labels":[],"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z","is_locked":false},{"number":7,"title":"issue7","body":"","state":"open","user":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"labels":[],"created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z","is_locked":false}]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[{"id":1,"name":"Bug","color":"#ee0701","description":""},{"id":2,"name":"Question","color":"#d876e3","description":""}]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[{"id":1,"title":"V1","description":"first milestone","state":"closed","created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z","closed_at":"2020-01-01T00:00:00Z"},{"id":2,"title":"V2 Finalize","description":"second milestone","state":"open","created_at":"2020-01-01T00:00:00Z","updated_at":"2020-01-01T00:00:00Z"}]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[]
|
||||
+3
File diff suppressed because one or more lines are too long
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
[{"id":1,"tag_name":"V1","target_commitish":"master","name":"First Release","body":"as title","draft":false,"prerelease":false,"created_at":"2020-01-01T00:00:00Z","published_at":"2020-01-01T00:00:00Z","author":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"assets":[]},{"id":2,"tag_name":"v2-rc1","target_commitish":"master","name":"Second Release","body":"this repo has:\n- issues\n- pulls","draft":false,"prerelease":true,"created_at":"2020-01-01T00:00:00Z","published_at":"2020-01-01T00:00:00Z","author":{"id":689,"login":"6543","full_name":"","email":"6543@obermui.de","avatar_url":"","username":"6543"},"assets":[]}]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
{"topics":[]}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
{"max_response_items":50,"default_paging_num":30,"default_git_trees_per_page":40,"default_max_blob_size":10485760}
|
||||
@@ -0,0 +1,3 @@
|
||||
Content-Type: application/json
|
||||
|
||||
{"version":"1.22.0"}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/plain
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/plain
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/plain
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/plain
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/plain
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
Content-Type: text/plain
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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 "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/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)
|
||||
apiForkRepo := DecodeJSON(t, resp, &api.Repository{})
|
||||
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
@@ -0,0 +1,178 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/routers/web/repo/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestActionsDeleteRun(t *testing.T) {
|
||||
now := time.Now()
|
||||
testCase := struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcomes map[string]*mockTaskOutcome
|
||||
expectedStatuses map[string]string
|
||||
}{
|
||||
treePath: ".gitea/workflows/test1.yml",
|
||||
fileContent: `name: test1
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .gitea/workflows/test1.yml
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job2
|
||||
job3:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job3
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job1",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
"job2": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job2",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
"job3": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job3",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusSuccess.String(),
|
||||
"job2": actions_model.StatusSuccess.String(),
|
||||
"job3": actions_model.StatusSuccess.String(),
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-delete-run-test", false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts)
|
||||
|
||||
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()
|
||||
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.GetLatestAttemptJobsByRepoAndRunID(t.Context(), apiRepo.ID, runID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
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/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
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++ {
|
||||
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/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWorkflowWithInputsContext(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)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-inputs-context", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(httpContext)(t)
|
||||
|
||||
wRunner := newMockRunner()
|
||||
wRunner.registerAsRepoRunner(t, user2.Name, repo.Name, "windows-runner", []string{"windows-runner"}, false)
|
||||
lRunner := newMockRunner()
|
||||
lRunner.registerAsRepoRunner(t, user2.Name, repo.Name, "linux-runner", []string{"linux-runner"}, false)
|
||||
|
||||
wf1TreePath := ".gitea/workflows/test-inputs-context.yml"
|
||||
wf1FileContent := `name: Test Inputs Context
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
os:
|
||||
description: 'OS'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- linux
|
||||
- windows
|
||||
|
||||
run-name: Build APP on ${{ inputs.os }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ inputs.os }}-runner
|
||||
steps:
|
||||
- run: echo 'Start building APP'
|
||||
`
|
||||
|
||||
opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent)
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, wf1TreePath, opts1)
|
||||
|
||||
// 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{
|
||||
"ref": "refs/heads/main",
|
||||
"os": "windows",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// linux-runner cannot fetch the task
|
||||
lRunner.fetchNoTask(t)
|
||||
|
||||
task := wRunner.fetchTask(t)
|
||||
_, _, run := getTaskAndJobAndRunByTaskID(t, task.Id)
|
||||
assert.Equal(t, "Build APP on windows", run.Title)
|
||||
})
|
||||
}
|
||||
|
||||
func getTaskAndJobAndRunByTaskID(t *testing.T, taskID int64) (*actions_model.ActionTask, *actions_model.ActionRunJob, *actions_model.ActionRun) {
|
||||
actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: taskID})
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID})
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID})
|
||||
return actionTask, actionRunJob, actionRun
|
||||
}
|
||||
@@ -0,0 +1,916 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/timeutil"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestJobWithNeeds(t *testing.T) {
|
||||
testCases := []struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcomes map[string]*mockTaskOutcome
|
||||
expectedStatuses map[string]string
|
||||
}{
|
||||
{
|
||||
treePath: ".gitea/workflows/job-with-needs.yml",
|
||||
fileContent: `name: job-with-needs
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/job-with-needs.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo job2
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
"job2": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusSuccess.String(),
|
||||
"job2": actions_model.StatusSuccess.String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/job-with-needs-fail.yml",
|
||||
fileContent: `name: job-with-needs-fail
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/job-with-needs-fail.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo job2
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_FAILURE,
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusFailure.String(),
|
||||
"job2": actions_model.StatusSkipped.String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/job-with-needs-fail-if.yml",
|
||||
fileContent: `name: job-with-needs-fail-if
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/job-with-needs-fail-if.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo job2
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_FAILURE,
|
||||
},
|
||||
"job2": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusFailure.String(),
|
||||
"job2": actions_model.StatusSuccess.String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("test "+tc.treePath, func(t *testing.T) {
|
||||
// create the workflow file
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+tc.treePath, tc.fileContent)
|
||||
fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
|
||||
|
||||
// fetch and execute task
|
||||
for i := 0; i < len(tc.outcomes); i++ {
|
||||
task := runner.fetchTask(t)
|
||||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||
outcome := tc.outcomes[jobName]
|
||||
assert.NotNil(t, outcome)
|
||||
runner.execTask(t, task, outcome)
|
||||
}
|
||||
|
||||
// check result
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
actionTaskRespAfter := DecodeJSON(t, resp, &api.ActionTaskResponse{})
|
||||
for _, apiTask := range actionTaskRespAfter.Entries {
|
||||
if apiTask.HeadSHA != fileResp.Commit.SHA {
|
||||
continue
|
||||
}
|
||||
status := apiTask.Status
|
||||
assert.Equal(t, status, tc.expectedStatuses[apiTask.Name])
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestJobNeedsMatrix(t *testing.T) {
|
||||
testCases := []struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcomes map[string]*mockTaskOutcome
|
||||
expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed
|
||||
}{
|
||||
{
|
||||
treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml",
|
||||
fileContent: `name: jobs-outputs-with-matrix
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/jobs-outputs-with-matrix.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||
strategy:
|
||||
matrix:
|
||||
version: [1, 2, 3]
|
||||
steps:
|
||||
- name: Generate output
|
||||
id: gen_output
|
||||
run: |
|
||||
version="${{ matrix.version }}"
|
||||
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1 (1)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (2)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "2",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (3)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||
"job1": {
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
Outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "2",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml",
|
||||
fileContent: `name: jobs-outputs-with-matrix-failure
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/jobs-outputs-with-matrix-failure.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||
strategy:
|
||||
matrix:
|
||||
version: [1, 2, 3]
|
||||
steps:
|
||||
- name: Generate output
|
||||
id: gen_output
|
||||
run: |
|
||||
version="${{ matrix.version }}"
|
||||
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1 (1)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (2)": {
|
||||
result: runnerv1.Result_RESULT_FAILURE,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (3)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||
"job1": {
|
||||
Result: runnerv1.Result_RESULT_FAILURE,
|
||||
Outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("test "+tc.treePath, func(t *testing.T) {
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+tc.treePath, tc.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
|
||||
|
||||
for i := 0; i < len(tc.outcomes); i++ {
|
||||
task := runner.fetchTask(t)
|
||||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||
outcome := tc.outcomes[jobName]
|
||||
assert.NotNil(t, outcome)
|
||||
runner.execTask(t, task, outcome)
|
||||
}
|
||||
|
||||
task := runner.fetchTask(t)
|
||||
actualTaskNeeds := task.Needs
|
||||
assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds))
|
||||
for jobID, tn := range tc.expectedTaskNeeds {
|
||||
actualNeed := actualTaskNeeds[jobID]
|
||||
assert.Equal(t, tn.Result, actualNeed.Result)
|
||||
assert.Len(t, actualNeed.Outputs, len(tn.Outputs))
|
||||
for outputKey, outputValue := range tn.Outputs {
|
||||
assert.Equal(t, outputValue, actualNeed.Outputs[outputKey])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 := DecodeJSON(t, resp, &api.ActionRunnersResponse{})
|
||||
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})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
// init the workflow
|
||||
wfTreePath := ".gitea/workflows/pull.yml"
|
||||
wfFileContent := `name: Pull Request
|
||||
on: pull_request
|
||||
jobs:
|
||||
wf1-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'test the pull'
|
||||
`
|
||||
opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts)
|
||||
// user2 creates a pull request
|
||||
doAPICreateFile(user2APICtx, "user2-patch.txt", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "user2/patch-1",
|
||||
Message: "create user2-patch.txt",
|
||||
Author: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("user2-fix")),
|
||||
})(t)
|
||||
apiPull, err := doAPICreatePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, "user2/patch-1")(t)
|
||||
assert.NoError(t, err)
|
||||
task := runner.fetchTask(t)
|
||||
gtCtx := task.Context.GetFields()
|
||||
actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID})
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID})
|
||||
assert.NoError(t, actionRun.LoadAttributes(t.Context()))
|
||||
|
||||
assert.Equal(t, user2.Name, gtCtx["actor"].GetStringValue())
|
||||
assert.Equal(t, setting.AppURL+"api/v1", gtCtx["api_url"].GetStringValue())
|
||||
assert.Equal(t, apiPull.Base.Ref, gtCtx["base_ref"].GetStringValue())
|
||||
runEvent := map[string]any{}
|
||||
assert.NoError(t, json.Unmarshal([]byte(actionRun.EventPayload), &runEvent))
|
||||
assert.True(t, reflect.DeepEqual(gtCtx["event"].GetStructValue().AsMap(), runEvent))
|
||||
assert.Equal(t, actionRun.TriggerEvent, gtCtx["event_name"].GetStringValue())
|
||||
assert.Equal(t, apiPull.Head.Ref, gtCtx["head_ref"].GetStringValue())
|
||||
assert.Equal(t, actionRunJob.JobID, gtCtx["job"].GetStringValue())
|
||||
assert.Equal(t, actionRun.Ref, gtCtx["ref"].GetStringValue())
|
||||
assert.Equal(t, (git.RefName(actionRun.Ref)).ShortName(), gtCtx["ref_name"].GetStringValue())
|
||||
assert.False(t, gtCtx["ref_protected"].GetBoolValue())
|
||||
assert.Equal(t, string((git.RefName(actionRun.Ref)).RefType()), gtCtx["ref_type"].GetStringValue())
|
||||
assert.Equal(t, actionRun.Repo.OwnerName+"/"+actionRun.Repo.Name, gtCtx["repository"].GetStringValue())
|
||||
assert.Equal(t, actionRun.Repo.OwnerName, gtCtx["repository_owner"].GetStringValue())
|
||||
assert.Equal(t, actionRun.Repo.HTMLURL(), gtCtx["repositoryUrl"].GetStringValue())
|
||||
assert.Equal(t, strconv.FormatInt(actionRunJob.RunID, 10), gtCtx["run_id"].GetStringValue())
|
||||
assert.Equal(t, strconv.FormatInt(actionRun.Index, 10), gtCtx["run_number"].GetStringValue())
|
||||
assert.Equal(t, strconv.FormatInt(actionRunJob.Attempt, 10), gtCtx["run_attempt"].GetStringValue())
|
||||
assert.Equal(t, "Actions", gtCtx["secret_source"].GetStringValue())
|
||||
assert.Equal(t, setting.AppURL, gtCtx["server_url"].GetStringValue())
|
||||
assert.Equal(t, actionRun.CommitSHA, gtCtx["sha"].GetStringValue())
|
||||
assert.Equal(t, actionRun.WorkflowID, gtCtx["workflow"].GetStringValue())
|
||||
assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue())
|
||||
token := gtCtx["token"].GetStringValue()
|
||||
assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:])
|
||||
})
|
||||
}
|
||||
|
||||
// Ephemeral
|
||||
func TestActionsGiteaContextEphemeral(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "actions-gitea-context", false)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, true)
|
||||
|
||||
// verify CleanupEphemeralRunners does not remove this runner
|
||||
err := actions_service.CleanupEphemeralRunners(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// init the workflow
|
||||
wfTreePath := ".gitea/workflows/pull.yml"
|
||||
wfFileContent := `name: Pull Request
|
||||
on: pull_request
|
||||
jobs:
|
||||
wf1-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'test the pull'
|
||||
wf2-job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'test the pull'
|
||||
`
|
||||
opts := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wfTreePath, opts)
|
||||
// user2 creates a pull request
|
||||
doAPICreateFile(user2APICtx, "user2-patch.txt", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "user2/patch-1",
|
||||
Message: "create user2-patch.txt",
|
||||
Author: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user2.Name,
|
||||
Email: user2.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("user2-fix")),
|
||||
})(t)
|
||||
apiPull, err := doAPICreatePullRequest(user2APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, "user2/patch-1")(t)
|
||||
assert.NoError(t, err)
|
||||
task := runner.fetchTask(t)
|
||||
gtCtx := task.Context.GetFields()
|
||||
actionTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
|
||||
actionRunJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: actionTask.JobID})
|
||||
actionRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: actionRunJob.RunID})
|
||||
assert.NoError(t, actionRun.LoadAttributes(t.Context()))
|
||||
|
||||
assert.Equal(t, user2.Name, gtCtx["actor"].GetStringValue())
|
||||
assert.Equal(t, setting.AppURL+"api/v1", gtCtx["api_url"].GetStringValue())
|
||||
assert.Equal(t, apiPull.Base.Ref, gtCtx["base_ref"].GetStringValue())
|
||||
runEvent := map[string]any{}
|
||||
assert.NoError(t, json.Unmarshal([]byte(actionRun.EventPayload), &runEvent))
|
||||
assert.True(t, reflect.DeepEqual(gtCtx["event"].GetStructValue().AsMap(), runEvent))
|
||||
assert.Equal(t, actionRun.TriggerEvent, gtCtx["event_name"].GetStringValue())
|
||||
assert.Equal(t, apiPull.Head.Ref, gtCtx["head_ref"].GetStringValue())
|
||||
assert.Equal(t, actionRunJob.JobID, gtCtx["job"].GetStringValue())
|
||||
assert.Equal(t, actionRun.Ref, gtCtx["ref"].GetStringValue())
|
||||
assert.Equal(t, (git.RefName(actionRun.Ref)).ShortName(), gtCtx["ref_name"].GetStringValue())
|
||||
assert.False(t, gtCtx["ref_protected"].GetBoolValue())
|
||||
assert.Equal(t, string((git.RefName(actionRun.Ref)).RefType()), gtCtx["ref_type"].GetStringValue())
|
||||
assert.Equal(t, actionRun.Repo.OwnerName+"/"+actionRun.Repo.Name, gtCtx["repository"].GetStringValue())
|
||||
assert.Equal(t, actionRun.Repo.OwnerName, gtCtx["repository_owner"].GetStringValue())
|
||||
assert.Equal(t, actionRun.Repo.HTMLURL(), gtCtx["repositoryUrl"].GetStringValue())
|
||||
assert.Equal(t, strconv.FormatInt(actionRunJob.RunID, 10), gtCtx["run_id"].GetStringValue())
|
||||
assert.Equal(t, strconv.FormatInt(actionRun.Index, 10), gtCtx["run_number"].GetStringValue())
|
||||
assert.Equal(t, strconv.FormatInt(actionRunJob.Attempt, 10), gtCtx["run_attempt"].GetStringValue())
|
||||
assert.Equal(t, "Actions", gtCtx["secret_source"].GetStringValue())
|
||||
assert.Equal(t, setting.AppURL, gtCtx["server_url"].GetStringValue())
|
||||
assert.Equal(t, actionRun.CommitSHA, gtCtx["sha"].GetStringValue())
|
||||
assert.Equal(t, actionRun.WorkflowID, gtCtx["workflow"].GetStringValue())
|
||||
assert.Equal(t, setting.Actions.DefaultActionsURL.URL(), gtCtx["gitea_default_actions_url"].GetStringValue())
|
||||
token := gtCtx["token"].GetStringValue()
|
||||
assert.Equal(t, actionTask.TokenLastEight, token[len(token)-8:])
|
||||
|
||||
// verify CleanupEphemeralRunners does not remove this runner
|
||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp, err := runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: 0,
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, resp.Msg.Task)
|
||||
|
||||
// verify CleanupEphemeralRunners does not remove this runner
|
||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = runner.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: actionTask.ID,
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
|
||||
resp, err = runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: 0,
|
||||
}))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
|
||||
resp, err = runner.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: 0,
|
||||
}))
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, resp)
|
||||
|
||||
// create a runner that picks a job and get force cancelled
|
||||
runnerToBeRemoved := newMockRunner()
|
||||
runnerToBeRemoved.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner-to-be-removed", []string{"ubuntu-latest"}, true)
|
||||
|
||||
taskToStopAPIObj := runnerToBeRemoved.fetchTask(t)
|
||||
|
||||
taskToStop := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: taskToStopAPIObj.Id})
|
||||
|
||||
// verify CleanupEphemeralRunners does not remove the custom crafted runner
|
||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
runnerToRemove := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: taskToStop.RunnerID})
|
||||
|
||||
err = actions_model.StopTask(t.Context(), taskToStop.ID, actions_model.StatusFailure)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// verify CleanupEphemeralRunners does remove the custom crafted runner
|
||||
err = actions_service.CleanupEphemeralRunners(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: runnerToRemove.ID})
|
||||
})
|
||||
}
|
||||
|
||||
func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository {
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||
Name: repoName,
|
||||
Private: isPrivate,
|
||||
Readme: "Default",
|
||||
AutoInit: true,
|
||||
DefaultBranch: "main",
|
||||
}).AddTokenAuth(authToken)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiRepo := DecodeJSON(t, resp, &api.Repository{})
|
||||
return apiRepo
|
||||
}
|
||||
|
||||
func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions {
|
||||
return &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: branch,
|
||||
Message: msg,
|
||||
Author: api.Identity{
|
||||
Name: u.Name,
|
||||
Email: u.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: u.Name,
|
||||
Email: u.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)),
|
||||
}
|
||||
}
|
||||
|
||||
func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse {
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts).
|
||||
AddTokenAuth(authToken)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
fileResponse := DecodeJSON(t, resp, &api.FileResponse{})
|
||||
return fileResponse
|
||||
}
|
||||
|
||||
// getTaskJobNameByTaskID get the job name of the task by task ID
|
||||
// there is currently not an API for querying a task by ID so we have to list all the tasks
|
||||
func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string {
|
||||
// FIXME: we may need to query several pages
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)).
|
||||
AddTokenAuth(authToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
taskRespBefore := DecodeJSON(t, resp, &api.ActionTaskResponse{})
|
||||
for _, apiTask := range taskRespBefore.Entries {
|
||||
if apiTask.ID == taskID {
|
||||
return apiTask.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TestLegacyRunsInCronTasks verifies that the background cron tasks correctly handle runs/jobs
|
||||
// created before migration v331 (legacy data with LatestAttemptID=0 and jobs with RunAttemptID=0).
|
||||
func TestLegacyRunsInCronTasks(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-legacy-cron", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(httpContext)(t)
|
||||
|
||||
// Far-past timestamp so the queries match regardless of the configured timeouts.
|
||||
oldTS := timeutil.TimeStamp(time.Now().Add(-30 * 24 * time.Hour).Unix())
|
||||
|
||||
// insertLegacyRunJob inserts a run + job without an ActionRunAttempt record, simulating data created before migration v331 (LatestAttemptID=0, job.RunAttemptID=0, job.AttemptJobID=0).
|
||||
insertLegacyRunJob := func(t *testing.T, index int64, runStatus, jobStatus actions_model.Status) (*actions_model.ActionRun, *actions_model.ActionRunJob) {
|
||||
t.Helper()
|
||||
|
||||
run := &actions_model.ActionRun{
|
||||
Title: fmt.Sprintf("legacy run %d", index),
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
WorkflowID: fmt.Sprintf("legacy-%d.yml", index),
|
||||
Index: index,
|
||||
TriggerUserID: user2.ID,
|
||||
Ref: "refs/heads/" + repo.DefaultBranch,
|
||||
CommitSHA: "0000000000000000000000000000000000000000",
|
||||
Event: "workflow_dispatch",
|
||||
TriggerEvent: "workflow_dispatch",
|
||||
EventPayload: "{}",
|
||||
Status: runStatus,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), run))
|
||||
|
||||
job := &actions_model.ActionRunJob{
|
||||
RunID: run.ID,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
Name: "legacy-job",
|
||||
Attempt: 1,
|
||||
JobID: "legacy-job",
|
||||
RunsOn: []string{"ubuntu-latest"},
|
||||
Status: jobStatus,
|
||||
RunAttemptID: 0,
|
||||
AttemptJobID: 0,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), job))
|
||||
|
||||
// backfill timestamps so the cron task queries can match them.
|
||||
_, err := db.GetEngine(t.Context()).Exec("UPDATE action_run SET created=?, updated=? WHERE id=?", int64(oldTS), int64(oldTS), run.ID)
|
||||
require.NoError(t, err)
|
||||
_, err = db.GetEngine(t.Context()).Exec("UPDATE action_run_job SET created=?, updated=? WHERE id=?", int64(oldTS), int64(oldTS), job.ID)
|
||||
require.NoError(t, err)
|
||||
run.Created, run.Updated = oldTS, oldTS
|
||||
job.Created, job.Updated = oldTS, oldTS
|
||||
return run, job
|
||||
}
|
||||
|
||||
t.Run("StopZombieTasks", func(t *testing.T) {
|
||||
run, job := insertLegacyRunJob(t, 10, actions_model.StatusRunning, actions_model.StatusRunning)
|
||||
|
||||
task := &actions_model.ActionTask{
|
||||
JobID: job.ID,
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusRunning,
|
||||
Started: oldTS,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
}
|
||||
task.GenerateAndFillToken()
|
||||
require.NoError(t, db.Insert(t.Context(), task))
|
||||
_, err := db.GetEngine(t.Context()).Exec("UPDATE action_task SET updated=? WHERE id=?", int64(oldTS), task.ID)
|
||||
require.NoError(t, err)
|
||||
job.TaskID = task.ID
|
||||
_, err = db.GetEngine(t.Context()).ID(job.ID).Cols("task_id").Update(job)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, actions_service.StopZombieTasks(t.Context()))
|
||||
|
||||
gotTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.ID})
|
||||
assert.Equal(t, actions_model.StatusFailure, gotTask.Status)
|
||||
gotJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID})
|
||||
assert.Equal(t, actions_model.StatusFailure, gotJob.Status)
|
||||
gotRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
assert.Equal(t, actions_model.StatusFailure, gotRun.Status)
|
||||
})
|
||||
|
||||
t.Run("StopEndlessTasks", func(t *testing.T) {
|
||||
run, job := insertLegacyRunJob(t, 20, actions_model.StatusRunning, actions_model.StatusRunning)
|
||||
|
||||
task := &actions_model.ActionTask{
|
||||
JobID: job.ID,
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusRunning,
|
||||
Started: oldTS,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
}
|
||||
task.GenerateAndFillToken()
|
||||
require.NoError(t, db.Insert(t.Context(), task))
|
||||
job.TaskID = task.ID
|
||||
_, err := db.GetEngine(t.Context()).ID(job.ID).Cols("task_id").Update(job)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, actions_service.StopEndlessTasks(t.Context()))
|
||||
|
||||
gotTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.ID})
|
||||
assert.Equal(t, actions_model.StatusFailure, gotTask.Status)
|
||||
gotJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID})
|
||||
assert.Equal(t, actions_model.StatusFailure, gotJob.Status)
|
||||
gotRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
assert.Equal(t, actions_model.StatusFailure, gotRun.Status)
|
||||
})
|
||||
|
||||
t.Run("CancelAbandonedJobs", func(t *testing.T) {
|
||||
run, job := insertLegacyRunJob(t, 30, actions_model.StatusWaiting, actions_model.StatusWaiting)
|
||||
|
||||
require.NoError(t, actions_service.CancelAbandonedJobs(t.Context()))
|
||||
|
||||
gotJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled, gotJob.Status)
|
||||
gotRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
assert.Equal(t, actions_model.StatusCancelled, gotRun.Status)
|
||||
})
|
||||
|
||||
t.Run("Cleanup", func(t *testing.T) {
|
||||
run, _ := insertLegacyRunJob(t, 40, actions_model.StatusSuccess, actions_model.StatusSuccess)
|
||||
|
||||
expiredArtifact := &actions_model.ActionArtifact{
|
||||
RunID: run.ID,
|
||||
RunAttemptID: 0, // legacy artifact
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: run.CommitSHA,
|
||||
StoragePath: fmt.Sprintf("artifacts/legacy-expired-%d.zip", run.ID),
|
||||
FileSize: 1,
|
||||
FileCompressedSize: 1,
|
||||
ContentEncodingOrType: actions_model.ContentTypeZip,
|
||||
ArtifactPath: "legacy-expired.zip",
|
||||
ArtifactName: "legacy-expired",
|
||||
Status: actions_model.ArtifactStatusUploadConfirmed,
|
||||
ExpiredUnix: oldTS,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), expiredArtifact))
|
||||
|
||||
require.NoError(t, actions_service.Cleanup(t.Context()))
|
||||
|
||||
gotArtifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: expiredArtifact.ID})
|
||||
assert.Equal(t, actions_model.ArtifactStatusExpired, gotArtifact.Status)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
// 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 "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
org_model "gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
unit_model "gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/lfs"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/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
|
||||
task.GenerateAndFillToken()
|
||||
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)
|
||||
repo := DecodeJSON(t, resp, &structs.Repository{})
|
||||
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))
|
||||
|
||||
task.GenerateAndFillToken()
|
||||
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,
|
||||
}
|
||||
task.GenerateAndFillToken()
|
||||
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,68 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsListFilters(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
session := loginUser(t, user5.Name)
|
||||
actionsURL := fmt.Sprintf("/%s/%s/actions", user5.Name, repo.Name)
|
||||
|
||||
t.Run("BranchDropdownListsBranches", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", actionsURL)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
var labels []string
|
||||
htmlDoc.doc.Find(`[data-test-id="filter-branch"] .menu a.item`).Each(func(_ int, a *goquery.Selection) {
|
||||
labels = append(labels, strings.TrimSpace(a.Text()))
|
||||
})
|
||||
assert.Contains(t, labels, "master")
|
||||
})
|
||||
|
||||
t.Run("FilterByBranch", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", actionsURL+"?branch=master")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
refs := htmlDoc.doc.Find(".run-list .run-list-ref")
|
||||
assert.Positive(t, refs.Length(), "filtered run list should not be empty")
|
||||
refs.Each(func(_ int, sel *goquery.Selection) {
|
||||
assert.Equal(t, "master", strings.TrimSpace(sel.Text()))
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("PaginationPreservesFilters", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", actionsURL+"?branch=master&limit=1")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
pageLinks := htmlDoc.doc.Find(".pagination a[href]")
|
||||
assert.Positive(t, pageLinks.Length(), "pagination should be rendered")
|
||||
pageLinks.Each(func(_ int, a *goquery.Selection) {
|
||||
u, err := url.Parse(a.AttrOr("href", ""))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "master", u.Query().Get("branch"), "pagination link must preserve branch filter")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/dbfs"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
actions_module "gitea.dev/modules/actions"
|
||||
"gitea.dev/modules/storage"
|
||||
|
||||
"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")
|
||||
|
||||
// The runner re-sends its final UpdateLog when the response was lost.
|
||||
// A sealed log must ack the re-send and still reject new appended rows.
|
||||
t.Run("re-sent finalize is idempotent", func(t *testing.T) {
|
||||
finalize := &runnerv1.UpdateLogRequest{TaskId: task.Id, Index: 0, Rows: nil, NoMore: true}
|
||||
resp, err := runner.client.runnerServiceClient.UpdateLog(t.Context(), connect.NewRequest(finalize))
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 0, resp.Msg.AckIndex)
|
||||
|
||||
_, err = runner.client.runnerServiceClient.UpdateLog(t.Context(), connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||
TaskId: task.Id, Index: 0, Rows: []*runnerv1.LogRow{{Content: "late"}}, NoMore: true,
|
||||
}))
|
||||
require.Error(t, err, "appending rows past the seal must be rejected")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestDownloadTaskLogs(t *testing.T) {
|
||||
now := time.Now()
|
||||
testCases := []struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcome []*mockTaskOutcome
|
||||
zstdEnabled bool
|
||||
}{
|
||||
{
|
||||
treePath: ".gitea/workflows/download-task-logs-zstd.yml",
|
||||
fileContent: `name: download-task-logs-zstd
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/download-task-logs-zstd.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1 with zstd enabled
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job2 with zstd enabled
|
||||
`,
|
||||
outcome: []*mockTaskOutcome{
|
||||
{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(1 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(2 * time.Second)),
|
||||
Content: "job1 zstd enabled",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(3 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(1 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(2 * time.Second)),
|
||||
Content: "job2 zstd enabled",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(3 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
zstdEnabled: true,
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/download-task-logs-no-zstd.yml",
|
||||
fileContent: `name: download-task-logs-no-zstd
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/download-task-logs-no-zstd.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1 with zstd disabled
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job2 with zstd disabled
|
||||
`,
|
||||
outcome: []*mockTaskOutcome{
|
||||
{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job1 zstd disabled",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job2 zstd disabled",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
zstdEnabled: false,
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-download-task-logs", 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)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("test "+tc.treePath, func(t *testing.T) {
|
||||
var resetFunc func()
|
||||
if tc.zstdEnabled {
|
||||
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd")
|
||||
assert.True(t, setting.Actions.LogCompression.IsZstd())
|
||||
} else {
|
||||
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none")
|
||||
assert.False(t, setting.Actions.LogCompression.IsZstd())
|
||||
}
|
||||
|
||||
// create the workflow file
|
||||
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+tc.treePath, tc.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
|
||||
|
||||
// fetch and execute tasks
|
||||
for _, outcome := range tc.outcome {
|
||||
task := runner.fetchTask(t)
|
||||
runner.execTask(t, task, outcome)
|
||||
|
||||
// check whether the log file exists
|
||||
logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id)
|
||||
if setting.Actions.LogCompression.IsZstd() {
|
||||
logFileName += ".zst"
|
||||
}
|
||||
_, err := storage.Actions.Stat(logFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, job, run := getTaskAndJobAndRunByTaskID(t, task.Id)
|
||||
|
||||
// download task logs and check content
|
||||
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")
|
||||
assert.Len(t, logTextLines, len(outcome.logRows))
|
||||
for idx, lr := range outcome.logRows {
|
||||
assert.Equal(
|
||||
t,
|
||||
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
|
||||
logTextLines[idx],
|
||||
)
|
||||
}
|
||||
|
||||
// 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, job.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
||||
assert.Len(t, logTextLines, len(outcome.logRows))
|
||||
for idx, lr := range outcome.logRows {
|
||||
assert.Equal(
|
||||
t,
|
||||
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
|
||||
logTextLines[idx],
|
||||
)
|
||||
}
|
||||
}
|
||||
resetFunc()
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("DownloadRerunTaskLogs", func(t *testing.T) {
|
||||
treePath := ".gitea/workflows/download-rerun-logs.yml"
|
||||
fileContent := `name: download-rerun-logs
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/download-rerun-logs.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo job2
|
||||
`
|
||||
|
||||
// create the workflow file
|
||||
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+treePath, fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, treePath, opts)
|
||||
|
||||
// first run
|
||||
job1Task1 := runner.fetchTask(t)
|
||||
_, job1, _ := getTaskAndJobAndRunByTaskID(t, job1Task1.Id)
|
||||
runner.execTask(t, job1Task1, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(1 * time.Second)),
|
||||
Content: "job1 first run",
|
||||
},
|
||||
},
|
||||
})
|
||||
job2Task1 := runner.fetchTask(t)
|
||||
_, job2, run := getTaskAndJobAndRunByTaskID(t, job2Task1.Id)
|
||||
runner.execTask(t, job2Task1, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(1 * time.Second)),
|
||||
Content: "job2 first run",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// check job1 log
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job1.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "job1 first run")
|
||||
// check job2 log
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job2.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "job2 first run")
|
||||
|
||||
// only rerun job2
|
||||
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)
|
||||
job2TaskRerun := runner.fetchTask(t)
|
||||
runner.execTask(t, job2TaskRerun, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(1 * time.Second)),
|
||||
Content: "job2 rerun",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
job1Rerun := getLatestAttemptJobByTemplateJobID(t, run.ID, job1.ID)
|
||||
assert.Equal(t, run.LatestAttemptID, job1Rerun.RunAttemptID)
|
||||
job2Rerun := getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
|
||||
assert.Equal(t, run.LatestAttemptID, job2Rerun.RunAttemptID)
|
||||
|
||||
// check job1 rerun log
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job1Rerun.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "job1 first run") // should return the log of first run because job1 didn't rerun
|
||||
// check job2 rerun log
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job2Rerun.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "job2 rerun")
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/timeutil"
|
||||
actions_web "gitea.dev/routers/web/repo/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsRerun(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
userAdmin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
sessionAdmin := loginUser(t, userAdmin.Name)
|
||||
|
||||
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-rerun", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(httpContext)(t)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
wfTreePath := ".gitea/workflows/actions-rerun-workflow-1.yml"
|
||||
wfFileContent := `name: actions-rerun-workflow-1
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/actions-rerun-workflow-1.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'job1'
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo 'job2'
|
||||
`
|
||||
|
||||
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create"+wfTreePath, wfFileContent)
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
|
||||
|
||||
// fetch and exec job1
|
||||
job1Task := runner.fetchTask(t)
|
||||
assert.Equal(t, "1", job1Task.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
_, job1, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
|
||||
runner.execTask(t, job1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
// RERUN-FAILURE: the run is not done
|
||||
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,
|
||||
})
|
||||
assert.EqualValues(t, 1, getRunLatestAttemptNum(t, run.ID))
|
||||
|
||||
// RERUN-1: rerun the run
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
|
||||
sessionAdmin.MakeRequest(t, req, http.StatusOK) // triggered by admin user
|
||||
// fetch and exec job1
|
||||
job1TaskR1 := runner.fetchTask(t)
|
||||
assert.Equal(t, "2", job1TaskR1.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
_, job1R1, _ := getTaskAndJobAndRunByTaskID(t, job1TaskR1.Id)
|
||||
assert.Equal(t, job1.AttemptJobID, job1R1.AttemptJobID)
|
||||
runner.execTask(t, job1TaskR1, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
// fetch and exec job2
|
||||
job2TaskR1 := runner.fetchTask(t)
|
||||
assert.Equal(t, "2", job2TaskR1.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
_, job2R1, _ := getTaskAndJobAndRunByTaskID(t, job2TaskR1.Id)
|
||||
assert.Equal(t, job2.AttemptJobID, job2R1.AttemptJobID)
|
||||
runner.execTask(t, job2TaskR1, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
assert.EqualValues(t, 2, getRunLatestAttemptNum(t, run.ID))
|
||||
|
||||
// RERUN-2: rerun job1
|
||||
job1 = getLatestAttemptJobByTemplateJobID(t, run.ID, job1.ID)
|
||||
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
|
||||
job1TaskR2 := runner.fetchTask(t)
|
||||
assert.Equal(t, "3", job1TaskR2.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
runner.execTask(t, job1TaskR2, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
// fetch and exec job2
|
||||
job2TaskR2 := runner.fetchTask(t)
|
||||
assert.Equal(t, "3", job2TaskR2.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
runner.execTask(t, job2TaskR2, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
assert.EqualValues(t, 3, getRunLatestAttemptNum(t, run.ID))
|
||||
|
||||
// RERUN-3: rerun job2
|
||||
job2 = getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
|
||||
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
|
||||
job2TaskR3 := runner.fetchTask(t)
|
||||
assert.Equal(t, "4", job2TaskR3.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
runner.execTask(t, job2TaskR3, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
runner.fetchNoTask(t)
|
||||
assert.EqualValues(t, 4, getRunLatestAttemptNum(t, run.ID))
|
||||
|
||||
runLatestAttempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
job2LatestAttempt := getLatestAttemptJobByTemplateJobID(t, run.ID, job2.ID)
|
||||
assert.Equal(t, runLatestAttempt.LatestAttemptID, job2LatestAttempt.RunAttemptID)
|
||||
|
||||
t.Run("AttemptAPI", func(t *testing.T) {
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/attempts/2", user2.Name, repo.Name, run.ID)).
|
||||
AddTokenAuth(token)
|
||||
attemptResp := MakeRequest(t, req, http.StatusOK)
|
||||
apiAttempt := DecodeJSON(t, attemptResp, &api.ActionWorkflowRun{})
|
||||
assert.Equal(t, run.ID, apiAttempt.ID)
|
||||
assert.EqualValues(t, 2, apiAttempt.RunAttempt)
|
||||
assert.Equal(t, "completed", apiAttempt.Status)
|
||||
assert.Equal(t, "success", apiAttempt.Conclusion)
|
||||
assert.NotNil(t, apiAttempt.PreviousAttemptURL)
|
||||
assert.True(t, strings.HasSuffix(*apiAttempt.PreviousAttemptURL, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/attempts/1", user2.Name, repo.Name, run.ID)))
|
||||
assert.Equal(t, user2.Name, apiAttempt.Actor.UserName)
|
||||
assert.Equal(t, userAdmin.Name, apiAttempt.TriggerActor.UserName)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs/%d/attempts/2/jobs", user2.Name, repo.Name, run.ID)).
|
||||
AddTokenAuth(token)
|
||||
attemptJobsResp := MakeRequest(t, req, http.StatusOK)
|
||||
apiAttemptJobs := DecodeJSON(t, attemptJobsResp, &api.ActionWorkflowJobsResponse{})
|
||||
assert.Len(t, apiAttemptJobs.Entries, 2)
|
||||
assert.ElementsMatch(t, []int64{job1R1.ID, job2R1.ID}, []int64{apiAttemptJobs.Entries[0].ID, apiAttemptJobs.Entries[1].ID})
|
||||
})
|
||||
|
||||
t.Run("MaxRerunAttempts", func(t *testing.T) {
|
||||
// The run has 4 attempts after the previous reruns. Lower the cap to 4 to hit the limit.
|
||||
defer test.MockVariableValue(&setting.Actions.MaxRerunAttempts, int64(4))()
|
||||
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
|
||||
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
assert.Contains(t, resp.Body.String(), "workflow run has reached the maximum")
|
||||
assert.EqualValues(t, 4, getRunLatestAttemptNum(t, run.ID))
|
||||
|
||||
// Raising the cap lets rerun proceed again.
|
||||
defer test.MockVariableValue(&setting.Actions.MaxRerunAttempts, int64(5))()
|
||||
|
||||
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
|
||||
job1TaskR4 := runner.fetchTask(t)
|
||||
assert.Equal(t, "5", job1TaskR4.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
runner.execTask(t, job1TaskR4, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
job2TaskR4 := runner.fetchTask(t)
|
||||
assert.Equal(t, "5", job2TaskR4.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
runner.execTask(t, job2TaskR4, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
assert.EqualValues(t, 5, getRunLatestAttemptNum(t, run.ID))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionsRerunLegacyNoAttemptRun(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)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-rerun-legacy", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(httpContext)(t)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
wfTreePath := ".gitea/workflows/actions-rerun-legacy.yml"
|
||||
wfFileContent := `name: actions-rerun-legacy
|
||||
on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'job1'
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo 'job2'
|
||||
`
|
||||
|
||||
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||
fileResp := createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
|
||||
require.NotNil(t, fileResp)
|
||||
|
||||
// Start preparing legacy data
|
||||
|
||||
payloads := mustParseSingleWorkflowPayloads(t, wfFileContent)
|
||||
now := timeutil.TimeStamp(time.Now().Unix())
|
||||
started := now - 20
|
||||
stopped := now - 10
|
||||
|
||||
legacyRun := &actions_model.ActionRun{
|
||||
Title: "legacy rerun test",
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
WorkflowID: "actions-rerun-legacy.yml",
|
||||
Index: 1,
|
||||
TriggerUserID: user2.ID,
|
||||
Ref: "refs/heads/" + repo.DefaultBranch,
|
||||
CommitSHA: fileResp.Commit.SHA,
|
||||
Event: "workflow_dispatch",
|
||||
TriggerEvent: "workflow_dispatch",
|
||||
EventPayload: "{}",
|
||||
Status: actions_model.StatusSuccess,
|
||||
Started: started,
|
||||
Stopped: stopped,
|
||||
Created: started - 5,
|
||||
Updated: stopped,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), legacyRun))
|
||||
// xorm does not update "created"-tagged fields via ORM methods; use raw SQL to backfill historical timestamps.
|
||||
_, err := db.GetEngine(t.Context()).Exec("UPDATE action_run SET created=?, updated=? WHERE id=?", int64(started-5), int64(stopped), legacyRun.ID)
|
||||
require.NoError(t, err)
|
||||
legacyRun.Created = started - 5
|
||||
legacyRun.Updated = stopped
|
||||
|
||||
legacyJob1 := &actions_model.ActionRunJob{
|
||||
RunID: legacyRun.ID,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: legacyRun.CommitSHA,
|
||||
Name: payloads["job1"].name,
|
||||
Attempt: 1,
|
||||
WorkflowPayload: payloads["job1"].payload,
|
||||
JobID: "job1",
|
||||
Needs: payloads["job1"].needs,
|
||||
RunsOn: payloads["job1"].runsOn,
|
||||
Status: actions_model.StatusSuccess,
|
||||
RunAttemptID: 0,
|
||||
AttemptJobID: 0,
|
||||
Started: started,
|
||||
Stopped: stopped,
|
||||
IsForkPullRequest: false,
|
||||
}
|
||||
legacyJob2 := &actions_model.ActionRunJob{
|
||||
RunID: legacyRun.ID,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: legacyRun.CommitSHA,
|
||||
Name: payloads["job2"].name,
|
||||
Attempt: 1,
|
||||
WorkflowPayload: payloads["job2"].payload,
|
||||
JobID: "job2",
|
||||
Needs: payloads["job2"].needs,
|
||||
RunsOn: payloads["job2"].runsOn,
|
||||
Status: actions_model.StatusSuccess,
|
||||
RunAttemptID: 0,
|
||||
AttemptJobID: 0,
|
||||
Started: started,
|
||||
Stopped: stopped,
|
||||
IsForkPullRequest: false,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), legacyJob1, legacyJob2))
|
||||
|
||||
legacyTask1 := &actions_model.ActionTask{
|
||||
JobID: legacyJob1.ID,
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Started: started,
|
||||
Stopped: stopped,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: legacyRun.CommitSHA,
|
||||
IsForkPullRequest: false,
|
||||
}
|
||||
legacyTask1.GenerateAndFillToken()
|
||||
legacyTask2 := &actions_model.ActionTask{
|
||||
JobID: legacyJob2.ID,
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Started: started,
|
||||
Stopped: stopped,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: legacyRun.CommitSHA,
|
||||
IsForkPullRequest: false,
|
||||
}
|
||||
legacyTask2.GenerateAndFillToken()
|
||||
require.NoError(t, db.Insert(t.Context(), legacyTask1, legacyTask2))
|
||||
|
||||
legacyJob1.TaskID = legacyTask1.ID
|
||||
legacyJob2.TaskID = legacyTask2.ID
|
||||
_, err = db.GetEngine(t.Context()).ID(legacyJob1.ID).Cols("task_id").Update(legacyJob1)
|
||||
require.NoError(t, err)
|
||||
_, err = db.GetEngine(t.Context()).ID(legacyJob2.ID).Cols("task_id").Update(legacyJob2)
|
||||
require.NoError(t, err)
|
||||
|
||||
legacyArtifact := &actions_model.ActionArtifact{
|
||||
RunID: legacyRun.ID,
|
||||
RunAttemptID: 0,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: repo.OwnerID,
|
||||
CommitSHA: legacyRun.CommitSHA,
|
||||
StoragePath: "artifacts/legacy-artifact.zip",
|
||||
FileSize: 123,
|
||||
FileCompressedSize: 123,
|
||||
ContentEncodingOrType: actions_model.ContentTypeZip,
|
||||
ArtifactPath: "legacy-artifact.zip",
|
||||
ArtifactName: "legacy-artifact",
|
||||
Status: actions_model.ArtifactStatusUploadConfirmed,
|
||||
ExpiredUnix: now + timeutil.Day,
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), legacyArtifact))
|
||||
|
||||
// Done preparing legacy data
|
||||
|
||||
// assert the web view for the legacy run before rerun
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, legacyRun.ID))
|
||||
legacyResp := session.MakeRequest(t, req, http.StatusOK)
|
||||
legacyView := DecodeJSON(t, legacyResp, &actions_web.ViewResponse{})
|
||||
// legacy run has no attempt records, so RunAttempt is 0 and Attempts list is empty
|
||||
assert.EqualValues(t, 0, legacyView.State.Run.RunAttempt)
|
||||
assert.Empty(t, legacyView.State.Run.Attempts)
|
||||
assert.Equal(t, "success", legacyView.State.Run.Status)
|
||||
assert.True(t, legacyView.State.Run.Done)
|
||||
// isLatestAttempt=true, done=true: can rerun but not cancel
|
||||
assert.False(t, legacyView.State.Run.CanCancel)
|
||||
assert.False(t, legacyView.State.Run.CanApprove)
|
||||
assert.True(t, legacyView.State.Run.CanRerun)
|
||||
assert.False(t, legacyView.State.Run.CanRerunFailed) // all jobs succeeded
|
||||
assert.True(t, legacyView.State.Run.CanDeleteArtifact)
|
||||
if assert.Len(t, legacyView.State.Run.Jobs, 2) {
|
||||
assert.Equal(t, legacyJob1.ID, legacyView.State.Run.Jobs[0].ID)
|
||||
assert.Equal(t, legacyJob2.ID, legacyView.State.Run.Jobs[1].ID)
|
||||
}
|
||||
if assert.Len(t, legacyView.Artifacts, 1) {
|
||||
assert.Equal(t, legacyArtifact.ArtifactName, legacyView.Artifacts[0].Name)
|
||||
assert.Equal(t, "completed", legacyView.Artifacts[0].Status)
|
||||
}
|
||||
|
||||
// rerun the legacy run
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, legacyRun.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
runAfterRerun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: legacyRun.ID})
|
||||
assert.EqualValues(t, 2, getRunLatestAttemptNum(t, legacyRun.ID))
|
||||
jobsAfterRerun, err := actions_model.GetRunJobsByRunAndAttemptID(t.Context(), legacyRun.ID, runAfterRerun.LatestAttemptID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobsAfterRerun, 2)
|
||||
rerunJobsByJobID := map[string]*actions_model.ActionRunJob{}
|
||||
for _, job := range jobsAfterRerun {
|
||||
rerunJobsByJobID[job.JobID] = job
|
||||
}
|
||||
require.Contains(t, rerunJobsByJobID, "job1")
|
||||
require.Contains(t, rerunJobsByJobID, "job2")
|
||||
assert.Equal(t, actions_model.StatusWaiting, rerunJobsByJobID["job1"].Status)
|
||||
assert.Equal(t, actions_model.StatusBlocked, rerunJobsByJobID["job2"].Status)
|
||||
|
||||
// fetch job1 rerun task
|
||||
job1TaskR1 := runner.fetchTask(t)
|
||||
assert.Equal(t, "2", job1TaskR1.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
rerunJob1Task, rerunJob1, rerunRun := getTaskAndJobAndRunByTaskID(t, job1TaskR1.Id)
|
||||
assert.Equal(t, legacyRun.ID, rerunRun.ID)
|
||||
assert.Equal(t, rerunJob1.RunAttemptID, rerunRun.LatestAttemptID)
|
||||
runner.execTask(t, job1TaskR1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
|
||||
// fetch job2 rerun task
|
||||
job2TaskR1 := runner.fetchTask(t)
|
||||
assert.Equal(t, "2", job2TaskR1.Context.GetFields()["run_attempt"].GetStringValue())
|
||||
rerunJob2Task, rerunJob2, _ := getTaskAndJobAndRunByTaskID(t, job2TaskR1.Id)
|
||||
runner.execTask(t, job2TaskR1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
runner.fetchNoTask(t)
|
||||
|
||||
// query the 2 attempts
|
||||
runAfterRerun = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: legacyRun.ID})
|
||||
attempt1, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(t.Context(), legacyRun.ID, 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, legacyRun.Created, attempt1.Created)
|
||||
assert.Equal(t, legacyRun.Started, attempt1.Started)
|
||||
assert.Equal(t, legacyRun.Stopped, attempt1.Stopped)
|
||||
attempt2, err := actions_model.GetRunAttemptByRunIDAndAttemptNum(t.Context(), legacyRun.ID, 2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, attempt2.ID, runAfterRerun.LatestAttemptID)
|
||||
assert.Equal(t, runAfterRerun.Created, attempt1.Created)
|
||||
assert.Equal(t, runAfterRerun.Started, attempt2.Started)
|
||||
assert.Equal(t, runAfterRerun.Stopped, attempt2.Stopped)
|
||||
|
||||
// assert legacy jobs
|
||||
legacyJob1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: legacyJob1.ID})
|
||||
legacyJob2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: legacyJob2.ID})
|
||||
assert.Equal(t, attempt1.ID, legacyJob1.RunAttemptID)
|
||||
assert.Equal(t, attempt1.ID, legacyJob2.RunAttemptID)
|
||||
assert.EqualValues(t, 1, legacyJob1.Attempt)
|
||||
assert.EqualValues(t, 1, legacyJob2.Attempt)
|
||||
assert.EqualValues(t, 1, legacyJob1.AttemptJobID)
|
||||
assert.EqualValues(t, 2, legacyJob2.AttemptJobID)
|
||||
legacyTask1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: legacyTask1.ID})
|
||||
legacyTask2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: legacyTask2.ID})
|
||||
assert.EqualValues(t, 1, legacyTask1.Attempt)
|
||||
assert.EqualValues(t, 1, legacyTask2.Attempt)
|
||||
|
||||
// assert legacy artifacts
|
||||
legacyArtifact = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: legacyArtifact.ID})
|
||||
assert.Equal(t, attempt1.ID, legacyArtifact.RunAttemptID)
|
||||
|
||||
// assert jobs of the latest rerun
|
||||
rerunJob1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: rerunJob1.ID})
|
||||
rerunJob2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: rerunJob2.ID})
|
||||
assert.Equal(t, attempt2.ID, rerunJob1.RunAttemptID)
|
||||
assert.Equal(t, attempt2.ID, rerunJob2.RunAttemptID)
|
||||
assert.Equal(t, legacyJob1.AttemptJobID, rerunJob1.AttemptJobID)
|
||||
assert.Equal(t, legacyJob2.AttemptJobID, rerunJob2.AttemptJobID)
|
||||
assert.EqualValues(t, 2, rerunJob1Task.Attempt)
|
||||
assert.EqualValues(t, 2, rerunJob2Task.Attempt)
|
||||
|
||||
// assert the web view for the original attempt
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/attempts/1", user2.Name, repo.Name, legacyRun.ID))
|
||||
attempt1Resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
attempt1View := DecodeJSON(t, attempt1Resp, &actions_web.ViewResponse{})
|
||||
assert.EqualValues(t, 1, attempt1View.State.Run.RunAttempt)
|
||||
if assert.Len(t, attempt1View.State.Run.Attempts, 2) {
|
||||
// attempts ordered by attempt DESC: index 0 = attempt #2 (latest), index 1 = attempt #1 (current)
|
||||
assert.False(t, attempt1View.State.Run.Attempts[0].Current)
|
||||
assert.True(t, attempt1View.State.Run.Attempts[0].Latest)
|
||||
assert.True(t, attempt1View.State.Run.Attempts[1].Current)
|
||||
assert.False(t, attempt1View.State.Run.Attempts[1].Latest)
|
||||
}
|
||||
// isLatestAttempt=false: all write operations disabled
|
||||
assert.False(t, attempt1View.State.Run.CanCancel)
|
||||
assert.False(t, attempt1View.State.Run.CanApprove)
|
||||
assert.False(t, attempt1View.State.Run.CanRerun)
|
||||
assert.False(t, attempt1View.State.Run.CanRerunFailed)
|
||||
assert.True(t, attempt1View.State.Run.CanDeleteArtifact)
|
||||
assert.Equal(t, legacyJob1.ID, attempt1View.State.Run.Jobs[0].ID)
|
||||
assert.Equal(t, legacyJob2.ID, attempt1View.State.Run.Jobs[1].ID)
|
||||
if assert.Len(t, attempt1View.Artifacts, 1) {
|
||||
assert.Equal(t, attempt1View.Artifacts[0].Name, legacyArtifact.ArtifactName)
|
||||
}
|
||||
|
||||
// assert the web view for the latest attempt
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, legacyRun.ID))
|
||||
attempt2Resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
attempt2View := DecodeJSON(t, attempt2Resp, &actions_web.ViewResponse{})
|
||||
assert.EqualValues(t, 2, attempt2View.State.Run.RunAttempt)
|
||||
if assert.Len(t, attempt2View.State.Run.Attempts, 2) {
|
||||
// attempts ordered by attempt DESC: index 0 = attempt #2 (latest, current), index 1 = attempt #1
|
||||
assert.True(t, attempt2View.State.Run.Attempts[0].Current)
|
||||
assert.True(t, attempt2View.State.Run.Attempts[0].Latest)
|
||||
assert.False(t, attempt2View.State.Run.Attempts[1].Current)
|
||||
assert.False(t, attempt2View.State.Run.Attempts[1].Latest)
|
||||
}
|
||||
// isLatestAttempt=true, done=true: can rerun but not cancel
|
||||
assert.False(t, attempt2View.State.Run.CanCancel)
|
||||
assert.False(t, attempt2View.State.Run.CanApprove)
|
||||
assert.True(t, attempt2View.State.Run.CanRerun)
|
||||
assert.False(t, attempt2View.State.Run.CanRerunFailed) // all jobs succeeded
|
||||
assert.True(t, attempt2View.State.Run.CanDeleteArtifact)
|
||||
assert.Equal(t, rerunJob1.ID, attempt2View.State.Run.Jobs[0].ID)
|
||||
assert.Equal(t, rerunJob2.ID, attempt2View.State.Run.Jobs[1].ID)
|
||||
assert.Empty(t, attempt2View.Artifacts)
|
||||
})
|
||||
}
|
||||
|
||||
type workflowJobPayload struct {
|
||||
name string
|
||||
payload []byte
|
||||
needs []string
|
||||
runsOn []string
|
||||
}
|
||||
|
||||
func mustParseSingleWorkflowPayloads(t *testing.T, workflowContent string) map[string]workflowJobPayload {
|
||||
t.Helper()
|
||||
|
||||
workflows, err := jobparser.Parse([]byte(workflowContent))
|
||||
require.NoError(t, err)
|
||||
|
||||
payloads := make(map[string]workflowJobPayload, len(workflows))
|
||||
for _, workflow := range workflows {
|
||||
id, job := workflow.Job()
|
||||
needs := job.Needs()
|
||||
require.NoError(t, workflow.SetJob(id, job.EraseNeeds()))
|
||||
payload, err := workflow.Marshal()
|
||||
require.NoError(t, err)
|
||||
payloads[id] = workflowJobPayload{
|
||||
name: job.Name,
|
||||
payload: payload,
|
||||
needs: needs,
|
||||
runsOn: job.RunsOn(),
|
||||
}
|
||||
}
|
||||
return payloads
|
||||
}
|
||||
|
||||
func getRunLatestAttemptNum(t *testing.T, runID int64) int64 {
|
||||
t.Helper()
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
attempt := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{ID: run.LatestAttemptID})
|
||||
return attempt.Attempt
|
||||
}
|
||||
@@ -0,0 +1,782 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/json"
|
||||
api "gitea.dev/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsReusableWorkflow(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
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 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user4Session := loginUser(t, user4.Name)
|
||||
user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
t.Run("Same-repo reusable workflow", func(t *testing.T) {
|
||||
apiRepo := createActionsTestRepo(t, user2Token, "workflow-call-test", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
|
||||
defaultRunner := newMockRunner()
|
||||
defaultRunner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-default-runner", []string{"ubuntu-latest"}, false)
|
||||
customRunner := newMockRunner()
|
||||
customRunner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-custom-runner", []string{"custom-os"}, false)
|
||||
|
||||
// add a variable for test
|
||||
req := NewRequestWithJSON(t, "POST",
|
||||
fmt.Sprintf("/api/v1/repos/%s/%s/actions/variables/myvar", repo.OwnerName, repo.Name), &api.CreateVariableOption{
|
||||
Value: "abcdef",
|
||||
}).
|
||||
AddTokenAuth(user2Token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
// add a secret for test
|
||||
req = NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets/mysecret", repo.OwnerName, repo.Name), api.CreateOrUpdateSecretOption{
|
||||
Data: "secRET-t0Ken",
|
||||
}).AddTokenAuth(user2Token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/reusable1.yaml",
|
||||
`name: Reusable1
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
str_input:
|
||||
type: string
|
||||
num_input:
|
||||
type: number
|
||||
bool_input:
|
||||
type: boolean
|
||||
parent_var:
|
||||
type: string
|
||||
needs_out:
|
||||
type: string
|
||||
secrets:
|
||||
PARENT_TOKEN:
|
||||
outputs:
|
||||
r1_out:
|
||||
value: ${{ jobs.reusable1_job2.outputs.r1j2_out }}
|
||||
|
||||
jobs:
|
||||
reusable1_job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'reusable1_job1'
|
||||
|
||||
reusable1_job2:
|
||||
needs: [reusable1_job1]
|
||||
outputs:
|
||||
r1j2_out: ${{ steps.gen_r1j2_output.outputs.out }}
|
||||
runs-on: custom-os
|
||||
steps:
|
||||
- id: gen_r1j2_output
|
||||
run: |
|
||||
echo "out=r1j2_out_data" >> "$GITHUB_OUTPUT"
|
||||
|
||||
reusable1_job3:
|
||||
needs: [reusable1_job2]
|
||||
uses: ./.gitea/workflows/reusable2.yaml
|
||||
with:
|
||||
msg: ${{ inputs.str_input }}
|
||||
`)
|
||||
|
||||
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/reusable2.yaml",
|
||||
`name: Reusable2
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
msg:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
reusable2_job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo ${{ inputs.msg }}
|
||||
`)
|
||||
|
||||
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/caller.yaml",
|
||||
`name: Caller
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/caller.yaml'
|
||||
jobs:
|
||||
caller_job1:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
prepared: ${{ steps.gen_output.outputs.pd }}
|
||||
steps:
|
||||
- id: gen_output
|
||||
run: |
|
||||
echo "pd=prepared_data" >> "$GITHUB_OUTPUT"
|
||||
|
||||
caller_job2:
|
||||
needs: [caller_job1]
|
||||
uses: './.gitea/workflows/reusable1.yaml'
|
||||
with:
|
||||
str_input: 'from_caller_job2'
|
||||
num_input: ${{ 2.3e2 }}
|
||||
bool_input: ${{ gitea.event_name == 'push' }}
|
||||
parent_var: ${{ vars.myvar }}
|
||||
needs_out: ${{ needs.caller_job1.outputs.prepared }}
|
||||
secrets:
|
||||
PARENT_TOKEN: ${{ secrets.mysecret }}
|
||||
|
||||
caller_job3:
|
||||
needs: [caller_job2]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo ${{ needs.caller_job1.outputs.r1_out }}
|
||||
`)
|
||||
|
||||
var (
|
||||
runID int64
|
||||
callerJob2ID, callerJob2AttemptJobID int64
|
||||
callerJob3AttemptJobID int64
|
||||
r1Job2ID, r1Job2AttemptJobID int64
|
||||
r1Job3ID, r1Job3AttemptJobID int64
|
||||
r2Job1AttemptJobID int64
|
||||
)
|
||||
|
||||
t.Run("Check initialized jobs", func(t *testing.T) {
|
||||
// run
|
||||
assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID})
|
||||
runID = run.ID
|
||||
|
||||
// caller_job1
|
||||
assert.Equal(t, 3, unittest.GetCount(t, &actions_model.ActionRunJob{RunID: runID}))
|
||||
callerJob1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "caller_job1"})
|
||||
assert.Equal(t, actions_model.StatusWaiting, callerJob1.Status)
|
||||
assert.False(t, callerJob1.IsReusableCaller)
|
||||
|
||||
// caller_job2
|
||||
callerJob2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "caller_job2"})
|
||||
callerJob2ID = callerJob2.ID
|
||||
callerJob2AttemptJobID = callerJob2.AttemptJobID
|
||||
assert.Equal(t, actions_model.StatusBlocked, callerJob2.Status)
|
||||
assert.True(t, callerJob2.IsReusableCaller)
|
||||
|
||||
// caller_job3
|
||||
callerJob3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "caller_job3"})
|
||||
callerJob3AttemptJobID = callerJob3.AttemptJobID
|
||||
assert.Equal(t, actions_model.StatusBlocked, callerJob3.Status)
|
||||
assert.False(t, callerJob3.IsReusableCaller)
|
||||
})
|
||||
|
||||
t.Run("First run", func(t *testing.T) {
|
||||
callerJob1Task := defaultRunner.fetchTask(t) // for caller_job1
|
||||
_, callerJob1, _ := getTaskAndJobAndRunByTaskID(t, callerJob1Task.Id)
|
||||
assert.Equal(t, "caller_job1", callerJob1.JobID)
|
||||
defaultRunner.fetchNoTask(t)
|
||||
defaultRunner.execTask(t, callerJob1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"prepared": "prepared_data",
|
||||
},
|
||||
})
|
||||
|
||||
r1Job1Task := defaultRunner.fetchTask(t) // for reusable1_job1
|
||||
_, r1Job1, _ := getTaskAndJobAndRunByTaskID(t, r1Job1Task.Id)
|
||||
assert.Equal(t, "reusable1_job1", r1Job1.JobID)
|
||||
assert.Equal(t, callerJob2ID, r1Job1.ParentJobID)
|
||||
payload := getWorkflowCallPayloadFromTask(t, r1Job1Task)
|
||||
if assert.Len(t, payload.Inputs, 5) {
|
||||
assert.Equal(t, "from_caller_job2", payload.Inputs["str_input"])
|
||||
assert.EqualValues(t, 230, payload.Inputs["num_input"])
|
||||
assert.Equal(t, true, payload.Inputs["bool_input"])
|
||||
assert.Equal(t, "abcdef", payload.Inputs["parent_var"])
|
||||
assert.Equal(t, "prepared_data", payload.Inputs["needs_out"])
|
||||
}
|
||||
if assert.Len(t, r1Job1Task.Secrets, 3) {
|
||||
assert.Contains(t, r1Job1Task.Secrets, "GITEA_TOKEN")
|
||||
assert.Contains(t, r1Job1Task.Secrets, "GITHUB_TOKEN")
|
||||
assert.Equal(t, "secRET-t0Ken", r1Job1Task.Secrets["PARENT_TOKEN"])
|
||||
}
|
||||
customRunner.fetchNoTask(t)
|
||||
defaultRunner.execTask(t, r1Job1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
// reusable1_job3 (a nested caller) needs reusable1_job2, so it stays Blocked until r1j2 succeeds.
|
||||
r1Job3Pre := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "reusable1_job3"})
|
||||
assert.Equal(t, actions_model.StatusBlocked, r1Job3Pre.Status)
|
||||
assert.False(t, r1Job3Pre.IsExpanded)
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRunJob{RunID: runID, JobID: "reusable2_job1"}))
|
||||
|
||||
r1Job2Task := customRunner.fetchTask(t) // for reusable1_job2
|
||||
_, r1Job2, _ := getTaskAndJobAndRunByTaskID(t, r1Job2Task.Id)
|
||||
assert.Equal(t, "reusable1_job2", r1Job2.JobID)
|
||||
r1Job2ID = r1Job2.ID
|
||||
r1Job2AttemptJobID = r1Job2.AttemptJobID
|
||||
if assert.Len(t, r1Job2Task.Needs, 1) {
|
||||
assert.Contains(t, r1Job2Task.Needs, "reusable1_job1")
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, r1Job2Task.Needs["reusable1_job1"].Result)
|
||||
}
|
||||
customRunner.execTask(t, r1Job2Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"r1j2_out": "r1j2_out_data",
|
||||
},
|
||||
})
|
||||
|
||||
// Now reusable1_job3 expands and reusable2_job1 becomes runnable.
|
||||
r1Job3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "reusable1_job3"})
|
||||
assert.True(t, r1Job3.IsReusableCaller)
|
||||
assert.True(t, r1Job3.IsExpanded)
|
||||
assert.Equal(t, callerJob2ID, r1Job3.ParentJobID)
|
||||
r1Job3ID = r1Job3.ID
|
||||
r1Job3AttemptJobID = r1Job3.AttemptJobID
|
||||
r2Job1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, JobID: "reusable2_job1"})
|
||||
assert.Equal(t, r1Job3ID, r2Job1.ParentJobID)
|
||||
r2Job1AttemptJobID = r2Job1.AttemptJobID
|
||||
|
||||
r2Job1Task := defaultRunner.fetchTask(t) // for reusable2_job1
|
||||
_, fetchedR2Job1, _ := getTaskAndJobAndRunByTaskID(t, r2Job1Task.Id)
|
||||
assert.Equal(t, "reusable2_job1", fetchedR2Job1.JobID)
|
||||
assert.Equal(t, r1Job3ID, fetchedR2Job1.ParentJobID)
|
||||
r2Job1Payload := getWorkflowCallPayloadFromTask(t, r2Job1Task)
|
||||
if assert.Len(t, r2Job1Payload.Inputs, 1) {
|
||||
assert.Equal(t, "from_caller_job2", r2Job1Payload.Inputs["msg"])
|
||||
}
|
||||
defaultRunner.execTask(t, r2Job1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
callerJob2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: callerJob2ID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, callerJob2.Status)
|
||||
|
||||
callerJob3Task := defaultRunner.fetchTask(t) // for caller_job3
|
||||
_, callerJob3, _ := getTaskAndJobAndRunByTaskID(t, callerJob3Task.Id)
|
||||
assert.Equal(t, "caller_job3", callerJob3.JobID)
|
||||
if assert.Len(t, callerJob3Task.Needs, 1) {
|
||||
assert.Contains(t, callerJob3Task.Needs, "caller_job2")
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, callerJob3Task.Needs["caller_job2"].Result)
|
||||
if assert.Len(t, callerJob3Task.Needs["caller_job2"].Outputs, 1) {
|
||||
assert.Equal(t, "r1j2_out_data", callerJob3Task.Needs["caller_job2"].Outputs["r1_out"])
|
||||
}
|
||||
}
|
||||
defaultRunner.execTask(t, callerJob3Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
callerRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, callerRun.Status)
|
||||
})
|
||||
|
||||
t.Run("Rerun 'reusable1_job2'", func(t *testing.T) {
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", repo.OwnerName, repo.Name, runID, r1Job2ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
||||
attempt2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{RunID: runID, Attempt: 2})
|
||||
assert.Equal(t, actions_model.StatusWaiting, attempt2.Status)
|
||||
callerJob2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, AttemptJobID: callerJob2AttemptJobID})
|
||||
assert.Equal(t, actions_model.StatusWaiting, callerJob2.Status)
|
||||
callerJob3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, AttemptJobID: callerJob3AttemptJobID})
|
||||
assert.Equal(t, actions_model.StatusBlocked, callerJob3.Status)
|
||||
|
||||
// reusable1_job3 needs reusable1_job2, so rerunning r1j2 pulls r1j3 (and its subtree) into the rerun set
|
||||
r1Job3Attempt2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, AttemptJobID: r1Job3AttemptJobID})
|
||||
assert.Equal(t, actions_model.StatusBlocked, r1Job3Attempt2.Status)
|
||||
assert.True(t, r1Job3Attempt2.IsReusableCaller)
|
||||
assert.False(t, r1Job3Attempt2.IsExpanded)
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, JobID: "reusable2_job1"}))
|
||||
|
||||
defaultRunner.fetchNoTask(t)
|
||||
r1Job2Task := customRunner.fetchTask(t)
|
||||
_, r1Job2, _ := getTaskAndJobAndRunByTaskID(t, r1Job2Task.Id)
|
||||
assert.Equal(t, "reusable1_job2", r1Job2.JobID)
|
||||
assert.Equal(t, callerJob2.ID, r1Job2.ParentJobID)
|
||||
assert.Equal(t, r1Job2AttemptJobID, r1Job2.AttemptJobID)
|
||||
assert.Equal(t, actions_model.StatusRunning, r1Job2.Status)
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
assert.Equal(t, actions_model.StatusRunning, run.Status)
|
||||
customRunner.execTask(t, r1Job2Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"r1j2_out": "r1j2_out_data_updated",
|
||||
},
|
||||
})
|
||||
|
||||
// r1j3 expands again. Its child reuses the AttemptJobID from attempt 1
|
||||
r1Job3Attempt2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, AttemptJobID: r1Job3AttemptJobID})
|
||||
assert.True(t, r1Job3Attempt2.IsExpanded)
|
||||
r2Job1Attempt2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, JobID: "reusable2_job1"})
|
||||
assert.Equal(t, r2Job1AttemptJobID, r2Job1Attempt2.AttemptJobID)
|
||||
assert.Equal(t, r1Job3Attempt2.ID, r2Job1Attempt2.ParentJobID)
|
||||
|
||||
r2Job1Task := defaultRunner.fetchTask(t)
|
||||
_, fetchedR2Job1, _ := getTaskAndJobAndRunByTaskID(t, r2Job1Task.Id)
|
||||
assert.Equal(t, "reusable2_job1", fetchedR2Job1.JobID)
|
||||
defaultRunner.execTask(t, r2Job1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
callerJob2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: callerJob2.ID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, callerJob2.Status)
|
||||
|
||||
callerJob3Task := defaultRunner.fetchTask(t)
|
||||
_, callerJob3, _ = getTaskAndJobAndRunByTaskID(t, callerJob3Task.Id)
|
||||
assert.Equal(t, "caller_job3", callerJob3.JobID)
|
||||
if assert.Len(t, callerJob3Task.Needs, 1) {
|
||||
assert.Contains(t, callerJob3Task.Needs, "caller_job2")
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, callerJob3Task.Needs["caller_job2"].Result)
|
||||
if assert.Len(t, callerJob3Task.Needs["caller_job2"].Outputs, 1) {
|
||||
assert.Equal(t, "r1j2_out_data_updated", callerJob3Task.Needs["caller_job2"].Outputs["r1_out"])
|
||||
}
|
||||
}
|
||||
defaultRunner.execTask(t, callerJob3Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
attempt2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{RunID: runID, Attempt: 2})
|
||||
assert.Equal(t, actions_model.StatusSuccess, attempt2.Status)
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, run.Status)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Cross-repo reusable workflow with collaborative owner", func(t *testing.T) {
|
||||
// libRepo: private, owned by user2.
|
||||
libAPIRepo := createActionsTestRepo(t, user2Token, "reusable-lib-private", true)
|
||||
libRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: libAPIRepo.ID})
|
||||
createRepoWorkflowFile(t, user2, user2Token, libRepo, ".gitea/workflows/reusable_lib.yaml",
|
||||
`name: ReusableLib
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
from:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
lib_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hello-${{ inputs.from }}
|
||||
`)
|
||||
|
||||
// consumerRepo: private, owned by user4.
|
||||
consumerAPIRepo := createActionsTestRepo(t, user4Token, "workflow-call-cross-repo", true)
|
||||
consumerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: consumerAPIRepo.ID})
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, consumerRepo.OwnerName, consumerRepo.Name, "mock-cross-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
createRepoWorkflowFile(t, user4, user4Token, consumerRepo, ".gitea/workflows/cross-caller.yaml",
|
||||
`name: CrossCaller
|
||||
on: push
|
||||
jobs:
|
||||
cross_job:
|
||||
uses: user2/reusable-lib-private/.gitea/workflows/reusable_lib.yaml@main
|
||||
with:
|
||||
from: 'consumer'
|
||||
`)
|
||||
|
||||
// Phase 1: no grant. The cross-repo read check fails, and NO ActionRun row gets persisted.
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRun{RepoID: consumerRepo.ID}))
|
||||
runner.fetchNoTask(t)
|
||||
|
||||
// Phase 2: user2 (libRepo owner) adds user4 (consumer owner) as a Collaborative Owner of libRepo.
|
||||
addCollabReq := NewRequestWithValues(t, "POST",
|
||||
fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", libRepo.OwnerName, libRepo.Name),
|
||||
map[string]string{"collaborative_owner": user4.Name})
|
||||
user2Session.MakeRequest(t, addCollabReq, http.StatusOK)
|
||||
|
||||
// Phase 3: trigger the workflow again
|
||||
createRepoWorkflowFile(t, user4, user4Token, consumerRepo, "marker.txt", "trigger after grant")
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: consumerRepo.ID})
|
||||
crossJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run.ID, JobID: "cross_job"})
|
||||
assert.True(t, crossJob.IsReusableCaller)
|
||||
assert.True(t, crossJob.IsExpanded)
|
||||
assert.Equal(t, actions_model.StatusWaiting, crossJob.Status)
|
||||
|
||||
libJobTask := runner.fetchTask(t)
|
||||
_, fetchedLibJob, _ := getTaskAndJobAndRunByTaskID(t, libJobTask.Id)
|
||||
assert.Equal(t, "lib_job", fetchedLibJob.JobID)
|
||||
assert.Equal(t, crossJob.ID, fetchedLibJob.ParentJobID)
|
||||
assert.Equal(t, consumerRepo.ID, fetchedLibJob.RepoID)
|
||||
payload := getWorkflowCallPayloadFromTask(t, libJobTask)
|
||||
if assert.Len(t, payload.Inputs, 1) {
|
||||
assert.Equal(t, "consumer", payload.Inputs["from"])
|
||||
}
|
||||
crossJob = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: crossJob.ID})
|
||||
assert.Equal(t, actions_model.StatusRunning, crossJob.Status)
|
||||
runner.execTask(t, libJobTask, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
|
||||
crossJob = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: crossJob.ID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, crossJob.Status)
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run.ID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, run.Status)
|
||||
})
|
||||
|
||||
t.Run("Public caller denied private target even with collaborative owner", func(t *testing.T) {
|
||||
// Isolates the run.Repo.IsPrivate gate: a public caller must be denied a private target even with a
|
||||
// collaborative-owner grant, since allowing it would expose private workflow content in a public run.
|
||||
|
||||
// libRepo: private, owned by user2.
|
||||
libAPIRepo := createActionsTestRepo(t, user2Token, "reusable-lib-public-denied", true)
|
||||
libRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: libAPIRepo.ID})
|
||||
createRepoWorkflowFile(t, user2, user2Token, libRepo, ".gitea/workflows/reusable_lib.yaml",
|
||||
`name: ReusableLib
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lib_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hello
|
||||
`)
|
||||
|
||||
// Grant first: user2 adds user4 as a collaborative owner of the private libRepo, so the grant is
|
||||
// satisfied and the public-caller gate is the only thing that can deny access.
|
||||
addCollabReq := NewRequestWithValues(t, "POST",
|
||||
fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", libRepo.OwnerName, libRepo.Name),
|
||||
map[string]string{"collaborative_owner": user4.Name})
|
||||
user2Session.MakeRequest(t, addCollabReq, http.StatusOK)
|
||||
|
||||
// consumerRepo: public, owned by user4.
|
||||
consumerAPIRepo := createActionsTestRepo(t, user4Token, "workflow-call-public-denied", false)
|
||||
consumerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: consumerAPIRepo.ID})
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, consumerRepo.OwnerName, consumerRepo.Name, "mock-public-denied-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
createRepoWorkflowFile(t, user4, user4Token, consumerRepo, ".gitea/workflows/cross-caller.yaml",
|
||||
`name: CrossCaller
|
||||
on: push
|
||||
jobs:
|
||||
cross_job:
|
||||
uses: user2/reusable-lib-public-denied/.gitea/workflows/reusable_lib.yaml@main
|
||||
`)
|
||||
|
||||
// Denied: the cross-repo read check fails for the public caller, so NO ActionRun is persisted and no task is dispatched.
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRun{RepoID: consumerRepo.ID}))
|
||||
runner.fetchNoTask(t)
|
||||
})
|
||||
|
||||
t.Run("Cross-repo callee with same-repo nested uses", func(t *testing.T) {
|
||||
// A same-repo `uses: ./...` inside a cross-repo reusable callee must resolve relative to the callee's own repo (matching GitHub's behavior), not the original triggering repo.
|
||||
|
||||
// Place a util.yaml with a distinguishable job name in BOTH repos to detect mis-resolution.
|
||||
|
||||
libAPIRepo := createActionsTestRepo(t, user2Token, "reusable-lib-nested", false)
|
||||
libRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: libAPIRepo.ID})
|
||||
createRepoWorkflowFile(t, user2, user2Token, libRepo, ".gitea/workflows/util.yaml",
|
||||
`name: UtilLib
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
util_lib_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo from-lib
|
||||
`)
|
||||
createRepoWorkflowFile(t, user2, user2Token, libRepo, ".gitea/workflows/lib.yaml",
|
||||
`name: LibNested
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
call_util_in_lib:
|
||||
uses: ./.gitea/workflows/util.yaml
|
||||
`)
|
||||
|
||||
consumerAPIRepo := createActionsTestRepo(t, user4Token, "consumer-nested-uses", false)
|
||||
consumerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: consumerAPIRepo.ID})
|
||||
|
||||
// A *different* util.yaml in the consumer repo: if `./` mis-resolves we'd see this job's name.
|
||||
createRepoWorkflowFile(t, user4, user4Token, consumerRepo, ".gitea/workflows/util.yaml",
|
||||
`name: UtilConsumer
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
util_consumer_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo from-consumer
|
||||
`)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, consumerRepo.OwnerName, consumerRepo.Name, "mock-nested-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
createRepoWorkflowFile(t, user4, user4Token, consumerRepo, ".gitea/workflows/caller.yaml",
|
||||
`name: NestedCaller
|
||||
on: push
|
||||
jobs:
|
||||
cross_job:
|
||||
uses: user2/reusable-lib-nested/.gitea/workflows/lib.yaml@main
|
||||
`)
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: consumerRepo.ID})
|
||||
crossJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run.ID, JobID: "cross_job"})
|
||||
assert.True(t, crossJob.IsReusableCaller)
|
||||
assert.True(t, crossJob.IsExpanded)
|
||||
|
||||
// cross_job's children come from libRepo/lib.yaml - their source must be libRepo + libRepo's commit.
|
||||
libHead, err := gitrepo.GetBranchCommitID(t.Context(), libRepo, libRepo.DefaultBranch)
|
||||
require.NoError(t, err)
|
||||
callUtilJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run.ID, JobID: "call_util_in_lib", ParentJobID: crossJob.ID})
|
||||
assert.True(t, callUtilJob.IsReusableCaller)
|
||||
assert.Equal(t, libRepo.ID, callUtilJob.WorkflowSourceRepoID)
|
||||
assert.Equal(t, libHead, callUtilJob.WorkflowSourceCommitSHA)
|
||||
|
||||
// call_util_in_lib has `uses: ./.gitea/workflows/util.yaml`, so its children should come from libRepo/util.yaml
|
||||
assert.True(t, callUtilJob.IsExpanded)
|
||||
unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: run.ID, JobID: "util_lib_job", ParentJobID: callUtilJob.ID})
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunJob{RunID: run.ID, JobID: "util_consumer_job"})
|
||||
})
|
||||
|
||||
t.Run("Missing callee file", func(t *testing.T) {
|
||||
// A caller workflow references a callee path that does not exist in the repo.
|
||||
|
||||
apiRepo := createActionsTestRepo(t, user2Token, "caller-missing-callee", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
|
||||
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/caller.yaml",
|
||||
`name: Caller
|
||||
on: push
|
||||
jobs:
|
||||
plain_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'job'
|
||||
call_missing:
|
||||
uses: ./.gitea/workflows/does-not-exist.yml
|
||||
`)
|
||||
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
|
||||
})
|
||||
|
||||
t.Run("Fork PR with secrets: inherit does not leak base repo secrets", func(t *testing.T) {
|
||||
// user2 owns the base repo, configures a secret, and registers a reusable workflow that declares a required secret.
|
||||
// The caller workflow uses `secrets: inherit`.
|
||||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "fork-pr-inherit-test", 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)
|
||||
|
||||
// Real secret that must never reach a fork PR task.
|
||||
req := NewRequestWithJSON(t, "PUT",
|
||||
fmt.Sprintf("/api/v1/repos/%s/%s/actions/secrets/leaked_secret", baseRepo.OwnerName, baseRepo.Name),
|
||||
api.CreateOrUpdateSecretOption{Data: "MUST-NOT-LEAK"}).AddTokenAuth(user2Token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-fork-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
createRepoWorkflowFile(t, user2, user2Token, baseRepo, ".gitea/workflows/reusable.yaml",
|
||||
`name: Reusable
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
leaked_secret:
|
||||
|
||||
jobs:
|
||||
callee:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo
|
||||
`)
|
||||
createRepoWorkflowFile(t, user2, user2Token, baseRepo, ".gitea/workflows/caller.yaml",
|
||||
`name: Caller
|
||||
on: pull_request
|
||||
jobs:
|
||||
call_reusable:
|
||||
uses: ./.gitea/workflows/reusable.yaml
|
||||
secrets: inherit
|
||||
`)
|
||||
|
||||
// user4 forks
|
||||
req = NewRequestWithJSON(t, "POST",
|
||||
fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
|
||||
&api.CreateForkOption{Name: new("fork-pr-inherit-test-fork")}).AddTokenAuth(user4Token)
|
||||
resp := MakeRequest(t, req, http.StatusAccepted)
|
||||
apiForkRepo := DecodeJSON(t, resp, &api.Repository{})
|
||||
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 pushes a change on the fork and opens a PR to base
|
||||
doAPICreateFile(user4APICtx, "user4-fix.txt", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "user4/branch",
|
||||
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("fix")),
|
||||
})(t)
|
||||
doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":user4/branch")(t)
|
||||
|
||||
// Approve the fork PR run.
|
||||
forkRun := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID})
|
||||
assert.True(t, forkRun.IsForkPullRequest)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/approve", baseRepo.OwnerName, baseRepo.Name, forkRun.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
task := runner.fetchTask(t)
|
||||
_, taskJob, taskRun := getTaskAndJobAndRunByTaskID(t, task.Id)
|
||||
assert.Equal(t, "callee", taskJob.JobID)
|
||||
assert.Equal(t, forkRun.ID, taskRun.ID)
|
||||
|
||||
// Only the auto-issued tokens should be present. The user-defined `leaked_secret` must not appear.
|
||||
assert.Contains(t, task.Secrets, "GITEA_TOKEN")
|
||||
assert.Contains(t, task.Secrets, "GITHUB_TOKEN")
|
||||
assert.NotContains(t, task.Secrets, "leaked_secret")
|
||||
for name, value := range task.Secrets {
|
||||
assert.NotEqual(t, "MUST-NOT-LEAK", value, "secret %q leaked the base repo's secret value into a fork PR task", name)
|
||||
}
|
||||
|
||||
runner.execTask(t, task, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
})
|
||||
|
||||
t.Run("Caller alternates expanding across attempts", func(t *testing.T) {
|
||||
apiRepo := createActionsTestRepo(t, user2Token, "caller-walkback-test", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-walkback-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
// Scenario:
|
||||
// attempt 1: gate succeeds -> caller expands -> inner runs (records inner.AttemptJobID = N)
|
||||
// attempt 2: rerun gate, mock Failure -> caller is Skipped without expanding (no children inserted)
|
||||
// attempt 3: rerun gate, mock Success -> caller expands again -> inner.AttemptJobID must equal N
|
||||
|
||||
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/lib.yaml",
|
||||
`name: Lib
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
inner:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo inner
|
||||
`)
|
||||
createRepoWorkflowFile(t, user2, user2Token, repo, ".gitea/workflows/main.yaml",
|
||||
`name: Main
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/main.yaml'
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo gate
|
||||
|
||||
caller:
|
||||
needs: [gate]
|
||||
uses: ./.gitea/workflows/lib.yaml
|
||||
`)
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID})
|
||||
runID := run.ID
|
||||
|
||||
latestAttempt := func() *actions_model.ActionRunAttempt {
|
||||
r := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
return unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunAttempt{ID: r.LatestAttemptID})
|
||||
}
|
||||
jobInLatest := func(jobID string) *actions_model.ActionRunJob {
|
||||
a := latestAttempt()
|
||||
return unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: a.ID, JobID: jobID})
|
||||
}
|
||||
|
||||
// attempt 1: gate Success -> caller expands -> inner runs
|
||||
gate1Task := runner.fetchTask(t)
|
||||
_, gate1, _ := getTaskAndJobAndRunByTaskID(t, gate1Task.Id)
|
||||
assert.Equal(t, "gate", gate1.JobID)
|
||||
runner.execTask(t, gate1Task, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
|
||||
inner1Task := runner.fetchTask(t)
|
||||
_, inner1, _ := getTaskAndJobAndRunByTaskID(t, inner1Task.Id)
|
||||
assert.Equal(t, "inner", inner1.JobID)
|
||||
innerAttemptJobID := inner1.AttemptJobID
|
||||
callerAttempt1 := jobInLatest("caller")
|
||||
assert.True(t, callerAttempt1.IsExpanded)
|
||||
assert.Equal(t, callerAttempt1.ID, inner1.ParentJobID)
|
||||
runner.execTask(t, inner1Task, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, run.Status)
|
||||
|
||||
// attempt 2: rerun gate, mock Failure -> caller stays unexpanded (Skipped)
|
||||
gateLatest := jobInLatest("gate")
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", repo.OwnerName, repo.Name, runID, gateLatest.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
gate2Task := runner.fetchTask(t)
|
||||
_, gate2, _ := getTaskAndJobAndRunByTaskID(t, gate2Task.Id)
|
||||
assert.Equal(t, "gate", gate2.JobID)
|
||||
runner.execTask(t, gate2Task, &mockTaskOutcome{result: runnerv1.Result_RESULT_FAILURE})
|
||||
|
||||
runner.fetchNoTask(t) // no inner because caller did not expand
|
||||
attempt2 := latestAttempt()
|
||||
assert.Equal(t, actions_model.StatusFailure, attempt2.Status)
|
||||
callerAttempt2 := jobInLatest("caller")
|
||||
assert.Equal(t, actions_model.StatusSkipped, callerAttempt2.Status)
|
||||
assert.False(t, callerAttempt2.IsExpanded)
|
||||
assert.Equal(t, 0, unittest.GetCount(t, &actions_model.ActionRunJob{RunID: runID, RunAttemptID: attempt2.ID, JobID: "inner"}))
|
||||
|
||||
// attempt 3: rerun gate, mock Success -> caller expands and inner reuses attempt 1's AttemptJobID
|
||||
gateLatest = jobInLatest("gate")
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", repo.OwnerName, repo.Name, runID, gateLatest.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
gate3Task := runner.fetchTask(t)
|
||||
runner.execTask(t, gate3Task, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
|
||||
inner3Task := runner.fetchTask(t)
|
||||
_, inner3, _ := getTaskAndJobAndRunByTaskID(t, inner3Task.Id)
|
||||
assert.Equal(t, "inner", inner3.JobID)
|
||||
assert.Equal(t, innerAttemptJobID, inner3.AttemptJobID)
|
||||
runner.execTask(t, inner3Task, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
|
||||
assert.Equal(t, actions_model.StatusSuccess, run.Status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// token must belong to u (the commit identity) and have write access to repo. Reuse the caller's
|
||||
// existing token rather than logging in per call, which would re-run bcrypt password verification each time.
|
||||
func createRepoWorkflowFile(t *testing.T, u *user_model.User, token string, repo *repo_model.Repository, treePath, content string) {
|
||||
opts := getWorkflowCreateFileOptions(u, repo.DefaultBranch, "create "+treePath, content)
|
||||
createWorkflowFile(t, token, repo.OwnerName, repo.Name, treePath, opts)
|
||||
}
|
||||
|
||||
func getWorkflowCallPayloadFromTask(t *testing.T, runnerTask *runnerv1.Task) *api.WorkflowCallPayload {
|
||||
eventJSON, err := runnerTask.GetContext().Fields["event"].GetStructValue().MarshalJSON()
|
||||
assert.NoError(t, err)
|
||||
var payload api.WorkflowCallPayload
|
||||
assert.NoError(t, json.Unmarshal(eventJSON, &payload))
|
||||
return &payload
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
actions_web "gitea.dev/routers/web/repo/actions"
|
||||
|
||||
"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)
|
||||
viewResp := DecodeJSON(t, resp, &actions_web.ViewResponse{})
|
||||
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-actions-view-url="/%s/%s/actions/runs/%d"`, user2.Name, repo.Name, 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())
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsRunnerModify(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, db.DeleteAllRecords("action_runner"))
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{OwnerID: user2.ID, Name: "user2-runner", TokenHash: "a", UUID: "a"})
|
||||
user2Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{OwnerID: user2.ID, Name: "user2-runner"})
|
||||
userWebURL := "/user/settings/actions/runners"
|
||||
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{OwnerID: org3.ID, Name: "org3-runner", TokenHash: "b", UUID: "b"}))
|
||||
org3Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{OwnerID: org3.ID, Name: "org3-runner"})
|
||||
orgWebURL := "/org/org3/settings/actions/runners"
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{RepoID: repo1.ID, Name: "repo1-runner", TokenHash: "c", UUID: "c"})
|
||||
repo1Runner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{RepoID: repo1.ID, Name: "repo1-runner"})
|
||||
repoWebURL := "/user2/repo1/settings/actions/runners"
|
||||
|
||||
_ = actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "global-runner", TokenHash: "d", UUID: "d"})
|
||||
globalRunner := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "global-runner"})
|
||||
adminWebURL := "/-/admin/actions/runners"
|
||||
|
||||
sessionAdmin := loginUser(t, "user1")
|
||||
sessionUser2 := loginUser(t, user2.Name)
|
||||
|
||||
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{
|
||||
"description": description,
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
|
||||
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})
|
||||
}
|
||||
|
||||
t.Run("UpdateUserRunner", func(t *testing.T) {
|
||||
theRunner := user2Runner
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
t.Skip("Admin can update any runner (not right but not too bad)")
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateOrgRunner", func(t *testing.T) {
|
||||
theRunner := org3Runner
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
t.Skip("Admin can update any runner (not right but not too bad)")
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateRepoRunner", func(t *testing.T) {
|
||||
theRunner := repo1Runner
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
t.Skip("Admin can update any runner (not right but not too bad)")
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateGlobalRunner", func(t *testing.T) {
|
||||
theRunner := globalRunner
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theRunner.ID)
|
||||
})
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateSuccess", func(t *testing.T) {
|
||||
t.Run("User", func(t *testing.T) {
|
||||
assertSuccess(t, sessionUser2, userWebURL, user2Runner.ID)
|
||||
})
|
||||
t.Run("Org", func(t *testing.T) {
|
||||
assertSuccess(t, sessionAdmin, orgWebURL, org3Runner.ID)
|
||||
})
|
||||
t.Run("Repo", func(t *testing.T) {
|
||||
assertSuccess(t, sessionUser2, repoWebURL, repo1Runner.ID)
|
||||
})
|
||||
t.Run("Admin", func(t *testing.T) {
|
||||
assertSuccess(t, sessionAdmin, adminWebURL, globalRunner.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("BulkAction", func(t *testing.T) {
|
||||
// Previous subtests deleted all runners; create a fresh set scoped to this subtest.
|
||||
require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "bulk-runner-1", TokenHash: "e", UUID: "e"}))
|
||||
require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "bulk-runner-2", TokenHash: "f", UUID: "f"}))
|
||||
require.NoError(t, actions_model.CreateRunner(ctx, &actions_model.ActionRunner{Name: "bulk-runner-3", TokenHash: "g", UUID: "g"}))
|
||||
r1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "bulk-runner-1"})
|
||||
r2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "bulk-runner-2"})
|
||||
r3 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{Name: "bulk-runner-3"})
|
||||
allIDs := []int64{r1.ID, r2.ID, r3.ID}
|
||||
bulkURL := adminWebURL + "/bulk"
|
||||
doBulk := func(t *testing.T, sess *TestSession, action string, ids []int64, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", bulkURL, map[string]string{
|
||||
"action": action,
|
||||
"ids": strings.Join(base.Int64sToStrings(ids), ","),
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
t.Run("NonAdminForbidden", func(t *testing.T) {
|
||||
doBulk(t, sessionUser2, "disable", allIDs, http.StatusForbidden)
|
||||
for _, id := range allIDs {
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.False(t, v.IsDisabled, "runner %d should not have been disabled", id)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidAction", func(t *testing.T) {
|
||||
doBulk(t, sessionAdmin, "evict", allIDs, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("DisableEnable", func(t *testing.T) {
|
||||
doBulk(t, sessionAdmin, "disable", allIDs, http.StatusOK)
|
||||
for _, id := range allIDs {
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.True(t, v.IsDisabled, "runner %d should be disabled", id)
|
||||
}
|
||||
doBulk(t, sessionAdmin, "enable", allIDs, http.StatusOK)
|
||||
for _, id := range allIDs {
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.False(t, v.IsDisabled, "runner %d should be enabled", id)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
doBulk(t, sessionAdmin, "delete", allIDs, http.StatusOK)
|
||||
for _, id := range allIDs {
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pingv1 "gitea.dev/actions-proto-go/ping/v1"
|
||||
"gitea.dev/actions-proto-go/ping/v1/pingv1connect"
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
"gitea.dev/actions-proto-go/runner/v1/runnerv1connect"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type mockRunner struct {
|
||||
client *mockRunnerClient
|
||||
}
|
||||
|
||||
type mockRunnerClient struct {
|
||||
pingServiceClient pingv1connect.PingServiceClient
|
||||
runnerServiceClient runnerv1connect.RunnerServiceClient
|
||||
}
|
||||
|
||||
func newMockRunner() *mockRunner {
|
||||
client := newMockRunnerClient("", "")
|
||||
return &mockRunner{client: client}
|
||||
}
|
||||
|
||||
func newMockRunnerClient(uuid, token string) *mockRunnerClient {
|
||||
baseURL := setting.AppURL + "api/actions"
|
||||
|
||||
opt := connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||
if uuid != "" {
|
||||
req.Header().Set("x-runner-uuid", uuid)
|
||||
}
|
||||
if token != "" {
|
||||
req.Header().Set("x-runner-token", token)
|
||||
}
|
||||
return next(ctx, req)
|
||||
}
|
||||
}))
|
||||
|
||||
client := &mockRunnerClient{
|
||||
pingServiceClient: pingv1connect.NewPingServiceClient(http.DefaultClient, baseURL, opt),
|
||||
runnerServiceClient: runnerv1connect.NewRunnerServiceClient(http.DefaultClient, baseURL, opt),
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (r *mockRunner) doPing(t *testing.T) {
|
||||
resp, err := r.client.pingServiceClient.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{
|
||||
Data: "mock-runner",
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello, mock-runner!", resp.Msg.Data)
|
||||
}
|
||||
|
||||
func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string, ephemeral bool) {
|
||||
r.doPing(t)
|
||||
resp, err := r.client.runnerServiceClient.Register(t.Context(), connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: name,
|
||||
Token: token,
|
||||
Version: "mock-runner-version",
|
||||
Labels: labels,
|
||||
Ephemeral: ephemeral,
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token)
|
||||
}
|
||||
|
||||
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, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
registrationToken := DecodeJSON(t, resp, &struct {
|
||||
Token string `json:"token"`
|
||||
}{})
|
||||
r.doRegister(t, runnerName, registrationToken.Token, labels, ephemeral)
|
||||
}
|
||||
|
||||
func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task {
|
||||
task := r.tryFetchTask(t, timeout...)
|
||||
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...)
|
||||
require.Nil(t, task, "a task is fetched")
|
||||
}
|
||||
|
||||
const defaultFetchTaskTimeout = 1 * time.Second
|
||||
|
||||
func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task {
|
||||
fetchTimeout := defaultFetchTaskTimeout
|
||||
if len(timeout) > 0 {
|
||||
fetchTimeout = timeout[0]
|
||||
}
|
||||
ddl := time.Now().Add(fetchTimeout)
|
||||
var task *runnerv1.Task
|
||||
for time.Now().Before(ddl) {
|
||||
task, _ = r.fetchTaskOnce(t, 0)
|
||||
if task != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
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
|
||||
logRows []*runnerv1.LogRow
|
||||
}
|
||||
|
||||
func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) {
|
||||
for idx, lr := range outcome.logRows {
|
||||
resp, err := r.client.runnerServiceClient.UpdateLog(t.Context(), connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||
TaskId: task.Id,
|
||||
Index: int64(idx),
|
||||
Rows: []*runnerv1.LogRow{lr},
|
||||
NoMore: idx == len(outcome.logRows)-1,
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, idx+1, resp.Msg.AckIndex)
|
||||
}
|
||||
sentOutputKeys := make([]string, 0, len(outcome.outputs))
|
||||
for outputKey, outputValue := range outcome.outputs {
|
||||
resp, err := r.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
Result: runnerv1.Result_RESULT_UNSPECIFIED,
|
||||
},
|
||||
Outputs: map[string]string{outputKey: outputValue},
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
sentOutputKeys = append(sentOutputKeys, outputKey)
|
||||
assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs)
|
||||
}
|
||||
resp, err := r.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
Result: outcome.result,
|
||||
StoppedAt: timestamppb.Now(),
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, outcome.result, resp.Msg.State.Result)
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
unit_model "gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/migration"
|
||||
api "gitea.dev/modules/structs"
|
||||
mirror_service "gitea.dev/services/mirror"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScheduleUpdate(t *testing.T) {
|
||||
t.Run("Push", testScheduleUpdatePush)
|
||||
t.Run("PullMerge", testScheduleUpdatePullMerge)
|
||||
t.Run("DisableAndEnableActionsUnit", testScheduleUpdateDisableAndEnableActionsUnit)
|
||||
t.Run("ArchiveAndUnarchive", testScheduleUpdateArchiveAndUnarchive)
|
||||
t.Run("MirrorSync", testScheduleUpdateMirrorSync)
|
||||
}
|
||||
|
||||
func testScheduleUpdatePush(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) {
|
||||
newCron := "30 5 * * 1,3"
|
||||
pushScheduleChange(t, u, repo, newCron)
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
return branch.CommitID, newCron
|
||||
})
|
||||
}
|
||||
|
||||
func testScheduleUpdatePullMerge(t *testing.T) {
|
||||
newBranchName := "feat1"
|
||||
workflowTreePath := ".gitea/workflows/actions-schedule.yml"
|
||||
workflowContent := `name: actions-schedule
|
||||
on:
|
||||
schedule:
|
||||
- cron: '@every 2m' # update to 2m
|
||||
jobs:
|
||||
job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'schedule workflow'
|
||||
`
|
||||
|
||||
mergeStyles := []repo_model.MergeStyle{
|
||||
repo_model.MergeStyleMerge,
|
||||
repo_model.MergeStyleRebase,
|
||||
repo_model.MergeStyleRebaseMerge,
|
||||
repo_model.MergeStyleSquash,
|
||||
repo_model.MergeStyleFastForwardOnly,
|
||||
}
|
||||
|
||||
for _, mergeStyle := range mergeStyles {
|
||||
t.Run(string(mergeStyle), func(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) {
|
||||
// update workflow file
|
||||
_, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{
|
||||
NewBranch: newBranchName,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "update",
|
||||
TreePath: workflowTreePath,
|
||||
ContentReader: strings.NewReader(workflowContent),
|
||||
},
|
||||
},
|
||||
Message: "update workflow schedule",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create pull request
|
||||
apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// merge pull request
|
||||
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"
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run(string(repo_model.MergeStyleManuallyMerged), func(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) {
|
||||
// enable manual-merge
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
HasPullRequests: new(true),
|
||||
AllowManualMerge: new(true),
|
||||
})(t)
|
||||
|
||||
// update workflow file
|
||||
fileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user, &files_service.ChangeRepoFilesOptions{
|
||||
NewBranch: newBranchName,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "update",
|
||||
TreePath: workflowTreePath,
|
||||
ContentReader: strings.NewReader(workflowContent),
|
||||
},
|
||||
},
|
||||
Message: "update workflow schedule",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// merge and push
|
||||
dstPath := t.TempDir()
|
||||
u.Path = repo.FullName() + ".git"
|
||||
u.User = url.UserPassword(repo.OwnerName, userPassword)
|
||||
doGitClone(dstPath, u)(t)
|
||||
doGitMerge(dstPath, "origin/"+newBranchName)(t)
|
||||
doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t)
|
||||
|
||||
// create pull request
|
||||
apiPull, err := doAPICreatePullRequest(testContext, repo.OwnerName, repo.Name, repo.DefaultBranch, newBranchName)(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// merge pull request manually
|
||||
doAPIManuallyMergePullRequest(testContext, repo.OwnerName, repo.Name, fileResp.Commit.SHA, apiPull.Index)(t)
|
||||
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
||||
assert.Equal(t, issues_model.PullRequestStatusManuallyMerged, pull.Status)
|
||||
return pull.MergedCommitID, "@every 2m"
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testScheduleUpdateMirrorSync(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) {
|
||||
// create mirror repo
|
||||
opts := migration.MigrateOptions{
|
||||
RepoName: "actions-schedule-mirror",
|
||||
Description: "Test mirror for actions-schedule",
|
||||
Private: false,
|
||||
Mirror: true,
|
||||
CloneAddr: repo.CloneLinkGeneral(t.Context()).HTTPS,
|
||||
}
|
||||
mirrorRepo, err := repo_service.CreateRepositoryDirectly(t.Context(), user, user, repo_service.CreateRepoOptions{
|
||||
Name: opts.RepoName,
|
||||
Description: opts.Description,
|
||||
IsPrivate: opts.Private,
|
||||
IsMirror: opts.Mirror,
|
||||
DefaultBranch: repo.DefaultBranch,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, mirrorRepo.IsMirror)
|
||||
mirrorRepo, err = repo_service.MigrateRepositoryGitData(t.Context(), user, mirrorRepo, opts, nil)
|
||||
assert.NoError(t, err)
|
||||
mirrorContext := NewAPITestContext(t, user.Name, mirrorRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
// enable actions unit for mirror repo
|
||||
assert.False(t, mirrorRepo.UnitEnabled(t.Context(), unit_model.TypeActions))
|
||||
doAPIEditRepository(mirrorContext, &api.EditRepoOption{
|
||||
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})
|
||||
assert.Equal(t, "@every 1m", scheduleSpec.Spec)
|
||||
|
||||
// update remote repo
|
||||
newCron := "30 5,17 * * 2,4"
|
||||
pushScheduleChange(t, u, repo, newCron)
|
||||
repoDefaultBranch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// sync
|
||||
ok := mirror_service.SyncPullMirror(t.Context(), mirrorRepo.ID)
|
||||
assert.True(t, ok)
|
||||
mirrorRepoDefaultBranch, err := git_model.GetBranch(t.Context(), mirrorRepo.ID, mirrorRepo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, repoDefaultBranch.CommitID, mirrorRepoDefaultBranch.CommitID)
|
||||
|
||||
// check updated schedule
|
||||
actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
|
||||
scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
|
||||
assert.Equal(t, newCron, scheduleSpec.Spec)
|
||||
|
||||
return repoDefaultBranch.CommitID, newCron
|
||||
})
|
||||
}
|
||||
|
||||
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: new(true),
|
||||
})(t)
|
||||
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
Archived: new(false),
|
||||
})(t)
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
return branch.CommitID, "@every 1m"
|
||||
})
|
||||
}
|
||||
|
||||
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: new(false),
|
||||
})(t)
|
||||
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
HasActions: new(true),
|
||||
})(t)
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
return branch.CommitID, "@every 1m"
|
||||
})
|
||||
}
|
||||
|
||||
type scheduleUpdateTrigger func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string)
|
||||
|
||||
func doTestScheduleUpdate(t *testing.T, updateTrigger scheduleUpdateTrigger) {
|
||||
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)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-schedule", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
assert.NoError(t, repo.LoadAttributes(t.Context()))
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(httpContext)(t)
|
||||
|
||||
wfTreePath := ".gitea/workflows/actions-schedule.yml"
|
||||
wfFileContent := `name: actions-schedule
|
||||
on:
|
||||
schedule:
|
||||
- cron: '@every 1m'
|
||||
jobs:
|
||||
job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'schedule workflow'
|
||||
`
|
||||
|
||||
opts1 := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||
apiFileResp := createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts1)
|
||||
|
||||
actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: apiFileResp.Commit.SHA})
|
||||
scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID})
|
||||
assert.Equal(t, "@every 1m", scheduleSpec.Spec)
|
||||
|
||||
commitID, expectedSpec := updateTrigger(t, u, httpContext, user2, repo)
|
||||
|
||||
actionSchedule = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: repo.ID, CommitSHA: commitID})
|
||||
scheduleSpec = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: repo.ID, ScheduleID: actionSchedule.ID})
|
||||
assert.Equal(t, expectedSpec, scheduleSpec.Spec)
|
||||
})
|
||||
}
|
||||
|
||||
func pushScheduleChange(t *testing.T, u *url.URL, repo *repo_model.Repository, newCron string) {
|
||||
workflowTreePath := ".gitea/workflows/actions-schedule.yml"
|
||||
workflowContent := `name: actions-schedule
|
||||
on:
|
||||
schedule:
|
||||
- cron: '` + newCron + `'
|
||||
jobs:
|
||||
job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'schedule workflow'
|
||||
`
|
||||
|
||||
dstPath := t.TempDir()
|
||||
u.Path = repo.FullName() + ".git"
|
||||
u.User = url.UserPassword(repo.OwnerName, userPassword)
|
||||
doGitClone(dstPath, u)(t)
|
||||
doGitCheckoutWriteFileCommit(localGitAddCommitOptions{
|
||||
LocalRepoPath: dstPath,
|
||||
CheckoutBranch: repo.DefaultBranch,
|
||||
TreeFilePath: workflowTreePath,
|
||||
TreeFileContent: workflowContent,
|
||||
})(t)
|
||||
doGitPushTestRepository(dstPath, "origin", repo.DefaultBranch)(t)
|
||||
}
|
||||
@@ -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 "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/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)
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsVariables(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
require.NoError(t, db.DeleteAllRecords("action_variable"))
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
_, _ = actions_model.InsertVariable(ctx, user2.ID, 0, "VAR", "user2-var", "user2-var-description")
|
||||
user2Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: user2.ID, Name: "VAR"})
|
||||
userWebURL := "/user/settings/actions/variables"
|
||||
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
|
||||
_, _ = actions_model.InsertVariable(ctx, org3.ID, 0, "VAR", "org3-var", "org3-var-description")
|
||||
org3Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{OwnerID: org3.ID, Name: "VAR"})
|
||||
orgWebURL := "/org/org3/settings/actions/variables"
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
_, _ = actions_model.InsertVariable(ctx, 0, repo1.ID, "VAR", "repo1-var", "repo1-var-description")
|
||||
repo1Var := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{RepoID: repo1.ID, Name: "VAR"})
|
||||
repoWebURL := "/user2/repo1/settings/actions/variables"
|
||||
|
||||
_, _ = actions_model.InsertVariable(ctx, 0, 0, "VAR", "global-var", "global-var-description")
|
||||
globalVar := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{Name: "VAR", Data: "global-var"})
|
||||
adminWebURL := "/-/admin/actions/variables"
|
||||
|
||||
sessionAdmin := loginUser(t, "user1")
|
||||
sessionUser2 := loginUser(t, user2.Name)
|
||||
|
||||
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{
|
||||
"name": "VAR",
|
||||
"data": data,
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id))
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
|
||||
doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusNotFound)
|
||||
doDelete(t, sess, baseURL, id, http.StatusNotFound)
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id})
|
||||
assert.Contains(t, v.Data, "-var")
|
||||
}
|
||||
|
||||
assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
|
||||
doUpdate(t, sess, baseURL, id, "ChangedData", http.StatusOK)
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionVariable{ID: id})
|
||||
assert.Equal(t, "ChangedData", v.Data)
|
||||
doDelete(t, sess, baseURL, id, http.StatusOK)
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionVariable{ID: id})
|
||||
}
|
||||
|
||||
t.Run("UpdateUserVar", func(t *testing.T) {
|
||||
theVar := user2Var
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateOrgVar", func(t *testing.T) {
|
||||
theVar := org3Var
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateRepoVar", func(t *testing.T) {
|
||||
theVar := repo1Var
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromAdmin", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, adminWebURL, theVar.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateGlobalVar", func(t *testing.T) {
|
||||
theVar := globalVar
|
||||
t.Run("FromOrg", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, orgWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromUser", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, userWebURL, theVar.ID)
|
||||
})
|
||||
t.Run("FromRepo", func(t *testing.T) {
|
||||
assertDenied(t, sessionAdmin, repoWebURL, theVar.ID)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateSuccess", func(t *testing.T) {
|
||||
t.Run("User", func(t *testing.T) {
|
||||
assertSuccess(t, sessionUser2, userWebURL, user2Var.ID)
|
||||
})
|
||||
t.Run("Org", func(t *testing.T) {
|
||||
assertSuccess(t, sessionAdmin, orgWebURL, org3Var.ID)
|
||||
})
|
||||
t.Run("Repo", func(t *testing.T) {
|
||||
assertSuccess(t, sessionUser2, repoWebURL, repo1Var.ID)
|
||||
})
|
||||
t.Run("Admin", func(t *testing.T) {
|
||||
assertSuccess(t, sessionAdmin, adminWebURL, globalVar.ID)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/system"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/setting/config"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminConfig(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAdminViewUsers(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
req := NewRequest(t, "GET", "/-/admin/users")
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
session = loginUser(t, "user2")
|
||||
req = NewRequest(t, "GET", "/-/admin/users")
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAdminViewUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
req := NewRequest(t, "GET", "/-/admin/users/1")
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
session = loginUser(t, "user2")
|
||||
req = NewRequest(t, "GET", "/-/admin/users/1")
|
||||
session.MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAdminEditUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
testSuccessfulEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"})
|
||||
}
|
||||
|
||||
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")
|
||||
req := NewRequestWithValues(t, "POST", "/-/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit", map[string]string{
|
||||
"user_name": formData.Name,
|
||||
"login_name": formData.LoginName,
|
||||
"login_type": "0-0",
|
||||
"email": formData.Email,
|
||||
})
|
||||
|
||||
session.MakeRequest(t, req, headerCode)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: formData.ID})
|
||||
assert.Equal(t, formData.Name, user.Name)
|
||||
assert.Equal(t, formData.LoginName, user.LoginName)
|
||||
assert.Equal(t, formData.Email, user.Email)
|
||||
}
|
||||
|
||||
func TestAdminDeleteUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
usersToDelete := []struct {
|
||||
userID int64
|
||||
purge bool
|
||||
}{
|
||||
{
|
||||
userID: 2,
|
||||
purge: true,
|
||||
},
|
||||
{
|
||||
userID: 8,
|
||||
},
|
||||
}
|
||||
|
||||
for _, entry := range usersToDelete {
|
||||
t.Run(fmt.Sprintf("DeleteUser%d", entry.userID), func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: entry.userID})
|
||||
assert.NotNil(t, user)
|
||||
|
||||
var query string
|
||||
if entry.purge {
|
||||
query = "?purge=true"
|
||||
}
|
||||
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query))
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
assertUserDeleted(t, entry.userID)
|
||||
unittest.CheckConsistencyFor(t, &user_model.User{})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type uploadArtifactResponse struct {
|
||||
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||
}
|
||||
|
||||
type getUploadArtifactRequest struct {
|
||||
Type string
|
||||
Name string
|
||||
RetentionDays int64
|
||||
}
|
||||
|
||||
func prepareTestEnvActionsArtifacts(t *testing.T) func() {
|
||||
t.Helper()
|
||||
f := tests.PrepareTestEnv(t, 1)
|
||||
tests.PrepareArtifactsStorage(t)
|
||||
return f
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadSingleFile(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
||||
Type: "actions_storage",
|
||||
Name: "artifact",
|
||||
}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
||||
assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
// get upload url
|
||||
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc-2.txt"
|
||||
|
||||
// upload artifact chunk
|
||||
body := strings.Repeat("C", 1024)
|
||||
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
||||
SetHeader("Content-Range", "bytes 0-1023/1024").
|
||||
SetHeader("x-tfs-filelength", "1024").
|
||||
SetHeader("x-actions-results-md5", "XVlf820rMInUi64wmMi6EA==") // base64(md5(body))
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
t.Logf("Create artifact confirm")
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-single").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadInvalidHash(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
// artifact id 54321 not exist
|
||||
url := "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts/8e5b948a454515dbabfc7eb718ddddddd/upload?itemPath=artifact/abc.txt"
|
||||
body := strings.Repeat("A", 1024)
|
||||
req := NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
||||
SetHeader("Content-Range", "bytes 0-1023/1024").
|
||||
SetHeader("x-tfs-filelength", "1024").
|
||||
SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
assert.Contains(t, resp.Body.String(), "Invalid artifact hash")
|
||||
}
|
||||
|
||||
func TestActionsArtifactConfirmUploadWithoutName(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
req := NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
assert.Contains(t, resp.Body.String(), "artifact name is empty")
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadWithoutToken(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/1/artifacts", nil)
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
type (
|
||||
listArtifactsResponseItem struct {
|
||||
Name string `json:"name"`
|
||||
FileContainerResourceURL string `json:"fileContainerResourceUrl"`
|
||||
}
|
||||
listArtifactsResponse struct {
|
||||
Count int64 `json:"count"`
|
||||
Value []listArtifactsResponseItem `json:"value"`
|
||||
}
|
||||
downloadArtifactResponseItem struct {
|
||||
Path string `json:"path"`
|
||||
ItemType string `json:"itemType"`
|
||||
ContentLocation string `json:"contentLocation"`
|
||||
}
|
||||
downloadArtifactResponse struct {
|
||||
Value []downloadArtifactResponseItem `json:"value"`
|
||||
}
|
||||
)
|
||||
|
||||
func TestActionsArtifactDownload(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
||||
assert.Equal(t, int64(2), listResp.Count)
|
||||
|
||||
// Return list might be in any order. Get one file.
|
||||
var artifactIdx int
|
||||
for i, artifact := range listResp.Value {
|
||||
if artifact.Name == "artifact-download" {
|
||||
artifactIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotNil(t, artifactIdx)
|
||||
assert.Equal(t, "artifact-download", listResp.Value[artifactIdx].Name)
|
||||
assert.Contains(t, listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
idx := strings.Index(listResp.Value[artifactIdx].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := listResp.Value[artifactIdx].FileContainerResourceURL[idx:] + "?itemPath=artifact-download"
|
||||
req = NewRequest(t, "GET", url).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
downloadResp := DecodeJSON(t, resp, &downloadArtifactResponse{})
|
||||
assert.Len(t, downloadResp.Value, 1)
|
||||
assert.Equal(t, "artifact-download/abc.txt", downloadResp.Value[0].Path)
|
||||
assert.Equal(t, "file", downloadResp.Value[0].ItemType)
|
||||
assert.Contains(t, downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url = downloadResp.Value[0].ContentLocation[idx:]
|
||||
req = NewRequest(t, "GET", url).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
body := strings.Repeat("A", 1024)
|
||||
assert.Equal(t, body, resp.Body.String())
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadMultipleFile(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
const testArtifactName = "multi-files"
|
||||
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
||||
Type: "actions_storage",
|
||||
Name: testArtifactName,
|
||||
}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
||||
assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
type uploadingFile struct {
|
||||
Path string
|
||||
Content string
|
||||
MD5 string
|
||||
}
|
||||
|
||||
files := []uploadingFile{
|
||||
{
|
||||
Path: "abc-3.txt",
|
||||
Content: strings.Repeat("D", 1024),
|
||||
MD5: "9nqj7E8HZmfQtPifCJ5Zww==",
|
||||
},
|
||||
{
|
||||
Path: "xyz/def-2.txt",
|
||||
Content: strings.Repeat("E", 1024),
|
||||
MD5: "/s1kKvxeHlUX85vaTaVxuA==",
|
||||
},
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
// get upload url
|
||||
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=" + testArtifactName + "/" + f.Path
|
||||
|
||||
// upload artifact chunk
|
||||
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(f.Content)).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
||||
SetHeader("Content-Range", "bytes 0-1023/1024").
|
||||
SetHeader("x-tfs-filelength", "1024").
|
||||
SetHeader("x-actions-results-md5", f.MD5) // base64(md5(body))
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
t.Logf("Create artifact confirm")
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName="+testArtifactName).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestActionsArtifactDownloadMultiFiles(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
const testArtifactName = "multi-file-download"
|
||||
|
||||
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
||||
assert.Equal(t, int64(2), listResp.Count)
|
||||
|
||||
var fileContainerResourceURL string
|
||||
for _, v := range listResp.Value {
|
||||
if v.Name == testArtifactName {
|
||||
fileContainerResourceURL = v.FileContainerResourceURL
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Contains(t, fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
idx := strings.Index(fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := fileContainerResourceURL[idx:] + "?itemPath=" + testArtifactName
|
||||
req = NewRequest(t, "GET", url).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
downloadResp := DecodeJSON(t, resp, &downloadArtifactResponse{})
|
||||
assert.Len(t, downloadResp.Value, 2)
|
||||
|
||||
downloads := [][]string{{"multi-file-download/abc.txt", "B"}, {"multi-file-download/xyz/def.txt", "C"}}
|
||||
for _, v := range downloadResp.Value {
|
||||
var bodyChar string
|
||||
var path string
|
||||
for _, d := range downloads {
|
||||
if v.Path == d[0] {
|
||||
path = d[0]
|
||||
bodyChar = d[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
value := v
|
||||
assert.Equal(t, path, value.Path)
|
||||
assert.Equal(t, "file", value.ItemType)
|
||||
assert.Contains(t, value.ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
|
||||
idx = strings.Index(value.ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url = value.ContentLocation[idx:]
|
||||
req = NewRequest(t, "GET", url).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, strings.Repeat(bodyChar, 1024), resp.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsArtifactUploadWithRetentionDays(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
||||
Type: "actions_storage",
|
||||
Name: "artifact-retention-days",
|
||||
RetentionDays: 9,
|
||||
}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
||||
assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
|
||||
assert.Contains(t, uploadResp.FileContainerResourceURL, "?retentionDays=9")
|
||||
|
||||
// get upload url
|
||||
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := uploadResp.FileContainerResourceURL[idx:] + "&itemPath=artifact-retention-days/abc.txt"
|
||||
|
||||
// upload artifact chunk
|
||||
body := strings.Repeat("A", 1024)
|
||||
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
||||
SetHeader("Content-Range", "bytes 0-1023/1024").
|
||||
SetHeader("x-tfs-filelength", "1024").
|
||||
SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
t.Logf("Create artifact confirm")
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-retention-days").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestActionsArtifactOverwrite(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
{
|
||||
// download old artifact uploaded by tests above, it should 1024 A
|
||||
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
||||
|
||||
idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := listResp.Value[0].FileContainerResourceURL[idx:] + "?itemPath=artifact-download"
|
||||
req = NewRequest(t, "GET", url).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
downloadResp := DecodeJSON(t, resp, &downloadArtifactResponse{})
|
||||
|
||||
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url = downloadResp.Value[0].ContentLocation[idx:]
|
||||
req = NewRequest(t, "GET", url).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
body := strings.Repeat("A", 1024)
|
||||
assert.Equal(t, resp.Body.String(), body)
|
||||
}
|
||||
|
||||
{
|
||||
// upload same artifact, it uses 4096 B
|
||||
req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
|
||||
Type: "actions_storage",
|
||||
Name: "artifact-download",
|
||||
}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
||||
|
||||
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact-download/abc.txt"
|
||||
body := strings.Repeat("B", 4096)
|
||||
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
|
||||
SetHeader("Content-Range", "bytes 0-4095/4096").
|
||||
SetHeader("x-tfs-filelength", "4096").
|
||||
SetHeader("x-actions-results-md5", "wUypcJFeZCK5T6r4lfqzqg==") // base64(md5(body))
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-download").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
{
|
||||
// download artifact again, it should 4096 B
|
||||
req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
||||
|
||||
var uploadedItem listArtifactsResponseItem
|
||||
for _, item := range listResp.Value {
|
||||
if item.Name == "artifact-download" {
|
||||
uploadedItem = item
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.Equal(t, "artifact-download", uploadedItem.Name)
|
||||
|
||||
idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url := uploadedItem.FileContainerResourceURL[idx:] + "?itemPath=artifact-download"
|
||||
req = NewRequest(t, "GET", url).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
downloadResp := DecodeJSON(t, resp, &downloadArtifactResponse{})
|
||||
|
||||
idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
|
||||
url = downloadResp.Value[0].ContentLocation[idx:]
|
||||
req = NewRequest(t, "GET", url).
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
body := strings.Repeat("B", 4096)
|
||||
assert.Equal(t, resp.Body.String(), body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionRunAttemptArtifact(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(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)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-run-attempt-artifact", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(httpContext)(t)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, repo.OwnerName, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
wfTreePath := ".gitea/workflows/run-attempt-artifact.yml"
|
||||
wfFileContent := `name: run-attempt-artifact
|
||||
on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'job1'
|
||||
`
|
||||
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, opts)
|
||||
|
||||
urlStr := fmt.Sprintf("/%s/%s/actions/run?workflow=%s", user2.Name, repo.Name, "run-attempt-artifact.yml")
|
||||
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
"ref": "refs/heads/main",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
t.Run("testActionRunAttemptArtifactV3", func(t *testing.T) {
|
||||
testActionRunAttemptArtifactV3(t, repo, session, runner)
|
||||
})
|
||||
|
||||
t.Run("testActionRunAttemptArtifactV4", func(t *testing.T) {
|
||||
testActionRunAttemptArtifactV4(t, repo, session, runner)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testActionRunAttemptArtifactV3(t *testing.T, repo *repo_model.Repository, session *TestSession, runner *mockRunner) {
|
||||
// first run
|
||||
task1 := runner.fetchTask(t)
|
||||
_, job1, run := getTaskAndJobAndRunByTaskID(t, task1.Id)
|
||||
require.NotZero(t, job1.RunAttemptID)
|
||||
taskToken1 := task1.Context.GetFields()["gitea_runtime_token"].GetStringValue()
|
||||
require.NotEmpty(t, taskToken1)
|
||||
uploadTestArtifactFile(t, run.ID, taskToken1, "artifact-attempt-1", "attempt-1.txt", strings.Repeat("A", 32))
|
||||
uploadTestArtifactFile(t, run.ID, taskToken1, "artifact-shared", "shared.txt", strings.Repeat("C", 32))
|
||||
attempt1Names := listArtifactNamesForRun(t, run.ID, taskToken1)
|
||||
assert.ElementsMatch(t, []string{"artifact-attempt-1", "artifact-shared"}, attempt1Names)
|
||||
|
||||
runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS}) // complete first run
|
||||
|
||||
// rerun
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", repo.OwnerName, repo.Name, run.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
task2 := runner.fetchTask(t)
|
||||
_, job2, _ := getTaskAndJobAndRunByTaskID(t, task2.Id)
|
||||
require.NotZero(t, job2.RunAttemptID)
|
||||
assert.NotEqual(t, job1.RunAttemptID, job2.RunAttemptID)
|
||||
taskToken2 := task2.Context.GetFields()["gitea_runtime_token"].GetStringValue()
|
||||
require.NotEmpty(t, taskToken2)
|
||||
uploadTestArtifactFile(t, run.ID, taskToken2, "artifact-attempt-2", "attempt-2.txt", strings.Repeat("B", 32))
|
||||
uploadTestArtifactFile(t, run.ID, taskToken2, "artifact-shared", "shared.txt", strings.Repeat("D", 32))
|
||||
attempt2Names := listArtifactNamesForRun(t, run.ID, taskToken2)
|
||||
assert.ElementsMatch(t, []string{"artifact-attempt-2", "artifact-shared"}, attempt2Names)
|
||||
assert.NotContains(t, attempt2Names, "artifact-attempt-1")
|
||||
|
||||
// "artifact-attempt-1" belongs to the first attempt, so the rerun token cannot access it
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/artifacts/%x/download_url?itemPath=artifact-attempt-1", run.ID, md5.Sum([]byte("artifact-attempt-1")))).
|
||||
AddTokenAuth(taskToken2)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// "artifact-shared" for each attempt has different content
|
||||
sharedContent1 := downloadArtifactFileContentByAttempt(t, session, repo.OwnerName, repo.Name, run.ID, "artifact-shared", 1, "shared.txt")
|
||||
assert.Equal(t, strings.Repeat("C", 32), sharedContent1)
|
||||
sharedContent2 := downloadArtifactFileContentByAttempt(t, session, repo.OwnerName, repo.Name, run.ID, "artifact-shared", 2, "shared.txt")
|
||||
assert.Equal(t, strings.Repeat("D", 32), sharedContent2)
|
||||
}
|
||||
|
||||
func uploadTestArtifactFile(t *testing.T, runID int64, authToken, artifactName, fileName, content string) {
|
||||
t.Helper()
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/artifacts", runID), getUploadArtifactRequest{
|
||||
Type: "actions_storage",
|
||||
Name: artifactName,
|
||||
}).AddTokenAuth(authToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
uploadResp := DecodeJSON(t, resp, &uploadArtifactResponse{})
|
||||
|
||||
idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
|
||||
uploadURL := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=" + artifactName + "/" + fileName
|
||||
contentLen := strconv.Itoa(len(content))
|
||||
contentMD5 := md5.Sum([]byte(content))
|
||||
req = NewRequestWithBody(t, "PUT", uploadURL, strings.NewReader(content)).
|
||||
AddTokenAuth(authToken).
|
||||
SetHeader("Content-Range", fmt.Sprintf("bytes 0-%d/%d", len(content)-1, len(content))).
|
||||
SetHeader("x-tfs-filelength", contentLen).
|
||||
SetHeader("x-actions-results-md5", base64.StdEncoding.EncodeToString(contentMD5[:]))
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "PATCH", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/artifacts?artifactName=%s", runID, artifactName)).
|
||||
AddTokenAuth(authToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func listArtifactNamesForRun(t *testing.T, runID int64, taskToken string) []string {
|
||||
t.Helper()
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/actions_pipeline/_apis/pipelines/workflows/%d/artifacts", runID)).
|
||||
AddTokenAuth(taskToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &listArtifactsResponse{})
|
||||
|
||||
names := make([]string, 0, len(listResp.Value))
|
||||
for _, item := range listResp.Value {
|
||||
names = append(names, item.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func downloadArtifactFileContentByAttempt(t *testing.T, session *TestSession, owner, repo string, runID int64, artifactName string, attempt int64, fileName string) string {
|
||||
t.Helper()
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/%s?attempt=%d", owner, repo, runID, url.PathEscape(artifactName), attempt))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
zr, err := zip.NewReader(bytes.NewReader(resp.Body.Bytes()), int64(resp.Body.Len()))
|
||||
require.NoError(t, err)
|
||||
for _, f := range zr.File {
|
||||
if f.Name != fileName {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(rc)
|
||||
rc.Close()
|
||||
require.NoError(t, err)
|
||||
return string(content)
|
||||
}
|
||||
|
||||
require.FailNowf(t, "artifact file not found", "artifact %q attempt %d does not contain file %q", artifactName, attempt, fileName)
|
||||
return ""
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/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)
|
||||
|
||||
u := DecodeJSON(t, resp, &api.User{})
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIActionsWorkflowRun(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
t.Run("GetWorkflowRun", testAPIActionsGetWorkflowRun)
|
||||
t.Run("GetWorkflowJob", testAPIActionsGetWorkflowJob)
|
||||
t.Run("ListUserWorkflows", testAPIActionsListUserWorkflows)
|
||||
t.Run("ListRepoWorkflows", testAPIActionsListRepoWorkflows)
|
||||
t.Run("DeleteRunCheckPermission", testAPIActionsDeleteRunCheckPermission)
|
||||
t.Run("DeleteRunRunning", testAPIActionsDeleteRunRunning)
|
||||
t.Run("DeleteRunGeneral", testAPIActionsDeleteRunGeneral)
|
||||
|
||||
t.Run("RerunWorkflowRun", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
testAPIActionsRerunWorkflowRun(t)
|
||||
})
|
||||
t.Run("RerunWorkflowJob", func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
testAPIActionsRerunWorkflowJob(t)
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIActionsGetWorkflowRun(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
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)
|
||||
jobList := DecodeJSON(t, resp, &api.ActionWorkflowJobsResponse{})
|
||||
|
||||
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) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/jobs/198198", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/jobs/198", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/jobs/196", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunCheckPermission(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)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunGeneral(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
testAPIActionsDeleteRunListArtifacts(t, repo, token, 2)
|
||||
testAPIActionsDeleteRunListTasks(t, repo, token, true)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNoContent)
|
||||
|
||||
testAPIActionsDeleteRunListArtifacts(t, repo, token, 0)
|
||||
testAPIActionsDeleteRunListTasks(t, repo, token, false)
|
||||
testAPIActionsDeleteRun(t, repo, token, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunRunning(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)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRun(t *testing.T, repo *repo_model.Repository, token string, expected int) {
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunListArtifacts(t *testing.T, repo *repo_model.Repository, token string, artifacts int) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/artifacts", repo.FullName())).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &api.ActionArtifactsResponse{})
|
||||
assert.Len(t, listResp.Entries, artifacts)
|
||||
}
|
||||
|
||||
func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository, token string, expected bool) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/tasks", repo.FullName())).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
listResp := DecodeJSON(t, resp, &api.ActionTaskResponse{})
|
||||
|
||||
findTask1 := false
|
||||
findTask2 := false
|
||||
for _, entry := range listResp.Entries {
|
||||
if entry.ID == 53 {
|
||||
findTask1 = true
|
||||
continue
|
||||
}
|
||||
if entry.ID == 54 {
|
||||
findTask2 = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
assert.Equal(t, expected, findTask1)
|
||||
assert.Equal(t, expected, findTask2)
|
||||
}
|
||||
|
||||
func testAPIActionsRerunWorkflowRun(t *testing.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)
|
||||
rerunResp := DecodeJSON(t, resp, &api.ActionWorkflowRun{})
|
||||
|
||||
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)
|
||||
latestAttempt, hasLatestAttempt, err := run.GetLatestAttempt(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.True(t, hasLatestAttempt)
|
||||
|
||||
job198 := getLatestAttemptJobByTemplateJobID(t, 795, 198)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job198.Status)
|
||||
assert.Equal(t, latestAttempt.Attempt, job198.Attempt)
|
||||
assert.Equal(t, int64(0), job198.TaskID)
|
||||
|
||||
job199 := getLatestAttemptJobByTemplateJobID(t, 795, 199)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
|
||||
assert.Equal(t, latestAttempt.Attempt, job199.Attempt)
|
||||
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) {
|
||||
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)
|
||||
rerunResp := DecodeJSON(t, resp, &api.ActionWorkflowJob{})
|
||||
|
||||
job199Rerun := getLatestAttemptJobByTemplateJobID(t, 795, 199)
|
||||
assert.Equal(t, job199Rerun.ID, 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)
|
||||
latestAttempt, hasLatestAttempt, err := run.GetLatestAttempt(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.True(t, hasLatestAttempt)
|
||||
|
||||
job198Rerun := getLatestAttemptJobByTemplateJobID(t, 795, 198)
|
||||
assert.Equal(t, actions_model.StatusSuccess, job198Rerun.Status)
|
||||
assert.Equal(t, latestAttempt.Attempt, job198Rerun.Attempt)
|
||||
assert.Equal(t, int64(0), job198Rerun.TaskID)
|
||||
assert.Equal(t, int64(53), job198Rerun.SourceTaskID)
|
||||
|
||||
job199Rerun = getLatestAttemptJobByTemplateJobID(t, 795, 199)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job199Rerun.Status)
|
||||
assert.Equal(t, latestAttempt.Attempt, job199Rerun.Attempt)
|
||||
assert.Equal(t, int64(0), job199Rerun.TaskID)
|
||||
assert.Equal(t, int64(0), job199Rerun.SourceTaskID)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIActionsListUserWorkflows(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
|
||||
|
||||
t.Run("Runs", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/actions/runs").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
runs := DecodeJSON(t, resp, &api.ActionWorkflowRunsResponse{})
|
||||
|
||||
assert.Positive(t, runs.TotalCount)
|
||||
assert.NotEmpty(t, runs.Entries)
|
||||
|
||||
for _, run := range runs.Entries {
|
||||
assert.NotEmpty(t, run.DisplayTitle, "display_title should be populated")
|
||||
assert.NotNil(t, run.Repository, "repository should be populated via batch loading")
|
||||
assert.NotEmpty(t, run.Repository.FullName, "repository full_name should be populated")
|
||||
assert.NotNil(t, run.TriggerActor, "trigger_actor should be populated via batch loading")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Jobs", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/actions/jobs").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
jobs := DecodeJSON(t, resp, &api.ActionWorkflowJobsResponse{})
|
||||
|
||||
assert.Positive(t, jobs.TotalCount)
|
||||
assert.NotEmpty(t, jobs.Entries)
|
||||
|
||||
for _, job := range jobs.Entries {
|
||||
assert.NotEmpty(t, job.Name, "job name should be populated")
|
||||
assert.NotEmpty(t, job.HTMLURL, "html_url should be populated via batch-loaded repo")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JobsDefaultOrderAsc", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/actions/jobs").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
jobs := DecodeJSON(t, resp, &api.ActionWorkflowJobsResponse{})
|
||||
|
||||
assert.GreaterOrEqual(t, len(jobs.Entries), 2, "need at least 2 jobs to verify ordering")
|
||||
for i := 1; i < len(jobs.Entries); i++ {
|
||||
assert.Less(t, jobs.Entries[i-1].ID, jobs.Entries[i].ID,
|
||||
"jobs should be ordered by ID ascending by default")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("JobsOrderedByIDDesc", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/actions/jobs?sort=id&order=desc").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
jobs := DecodeJSON(t, resp, &api.ActionWorkflowJobsResponse{})
|
||||
|
||||
assert.GreaterOrEqual(t, len(jobs.Entries), 2, "need at least 2 jobs to verify ordering")
|
||||
for i := 1; i < len(jobs.Entries); i++ {
|
||||
assert.Greater(t, jobs.Entries[i-1].ID, jobs.Entries[i].ID,
|
||||
"jobs should be ordered by ID descending")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIActionsListRepoWorkflows(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs", repo.FullName())).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
runs := DecodeJSON(t, resp, &api.ActionWorkflowRunsResponse{})
|
||||
|
||||
assert.Positive(t, runs.TotalCount)
|
||||
assert.NotEmpty(t, runs.Entries)
|
||||
|
||||
for _, run := range runs.Entries {
|
||||
assert.NotNil(t, run.Repository, "repository should be populated from ctx.Repo")
|
||||
assert.Equal(t, repo.FullName(), run.Repository.FullName, "repository full_name should match")
|
||||
assert.NotNil(t, run.TriggerActor, "trigger_actor should be populated")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIActionsRunner(t *testing.T) {
|
||||
t.Run("AdminRunner", testActionsRunnerAdmin)
|
||||
t.Run("UserRunner", testActionsRunnerUser)
|
||||
t.Run("OwnerRunner", testActionsRunnerOwner)
|
||||
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"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
req := NewRequest(t, "POST", "/api/v1/admin/actions/runners/registration-token").AddTokenAuth(token)
|
||||
tokenResp := MakeRequest(t, req, http.StatusOK)
|
||||
registrationToken := DecodeJSON(t, tokenResp, &struct {
|
||||
Token string `json:"token"`
|
||||
}{})
|
||||
assert.NotEmpty(t, registrationToken.Token)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/admin/actions/runners").AddTokenAuth(token)
|
||||
runnerListResp := MakeRequest(t, req, http.StatusOK)
|
||||
runnerList := DecodeJSON(t, runnerListResp, &api.ActionRunnersResponse{})
|
||||
|
||||
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 })
|
||||
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 := DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
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)
|
||||
|
||||
runner := DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
|
||||
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)
|
||||
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Verify runner deletion
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func testActionsRunnerUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
userUsername := "user1"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteUser)
|
||||
req := NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(token)
|
||||
tokenResp := MakeRequest(t, req, http.StatusOK)
|
||||
registrationToken := DecodeJSON(t, tokenResp, &struct {
|
||||
Token string `json:"token"`
|
||||
}{})
|
||||
assert.NotEmpty(t, registrationToken.Token)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/user/actions/runners").AddTokenAuth(token)
|
||||
runnerListResp := MakeRequest(t, req, http.StatusOK)
|
||||
runnerList := DecodeJSON(t, runnerListResp, &api.ActionRunnersResponse{})
|
||||
|
||||
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)
|
||||
assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
|
||||
|
||||
// Verify get the runner by id
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
runnerResp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
runner := DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
|
||||
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)
|
||||
runner = DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
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)
|
||||
|
||||
// Verify runner deletion
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testActionsRunnerOwner(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("GetRunner", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
|
||||
// Verify get the runner by id with read scope
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
|
||||
runnerResp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
runner := DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("Access", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteOrganization)
|
||||
req := NewRequest(t, "POST", "/api/v1/orgs/org3/actions/runners/registration-token").AddTokenAuth(token)
|
||||
tokenResp := MakeRequest(t, req, http.StatusOK)
|
||||
registrationToken := DecodeJSON(t, tokenResp, &struct {
|
||||
Token string `json:"token"`
|
||||
}{})
|
||||
assert.NotEmpty(t, registrationToken.Token)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs/org3/actions/runners").AddTokenAuth(token)
|
||||
runnerListResp := MakeRequest(t, req, http.StatusOK)
|
||||
runnerList := DecodeJSON(t, runnerListResp, &api.ActionRunnersResponse{})
|
||||
|
||||
idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34347 })
|
||||
require.NotEqual(t, -1, idx)
|
||||
expectedRunner := runnerList.Entries[idx]
|
||||
|
||||
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)
|
||||
assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
|
||||
|
||||
// Verify get the runner by id
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||
runnerResp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
runner := DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
|
||||
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)
|
||||
runner = DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
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)
|
||||
|
||||
// Verify runner deletion
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeleteReadScopeForbidden", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
// Verify delete the runner by id is forbidden with read scope
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
|
||||
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)
|
||||
// Verify get the runner by id with read scope
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("GetAdminRunner", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
|
||||
// Verify get a runner by id of different entity is not found
|
||||
// runner.EditableInContext(ownerID, repoID) false
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34349)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeleteAdminRunner", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteOrganization)
|
||||
// Verify delete a runner by id of different entity is not found
|
||||
// runner.EditableInContext(ownerID, repoID) false
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34349)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func testActionsRunnerRepo(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("GetRunner", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
|
||||
// Verify get the runner by id with read scope
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
|
||||
runnerResp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
runner := DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
t.Run("Access", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequest(t, "POST", "/api/v1/repos/user2/repo1/actions/runners/registration-token").AddTokenAuth(token)
|
||||
tokenResp := MakeRequest(t, req, http.StatusOK)
|
||||
registrationToken := DecodeJSON(t, tokenResp, &struct {
|
||||
Token string `json:"token"`
|
||||
}{})
|
||||
assert.NotEmpty(t, registrationToken.Token)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/actions/runners").AddTokenAuth(token)
|
||||
runnerListResp := MakeRequest(t, req, http.StatusOK)
|
||||
runnerList := DecodeJSON(t, runnerListResp, &api.ActionRunnersResponse{})
|
||||
|
||||
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)
|
||||
assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
|
||||
|
||||
// Verify get the runner by id
|
||||
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)
|
||||
|
||||
runner := DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
|
||||
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)
|
||||
runner = DecodeJSON(t, runnerResp, &api.ActionRunner{})
|
||||
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)
|
||||
|
||||
// Verify runner deletion
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeleteReadScopeForbidden", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
// Verify delete the runner by id is forbidden with read scope
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
|
||||
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)
|
||||
// Verify get the runner by id with read scope
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("GetAdminRunnerNotFound", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
|
||||
// Verify get a runner by id of different entity is not found
|
||||
// runner.EditableInContext(ownerID, repoID) false
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeleteAdminRunnerNotFound", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
|
||||
// Verify delete a runner by id of different entity is not found
|
||||
// runner.EditableInContext(ownerID, repoID) false
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeleteAdminRunnerNotFoundUnknownID", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
|
||||
// Verify delete a runner by unknown id is not found
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 4384797347934)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIAdminOrgCreate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
|
||||
|
||||
t.Run("CreateOrg", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
org := api.CreateOrgOption{
|
||||
UserName: "user2_org",
|
||||
FullName: "User2's organization",
|
||||
Description: "This organization created by admin for user2",
|
||||
Website: "https://try.gitea.io",
|
||||
Location: "Shanghai",
|
||||
Visibility: "private",
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs", &org).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
apiOrg := DecodeJSON(t, resp, &api.Organization{})
|
||||
|
||||
assert.Equal(t, org.UserName, 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)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
||||
Name: org.UserName,
|
||||
LowerName: strings.ToLower(org.UserName),
|
||||
FullName: org.FullName,
|
||||
})
|
||||
})
|
||||
t.Run("CreateBadVisibility", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
org := api.CreateOrgOption{
|
||||
UserName: "user2_org",
|
||||
FullName: "User2's organization",
|
||||
Description: "This organization created by admin for user2",
|
||||
Website: "https://try.gitea.io",
|
||||
Location: "Shanghai",
|
||||
Visibility: "notvalid",
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs", &org).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
})
|
||||
t.Run("CreateNotAdmin", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
nonAdminUsername := "user2"
|
||||
session := loginUser(t, nonAdminUsername)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
|
||||
org := api.CreateOrgOption{
|
||||
UserName: "user2_org",
|
||||
FullName: "User2's organization",
|
||||
Description: "This organization created by admin for user2",
|
||||
Website: "https://try.gitea.io",
|
||||
Location: "Shanghai",
|
||||
Visibility: "public",
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs", &org).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/glob"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
// user1 is an admin user
|
||||
session := loginUser(t, "user1")
|
||||
keyOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
|
||||
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
|
||||
urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys", keyOwner.Name)
|
||||
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
|
||||
"title": "test-key",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
newPublicKey := DecodeJSON(t, resp, &api.PublicKey{})
|
||||
unittest.AssertExistsAndLoadBean(t, &asymkey_model.PublicKey{
|
||||
ID: newPublicKey.ID,
|
||||
Name: newPublicKey.Title,
|
||||
Fingerprint: newPublicKey.Fingerprint,
|
||||
OwnerID: keyOwner.ID,
|
||||
})
|
||||
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d", keyOwner.Name, newPublicKey.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
unittest.AssertNotExistsBean(t, &asymkey_model.PublicKey{ID: newPublicKey.ID})
|
||||
}
|
||||
|
||||
func TestAPIAdminDeleteMissingSSHKey(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// user1 is an admin user
|
||||
token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteAdmin)
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/admin/users/user1/keys/%d", unittest.NonexistentID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
normalUsername := "user2"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys", adminUsername)
|
||||
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
|
||||
"title": "test-key",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
newPublicKey := DecodeJSON(t, resp, &api.PublicKey{})
|
||||
|
||||
token = getUserToken(t, normalUsername, auth_model.AccessTokenScopeAll)
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d", adminUsername, newPublicKey.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAPISudoUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
normalUsername := "user2"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/user?sudo="+normalUsername).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
user := DecodeJSON(t, resp, &api.User{})
|
||||
|
||||
assert.Equal(t, normalUsername, user.UserName)
|
||||
}
|
||||
|
||||
func TestAPISudoUserForbidden(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
normalUsername := "user2"
|
||||
|
||||
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadAdmin)
|
||||
req := NewRequest(t, "GET", "/api/v1/user?sudo="+adminUsername).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAPIListUsers(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadAdmin)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/admin/users").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
users := DecodeJSON(t, resp, []api.User{})
|
||||
|
||||
found := false
|
||||
for _, user := range users {
|
||||
if user.UserName == adminUsername {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
assert.True(t, found)
|
||||
numberOfUsers := unittest.GetCount(t, &user_model.User{}, "type = 0")
|
||||
assert.Len(t, users, numberOfUsers)
|
||||
}
|
||||
|
||||
func TestAPIListUsersNotLoggedIn(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
req := NewRequest(t, "GET", "/api/v1/admin/users")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestAPIListUsersNonAdmin(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
nonAdminUsername := "user2"
|
||||
token := getUserToken(t, nonAdminUsername, auth_model.AccessTokenScopeAll)
|
||||
req := NewRequest(t, "GET", "/api/v1/admin/users").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAPICreateUserInvalidEmail(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
req := NewRequestWithValues(t, "POST", "/api/v1/admin/users", map[string]string{
|
||||
"email": "invalid_email@domain.com\r\n",
|
||||
"full_name": "invalid user",
|
||||
"login_name": "invalidUser",
|
||||
"must_change_password": "true",
|
||||
"password": "password",
|
||||
"send_notify": "true",
|
||||
"source_id": "0",
|
||||
"username": "invalidUser",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
func TestAPICreateAndDeleteUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
|
||||
req := NewRequestWithValues(
|
||||
t,
|
||||
"POST",
|
||||
"/api/v1/admin/users",
|
||||
map[string]string{
|
||||
"email": "deleteme@domain.com",
|
||||
"full_name": "delete me",
|
||||
"login_name": "deleteme",
|
||||
"must_change_password": "true",
|
||||
"password": "password",
|
||||
"send_notify": "true",
|
||||
"source_id": "0",
|
||||
"username": "deleteme",
|
||||
},
|
||||
).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
req = NewRequest(t, "DELETE", "/api/v1/admin/users/deleteme").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPIEditUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
urlStr := "/api/v1/admin/users/" + "user2"
|
||||
|
||||
fullNameToChange := "Full Name User 2"
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
// required
|
||||
"login_name": "user2",
|
||||
"source_id": "0",
|
||||
// to change
|
||||
"full_name": fullNameToChange,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
|
||||
assert.Equal(t, fullNameToChange, user2.FullName)
|
||||
|
||||
empty := ""
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
|
||||
LoginName: "user2",
|
||||
SourceID: 0,
|
||||
Email: &empty,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
errMap := make(map[string]any)
|
||||
json.Unmarshal(resp.Body.Bytes(), &errMap)
|
||||
assert.Equal(t, "e-mail invalid [email: ]", errMap["message"].(string))
|
||||
|
||||
user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
|
||||
assert.False(t, user2.IsRestricted)
|
||||
bTrue := true
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
|
||||
// required
|
||||
LoginName: "user2",
|
||||
SourceID: 0,
|
||||
// to change
|
||||
Restricted: &bTrue,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"})
|
||||
assert.True(t, user2.IsRestricted)
|
||||
}
|
||||
|
||||
func TestAPICreateRepoForUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
|
||||
req := NewRequestWithJSON(
|
||||
t,
|
||||
"POST",
|
||||
fmt.Sprintf("/api/v1/admin/users/%s/repos", adminUsername),
|
||||
&api.CreateRepoOption{
|
||||
Name: "admincreatedrepo",
|
||||
},
|
||||
).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
func TestAPIRenameUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
urlStr := fmt.Sprintf("/api/v1/admin/users/%s/rename", "user2")
|
||||
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
// required
|
||||
"new_name": "User2",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename", "User2")
|
||||
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
// required
|
||||
"new_name": "User2-2-2",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
// required
|
||||
"new_name": "user1",
|
||||
}).AddTokenAuth(token)
|
||||
// the old user name still be used by with a redirect
|
||||
MakeRequest(t, req, http.StatusTemporaryRedirect)
|
||||
|
||||
urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename", "User2-2-2")
|
||||
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
// required
|
||||
"new_name": "user1",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
// required
|
||||
"new_name": "user2",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPICron(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// user1 is an admin user
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadAdmin)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/admin/cron").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
assert.Equal(t, "29", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
crons := DecodeJSON(t, resp, []api.Cron{})
|
||||
assert.Len(t, crons, 29)
|
||||
})
|
||||
|
||||
t.Run("Execute", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
now := time.Now()
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
|
||||
// Archive cleanup is harmless, because in the test environment there are none
|
||||
// and is thus an NOOP operation and therefore doesn't interfere with any other
|
||||
// tests.
|
||||
req := NewRequest(t, "POST", "/api/v1/admin/cron/archive_cleanup").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Check for the latest run time for this cron, to ensure it has been run.
|
||||
req = NewRequest(t, "GET", "/api/v1/admin/cron").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
crons := DecodeJSON(t, resp, []api.Cron{})
|
||||
|
||||
for _, cron := range crons {
|
||||
if cron.Name == "archive_cleanup" {
|
||||
assert.True(t, now.Before(cron.Prev))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPICreateUser_NotAllowedEmailDomain(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("example.org")})()
|
||||
|
||||
adminUsername := "user1"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/api/v1/admin/users", map[string]string{
|
||||
"email": "allowedUser1@example1.org",
|
||||
"login_name": "allowedUser1",
|
||||
"username": "allowedUser1",
|
||||
"password": "allowedUser1_pass",
|
||||
"must_change_password": "true",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
assert.Equal(t, "the domain of user email allowedUser1@example1.org conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", resp.Header().Get("X-Gitea-Warning"))
|
||||
|
||||
req = NewRequest(t, "DELETE", "/api/v1/admin/users/allowedUser1").AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPIEditUser_NotAllowedEmailDomain(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("example.org")})()
|
||||
|
||||
adminUsername := "user1"
|
||||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
|
||||
urlStr := "/api/v1/admin/users/" + "user2"
|
||||
|
||||
newEmail := "user2@example1.com"
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
|
||||
LoginName: "user2",
|
||||
SourceID: 0,
|
||||
Email: &newEmail,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusBadRequest)
|
||||
errMap := make(map[string]string)
|
||||
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &errMap))
|
||||
assert.Equal(t, "the domain of user email user2@example1.com conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", errMap["message"])
|
||||
|
||||
originalEmail := "user2@example.org"
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{
|
||||
LoginName: "user2",
|
||||
SourceID: 0,
|
||||
Email: &originalEmail,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIAuth(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
req := NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user2")
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user2", "wrong-password")
|
||||
resp := MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/user").AddBasicAuth("user-not-exist")
|
||||
resp = MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/users/user2/repos").AddTokenAuth("Bearer wrong_token")
|
||||
resp = MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.Contains(t, resp.Body.String(), `{"message":"invalid username, password or token"`)
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branches/%s", branchName).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, NoExpectedStatus)
|
||||
if !exists {
|
||||
assert.Equal(t, http.StatusNotFound, resp.Code)
|
||||
return
|
||||
}
|
||||
assert.Equal(t, http.StatusOK, resp.Code)
|
||||
branch := DecodeJSON(t, resp, &api.Branch{})
|
||||
assert.Equal(t, branchName, branch.Name)
|
||||
assert.True(t, branch.UserCanPush)
|
||||
assert.True(t, branch.UserCanMerge)
|
||||
}
|
||||
|
||||
func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) *api.BranchProtection {
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branch_protections/%s", branchName).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, expectedHTTPStatus)
|
||||
|
||||
if resp.Code == http.StatusOK {
|
||||
branchProtection := DecodeJSON(t, resp, &api.BranchProtection{})
|
||||
assert.Equal(t, branchName, branchProtection.RuleName)
|
||||
return branchProtection
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func testAPICreateBranchProtection(t *testing.T, branchName string, expectedPriority, expectedHTTPStatus int) {
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{
|
||||
RuleName: branchName,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, expectedHTTPStatus)
|
||||
|
||||
if resp.Code == http.StatusCreated {
|
||||
branchProtection := DecodeJSON(t, resp, &api.BranchProtection{})
|
||||
assert.Equal(t, branchName, branchProtection.RuleName)
|
||||
assert.EqualValues(t, expectedPriority, branchProtection.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.BranchProtection, expectedHTTPStatus int) {
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/branch_protections/"+branchName, body).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, expectedHTTPStatus)
|
||||
|
||||
if resp.Code == http.StatusOK {
|
||||
branchProtection := DecodeJSON(t, resp, &api.BranchProtection{})
|
||||
assert.Equal(t, branchName, branchProtection.RuleName)
|
||||
}
|
||||
}
|
||||
|
||||
func testAPIDeleteBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branch_protections/%s", branchName).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, expectedHTTPStatus)
|
||||
}
|
||||
|
||||
func testAPIDeleteBranch(t *testing.T, branchName string, expectedHTTPStatus int) {
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branches/%s", branchName).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, expectedHTTPStatus)
|
||||
}
|
||||
|
||||
func TestAPIGetBranch(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
for _, test := range []struct {
|
||||
BranchName string
|
||||
Exists bool
|
||||
}{
|
||||
{"master", true},
|
||||
{"master/doesnotexist", false},
|
||||
{"feature/1", true},
|
||||
{"feature/1/doesnotexist", false},
|
||||
} {
|
||||
testAPIGetBranch(t, test.BranchName, test.Exists)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPICreateBranch(t *testing.T) {
|
||||
onGiteaRun(t, testAPICreateBranches)
|
||||
}
|
||||
|
||||
func testAPICreateBranches(t *testing.T, giteaURL *url.URL) {
|
||||
username := "user2"
|
||||
ctx := NewAPITestContext(t, username, "my-noo-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
giteaURL.Path = ctx.GitPath()
|
||||
|
||||
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
|
||||
testCases := []struct {
|
||||
OldBranch string
|
||||
NewBranch string
|
||||
ExpectedHTTPStatus int
|
||||
}{
|
||||
// Creating branch from default branch
|
||||
{
|
||||
OldBranch: "",
|
||||
NewBranch: "new_branch_from_default_branch",
|
||||
ExpectedHTTPStatus: http.StatusCreated,
|
||||
},
|
||||
// Creating branch from master
|
||||
{
|
||||
OldBranch: "master",
|
||||
NewBranch: "new_branch_from_master_1",
|
||||
ExpectedHTTPStatus: http.StatusCreated,
|
||||
},
|
||||
// Trying to create from master but already exists
|
||||
{
|
||||
OldBranch: "master",
|
||||
NewBranch: "new_branch_from_master_1",
|
||||
ExpectedHTTPStatus: http.StatusConflict,
|
||||
},
|
||||
// Trying to create from other branch (not default branch)
|
||||
// ps: it can't test the case-sensitive behavior here: the "BRANCH_2" can't be created by git on a case-insensitive filesystem, it makes the test fail quickly before the database code.
|
||||
// Suppose some users are running Gitea on a case-insensitive filesystem, it seems that it's unable to support case-sensitive branch names.
|
||||
{
|
||||
OldBranch: "new_branch_from_master_1",
|
||||
NewBranch: "branch_2",
|
||||
ExpectedHTTPStatus: http.StatusCreated,
|
||||
},
|
||||
// Trying to create from a branch which does not exist
|
||||
{
|
||||
OldBranch: "does_not_exist",
|
||||
NewBranch: "new_branch_from_non_existent",
|
||||
ExpectedHTTPStatus: http.StatusNotFound,
|
||||
},
|
||||
// Trying to create a branch with UTF8
|
||||
{
|
||||
OldBranch: "master",
|
||||
NewBranch: "test-👀",
|
||||
ExpectedHTTPStatus: http.StatusCreated,
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
session := ctx.Session
|
||||
t.Run(test.NewBranch, func(t *testing.T) {
|
||||
testAPICreateBranch(t, session, "user2", "my-noo-repo", test.OldBranch, test.NewBranch, test.ExpectedHTTPStatus)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBranch, newBranch string, status int) bool {
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+user+"/"+repo+"/branches", &api.CreateBranchRepoOption{
|
||||
BranchName: newBranch,
|
||||
OldBranchName: oldBranch,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, status)
|
||||
|
||||
branch := DecodeJSON(t, resp, &api.Branch{})
|
||||
|
||||
if resp.Result().StatusCode == http.StatusCreated {
|
||||
assert.Equal(t, newBranch, branch.Name)
|
||||
}
|
||||
|
||||
return resp.Result().StatusCode == status
|
||||
}
|
||||
|
||||
func TestAPIRenameBranch(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
t.Run("RenameBranchWithEmptyRepo", func(t *testing.T) {
|
||||
testAPIRenameBranch(t, "user10", "user10", "repo6", "master", "test", http.StatusNotFound)
|
||||
})
|
||||
t.Run("RenameBranchWithSameBranchNames", func(t *testing.T) {
|
||||
resp := testAPIRenameBranch(t, "user2", "user2", "repo1", "master", "master", http.StatusUnprocessableEntity)
|
||||
assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.")
|
||||
})
|
||||
t.Run("RenameBranchThatAlreadyExists", func(t *testing.T) {
|
||||
resp := testAPIRenameBranch(t, "user2", "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity)
|
||||
assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.")
|
||||
})
|
||||
t.Run("RenameBranchWithNonExistentBranch", func(t *testing.T) {
|
||||
resp := testAPIRenameBranch(t, "user2", "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound)
|
||||
assert.Contains(t, resp.Body.String(), "Branch doesn't exist.")
|
||||
})
|
||||
t.Run("RenameBranchWithNonAdminDoer", func(t *testing.T) {
|
||||
// don't allow default branch renaming
|
||||
resp := testAPIRenameBranch(t, "user40", "user2", "repo1", "master", "new-branch-name", http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "User must be a repo or site admin to rename default or protected branches.")
|
||||
|
||||
// don't allow protected branch renaming
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branches", &api.CreateBranchRepoOption{
|
||||
BranchName: "protected-branch",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
testAPICreateBranchProtection(t, "protected-branch", 1, http.StatusCreated)
|
||||
resp = testAPIRenameBranch(t, "user40", "user2", "repo1", "protected-branch", "new-branch-name", http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "User must be a repo or site admin to rename default or protected branches.")
|
||||
})
|
||||
t.Run("RenameBranchWithGlobedBasedProtectionRulesAndAdminAccess", func(t *testing.T) {
|
||||
// don't allow branch that falls under glob-based protection rules to be renamed
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{
|
||||
RuleName: "protected/**",
|
||||
EnablePush: true,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
from := "protected/1"
|
||||
req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branches", &api.CreateBranchRepoOption{
|
||||
BranchName: from,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
resp := testAPIRenameBranch(t, "user2", "user2", "repo1", from, "new-branch-name", http.StatusForbidden)
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
Name: to,
|
||||
}).AddTokenAuth(token)
|
||||
return MakeRequest(t, req, expectedHTTPStatus)
|
||||
}
|
||||
|
||||
func TestAPIBranchProtection(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("Basic", testAPIBranchProtectionBasic)
|
||||
t.Run("BypassAllowlistValidation", testAPIBranchProtectionBypassAllowlistValidation)
|
||||
}
|
||||
|
||||
func testAPIBranchProtectionBasic(t *testing.T) {
|
||||
// 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", 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)
|
||||
|
||||
testAPIGetBranchProtection(t, "master", http.StatusOK)
|
||||
testAPIEditBranchProtection(t, "master", &api.BranchProtection{
|
||||
EnablePush: true,
|
||||
}, http.StatusOK)
|
||||
|
||||
// enable status checks, require the "test1" check to pass
|
||||
testAPIEditBranchProtection(t, "master", &api.BranchProtection{
|
||||
EnableStatusCheck: true,
|
||||
StatusCheckContexts: []string{"test1"},
|
||||
}, http.StatusOK)
|
||||
bp := testAPIGetBranchProtection(t, "master", http.StatusOK)
|
||||
assert.True(t, bp.EnableStatusCheck)
|
||||
assert.Equal(t, []string{"test1"}, bp.StatusCheckContexts)
|
||||
|
||||
// disable status checks, clear the list of required checks
|
||||
testAPIEditBranchProtection(t, "master", &api.BranchProtection{
|
||||
EnableStatusCheck: false,
|
||||
StatusCheckContexts: []string{},
|
||||
}, http.StatusOK)
|
||||
bp = testAPIGetBranchProtection(t, "master", http.StatusOK)
|
||||
assert.False(t, bp.EnableStatusCheck)
|
||||
assert.Equal(t, []string{}, bp.StatusCheckContexts)
|
||||
|
||||
testAPIDeleteBranchProtection(t, "master", http.StatusNoContent)
|
||||
|
||||
// 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 testAPIBranchProtectionBypassAllowlistValidation(t *testing.T) {
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
t.Run("IgnoreInvalidBypassUsernamesWhenDisabled", func(t *testing.T) {
|
||||
ruleName := "bypass-disabled-invalid-user"
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.CreateBranchProtectionOption{
|
||||
RuleName: ruleName,
|
||||
EnableBypassAllowlist: false,
|
||||
BypassAllowlistUsernames: []string{"nonexistent-user"},
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
testAPIDeleteBranchProtection(t, ruleName, http.StatusNoContent)
|
||||
})
|
||||
|
||||
t.Run("IgnoreInvalidBypassTeamsWhenDisabled", func(t *testing.T) {
|
||||
ruleName := "bypass-disabled-invalid-team"
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/org3/repo3/branch_protections", &api.CreateBranchProtectionOption{
|
||||
RuleName: ruleName,
|
||||
EnableBypassAllowlist: false,
|
||||
BypassAllowlistTeams: []string{"nonexistent-team"},
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
deleteReq := NewRequestf(t, "DELETE", "/api/v1/repos/org3/repo3/branch_protections/%s", ruleName).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, deleteReq, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPICreateBranchWithSyncBranches(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
branches, err := db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{
|
||||
RepoID: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, branches, 8)
|
||||
|
||||
// make a broke repository with no branch on database
|
||||
_, err = db.DeleteByBean(t.Context(), git_model.Branch{RepoID: 1})
|
||||
assert.NoError(t, err)
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
giteaURL.Path = ctx.GitPath()
|
||||
|
||||
testAPICreateBranch(t, ctx.Session, "user2", "repo1", "", "new_branch", http.StatusCreated)
|
||||
})
|
||||
|
||||
branches, err = db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{
|
||||
RepoID: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, branches, 9)
|
||||
|
||||
branches, err = db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{
|
||||
RepoID: 1,
|
||||
Keyword: "new_branch",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, branches, 1)
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIGetCommentAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
|
||||
assert.NoError(t, comment.LoadIssue(t.Context()))
|
||||
assert.NoError(t, comment.LoadAttachments(t.Context()))
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: comment.Attachments[0].ID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID).
|
||||
AddTokenAuth(token)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID).
|
||||
AddTokenAuth(token)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
||||
|
||||
expect := convert.ToAPIAttachment(repo, attachment)
|
||||
assert.Equal(t, expect.ID, apiAttachment.ID)
|
||||
assert.Equal(t, expect.Name, apiAttachment.Name)
|
||||
assert.Equal(t, expect.UUID, apiAttachment.UUID)
|
||||
assert.Equal(t, expect.Created.Unix(), apiAttachment.Created.Unix())
|
||||
assert.Equal(t, expect.DownloadURL, apiAttachment.DownloadURL)
|
||||
}
|
||||
|
||||
func TestAPIListCommentAttachments(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID).
|
||||
AddTokenAuth(token)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
apiAttachments := DecodeJSON(t, resp, []*api.Attachment{})
|
||||
expectedCount := unittest.GetCount(t, &repo_model.Attachment{CommentID: comment.ID})
|
||||
assert.Len(t, apiAttachments, expectedCount)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachments[0].ID, CommentID: comment.ID})
|
||||
}
|
||||
|
||||
func TestAPICreateCommentAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
// Setup multi-part
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", "image.png")
|
||||
assert.NoError(t, err)
|
||||
_, err = part.Write(testGeneratePngBytes())
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID), body).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", writer.FormDataContentType())
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID})
|
||||
}
|
||||
|
||||
func TestAPICreateCommentAttachmentWithUnallowedFile(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
filename := "file.bad"
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
// Setup multi-part.
|
||||
writer := multipart.NewWriter(body)
|
||||
_, err := writer.CreateFormFile("attachment", filename)
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets", repoOwner.Name, repo.Name, comment.ID), body).
|
||||
AddTokenAuth(token).
|
||||
SetHeader("Content-Type", writer.FormDataContentType())
|
||||
|
||||
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
func TestAPIEditCommentAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
const newAttachmentName = "newAttachmentName.txt"
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d",
|
||||
repoOwner.Name, repo.Name, comment.ID, attachment.ID)
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
"name": newAttachmentName,
|
||||
}).AddTokenAuth(token)
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, CommentID: comment.ID, Name: apiAttachment.Name})
|
||||
}
|
||||
|
||||
func TestAPIEditCommentAttachmentWithUnallowedFile(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
filename := "file.bad"
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d",
|
||||
repoOwner.Name, repo.Name, comment.ID, attachment.ID)
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
"name": filename,
|
||||
}).AddTokenAuth(token)
|
||||
|
||||
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
func TestAPIDeleteCommentAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 6})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: attachment.CommentID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID)).
|
||||
AddTokenAuth(token)
|
||||
session.MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Attachment{ID: attachment.ID, CommentID: comment.ID})
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIListRepoComments(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments", repoOwner.Name, repo.Name))
|
||||
req := NewRequest(t, "GET", link.String())
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
apiComments := DecodeJSON(t, resp, []*api.Comment{})
|
||||
assert.Len(t, apiComments, 2)
|
||||
for _, apiComment := range apiComments {
|
||||
c := &issues_model.Comment{ID: apiComment.ID}
|
||||
unittest.AssertExistsAndLoadBean(t, c,
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: c.IssueID, RepoID: repo.ID})
|
||||
}
|
||||
|
||||
// test before and since filters
|
||||
query := url.Values{}
|
||||
before := "2000-01-01T00:00:11+00:00" // unix: 946684811
|
||||
since := "2000-01-01T00:00:12+00:00" // unix: 946684812
|
||||
query.Add("before", before)
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String())
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiComments = DecodeJSON(t, resp, []*api.Comment{})
|
||||
assert.Len(t, apiComments, 1)
|
||||
assert.EqualValues(t, 2, apiComments[0].ID)
|
||||
|
||||
query.Del("before")
|
||||
query.Add("since", since)
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String())
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiComments = DecodeJSON(t, resp, []*api.Comment{})
|
||||
assert.Len(t, apiComments, 1)
|
||||
assert.EqualValues(t, 3, apiComments[0].ID)
|
||||
}
|
||||
|
||||
func TestAPIListIssueComments(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{},
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeReadIssue)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/comments", repoOwner.Name, repo.Name, issue.Index).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
comments := DecodeJSON(t, resp, []*api.Comment{})
|
||||
expectedCount := unittest.GetCount(t, &issues_model.Comment{IssueID: issue.ID},
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
assert.Len(t, comments, expectedCount)
|
||||
}
|
||||
|
||||
func TestAPICreateComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
const commentBody = "Comment body"
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments",
|
||||
repoOwner.Name, repo.Name, issue.Index)
|
||||
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
"body": commentBody,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
updatedComment := DecodeJSON(t, resp, &api.Comment{})
|
||||
assert.Equal(t, commentBody, updatedComment.Body)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: updatedComment.ID, IssueID: issue.ID, Content: commentBody})
|
||||
|
||||
t.Run("BlockedByRepoOwner", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{
|
||||
"body": commentBody,
|
||||
}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("BlockedByIssuePoster", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 13})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", repo.OwnerName, repo.Name, issue.Index), map[string]string{
|
||||
"body": commentBody,
|
||||
}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteRepository))
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIGetComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
|
||||
assert.NoError(t, comment.LoadIssue(t.Context()))
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeReadIssue)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
apiComment := DecodeJSON(t, resp, &api.Comment{})
|
||||
|
||||
assert.NoError(t, comment.LoadPoster(t.Context()))
|
||||
expect := convert.ToAPIComment(t.Context(), repo, comment)
|
||||
|
||||
assert.Equal(t, expect.ID, apiComment.ID)
|
||||
assert.Equal(t, expect.Poster.FullName, apiComment.Poster.FullName)
|
||||
assert.Equal(t, expect.Body, apiComment.Body)
|
||||
assert.Equal(t, expect.Created.Unix(), apiComment.Created.Unix())
|
||||
}
|
||||
|
||||
func TestAPIGetSystemUserComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
for _, systemUser := range []*user_model.User{
|
||||
user_model.NewGhostUser(),
|
||||
user_model.NewActionsUser(),
|
||||
} {
|
||||
body := "Hello " + systemUser.Name
|
||||
comment, err := issues_model.CreateComment(t.Context(), &issues_model.CreateCommentOptions{
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Doer: systemUser,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
Content: body,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
apiComment := DecodeJSON(t, resp, &api.Comment{})
|
||||
|
||||
if assert.NotNil(t, apiComment.Poster) {
|
||||
if assert.Equal(t, systemUser.ID, apiComment.Poster.ID) {
|
||||
assert.NoError(t, comment.LoadPoster(t.Context()))
|
||||
assert.Equal(t, systemUser.Name, apiComment.Poster.UserName)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, body, apiComment.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIEditComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
const newCommentBody = "This is the new comment body"
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 8},
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||
// Using the ID of a comment that does not belong to the repository must fail
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d",
|
||||
repoOwner.Name, repo.Name, comment.ID)
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
"body": newCommentBody,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d",
|
||||
repoOwner.Name, repo.Name, comment.ID)
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
"body": newCommentBody,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
updatedComment := DecodeJSON(t, resp, &api.Comment{})
|
||||
assert.Equal(t, comment.ID, updatedComment.ID)
|
||||
assert.Equal(t, newCommentBody, updatedComment.Body)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID, IssueID: issue.ID, Content: newCommentBody})
|
||||
}
|
||||
|
||||
func TestAPIDeleteComment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 8},
|
||||
unittest.Cond("type = ?", issues_model.CommentTypeComment))
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: comment.IssueID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||
// Using the ID of a comment that does not belong to the repository must fail
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
|
||||
}
|
||||
|
||||
func TestAPIListIssueTimeline(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// load comment
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
// make request
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/timeline", repoOwner.Name, repo.Name, issue.Index)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// check if lens of list returned by API and
|
||||
// lists extracted directly from DB are the same
|
||||
comments := DecodeJSON(t, resp, []*api.TimelineComment{})
|
||||
expectedCount := unittest.GetCount(t, &issues_model.Comment{IssueID: issue.ID})
|
||||
assert.Len(t, comments, expectedCount)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2017 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
org_model "gitea.dev/models/organization"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
org_service "gitea.dev/services/org"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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 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"})
|
||||
|
||||
// fork into a limited org
|
||||
limitedOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
|
||||
assert.Equal(t, api.VisibleTypeLimited, limitedOrg.Visibility)
|
||||
|
||||
ownerTeam1, err := org_model.OrgFromUser(limitedOrg).GetOwnerTeam(t.Context())
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, org_service.AddTeamMember(t.Context(), ownerTeam1, user1))
|
||||
user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
|
||||
Organization: &limitedOrg.Name,
|
||||
}).AddTokenAuth(user1Token)
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
// fork into a private org
|
||||
user4Sess := loginUser(t, "user4")
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user4"})
|
||||
privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
|
||||
assert.Equal(t, api.VisibleTypePrivate, privateOrg.Visibility)
|
||||
|
||||
ownerTeam2, err := org_model.OrgFromUser(privateOrg).GetOwnerTeam(t.Context())
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, org_service.AddTeamMember(t.Context(), ownerTeam2, user4))
|
||||
user4Token := getTokenForLoggedInUser(t, user4Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
|
||||
req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
|
||||
Organization: &privateOrg.Name,
|
||||
}).AddTokenAuth(user4Token)
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
t.Run("Anonymous", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
forks := DecodeJSON(t, resp, []*api.Repository{})
|
||||
assert.Empty(t, forks)
|
||||
assert.Equal(t, "0", resp.Header().Get("X-Total-Count"))
|
||||
})
|
||||
|
||||
t.Run("Logged in", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
forks := DecodeJSON(t, resp, []*api.Repository{})
|
||||
assert.Len(t, forks, 2)
|
||||
assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
assert.NoError(t, org_service.AddTeamMember(t.Context(), ownerTeam2, user1))
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
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) {
|
||||
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)
|
||||
|
||||
// create fork from a private repository
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+repo2.FullName()+"/forks", &api.CreateForkOption{
|
||||
Organization: &privateOrg.Name,
|
||||
Name: new("forked-repo"),
|
||||
}).AddTokenAuth(user1Token)
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
// test get a private fork without clear permissions
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/"+repo2.FullName()+"/forks").AddTokenAuth(user1Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
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)
|
||||
assert.Equal(t, privateOrg.Name, forks[0].Owner.UserName)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/modules/options"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIListGitignoresTemplates(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/gitignore/templates")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
templateList := DecodeJSON(t, resp, []string{}) // this is a very long list
|
||||
assert.Contains(t, templateList, "C++")
|
||||
assert.Contains(t, templateList, "Go")
|
||||
}
|
||||
|
||||
func TestAPIGetGitignoreTemplateInfo(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// If Gitea has for some reason no Gitignore templates, we need to skip this test
|
||||
if len(repo_module.Gitignores) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Use the first template for the test
|
||||
templateName := repo_module.Gitignores[0]
|
||||
|
||||
urlStr := "/api/v1/gitignore/templates/" + templateName
|
||||
req := NewRequest(t, "GET", urlStr)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
templateInfo := DecodeJSON(t, resp, &api.GitignoreTemplateInfo{})
|
||||
|
||||
// We get the text of the template here
|
||||
text, _ := options.Gitignore(templateName)
|
||||
|
||||
assert.Equal(t, templateInfo.Name, templateName)
|
||||
assert.Equal(t, templateInfo.Source, string(text))
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// Copyright 2017 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type makeRequestFunc func(testing.TB, *RequestWrapper, int) *httptest.ResponseRecorder
|
||||
|
||||
func TestGPGKeys(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
tokenWithGPGKeyScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
makeRequest makeRequestFunc
|
||||
token string
|
||||
results []int
|
||||
}{
|
||||
{
|
||||
name: "NoLogin", makeRequest: MakeRequest, token: "",
|
||||
results: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
|
||||
},
|
||||
{
|
||||
name: "LoggedAsUser2", makeRequest: session.MakeRequest, token: token,
|
||||
results: []int{http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden},
|
||||
},
|
||||
{
|
||||
name: "LoggedAsUser2WithScope", makeRequest: session.MakeRequest, token: tokenWithGPGKeyScope,
|
||||
results: []int{http.StatusOK, http.StatusOK, http.StatusNotFound, http.StatusNoContent, http.StatusUnprocessableEntity, http.StatusNotFound, http.StatusCreated, http.StatusNotFound, http.StatusCreated},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
// Basic test on result code
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run("ViewOwnGPGKeys", func(t *testing.T) {
|
||||
testViewOwnGPGKeys(t, tc.makeRequest, tc.token, tc.results[0])
|
||||
})
|
||||
t.Run("ViewGPGKeys", func(t *testing.T) {
|
||||
testViewGPGKeys(t, tc.makeRequest, tc.token, tc.results[1])
|
||||
})
|
||||
t.Run("GetGPGKey", func(t *testing.T) {
|
||||
testGetGPGKey(t, tc.makeRequest, tc.token, tc.results[2])
|
||||
})
|
||||
t.Run("DeleteGPGKey", func(t *testing.T) {
|
||||
testDeleteGPGKey(t, tc.makeRequest, tc.token, tc.results[3])
|
||||
})
|
||||
|
||||
t.Run("CreateInvalidGPGKey", func(t *testing.T) {
|
||||
testCreateInvalidGPGKey(t, tc.makeRequest, tc.token, tc.results[4])
|
||||
})
|
||||
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])
|
||||
})
|
||||
t.Run("CreateValidSecondaryEmailGPGKeyNotActivated", func(t *testing.T) {
|
||||
testCreateValidSecondaryEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[7])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Check state after basic add
|
||||
t.Run("CheckState", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/gpg_keys"). // GET all keys
|
||||
AddTokenAuth(tokenWithGPGKeyScope)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
keys := DecodeJSON(t, resp, []*api.GPGKey{})
|
||||
assert.Len(t, keys, 1)
|
||||
|
||||
primaryKey1 := keys[0] // Primary key 1
|
||||
assert.Equal(t, "38EA3BCED732982C", primaryKey1.KeyID)
|
||||
assert.Len(t, primaryKey1.Emails, 1)
|
||||
assert.Equal(t, "user2@example.com", primaryKey1.Emails[0].Email)
|
||||
assert.True(t, primaryKey1.Emails[0].Verified)
|
||||
|
||||
subKey := primaryKey1.SubsKey[0] // Subkey of 38EA3BCED732982C
|
||||
assert.Equal(t, "70D7C694D17D03AD", subKey.KeyID)
|
||||
assert.Empty(t, subKey.Emails)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(primaryKey1.ID, 10)). // Primary key 1
|
||||
AddTokenAuth(tokenWithGPGKeyScope)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
key := DecodeJSON(t, resp, &api.GPGKey{})
|
||||
assert.Equal(t, "38EA3BCED732982C", key.KeyID)
|
||||
assert.Len(t, key.Emails, 1)
|
||||
assert.Equal(t, "user2@example.com", key.Emails[0].Email)
|
||||
assert.True(t, key.Emails[0].Verified)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(subKey.ID, 10)). // Subkey of 38EA3BCED732982C
|
||||
AddTokenAuth(tokenWithGPGKeyScope)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
key = DecodeJSON(t, resp, &api.GPGKey{})
|
||||
assert.Equal(t, "70D7C694D17D03AD", key.KeyID)
|
||||
assert.Empty(t, key.Emails)
|
||||
})
|
||||
|
||||
// Check state after basic add
|
||||
t.Run("CheckCommits", func(t *testing.T) {
|
||||
t.Run("NotSigned", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/branches/not-signed").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
branch := DecodeJSON(t, resp, &api.Branch{})
|
||||
assert.False(t, branch.Commit.Verification.Verified)
|
||||
})
|
||||
|
||||
t.Run("SignedWithNotValidatedEmail", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/branches/good-sign-not-yet-validated").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
branch := DecodeJSON(t, resp, &api.Branch{})
|
||||
assert.False(t, branch.Commit.Verification.Verified)
|
||||
})
|
||||
|
||||
t.Run("SignedWithValidEmail", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo16/branches/good-sign").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
branch := DecodeJSON(t, resp, &api.Branch{})
|
||||
assert.True(t, branch.Commit.Verification.Verified)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testViewOwnGPGKeys(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/gpg_keys").
|
||||
AddTokenAuth(token)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testViewGPGKeys(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
req := NewRequest(t, "GET", "/api/v1/users/user2/gpg_keys").
|
||||
AddTokenAuth(token)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testGetGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/gpg_keys/1").
|
||||
AddTokenAuth(token)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testDeleteGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
req := NewRequest(t, "DELETE", "/api/v1/user/gpg_keys/1").
|
||||
AddTokenAuth(token)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testCreateGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int, publicKey string) {
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", api.CreateGPGKeyOption{
|
||||
ArmoredKey: publicKey,
|
||||
}).AddTokenAuth(token)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testCreateInvalidGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
testCreateGPGKey(t, makeRequest, token, expected, "invalid_key")
|
||||
}
|
||||
|
||||
func testCreateNoneRegisteredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmGUygBCACjCNbKvMGgp0fd5vyFW9olE1CLCSyyF9gQN2hSuzmZLuAZF2Kh
|
||||
dCMCG2T1UwzUB/yWUFWJ2BtCwSjuaRv+cGohqEy6bhEBV90peGA33lHfjx7wP25O
|
||||
7moAphDOTZtDj1AZfCh/PTcJut8Lc0eRDMhNyp/bYtO7SHNT1Hr6rrCV/xEtSAvR
|
||||
3b148/tmIBiSadaLwc558KU3ucjnW5RVGins3AjBZ+TuT4XXVH/oeLSeXPSJ5rt1
|
||||
rHwaseslMqZ4AbvwFLx5qn1OC9rEQv/F548QsA8m0IntLjoPon+6wcubA9Gra21c
|
||||
Fp6aRYl9x7fiqXDLg8i3s2nKdV7+e6as6Tp9ABEBAAG0FG5vdGtub3duQGV4YW1w
|
||||
bGUuY29tiQEcBBABAgAGBQJZhlMoAAoJEC8+pvYULDtte/wH/2JNrhmHwDY+hMj0
|
||||
batIK4HICnkKxjIgbha80P2Ao08NkzSge58fsxiKDFYAQjHui+ZAw4dq79Ax9AOO
|
||||
Iv2GS9+DUfWhrb6RF+vNuJldFzcI0rTW/z2q+XGKrUCwN3khJY5XngHfQQrdBtMK
|
||||
qsoUXz/5B8g422RTbo/SdPsyYAV6HeLLeV3rdgjI1fpaW0seZKHeTXQb/HvNeuPg
|
||||
qz+XV1g6Gdqa1RjDOaX7A8elVKxrYq3LBtc93FW+grBde8n7JL0zPM3DY+vJ0IJZ
|
||||
INx/MmBfmtCq05FqNclvU+sj2R3N1JJOtBOjZrJHQbJhzoILou8AkxeX1A+q9OAz
|
||||
1geiY5E=
|
||||
=TkP3
|
||||
-----END PGP PUBLIC KEY BLOCK-----`)
|
||||
}
|
||||
|
||||
func testCreateValidGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
// User2 <user2@example.com> //primary & activated
|
||||
testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmGVsMBCACuxgZ7W7rI9xN08Y4M7B8yx/6/I4Slm94+wXf8YNRvAyqj30dW
|
||||
VJhyBcnfNRDLKSQp5o/hhfDkCgdqBjLa1PnHlGS3PXJc0hP/FyYPD2BFvNMPpCYS
|
||||
eu3T1qKSNXm6X0XOWD2LIrdiDC8HaI9FqZVMI/srMK2CF8XCL2m67W1FuoPlWzod
|
||||
5ORy0IZB7spoF0xihmcgnEGElRmdo5w/vkGH8U7Zyn9Eb57UVFeafgeskf4wqB23
|
||||
BjbMdW2YaB+yzMRwYgOnD5lnBD4uqSmvjaV9C0kxn7x+oJkkiRV8/z1cNcO+BaeQ
|
||||
Akh/yTTeTzYGSc/ZOqCX1O+NOPgSeixVlqenABEBAAG0GVVzZXIyIDx1c2VyMkBl
|
||||
eGFtcGxlLmNvbT6JAVQEEwEIAD4WIQRXgbSh0TtGbgRd7XI46jvO1zKYLAUCWYZW
|
||||
wwIbAwUJA8JnAAULCQgHAgYVCAkKCwIEFgIDAQIeAQIXgAAKCRA46jvO1zKYLF/e
|
||||
B/91wm2KLMIQBZBA9WA2/+9rQWTo9EqgYrXN60rEzX3cYJWXZiE4DrKR1oWDGNLi
|
||||
KXOCW62snvJldolBqq0ZqaKvPKzl0Y5TRqbYEc9AjUSqgRin1b+G2DevLGT4ibq+
|
||||
7ocQvz0XkASEUAgHahp0Ubiiib1521WwT/duL+AG8Gg0+DK09RfV3eX5/EOkQCKv
|
||||
8cutqgsd2Smz40A8wXuJkRcipZBtrB/GkUaZ/eJdwEeSYZjEA9GWF61LJT2stvRN
|
||||
HCk7C3z3pVEek1PluiFs/4VN8BG8yDzW4c0tLty4Fj3VwPqwIbB5AJbquVfhQCb4
|
||||
Eep2lm3Lc9b1OwO5N3coPJkouQENBFmGVsMBCADAGba2L6NCOE1i3WIP6CPzbdOo
|
||||
N3gdTfTgccAx9fNeon9jor+3tgEjlo9/6cXiRoksOV6W4wFab/ZwWgwN6JO4CGvZ
|
||||
Wi7EQwMMMp1E36YTojKQJrcA9UvMnTHulqQQ88F5E845DhzFQM3erv42QZZMBAX3
|
||||
kXCgy1GNFocl6tLUvJdEqs+VcJGGANMpmzE4WLa8KhSYnxipwuQ62JBy9R+cHyKT
|
||||
OARk8znRqSu5bT3LtlrZ/HXu+6Oy4+2uCdNzZIh5J5tPS7CPA6ptl88iGVBte/CJ
|
||||
7cjgJWSQqeYp2Y5QvsWAivkQ4Ww9plHbbwV0A2eaHsjjWzlUl3HoJ/snMOhBABEB
|
||||
AAGJATwEGAEIACYWIQRXgbSh0TtGbgRd7XI46jvO1zKYLAUCWYZWwwIbDAUJA8Jn
|
||||
AAAKCRA46jvO1zKYLBwLCACQOpeRVrwIKVaWcPMYjVHHJsGscaLKpgpARAUgbiG6
|
||||
Cbc2WI8Sm3fRwrY0VAfN+u9QwrtvxANcyB3vTgTzw7FimfhOimxiTSO8HQCfjDZF
|
||||
Xly8rq+Fua7+ClWUpy21IekW41VvZYjH2sL6EVP+UcEOaGAyN53XfhaRVZPhNtZN
|
||||
NKAE9N5EG3rbsZ33LzJj40rEKlzFSseAAPft8qA3IXjzFBx+PQXHMpNCagL79he6
|
||||
lqockTJ+oPmta4CF/J0U5LUr1tOZXheL3TP6m8d08gDrtn0YuGOPk87i9sJz+jR9
|
||||
uy6MA3VSB99SK9ducGmE1Jv8mcziREroz2TEGr0zPs6h
|
||||
=J59D
|
||||
-----END PGP PUBLIC KEY BLOCK-----`)
|
||||
}
|
||||
|
||||
func testCreateValidSecondaryEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
// User2 <user2-2@example.com> //secondary and not activated
|
||||
testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQGNBGC2K2cBDAC1+Xgk+8UfhASVgRngQi4rnQ8k0t+bWsBz4Czd26+cxVDRwlTT
|
||||
8PALdrbrY/e9iXjcVcZ8Npo4UYe7/LfnL57dc7tgbenRGYYrWyVoNNv58BVw4xCY
|
||||
RmgvdHWIIPGuz3aME0smHxbJ2KewYTqjTPuVKF/wrHTwCpVWdjYKC5KDo3yx0mro
|
||||
xf9vOJOnkWNMiEw7TiZfkrbUqxyA53BVsSNKRX5C3b4FJcVT7eiAq7sDAaFxjEHy
|
||||
ahZslmvg7XZxWzSVzxDNesR7f4xuop8HBjzaluJoVuwiyWculTvz1b6hyHVQr+ad
|
||||
h8JGjj1tySI65OTFsTuptsfHXjtjl/NR4P6BXkf+FVwweaTQaEzpHkv0m9b9pY43
|
||||
CY/8XtS4uNPermiLG/Z0BB1eOCdoOQVHpjOa55IXQWhxXB6NZVyowiUbrR7jLDQy
|
||||
5JP7D1HmErTR8JRm3VDqGbSaCgugRgFX+lb/fpgFp9k02OeK+JQudolZOt1mVk+T
|
||||
C4xmEWxfiH15/JMAEQEAAbQbdXNlcjIgPHVzZXIyLTJAZXhhbXBsZS5jb20+iQHU
|
||||
BBMBCAA+FiEEB/Y4DM3Ba2H9iXmlPO9G70C+/D4FAmC2K2cCGwMFCQPCZwAFCwkI
|
||||
BwIGFQoJCAsCBBYCAwECHgECF4AACgkQPO9G70C+/D59/Av/XZIhCH4X2FpxCO3d
|
||||
oCa+sbYkBL5xeUoPfAx5ThXzqL/tllO88TKTMEGZF3k5pocXWH0xmhqlvDTcdb0i
|
||||
W3O0CN8FLmuotU51c0JC1mt9zwJP9PeJNyqxrMm01Yzj55z/Dz3QHSTlDjrWTWjn
|
||||
YBqDf2HfdM177oydfSYmevZni1aDmBalWpFPRvqISCO7uFnvg1hJQ5mD/0qie663
|
||||
QJ8LAAANg32H9DyPnYi9wU62WX0DMUVTjKctT3cnYCbirjjJ7ZlCCm+cf61CRX1B
|
||||
E1Ng/Ef3ZcUfXWitZSjfET/pKEMSNjsQawFpZ/LPCBl+UPHzaTPAASeGJvcbZ3py
|
||||
wZQLQc1MCu2hmMBQ8zHQTdS2Pp0RISxCQLYvVQL6DrcJDNiSqn9p9RQt5c5r5Pjx
|
||||
80BIPcjj3glOVP7PYE2azQAkt6reEjhimwCfjeDpiPnkBTY7Av2jCcUFhhemDY/j
|
||||
TRXK1paLphhJ36zC22SeHGxNNakjjuUakqB85DEUeoWuVm6ouQGNBGC2K2cBDADx
|
||||
G2rIAgMjdPtofhkEZXwv6zdNwmYOlIIM+59bam9Ep/vFq8F5f+xldevm5dvM8SeR
|
||||
pNwDGSOUf5OKBWBdsJFhlYBl7+EcKd/Tent/XS6JoA9ffF33b+r04L543+ykiKON
|
||||
WYeYi0F4WwYTIQgqZHJze1sPVkYGR5F0bL8PAcLuwd5dzZVi/q2HakrGdg29N8oY
|
||||
b/XnoR7FflPrNYdzO6hawi5Inx7KS7aWa0ZkARb0F4HSct+/m6nAZVsoJINLudyQ
|
||||
ut2NWeU8rWIm1hqyIxQFvuQJy46umq++10J/sWA98bkg41Rx+72+eP7DM5v8IgUp
|
||||
clJsfljRXIBWbmRAVZvtNI7PX9fwMMhf4M7wHO7G2WV39o1exKps5xFFcn8PUQiX
|
||||
jCSR81M145CgCdmLUR1y0pdkN/WIqjXBhkPIvO2dxEcodMNHb1aUUuUOnww6+xIP
|
||||
8rGVw+a2DUiALc8Qr5RP21AYKRctfiwhSQh2KODveMtyLI3U9C/eLRPp+QM3XB8A
|
||||
EQEAAYkBvAQYAQgAJhYhBAf2OAzNwWth/Yl5pTzvRu9Avvw+BQJgtitnAhsMBQkD
|
||||
wmcAAAoJEDzvRu9Avvw+3FcMAJBwupyJ4zwQFxTJ5BkDlusG3U2FXEf3bDrXhvNd
|
||||
qi8eS8Vo/vRiH/w/my5JFpz1o2tJToryF71D+uF5DTItalKquhsQ9reAEmXggqOh
|
||||
9Jd9mWJIEEWcRORiLNDKENKvE8bouw4U4hRaSF0IaGzAe5mO+oOvwal8L97wFxrZ
|
||||
4leM1GzkopiuNfbkkBBw2KJcMjYBHzzXSCALnVwhjbgkBEWPIg38APT3cr9KfnMM
|
||||
q8+tvsGLj4piAl3Lww7+GhSsDOUXH8btR41BSAQDrbO5q6oi/h4nuxoNmQIDW/Ug
|
||||
s+dd5hnY2FtHRjb4FCR9kAjdTE6stc8wzohWfbg1N+12TTA2ylByAumICVXixavH
|
||||
RJ7l0OiWJk388qw9mqh3k8HcBxL7OfDlFC9oPmCS0iYiIwW/Yc80kBhoxcvl/Xa7
|
||||
mIMMn8taHIaQO7v9ln2EVQYTzbNCmwTw9ovTM0j/Pbkg2EftfP1TCoxQHvBnsCED
|
||||
6qgtsUdi5eviONRkBgeZtN3oxA==
|
||||
=MgDv
|
||||
-----END PGP PUBLIC KEY BLOCK-----`)
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/queue"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type APITestContext struct {
|
||||
Reponame string
|
||||
Session *TestSession
|
||||
Token string
|
||||
Username string
|
||||
ExpectedCode int
|
||||
}
|
||||
|
||||
func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.AccessTokenScope) APITestContext {
|
||||
session := loginUser(t, username)
|
||||
if len(scope) == 0 {
|
||||
// FIXME: legacy logic: no scope means all
|
||||
scope = []auth.AccessTokenScope{auth.AccessTokenScopeAll}
|
||||
}
|
||||
token := getTokenForLoggedInUser(t, session, scope...)
|
||||
return APITestContext{
|
||||
Session: session,
|
||||
Token: token,
|
||||
Username: username,
|
||||
Reponame: reponame,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx APITestContext) GitPath() string {
|
||||
return fmt.Sprintf("%s/%s.git", ctx.Username, ctx.Reponame)
|
||||
}
|
||||
|
||||
func doAPICreateRepository(ctx APITestContext, empty bool, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
createRepoOption := &api.CreateRepoOption{
|
||||
AutoInit: !empty,
|
||||
Description: "Temporary repo",
|
||||
Name: ctx.Reponame,
|
||||
Private: true,
|
||||
Template: true,
|
||||
Gitignores: "",
|
||||
License: "WTFPL",
|
||||
Readme: "Default",
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", createRepoOption).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
repository := DecodeJSON(t, resp, &api.Repository{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIEditRepository(ctx APITestContext, editRepoOption *api.EditRepoOption, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)), editRepoOption).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
repository := DecodeJSON(t, resp, &api.Repository{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIAddCollaborator(ctx APITestContext, username string, mode perm.AccessMode) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
permission := api.RepoWritePermissionRead
|
||||
|
||||
if mode == perm.AccessModeAdmin {
|
||||
permission = api.RepoWritePermissionAdmin
|
||||
} else if mode > perm.AccessModeRead {
|
||||
permission = api.RepoWritePermissionWrite
|
||||
}
|
||||
addCollaboratorOption := &api.AddCollaboratorOption{
|
||||
Permission: &permission,
|
||||
}
|
||||
req := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/collaborators/%s", ctx.Username, ctx.Reponame, username), addCollaboratorOption).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIForkRepository(ctx APITestContext, username string, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
createForkOption := &api.CreateForkOption{}
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", username, ctx.Reponame), createForkOption).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusAccepted)
|
||||
repository := DecodeJSON(t, resp, &api.Repository{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIGetRepository(ctx APITestContext, callback ...func(*testing.T, api.Repository)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", ctx.Username, ctx.Reponame)).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
repository := DecodeJSON(t, resp, &api.Repository{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIDeleteRepository(ctx APITestContext) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s", ctx.Username, ctx.Reponame)).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func doAPICreateUserKey(ctx APITestContext, keyname, keyFile string, callback ...func(*testing.T, api.PublicKey)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
dataPubKey, err := os.ReadFile(keyFile + ".pub")
|
||||
assert.NoError(t, err)
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", &api.CreateKeyOption{
|
||||
Title: keyname,
|
||||
Key: string(dataPubKey),
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
publicKey := DecodeJSON(t, resp, &api.PublicKey{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *publicKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIDeleteUserKey(ctx APITestContext, keyID int64) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/keys/%d", keyID)).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func doAPICreateDeployKey(ctx APITestContext, keyname, keyFile string, readOnly bool) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
dataPubKey, err := os.ReadFile(keyFile + ".pub")
|
||||
assert.NoError(t, err)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/keys", ctx.Username, ctx.Reponame), api.CreateKeyOption{
|
||||
Title: keyname,
|
||||
Key: string(dataPubKey),
|
||||
ReadOnly: readOnly,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
}
|
||||
|
||||
func doAPICreatePullRequest(ctx APITestContext, owner, repo, baseBranch, headBranch string) func(*testing.T) (api.PullRequest, error) {
|
||||
return func(t *testing.T) (api.PullRequest, error) {
|
||||
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner, repo), &api.CreatePullRequestOption{
|
||||
Head: headBranch,
|
||||
Base: baseBranch,
|
||||
Title: fmt.Sprintf("create a pr from %s to %s", headBranch, baseBranch),
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
|
||||
expected := http.StatusCreated
|
||||
if ctx.ExpectedCode != 0 {
|
||||
expected = ctx.ExpectedCode
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, expected)
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
pr := api.PullRequest{}
|
||||
err := decoder.Decode(&pr)
|
||||
return pr, err
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIGetPullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) (api.PullRequest, error) {
|
||||
return func(t *testing.T) (api.PullRequest, error) {
|
||||
req := NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner, repo, index)).
|
||||
AddTokenAuth(ctx.Token)
|
||||
|
||||
expected := http.StatusOK
|
||||
if ctx.ExpectedCode != 0 {
|
||||
expected = ctx.ExpectedCode
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, expected)
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
pr := api.PullRequest{}
|
||||
err := decoder.Decode(&pr)
|
||||
return pr, err
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
|
||||
|
||||
var req *RequestWrapper
|
||||
var resp *httptest.ResponseRecorder
|
||||
|
||||
for range 6 {
|
||||
req = NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
|
||||
MergeMessageField: "doAPIMergePullRequest Merge",
|
||||
Do: string(repo_model.MergeStyleMerge),
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
|
||||
resp = ctx.Session.MakeRequest(t, req, NoExpectedStatus)
|
||||
|
||||
if resp.Code != http.StatusMethodNotAllowed {
|
||||
break
|
||||
}
|
||||
err := DecodeJSON(t, resp, &api.APIError{})
|
||||
assert.Equal(t, "Please try again later", err.Message)
|
||||
queue.GetManager().FlushAll(t.Context(), 5*time.Second)
|
||||
<-time.After(1 * time.Second)
|
||||
}
|
||||
|
||||
expected := ctx.ExpectedCode
|
||||
if expected == 0 {
|
||||
expected = http.StatusOK
|
||||
}
|
||||
|
||||
if !assert.Equal(t, expected, resp.Code,
|
||||
"Request: %s %s", req.Method, req.URL.String()) {
|
||||
logUnexpectedResponse(t, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIManuallyMergePullRequest(ctx APITestContext, owner, repo, commitID string, index int64) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
|
||||
req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
|
||||
Do: string(repo_model.MergeStyleManuallyMerged),
|
||||
MergeCommitID: commitID,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)
|
||||
req := NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{
|
||||
MergeMessageField: "doAPIMergePullRequest Merge",
|
||||
Do: string(repo_model.MergeStyleMerge),
|
||||
MergeWhenChecksSucceed: true,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func doAPICancelAutoMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequest(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge", owner, repo, index)).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, branch).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
branch := DecodeJSON(t, resp, &api.Branch{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *branch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ctx.Username, ctx.Reponame, treepath), &options).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
contents := DecodeJSON(t, resp, &api.FileResponse{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPICreateOrganization(ctx APITestContext, options *api.CreateOrgOption, callback ...func(*testing.T, api.Organization)) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &options).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
contents := DecodeJSON(t, resp, &api.Organization{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPICreateOrganizationRepository(ctx APITestContext, orgName string, options *api.CreateRepoOption, callback ...func(*testing.T, api.Repository)) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/repos", orgName), &options).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
contents := DecodeJSON(t, resp, &api.Repository{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPICreateOrganizationTeam(ctx APITestContext, orgName string, options *api.CreateTeamOption, callback ...func(*testing.T, api.Team)) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &options).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
contents := DecodeJSON(t, resp, &api.Team{})
|
||||
if len(callback) > 0 {
|
||||
callback[0](t, *contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIAddUserToOrganizationTeam(ctx APITestContext, teamID int64, username string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/teams/%d/members/%s", teamID, username)).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, repoName string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s", teamID, orgName, repoName)).
|
||||
AddTokenAuth(ctx.Token)
|
||||
if ctx.ExpectedCode != 0 {
|
||||
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
|
||||
return
|
||||
}
|
||||
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/42wim/httpsig"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
httpsigPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAQEAqjmQeb5Eb1xV7qbNf9ErQ0XRvKZWzUsLFhJzZz+Ab7q8WtPs91vQ
|
||||
fBiypw4i8OTG6WzDcgZaV8Ndxn7iHnIstdA1k89MVG4stydymmwmk9+mrCMNsu5OmdIy9F
|
||||
AZ61RDcKuf5VG2WKkmeK0VO+OMJIYfE1C6czNeJ6UAmcIOmhGxvjMI83XUO9n0ftwTwayp
|
||||
+XU5prvKx/fTvlPjbraPNU4OzwPjVLqXBzpoXYhBquPaZYFRVyvfFZLObYsmy+BrsxcloM
|
||||
l+9w4P0ATJ9njB7dRDL+RrN4uhhYSihqOK4w4vaiOj1+aA0eC0zXunEfLXfGIVQ/FhWcCy
|
||||
5f72mMiKnQAAA9AxSmzFMUpsxQAAAAdzc2gtcnNhAAABAQCqOZB5vkRvXFXups1/0StDRd
|
||||
G8plbNSwsWEnNnP4Bvurxa0+z3W9B8GLKnDiLw5MbpbMNyBlpXw13GfuIeciy10DWTz0xU
|
||||
biy3J3KabCaT36asIw2y7k6Z0jL0UBnrVENwq5/lUbZYqSZ4rRU744wkhh8TULpzM14npQ
|
||||
CZwg6aEbG+MwjzddQ72fR+3BPBrKn5dTmmu8rH99O+U+Nuto81Tg7PA+NUupcHOmhdiEGq
|
||||
49plgVFXK98Vks5tiybL4GuzFyWgyX73Dg/QBMn2eMHt1EMv5Gs3i6GFhKKGo4rjDi9qI6
|
||||
PX5oDR4LTNe6cR8td8YhVD8WFZwLLl/vaYyIqdAAAAAwEAAQAAAQBz+nyBNi2SYir6SxPA
|
||||
flcnoq5gBkUl4ndPNosCUbXEakpi5/mQHzJRGtK+F1efIYCVEdGoIsPy/90onNKbQ9dKmO
|
||||
2oI5kx/U7iCzJ+HCm8nqkEp21x+AP9scWdx+Wg/OxmG8j5iU7f4X+gwOyyvTqCuA78Lgia
|
||||
7Oi9wiJCoIEqXr6dRYGJzfASwKA2dj995HzATexleLSD5fQCmZTF+Vh5OQ5WmE+c53JdZS
|
||||
T3Plie/P/smgSWBtf1fWr6JL2+EBsqQsIK1Jo7r/7rxsz+ILoVfnneNQY4QSa9W+t6ZAI+
|
||||
caSA0Guv7vC92ewjlMVlwKa3XaEjMJb5sFlg1r6TYMwBAAAAgQDQwXvgSXNaSHIeH53/Ab
|
||||
t4BlNibtxK8vY8CZFloAKXkjrivKSlDAmQCM0twXOweX2ScPjE+XlSMV4AUsv/J6XHGHci
|
||||
W3+PGIBfc/fQRBpiyhzkoXYDVrlkSKHffCnAqTUQlYkhr0s7NkZpEeqPE0doAUs4dK3Iqb
|
||||
zdtz8e5BPXZwAAAIEA4U/JskIu5Oge8Is2OLOhlol0EJGw5JGodpFyhbMC+QYK9nYqy7wI
|
||||
a6mZ2EfOjjwIZD/+wYyulw6cRve4zXwgzUEXLIKp8/H3sYvJK2UMeP7y68sQFqGxbm6Rnh
|
||||
tyBBSaJQnOXVOFf9gqZGCyO/J0Illg3AXTuC8KS/cxwasC38EAAACBAMFo/6XQoR6E3ynj
|
||||
VBaz2SilWqQBixUyvcNz8LY73IIDCecoccRMFSEKhWtvlJijxvFbF9M8g9oKAVPuub4V5r
|
||||
CGmwVPEd5yt4C2iyV0PhLp1PA2/i42FpCSnHaz/EXSz6ncTZcOMMuDqUbgUUpQg4VSUDl9
|
||||
fhTNAzWwZoQ91aHdAAAAFHUwMDIyMTQ2QGljdHMtcC1ueC03AQIDBAUG
|
||||
-----END OPENSSH PRIVATE KEY-----`
|
||||
httpsigCertificate = `ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgiR7SU8gmZLhopx4Y03nOXVuAb+4fyMcJYjMGcE1Z2oEAAAADAQABAAABAQCqOZB5vkRvXFXups1/0StDRdG8plbNSwsWEnNnP4Bvurxa0+z3W9B8GLKnDiLw5MbpbMNyBlpXw13GfuIeciy10DWTz0xUbiy3J3KabCaT36asIw2y7k6Z0jL0UBnrVENwq5/lUbZYqSZ4rRU744wkhh8TULpzM14npQCZwg6aEbG+MwjzddQ72fR+3BPBrKn5dTmmu8rH99O+U+Nuto81Tg7PA+NUupcHOmhdiEGq49plgVFXK98Vks5tiybL4GuzFyWgyX73Dg/QBMn2eMHt1EMv5Gs3i6GFhKKGo4rjDi9qI6PX5oDR4LTNe6cR8td8YhVD8WFZwLLl/vaYyIqdAAAAAAAAAAEAAAABAAAABXVzZXIxAAAACQAAAAV1c2VyMQAAAABimoIOAAAAAMCWkRMAAAAAAAAAggAAABVwZXJtaXQtWDExLWZvcndhcmRpbmcAAAAAAAAAF3Blcm1pdC1hZ2VudC1mb3J3YXJkaW5nAAAAAAAAABZwZXJtaXQtcG9ydC1mb3J3YXJkaW5nAAAAAAAAAApwZXJtaXQtcHR5AAAAAAAAAA5wZXJtaXQtdXNlci1yYwAAAAAAAAAAAAABlwAAAAdzc2gtcnNhAAAAAwEAAQAAAYEAm+AwtXTBZyeqV1qOxjMU3Ibc5iR2M3zerGfRQDxUeIozC3xpIvqJbzjDuRapdf8hpxn2xC0GtUusuLIUr4/+Svs1BUnJhF2H9xnK/O0aopS5MpNekUvnBzQdbvO8Ux2xE2mt58giXhkEaXeCEODSqG++OZsA2e40AR/AGRJ4OdDofMvH4vLJAQQc2mKdYpYL8xu+NC+7nsenx1etpsqtEl3gmvqCVI6t9uhVPMvlbGt9h/AN3u7ToF2T3bdk1TZbcdkvR9ljvETIuy32ksAETX8tc7vm30edK+nn/GMeWCgjM+MFm9Uh1NRkvNNJozo5SJy0DkWETTJUsEdfry5VQ3IjqhWqQ0m4/mDlTmsEdEdWqpUiqWZLd9w7jgT8fanuglZyIu2fj8fyqjPjiws5S2P0Uvi28UKQ1nH01UYj/kuakU3BNzN1IqDf3tARP9fjKV/dCBqb1ZAOtyC2GyhGuGzNwEi+woUwq+sTeV0/hqVSb3hSitXHzcfRMRyOK82BAAABlAAAAAxyc2Etc2hhMi01MTIAAAGAMBfgZFvz4BdxriGKYd6eRhMo6hf+I8S9uzNRsflJXHuA+HR9ExIm/Q9JjKmfThQzNyGGBOBILaDU205SAJuG+kk3SieSQDd75ZQd8YmNlCc+516AriOsTiyVCupnf3I2euTjMZqEZbJcBbkBljppTOWQVN7xxE8QakDfGhg0+RjJE9wYOTmkKpDBfII5Nw8V5DoOD7kNEpXYqHdy/8lVxpqUYNIP1J0dNP4f6qBcZcM1PDA12q8zwIGqSNNjf2UXY/Nr8nv9CnK4fB8NDOPKTBa4cm48BGbvM/X0l6dYKswuZ9Np8lw+y6+GxTgznGCrkzMmuEV4FzSq4xHp41H2L2MTwUkwYaeyG1VP6aWkvn6zPkSxaaJDfQX7CAFe17IhIGXR0UPLjKjh35nDLzMWb/W6/W1lK9YkZNHXSf7Z9m9MUAZN7yQgOggGsuYEW4imZxvZizMd+fdDu9mbhr0FDis89I7MSJDnyYRE9FXS7p3QpppBwGcss/9yV3JV3Bjc`
|
||||
)
|
||||
|
||||
func TestHTTPSigPubKey(t *testing.T) {
|
||||
// Add our public key to user1
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.SSH.MinimumKeySizeCheck, false)()
|
||||
session := loginUser(t, "user1")
|
||||
token := url.QueryEscape(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser))
|
||||
keyType := "ssh-rsa"
|
||||
keyContent := "AAAAB3NzaC1yc2EAAAADAQABAAABAQCqOZB5vkRvXFXups1/0StDRdG8plbNSwsWEnNnP4Bvurxa0+z3W9B8GLKnDiLw5MbpbMNyBlpXw13GfuIeciy10DWTz0xUbiy3J3KabCaT36asIw2y7k6Z0jL0UBnrVENwq5/lUbZYqSZ4rRU744wkhh8TULpzM14npQCZwg6aEbG+MwjzddQ72fR+3BPBrKn5dTmmu8rH99O+U+Nuto81Tg7PA+NUupcHOmhdiEGq49plgVFXK98Vks5tiybL4GuzFyWgyX73Dg/QBMn2eMHt1EMv5Gs3i6GFhKKGo4rjDi9qI6PX5oDR4LTNe6cR8td8YhVD8WFZwLLl/vaYyIqd"
|
||||
rawKeyBody := api.CreateKeyOption{
|
||||
Title: "test-key",
|
||||
Key: keyType + " " + keyContent,
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", rawKeyBody).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// parse our private key and create the httpsig request
|
||||
sshSigner, _ := ssh.ParsePrivateKey([]byte(httpsigPrivateKey))
|
||||
keyID := ssh.FingerprintSHA256(sshSigner.PublicKey())
|
||||
|
||||
// create the request
|
||||
token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadAdmin)
|
||||
req = NewRequest(t, "GET", "/api/v1/admin/users").
|
||||
AddTokenAuth(token)
|
||||
|
||||
signer, _, err := httpsig.NewSSHSigner(sshSigner, httpsig.DigestSha512, []string{httpsig.RequestTarget, "(created)", "(expires)"}, httpsig.Signature, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// sign the request
|
||||
err = signer.SignRequest(keyID, req.Request, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// make the request
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestHTTPSigCert(t *testing.T) {
|
||||
// Add our public key to user1
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/keys", map[string]string{
|
||||
"content": "user1",
|
||||
"title": "principal",
|
||||
"type": "principal",
|
||||
})
|
||||
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
pkcert, _, _, _, err := ssh.ParseAuthorizedKey([]byte(httpsigCertificate))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// parse our private key and create the httpsig request
|
||||
sshSigner, _ := ssh.ParsePrivateKey([]byte(httpsigPrivateKey))
|
||||
keyID := "gitea"
|
||||
|
||||
// create our certificate signer using the ssh signer and our certificate
|
||||
certSigner, err := ssh.NewCertSigner(pkcert.(*ssh.Certificate), sshSigner)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create the request
|
||||
req = NewRequest(t, "GET", "/api/v1/admin/users")
|
||||
|
||||
// add our cert to the request
|
||||
certString := base64.RawStdEncoding.EncodeToString(pkcert.(*ssh.Certificate).Marshal())
|
||||
req.SetHeader("x-ssh-certificate", certString)
|
||||
|
||||
signer, _, err := httpsig.NewSSHSigner(certSigner, httpsig.DigestSha512, []string{httpsig.RequestTarget, "(created)", "(expires)", "x-ssh-certificate"}, httpsig.Signature, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// sign the request
|
||||
err = signer.SignRequest(keyID, req.Request, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// make the request
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIGetIssueAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, issue.Index, attachment.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
|
||||
}
|
||||
|
||||
func TestAPIListIssueAttachments(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index)).
|
||||
AddTokenAuth(token)
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
apiAttachment := DecodeJSON(t, resp, []api.Attachment{})
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment[0].ID, IssueID: issue.ID})
|
||||
}
|
||||
|
||||
func TestAPICreateIssueAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
// Setup multi-part
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", "image.png")
|
||||
assert.NoError(t, err)
|
||||
_, err = part.Write(testGeneratePngBytes())
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body).
|
||||
AddTokenAuth(token)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID})
|
||||
}
|
||||
|
||||
func TestAPICreateIssueAttachmentWithUnallowedFile(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
filename := "file.bad"
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
// Setup multi-part.
|
||||
writer := multipart.NewWriter(body)
|
||||
_, err := writer.CreateFormFile("attachment", filename)
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets", repoOwner.Name, repo.Name, issue.Index), body).
|
||||
AddTokenAuth(token)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
|
||||
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
func TestAPIEditIssueAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
const newAttachmentName = "hello_world.txt"
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d",
|
||||
repoOwner.Name, repo.Name, issue.Index, attachment.ID)
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
"name": newAttachmentName,
|
||||
}).AddTokenAuth(token)
|
||||
resp := session.MakeRequest(t, req, http.StatusCreated)
|
||||
apiAttachment := DecodeJSON(t, resp, &api.Attachment{})
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: apiAttachment.ID, IssueID: issue.ID, Name: apiAttachment.Name})
|
||||
}
|
||||
|
||||
func TestAPIEditIssueAttachmentWithUnallowedFile(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: attachment.IssueID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
filename := "file.bad"
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, issue.Index, attachment.ID)
|
||||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
|
||||
"name": filename,
|
||||
}).AddTokenAuth(token)
|
||||
|
||||
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
func TestAPIDeleteIssueAttachment(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: attachment.IssueID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d", repoOwner.Name, repo.Name, issue.Index, attachment.ID)).
|
||||
AddTokenAuth(token)
|
||||
session.MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Attachment{ID: attachment.ID, IssueID: issue.ID})
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func createIssueConfig(t *testing.T, user *user_model.User, repo *repo_model.Repository, issueConfig map[string]any) {
|
||||
config, err := yaml.Marshal(issueConfig)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/config.yaml", repo.DefaultBranch, string(config))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func getIssueConfig(t *testing.T, owner, repo string) api.IssueConfig {
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config", owner, repo)
|
||||
req := NewRequest(t, "GET", urlStr)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
issueConfig := DecodeJSON(t, resp, &api.IssueConfig{})
|
||||
|
||||
return *issueConfig
|
||||
}
|
||||
|
||||
func TestAPIRepoGetIssueConfig(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
issueConfig := getIssueConfig(t, owner.Name, repo.Name)
|
||||
|
||||
assert.True(t, issueConfig.BlankIssuesEnabled)
|
||||
assert.Empty(t, issueConfig.ContactLinks)
|
||||
})
|
||||
|
||||
t.Run("DisableBlankIssues", func(t *testing.T) {
|
||||
config := make(map[string]any)
|
||||
config["blank_issues_enabled"] = false
|
||||
|
||||
createIssueConfig(t, owner, repo, config)
|
||||
|
||||
issueConfig := getIssueConfig(t, owner.Name, repo.Name)
|
||||
|
||||
assert.False(t, issueConfig.BlankIssuesEnabled)
|
||||
assert.Empty(t, issueConfig.ContactLinks)
|
||||
})
|
||||
|
||||
t.Run("ContactLinks", func(t *testing.T) {
|
||||
contactLink := make(map[string]string)
|
||||
contactLink["name"] = "TestName"
|
||||
contactLink["url"] = "https://example.com"
|
||||
contactLink["about"] = "TestAbout"
|
||||
|
||||
config := make(map[string]any)
|
||||
config["contact_links"] = []map[string]string{contactLink}
|
||||
|
||||
createIssueConfig(t, owner, repo, config)
|
||||
|
||||
issueConfig := getIssueConfig(t, owner.Name, repo.Name)
|
||||
|
||||
assert.True(t, issueConfig.BlankIssuesEnabled)
|
||||
assert.Len(t, issueConfig.ContactLinks, 1)
|
||||
|
||||
assert.Equal(t, "TestName", issueConfig.ContactLinks[0].Name)
|
||||
assert.Equal(t, "https://example.com", issueConfig.ContactLinks[0].URL)
|
||||
assert.Equal(t, "TestAbout", issueConfig.ContactLinks[0].About)
|
||||
})
|
||||
|
||||
t.Run("Full", func(t *testing.T) {
|
||||
contactLink := make(map[string]string)
|
||||
contactLink["name"] = "TestName"
|
||||
contactLink["url"] = "https://example.com"
|
||||
contactLink["about"] = "TestAbout"
|
||||
|
||||
config := make(map[string]any)
|
||||
config["blank_issues_enabled"] = false
|
||||
config["contact_links"] = []map[string]string{contactLink}
|
||||
|
||||
createIssueConfig(t, owner, repo, config)
|
||||
|
||||
issueConfig := getIssueConfig(t, owner.Name, repo.Name)
|
||||
|
||||
assert.False(t, issueConfig.BlankIssuesEnabled)
|
||||
assert.Len(t, issueConfig.ContactLinks, 1)
|
||||
|
||||
assert.Equal(t, "TestName", issueConfig.ContactLinks[0].Name)
|
||||
assert.Equal(t, "https://example.com", issueConfig.ContactLinks[0].URL)
|
||||
assert.Equal(t, "TestAbout", issueConfig.ContactLinks[0].About)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIRepoIssueConfigPaths(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
templateConfigCandidates := []string{
|
||||
".gitea/ISSUE_TEMPLATE/config",
|
||||
".gitea/issue_template/config",
|
||||
".github/ISSUE_TEMPLATE/config",
|
||||
".github/issue_template/config",
|
||||
}
|
||||
|
||||
for _, candidate := range templateConfigCandidates {
|
||||
for _, extension := range []string{".yaml", ".yml"} {
|
||||
fullPath := candidate + extension
|
||||
t.Run(fullPath, func(t *testing.T) {
|
||||
configMap := make(map[string]any)
|
||||
configMap["blank_issues_enabled"] = false
|
||||
|
||||
configData, err := yaml.Marshal(configMap)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = createFile(owner, repo, fullPath, string(configData))
|
||||
assert.NoError(t, err)
|
||||
|
||||
issueConfig := getIssueConfig(t, owner.Name, repo.Name)
|
||||
|
||||
assert.False(t, issueConfig.BlankIssuesEnabled)
|
||||
assert.Empty(t, issueConfig.ContactLinks)
|
||||
|
||||
_, err = deleteFileInBranch(owner, repo, fullPath, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIRepoValidateIssueConfig(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 49})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issue_config/validate", owner.Name, repo.Name)
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", urlStr)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
issueConfigValidation := DecodeJSON(t, resp, &api.IssueConfigValidation{})
|
||||
|
||||
assert.True(t, issueConfigValidation.Valid)
|
||||
assert.Empty(t, issueConfigValidation.Message)
|
||||
})
|
||||
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
config := make(map[string]any)
|
||||
config["blank_issues_enabled"] = "Test"
|
||||
|
||||
createIssueConfig(t, owner, repo, config)
|
||||
|
||||
req := NewRequest(t, "GET", urlStr)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
issueConfigValidation := DecodeJSON(t, resp, &api.IssueConfigValidation{})
|
||||
|
||||
assert.False(t, issueConfigValidation.Valid)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/perm"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func enableRepoDependencies(t *testing.T, repoID int64) {
|
||||
t.Helper()
|
||||
|
||||
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypeIssues})
|
||||
repoUnit.IssuesConfig().EnableDependencies = true
|
||||
assert.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
|
||||
}
|
||||
|
||||
func TestAPICreateIssueDependencyCrossRepoPermission(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
|
||||
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.True(t, dependencyRepo.IsPrivate)
|
||||
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
|
||||
|
||||
enableRepoDependencies(t, targetIssue.RepoID)
|
||||
enableRepoDependencies(t, dependencyRepo.ID)
|
||||
|
||||
// remove user 40 access from target repository
|
||||
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
|
||||
assert.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
|
||||
dependencyMeta := &api.IssueMeta{
|
||||
Owner: "org3",
|
||||
Name: "repo3",
|
||||
Index: dependencyIssue.Index,
|
||||
}
|
||||
|
||||
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
|
||||
// user40 has no access to both target issue and dependency issue
|
||||
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestWithJSON(t, "POST", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
|
||||
// add user40 as a collaborator to dependency repository with read permission
|
||||
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
|
||||
|
||||
// try again after getting read permission to dependency repository
|
||||
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
|
||||
// add user40 as a collaborator to target repository with write permission
|
||||
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIDeleteIssueDependencyCrossRepoPermission(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
targetRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
targetIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: targetRepo.ID, Index: 1})
|
||||
dependencyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.True(t, dependencyRepo.IsPrivate)
|
||||
dependencyIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: dependencyRepo.ID, Index: 1})
|
||||
|
||||
enableRepoDependencies(t, targetIssue.RepoID)
|
||||
enableRepoDependencies(t, dependencyRepo.ID)
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
assert.NoError(t, issues_model.CreateIssueDependency(t.Context(), user1, targetIssue, dependencyIssue))
|
||||
|
||||
// remove user 40 access from target repository
|
||||
_, err := db.DeleteByID[access_model.Access](t.Context(), 30)
|
||||
assert.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/dependencies", "user2", "repo1", targetIssue.Index)
|
||||
dependencyMeta := &api.IssueMeta{
|
||||
Owner: "org3",
|
||||
Name: "repo3",
|
||||
Index: dependencyIssue.Index,
|
||||
}
|
||||
|
||||
user40 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 40})
|
||||
// user40 has no access to both target issue and dependency issue
|
||||
writerToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
|
||||
req := NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
|
||||
// add user40 as a collaborator to dependency repository with read permission
|
||||
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), dependencyRepo, user40, perm.AccessModeRead))
|
||||
|
||||
// try again after getting read permission to dependency repository
|
||||
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
|
||||
// add user40 as a collaborator to target repository with write permission
|
||||
assert.NoError(t, repo_service.AddOrUpdateCollaborator(t.Context(), targetRepo, user40, perm.AccessModeWrite))
|
||||
|
||||
req = NewRequestWithJSON(t, "DELETE", url, dependencyMeta).
|
||||
AddTokenAuth(writerToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueDependency{
|
||||
IssueID: targetIssue.ID,
|
||||
DependencyID: dependencyIssue.ID,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIModifyLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
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/labels", owner.Name, repo.Name)
|
||||
|
||||
// CreateLabel
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||
Name: "TestL 1",
|
||||
Color: "abcdef",
|
||||
Description: "test label",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiLabel := DecodeJSON(t, resp, &api.Label{})
|
||||
dbLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: apiLabel.ID, RepoID: repo.ID})
|
||||
assert.Equal(t, dbLabel.Name, apiLabel.Name)
|
||||
assert.Equal(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||
Name: "TestL 2",
|
||||
Color: "#123456",
|
||||
Description: "jet another test label",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||
Name: "WrongTestL",
|
||||
Color: "#12345g",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
// ListLabels
|
||||
req = NewRequest(t, "GET", urlStr).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiLabels := DecodeJSON(t, resp, []*api.Label{})
|
||||
assert.Len(t, apiLabels, 2)
|
||||
|
||||
// GetLabel
|
||||
singleURLStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels/%d", owner.Name, repo.Name, dbLabel.ID)
|
||||
req = NewRequest(t, "GET", singleURLStr).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiLabel = DecodeJSON(t, resp, &api.Label{})
|
||||
assert.Equal(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
|
||||
|
||||
// EditLabel
|
||||
newName := "LabelNewName"
|
||||
newColor := "09876a"
|
||||
newColorWrong := "09g76a"
|
||||
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
|
||||
Name: &newName,
|
||||
Color: &newColor,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiLabel = DecodeJSON(t, resp, &api.Label{})
|
||||
assert.Equal(t, newColor, apiLabel.Color)
|
||||
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
|
||||
Color: &newColorWrong,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
// DeleteLabel
|
||||
req = NewRequest(t, "DELETE", singleURLStr).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPIAddIssueLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
_ = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID, ID: 2})
|
||||
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/labels",
|
||||
repo.OwnerName, repo.Name, issue.Index)
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
|
||||
Labels: []any{1, 2},
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiLabels := DecodeJSON(t, resp, []*api.Label{})
|
||||
assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID}))
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: 2})
|
||||
}
|
||||
|
||||
func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) {
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6, RepoID: repo.ID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
repoLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 10, RepoID: repo.ID})
|
||||
orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 4, OrgID: owner.ID})
|
||||
|
||||
user1Session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// add the org label and the repo label to the issue
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index)
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
|
||||
Labels: []any{repoLabel.Name, orgLabel.Name},
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiLabels := DecodeJSON(t, resp, []*api.Label{})
|
||||
assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID}))
|
||||
var apiLabelNames []string
|
||||
for _, label := range apiLabels {
|
||||
apiLabelNames = append(apiLabelNames, label.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, apiLabelNames, []string{repoLabel.Name, orgLabel.Name})
|
||||
|
||||
// delete labels
|
||||
req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPIReplaceIssueLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID})
|
||||
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/labels",
|
||||
owner.Name, repo.Name, issue.Index)
|
||||
req := NewRequestWithJSON(t, "PUT", urlStr, &api.IssueLabelsOption{
|
||||
Labels: []any{label.ID},
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiLabels := DecodeJSON(t, resp, []*api.Label{})
|
||||
if assert.Len(t, apiLabels, 1) {
|
||||
assert.Equal(t, label.ID, apiLabels[0].ID)
|
||||
}
|
||||
|
||||
unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issue.ID}, 1)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
|
||||
}
|
||||
|
||||
func TestAPIReplaceIssueLabelsWithLabelNames(t *testing.T) {
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{RepoID: repo.ID})
|
||||
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/labels",
|
||||
owner.Name, repo.Name, issue.Index)
|
||||
req := NewRequestWithJSON(t, "PUT", urlStr, &api.IssueLabelsOption{
|
||||
Labels: []any{label.Name},
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiLabels := DecodeJSON(t, resp, []*api.Label{})
|
||||
if assert.Len(t, apiLabels, 1) {
|
||||
assert.Equal(t, label.Name, apiLabels[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIModifyOrgLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
user := "user1"
|
||||
session := loginUser(t, user)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
|
||||
urlStr := fmt.Sprintf("/api/v1/orgs/%s/labels", owner.Name)
|
||||
|
||||
// CreateLabel
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||
Name: "TestL 1",
|
||||
Color: "abcdef",
|
||||
Description: "test label",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiLabel := DecodeJSON(t, resp, &api.Label{})
|
||||
dbLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: apiLabel.ID, OrgID: owner.ID})
|
||||
assert.Equal(t, dbLabel.Name, apiLabel.Name)
|
||||
assert.Equal(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||
Name: "TestL 2",
|
||||
Color: "#123456",
|
||||
Description: "jet another test label",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateLabelOption{
|
||||
Name: "WrongTestL",
|
||||
Color: "#12345g",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
// ListLabels
|
||||
req = NewRequest(t, "GET", urlStr).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiLabels := DecodeJSON(t, resp, []*api.Label{})
|
||||
assert.Len(t, apiLabels, 4)
|
||||
|
||||
// GetLabel
|
||||
singleURLStr := fmt.Sprintf("/api/v1/orgs/%s/labels/%d", owner.Name, dbLabel.ID)
|
||||
req = NewRequest(t, "GET", singleURLStr).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiLabel = DecodeJSON(t, resp, &api.Label{})
|
||||
assert.Equal(t, strings.TrimLeft(dbLabel.Color, "#"), apiLabel.Color)
|
||||
|
||||
// EditLabel
|
||||
newName := "LabelNewName"
|
||||
newColor := "09876a"
|
||||
newColorWrong := "09g76a"
|
||||
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
|
||||
Name: &newName,
|
||||
Color: &newColor,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiLabel = DecodeJSON(t, resp, &api.Label{})
|
||||
assert.Equal(t, newColor, apiLabel.Color)
|
||||
req = NewRequestWithJSON(t, "PATCH", singleURLStr, &api.EditLabelOption{
|
||||
Color: &newColorWrong,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
// DeleteLabel
|
||||
req = NewRequest(t, "DELETE", singleURLStr).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPILockIssue(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("Lock", func(t *testing.T) {
|
||||
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
assert.False(t, issueBefore.IsLocked)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/lock", owner.Name, repo.Name, issueBefore.Index)
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// check lock issue
|
||||
req := NewRequestWithJSON(t, "PUT", urlStr, api.LockIssueOption{Reason: "Spam"}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
assert.True(t, issueAfter.IsLocked)
|
||||
|
||||
// check with other user
|
||||
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
||||
session34 := loginUser(t, user34.Name)
|
||||
token34 := getTokenForLoggedInUser(t, session34, auth_model.AccessTokenScopeAll)
|
||||
req = NewRequestWithJSON(t, "PUT", urlStr, api.LockIssueOption{Reason: "Spam"}).AddTokenAuth(token34)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("Unlock", func(t *testing.T) {
|
||||
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/lock", owner.Name, repo.Name, issueBefore.Index)
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
lockReq := NewRequestWithJSON(t, "PUT", urlStr, api.LockIssueOption{Reason: "Spam"}).AddTokenAuth(token)
|
||||
MakeRequest(t, lockReq, http.StatusNoContent)
|
||||
|
||||
// check unlock issue
|
||||
req := NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
assert.False(t, issueAfter.IsLocked)
|
||||
|
||||
// check with other user
|
||||
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
||||
session34 := loginUser(t, user34.Name)
|
||||
token34 := getTokenForLoggedInUser(t, session34, auth_model.AccessTokenScopeAll)
|
||||
req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token34)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIIssuesMilestone(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: milestone.RepoID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
assert.Equal(t, int64(1), int64(milestone.NumIssues))
|
||||
assert.Equal(t, structs.StateOpen, milestone.State())
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// update values of issue
|
||||
milestoneState := "closed"
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%d", owner.Name, repo.Name, milestone.ID)
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, structs.EditMilestoneOption{
|
||||
State: &milestoneState,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiMilestone := DecodeJSON(t, resp, &structs.Milestone{})
|
||||
assert.EqualValues(t, "closed", apiMilestone.State)
|
||||
|
||||
req = NewRequest(t, "GET", urlStr).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiMilestone2 := DecodeJSON(t, resp, &structs.Milestone{})
|
||||
assert.EqualValues(t, "closed", apiMilestone2.State)
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/milestones", owner.Name, repo.Name), structs.CreateMilestoneOption{
|
||||
Title: "wow",
|
||||
Description: "closed one",
|
||||
State: "closed",
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
apiMilestone = DecodeJSON(t, resp, &structs.Milestone{})
|
||||
assert.Equal(t, "wow", apiMilestone.Title)
|
||||
assert.Equal(t, structs.StateClosed, apiMilestone.State)
|
||||
assert.Nil(t, apiMilestone.Deadline)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s", owner.Name, repo.Name, "all")).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiMilestones := DecodeJSON(t, resp, []structs.Milestone{})
|
||||
assert.Len(t, apiMilestones, 4)
|
||||
assert.Nil(t, apiMilestones[0].Deadline)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%s", owner.Name, repo.Name, apiMilestones[2].Title)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiMilestone = DecodeJSON(t, resp, &structs.Milestone{})
|
||||
assert.Equal(t, apiMilestones[2], *apiMilestone)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/milestones?state=%s&name=%s", owner.Name, repo.Name, "all", "milestone2")).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiMilestones = DecodeJSON(t, resp, []structs.Milestone{})
|
||||
assert.Len(t, apiMilestones, 1)
|
||||
assert.Equal(t, int64(2), apiMilestones[0].ID)
|
||||
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/milestones/%d", owner.Name, repo.Name, apiMilestone.ID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIPinIssue(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Pin the Issue
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Check if the Issue is pinned
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
issueAPI := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, 1, issueAPI.PinOrder)
|
||||
}
|
||||
|
||||
func TestAPIUnpinIssue(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Pin the Issue
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Check if the Issue is pinned
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
issueAPI := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, 1, issueAPI.PinOrder)
|
||||
|
||||
// Unpin the Issue
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Check if the Issue is no longer pinned
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
issueAPI = DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, 0, issueAPI.PinOrder)
|
||||
}
|
||||
|
||||
func TestAPIMoveIssuePin(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, RepoID: repo.ID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Pin the first Issue
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Check if the first Issue is pinned at position 1
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
issueAPI := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, 1, issueAPI.PinOrder)
|
||||
|
||||
// Pin the second Issue
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue2.Index)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Move the first Issue to position 2
|
||||
req = NewRequest(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin/2", repo.OwnerName, repo.Name, issue.Index)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Check if the first Issue is pinned at position 2
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue.Index))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
issueAPI3 := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, 2, issueAPI3.PinOrder)
|
||||
|
||||
// Check if the second Issue is pinned at position 1
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", repo.OwnerName, repo.Name, issue2.Index))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
issueAPI4 := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, 1, issueAPI4.PinOrder)
|
||||
}
|
||||
|
||||
func TestAPIListPinnedIssues(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Pin the Issue
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin", repo.OwnerName, repo.Name, issue.Index)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Check if the Issue is in the List
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/pinned", repo.OwnerName, repo.Name))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
issueList := DecodeJSON(t, resp, []api.Issue{})
|
||||
|
||||
assert.Len(t, issueList, 1)
|
||||
assert.Equal(t, issue.ID, issueList[0].ID)
|
||||
}
|
||||
|
||||
func TestAPIListPinnedPullrequests(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/pulls/pinned", repo.OwnerName, repo.Name))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
prList := DecodeJSON(t, resp, []api.PullRequest{})
|
||||
|
||||
assert.Empty(t, prList)
|
||||
}
|
||||
|
||||
func TestAPINewPinAllowed(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/new_pin_allowed", owner.Name, repo.Name))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
newPinsAllowed := DecodeJSON(t, resp, &api.NewIssuePinsAllowed{})
|
||||
|
||||
assert.True(t, newPinsAllowed.Issues)
|
||||
assert.True(t, newPinsAllowed.PullRequests)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIIssuesReactions(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
_ = issue.LoadRepo(t.Context())
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions", owner.Name, issue.Repo.Name, issue.Index)
|
||||
|
||||
// Try to add not allowed reaction
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "wrong",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// Delete not allowed reaction
|
||||
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
|
||||
Reaction: "zzz",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Add allowed reaction
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "rocket",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiNewReaction := DecodeJSON(t, resp, &api.Reaction{})
|
||||
|
||||
// Add existing reaction
|
||||
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})
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "rocket",
|
||||
}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue))
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// Get end result of reaction list of issue #1
|
||||
req = NewRequest(t, "GET", urlStr).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiReactions := DecodeJSON(t, resp, []*api.Reaction{})
|
||||
expectResponse := make(map[int]api.Reaction)
|
||||
expectResponse[0] = api.Reaction{
|
||||
User: convert.ToUser(t.Context(), user2, user2),
|
||||
Reaction: "eyes",
|
||||
Created: time.Unix(1573248003, 0),
|
||||
}
|
||||
expectResponse[1] = *apiNewReaction
|
||||
assert.Len(t, apiReactions, 2)
|
||||
for i, r := range apiReactions {
|
||||
assert.Equal(t, expectResponse[i].Reaction, r.Reaction)
|
||||
assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix())
|
||||
assert.Equal(t, expectResponse[i].User.ID, r.User.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPICommentReactions(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2})
|
||||
_ = comment.LoadIssue(t.Context())
|
||||
issue := comment.Issue
|
||||
_ = issue.LoadRepo(t.Context())
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions", owner.Name, issue.Repo.Name, comment.ID)
|
||||
|
||||
// Try to add not allowed reaction
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "wrong",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// Delete none existing reaction
|
||||
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
|
||||
Reaction: "eyes",
|
||||
}).AddTokenAuth(token)
|
||||
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
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/reactions", repoOwner.Name, repo.Name, comment.ID)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequest(t, "GET", urlStr).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
// Add allowed reaction
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiNewReaction := DecodeJSON(t, resp, &api.Reaction{})
|
||||
|
||||
// Add existing reaction
|
||||
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).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiReactions := DecodeJSON(t, resp, []*api.Reaction{})
|
||||
expectResponse := make(map[int]api.Reaction)
|
||||
expectResponse[0] = api.Reaction{
|
||||
User: convert.ToUser(t.Context(), user2, user2),
|
||||
Reaction: "laugh",
|
||||
Created: time.Unix(1573248004, 0),
|
||||
}
|
||||
expectResponse[1] = api.Reaction{
|
||||
User: convert.ToUser(t.Context(), user1, user1),
|
||||
Reaction: "laugh",
|
||||
Created: time.Unix(1573248005, 0),
|
||||
}
|
||||
expectResponse[2] = *apiNewReaction
|
||||
assert.Len(t, apiReactions, 3)
|
||||
for i, r := range apiReactions {
|
||||
assert.Equal(t, expectResponse[i].Reaction, r.Reaction)
|
||||
assert.Equal(t, expectResponse[i].Created.Unix(), r.Created.Unix())
|
||||
assert.Equal(t, expectResponse[i].User.ID, r.User.ID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIListStopWatches(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopeReadUser)
|
||||
req := NewRequest(t, "GET", "/api/v1/user/stopwatches").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiWatches := DecodeJSON(t, resp, []*api.StopWatch{})
|
||||
stopwatch := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: owner.ID})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: stopwatch.IssueID})
|
||||
if assert.Len(t, apiWatches, 1) {
|
||||
assert.Equal(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
|
||||
assert.Equal(t, issue.Index, apiWatches[0].IssueIndex)
|
||||
assert.Equal(t, issue.Title, apiWatches[0].IssueTitle)
|
||||
assert.Equal(t, repo.Name, apiWatches[0].RepoName)
|
||||
assert.Equal(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
|
||||
assert.Positive(t, apiWatches[0].Seconds)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIStopStopWatches(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
_ = issue.LoadRepo(t.Context())
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/issues/%d/stopwatch/stop", owner.Name, issue.Repo.Name, issue.Index).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
}
|
||||
|
||||
func TestAPICancelStopWatches(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
_ = issue.LoadRepo(t.Context())
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/stopwatch/delete", owner.Name, issue.Repo.Name, issue.Index).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
}
|
||||
|
||||
func TestAPIStartStopWatches(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||
_ = issue.LoadRepo(t.Context())
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/issues/%d/stopwatch/start", owner.Name, issue.Repo.Name, issue.Index).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIIssueSubscriptions(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
issue3 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||
issue4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4})
|
||||
issue5 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 8})
|
||||
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue1.PosterID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
testSubscription := func(issue *issues_model.Issue, isWatching bool) {
|
||||
issueRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/check", issueRepo.OwnerName, issueRepo.Name, issue.Index)).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
wi := DecodeJSON(t, resp, &api.WatchInfo{})
|
||||
|
||||
assert.Equal(t, isWatching, wi.Subscribed)
|
||||
assert.Equal(t, !isWatching, wi.Ignored)
|
||||
assert.Equal(t, issue.APIURL(t.Context())+"/subscriptions", wi.URL)
|
||||
assert.EqualValues(t, issue.CreatedUnix, wi.CreatedAt.Unix())
|
||||
assert.Equal(t, issueRepo.APIURL(), wi.RepositoryURL)
|
||||
}
|
||||
|
||||
testSubscription(issue1, true)
|
||||
testSubscription(issue2, true)
|
||||
testSubscription(issue3, true)
|
||||
testSubscription(issue4, false)
|
||||
testSubscription(issue5, false)
|
||||
|
||||
issue1Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue1.RepoID})
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s", issue1Repo.OwnerName, issue1Repo.Name, issue1.Index, owner.Name)
|
||||
req := NewRequest(t, "DELETE", urlStr).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
testSubscription(issue1, false)
|
||||
|
||||
req = NewRequest(t, "DELETE", urlStr).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
testSubscription(issue1, false)
|
||||
|
||||
issue5Repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue5.RepoID})
|
||||
urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s", issue5Repo.OwnerName, issue5Repo.Name, issue5.Index, owner.Name)
|
||||
req = NewRequest(t, "PUT", urlStr).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
testSubscription(issue5, true)
|
||||
|
||||
req = NewRequest(t, "PUT", urlStr).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
testSubscription(issue5, true)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIIssueTemplateList(t *testing.T) {
|
||||
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
|
||||
|
||||
// no issue template
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issue_templates")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
issueTemplates := DecodeJSON(t, resp, []*api.IssueTemplate{})
|
||||
assert.Empty(t, issueTemplates)
|
||||
|
||||
// one correct issue template and some incorrect issue templates
|
||||
err := createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-ok.md", repo.DefaultBranch, `----
|
||||
name: foo
|
||||
about: bar
|
||||
----
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-err1.yml", repo.DefaultBranch, `name: '`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = createOrReplaceFileInBranch(user, repo, ".gitea/ISSUE_TEMPLATE/tmpl-err2.yml", repo.DefaultBranch, `other: `)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/issue_templates")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
issueTemplates = DecodeJSON(t, resp, []*api.IssueTemplate{})
|
||||
assert.Len(t, issueTemplates, 1)
|
||||
assert.Equal(t, "foo", issueTemplates[0].Name)
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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)
|
||||
t.Run("IssueProjects", testAPIIssueProjects)
|
||||
}
|
||||
|
||||
func testAPIListIssues(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name))
|
||||
|
||||
link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode()
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||
apiIssues := DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}))
|
||||
for _, apiIssue := range apiIssues {
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: apiIssue.ID, RepoID: repo.ID})
|
||||
}
|
||||
|
||||
// test milestone filter
|
||||
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "type": {"all"}, "milestones": {"ignore,milestone1,3,4"}}.Encode()
|
||||
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
if assert.Len(t, apiIssues, 2) {
|
||||
assert.EqualValues(t, 3, apiIssues[0].Milestone.ID)
|
||||
assert.EqualValues(t, 1, apiIssues[1].Milestone.ID)
|
||||
}
|
||||
|
||||
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "created_by": {"user2"}}.Encode()
|
||||
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
if assert.Len(t, apiIssues, 1) {
|
||||
assert.EqualValues(t, 5, apiIssues[0].ID)
|
||||
}
|
||||
|
||||
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "assigned_by": {"user1"}}.Encode()
|
||||
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
if assert.Len(t, apiIssues, 1) {
|
||||
assert.EqualValues(t, 1, apiIssues[0].ID)
|
||||
}
|
||||
|
||||
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "mentioned_by": {"user4"}}.Encode()
|
||||
resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
if assert.Len(t, apiIssues, 1) {
|
||||
assert.EqualValues(t, 1, apiIssues[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func testAPIListIssuesPublicOnly(t *testing.T) {
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID})
|
||||
|
||||
session := loginUser(t, owner1.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner1.Name, repo1.Name))
|
||||
link.RawQuery = url.Values{"state": {"all"}}.Encode()
|
||||
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
owner2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo2.OwnerID})
|
||||
|
||||
session = loginUser(t, owner2.Name)
|
||||
token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner2.Name, repo2.Name))
|
||||
link.RawQuery = url.Values{"state": {"all"}}.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testAPICreateIssue(t *testing.T) {
|
||||
const body, title = "apiTestBody", "apiTestTitle"
|
||||
|
||||
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name)
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
|
||||
Body: body,
|
||||
Title: title,
|
||||
Assignee: owner.Name,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiIssue := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, body, apiIssue.Body)
|
||||
assert.Equal(t, title, apiIssue.Title)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
||||
RepoID: repoBefore.ID,
|
||||
AssigneeID: owner.ID,
|
||||
Content: body,
|
||||
Title: title,
|
||||
})
|
||||
|
||||
repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
assert.Equal(t, repoBefore.NumIssues+1, repoAfter.NumIssues)
|
||||
assert.Equal(t, repoBefore.NumClosedIssues, repoAfter.NumClosedIssues)
|
||||
|
||||
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
|
||||
Title: title,
|
||||
}).AddTokenAuth(getUserToken(t, user34.Name, auth_model.AccessTokenScopeWriteIssue))
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func testAPICreateIssueParallel(t *testing.T) {
|
||||
// HINT: 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 released.
|
||||
//
|
||||
// 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) } }
|
||||
// Usually the test gets stuck in fewer than 10 iterations without this "sleep".
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
const body, title = "apiTestBody", "apiTestTitle"
|
||||
|
||||
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range 10 {
|
||||
wg.Go(func() {
|
||||
t.Run(fmt.Sprintf("ParallelCreateIssue_%d", i), func(t *testing.T) {
|
||||
newTitle := title + strconv.Itoa(i)
|
||||
newBody := body + strconv.Itoa(i)
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
|
||||
Body: newBody,
|
||||
Title: newTitle,
|
||||
Assignee: owner.Name,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiIssue := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, newBody, apiIssue.Body)
|
||||
assert.Equal(t, newTitle, apiIssue.Title)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{
|
||||
RepoID: repoBefore.ID,
|
||||
AssigneeID: owner.ID,
|
||||
Content: newBody,
|
||||
Title: newTitle,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
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})
|
||||
assert.NoError(t, issueBefore.LoadAttributes(t.Context()))
|
||||
assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
|
||||
assert.Equal(t, api.StateOpen, issueBefore.State())
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// update values of issue
|
||||
issueState := "closed"
|
||||
removeDeadline := true
|
||||
milestone := int64(4)
|
||||
body := "new content!"
|
||||
title := "new title from api set"
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
State: &issueState,
|
||||
RemoveDeadline: &removeDeadline,
|
||||
Milestone: &milestone,
|
||||
Body: &body,
|
||||
Title: title,
|
||||
|
||||
// ToDo change more
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiIssue := DecodeJSON(t, resp, &api.Issue{})
|
||||
|
||||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
||||
repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
||||
|
||||
// check comment history
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: issueAfter.ID, OldTitle: issueBefore.Title, NewTitle: title})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: issueAfter.ID, ContentText: body, IsFirstCreated: false})
|
||||
|
||||
// check deleted user
|
||||
assert.Equal(t, int64(500), issueAfter.PosterID)
|
||||
assert.NoError(t, issueAfter.LoadAttributes(t.Context()))
|
||||
assert.Equal(t, int64(-1), issueAfter.PosterID)
|
||||
assert.Equal(t, int64(-1), issueBefore.PosterID)
|
||||
assert.Equal(t, int64(-1), apiIssue.Poster.ID)
|
||||
|
||||
// check repo change
|
||||
assert.Equal(t, repoBefore.NumClosedIssues+1, repoAfter.NumClosedIssues)
|
||||
|
||||
// API response
|
||||
assert.Equal(t, api.StateClosed, apiIssue.State)
|
||||
assert.Equal(t, milestone, apiIssue.Milestone.ID)
|
||||
assert.Equal(t, body, apiIssue.Body)
|
||||
assert.Nil(t, apiIssue.Deadline)
|
||||
assert.Equal(t, title, apiIssue.Title)
|
||||
|
||||
// in database
|
||||
assert.Equal(t, api.StateClosed, issueAfter.State())
|
||||
assert.Equal(t, milestone, issueAfter.MilestoneID)
|
||||
assert.Equal(t, int64(0), int64(issueAfter.DeadlineUnix))
|
||||
assert.Equal(t, body, issueAfter.Content)
|
||||
assert.Equal(t, title, issueAfter.Title)
|
||||
}
|
||||
|
||||
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)
|
||||
query := url.Values{}
|
||||
var apiIssues []*api.Issue
|
||||
|
||||
link.RawQuery = query.Encode()
|
||||
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, expectedIssueCount)
|
||||
|
||||
publicOnlyToken := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 15) // 15 public issues
|
||||
|
||||
since := "2000-01-01T00:50:01+00:00" // 946687801
|
||||
before := time.Unix(999307200, 0).Format(time.RFC3339)
|
||||
query.Add("since", since)
|
||||
query.Add("before", before)
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 11)
|
||||
query.Del("since")
|
||||
query.Del("before")
|
||||
|
||||
query.Add("state", "closed")
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 2)
|
||||
|
||||
query.Set("state", "all")
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Equal(t, "22", resp.Header().Get("X-Total-Count"))
|
||||
assert.Len(t, apiIssues, 20)
|
||||
|
||||
query.Add("limit", "10")
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Equal(t, "22", resp.Header().Get("X-Total-Count"))
|
||||
assert.Len(t, apiIssues, 10)
|
||||
|
||||
query = url.Values{"assigned": {"true"}, "state": {"all"}}
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 2)
|
||||
|
||||
query = url.Values{"milestones": {"milestone1"}, "state": {"all"}}
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 1)
|
||||
|
||||
query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}}
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 2)
|
||||
|
||||
query = url.Values{"owner": {"user2"}} // user
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 8)
|
||||
|
||||
query = url.Values{"owner": {"org3"}} // organization
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 5)
|
||||
|
||||
query = url.Values{"owner": {"org3"}, "team": {"team1"}} // organization + team
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
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)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
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)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
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)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
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)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 3)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
link, _ := url.Parse("/api/v1/repos/issues/search")
|
||||
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue)
|
||||
query := url.Values{}
|
||||
var apiIssues []*api.Issue
|
||||
|
||||
link.RawQuery = query.Encode()
|
||||
req := NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, expectedIssueCount)
|
||||
|
||||
query.Add("labels", "label1")
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 2)
|
||||
|
||||
// multiple labels
|
||||
query.Set("labels", "label1,label2")
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 2)
|
||||
|
||||
// an org label
|
||||
query.Set("labels", "orglabel4")
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 1)
|
||||
|
||||
// org and repo label
|
||||
query.Set("labels", "label2,orglabel4")
|
||||
query.Add("state", "all")
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
assert.Len(t, apiIssues, 2)
|
||||
|
||||
// org and repo label which share the same issue
|
||||
query.Set("labels", "label1,orglabel4")
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssues = DecodeJSON(t, resp, []*api.Issue{})
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIIssueProjects(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name)
|
||||
|
||||
// Create issue with a project
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
|
||||
Title: "issue with project",
|
||||
Body: "test body",
|
||||
Projects: []int64{1},
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
apiIssue := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Len(t, apiIssue.Projects, 1)
|
||||
assert.EqualValues(t, 1, apiIssue.Projects[0].ID)
|
||||
|
||||
// Get issue should include projects
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%d", urlStr, apiIssue.Index)).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiIssue = DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Len(t, apiIssue.Projects, 1)
|
||||
assert.EqualValues(t, 1, apiIssue.Projects[0].ID)
|
||||
|
||||
// Edit issue to remove projects
|
||||
emptyProjects := []int64{}
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("%s/%d", urlStr, apiIssue.Index), &api.EditIssueOption{
|
||||
Projects: &emptyProjects,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
apiIssue = DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Empty(t, apiIssue.Projects)
|
||||
|
||||
// Edit issue to add project back
|
||||
projects := []int64{1}
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("%s/%d", urlStr, apiIssue.Index), &api.EditIssueOption{
|
||||
Projects: &projects,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
apiIssue = DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Len(t, apiIssue.Projects, 1)
|
||||
assert.EqualValues(t, 1, apiIssue.Projects[0].ID)
|
||||
|
||||
// Test invalid project ID
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
|
||||
Title: "issue with invalid project",
|
||||
Body: "test body",
|
||||
Projects: []int64{99999},
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
// Test project from different repo (project 2 is for repo 3)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
|
||||
Title: "issue with inaccessible project",
|
||||
Body: "test body",
|
||||
Projects: []int64{2},
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIGetTrackedTimes(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
assert.NoError(t, issue2.LoadRepo(t.Context()))
|
||||
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
|
||||
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/times", user2.Name, issue2.Repo.Name, issue2.Index).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiTimes := DecodeJSON(t, resp, api.TrackedTimeList{})
|
||||
expect, err := issues_model.GetTrackedTimes(t.Context(), &issues_model.FindTrackedTimesOptions{IssueID: issue2.ID})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, apiTimes, 3)
|
||||
|
||||
for i, time := range expect {
|
||||
assert.Equal(t, time.ID, apiTimes[i].ID)
|
||||
assert.Equal(t, issue2.Title, apiTimes[i].Issue.Title)
|
||||
assert.Equal(t, issue2.ID, apiTimes[i].IssueID)
|
||||
assert.Equal(t, time.Created.Unix(), apiTimes[i].Created.Unix())
|
||||
assert.Equal(t, time.Time, apiTimes[i].Time)
|
||||
user, err := user_model.GetUserByID(t.Context(), time.UserID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Name, apiTimes[i].UserName)
|
||||
}
|
||||
|
||||
// test filter
|
||||
since := "2000-01-01T00%3A00%3A02%2B00%3A00" // 946684802
|
||||
before := "2000-01-01T00%3A00%3A12%2B00%3A00" // 946684812
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/times?since=%s&before=%s", user2.Name, issue2.Repo.Name, issue2.Index, since, before).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
filterAPITimes := DecodeJSON(t, resp, api.TrackedTimeList{})
|
||||
assert.Len(t, filterAPITimes, 2)
|
||||
assert.Equal(t, int64(3), filterAPITimes[0].ID)
|
||||
assert.Equal(t, int64(6), filterAPITimes[1].ID)
|
||||
}
|
||||
|
||||
func TestAPIDeleteTrackedTime(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
time6 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 6})
|
||||
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
assert.NoError(t, issue2.LoadRepo(t.Context()))
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// Deletion not allowed
|
||||
req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time6.ID).
|
||||
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)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
// Delete non existing time
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// Reset time of user 2 on issue 2
|
||||
trackedSeconds, err := issues_model.GetTrackedSeconds(t.Context(), issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3661), trackedSeconds)
|
||||
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times", user2.Name, issue2.Repo.Name, issue2.Index).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
trackedSeconds, err = issues_model.GetTrackedSeconds(t.Context(), issues_model.FindTrackedTimesOptions{IssueID: 2, UserID: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), trackedSeconds)
|
||||
}
|
||||
|
||||
func TestAPIAddTrackedTimes(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2})
|
||||
assert.NoError(t, issue2.LoadRepo(t.Context()))
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
session := loginUser(t, admin.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/times", user2.Name, issue2.Repo.Name, issue2.Index)
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.AddTimeOption{
|
||||
Time: 33,
|
||||
User: user2.Name,
|
||||
Created: time.Unix(947688818, 0),
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiNewTime := DecodeJSON(t, resp, &api.TrackedTime{})
|
||||
|
||||
assert.EqualValues(t, 33, apiNewTime.Time)
|
||||
assert.Equal(t, user2.ID, apiNewTime.UserID)
|
||||
assert.EqualValues(t, 947688818, apiNewTime.Created.Unix())
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// Copyright 2017 The Gogs Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestViewDeployKeysNoLogin(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/keys")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestCreateDeployKeyNoLogin(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/keys", api.CreateKeyOption{
|
||||
Title: "title",
|
||||
Key: "key",
|
||||
})
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestGetDeployKeyNoLogin(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/keys/1")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestDeleteDeployKeyNoLogin(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
req := NewRequest(t, "DELETE", "/api/v1/repos/user2/repo1/keys/1")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestCreateReadOnlyDeployKey(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo1"})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name)
|
||||
rawKeyBody := api.CreateKeyOption{
|
||||
Title: "read-only",
|
||||
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
|
||||
ReadOnly: true,
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
newDeployKey := DecodeJSON(t, resp, &api.DeployKey{})
|
||||
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
|
||||
ID: newDeployKey.ID,
|
||||
Name: rawKeyBody.Title,
|
||||
Content: rawKeyBody.Key,
|
||||
Mode: perm.AccessModeRead,
|
||||
})
|
||||
|
||||
// Using the ID of a key that does not belong to the repository must fail
|
||||
{
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/keys/%d", repoOwner.Name, repo.Name, newDeployKey.ID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
session5 := loginUser(t, "user5")
|
||||
token5 := getTokenForLoggedInUser(t, session5, auth_model.AccessTokenScopeWriteRepository)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user5/repo4/keys/%d", newDeployKey.ID)).
|
||||
AddTokenAuth(token5)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateReadWriteDeployKey(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo1"})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, repoOwner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys", repoOwner.Name, repo.Name)
|
||||
rawKeyBody := api.CreateKeyOption{
|
||||
Title: "read-write",
|
||||
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", keysURL, rawKeyBody).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
newDeployKey := DecodeJSON(t, resp, &api.DeployKey{})
|
||||
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
|
||||
ID: newDeployKey.ID,
|
||||
Name: rawKeyBody.Title,
|
||||
Content: rawKeyBody.Key,
|
||||
Mode: perm.AccessModeWrite,
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateUserKey(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
token := url.QueryEscape(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser))
|
||||
keyType := "ssh-rsa"
|
||||
keyContent := "AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM="
|
||||
rawKeyBody := api.CreateKeyOption{
|
||||
Title: "test-key",
|
||||
Key: keyType + " " + keyContent,
|
||||
}
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", rawKeyBody).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
newPublicKey := DecodeJSON(t, resp, &api.PublicKey{})
|
||||
fingerprint, err := asymkey_model.CalcFingerprint(rawKeyBody.Key)
|
||||
assert.NoError(t, err)
|
||||
unittest.AssertExistsAndLoadBean(t, &asymkey_model.PublicKey{
|
||||
ID: newPublicKey.ID,
|
||||
OwnerID: user.ID,
|
||||
Name: rawKeyBody.Title,
|
||||
Fingerprint: fingerprint,
|
||||
Mode: perm.AccessModeWrite,
|
||||
})
|
||||
|
||||
// Search by fingerprint
|
||||
req = NewRequest(t, "GET", "/api/v1/user/keys?fingerprint="+newPublicKey.Fingerprint).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
fingerprintPublicKeys := DecodeJSON(t, resp, []api.PublicKey{})
|
||||
assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
|
||||
assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
|
||||
assert.Equal(t, user.ID, fingerprintPublicKeys[0].Owner.ID)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/keys?fingerprint=%s", user.Name, newPublicKey.Fingerprint)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
fingerprintPublicKeys = DecodeJSON(t, resp, []api.PublicKey{})
|
||||
assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
|
||||
assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
|
||||
assert.Equal(t, user.ID, fingerprintPublicKeys[0].Owner.ID)
|
||||
|
||||
// Fail search by fingerprint
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/keys?fingerprint=%sA", newPublicKey.Fingerprint)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
fingerprintPublicKeys = DecodeJSON(t, resp, []api.PublicKey{})
|
||||
assert.Empty(t, fingerprintPublicKeys)
|
||||
|
||||
// Fail searching for wrong users key
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/keys?fingerprint=%s", "user2", newPublicKey.Fingerprint)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
fingerprintPublicKeys = DecodeJSON(t, resp, []api.PublicKey{})
|
||||
assert.Empty(t, fingerprintPublicKeys)
|
||||
|
||||
// Now login as user 2
|
||||
session2 := loginUser(t, "user2")
|
||||
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
// Should find key even though not ours, but we shouldn't know whose it is
|
||||
req = NewRequest(t, "GET", "/api/v1/user/keys?fingerprint="+newPublicKey.Fingerprint).
|
||||
AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
fingerprintPublicKeys = DecodeJSON(t, resp, []api.PublicKey{})
|
||||
assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
|
||||
assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
|
||||
assert.Nil(t, fingerprintPublicKeys[0].Owner)
|
||||
|
||||
// Should find key even though not ours, but we shouldn't know whose it is
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/keys?fingerprint=%s", user.Name, newPublicKey.Fingerprint)).
|
||||
AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
fingerprintPublicKeys = DecodeJSON(t, resp, []api.PublicKey{})
|
||||
assert.Equal(t, newPublicKey.Fingerprint, fingerprintPublicKeys[0].Fingerprint)
|
||||
assert.Equal(t, newPublicKey.ID, fingerprintPublicKeys[0].ID)
|
||||
assert.Nil(t, fingerprintPublicKeys[0].Owner)
|
||||
|
||||
// Fail when searching for key if it is not ours
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/keys?fingerprint=%s", "user2", newPublicKey.Fingerprint)).
|
||||
AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
fingerprintPublicKeys = DecodeJSON(t, resp, []api.PublicKey{})
|
||||
assert.Empty(t, fingerprintPublicKeys)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user