{{if .IsRepo}}{{template "repo/header" .}}{{end}} -
- 404 -

{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404"}}{{end}}

- {{if .NotFoundGoBackURL}}{{ctx.Locale.Tr "go_back"}}{{end}} +
+
+
404 Not Found
+
+
{{if .NotFoundPrompt}}{{.NotFoundPrompt}}{{else}}{{ctx.Locale.Tr "error404"}}{{end}}
+ {{if .NotFoundGoBackURL}} + {{ctx.Locale.Tr "go_back"}} + {{end}} +
+
{{template "base/footer" .}} diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index 566fddcec1..0e8d0f6593 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -33,17 +33,18 @@
{{template "base/alert" .}} -
-

Internal Server Error

-
-
- {{if .ErrorMsg}} -

{{ctx.Locale.Tr "error.occurred"}}:

-
{{.ErrorMsg}}
- {{end}} -
- {{if or .SignedUser.IsAdmin .ShowFooterVersion}}

{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}

{{end}} - {{if .SignedUser.IsAdmin}}

{{ctx.Locale.Tr "error.report_message"}}

{{end}} +
+
500 Internal Server Error
+ {{if .ErrorMsg}} +
+

{{ctx.Locale.Tr "error.occurred"}}:

+
{{.ErrorMsg}}
+
+ {{end}} +
+ {{if or .SignedUser.IsAdmin .ShowFooterVersion}}

{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}

{{end}} + {{if .SignedUser.IsAdmin}}

{{ctx.Locale.Tr "error.report_message"}}

{{end}} +
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 34f09f0587..4aa64c5376 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3689,7 +3689,7 @@ }, { "type": "string", - "description": "sort repos by attribute. Supported values are \"alpha\", \"created\", \"updated\", \"size\", and \"id\". Default is \"alpha\"", + "description": "sort repos by attribute. Supported values are \"alpha\", \"created\", \"updated\", \"size\", \"git_size\", \"lfs_size\", \"stars\", \"forks\" and \"id\". Default is \"alpha\"", "name": "sort", "in": "query" }, @@ -13797,6 +13797,233 @@ } } }, + "/repos/{owner}/{repo}/tag_protections": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "List tag protections for a repository", + "operationId": "repoListTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/TagProtectionList" + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Create a tag protections for a repository", + "operationId": "repoCreateTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/CreateTagProtectionOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/TagProtection" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, + "/repos/{owner}/{repo}/tag_protections/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Get a specific tag protection for the repository", + "operationId": "repoGetTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of the tag protect to get", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/TagProtection" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Delete a specific tag protection for the repository", + "operationId": "repoDeleteTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of protected tag", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Edit a tag protections for a repository. Only fields that are set will be changed", + "operationId": "repoEditTagProtection", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of protected tag", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditTagProtectionOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/TagProtection" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + } + }, "/repos/{owner}/{repo}/tags": { "get": { "produces": [ @@ -19954,6 +20181,31 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CreateTagProtectionOption": { + "description": "CreateTagProtectionOption options for creating a tag protection", + "type": "object", + "properties": { + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + }, + "whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistUsernames" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CreateTeamOption": { "description": "CreateTeamOption options for creating a team", "type": "object", @@ -20870,6 +21122,31 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "EditTagProtectionOption": { + "description": "EditTagProtectionOption options for editing a tag protection", + "type": "object", + "properties": { + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + }, + "whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistUsernames" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "EditTeamOption": { "description": "EditTeamOption options for editing a team", "type": "object", @@ -22127,7 +22404,7 @@ "type": "object", "properties": { "Context": { - "description": "Context to render\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", "type": "string" }, "Mode": { @@ -22150,7 +22427,7 @@ "type": "object", "properties": { "Context": { - "description": "Context to render\n\nin: body", + "description": "URL path for rendering issue, media and file links\nExpected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}\n\nin: body", "type": "string" }, "FilePath": { @@ -24024,6 +24301,46 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "TagProtection": { + "description": "TagProtection represents a tag protection", + "type": "object", + "properties": { + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "name_pattern": { + "type": "string", + "x-go-name": "NamePattern" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + }, + "whitelist_teams": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistTeams" + }, + "whitelist_usernames": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "WhitelistUsernames" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "Team": { "description": "Team represents a team in an organization", "type": "object", @@ -25635,6 +25952,21 @@ } } }, + "TagProtection": { + "description": "TagProtection", + "schema": { + "$ref": "#/definitions/TagProtection" + } + }, + "TagProtectionList": { + "description": "TagProtectionList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/TagProtection" + } + } + }, "TasksList": { "description": "TasksList", "schema": { diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl index 9872096fbc..51e0e3b982 100644 --- a/templates/user/auth/signin_inner.tmpl +++ b/templates/user/auth/signin_inner.tmpl @@ -9,6 +9,8 @@ {{end}}
+ {{template "user/auth/webauthn_error" .}} +
{{.CsrfTokenHtml}}
@@ -49,6 +51,10 @@
{{end}} + + {{if .OAuth2Providers}}
{{ctx.Locale.Tr "sign_in_or"}} diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl index 278907e43f..b47a21e87c 100644 --- a/templates/user/dashboard/issues.tmpl +++ b/templates/user/dashboard/issues.tmpl @@ -51,11 +51,7 @@ - {{if .PageIsPulls}} - {{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr "search.pull_kind") "Tooltip" (ctx.Locale.Tr "explorer.go")}} - {{else}} - {{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr "search.issue_kind") "Tooltip" (ctx.Locale.Tr "explorer.go")}} - {{end}} + {{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr (Iif .PageIsPulls "search.pull_kind" "search.issue_kind")) "Tooltip" (ctx.Locale.Tr "explore.go_to")}}
diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl index 464228289e..7982cbd950 100644 --- a/templates/user/dashboard/navbar.tmpl +++ b/templates/user/dashboard/navbar.tmpl @@ -81,17 +81,17 @@ {{svg "octicon-rss"}} {{ctx.Locale.Tr "activities"}} - {{if not .UnitIssuesGlobalDisabled}} + {{if not ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled}} {{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "issues"}} {{end}} - {{if not .UnitPullsGlobalDisabled}} + {{if not ctx.Consts.RepoUnitTypePullRequests.UnitGlobalDisabled}} {{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "pull_requests"}} {{end}} - {{if and .ShowMilestonesDashboardPage (not (and .UnitIssuesGlobalDisabled .UnitPullsGlobalDisabled))}} + {{if and .ShowMilestonesDashboardPage (not (and ctx.Consts.RepoUnitTypeIssues.UnitGlobalDisabled ctx.Consts.RepoUnitTypePullRequests.UnitGlobalDisabled))}} {{svg "octicon-milestone"}} {{ctx.Locale.Tr "milestones"}} diff --git a/tests/e2e/README.md b/tests/e2e/README.md index e5fd1ca6c0..db083793d8 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -65,7 +65,7 @@ TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME= ## Running individual tests -Example command to run `example.test.e2e.js` test file: +Example command to run `example.test.e2e.ts` test file: _Note: unlike integration tests, this filtering is at the file level, not function_ diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index c8a792d6a3..d6d27e66be 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -28,7 +28,7 @@ import ( "code.gitea.io/gitea/tests" ) -var testE2eWebRoutes *web.Route +var testE2eWebRoutes *web.Router func TestMain(m *testing.M) { defer log.GetManager().Close() @@ -73,10 +73,10 @@ func TestMain(m *testing.M) { os.Exit(exitVal) } -// TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.js" files in this directory and build a test for each. +// TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.ts" files in this directory and build a test for each. func TestE2e(t *testing.T) { // Find the paths of all e2e test files in test directory. - searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.js") + searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.ts") paths, err := filepath.Glob(searchGlob) if err != nil { t.Fatal(err) diff --git a/tests/e2e/example.test.e2e.js b/tests/e2e/example.test.e2e.ts similarity index 52% rename from tests/e2e/example.test.e2e.js rename to tests/e2e/example.test.e2e.ts index 57c69a2917..1689f1b8ef 100644 --- a/tests/e2e/example.test.e2e.js +++ b/tests/e2e/example.test.e2e.ts @@ -1,57 +1,56 @@ -// @ts-check import {test, expect} from '@playwright/test'; -import {login_user, save_visual, load_logged_in_context} from './utils_e2e.js'; +import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); }); -test('Load Homepage', async ({page}) => { +test('homepage', async ({page}) => { const response = await page.goto('/'); - await expect(response?.status()).toBe(200); // Status OK + expect(response?.status()).toBe(200); // Status OK await expect(page).toHaveTitle(/^Gitea: Git with a cup of tea\s*$/); await expect(page.locator('.logo')).toHaveAttribute('src', '/assets/img/logo.svg'); }); -test('Test Register Form', async ({page}, workerInfo) => { +test('register', async ({page}, workerInfo) => { const response = await page.goto('/user/sign_up'); - await expect(response?.status()).toBe(200); // Status OK - await page.type('input[name=user_name]', `e2e-test-${workerInfo.workerIndex}`); - await page.type('input[name=email]', `e2e-test-${workerInfo.workerIndex}@test.com`); - await page.type('input[name=password]', 'test123test123'); - await page.type('input[name=retype]', 'test123test123'); + expect(response?.status()).toBe(200); // Status OK + await page.locator('input[name=user_name]').fill(`e2e-test-${workerInfo.workerIndex}`); + await page.locator('input[name=email]').fill(`e2e-test-${workerInfo.workerIndex}@test.com`); + await page.locator('input[name=password]').fill('test123test123'); + await page.locator('input[name=retype]').fill('test123test123'); await page.click('form button.ui.primary.button:visible'); // Make sure we routed to the home page. Else login failed. - await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); + expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible(); await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!'); save_visual(page); }); -test('Test Login Form', async ({page}, workerInfo) => { +test('login', async ({page}, workerInfo) => { const response = await page.goto('/user/login'); - await expect(response?.status()).toBe(200); // Status OK + expect(response?.status()).toBe(200); // Status OK - await page.type('input[name=user_name]', `user2`); - await page.type('input[name=password]', `password`); + await page.locator('input[name=user_name]').fill(`user2`); + await page.locator('input[name=password]').fill(`password`); await page.click('form button.ui.primary.button:visible'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle - await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); + expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); save_visual(page); }); -test('Test Logged In User', async ({browser}, workerInfo) => { +test('logged in user', async ({browser}, workerInfo) => { const context = await load_logged_in_context(browser, workerInfo, 'user2'); const page = await context.newPage(); await page.goto('/'); // Make sure we routed to the home page. Else login failed. - await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); + expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`); save_visual(page); }); diff --git a/tests/e2e/utils_e2e.js b/tests/e2e/utils_e2e.ts similarity index 81% rename from tests/e2e/utils_e2e.js rename to tests/e2e/utils_e2e.ts index d60c78b16e..14ec836600 100644 --- a/tests/e2e/utils_e2e.js +++ b/tests/e2e/utils_e2e.ts @@ -1,4 +1,5 @@ import {expect} from '@playwright/test'; +import {env} from 'node:process'; const ARTIFACTS_PATH = `tests/e2e/test-artifacts`; const LOGIN_PASSWORD = 'password'; @@ -13,16 +14,16 @@ export async function login_user(browser, workerInfo, user) { // Route to login page // Note: this could probably be done more quickly with a POST const response = await page.goto('/user/login'); - await expect(response?.status()).toBe(200); // Status OK + 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.click('form button.ui.primary.button:visible'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle - await expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`); + expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`); // Save state await context.storageState({path: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); @@ -44,8 +45,8 @@ export async function load_logged_in_context(browser, workerInfo, user) { export async function save_visual(page) { // Optionally include visual testing - if (process.env.VISUAL_TEST) { - await page.waitForLoadState('networkidle'); + if (env.VISUAL_TEST) { + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle // Mock page/version string await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK'); await expect(page).toHaveScreenshot({ diff --git a/tests/integration/api_packages_rubygems_test.go b/tests/integration/api_packages_rubygems_test.go index 5670731c49..fe9283df4d 100644 --- a/tests/integration/api_packages_rubygems_test.go +++ b/tests/integration/api_packages_rubygems_test.go @@ -4,7 +4,11 @@ package integration import ( + "archive/tar" "bytes" + "compress/gzip" + "crypto/sha256" + "crypto/sha512" "encoding/base64" "fmt" "mime/multipart" @@ -21,101 +25,167 @@ import ( "github.com/stretchr/testify/assert" ) +type tarFile struct { + Name string + Data []byte +} + +func makeArchiveFileTar(files []*tarFile) []byte { + buf := new(bytes.Buffer) + tarWriter := tar.NewWriter(buf) + for _, file := range files { + _ = tarWriter.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: file.Name, + Mode: 0o644, + Size: int64(len(file.Data)), + }) + _, _ = tarWriter.Write(file.Data) + } + _ = tarWriter.Close() + return buf.Bytes() +} + +func makeArchiveFileGz(data []byte) []byte { + buf := new(bytes.Buffer) + gzWriter, _ := gzip.NewWriterLevel(buf, gzip.NoCompression) + _, _ = gzWriter.Write(data) + _ = gzWriter.Close() + return buf.Bytes() +} + +func makeRubyGem(name, version string) []byte { + metadataContent := fmt.Sprintf(`--- !ruby/object:Gem::Specification +name: %s +version: !ruby/object:Gem::Version + version: %s +platform: ruby +authors: +- Gitea +autorequire: +bindir: bin +cert_chain: [] +date: 2021-08-23 00:00:00.000000000 Z +dependencies: +- !ruby/object:Gem::Dependency + name: runtime-dep + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 1.2.0 + - - "<" + - !ruby/object:Gem::Version + version: '2.0' + type: :runtime + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 1.2.0 + - - "<" + - !ruby/object:Gem::Version + version: '2.0' +- !ruby/object:Gem::Dependency + name: dev-dep + requirement: !ruby/object:Gem::Requirement + requirements: + - - "~>" + - !ruby/object:Gem::Version + version: '5.2' + type: :development + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - "~>" + - !ruby/object:Gem::Version + version: '5.2' +description: RubyGems package test +email: rubygems@gitea.io +executables: [] +extensions: [] +extra_rdoc_files: [] +files: +- lib/gitea.rb +homepage: https://gitea.io/ +licenses: +- MIT +metadata: {} +post_install_message: +rdoc_options: [] +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 2.3.0 +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '1.0' +requirements: [] +rubyforge_project: +rubygems_version: 2.7.6.2 +signing_key: +specification_version: 4 +summary: Gitea package +test_files: [] +`, name, version) + + metadataGz := makeArchiveFileGz([]byte(metadataContent)) + dataTarGz := makeArchiveFileGz(makeArchiveFileTar([]*tarFile{ + { + Name: "lib/gitea.rb", + Data: []byte("class Gitea\nend"), + }, + })) + + checksumsYaml := fmt.Sprintf(`--- +SHA256: + metadata.gz: %x + data.tar.gz: %x +SHA512: + metadata.gz: %x + data.tar.gz: %x +`, sha256.Sum256(metadataGz), sha256.Sum256(dataTarGz), sha512.Sum512(metadataGz), sha512.Sum512(dataTarGz)) + + files := []*tarFile{ + { + Name: "data.tar.gz", + Data: dataTarGz, + }, + { + Name: "metadata.gz", + Data: metadataGz, + }, + { + Name: "checksums.yaml.gz", + Data: makeArchiveFileGz([]byte(checksumsYaml)), + }, + } + return makeArchiveFileTar(files) +} + func TestPackageRubyGems(t *testing.T) { defer tests.PrepareTestEnv(t)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - packageName := "gitea" - packageVersion := "1.0.5" - packageFilename := "gitea-1.0.5.gem" + testGemName := "gitea" + testGemVersion := "1.0.5" + testGemContent := makeRubyGem(testGemName, testGemVersion) + testGemContentChecksum := fmt.Sprintf("%x", sha256.Sum256(testGemContent)) - gemContent, _ := base64.StdEncoding.DecodeString(`bWV0YWRhdGEuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAw -MAAwMDAwMDAwADAwMDAwMDAxMDQxADE0MTEwNzcyMzY2ADAxMzQ0MQAgMAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVsAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAw -MDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf -iwgA9vQjYQID1VVNb9QwEL37V5he9pRsmlJAFlQckCoOXAriQIUix5nNmsYf2JOqKwS/nYmz2d3Q -qqCCKpFdadfjmfdm5nmcLMv4k9DXm6Wrv4BCcQ5GiPcelF5pJVE7y6w0IHirESS7hhDJJu4I+jhu -Mc53Tsd5kZ8y30lcuWAEH2KY7HHtQhQs4+cJkwwuwNdeB6JhtbaNDoLTL1MQsFJrqQnr8jNrJJJH -WZTHWfEiK094UYj0zYvp4Z9YAx5sA1ZpSCS3M30zeWwo2bG60FvUBjIKJts2GwMW76r0Yr9NzjN3 -YhwsGX2Ozl4dpcWwvK9d43PQtDIv9igvHwSyIIwFmXHjqTqxLY8MPkCADmQk80p2EfZ6VbM6/ue6 -/1D0Bq7/qeA/zh6W82leHmhFWUHn/JbsEfT6q7QbiCpoj8l0QcEUFLmX6kq2wBEiMjBSd+Pwt7T5 -Ot0kuXYMbkD1KOuOBnWYb7hBsAP4bhlkFRqnqpWefMZ/pHCn6+WIFGq2dgY8EQq+RvRRLJcTyZJ1 -WhHqGPTu7QdmACXdJFLwb9+ZdxErbSPKrqsMxJhAWCJ1qaqRdtu6yktcT/STsamG0qp7rsa5EL/K -MBua30uw4ynzExqYWRJDfx8/kQWN3PwsDh2jYLr1W+pZcAmCs9splvnz/Flesqhbq21bXcGG/OLh -+2fv/JTF3hgZyCW9OaZjxoZjdnBGfgKpxZyJ1QYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGF0 -YS50YXIuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAwMAAw -MDAwMDAwADAwMDAwMDAwMjQyADE0MTEwNzcyMzY2ADAxMzM2MQAgMAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfiwgA -9vQjYQID7M/NCsMgDABgz32KrA/QxersK/Q17ExXIcyhlr7+HLv1sJ02KPhBCPk5JOyn881nsl2c -xI+gRDRaC3zbZ8RBCamlxGHolTFlX11kLwDFH6wp21hO2RYi/rD3bb5/7iCubFOCMbBtABzNkIjn -bvGlAnisOUE7EnOALUR2p7b06e6aV4iqqqrquJ4AAAD//wMA+sA/NQAIAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNoZWNr -c3Vtcy55YW1sLmd6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwNDQ0ADAwMDAwMDAAMDAw -MDAwMAAwMDAwMDAwMDQ1MAAxNDExMDc3MjM2NgAwMTQ2MTIAIDAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdXN0YXIAMDB3aGVlbAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAwMDAwMAAwMDAwMDAwAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4sIAPb0 -I2ECA2WQOa4UQAxE8znFXGCQ21vbPyMj5wRuL0Qk6EecnmZCyKyy9FSvXq/X4/u3ryj68Xg+f/Zn -VHzGlx+/P57qvU4XxWalBKftSXOgCjNYkdRycrC5Axem+W4HqS12PNEv7836jF9vnlHxwSyxKY+y -go0cPblyHzkrZ4HF1GSVhe7mOOoasXNk2fnbUxb+19Pp9tobD/QlJKMX7y204PREh6nQ5hG9Alw6 -x4TnmtA+aekGfm6wAseog2LSgpR4Q7cYnAH3K4qAQa6A6JCC1gpuY7P+9YxE5SZ+j0eVGbaBTwBQ -iIqRUyyzLCoFCBdYNWxniapTavD97blXTzFvgoVoAsKBAtlU48cdaOmeZDpwV01OtcGwjscfeUrY -B9QBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) + testAnotherGemName := "gitea-another" + testAnotherGemVersion := "0.99" root := fmt.Sprintf("/api/packages/%s/rubygems", user.Name) - uploadFile := func(t *testing.T, expectedStatus int) { - req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/api/v1/gems", root), bytes.NewReader(gemContent)). + uploadFile := func(t *testing.T, content []byte, expectedStatus int) { + req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/api/v1/gems", root), bytes.NewReader(content)). AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } @@ -123,7 +193,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) t.Run("Upload", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - uploadFile(t, http.StatusCreated) + uploadFile(t, testGemContent, http.StatusCreated) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems) assert.NoError(t, err) @@ -133,34 +203,33 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) assert.NoError(t, err) assert.NotNil(t, pd.SemVer) assert.IsType(t, &rubygems.Metadata{}, pd.Metadata) - assert.Equal(t, packageName, pd.Package.Name) - assert.Equal(t, packageVersion, pd.Version.Version) + assert.Equal(t, testGemName, pd.Package.Name) + assert.Equal(t, testGemVersion, pd.Version.Version) pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID) assert.NoError(t, err) assert.Len(t, pfs, 1) - assert.Equal(t, packageFilename, pfs[0].Name) + assert.Equal(t, fmt.Sprintf("%s-%s.gem", testGemName, testGemVersion), pfs[0].Name) assert.True(t, pfs[0].IsLead) pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID) assert.NoError(t, err) - assert.Equal(t, int64(4608), pb.Size) + assert.EqualValues(t, len(testGemContent), pb.Size) }) t.Run("UploadExists", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - - uploadFile(t, http.StatusConflict) + uploadFile(t, testGemContent, http.StatusConflict) }) t.Run("Download", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", fmt.Sprintf("%s/gems/%s", root, packageFilename)). + req := NewRequest(t, "GET", fmt.Sprintf("%s/gems/%s-%s.gem", root, testGemName, testGemVersion)). AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, gemContent, resp.Body.Bytes()) + assert.Equal(t, testGemContent, resp.Body.Bytes()) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems) assert.NoError(t, err) @@ -171,7 +240,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) t.Run("DownloadGemspec", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - req := NewRequest(t, "GET", fmt.Sprintf("%s/quick/Marshal.4.8/%sspec.rz", root, packageFilename)). + req := NewRequest(t, "GET", fmt.Sprintf("%s/quick/Marshal.4.8/%s-%s.gemspec.rz", root, testGemName, testGemVersion)). AddBasicAuth(user.Name) resp := MakeRequest(t, req, http.StatusOK) @@ -206,22 +275,63 @@ gAAAAP//MS06Gw==`) enumeratePackages(t, "prerelease_specs.4.8.gz", b) }) - t.Run("Delete", func(t *testing.T) { + t.Run("UploadAnother", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + uploadFile(t, makeRubyGem(testAnotherGemName, testAnotherGemVersion), http.StatusCreated) + }) + + t.Run("PackageInfo", func(t *testing.T) { defer tests.PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/info/%s", root, testGemName)).AddBasicAuth(user.Name) + resp := MakeRequest(t, req, http.StatusOK) + expected := fmt.Sprintf(`--- +1.0.5 runtime-dep:>= 1.2.0&< 2.0|checksum:%s,ruby:>= 2.3.0,rubygems:>= 1.0 +`, testGemContentChecksum) + assert.Equal(t, expected, resp.Body.String()) + }) + + t.Run("Versions", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/versions", root)).AddBasicAuth(user.Name) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, `--- +gitea 1.0.5 08843c2dd0ea19910e6b056b98e38f1c +gitea-another 0.99 8b639e4048d282941485368ec42609be +`, resp.Body.String()) + }) + + deleteGemPackage := func(t *testing.T, packageName, packageVersion string) { body := bytes.Buffer{} writer := multipart.NewWriter(&body) - writer.WriteField("gem_name", packageName) - writer.WriteField("version", packageVersion) - writer.Close() - + _ = writer.WriteField("gem_name", packageName) + _ = writer.WriteField("version", packageVersion) + _ = writer.Close() req := NewRequestWithBody(t, "DELETE", fmt.Sprintf("%s/api/v1/gems/yank", root), &body). SetHeader("Content-Type", writer.FormDataContentType()). AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusOK) + } + t.Run("DeleteAll", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + deleteGemPackage(t, testGemName, testGemVersion) + deleteGemPackage(t, testAnotherGemName, testAnotherGemVersion) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems) assert.NoError(t, err) assert.Empty(t, pvs) }) + + t.Run("PackageInfoAfterDelete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/info/%s", root, testGemName)).AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusNotFound) + }) + + t.Run("VersionsAfterDelete", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + req := NewRequest(t, "GET", fmt.Sprintf("%s/versions", root)).AddBasicAuth(user.Name) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "---\n", resp.Body.String()) + }) } diff --git a/tests/integration/compare_test.go b/tests/integration/compare_test.go index 9f73ac80e2..d960416b3a 100644 --- a/tests/integration/compare_test.go +++ b/tests/integration/compare_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/test" repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" @@ -30,9 +31,9 @@ func TestCompareTag(t *testing.T) { // A dropdown for both base and head. assert.Lenf(t, selection.Nodes, 2, "The template has changed") - req = NewRequest(t, "GET", "/user2/repo1/compare/invalid") + req = NewRequest(t, "GET", "/user2/repo1/compare/invalid").SetHeader("Accept", "text/html") resp = session.MakeRequest(t, req, http.StatusNotFound) - assert.False(t, strings.Contains(resp.Body.String(), "/assets/img/500.png"), "expect 404 page not 500") + assert.True(t, test.IsNormalPageCompleted(resp.Body.String()), "expect 404 page not 500") } // Compare with inferred default branch (master) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index 18f415083c..ae8ff51d43 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -40,7 +40,7 @@ import ( "github.com/xeipuuv/gojsonschema" ) -var testWebRoutes *web.Route +var testWebRoutes *web.Router type NilResponseRecorder struct { httptest.ResponseRecorder diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go index d103e2b0a9..d3b30448fc 100644 --- a/tests/integration/links_test.go +++ b/tests/integration/links_test.go @@ -37,8 +37,6 @@ func TestLinksNoLogin(t *testing.T) { "/user2/repo1/projects", "/user2/repo1/projects/1", "/user2/repo1/releases/tag/delete-tag", // It's the only one existing record on release.yml which has is_tag: true - "/assets/img/404.png", - "/assets/img/500.png", "/.well-known/security.txt", } diff --git a/tests/integration/nonascii_branches_test.go b/tests/integration/nonascii_branches_test.go index 8917a9b574..a189273eac 100644 --- a/tests/integration/nonascii_branches_test.go +++ b/tests/integration/nonascii_branches_test.go @@ -4,6 +4,7 @@ package integration import ( + "fmt" "net/http" "net/url" "path" @@ -14,22 +15,6 @@ import ( "github.com/stretchr/testify/assert" ) -func testSrcRouteRedirect(t *testing.T, session *TestSession, user, repo, route, expectedLocation string, expectedStatus int) { - prefix := path.Join("/", user, repo, "src") - - // Make request - req := NewRequest(t, "GET", path.Join(prefix, route)) - resp := session.MakeRequest(t, req, http.StatusSeeOther) - - // Check Location header - location := resp.Header().Get("Location") - assert.Equal(t, path.Join(prefix, expectedLocation), location) - - // Perform redirect - req = NewRequest(t, "GET", location) - session.MakeRequest(t, req, expectedStatus) -} - func setDefaultBranch(t *testing.T, session *TestSession, user, repo, branch string) { location := path.Join("/", user, repo, "settings/branches") csrf := GetCSRF(t, session, location) @@ -41,7 +26,7 @@ func setDefaultBranch(t *testing.T, session *TestSession, user, repo, branch str session.MakeRequest(t, req, http.StatusSeeOther) } -func TestNonasciiBranches(t *testing.T) { +func TestNonAsciiBranches(t *testing.T) { testRedirects := []struct { from string to string @@ -98,6 +83,7 @@ func TestNonasciiBranches(t *testing.T) { to: "branch/%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81", status: http.StatusOK, }, + // Tags { from: "Тэг", @@ -119,6 +105,7 @@ func TestNonasciiBranches(t *testing.T) { to: "tag/%E3%82%BF%E3%82%B0/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md", status: http.StatusOK, }, + // Files { from: "README.md", @@ -135,6 +122,7 @@ func TestNonasciiBranches(t *testing.T) { to: "branch/Plus+Is+Not+Space/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB.md", status: http.StatusNotFound, // it's not on default branch }, + // Same but url-encoded (few tests) { from: "%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81", @@ -205,10 +193,23 @@ func TestNonasciiBranches(t *testing.T) { session := loginUser(t, user) setDefaultBranch(t, session, user, repo, "Plus+Is+Not+Space") + defer setDefaultBranch(t, session, user, repo, "master") for _, test := range testRedirects { - testSrcRouteRedirect(t, session, user, repo, test.from, test.to, test.status) - } + t.Run(test.from, func(t *testing.T) { + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/src/%s", user, repo, test.from)) + resp := session.MakeRequest(t, req, http.StatusSeeOther) + if resp.Code != http.StatusSeeOther { + return + } - setDefaultBranch(t, session, user, repo, "master") + redirectLocation := resp.Header().Get("Location") + if !assert.Equal(t, fmt.Sprintf("/%s/%s/src/%s", user, repo, test.to), redirectLocation) { + return + } + + req = NewRequest(t, "GET", redirectLocation) + session.MakeRequest(t, req, test.status) + }) + } } diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index 7633d6915f..9f938c4099 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -247,7 +247,7 @@ func TestChangeRepoFilesForCreate(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetParams(":id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -284,7 +284,7 @@ func TestChangeRepoFilesForUpdate(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetParams(":id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -318,7 +318,7 @@ func TestChangeRepoFilesForUpdateWithFileMove(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetParams(":id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -369,7 +369,7 @@ func TestChangeRepoFilesWithoutBranchNames(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetParams(":id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -405,7 +405,7 @@ func testDeleteRepoFiles(t *testing.T, u *url.URL) { // setup unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetParams(":id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -444,7 +444,7 @@ func testDeleteRepoFilesWithoutBranchNames(t *testing.T, u *url.URL) { // setup unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetParams(":id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) @@ -474,7 +474,7 @@ func TestChangeRepoFilesErrors(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetParams(":id", "1") + ctx.SetPathParam(":id", "1") contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..7ddbada765 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "include": [ + "*", + "tests/e2e/**/*", + "tools/**/*", + "web_src/js/**/*", + ], + "compilerOptions": { + "target": "es2020", + "module": "node16", + "moduleResolution": "node16", + "lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"], + "allowImportingTsExtensions": true, + "allowJs": true, + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "esModuleInterop": true, + "isolatedModules": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "stripInternal": true, + "strict": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": false, + "exactOptionalPropertyTypes": false, + } +} diff --git a/vitest.config.js b/vitest.config.ts similarity index 100% rename from vitest.config.js rename to vitest.config.ts diff --git a/web_src/css/base.css b/web_src/css/base.css index eef4eb6eff..223d9fbad6 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -477,6 +477,20 @@ img.ui.avatar, padding-bottom: 80px; } +.status-page-error { + margin-top: max(45vh - 90px, 80px); + margin-bottom: 80px; +} + +.status-page-error-title { + font-size: 48px; + margin-bottom: 14px; /* some elements below may use tw-my-4 or tw-my-8, so use 14px as a minimal margin */ + line-height: initial; + text-align: center; + font-weight: var(--font-weight-bold); + color: var(--color-text-light-2); +} + /* add margin below .secondary nav when it is the first child */ .page-content > :first-child.secondary-nav { margin-bottom: 14px; diff --git a/web_src/css/features/gitgraph.css b/web_src/css/features/gitgraph.css index 6a04c44e51..f8f7e35cdc 100644 --- a/web_src/css/features/gitgraph.css +++ b/web_src/css/features/gitgraph.css @@ -123,12 +123,6 @@ padding-bottom: 1px; } -#git-graph-container #rev-list .author img.ui.avatar { - width: auto; - height: 18px; - max-width: none; -} - #git-graph-container #graph-raw-list { margin: 0; } diff --git a/web_src/css/features/tribute.css b/web_src/css/features/tribute.css index bd843675e1..99a026b9bc 100644 --- a/web_src/css/features/tribute.css +++ b/web_src/css/features/tribute.css @@ -17,7 +17,6 @@ .tribute-container li span.fullname { font-weight: var(--font-weight-normal); font-size: 0.8rem; - margin-left: 3px; } .tribute-container li.highlight, @@ -29,14 +28,5 @@ .tribute-item { display: flex; align-items: center; -} - -.tribute-item .emoji, -.tribute-item img[src*="/avatar/"] { - margin-right: 0.5rem; -} - -.tribute-container img { - width: 1.5rem !important; - height: 1.5rem !important; + gap: 6px; } diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index 9546c11d6a..d2dcf2ec6e 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -2,7 +2,11 @@ overflow: hidden; font-size: 16px; line-height: 1.5 !important; - overflow-wrap: anywhere; + overflow-wrap: break-word; +} + +.conversation-holder .markup { + overflow-wrap: anywhere; /* prevent overflow in code comments. TODO: properly restrict .conversation-holder width and remove this */ } .markup > *:first-child { diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index a86c9234aa..481e997d4f 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -92,20 +92,22 @@ code.language-math.is-loading::after { } } -@keyframes pulse { +/* 1p5 means 1-point-5. it can't use "pulse" here, otherwise the animation is not right (maybe due to some conflicts */ +@keyframes pulse-1p5 { 0% { transform: scale(1); } 50% { - transform: scale(1.8); + transform: scale(1.5); } 100% { transform: scale(1); } } -.pulse { - animation: pulse 2s linear; +/* pulse animation for scale(1.5) in 200ms */ +.pulse-1p5-200 { + animation: pulse-1p5 200ms linear; } .ui.modal, diff --git a/web_src/css/modules/comment.css b/web_src/css/modules/comment.css index 672808e9cc..cda16fdddc 100644 --- a/web_src/css/modules/comment.css +++ b/web_src/css/modules/comment.css @@ -50,10 +50,6 @@ background: none; } -.ui.comments .comment .avatar { - width: 30px; -} - .ui.comments .comment > .content { display: flex; flex-direction: column; diff --git a/web_src/css/modules/toast.css b/web_src/css/modules/toast.css index 2a9f78e017..1145f3b1b5 100644 --- a/web_src/css/modules/toast.css +++ b/web_src/css/modules/toast.css @@ -22,17 +22,31 @@ overflow-wrap: anywhere; } -.toast-close, -.toast-icon { - color: currentcolor; +.toast-close { border-radius: var(--border-radius); - background: transparent; - border: none; - display: flex; width: 30px; height: 30px; justify-content: center; +} + +.toast-icon { + display: inline-flex; + width: 30px; + height: 30px; align-items: center; + justify-content: center; +} + +.toast-duplicate-number::before { + content: "("; +} +.toast-duplicate-number { + display: inline-block; + margin-right: 5px; + user-select: none; +} +.toast-duplicate-number::after { + content: ")"; } .toast-close:hover { diff --git a/web_src/css/org.css b/web_src/css/org.css index 32e8a914fa..148cb975e4 100644 --- a/web_src/css/org.css +++ b/web_src/css/org.css @@ -127,8 +127,6 @@ } .page-content.organization .members .ui.avatar { - width: 48px; - height: 48px; margin-right: 5px; margin-bottom: 5px; } diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 357a4ee195..f34b1e7ea5 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -79,6 +79,11 @@ white-space: nowrap; } +.repository .issue-content-right .filter.menu { + max-height: 500px; + overflow-x: auto; +} + .repository .filter.menu.labels .label-filter .menu .info { display: inline-block; padding: 0.5rem 0; @@ -455,14 +460,12 @@ td .commit-summary { } .repository.file.editor .commit-form-wrapper { - padding-left: 64px; + padding-left: 48px; } .repository.file.editor .commit-form-wrapper .commit-avatar { float: left; - margin-left: -64px; - width: 3em; - height: auto; + margin-left: -48px; } .repository.file.editor .commit-form-wrapper .commit-form { @@ -530,14 +533,6 @@ td .commit-summary { min-width: 100px; } -.repository.new.issue .comment.form .comment .avatar { - width: 3em; -} - -.repository.new.issue .comment.form .content { - margin-left: 4em; -} - .repository.new.issue .comment.form .content::before, .repository.new.issue .comment.form .content::after { right: 100%; @@ -566,11 +561,6 @@ td .commit-summary { font-size: 14px; } -.repository.new.issue .comment.form .issue-content-right .filter.menu { - max-height: 500px; - overflow-x: auto; -} - .repository.view.issue .instruct-toggle { display: inline-block; } @@ -1220,16 +1210,6 @@ td .commit-summary { border: 1px solid var(--color-light-border); } -.repository #commits-table td.sha .sha.label .ui.signature.avatar, -.repository #repo-files-table .sha.label .ui.signature.avatar, -.repository #repo-file-commit-box .sha.label .ui.signature.avatar, -.repository #rev-list .sha.label .ui.signature.avatar, -.repository .timeline-item.commits-list .singular-commit .sha.label .ui.signature.avatar { - height: 16px; - margin-bottom: 0; - width: 16px; -} - .repository #commits-table td.sha .sha.label .detail.icon, .repository #repo-files-table .sha.label .detail.icon, .repository #repo-file-commit-box .sha.label .detail.icon, @@ -1246,14 +1226,6 @@ td .commit-summary { border-bottom-left-radius: 0; } -.repository #commits-table td.sha .sha.label .detail.icon img, -.repository #repo-files-table .sha.label .detail.icon img, -.repository #repo-file-commit-box .sha.label .detail.icon img, -.repository #rev-list .sha.label .detail.icon img, -.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon img { - margin-right: 0; -} - .repository #commits-table td.sha .sha.label .detail.icon .svg, .repository #repo-files-table .sha.label .detail.icon .svg, .repository #repo-file-commit-box .sha.label .detail.icon .svg, @@ -1935,8 +1907,6 @@ td .commit-summary { } .user-cards .list .item .avatar { - width: 48px; - height: 48px; float: left; display: block; margin-right: 10px; diff --git a/web_src/js/components/.eslintrc.yaml b/web_src/js/components/.eslintrc.yaml deleted file mode 100644 index a79e96f330..0000000000 --- a/web_src/js/components/.eslintrc.yaml +++ /dev/null @@ -1,22 +0,0 @@ -plugins: - - eslint-plugin-vue - - eslint-plugin-vue-scoped-css - -extends: - - ../../../.eslintrc.yaml - - plugin:vue/vue3-recommended - - plugin:vue-scoped-css/vue3-recommended - -parserOptions: - sourceType: module - ecmaVersion: latest - -env: - browser: true - -rules: - vue/attributes-order: [0] - vue/html-closing-bracket-spacing: [2, {startTag: never, endTag: never, selfClosingTag: never}] - vue/max-attributes-per-line: [0] - vue/singleline-html-element-content-newline: [0] - vue-scoped-css/enforce-style-type: [0] diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue index 6a4a84f615..53e0bce76c 100644 --- a/web_src/js/components/DiffCommitSelector.vue +++ b/web_src/js/components/DiffCommitSelector.vue @@ -132,7 +132,7 @@ export default { })); this.commits.reverse(); this.lastReviewCommitSha = results.last_review_commit_sha || null; - if (this.lastReviewCommitSha && this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) === -1) { + if (this.lastReviewCommitSha && !this.commits.some((x) => x.id === this.lastReviewCommitSha)) { // the lastReviewCommit is not available (probably due to a force push) // reset the last review commit sha this.lastReviewCommitSha = null; diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 7f6524c7e3..e751018f90 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -89,7 +89,9 @@ const sfc = { // load job data and then auto-reload periodically // need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener await this.loadJob(); - this.intervalID = setInterval(this.loadJob, 1000); + this.intervalID = setInterval(() => { + this.loadJob(); + }, 1000); document.body.addEventListener('click', this.closeDropdown); this.hashChangeListener(); window.addEventListener('hashchange', this.hashChangeListener); @@ -797,7 +799,7 @@ export function initRepositoryActionView() { } -