mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-03-27 09:33:04 -04:00
Last known backend change for #11311, fixing up some loose ends on the repository APIs related to repo-specific access tokens. Adds automated testing, and aligns permissions where necessary, to ensure that repo-specific access tokens can't change the administrative state of the repositories that they are limited to. Repo-specific access tokens cannot be used to: - convert a mirror into a normal repo, - create a new repository from a template, - transfer ownership of a repository - create a new repository (already protected, but test automation added), - delete a repository (already protected, but test automation added), - editing a repository's settings (already protected, but test automation added). **Breaking**: The template generation (`POST /repos/{template_owner}/{template_repo}/generate`) and repository deletion (`DELETE /repos/{username}/{reponame}`) APIs have been updated to require the same permission scope as creating a new repository. Either `write:user` or `write:organization` is required, depending on the owner of the repository being created or deleted. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11736 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
281 lines
9 KiB
Go
281 lines
9 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
auth_model "forgejo.org/models/auth"
|
|
repo_model "forgejo.org/models/repo"
|
|
"forgejo.org/models/unittest"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/storage"
|
|
"forgejo.org/modules/test"
|
|
|
|
runnerv1 "code.forgejo.org/forgejo/actions-proto/runner/v1"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
func TestActionsDownloadTaskLogs(t *testing.T) {
|
|
if !setting.Database.Type.IsSQLite3() {
|
|
t.Skip()
|
|
}
|
|
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
|
|
`,
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
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
|
|
`,
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
zstdEnabled: false,
|
|
},
|
|
}
|
|
onApplicationRun(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"})
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(fmt.Sprintf("test %s", 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, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
|
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
|
|
|
|
// fetch and execute task
|
|
task := runner.fetchTask(t)
|
|
runner.execTask(t, task, tc.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)
|
|
require.NoError(t, err)
|
|
|
|
// download task logs and check content
|
|
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
|
|
attempt := task.Context.GetFields()["run_attempt"].GetStringValue()
|
|
logURL := fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/attempt/%s/logs", user2.Name, repo.Name, runIndex, attempt)
|
|
req := NewRequest(t, "GET", logURL)
|
|
req.AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
|
assert.Len(t, logTextLines, len(tc.outcome.logRows))
|
|
for idx, lr := range tc.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()
|
|
})
|
|
}
|
|
|
|
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteUser)
|
|
doAPIDeleteRepository(httpContext)(t)
|
|
})
|
|
}
|
|
|
|
func TestActionsDownloadTaskRerunLogs(t *testing.T) {
|
|
if !setting.Database.Type.IsSQLite3() {
|
|
t.Skip()
|
|
}
|
|
now := time.Now()
|
|
treePath := ".gitea/workflows/download-task-logs.yml"
|
|
fileContent := `name: download-task-logs
|
|
on:
|
|
push:
|
|
paths:
|
|
- '.gitea/workflows/download-task-logs.yml'
|
|
jobs:
|
|
job1:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- run: |
|
|
echo "Run attempt: ${{ github.run_attempt }}"
|
|
`
|
|
firstOutcome := &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: "Run attempt: 1",
|
|
},
|
|
{
|
|
Time: timestamppb.New(now.Add(3 * time.Second)),
|
|
Content: "\U0001F3C1 Job succeeded",
|
|
},
|
|
},
|
|
}
|
|
secondOutcome := &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: "Run attempt: 2",
|
|
},
|
|
{
|
|
Time: timestamppb.New(now.Add(3 * time.Second)),
|
|
Content: "\U0001F3C1 Job succeeded",
|
|
},
|
|
},
|
|
}
|
|
|
|
onApplicationRun(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)
|
|
|
|
repo := createActionsTestRepo(t, token, "actions-download-task-logs", false)
|
|
|
|
runner := newMockRunner()
|
|
runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"})
|
|
|
|
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, fmt.Sprintf("Create %s", treePath), fileContent)
|
|
createWorkflowFile(t, token, user2.Name, repo.Name, treePath, opts)
|
|
|
|
// Execute first run
|
|
task := runner.fetchTask(t)
|
|
runner.execTask(t, task, firstOutcome)
|
|
|
|
// Download task logs and check content
|
|
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
|
|
attempt := task.Context.GetFields()["run_attempt"].GetStringValue()
|
|
logURL := fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/attempt/%s/logs", user2.Name, repo.Name, runIndex, attempt)
|
|
logRequest := NewRequest(t, "GET", logURL)
|
|
logResponse := session.MakeRequest(t, logRequest, http.StatusOK)
|
|
logTextLines := strings.Split(strings.TrimSpace(logResponse.Body.String()), "\n")
|
|
assert.Len(t, logTextLines, len(firstOutcome.logRows))
|
|
for idx, lr := range firstOutcome.logRows {
|
|
assert.Equal(
|
|
t,
|
|
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
|
|
logTextLines[idx],
|
|
)
|
|
}
|
|
|
|
// Trigger rerun
|
|
rerunURL := fmt.Sprintf("/%s/%s/actions/runs/%s/rerun", user2.Name, repo.Name, runIndex)
|
|
rerunRequest := NewRequest(t, "POST", rerunURL)
|
|
session.MakeRequest(t, rerunRequest, http.StatusOK)
|
|
|
|
// Execute rerun task
|
|
rerunTask := runner.fetchTask(t)
|
|
runner.execTask(t, rerunTask, secondOutcome)
|
|
|
|
// Download rerun task logs and check content
|
|
rerunIndex := rerunTask.Context.GetFields()["run_number"].GetStringValue()
|
|
rerunAttempt := rerunTask.Context.GetFields()["run_attempt"].GetStringValue()
|
|
rerunLogURL := fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/attempt/%s/logs", user2.Name, repo.Name, rerunIndex, rerunAttempt)
|
|
rerunLogRequest := NewRequest(t, "GET", rerunLogURL)
|
|
rerunLogResponse := session.MakeRequest(t, rerunLogRequest, http.StatusOK)
|
|
rerunLogTextLines := strings.Split(strings.TrimSpace(rerunLogResponse.Body.String()), "\n")
|
|
assert.Len(t, rerunLogTextLines, len(secondOutcome.logRows))
|
|
for idx, lr := range secondOutcome.logRows {
|
|
assert.Equal(
|
|
t,
|
|
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
|
|
rerunLogTextLines[idx],
|
|
)
|
|
}
|
|
|
|
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteUser)
|
|
doAPIDeleteRepository(httpContext)(t)
|
|
})
|
|
}
|