+ {{/* these styles are quite tricky and should also apply to the signup and link_account pages */}}
{{template "user/auth/signin_inner" .}}
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index fbf86a92bf..de3a1cdea7 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -60,10 +60,11 @@
- {{template "user/auth/webauthn_error" .}} -
- + {{if .EnablePasskeyAuth}} + {{template "user/auth/webauthn_error" .}} + + {{end}} {{if .ShowRegistrationButton}}
diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index b3b2a4205e..d66568199d 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -59,12 +59,12 @@
+ {{if not .LinkAccountMode}}
- {{if not .LinkAccountMode}}
{{ctx.Locale.Tr "auth.already_have_account"}} {{ctx.Locale.Tr "auth.sign_in_now"}}
- {{end}}
+ {{end}}
diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 1c1ba5566f..739be586b8 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -73,10 +73,13 @@ {{else if .GetOpType.InActions "publish_release"}} {{$linkText := .Content | ctx.RenderUtils.RenderEmoji}} {{ctx.Locale.Tr "action.publish_release" (.GetRepoLink ctx) (printf "%s/releases/tag/%s" (.GetRepoLink ctx) .GetTag) (.ShortRepoPath ctx) $linkText}} - {{else if .GetOpType.InActions "review_dismissed"}} + {{else if .GetOpType.InActions "pull_review_dismissed"}} {{$index := index .GetIssueInfos 0}} {{$reviewer := index .GetIssueInfos 1}} {{ctx.Locale.Tr "action.review_dismissed" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx) $reviewer}} + {{else if .GetOpType.InActions "auto_merge_pull_request"}} + {{$index := index .GetIssueInfos 0}} + {{ctx.Locale.Tr "action.auto_merge_pull_request" (printf "%s/pulls/%s" (.GetRepoLink ctx) $index) $index (.ShortRepoPath ctx)}} {{end}} {{DateUtils.TimeSince .GetCreate}}
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index c0059d3cd4..7c1a69a6f5 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -100,7 +100,7 @@ {{if .TotalTrackedTime}}
{{svg "octicon-clock"}} - {{.TotalTrackedTime|Sec2Time}} + {{.TotalTrackedTime|Sec2Hour}}
{{end}} {{if .UpdatedUnix}} diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts index 14ec836600..3e92e0d3c2 100644 --- a/tests/e2e/utils_e2e.ts +++ b/tests/e2e/utils_e2e.ts @@ -1,12 +1,13 @@ import {expect} from '@playwright/test'; import {env} from 'node:process'; +import type {Browser, Page, WorkerInfo} from '@playwright/test'; const ARTIFACTS_PATH = `tests/e2e/test-artifacts`; const LOGIN_PASSWORD = 'password'; // log in user and store session info. This should generally be // run in test.beforeAll(), then the session can be loaded in tests. -export async function login_user(browser, workerInfo, user) { +export async function login_user(browser: Browser, workerInfo: WorkerInfo, user: string) { // Set up a new context const context = await browser.newContext(); const page = await context.newPage(); @@ -17,8 +18,8 @@ export async function login_user(browser, workerInfo, user) { expect(response?.status()).toBe(200); // Status OK // Fill out form - await page.type('input[name=user_name]', user); - await page.type('input[name=password]', LOGIN_PASSWORD); + await page.locator('input[name=user_name]').fill(user); + await page.locator('input[name=password]').fill(LOGIN_PASSWORD); await page.click('form button.ui.primary.button:visible'); await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle @@ -31,7 +32,7 @@ export async function login_user(browser, workerInfo, user) { return context; } -export async function load_logged_in_context(browser, workerInfo, user) { +export async function load_logged_in_context(browser: Browser, workerInfo: WorkerInfo, user: string) { let context; try { context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); @@ -43,7 +44,7 @@ export async function load_logged_in_context(browser, workerInfo, user) { return context; } -export async function save_visual(page) { +export async function save_visual(page: Page) { // Optionally include visual testing if (env.VISUAL_TEST) { await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/HEAD b/tests/gitea-repositories-meta/user2/test_commit_revert.git/HEAD deleted file mode 100644 index b870d82622..0000000000 --- a/tests/gitea-repositories-meta/user2/test_commit_revert.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/main diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/config b/tests/gitea-repositories-meta/user2/test_commit_revert.git/config deleted file mode 100644 index 57bbcba5be..0000000000 --- a/tests/gitea-repositories-meta/user2/test_commit_revert.git/config +++ /dev/null @@ -1,8 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true - ignorecase = true - precomposeunicode = true -[remote "origin"] - url = https://try.gitea.io/me-heer/test_commit_revert.git diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.idx b/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.idx deleted file mode 100644 index 77bcbe7fb4..0000000000 Binary files a/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.idx and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.pack b/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.pack deleted file mode 100644 index 7271cdaeb8..0000000000 Binary files a/tests/gitea-repositories-meta/user2/test_commit_revert.git/objects/pack/pack-91200c8e6707636a6cc3e0d8101fba08b19dcb91.pack and /dev/null differ diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/packed-refs b/tests/gitea-repositories-meta/user2/test_commit_revert.git/packed-refs deleted file mode 100644 index 1f546d7fd5..0000000000 --- a/tests/gitea-repositories-meta/user2/test_commit_revert.git/packed-refs +++ /dev/null @@ -1,3 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted -46aa6ab2c881ae90e15d9ccfc947d1625c892ce5 refs/heads/develop -deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7 refs/heads/main diff --git a/tests/gitea-repositories-meta/user2/test_commit_revert.git/refs/heads/main b/tests/gitea-repositories-meta/user2/test_commit_revert.git/refs/heads/main deleted file mode 100644 index ab80ca3ca6..0000000000 --- a/tests/gitea-repositories-meta/user2/test_commit_revert.git/refs/heads/main +++ /dev/null @@ -1 +0,0 @@ -deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7 diff --git a/tests/integration/actions_job_test.go b/tests/integration/actions_job_test.go index e13277678d..a0c06b06fd 100644 --- a/tests/integration/actions_job_test.go +++ b/tests/integration/actions_job_test.go @@ -4,17 +4,23 @@ package integration import ( + "context" "encoding/base64" "fmt" "net/http" "net/url" + "reflect" "testing" "time" actions_model "code.gitea.io/gitea/models/actions" auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" @@ -347,6 +353,91 @@ jobs: }) } +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"}) + + // 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, fmt.Sprintf("create %s", 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(context.Background())) + + 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, fmt.Sprint(actionRunJob.RunID), gtCtx["run_id"].GetStringValue()) + assert.Equal(t, fmt.Sprint(actionRun.Index), gtCtx["run_number"].GetStringValue()) + assert.Equal(t, fmt.Sprint(actionRunJob.Attempt), 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:]) + + doAPIDeleteRepository(user2APICtx)(t) + }) +} + func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository { req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{ Name: repoName, diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 2c76aa826f..8ea9b34efe 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -81,12 +81,12 @@ func TestPullRequestTargetEvent(t *testing.T) { OldBranch: "main", NewBranch: "main", Author: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Committer: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), @@ -109,12 +109,12 @@ func TestPullRequestTargetEvent(t *testing.T) { OldBranch: "main", NewBranch: "fork-branch-1", Author: &files_service.IdentityOptions{ - Name: user4.Name, - Email: user4.Email, + GitUserName: user4.Name, + GitUserEmail: user4.Email, }, Committer: &files_service.IdentityOptions{ - Name: user4.Name, - Email: user4.Email, + GitUserName: user4.Name, + GitUserEmail: user4.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), @@ -164,12 +164,12 @@ func TestPullRequestTargetEvent(t *testing.T) { OldBranch: "main", NewBranch: "fork-branch-2", Author: &files_service.IdentityOptions{ - Name: user4.Name, - Email: user4.Email, + GitUserName: user4.Name, + GitUserEmail: user4.Email, }, Committer: &files_service.IdentityOptions{ - Name: user4.Name, - Email: user4.Email, + GitUserName: user4.Name, + GitUserEmail: user4.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), @@ -237,12 +237,12 @@ func TestSkipCI(t *testing.T) { OldBranch: "master", NewBranch: "master", Author: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Committer: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), @@ -268,12 +268,12 @@ func TestSkipCI(t *testing.T) { OldBranch: "master", NewBranch: "master", Author: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Committer: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), @@ -299,12 +299,12 @@ func TestSkipCI(t *testing.T) { OldBranch: "master", NewBranch: "test-skip-ci", Author: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Committer: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), @@ -356,12 +356,12 @@ func TestCreateDeleteRefEvent(t *testing.T) { OldBranch: "main", NewBranch: "main", Author: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Committer: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), @@ -470,12 +470,12 @@ func TestPullRequestCommitStatusEvent(t *testing.T) { OldBranch: "main", NewBranch: "main", Author: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Committer: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), @@ -576,12 +576,12 @@ func TestPullRequestCommitStatusEvent(t *testing.T) { OldBranch: testBranch, NewBranch: testBranch, Author: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Committer: &files_service.IdentityOptions{ - Name: user2.Name, - Email: user2.Email, + GitUserName: user2.Name, + GitUserEmail: user2.Email, }, Dates: &files_service.CommitDateOptions{ Author: time.Now(), diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 564c25f992..127cd3f4f0 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -190,28 +190,61 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran func TestAPIUpdateBranch(t *testing.T) { onGiteaRun(t, func(t *testing.T, _ *url.URL) { t.Run("UpdateBranchWithEmptyRepo", func(t *testing.T) { - testAPIUpdateBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound) + testAPIUpdateBranch(t, "user10", "user10", "repo6", "master", "test", http.StatusNotFound) }) t.Run("UpdateBranchWithSameBranchNames", func(t *testing.T) { - resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "master", http.StatusUnprocessableEntity) + resp := testAPIUpdateBranch(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("UpdateBranchThatAlreadyExists", func(t *testing.T) { - resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity) + resp := testAPIUpdateBranch(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("UpdateBranchWithNonExistentBranch", func(t *testing.T) { - resp := testAPIUpdateBranch(t, "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound) + resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound) assert.Contains(t, resp.Body.String(), "Branch doesn't exist.") }) - t.Run("RenameBranchNormalScenario", func(t *testing.T) { - testAPIUpdateBranch(t, "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent) + t.Run("UpdateBranchWithNonAdminDoer", func(t *testing.T) { + // don't allow default branch renaming + resp := testAPIUpdateBranch(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 = testAPIUpdateBranch(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("UpdateBranchWithGlobedBasedProtectionRulesAndAdminAccess", 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 := testAPIUpdateBranch(t, "user2", "user2", "repo1", from, "new-branch-name", http.StatusForbidden) + assert.Contains(t, resp.Body.String(), "Branch is protected by glob-based protection rules.") + }) + t.Run("UpdateBranchNormalScenario", func(t *testing.T) { + testAPIUpdateBranch(t, "user2", "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent) }) }) } -func testAPIUpdateBranch(t *testing.T, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder { - token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteRepository) +func testAPIUpdateBranch(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.UpdateBranchRepoOption{ Name: to, }).AddTokenAuth(token) diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go index 580bb459e7..69f37f4574 100644 --- a/tests/integration/api_fork_test.go +++ b/tests/integration/api_fork_test.go @@ -10,6 +10,7 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" org_model "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" @@ -81,8 +82,8 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) { var forks []*api.Repository DecodeJSON(t, resp, &forks) - assert.Len(t, forks, 1) - assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count")) + assert.Len(t, forks, 2) + assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count")) assert.NoError(t, org_service.AddTeamMember(db.DefaultContext, ownerTeam2, user1)) @@ -96,3 +97,31 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) { assert.EqualValues(t, "2", resp.Header().Get("X-Total-Count")) }) } + +func TestGetPrivateReposForks(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + user1Sess := loginUser(t, "user1") + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // private repository + privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23}) + user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository) + + forkedRepoName := "forked-repo" + // create fork from a private repository + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+repo2.FullName()+"/forks", &api.CreateForkOption{ + Organization: &privateOrg.Name, + Name: &forkedRepoName, + }).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 := []*api.Repository{} + DecodeJSON(t, resp, &forks) + assert.Len(t, forks, 1) + assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count")) + assert.EqualValues(t, "forked-repo", forks[0].Name) + assert.EqualValues(t, privateOrg.Name, forks[0].Owner.UserName) +} diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index fff121490c..d766b1e8be 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -6,7 +6,6 @@ package integration import ( "fmt" "net/http" - "net/url" "strings" "testing" @@ -19,46 +18,52 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" ) -func TestAPIOrgCreate(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { - token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization) +func TestAPIOrgCreateRename(t *testing.T) { + defer tests.PrepareTestEnv(t)() + token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization) - org := api.CreateOrgOption{ - UserName: "user1_org", - FullName: "User1's organization", - Description: "This organization created by user1", - Website: "https://try.gitea.io", - Location: "Shanghai", - Visibility: "limited", - } - req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusCreated) + org := api.CreateOrgOption{ + UserName: "user1_org", + FullName: "User1's organization", + Description: "This organization created by user1", + Website: "https://try.gitea.io", + Location: "Shanghai", + Visibility: "limited", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) - var apiOrg api.Organization - DecodeJSON(t, resp, &apiOrg) + var apiOrg api.Organization + DecodeJSON(t, resp, &apiOrg) - 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) + 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, - }) + unittest.AssertExistsAndLoadBean(t, &user_model.User{ + Name: org.UserName, + LowerName: strings.ToLower(org.UserName), + FullName: org.FullName, + }) + // check org name + req = NewRequestf(t, "GET", "/api/v1/orgs/%s", org.UserName).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiOrg) + assert.EqualValues(t, org.UserName, apiOrg.Name) + + t.Run("CheckPermission", func(t *testing.T) { // Check owner team permission ownerTeam, _ := org_model.GetOwnerTeam(db.DefaultContext, apiOrg.ID) - for _, ut := range unit_model.AllRepoUnitTypes { up := perm.AccessModeOwner if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki { @@ -71,25 +76,10 @@ func TestAPIOrgCreate(t *testing.T) { AccessMode: up, }) } + }) - req = NewRequestf(t, "GET", "/api/v1/orgs/%s", org.UserName). - AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - DecodeJSON(t, resp, &apiOrg) - assert.EqualValues(t, org.UserName, apiOrg.Name) - - req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org.UserName). - AddTokenAuth(token) - resp = MakeRequest(t, req, http.StatusOK) - - var repos []*api.Repository - DecodeJSON(t, resp, &repos) - for _, repo := range repos { - assert.False(t, repo.Private) - } - - req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", org.UserName). - AddTokenAuth(token) + t.Run("CheckMembers", func(t *testing.T) { + req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", org.UserName).AddTokenAuth(token) resp = MakeRequest(t, req, http.StatusOK) // user1 on this org is public @@ -98,76 +88,89 @@ func TestAPIOrgCreate(t *testing.T) { assert.Len(t, users, 1) assert.EqualValues(t, "user1", users[0].UserName) }) + + t.Run("RenameOrg", func(t *testing.T) { + req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/user1_org/rename", &api.RenameOrgOption{ + NewName: "renamed_org", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusNoContent) + unittest.AssertExistsAndLoadBean(t, &org_model.Organization{Name: "renamed_org"}) + org.UserName = "renamed_org" // update the variable so the following tests could still use it + }) + + t.Run("ListRepos", func(t *testing.T) { + // FIXME: this test is wrong, there is no repository at all, so the for-loop is empty + req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org.UserName).AddTokenAuth(token) + resp = MakeRequest(t, req, http.StatusOK) + var repos []*api.Repository + DecodeJSON(t, resp, &repos) + for _, repo := range repos { + assert.False(t, repo.Private) + } + }) } func TestAPIOrgEdit(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { - session := loginUser(t, "user1") + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization) - org := api.EditOrgOption{ - FullName: "Org3 organization new full name", - Description: "A new description", - Website: "https://try.gitea.io/new", - Location: "Beijing", - Visibility: "private", - } - req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization) + org := api.EditOrgOption{ + FullName: "Org3 organization new full name", + Description: "A new description", + Website: "https://try.gitea.io/new", + Location: "Beijing", + Visibility: "private", + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) - var apiOrg api.Organization - DecodeJSON(t, resp, &apiOrg) + var apiOrg api.Organization + DecodeJSON(t, resp, &apiOrg) - assert.Equal(t, "org3", apiOrg.Name) - assert.Equal(t, org.FullName, apiOrg.FullName) - assert.Equal(t, org.Description, apiOrg.Description) - assert.Equal(t, org.Website, apiOrg.Website) - assert.Equal(t, org.Location, apiOrg.Location) - assert.Equal(t, org.Visibility, apiOrg.Visibility) - }) + assert.Equal(t, "org3", apiOrg.Name) + assert.Equal(t, org.FullName, apiOrg.FullName) + assert.Equal(t, org.Description, apiOrg.Description) + assert.Equal(t, org.Website, apiOrg.Website) + assert.Equal(t, org.Location, apiOrg.Location) + assert.Equal(t, org.Visibility, apiOrg.Visibility) } func TestAPIOrgEditBadVisibility(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { - session := loginUser(t, "user1") + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user1") - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization) - org := api.EditOrgOption{ - FullName: "Org3 organization new full name", - Description: "A new description", - Website: "https://try.gitea.io/new", - Location: "Beijing", - Visibility: "badvisibility", - } - req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org). - AddTokenAuth(token) - MakeRequest(t, req, http.StatusUnprocessableEntity) - }) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization) + org := api.EditOrgOption{ + FullName: "Org3 organization new full name", + Description: "A new description", + Website: "https://try.gitea.io/new", + Location: "Beijing", + Visibility: "badvisibility", + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusUnprocessableEntity) } func TestAPIOrgDeny(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { - setting.Service.RequireSignInView = true - defer func() { - setting.Service.RequireSignInView = false - }() + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Service.RequireSignInView, true)() - orgName := "user1_org" - req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName) - MakeRequest(t, req, http.StatusNotFound) + orgName := "user1_org" + req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName) + MakeRequest(t, req, http.StatusNotFound) - req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName) - MakeRequest(t, req, http.StatusNotFound) + req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName) + MakeRequest(t, req, http.StatusNotFound) - req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName) - MakeRequest(t, req, http.StatusNotFound) - }) + req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName) + MakeRequest(t, req, http.StatusNotFound) } func TestAPIGetAll(t *testing.T) { defer tests.PrepareTestEnv(t)() - token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadOrganization) // accessing with a token will return all orgs @@ -192,37 +195,36 @@ func TestAPIGetAll(t *testing.T) { } func TestAPIOrgSearchEmptyTeam(t *testing.T) { - onGiteaRun(t, func(*testing.T, *url.URL) { - token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization) - orgName := "org_with_empty_team" + defer tests.PrepareTestEnv(t)() + token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization) + orgName := "org_with_empty_team" - // create org - req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{ - UserName: orgName, - }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusCreated) + // create org + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{ + UserName: orgName, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) - // create team with no member - req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{ - Name: "Empty", - IncludesAllRepositories: true, - Permission: "read", - Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"}, - }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusCreated) + // create team with no member + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{ + Name: "Empty", + IncludesAllRepositories: true, + Permission: "read", + Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"}, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) - // case-insensitive search for teams that have no members - req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")). - AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) - data := struct { - Ok bool - Data []*api.Team - }{} - DecodeJSON(t, resp, &data) - assert.True(t, data.Ok) - if assert.Len(t, data.Data, 1) { - assert.EqualValues(t, "Empty", data.Data[0].Name) - } - }) + // case-insensitive search for teams that have no members + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")). + AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + data := struct { + Ok bool + Data []*api.Team + }{} + DecodeJSON(t, resp, &data) + assert.True(t, data.Ok) + if assert.Len(t, data.Data, 1) { + assert.EqualValues(t, "Empty", data.Data[0].Name) + } } diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 122afbfa08..22f26d87d4 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -471,6 +471,15 @@ func TestAPIMirrorSyncNonMirrorRepo(t *testing.T) { assert.Equal(t, "Repository is not a mirror", errRespJSON["message"]) } +func testAPIOrgCreateRepo(t *testing.T, session *TestSession, orgName, repoName string, status int) { + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository) + + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", orgName), &api.CreateRepoOption{ + Name: repoName, + }).AddTokenAuth(token) + MakeRequest(t, req, status) +} + func TestAPIOrgRepoCreate(t *testing.T) { testCases := []struct { ctxUserID int64 @@ -488,11 +497,7 @@ func TestAPIOrgRepoCreate(t *testing.T) { for _, testCase := range testCases { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID}) session := loginUser(t, user.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository) - req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos", testCase.orgName), &api.CreateRepoOption{ - Name: testCase.repoName, - }).AddTokenAuth(token) - MakeRequest(t, req, testCase.expectedStatus) + testAPIOrgCreateRepo(t, session, testCase.orgName, testCase.repoName, testCase.expectedStatus) } } @@ -735,5 +740,5 @@ func TestAPIRepoGetAssignees(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) var assignees []*api.User DecodeJSON(t, resp, &assignees) - assert.Len(t, assignees, 1) + assert.Len(t, assignees, 2) } diff --git a/tests/integration/api_wiki_test.go b/tests/integration/api_wiki_test.go index 05d90fc4e3..8e5f67e282 100644 --- a/tests/integration/api_wiki_test.go +++ b/tests/integration/api_wiki_test.go @@ -172,6 +172,19 @@ func TestAPIListWikiPages(t *testing.T) { assert.Equal(t, dummymeta, meta) } +func testAPICreateWikiPage(t *testing.T, session *TestSession, userName, repoName, title string, status int) { + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", userName, repoName) + + req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{ + Title: title, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")), + Message: "", + }).AddTokenAuth(token) + MakeRequest(t, req, status) +} + func TestAPINewWikiPage(t *testing.T) { for _, title := range []string{ "New page", @@ -180,16 +193,7 @@ func TestAPINewWikiPage(t *testing.T) { defer tests.PrepareTestEnv(t)() username := "user2" session := loginUser(t, username) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new", username, "repo1") - - req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateWikiPageOptions{ - Title: title, - ContentBase64: base64.StdEncoding.EncodeToString([]byte("Wiki page content for API unit tests")), - Message: "", - }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusCreated) + testAPICreateWikiPage(t, session, username, "repo1", title, http.StatusCreated) } } diff --git a/tests/integration/benchmarks_test.go b/tests/integration/benchmarks_test.go deleted file mode 100644 index 62da761d2d..0000000000 --- a/tests/integration/benchmarks_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package integration - -import ( - "math/rand/v2" - "net/http" - "net/url" - "testing" - - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - api "code.gitea.io/gitea/modules/structs" -) - -// StringWithCharset random string (from https://www.calhoun.io/creating-random-strings-in-go/) -func StringWithCharset(length int, charset string) string { - b := make([]byte, length) - for i := range b { - b[i] = charset[rand.IntN(len(charset))] - } - return string(b) -} - -func BenchmarkRepoBranchCommit(b *testing.B) { - onGiteaRun(b, func(b *testing.B, u *url.URL) { - samples := []int64{1, 2, 3} - b.ResetTimer() - - for _, repoID := range samples { - b.StopTimer() - repo := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: repoID}) - b.StartTimer() - b.Run(repo.Name, func(b *testing.B) { - session := loginUser(b, "user2") - b.ResetTimer() - b.Run("CreateBranch", func(b *testing.B) { - b.StopTimer() - branchName := StringWithCharset(5+rand.IntN(10), "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - b.StartTimer() - for i := 0; i < b.N; i++ { - b.Run("new_"+branchName, func(b *testing.B) { - b.Skip("benchmark broken") // TODO fix - testAPICreateBranch(b, session, repo.OwnerName, repo.Name, repo.DefaultBranch, "new_"+branchName, http.StatusCreated) - }) - } - }) - b.Run("GetBranches", func(b *testing.B) { - req := NewRequestf(b, "GET", "/api/v1/repos/%s/branches", repo.FullName()) - session.MakeRequest(b, req, http.StatusOK) - }) - b.Run("AccessCommits", func(b *testing.B) { - var branches []*api.Branch - req := NewRequestf(b, "GET", "/api/v1/repos/%s/branches", repo.FullName()) - resp := session.MakeRequest(b, req, http.StatusOK) - DecodeJSON(b, resp, &branches) - b.ResetTimer() // We measure from here - if len(branches) != 0 { - for i := 0; i < b.N; i++ { - req := NewRequestf(b, "GET", "/api/v1/repos/%s/commits?sha=%s", repo.FullName(), branches[i%len(branches)].Name) - session.MakeRequest(b, req, http.StatusOK) - } - } - }) - }) - } - }) -} diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index f0f71b80d1..fa58b8df42 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -4,17 +4,26 @@ package integration import ( + "bytes" "fmt" + "io" + "mime/multipart" "net/http" "net/http/httptest" "net/url" "path" "testing" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" - gitea_context "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCreateFile(t *testing.T) { @@ -58,9 +67,8 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) // Check if master branch has been locked successfully - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) - assert.NotNil(t, flashCookie) - assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Brule%2B%2522master%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) + flashMsg := session.GetCookieFlashMessage() + assert.EqualValues(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg) // Request editor page req = NewRequest(t, "GET", "/user2/repo1/_new/master/") @@ -98,9 +106,8 @@ func TestCreateFileOnProtectedBranch(t *testing.T) { assert.EqualValues(t, "/user2/repo1/settings/branches", res["redirect"]) // Check if master branch has been locked successfully - flashCookie = session.GetCookie(gitea_context.CookieNameFlash) - assert.NotNil(t, flashCookie) - assert.EqualValues(t, "error%3DRemoving%2Bbranch%2Bprotection%2Brule%2B%25221%2522%2Bfailed.", flashCookie.Value) + flashMsg = session.GetCookieFlashMessage() + assert.EqualValues(t, `Removing branch protection rule "1" failed.`, flashMsg.ErrorMsg) }) } @@ -176,3 +183,156 @@ func TestEditFileToNewBranch(t *testing.T) { testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") }) } + +func TestWebGitCommitEmail(t *testing.T) { + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + require.True(t, user.KeepEmailPrivate) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath()) + defer gitRepo.Close() + getLastCommit := func(t *testing.T) *git.Commit { + c, err := gitRepo.GetBranchCommit("master") + require.NoError(t, err) + return c + } + + session := loginUser(t, user.Name) + + makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder { + lastCommit := getLastCommit(t) + params["_csrf"] = GetUserCSRFToken(t, session) + params["last_commit"] = lastCommit.ID.String() + params["commit_choice"] = "direct" + req := NewRequestWithValues(t, "POST", link, params) + resp := session.MakeRequest(t, req, NoExpectedStatus) + newCommit := getLastCommit(t) + if expectedUserName == "" { + require.Equal(t, lastCommit.ID.String(), newCommit.ID.String()) + htmlDoc := NewHTMLParser(t, resp.Body) + errMsg := htmlDoc.doc.Find(".ui.negative.message").Text() + assert.Contains(t, errMsg, translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email")) + } else { + require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String()) + assert.EqualValues(t, expectedUserName, newCommit.Author.Name) + assert.EqualValues(t, expectedEmail, newCommit.Author.Email) + assert.EqualValues(t, expectedUserName, newCommit.Committer.Name) + assert.EqualValues(t, expectedEmail, newCommit.Committer.Email) + } + return resp + } + + uploadFile := func(t *testing.T, name, content string) string { + body := &bytes.Buffer{} + uploadForm := multipart.NewWriter(body) + file, _ := uploadForm.CreateFormFile("file", name) + _, _ = io.Copy(file, bytes.NewBufferString(content)) + _ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session)) + _ = uploadForm.Close() + + req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body) + req.Header.Add("Content-Type", uploadForm.FormDataContentType()) + resp := session.MakeRequest(t, req, http.StatusOK) + + respMap := map[string]string{} + DecodeJSON(t, resp, &respMap) + return respMap["uuid"] + } + + t.Run("EmailInactive", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}) + require.False(t, email.IsActivated) + makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ + "tree_path": "README.md", + "content": "test content", + "commit_email": email.Email, + }, "", "") + }) + + t.Run("EmailInvalid", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true}) + require.NotEqualValues(t, email.UID, user.ID) + makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ + "tree_path": "README.md", + "content": "test content", + "commit_email": email.Email, + }, "", "") + }) + + testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) { + t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + paramsForKeepPrivate["commit_email"] = "" + resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") + }) + t.Run("ChooseEmail", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + paramsForChosenEmail["commit_email"] = "user2@example.com" + resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com") + }) + return resp1, resp2 + } + + t.Run("Edit", func(t *testing.T) { + testWebGit(t, + "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"}, + "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"}, + ) + }) + + t.Run("UploadDelete", func(t *testing.T) { + file1UUID := uploadFile(t, "file1", "File 1") + file2UUID := uploadFile(t, "file2", "File 2") + testWebGit(t, + "/user2/repo1/_upload/master", map[string]string{"files": file1UUID}, + "/user2/repo1/_upload/master", map[string]string{"files": file2UUID}, + ) + testWebGit(t, + "/user2/repo1/_delete/master/file1", map[string]string{}, + "/user2/repo1/_delete/master/file2", map[string]string{}, + ) + }) + + t.Run("ApplyPatchCherryPick", func(t *testing.T) { + testWebGit(t, + "/user2/repo1/_diffpatch/master", map[string]string{ + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt +new file mode 100644 +index 0000000000..aaaaaaaaaa +--- /dev/null ++++ b/patch-file-1.txt +@@ -0,0 +1 @@ ++File 1 +`, + }, + "/user2/repo1/_diffpatch/master", map[string]string{ + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt +new file mode 100644 +index 0000000000..bbbbbbbbbb +--- /dev/null ++++ b/patch-file-2.txt +@@ -0,0 +1 @@ ++File 2 +`, + }, + ) + + commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt") + require.NoError(t, err) + commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt") + require.NoError(t, err) + resp1, _ := testWebGit(t, + "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"}, + "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"}, + ) + + // By the way, test the "cherrypick" page: a successful revert redirects to the main branch + assert.EqualValues(t, "/user2/repo1/src/branch/master", resp1.Header().Get("Location")) + }) + }) +} diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 0801b093df..b19774a826 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -14,6 +14,7 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -24,6 +25,7 @@ import ( "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder { @@ -60,7 +62,9 @@ func TestEmptyRepoAddFile(t *testing.T) { session := loginUser(t, "user30") req := NewRequest(t, "GET", "/user30/empty") resp := session.MakeRequest(t, req, http.StatusOK) - assert.Contains(t, resp.Body.String(), "empty-repo-guide") + bodyString := resp.Body.String() + assert.Contains(t, bodyString, "empty-repo-guide") + assert.True(t, test.IsNormalPageCompleted(bodyString)) req = NewRequest(t, "GET", "/user30/empty/_new/"+setting.Repository.DefaultBranch) resp = session.MakeRequest(t, req, http.StatusOK) @@ -80,6 +84,29 @@ func TestEmptyRepoAddFile(t *testing.T) { req = NewRequest(t, "GET", redirect) resp = session.MakeRequest(t, req, http.StatusOK) assert.Contains(t, resp.Body.String(), "newly-added-test-file") + + // the repo is not empty anymore + req = NewRequest(t, "GET", "/user30/empty") + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "test-file.md") + + // if the repo is in incorrect state, it should be able to self-heal (recover to correct state) + user30EmptyRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"}) + user30EmptyRepo.IsEmpty = true + user30EmptyRepo.DefaultBranch = "no-such" + _, err := db.GetEngine(db.DefaultContext).ID(user30EmptyRepo.ID).Cols("is_empty", "default_branch").Update(user30EmptyRepo) + require.NoError(t, err) + user30EmptyRepo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 30, Name: "empty"}) + assert.True(t, user30EmptyRepo.IsEmpty) + + req = NewRequest(t, "GET", "/user30/empty") + resp = session.MakeRequest(t, req, http.StatusSeeOther) + redirect = test.RedirectURL(resp) + assert.Equal(t, "/user30/empty", redirect) + + req = NewRequest(t, "GET", "/user30/empty") + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "test-file.md") } func TestEmptyRepoUploadFile(t *testing.T) { diff --git a/tests/integration/feed_repo_test.go b/tests/integration/feed_repo_test.go new file mode 100644 index 0000000000..132ed32ced --- /dev/null +++ b/tests/integration/feed_repo_test.go @@ -0,0 +1,35 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "encoding/xml" + "net/http" + "testing" + + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestFeedRepo(t *testing.T) { + t.Run("RSS", func(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + req := NewRequest(t, "GET", "/user2/repo1.rss") + resp := MakeRequest(t, req, http.StatusOK) + + data := resp.Body.String() + assert.Contains(t, data, ` a") - assert.EqualValues(t, 1, releaseList.Length()) - assert.EqualValues(t, "3 commits", releaseList.First().Text()) + t.Run("NoLogin", func(t *testing.T) { + req := NewRequest(t, "GET", "/user2/repo-release/releases/tag/v1.0") + resp := MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + // check the "number of commits to main since this release" + releaseList := htmlDoc.doc.Find("#release-list .ahead > a") + assert.EqualValues(t, 1, releaseList.Length()) + assert.EqualValues(t, "3 commits", releaseList.First().Text()) + }) + t.Run("Login", func(t *testing.T) { + session := loginUser(t, "user1") + req := NewRequest(t, "GET", "/user2/repo1/releases/tag/delete-tag") // "delete-tag" is the only one with is_tag=true (although strange name) + resp := session.MakeRequest(t, req, http.StatusOK) + // the New Release button should contain the tag name + assert.Contains(t, resp.Body.String(), ``) + }) } func TestViewReleaseListLogin(t *testing.T) { diff --git a/tests/integration/rename_branch_test.go b/tests/integration/rename_branch_test.go index 576264ba95..492fdf781b 100644 --- a/tests/integration/rename_branch_test.go +++ b/tests/integration/rename_branch_test.go @@ -11,7 +11,6 @@ import ( git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - gitea_context "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -82,9 +81,8 @@ func testRenameBranch(t *testing.T, u *url.URL) { "to": "branch1", }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie := session.GetCookie(gitea_context.CookieNameFlash) - assert.NotNil(t, flashCookie) - assert.Contains(t, flashCookie.Value, "error") + flashMsg := session.GetCookieFlashMessage() + assert.NotEmpty(t, flashMsg.ErrorMsg) branch2 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) assert.Equal(t, "branch2", branch2.Name) @@ -110,9 +108,8 @@ func testRenameBranch(t *testing.T, u *url.URL) { }) session.MakeRequest(t, req, http.StatusSeeOther) - flashCookie = session.GetCookie(gitea_context.CookieNameFlash) - assert.NotNil(t, flashCookie) - assert.Contains(t, flashCookie.Value, "success") + flashMsg = session.GetCookieFlashMessage() + assert.NotEmpty(t, flashMsg.SuccessMsg) unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch2"}) branch1 = unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "branch1"}) diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index 2b4c417334..f9cf13112a 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -11,13 +11,8 @@ import ( "strings" "testing" - auth_model "code.gitea.io/gitea/models/auth" - org_model "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/tests" @@ -142,19 +137,51 @@ func TestCreateBranchInvalidCSRF(t *testing.T) { assert.Contains(t, resp.Body.String(), "Invalid CSRF token") } -func prepareBranch(t *testing.T, session *TestSession, repo *repo_model.Repository) { - baseRefSubURL := fmt.Sprintf("branch/%s", repo.DefaultBranch) - +func prepareRecentlyPushedBranchTest(t *testing.T, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) { + refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch) + baseRepoPath := baseRepo.OwnerName + "/" + baseRepo.Name + headRepoPath := headRepo.OwnerName + "/" + headRepo.Name + // Case 1: Normal branch changeset to display pushed message // create branch with no new commit - testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "no-commit", http.StatusSeeOther) + testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, refSubURL, "no-commit", http.StatusSeeOther) // create branch with commit - testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "new-commit", http.StatusSeeOther) - testAPINewFile(t, session, repo.OwnerName, repo.Name, "new-commit", "new-commit.txt", "new-commit") + testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "new-commit", fmt.Sprintf("new-file-%s.txt", headRepo.Name), "new-commit") - // create deleted branch - testCreateBranch(t, session, repo.OwnerName, repo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther) - testUIDeleteBranch(t, session, repo.OwnerName, repo.Name, "deleted-branch") + // create a branch then delete it + testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther) + testUIDeleteBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "deleted-branch") + + // only `new-commit` branch has commits ahead the base branch + checkRecentlyPushedNewBranches(t, headSession, headRepoPath, []string{"new-commit"}) + if baseRepo.RepoPath() != headRepo.RepoPath() { + checkRecentlyPushedNewBranches(t, headSession, baseRepoPath, []string{fmt.Sprintf("%v:new-commit", headRepo.FullName())}) + } + + // Case 2: Create PR so that `new-commit` branch will not show + testCreatePullToDefaultBranch(t, headSession, baseRepo, headRepo, "new-commit", "merge new-commit to default branch") + // No push message show because of active PR + checkRecentlyPushedNewBranches(t, headSession, headRepoPath, []string{}) + if baseRepo.RepoPath() != headRepo.RepoPath() { + checkRecentlyPushedNewBranches(t, headSession, baseRepoPath, []string{}) + } +} + +func prepareRecentlyPushedBranchSpecialTest(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository) { + refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch) + baseRepoPath := baseRepo.OwnerName + "/" + baseRepo.Name + headRepoPath := headRepo.OwnerName + "/" + headRepo.Name + // create branch with no new commit + testCreateBranch(t, session, headRepo.OwnerName, headRepo.Name, refSubURL, "no-commit-special", http.StatusSeeOther) + + // update base (default) branch before head branch is updated + testAPINewFile(t, session, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, fmt.Sprintf("new-file-special-%s.txt", headRepo.Name), "new-commit") + + // Though we have new `no-commit` branch, but the headBranch is not newer or commits ahead baseBranch. No message show. + checkRecentlyPushedNewBranches(t, session, headRepoPath, []string{}) + if baseRepo.RepoPath() != headRepo.RepoPath() { + checkRecentlyPushedNewBranches(t, session, baseRepoPath, []string{}) + } } func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository, headBranch, title string) string { @@ -169,6 +196,9 @@ func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo, } func prepareRepoPR(t *testing.T, baseSession, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) { + refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch) + testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, refSubURL, "new-commit", http.StatusSeeOther) + // create opening PR testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "opening-pr", http.StatusSeeOther) testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "opening-pr", "opening pr") @@ -210,65 +240,19 @@ func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath func TestRecentlyPushedNewBranches(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { - user1Session := loginUser(t, "user1") - user2Session := loginUser(t, "user2") user12Session := loginUser(t, "user12") - user13Session := loginUser(t, "user13") - // prepare branch and PRs in original repo + // Same reposioty check repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) - prepareBranch(t, user12Session, repo10) prepareRepoPR(t, user12Session, user12Session, repo10, repo10) - - // outdated new branch should not be displayed - checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"new-commit"}) + prepareRecentlyPushedBranchTest(t, user12Session, repo10, repo10) + prepareRecentlyPushedBranchSpecialTest(t, user12Session, repo10, repo10) // create a fork repo in public org - testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", "new-commit") + testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", repo10.DefaultBranch) orgPublicForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 25, Name: "org25_fork_repo10"}) prepareRepoPR(t, user12Session, user12Session, repo10, orgPublicForkRepo) - - // user12 is the owner of the repo10 and the organization org25 - // in repo10, user12 has opening/closed/merged pr and closed/merged pr with deleted branch - checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"org25/org25_fork_repo10:new-commit", "new-commit"}) - - userForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) - testCtx := NewAPITestContext(t, repo10.OwnerName, repo10.Name, auth_model.AccessTokenScopeWriteRepository) - t.Run("AddUser13AsCollaborator", doAPIAddCollaborator(testCtx, "user13", perm.AccessModeWrite)) - prepareBranch(t, user13Session, userForkRepo) - prepareRepoPR(t, user13Session, user13Session, repo10, userForkRepo) - - // create branch with same name in different repo by user13 - testCreateBranch(t, user13Session, repo10.OwnerName, repo10.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther) - testCreateBranch(t, user13Session, userForkRepo.OwnerName, userForkRepo.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther) - testCreatePullToDefaultBranch(t, user13Session, repo10, userForkRepo, "same-name-branch", "same name branch pr") - - // user13 pushed 2 branches with the same name in repo10 and repo11 - // and repo11's branch has a pr, but repo10's branch doesn't - // in this case, we should get repo10's branch but not repo11's branch - checkRecentlyPushedNewBranches(t, user13Session, "user12/repo10", []string{"same-name-branch", "user13/repo11:new-commit"}) - - // create a fork repo in private org - testRepoFork(t, user1Session, repo10.OwnerName, repo10.Name, "private_org35", "org35_fork_repo10", "new-commit") - orgPrivateForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 35, Name: "org35_fork_repo10"}) - prepareRepoPR(t, user1Session, user1Session, repo10, orgPrivateForkRepo) - - // user1 is the owner of private_org35 and no write permission to repo10 - // so user1 can only see the branch in org35_fork_repo10 - checkRecentlyPushedNewBranches(t, user1Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:new-commit"}) - - // user2 push a branch in private_org35 - testCreateBranch(t, user2Session, orgPrivateForkRepo.OwnerName, orgPrivateForkRepo.Name, "branch/new-commit", "user-read-permission", http.StatusSeeOther) - // convert write permission to read permission for code unit - token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization) - req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d", 24), &api.EditTeamOption{ - Name: "team24", - UnitsMap: map[string]string{"repo.code": "read"}, - }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) - teamUnit := unittest.AssertExistsAndLoadBean(t, &org_model.TeamUnit{TeamID: 24, Type: unit.TypeCode}) - assert.Equal(t, perm.AccessModeRead, teamUnit.AccessMode) - // user2 can see the branch as it is created by user2 - checkRecentlyPushedNewBranches(t, user2Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:user-read-permission"}) + prepareRecentlyPushedBranchTest(t, user12Session, repo10, orgPublicForkRepo) + prepareRecentlyPushedBranchSpecialTest(t, user12Session, repo10, orgPublicForkRepo) }) } diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index 267fd0d56e..cbe5e4bb3f 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -118,7 +118,8 @@ func TestForkListLimitedAndPrivateRepos(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/forks") resp := user1Sess.MakeRequest(t, req, http.StatusOK) htmlDoc := NewHTMLParser(t, resp.Body) - assert.EqualValues(t, 1, htmlDoc.Find(forkItemSelector).Length()) + // since user1 is an admin, he can get both of the forked repositories + assert.EqualValues(t, 2, htmlDoc.Find(forkItemSelector).Length()) assert.NoError(t, org_service.AddTeamMember(db.DefaultContext, ownerTeam2, user1)) resp = user1Sess.MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go new file mode 100644 index 0000000000..e928b04e9b --- /dev/null +++ b/tests/integration/repo_merge_upstream_test.go @@ -0,0 +1,151 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + "time" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepoMergeUpstream(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + forkUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + baseUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: baseRepo.OwnerID}) + + checkFileContent := func(branch, exp string) { + req := NewRequest(t, "GET", fmt.Sprintf("/%s/test-repo-fork/raw/branch/%s/new-file.txt", forkUser.Name, branch)) + resp := MakeRequest(t, req, http.StatusOK) + require.Equal(t, exp, resp.Body.String()) + } + + session := loginUser(t, forkUser.Name) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + + // create a fork + req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseUser.Name, baseRepo.Name), &api.CreateForkOption{ + Name: util.ToPointer("test-repo-fork"), + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusAccepted) + forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: forkUser.ID, Name: "test-repo-fork"}) + + // create fork-branch + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "new_branch_name": "fork-branch", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + queryMergeUpstreamButtonLink := func(htmlDoc *HTMLDoc) string { + return htmlDoc.Find(`button[data-url*="merge-upstream"]`).AttrOr("data-url", "") + } + + t.Run("HeadBeforeBase", func(t *testing.T) { + // add a file in base repo + sessionBaseUser := loginUser(t, baseUser.Name) + require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1")) + + var mergeUpstreamLink string + t.Run("DetectDefaultBranch", func(t *testing.T) { + // the repo shows a prompt to "sync fork" (defaults to the default branch) + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc) + if mergeUpstreamLink == "" { + return false + } + respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html() + return strings.Contains(respMsg, `This branch is 1 commit behind user2/repo1:master`) + }, 5*time.Second, 100*time.Millisecond) + }) + + t.Run("DetectSameBranch", func(t *testing.T) { + // if the fork-branch name also exists in the base repo, then use that branch instead + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/master", map[string]string{ + "_csrf": GetUserCSRFToken(t, sessionBaseUser), + "new_branch_name": "fork-branch", + }) + sessionBaseUser.MakeRequest(t, req, http.StatusSeeOther) + + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc) + if mergeUpstreamLink == "" { + return false + } + respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html() + return strings.Contains(respMsg, `This branch is 1 commit behind user2/repo1:fork-branch`) + }, 5*time.Second, 100*time.Millisecond) + }) + + // click the "sync fork" button + req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)}) + session.MakeRequest(t, req, http.StatusOK) + checkFileContent("fork-branch", "test-content-1") + + // delete the "fork-branch" from the base repo + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete?name=fork-branch", map[string]string{ + "_csrf": GetUserCSRFToken(t, sessionBaseUser), + }) + sessionBaseUser.MakeRequest(t, req, http.StatusOK) + }) + + t.Run("BaseChangeAfterHeadChange", func(t *testing.T) { + // update the files: base first, head later, and check the prompt + require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-2")) + require.NoError(t, createOrReplaceFileInBranch(forkUser, forkRepo, "new-file-other.txt", "fork-branch", "test-content-other")) + + // make sure the base branch's update time is before the fork, to make it test the complete logic + baseBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: baseRepo.ID, Name: "master"}) + forkBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: forkRepo.ID, Name: "fork-branch"}) + _, err := db.GetEngine(db.DefaultContext).ID(forkBranch.ID).Update(&git_model.Branch{UpdatedUnix: baseBranch.UpdatedUnix + 1}) + require.NoError(t, err) + + // the repo shows a prompt to "sync fork" + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html() + return strings.Contains(respMsg, `The base branch user2/repo1:master has new changes`) + }, 5*time.Second, 100*time.Millisecond) + + // and do the merge-upstream by API + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{ + Branch: "fork-branch", + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + checkFileContent("fork-branch", "test-content-2") + + var mergeResp api.MergeUpstreamResponse + DecodeJSON(t, resp, &mergeResp) + assert.Equal(t, "merge", mergeResp.MergeStyle) + + // after merge, there should be no "sync fork" button anymore + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + return queryMergeUpstreamButtonLink(htmlDoc) == "" + }, 5*time.Second, 100*time.Millisecond) + }) + }) +} diff --git a/tests/integration/repo_mergecommit_revert_test.go b/tests/integration/repo_mergecommit_revert_test.go deleted file mode 100644 index 103fb47e2b..0000000000 --- a/tests/integration/repo_mergecommit_revert_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package integration - -import ( - "net/http" - "testing" - - "code.gitea.io/gitea/tests" - - "github.com/stretchr/testify/assert" -) - -func TestRepoMergeCommitRevert(t *testing.T) { - defer tests.PrepareTestEnv(t)() - session := loginUser(t, "user2") - - req := NewRequest(t, "GET", "/user2/test_commit_revert/_cherrypick/deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7/main?ref=main&refType=branch&cherry-pick-type=revert") - resp := session.MakeRequest(t, req, http.StatusOK) - - htmlDoc := NewHTMLParser(t, resp.Body) - req = NewRequestWithValues(t, "POST", "/user2/test_commit_revert/_cherrypick/deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7/main", map[string]string{ - "_csrf": htmlDoc.GetCSRF(), - "last_commit": "deebcbc752e540bab4ce3ee713d3fc8fdc35b2f7", - "page_has_posted": "true", - "revert": "true", - "commit_summary": "reverting test commit", - "commit_message": "test message", - "commit_choice": "direct", - "new_branch_name": "test-revert-branch-1", - }) - resp = session.MakeRequest(t, req, http.StatusSeeOther) - - // A successful revert redirects to the main branch - assert.EqualValues(t, "/user2/test_commit_revert/src/branch/main", resp.Header().Get("Location")) -} diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index ef44a9e2d0..2f9a815fef 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -4,10 +4,23 @@ package integration import ( + "context" + "fmt" + "io" "net/http" + "net/http/httptest" + "net/url" "strings" "testing" + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/json" + api "code.gitea.io/gitea/modules/structs" + webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/tests" "github.com/PuerkitoBio/goquery" @@ -39,3 +52,552 @@ func TestNewWebHookLink(t *testing.T) { }) } } + +func testAPICreateWebhookForRepo(t *testing.T, session *TestSession, userName, repoName, url, event string) { + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+userName+"/"+repoName+"/hooks", api.CreateHookOption{ + Type: "gitea", + Config: api.CreateHookOptionConfig{ + "content_type": "json", + "url": url, + }, + Events: []string{event}, + Active: true, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) +} + +func testCreateWebhookForRepo(t *testing.T, session *TestSession, webhookType, userName, repoName, url, eventKind string) { + csrf := GetUserCSRFToken(t, session) + req := NewRequestWithValues(t, "POST", "/"+userName+"/"+repoName+"/settings/hooks/"+webhookType+"/new", map[string]string{ + "_csrf": csrf, + "payload_url": url, + "events": eventKind, + "active": "true", + "content_type": fmt.Sprintf("%d", webhook.ContentTypeJSON), + "http_method": "POST", + }) + session.MakeRequest(t, req, http.StatusSeeOther) +} + +func testAPICreateWebhookForOrg(t *testing.T, session *TestSession, userName, url, event string) { + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + req := NewRequestWithJSON(t, "POST", "/api/v1/orgs/"+userName+"/hooks", api.CreateHookOption{ + Type: "gitea", + Config: api.CreateHookOptionConfig{ + "content_type": "json", + "url": url, + }, + Events: []string{event}, + Active: true, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) +} + +type mockWebhookProvider struct { + server *httptest.Server +} + +func newMockWebhookProvider(callback func(r *http.Request), status int) *mockWebhookProvider { + m := &mockWebhookProvider{} + m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callback(r) + w.WriteHeader(status) + })) + return m +} + +func (m *mockWebhookProvider) URL() string { + if m.server == nil { + return "" + } + return m.server.URL +} + +// Close closes the mock webhook http server +func (m *mockWebhookProvider) Close() { + if m.server != nil { + m.server.Close() + m.server = nil + } +} + +func Test_WebhookCreate(t *testing.T) { + var payloads []api.CreatePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.CreatePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = string(webhook_module.HookEventCreate) + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "create") + + // 2. trigger the webhook + testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + + // 3. validate the webhook is triggered + assert.Len(t, payloads, 1) + assert.EqualValues(t, string(webhook_module.HookEventCreate), triggeredEvent) + assert.EqualValues(t, "repo1", payloads[0].Repo.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName) + assert.EqualValues(t, "master2", payloads[0].Ref) + assert.EqualValues(t, "branch", payloads[0].RefType) + }) +} + +func Test_WebhookDelete(t *testing.T) { + var payloads []api.DeletePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.DeletePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "delete" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "delete") + + // 2. trigger the webhook + testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + testAPIDeleteBranch(t, "master2", http.StatusNoContent) + + // 3. validate the webhook is triggered + assert.EqualValues(t, "delete", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "repo1", payloads[0].Repo.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName) + assert.EqualValues(t, "master2", payloads[0].Ref) + assert.EqualValues(t, "branch", payloads[0].RefType) + }) +} + +func Test_WebhookFork(t *testing.T) { + var payloads []api.ForkPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.ForkPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "fork" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user1") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "fork") + + // 2. trigger the webhook + testRepoFork(t, session, "user2", "repo1", "user1", "repo1-fork", "master") + + // 3. validate the webhook is triggered + assert.EqualValues(t, "fork", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "repo1-fork", payloads[0].Repo.Name) + assert.EqualValues(t, "user1/repo1-fork", payloads[0].Repo.FullName) + assert.EqualValues(t, "repo1", payloads[0].Forkee.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Forkee.FullName) + }) +} + +func Test_WebhookIssueComment(t *testing.T) { + var payloads []api.IssueCommentPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.IssueCommentPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "issue_comment" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issue_comment") + + // 2. trigger the webhook + issueURL := testNewIssue(t, session, "user2", "repo1", "Title2", "Description2") + testIssueAddComment(t, session, issueURL, "issue title2 comment1", "") + + // 3. validate the webhook is triggered + assert.EqualValues(t, "issue_comment", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "created", payloads[0].Action) + assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName) + assert.EqualValues(t, "Title2", payloads[0].Issue.Title) + assert.EqualValues(t, "Description2", payloads[0].Issue.Body) + assert.EqualValues(t, "issue title2 comment1", payloads[0].Comment.Body) + }) +} + +func Test_WebhookRelease(t *testing.T) { + var payloads []api.ReleasePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.ReleasePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "release" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "release") + + // 2. trigger the webhook + createNewRelease(t, session, "/user2/repo1", "v0.0.99", "v0.0.99", false, false) + + // 3. validate the webhook is triggered + assert.EqualValues(t, "release", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "repo1", payloads[0].Repository.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Repository.FullName) + assert.EqualValues(t, "v0.0.99", payloads[0].Release.TagName) + assert.False(t, payloads[0].Release.IsDraft) + assert.False(t, payloads[0].Release.IsPrerelease) + }) +} + +func Test_WebhookPush(t *testing.T) { + var payloads []api.PushPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.PushPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "push" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "push") + + // 2. trigger the webhook + testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push") + + // 3. validate the webhook is triggered + assert.EqualValues(t, "push", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "repo1", payloads[0].Repo.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName) + assert.Len(t, payloads[0].Commits, 1) + assert.EqualValues(t, []string{"test_webhook_push.md"}, payloads[0].Commits[0].Added) + }) +} + +func Test_WebhookIssue(t *testing.T) { + var payloads []api.IssuePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.IssuePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "issues" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issues") + + // 2. trigger the webhook + testNewIssue(t, session, "user2", "repo1", "Title1", "Description1") + + // 3. validate the webhook is triggered + assert.EqualValues(t, "issues", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "opened", payloads[0].Action) + assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName) + assert.EqualValues(t, "Title1", payloads[0].Issue.Title) + assert.EqualValues(t, "Description1", payloads[0].Issue.Body) + }) +} + +func Test_WebhookPullRequest(t *testing.T) { + var payloads []api.PullRequestPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.PullRequestPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "pull_request" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request") + + testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + // 2. trigger the webhook + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request") + + // 3. validate the webhook is triggered + assert.EqualValues(t, "pull_request", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "repo1", payloads[0].PullRequest.Base.Repository.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].PullRequest.Base.Repository.FullName) + assert.EqualValues(t, "repo1", payloads[0].PullRequest.Head.Repository.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].PullRequest.Head.Repository.FullName) + assert.EqualValues(t, 0, payloads[0].PullRequest.Additions) + }) +} + +func Test_WebhookPullRequestComment(t *testing.T) { + var payloads []api.IssueCommentPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.IssueCommentPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "pull_request_comment" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request_comment") + + // 2. trigger the webhook + testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + prID := testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request") + + testIssueAddComment(t, session, "/user2/repo1/pulls/"+prID, "pull title2 comment1", "") + + // 3. validate the webhook is triggered + assert.EqualValues(t, "pull_request_comment", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "created", payloads[0].Action) + assert.EqualValues(t, "repo1", payloads[0].Issue.Repo.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Issue.Repo.FullName) + assert.EqualValues(t, "first pull request", payloads[0].Issue.Title) + assert.EqualValues(t, "", payloads[0].Issue.Body) + assert.EqualValues(t, "pull title2 comment1", payloads[0].Comment.Body) + }) +} + +func Test_WebhookWiki(t *testing.T) { + var payloads []api.WikiPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.WikiPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "wiki" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "wiki") + + // 2. trigger the webhook + testAPICreateWikiPage(t, session, "user2", "repo1", "Test Wiki Page", http.StatusCreated) + + // 3. validate the webhook is triggered + assert.EqualValues(t, "wiki", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "created", payloads[0].Action) + assert.EqualValues(t, "repo1", payloads[0].Repository.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Repository.FullName) + assert.EqualValues(t, "Test-Wiki-Page", payloads[0].Page) + }) +} + +func Test_WebhookRepository(t *testing.T) { + var payloads []api.RepositoryPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.RepositoryPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "repository" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user1") + + testAPICreateWebhookForOrg(t, session, "org3", provider.URL(), "repository") + + // 2. trigger the webhook + testAPIOrgCreateRepo(t, session, "org3", "repo_new", http.StatusCreated) + + // 3. validate the webhook is triggered + assert.EqualValues(t, "repository", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "created", payloads[0].Action) + assert.EqualValues(t, "org3", payloads[0].Organization.UserName) + assert.EqualValues(t, "repo_new", payloads[0].Repository.Name) + assert.EqualValues(t, "org3/repo_new", payloads[0].Repository.FullName) + }) +} + +func Test_WebhookPackage(t *testing.T) { + var payloads []api.PackagePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.PackagePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "package" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user1") + + testAPICreateWebhookForOrg(t, session, "org3", provider.URL(), "package") + + // 2. trigger the webhook + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll) + url := fmt.Sprintf("/api/packages/%s/generic/%s/%s", "org3", "gitea", "v1.24.0") + req := NewRequestWithBody(t, "PUT", url+"/gitea", strings.NewReader("This is a dummy file")). + AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + // 3. validate the webhook is triggered + assert.EqualValues(t, "package", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "created", payloads[0].Action) + assert.EqualValues(t, "gitea", payloads[0].Package.Name) + assert.EqualValues(t, "generic", payloads[0].Package.Type) + assert.EqualValues(t, "org3", payloads[0].Organization.UserName) + assert.EqualValues(t, "v1.24.0", payloads[0].Package.Version) + }) +} + +func Test_WebhookStatus(t *testing.T) { + var payloads []api.CommitStatusPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + assert.Contains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should contain status") + assert.Contains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should contain status") + assert.Contains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should contain status") + content, _ := io.ReadAll(r.Body) + var payload api.CommitStatusPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "status" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "status") + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + + gitRepo1, err := gitrepo.OpenRepository(context.Background(), repo1) + assert.NoError(t, err) + commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) + assert.NoError(t, err) + + // 2. trigger the webhook + testCtx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeAll) + + // update a status for a commit via API + doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{ + State: api.CommitStatusSuccess, + TargetURL: "http://test.ci/", + Description: "", + Context: "testci", + })(t) + + // 3. validate the webhook is triggered + assert.EqualValues(t, "status", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, commitID, payloads[0].Commit.ID) + assert.EqualValues(t, "repo1", payloads[0].Repo.Name) + assert.EqualValues(t, "user2/repo1", payloads[0].Repo.FullName) + assert.EqualValues(t, "testci", payloads[0].Context) + assert.EqualValues(t, commitID, payloads[0].SHA) + }) +} + +func Test_WebhookStatus_NoWrongTrigger(t *testing.T) { + var trigger string + provider := newMockWebhookProvider(func(r *http.Request) { + assert.NotContains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should not contain status") + assert.NotContains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should not contain status") + assert.NotContains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should not contain status") + trigger = "push" + }, http.StatusOK) + defer provider.Close() + + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + // create a push_only webhook from web UI + testCreateWebhookForRepo(t, session, "gitea", "user2", "repo1", provider.URL(), "push_only") + + // 2. trigger the webhook with a push action + testCreateFile(t, session, "user2", "repo1", "master", "test_webhook_push.md", "# a test file for webhook push") + + // 3. validate the webhook is triggered with right event + assert.EqualValues(t, "push", trigger) + }) +} diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index d86dcc01fe..ef520a2e51 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -71,8 +71,8 @@ func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.Chang NewBranch: repo.DefaultBranch, Message: "Deletes README.md", Author: &files_service.IdentityOptions{ - Name: "Bob Smith", - Email: "bob@smith.com", + GitUserName: "Bob Smith", + GitUserEmail: "bob@smith.com", }, Committer: nil, } diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index d7c0b1bcd3..6738d998d7 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -15,8 +15,11 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" + "github.com/markbates/goth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -76,7 +79,7 @@ func TestSigninWithRememberMe(t *testing.T) { }) session.MakeRequest(t, req, http.StatusSeeOther) - c := session.GetCookie(setting.CookieRememberName) + c := session.GetRawCookie(setting.CookieRememberName) assert.NotNil(t, c) session = emptyTestSession(t) @@ -95,9 +98,14 @@ func TestSigninWithRememberMe(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } -func TestEnablePasswordSignInForm(t *testing.T) { +func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) { defer tests.PrepareTestEnv(t)() + mockLinkAccount := func(ctx *context.Context) { + gothUser := goth.User{Email: "invalid-email", Name: "."} + _ = ctx.Session.Set("linkAccountGothUser", gothUser) + } + t.Run("EnablePasswordSignInForm=false", func(t *testing.T) { defer tests.PrintCurrentTest(t)() defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, false)() @@ -108,6 +116,12 @@ func TestEnablePasswordSignInForm(t *testing.T) { req = NewRequest(t, "POST", "/user/login") MakeRequest(t, req, http.StatusForbidden) + + req = NewRequest(t, "GET", "/user/link_account") + defer web.RouteMockReset() + web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount) + resp = MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", false) }) t.Run("EnablePasswordSignInForm=true", func(t *testing.T) { @@ -120,5 +134,29 @@ func TestEnablePasswordSignInForm(t *testing.T) { req = NewRequest(t, "POST", "/user/login") MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", "/user/link_account") + defer web.RouteMockReset() + web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount) + resp = MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", true) + }) + + t.Run("EnablePasskeyAuth=false", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Service.EnablePasskeyAuth, false)() + + req := NewRequest(t, "GET", "/user/login") + resp := MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", false) + }) + + t.Run("EnablePasskeyAuth=true", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + defer test.MockVariableValue(&setting.Service.EnablePasskeyAuth, true)() + + req := NewRequest(t, "GET", "/user/login") + resp := MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", true) }) } diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index 5b6f28d1ff..bf248a4dde 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -134,8 +134,7 @@ Note: This user hasn't uploaded any GPG keys. =twTO ------END PGP PUBLIC KEY BLOCK----- -`) +-----END PGP PUBLIC KEY BLOCK-----`) // Import key // User1 session := loginUser(t, "user1") @@ -169,8 +168,7 @@ C0TLXKur6NVYQMn01iyL+FZzRpEWNuYF3f9QeeLJ/+l2DafESNhNTy17+RPmacK6 7XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy/TKTKhGgZNINj1iaPFyOkQgKR5M GrE0MHOxUbc9tbtyk0F1SuzREUBH =DDXw ------END PGP PUBLIC KEY BLOCK----- -`) +-----END PGP PUBLIC KEY BLOCK-----`) // Export new key testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK----- @@ -201,8 +199,7 @@ C0TLXKur6NVYQMn01iyL+FZzRpEWNuYF3f9QeeLJ/+l2DafESNhNTy17+RPmacK6 7XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy/TKTKhGgZNINj1iaPFyOkQgKR5M GrE0MHOxUbc9tbtyk0F1SuzREUBH =WFf5 ------END PGP PUBLIC KEY BLOCK----- -`) +-----END PGP PUBLIC KEY BLOCK-----`) } func testExportUserGPGKeys(t *testing.T, user, expected string) { diff --git a/tests/mssql.ini.tmpl b/tests/mssql.ini.tmpl index b50816b2cd..ffba516ed3 100644 --- a/tests/mssql.ini.tmpl +++ b/tests/mssql.ini.tmpl @@ -26,6 +26,9 @@ TYPE = immediate [queue.push_update] TYPE = immediate +[queue.webhook_sender] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/gitea-repositories @@ -111,3 +114,6 @@ ENABLED = true [actions] ENABLED = true + +[webhook] +ALLOWED_HOST_LIST = 127.0.0.1 diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl index ec8307acc3..e2f2e1390a 100644 --- a/tests/mysql.ini.tmpl +++ b/tests/mysql.ini.tmpl @@ -28,6 +28,9 @@ TYPE = immediate [queue.push_update] TYPE = immediate +[queue.webhook_sender] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/gitea-repositories @@ -118,3 +121,6 @@ REPLY_TO_ADDRESS = incoming+%{token}@localhost [actions] ENABLED = true + +[webhook] +ALLOWED_HOST_LIST = 127.0.0.1 diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl index 139ea9c2b7..483b9ed0cd 100644 --- a/tests/pgsql.ini.tmpl +++ b/tests/pgsql.ini.tmpl @@ -27,6 +27,9 @@ TYPE = immediate [queue.push_update] TYPE = immediate +[queue.webhook_sender] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/gitea-repositories @@ -127,3 +130,6 @@ ENABLED = true [actions] ENABLED = true + +[webhook] +ALLOWED_HOST_LIST = 127.0.0.1 diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 2f7a3e8182..e837860c26 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -22,6 +22,9 @@ TYPE = immediate [queue.push_update] TYPE = immediate +[queue.webhook_sender] +TYPE = immediate + [repository] ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea-repositories @@ -116,3 +119,6 @@ RENDER_CONTENT_MODE=sanitized [actions] ENABLED = true + +[webhook] +ALLOWED_HOST_LIST = 127.0.0.1 diff --git a/tsconfig.json b/tsconfig.json index d32cca0aaa..ebcb77cd7d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,11 @@ "verbatimModuleSyntax": true, "stripInternal": true, "strict": false, + "strictBindCallApply": true, + "strictBuiltinIteratorReturn": true, "strictFunctionTypes": true, + "noImplicitAny": true, + "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "noPropertyAccessFromIndexSignature": false, diff --git a/updates.config.js b/updates.config.js index a4a2fa5228..4ef1ca701b 100644 --- a/updates.config.js +++ b/updates.config.js @@ -3,6 +3,7 @@ export default { '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled 'eslint', // need to migrate to eslint flat config first 'eslint-plugin-array-func', // need to migrate to eslint flat config first + 'eslint-plugin-github', // need to migrate to eslint 9 - https://github.com/github/eslint-plugin-github/issues/585 'eslint-plugin-no-use-extend-native', // need to migrate to eslint flat config first 'eslint-plugin-vitest', // need to migrate to eslint flat config first ], diff --git a/web_src/css/base.css b/web_src/css/base.css index a1ee7044ec..76d7d82a5c 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -405,11 +405,6 @@ a.label, color: var(--color-text-light-2); } -.ui.form textarea:not([rows]) { - height: var(--min-height-textarea); /* override fomantic default 12em */ - min-height: var(--min-height-textarea); /* override fomantic default 8em */ -} - /* styles from removed fomantic transition module */ .hidden.transition { visibility: hidden; @@ -511,97 +506,6 @@ img.ui.avatar, margin-top: calc(var(--page-spacing) - 1rem); } -.ui.form .fields.error .field textarea, -.ui.form .fields.error .field select, -.ui.form .fields.error .field input:not([type]), -.ui.form .fields.error .field input[type="date"], -.ui.form .fields.error .field input[type="datetime-local"], -.ui.form .fields.error .field input[type="email"], -.ui.form .fields.error .field input[type="number"], -.ui.form .fields.error .field input[type="password"], -.ui.form .fields.error .field input[type="search"], -.ui.form .fields.error .field input[type="tel"], -.ui.form .fields.error .field input[type="time"], -.ui.form .fields.error .field input[type="text"], -.ui.form .fields.error .field input[type="file"], -.ui.form .fields.error .field input[type="url"], -.ui.form .fields.error .field .ui.dropdown, -.ui.form .fields.error .field .ui.dropdown .item, -.ui.form .field.error .ui.dropdown, -.ui.form .field.error .ui.dropdown .text, -.ui.form .field.error .ui.dropdown .item, -.ui.form .field.error textarea, -.ui.form .field.error select, -.ui.form .field.error input:not([type]), -.ui.form .field.error input[type="date"], -.ui.form .field.error input[type="datetime-local"], -.ui.form .field.error input[type="email"], -.ui.form .field.error input[type="number"], -.ui.form .field.error input[type="password"], -.ui.form .field.error input[type="search"], -.ui.form .field.error input[type="tel"], -.ui.form .field.error input[type="time"], -.ui.form .field.error input[type="text"], -.ui.form .field.error input[type="file"], -.ui.form .field.error input[type="url"], -.ui.form .field.error select:focus, -.ui.form .field.error input:not([type]):focus, -.ui.form .field.error input[type="date"]:focus, -.ui.form .field.error input[type="datetime-local"]:focus, -.ui.form .field.error input[type="email"]:focus, -.ui.form .field.error input[type="number"]:focus, -.ui.form .field.error input[type="password"]:focus, -.ui.form .field.error input[type="search"]:focus, -.ui.form .field.error input[type="tel"]:focus, -.ui.form .field.error input[type="time"]:focus, -.ui.form .field.error input[type="text"]:focus, -.ui.form .field.error input[type="file"]:focus, -.ui.form .field.error input[type="url"]:focus { - background-color: var(--color-error-bg); - border-color: var(--color-error-border); - color: var(--color-error-text); -} - -.ui.form .fields.error .field .ui.dropdown, -.ui.form .field.error .ui.dropdown, -.ui.form .fields.error .field .ui.dropdown:hover, -.ui.form .field.error .ui.dropdown:hover { - border-color: var(--color-error-border) !important; -} - -.ui.form .fields.error .field .ui.dropdown .menu .item:hover, -.ui.form .field.error .ui.dropdown .menu .item:hover { - background-color: var(--color-error-bg-hover); -} - -.ui.form .fields.error .field .ui.dropdown .menu .active.item, -.ui.form .field.error .ui.dropdown .menu .active.item { - background-color: var(--color-error-bg-active) !important; -} - -.ui.form .fields.error .dropdown .menu, -.ui.form .field.error .dropdown .menu { - border-color: var(--color-error-border) !important; -} - -input:-webkit-autofill, -input:-webkit-autofill:focus, -input:-webkit-autofill:hover, -input:-webkit-autofill:active, -.ui.form .field.field input:-webkit-autofill, -.ui.form .field.field input:-webkit-autofill:focus, -.ui.form .field.field input:-webkit-autofill:hover, -.ui.form .field.field input:-webkit-autofill:active { - -webkit-background-clip: text; - -webkit-text-fill-color: var(--color-text); - box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important; - border-color: var(--color-primary-light-4) !important; -} - -.ui.form .field.muted { - opacity: var(--opacity-disabled); -} - .text.primary { color: var(--color-primary) !important; } @@ -618,38 +522,18 @@ input:-webkit-autofill:active, color: var(--color-yellow) !important; } -.text.olive { - color: var(--color-olive) !important; -} - .text.green { color: var(--color-green) !important; } -.text.teal { - color: var(--color-teal) !important; -} - .text.blue { color: var(--color-blue) !important; } -.text.violet { - color: var(--color-violet) !important; -} - .text.purple { color: var(--color-purple) !important; } -.text.pink { - color: var(--color-pink) !important; -} - -.text.brown { - color: var(--color-brown) !important; -} - .text.black { color: var(--color-text) !important; } @@ -727,18 +611,6 @@ input:-webkit-autofill:active, vertical-align: middle; } -.ui .form .autofill-dummy { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - z-index: -10000; -} - -.ui .form .sub.field { - margin-left: 25px; -} - .ui .sha.label { font-family: var(--fonts-monospace); font-size: 13px; @@ -766,46 +638,6 @@ input:-webkit-autofill:active, font-weight: var(--font-weight-normal); } -.ui .background.red { - background-color: var(--color-red) !important; -} - -.ui .background.blue { - background-color: var(--color-blue) !important; -} - -.ui .background.black { - background-color: var(--color-black) !important; -} - -.ui .background.grey { - background-color: var(--color-grey) !important; -} - -.ui .background.light.grey { - background-color: var(--color-grey) !important; -} - -.ui .background.green { - background-color: var(--color-green) !important; -} - -.ui .background.purple { - background-color: var(--color-purple) !important; -} - -.ui .background.yellow { - background-color: var(--color-yellow) !important; -} - -.ui .background.orange { - background-color: var(--color-orange) !important; -} - -.ui .background.gold { - background-color: var(--color-gold) !important; -} - .ui .migrate { color: var(--color-text-light-2) !important; } @@ -822,46 +654,6 @@ input:-webkit-autofill:active, border: 1px solid; } -.ui .border.red { - border-color: var(--color-red) !important; -} - -.ui .border.blue { - border-color: var(--color-blue) !important; -} - -.ui .border.black { - border-color: var(--color-black) !important; -} - -.ui .border.grey { - border-color: var(--color-grey) !important; -} - -.ui .border.light.grey { - border-color: var(--color-grey) !important; -} - -.ui .border.green { - border-color: var(--color-green) !important; -} - -.ui .border.purple { - border-color: var(--color-purple) !important; -} - -.ui .border.yellow { - border-color: var(--color-yellow) !important; -} - -.ui .border.orange { - border-color: var(--color-orange) !important; -} - -.ui .border.gold { - border-color: var(--color-gold) !important; -} - .ui.floating.dropdown .overflow.menu .scrolling.menu.items { border-radius: 0 !important; box-shadow: none !important; diff --git a/web_src/css/form.css b/web_src/css/form.css index 42b97413fa..7a2cf4fcac 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -1,3 +1,111 @@ +.ui .form .autofill-dummy { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + z-index: -10000; +} + +.ui .form .sub.field { + margin-left: 25px; +} + +.ui.form .fields.error .field textarea, +.ui.form .fields.error .field select, +.ui.form .fields.error .field input:not([type]), +.ui.form .fields.error .field input[type="date"], +.ui.form .fields.error .field input[type="datetime-local"], +.ui.form .fields.error .field input[type="email"], +.ui.form .fields.error .field input[type="number"], +.ui.form .fields.error .field input[type="password"], +.ui.form .fields.error .field input[type="search"], +.ui.form .fields.error .field input[type="tel"], +.ui.form .fields.error .field input[type="time"], +.ui.form .fields.error .field input[type="text"], +.ui.form .fields.error .field input[type="file"], +.ui.form .fields.error .field input[type="url"], +.ui.form .fields.error .field .ui.dropdown, +.ui.form .fields.error .field .ui.dropdown .item, +.ui.form .field.error .ui.dropdown, +.ui.form .field.error .ui.dropdown .text, +.ui.form .field.error .ui.dropdown .item, +.ui.form .field.error textarea, +.ui.form .field.error select, +.ui.form .field.error input:not([type]), +.ui.form .field.error input[type="date"], +.ui.form .field.error input[type="datetime-local"], +.ui.form .field.error input[type="email"], +.ui.form .field.error input[type="number"], +.ui.form .field.error input[type="password"], +.ui.form .field.error input[type="search"], +.ui.form .field.error input[type="tel"], +.ui.form .field.error input[type="time"], +.ui.form .field.error input[type="text"], +.ui.form .field.error input[type="file"], +.ui.form .field.error input[type="url"], +.ui.form .field.error select:focus, +.ui.form .field.error input:not([type]):focus, +.ui.form .field.error input[type="date"]:focus, +.ui.form .field.error input[type="datetime-local"]:focus, +.ui.form .field.error input[type="email"]:focus, +.ui.form .field.error input[type="number"]:focus, +.ui.form .field.error input[type="password"]:focus, +.ui.form .field.error input[type="search"]:focus, +.ui.form .field.error input[type="tel"]:focus, +.ui.form .field.error input[type="time"]:focus, +.ui.form .field.error input[type="text"]:focus, +.ui.form .field.error input[type="file"]:focus, +.ui.form .field.error input[type="url"]:focus { + background-color: var(--color-error-bg); + border-color: var(--color-error-border); + color: var(--color-error-text); +} + +.ui.form .fields.error .field .ui.dropdown, +.ui.form .field.error .ui.dropdown, +.ui.form .fields.error .field .ui.dropdown:hover, +.ui.form .field.error .ui.dropdown:hover { + border-color: var(--color-error-border) !important; +} + +.ui.form .fields.error .field .ui.dropdown .menu .item:hover, +.ui.form .field.error .ui.dropdown .menu .item:hover { + background-color: var(--color-error-bg-hover); +} + +.ui.form .fields.error .field .ui.dropdown .menu .active.item, +.ui.form .field.error .ui.dropdown .menu .active.item { + background-color: var(--color-error-bg-active) !important; +} + +.ui.form .fields.error .dropdown .menu, +.ui.form .field.error .dropdown .menu { + border-color: var(--color-error-border) !important; +} + +input:-webkit-autofill, +input:-webkit-autofill:focus, +input:-webkit-autofill:hover, +input:-webkit-autofill:active, +.ui.form .field.field input:-webkit-autofill, +.ui.form .field.field input:-webkit-autofill:focus, +.ui.form .field.field input:-webkit-autofill:hover, +.ui.form .field.field input:-webkit-autofill:active { + -webkit-background-clip: text; + -webkit-text-fill-color: var(--color-text); + box-shadow: 0 0 0 100px var(--color-primary-light-6) inset !important; + border-color: var(--color-primary-light-4) !important; +} + +.ui.form .field.muted { + opacity: var(--opacity-disabled); +} + +.ui.form textarea:not([rows]) { + height: var(--min-height-textarea); /* override fomantic default 12em */ + min-height: var(--min-height-textarea); /* override fomantic default 8em */ +} + .ui.input textarea, .ui.form textarea, .ui.form input:not([type]), diff --git a/web_src/css/modules/tippy.css b/web_src/css/modules/tippy.css index 55b9751cc6..4438a31c9d 100644 --- a/web_src/css/modules/tippy.css +++ b/web_src/css/modules/tippy.css @@ -28,6 +28,10 @@ z-index: 1; } +.tippy-box[data-theme="default"] { + box-shadow: 0 6px 18px var(--color-shadow); +} + /* bare theme, no styling at all, except box-shadow */ .tippy-box[data-theme="bare"] { border: none; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 22bbe3cc23..2752174f86 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -784,7 +784,7 @@ td .commit-summary { box-shadow: none; } -.repository.view.issue .ui.depending .item.is-closed .title { +.repository.view.issue .ui.depending .item.is-closed .issue-dependency-title { text-decoration: line-through; } @@ -1480,16 +1480,12 @@ td .commit-summary { } .comment-header { - border: none !important; background: var(--color-box-header); - border-bottom: 1px solid var(--color-secondary) !important; - font-weight: var(--font-weight-normal) !important; - padding: 0.5rem 1rem; - margin: 0 !important; + border-bottom: 1px solid var(--color-secondary); + padding: 0 1rem; position: relative; color: var(--color-text); min-height: 41px; - background-color: var(--color-box-header); display: flex; justify-content: space-between; align-items: center; @@ -1634,7 +1630,7 @@ td .commit-summary { } .repo-button-row-left { - flex: 1; + flex-grow: 1; } .repo-button-row .button { diff --git a/web_src/css/repo/clone.css b/web_src/css/repo/clone.css index 3f6a1323fe..c6887fbf16 100644 --- a/web_src/css/repo/clone.css +++ b/web_src/css/repo/clone.css @@ -20,10 +20,12 @@ .clone-panel-tab .item { padding: 5px 10px; background: none; + color: var(--color-text-light-2); } .clone-panel-tab .item.active { - border-bottom: 3px solid var(--color-secondary); + color: var(--color-text-dark); + border-bottom: 3px solid currentcolor; } .clone-panel-tab + .divider { diff --git a/web_src/css/repo/commit-sign.css b/web_src/css/repo/commit-sign.css index 834fdd95d1..56eee62ffc 100644 --- a/web_src/css/repo/commit-sign.css +++ b/web_src/css/repo/commit-sign.css @@ -9,6 +9,7 @@ .ui.label.commit-id-short { font-family: var(--fonts-monospace); + height: 24px; } .ui.label.commit-id-short > .commit-sign-badge { @@ -16,7 +17,7 @@ padding: 0; border: 0 !important; border-radius: 0; - background: transparent; + background: transparent !important; } .ui.label.commit-id-short > .commit-sign-badge:hover { diff --git a/web_src/css/repo/home-file-list.css b/web_src/css/repo/home-file-list.css index 19ba1f2bcb..189b6406d4 100644 --- a/web_src/css/repo/home-file-list.css +++ b/web_src/css/repo/home-file-list.css @@ -65,6 +65,7 @@ } #repo-files-table .repo-file-last-commit { + min-width: 0; /* otherwise the flex axis is not limited and the text might overflow in Pale Moon */ background: var(--color-box-header); } diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index 65005e2263..96551979ea 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -1,7 +1,8 @@ .repo-grid-filelist-sidebar { display: grid; - grid-template-columns: auto 300px; + grid-template-columns: auto 280px; grid-template-rows: auto auto 1fr; + gap: var(--page-spacing); } .repo-home-filelist { @@ -13,13 +14,11 @@ .repo-home-sidebar-top { grid-column: 2; grid-row: 1; - padding-left: 1em; } .repo-home-sidebar-bottom { grid-column: 2; grid-row: 2; - padding-left: 1em; } .repo-home-sidebar-bottom .flex-list > :first-child { diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index 41793d60ed..876292fc94 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -1,5 +1,5 @@