Browse Source

Kd/ci playwright go test (#20123)

* Add initial playwright config

* Simplify Makefile

* Simplify Makefile

* Use correct config files

* Update playwright settings

* Fix package-lock file

* Don't use test logger for e2e tests

* fix frontend lint

* Allow passing TEST_LOGGER variable

* Init postgres database

* use standard gitea env variables

* Update playwright

* update drone

* Move empty env var to commands

* Cleanup

* Move integrations to subfolder

* tests integrations to tests integraton

* Run e2e tests with go test

* Fix linting

* install CI deps

* Add files to ESlint

* Fix drone typo

* Don't log to console in CI

* Use go test http server

* Add build step before tests

* Move shared init function to common package

* fix drone

* Clean up tests

* Fix linting

* Better mocking for page + version string

* Cleanup test generation

* Remove dependency on gitea binary

* Fix linting

* add initial support for running specific tests

* Add ACCEPT_VISUAL variable

* don't require git-lfs

* Add initial documentation

* Review feedback

* Add logged in session test

* Attempt fixing drone race

* Cleanup and bump version

* Bump deps

* Review feedback

* simplify installation

* Fix ci

* Update install docs
pull/21039/head
Kyle D 1 month ago committed by GitHub
parent
commit
c8ded77680
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 69
      .drone.yml
  2. 23
      .gitignore
  3. 8
      CONTRIBUTING.md
  4. 188
      Makefile
  5. 6
      build/code-batch-process.go
  6. 2
      contrib/pr/checkout.go
  7. 4
      docs/content/doc/developers/guidelines-backend.md
  8. 2
      docs/content/doc/developers/hacking-on-gitea.en-us.md
  9. 4
      models/migrations/migrations_test.go
  10. 2
      models/migrations/testlogger_test.go
  11. 4
      models/unittest/testdb.go
  12. 2
      modules/markup/html_test.go
  13. 2
      modules/markup/markdown/markdown_test.go
  14. 45
      package-lock.json
  15. 1
      package.json
  16. 100
      playwright.config.js
  17. 93
      tests/e2e/README.md
  18. 120
      tests/e2e/e2e_test.go
  19. 57
      tests/e2e/example.test.e2e.js
  20. 60
      tests/e2e/utils_e2e.js
  21. 57
      tests/e2e/utils_e2e_test.go
  22. 0
      tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/HEAD
  23. 0
      tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/config
  24. 0
      tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description
  25. 0
      tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude
  26. 0
      tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/74/8bf557dfc9c6457998b5118a6c8b2129f56c30
  27. 0
      tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/a5/46f86c7dd182592b96639045e176dde8df76ef
  28. 0
      tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/b8/95782bd271fdd266dd06e5880ea4abdc3a0dc7
  29. 0
      tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/refs/heads/master
  30. 0
      tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/HEAD
  31. 0
      tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/config
  32. 0
      tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description
  33. 0
      tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude
  34. 0
      tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/21/2f14c8b713de38bd0b3fb23bd288369b01668a
  35. 0
      tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/90/e402c3937a4639725fcc59ca1f529e7dc8506f
  36. 0
      tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/ed/d9c1000cd1444efd63e153e3554c8d5656bf65
  37. 0
      tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/refs/heads/master
  38. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/HEAD
  39. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/config
  40. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/description
  41. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-checkout
  42. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-commit
  43. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/hooks/post-merge
  44. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/hooks/pre-push
  45. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/index
  46. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/d6/f1/d6f175817f886ec6fbbc1515326465fa96c3bfd54a4ea06cfd6dbbd8340e0152
  47. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/lfs/objects/fb/8f/fb8f7d8435968c4f82a726a92395be4d16f2f63116caf36c8ad35c60831ab041
  48. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/objects/54/6244003622c64b2fc3c2cd544d7a29882c8383
  49. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/objects/6a/6ccf5d874fec134ee712572cc03a0f2dd7afec
  50. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/objects/a6/7134b8484c2abe9fa954e1fd83b39b271383ed
  51. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/objects/b7/01ed6ffe410f0c3ac204b929ea47cfec6cef54
  52. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/objects/f2/07b74f55cd7f9e800b7550d587cbc488f6eaf1
  53. 0
      tests/gitea-repositories-meta/migration/lfs-test.git/refs/heads/master
  54. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMITMESSAGE
  55. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/COMMIT_EDITMSG
  56. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/HEAD
  57. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/config
  58. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/config.backup
  59. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/description
  60. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/index
  61. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/info/exclude
  62. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/HEAD
  63. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/branch1
  64. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/logs/refs/heads/master
  65. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
  66. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
  67. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
  68. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/branch1
  69. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker.git/refs/heads/master
  70. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMITMESSAGE
  71. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/COMMIT_EDITMSG
  72. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/HEAD
  73. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config
  74. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/config.backup
  75. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/description
  76. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/index
  77. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/info/exclude
  78. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/HEAD
  79. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/branch1
  80. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/logs/refs/heads/master
  81. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
  82. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
  83. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
  84. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/branch1
  85. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_alpha.git/refs/heads/master
  86. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMITMESSAGE
  87. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/COMMIT_EDITMSG
  88. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/HEAD
  89. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config
  90. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/config.backup
  91. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/description
  92. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/index
  93. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/info/exclude
  94. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/HEAD
  95. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/branch1
  96. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/logs/refs/heads/master
  97. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/ba/ea7d6e6b7773a80bcede323cfb21dfe9d4b855
  98. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/c2/a1ad4c931cebe27c7e39176fe7119b5557c9eb
  99. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/objects/cd/aca8cf1d36e1e4e508a940f6e157e239beccfa
  100. 0
      tests/gitea-repositories-meta/org26/repo_external_tracker_numeric.git/refs/heads/branch1
  101. Some files were not shown because too many files have changed in this diff Show More

69
.drone.yml

@ -498,6 +498,75 @@ steps:
- name: deps
path: /go
---
kind: pipeline
type: docker
name: testing-e2e
platform:
os: linux
arch: amd64
depends_on:
- compliance
trigger:
event:
- pull_request
volumes:
- name: deps
temp: {}
services:
- name: pgsql
pull: default
image: postgres:10
environment:
POSTGRES_DB: testgitea-e2e
POSTGRES_PASSWORD: postgres
POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate='en_US.UTF-8' --lc-ctype='en_US.UTF-8'
steps:
- name: deps-frontend
image: node:16
pull: always
commands:
- make deps-frontend
- name: build-frontend
image: node:16
commands:
- make frontend
depends_on: [deps-frontend]
- name: deps-backend
image: golang:1.18
pull: always
commands:
- make deps-backend
volumes:
- name: deps
path: /go
# TODO: We should probably build all dependencies into a test image
- name: test-e2e
image: mcr.microsoft.com/playwright:v1.23.1-focal
commands:
- curl -sLO https://go.dev/dl/go1.18.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.18.linux-amd64.tar.gz
- groupadd --gid 1001 gitea && useradd -m --gid 1001 --uid 1001 gitea
- apt-get -qq update && apt-get -qqy install build-essential
- export TEST_PGSQL_SCHEMA=''
- ./build/test-env-prepare.sh
- su gitea bash -c "export PATH=$PATH:/usr/local/go/bin && timeout -s ABRT 40m make test-e2e-pgsql"
environment:
GOPROXY: https://goproxy.io
GOSUMDB: sum.golang.org
USE_REPO_TEST_DIR: 1
TEST_PGSQL_DBNAME: 'testgitea-e2e'
DEBIAN_FRONTEND: noninteractive
depends_on: [build-frontend, deps-backend]
---
kind: pipeline
name: update_translations

23
.gitignore vendored

@ -63,21 +63,14 @@ cpu.out
/indexers
/log
/public/img/avatar
/integrations/gitea-integration-mysql
/integrations/gitea-integration-mysql8
/integrations/gitea-integration-pgsql
/integrations/gitea-integration-sqlite
/integrations/gitea-integration-mssql
/integrations/indexers-mysql
/integrations/indexers-mysql8
/integrations/indexers-pgsql
/integrations/indexers-sqlite
/integrations/indexers-mssql
/integrations/sqlite.ini
/integrations/mysql.ini
/integrations/mysql8.ini
/integrations/pgsql.ini
/integrations/mssql.ini
/tests/integration/gitea-integration-*
/tests/integration/indexers-*
/tests/e2e/gitea-e2e-*
/tests/e2e/indexers-*
/tests/e2e/reports
/tests/e2e/test-artifacts
/tests/e2e/test-snapshots
/tests/*.ini
/node_modules
/yarn.lock
/yarn-error.log

8
CONTRIBUTING.md

@ -86,8 +86,10 @@ Here's how to run the test suite:
| | |
| :------------------------------------- | :----------------------------------------------- |
|``make test[\#TestSpecificName]`` | run unit test |
|``make test-sqlite[\#TestSpecificName]``| run [integration](integrations) test for SQLite |
|[More details about integrations](integrations/README.md) |
|``make test-sqlite[\#TestSpecificName]``| run [integration](tests/integration) test for SQLite |
|[More details about integration tests](tests/integration/README.md) |
|``make test-e2e-sqlite[\#TestSpecificFileName]``| run [end-to-end](tests/e2e) test for SQLite |
|[More details about e2e tests](tests/e2e/README.md) |
## Vendoring
@ -168,7 +170,7 @@ import (
To maintain understandable code and avoid circular dependencies it is important to have a good structure of the code. The Gitea code is divided into the following parts:
- **integration:** Integrations tests
- **integration:** Integration tests
- **models:** Contains the data structures used by xorm to construct database tables. It also contains supporting functions to query and update the database. Dependencies to other code in Gitea should be avoided although some modules might be needed (for example for logging).
- **models/fixtures:** Sample model data used in integration tests.
- **models/migrations:** Handling of database migrations between versions. PRs that changes a database structure shall also have a migration step.

188
Makefile

@ -98,7 +98,7 @@ LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(G
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64
GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/models/migrations code.gitea.io/gitea/integrations/migration-test code.gitea.io/gitea/integrations,$(shell $(GO) list ./... | grep -v /vendor/))
GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/models/migrations code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/))
FOMANTIC_WORK_DIR := web_src/fomantic
@ -122,7 +122,7 @@ TEST_TAGS ?= sqlite sqlite_unlock_notify
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(FOMANTIC_WORK_DIR)/node_modules $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
GO_DIRS := cmd integrations models modules routers build services tools
GO_DIRS := cmd tests models modules routers build services tools
GO_SOURCES := $(wildcard *.go)
GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" -not -path modules/options/bindata.go -not -path modules/public/bindata.go -not -path modules/templates/bindata.go)
@ -131,6 +131,11 @@ ifeq ($(filter $(TAGS_SPLIT),bindata),bindata)
GO_SOURCES += $(BINDATA_DEST)
endif
# Force installation of playwright dependencies by setting this flag
ifdef DEPS_PLAYWRIGHT
PLAYWRIGHT_FLAGS += --with-deps
endif
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g
SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g
@ -182,6 +187,7 @@ help:
@echo " - test test everything"
@echo " - test-frontend test frontend files"
@echo " - test-backend test backend files"
@echo " - test-e2e[\#TestSpecificName] test end to end using playwright"
@echo " - webpack build webpack files"
@echo " - svg build svg files"
@echo " - fomantic build fomantic files"
@ -236,9 +242,13 @@ clean:
$(GO) clean -i ./...
rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \
integrations*.test \
integrations/gitea-integration-pgsql/ integrations/gitea-integration-mysql/ integrations/gitea-integration-mysql8/ integrations/gitea-integration-sqlite/ \
integrations/gitea-integration-mssql/ integrations/indexers-mysql/ integrations/indexers-mysql8/ integrations/indexers-pgsql integrations/indexers-sqlite \
integrations/indexers-mssql integrations/mysql.ini integrations/mysql8.ini integrations/pgsql.ini integrations/mssql.ini man/
e2e*.test \
tests/integration/gitea-integration-pgsql/ tests/integration/gitea-integration-mysql/ tests/integration/gitea-integration-mysql8/ tests/integration/gitea-integration-sqlite/ \
tests/integration/gitea-integration-mssql/ tests/integration/indexers-mysql/ tests/integration/indexers-mysql8/ tests/integration/indexers-pgsql tests/integration/indexers-sqlite \
tests/integration/indexers-mssql tests/mysql.ini tests/mysql8.ini tests/pgsql.ini tests/mssql.ini man/ \
tests/e2e/gitea-e2e-pgsql/ tests/e2e/gitea-e2e-mysql/ tests/e2e/gitea-e2e-mysql8/ tests/e2e/gitea-e2e-sqlite/ \
tests/e2e/gitea-e2e-mssql/ tests/e2e/indexers-mysql/ tests/e2e/indexers-mysql8/ tests/e2e/indexers-pgsql/ tests/e2e/indexers-sqlite/ \
tests/e2e/indexers-mssql/ tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
.PHONY: fmt
fmt:
@ -318,7 +328,7 @@ lint: lint-frontend lint-backend
.PHONY: lint-frontend
lint-frontend: node_modules
npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js docs/assets/js
npx eslint --color --max-warnings=0 --ext js,vue web_src/js build *.config.js docs/assets/js tests/e2e/*.test.e2e.js tests/e2e/utils_e2e.js
npx stylelint --color --max-warnings=0 web_src/less
npx spectral lint -q -F hint $(SWAGGER_SPEC)
npx markdownlint docs *.md
@ -399,24 +409,26 @@ tidy-check: tidy
generate-ini-sqlite:
sed -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
integrations/sqlite.ini.tmpl > integrations/sqlite.ini
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/sqlite.ini.tmpl > tests/sqlite.ini
.PHONY: test-sqlite
test-sqlite: integrations.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test
.PHONY: test-sqlite\#%
test-sqlite\#%: integrations.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*)
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*)
.PHONY: test-sqlite-migration
test-sqlite-migration: migrations.sqlite.test migrations.individual.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.sqlite.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.individual.sqlite.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./migrations.sqlite.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./migrations.individual.sqlite.test
.PHONY: test-sqlite-migration\#%
test-sqlite-migration\#%: migrations.sqlite.test migrations.individual.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./migrations.individual.sqlite.test -test.run $(subst .,/,$*)
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./migrations.individual.sqlite.test -test.run $(subst .,/,$*)
generate-ini-mysql:
@ -425,20 +437,22 @@ generate-ini-mysql:
-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \
-e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
integrations/mysql.ini.tmpl > integrations/mysql.ini
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/mysql.ini.tmpl > tests/mysql.ini
.PHONY: test-mysql
test-mysql: integrations.mysql.test generate-ini-mysql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.mysql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test
.PHONY: test-mysql\#%
test-mysql\#%: integrations.mysql.test generate-ini-mysql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*)
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*)
.PHONY: test-mysql-migration
test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test generate-ini-mysql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./migrations.mysql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./migrations.individual.mysql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./migrations.mysql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./migrations.individual.mysql.test
generate-ini-mysql8:
sed -e 's|{{TEST_MYSQL8_HOST}}|${TEST_MYSQL8_HOST}|g' \
@ -446,20 +460,22 @@ generate-ini-mysql8:
-e 's|{{TEST_MYSQL8_USERNAME}}|${TEST_MYSQL8_USERNAME}|g' \
-e 's|{{TEST_MYSQL8_PASSWORD}}|${TEST_MYSQL8_PASSWORD}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
integrations/mysql8.ini.tmpl > integrations/mysql8.ini
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/mysql8.ini.tmpl > tests/mysql8.ini
.PHONY: test-mysql8
test-mysql8: integrations.mysql8.test generate-ini-mysql8
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./integrations.mysql8.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./integrations.mysql8.test
.PHONY: test-mysql8\#%
test-mysql8\#%: integrations.mysql8.test generate-ini-mysql8
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./integrations.mysql8.test -test.run $(subst .,/,$*)
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./integrations.mysql8.test -test.run $(subst .,/,$*)
.PHONY: test-mysql8-migration
test-mysql8-migration: migrations.mysql8.test migrations.individual.mysql8.test generate-ini-mysql8
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./migrations.mysql8.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql8.ini ./migrations.individual.mysql8.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./migrations.mysql8.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./migrations.individual.mysql8.test
generate-ini-pgsql:
sed -e 's|{{TEST_PGSQL_HOST}}|${TEST_PGSQL_HOST}|g' \
@ -468,20 +484,22 @@ generate-ini-pgsql:
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
integrations/pgsql.ini.tmpl > integrations/pgsql.ini
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/pgsql.ini.tmpl > tests/pgsql.ini
.PHONY: test-pgsql
test-pgsql: integrations.pgsql.test generate-ini-pgsql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./integrations.pgsql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test
.PHONY: test-pgsql\#%
test-pgsql\#%: integrations.pgsql.test generate-ini-pgsql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*)
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*)
.PHONY: test-pgsql-migration
test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test generate-ini-pgsql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./migrations.pgsql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./migrations.individual.pgsql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./migrations.pgsql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./migrations.individual.pgsql.test
generate-ini-mssql:
sed -e 's|{{TEST_MSSQL_HOST}}|${TEST_MSSQL_HOST}|g' \
@ -489,85 +507,140 @@ generate-ini-mssql:
-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \
-e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
integrations/mssql.ini.tmpl > integrations/mssql.ini
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/mssql.ini.tmpl > tests/mssql.ini
.PHONY: test-mssql
test-mssql: integrations.mssql.test generate-ini-mssql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./integrations.mssql.test
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test
.PHONY: test-mssql\#%
test-mssql\#%: integrations.mssql.test generate-ini-mssql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*)
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*)
.PHONY: test-mssql-migration
test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test generate-ini-mssql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./migrations.mssql.test -test.failfast
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./migrations.individual.mssql.test -test.failfast
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./migrations.mssql.test -test.failfast
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./migrations.individual.mssql.test -test.failfast
.PHONY: playwright
playwright: $(PLAYWRIGHT_DIR)
npm install --no-save @playwright/test
npx playwright install $(PLAYWRIGHT_FLAGS)
.PHONY: test-e2e%
test-e2e%: TEST_TYPE ?= e2e
# Clear display env variable. Otherwise, chromium tests can fail.
DISPLAY=
.PHONY: test-e2e
test-e2e: test-e2e-sqlite
.PHONY: test-e2e-sqlite
test-e2e-sqlite: playwright e2e.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test
.PHONY: test-e2e-sqlite\#%
test-e2e-sqlite\#%: playwright e2e.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestE2e/$*
.PHONY: test-e2e-mysql
test-e2e-mysql: playwright e2e.mysql.test generate-ini-mysql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test
.PHONY: test-e2e-mysql\#%
test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test -test.run TestE2e/$*
.PHONY: test-e2e-mysql8
test-e2e-mysql8: playwright e2e.mysql8.test generate-ini-mysql8
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./e2e.mysql8.test
.PHONY: test-e2e-mysql8\#%
test-e2e-mysql8\#%: playwright e2e.mysql8.test generate-ini-mysql8
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql8.ini ./e2e.mysql8.test -test.run TestE2e/$*
.PHONY: test-e2e-pgsql
test-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test
.PHONY: test-e2e-pgsql\#%
test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test -test.run TestE2e/$*
.PHONY: test-e2e-mssql
test-e2e-mssql: playwright e2e.mssql.test generate-ini-mssql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test
.PHONY: test-e2e-mssql\#%
test-e2e-mssql\#%: playwright e2e.mssql.test generate-ini-mssql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e/$*
.PHONY: bench-sqlite
bench-sqlite: integrations.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
.PHONY: bench-mysql
bench-mysql: integrations.mysql.test generate-ini-mysql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
.PHONY: bench-mssql
bench-mssql: integrations.mssql.test generate-ini-mssql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
.PHONY: bench-pgsql
bench-pgsql: integrations.pgsql.test generate-ini-pgsql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench .
.PHONY: integration-test-coverage
integration-test-coverage: integrations.cover.test generate-ini-mysql
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out
.PHONY: integration-test-coverage-sqlite
integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite
GITEA_ROOT="$(CURDIR)" GITEA_CONF=integrations/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out
GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out
integrations.mysql.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.mysql.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql.test
integrations.mysql8.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.mysql8.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql8.test
integrations.pgsql.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.pgsql.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.pgsql.test
integrations.mssql.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.mssql.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mssql.test
integrations.sqlite.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -o integrations.sqlite.test -tags '$(TEST_TAGS)'
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.sqlite.test -tags '$(TEST_TAGS)'
integrations.cover.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.test
integrations.cover.sqlite.test: git-check $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)'
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -coverpkg $(shell echo $(GO_PACKAGES) | tr ' ' ',') -o integrations.cover.sqlite.test -tags '$(TEST_TAGS)'
.PHONY: migrations.mysql.test
migrations.mysql.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.mysql.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql.test
.PHONY: migrations.mysql8.test
migrations.mysql8.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.mysql8.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql8.test
.PHONY: migrations.pgsql.test
migrations.pgsql.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.pgsql.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.pgsql.test
.PHONY: migrations.mssql.test
migrations.mssql.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.mssql.test
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mssql.test
.PHONY: migrations.sqlite.test
migrations.sqlite.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/integrations/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)'
.PHONY: migrations.individual.mysql.test
migrations.individual.mysql.test: $(GO_SOURCES)
@ -589,6 +662,21 @@ migrations.individual.mssql.test: $(GO_SOURCES)
migrations.individual.sqlite.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/models/migrations -o migrations.individual.sqlite.test -tags '$(TEST_TAGS)'
e2e.mysql.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql.test
e2e.mysql8.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql8.test
e2e.pgsql.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.pgsql.test
e2e.mssql.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mssql.test
e2e.sqlite.test: $(GO_SOURCES)
$(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.sqlite.test -tags '$(TEST_TAGS)'
.PHONY: check
check: test

6
build/code-batch-process.go

@ -61,7 +61,7 @@ func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error)
"build",
"cmd",
"contrib",
"integrations",
"tests",
"models",
"modules",
"routers",
@ -71,8 +71,8 @@ func newFileCollector(fileFilter string, batchSize int) (*fileCollector, error)
co.includePatterns = append(co.includePatterns, regexp.MustCompile(`.*\.go$`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`.*\bbindata\.go$`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`integrations/gitea-repositories-meta`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`integrations/migration-test`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/gitea-repositories-meta`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`tests/integration/migration-test`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`modules/git/tests`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/fixtures`))
co.excludePatterns = append(co.excludePatterns, regexp.MustCompile(`models/migrations/fixtures`))

2
contrib/pr/checkout.go

@ -112,7 +112,7 @@ func runPR() {
unittest.LoadFixtures()
util.RemoveAll(setting.RepoRootPath)
util.RemoveAll(repo_module.LocalCopyPath())
unittest.CopyDir(path.Join(curDir, "integrations/gitea-repositories-meta"), setting.RepoRootPath)
unittest.CopyDir(path.Join(curDir, "tests/gitea-repositories-meta"), setting.RepoRootPath)
log.Printf("[PR] Setting up router\n")
// routers.GlobalInit()

4
docs/content/doc/developers/guidelines-backend.md

@ -33,7 +33,9 @@ To maintain understandable code and avoid circular dependencies it is important
- `build`: Scripts to help build Gitea.
- `cmd`: All Gitea actual sub commands includes web, doctor, serv, hooks, admin and etc. `web` will start the web service. `serv` and `hooks` will be invoked by Git or OpenSSH. Other sub commands could help to maintain Gitea.
- `integrations`: Integration tests
- `tests`: Common test utility functions
- `tests/integration`: Integration tests, to test back-end regressions
- `tests/e2e`: E2e tests, to test test front-end <> back-end compatibility and visual regressions.
- `models`: Contains the data structures used by xorm to construct database tables. It also contains functions to query and update the database. Dependencies to other Gitea code should be avoided. You can make exceptions in cases such as logging.
- `models/db`: Basic database operations. All other `models/xxx` packages should depend on this package. The `GetEngine` function should only be invoked from `models/`.
- `models/fixtures`: Sample data used in unit tests and integration tests. One `yml` file means one table which will be loaded into database when beginning the tests.

2
docs/content/doc/developers/hacking-on-gitea.en-us.md

@ -309,7 +309,7 @@ will run the integration tests in an SQLite environment. Integration tests
require `git lfs` to be installed. Other database tests are available but
may need adjustment to the local environment.
Take a look at [`integrations/README.md`](https://github.com/go-gitea/gitea/blob/main/integrations/README.md)
Take a look at [`tests/integration/README.md`](https://github.com/go-gitea/gitea/blob/main/tests/integration/README.md)
for more information and how to run a single test.
### Testing for a PR

4
models/migrations/migrations_test.go

@ -46,7 +46,7 @@ func TestMain(m *testing.M) {
giteaConf := os.Getenv("GITEA_CONF")
if giteaConf == "" {
giteaConf = path.Join(filepath.Dir(setting.AppPath), "integrations/sqlite.ini")
giteaConf = path.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini")
fmt.Printf("Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf)
}
@ -205,7 +205,7 @@ func prepareTestEnv(t *testing.T, skip int, syncModels ...interface{}) (*xorm.En
ourSkip += skip
deferFn := PrintCurrentTest(t, ourSkip)
assert.NoError(t, os.RemoveAll(setting.RepoRootPath))
assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "integrations/gitea-repositories-meta"), setting.RepoRootPath))
assert.NoError(t, unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), setting.RepoRootPath))
ownerDirs, err := os.ReadDir(setting.RepoRootPath)
if err != nil {
assert.NoError(t, err, "unable to read the new repo root: %v\n", err)

2
models/migrations/testlogger_test.go

@ -188,5 +188,5 @@ func (log *TestLogger) GetName() string {
func init() {
log.Register("test", NewTestLogger)
_, filename, _, _ := runtime.Caller(0)
prefix = strings.TrimSuffix(filename, "integrations/testlogger.go")
prefix = strings.TrimSuffix(filename, "tests/testlogger.go")
}

4
models/unittest/testdb.go

@ -116,7 +116,7 @@ func MainTest(m *testing.M, testOpts *TestOptions) {
if err = util.RemoveAll(repoRootPath); err != nil {
fatalTestError("util.RemoveAll: %v\n", err)
}
if err = CopyDir(filepath.Join(testOpts.GiteaRootPath, "integrations", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
if err = CopyDir(filepath.Join(testOpts.GiteaRootPath, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
fatalTestError("util.CopyDir: %v\n", err)
}
@ -202,7 +202,7 @@ func PrepareTestDatabase() error {
func PrepareTestEnv(t testing.TB) {
assert.NoError(t, PrepareTestDatabase())
assert.NoError(t, util.RemoveAll(setting.RepoRootPath))
metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
assert.NoError(t, CopyDir(metaPath, setting.RepoRootPath))
ownerDirs, err := os.ReadDir(setting.RepoRootPath)
assert.NoError(t, err)

2
modules/markup/html_test.go

@ -24,7 +24,7 @@ import (
var localMetas = map[string]string{
"user": "gogits",
"repo": "gogs",
"repoPath": "../../integrations/gitea-repositories-meta/user13/repo11.git/",
"repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/",
}
func TestMain(m *testing.M) {

2
modules/markup/markdown/markdown_test.go

@ -29,7 +29,7 @@ const (
var localMetas = map[string]string{
"user": "gogits",
"repo": "gogs",
"repoPath": "../../../integrations/gitea-repositories-meta/user13/repo11.git/",
"repoPath": "../../../tests/gitea-repositories-meta/user13/repo11.git/",
}
func TestMain(m *testing.M) {

45
package-lock.json generated

@ -46,6 +46,7 @@
"wrap-ansi": "8.0.1"
},
"devDependencies": {
"@playwright/test": "1.25.1",
"@stoplight/spectral-cli": "6.5.1",
"eslint": "8.22.0",
"eslint-plugin-import": "2.26.0",
@ -1356,6 +1357,22 @@
"node": ">= 8"
}
},
"node_modules/@playwright/test": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.25.1.tgz",
"integrity": "sha512-IJ4X0yOakXtwkhbnNzKkaIgXe6df7u3H3FnuhI9Jqh+CdO0e/lYQlDLYiyI9cnXK8E7UAppAWP+VqAv6VX7HQg==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.25.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
@ -9028,6 +9045,18 @@
"node": ">=8"
}
},
"node_modules/playwright-core": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.25.1.tgz",
"integrity": "sha512-lSvPCmA2n7LawD2Hw7gSCLScZ+vYRkhU8xH0AapMyzwN+ojoDqhkH/KIEUxwNu2PjPoE/fcE0wLAksdOhJ2O5g==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
@ -12789,6 +12818,16 @@
"fastq": "^1.6.0"
}
},
"@playwright/test": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.25.1.tgz",
"integrity": "sha512-IJ4X0yOakXtwkhbnNzKkaIgXe6df7u3H3FnuhI9Jqh+CdO0e/lYQlDLYiyI9cnXK8E7UAppAWP+VqAv6VX7HQg==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.25.1"
}
},
"@popperjs/core": {
"version": "2.11.6",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
@ -18627,6 +18666,12 @@
}
}
},
"playwright-core": {
"version": "1.25.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.25.1.tgz",
"integrity": "sha512-lSvPCmA2n7LawD2Hw7gSCLScZ+vYRkhU8xH0AapMyzwN+ojoDqhkH/KIEUxwNu2PjPoE/fcE0wLAksdOhJ2O5g==",
"dev": true
},
"pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",

1
package.json

@ -46,6 +46,7 @@
"wrap-ansi": "8.0.1"
},
"devDependencies": {
"@playwright/test": "1.25.1",
"@stoplight/spectral-cli": "6.5.1",
"eslint": "8.22.0",
"eslint-plugin-import": "2.26.0",

100
playwright.config.js

@ -0,0 +1,100 @@
// @ts-check
import {devices} from '@playwright/test';
const BASE_URL = process.env.GITEA_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000';
/**
* @see https://playwright.dev/docs/test-configuration
* @type {import('@playwright/test').PlaywrightTestConfig}
*/
export default {
testDir: './tests/e2e/',
testMatch: /.*\.test\.e2e\.js/, // Match any .test.e2e.js files
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 2000
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: process.env.CI ? 'list' : [['list'], ['html', {outputFolder: 'tests/e2e/reports/', open: 'never'}]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
headless: true, // set to false to debug
locale: 'en-US',
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 1000,
/* Maximum time allowed for navigation, such as `page.goto()`. */
navigationTimeout: 5 * 1000,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: BASE_URL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
/* Project-specific settings. */
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: {
...devices['Pixel 5'],
},
},
{
name: 'Mobile Safari',
use: {
...devices['iPhone 12'],
},
},
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
outputDir: 'tests/e2e/test-artifacts/',
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
snapshotDir: 'tests/e2e/test-snapshots/',
};

93
tests/e2e/README.md

@ -0,0 +1,93 @@
# End to end tests
E2e tests largely follow the same syntax as [integration tests](tests/e2e/README.md).
Whereas integration tests are intended to mock and stress the back-end, server-side code, e2e tests the interface between front-end and back-end, as well as visual regressions with both assertions and visual comparisons.
They can be run with make commands for the appropriate backends, namely:
```shell
make test-sqlite
make test-pgsql
make test-mysql
make test-mysql8
make test-mssql
```
Make sure to perform a clean front-end build before running tests:
```
make clean frontend
```
## Install playwright system dependencies
```
npx playwright install-deps
```
## Run all tests via local drone
```
drone exec --local --build-event "pull_request"
```
## Run sqlite e2e tests
Start tests
```
make test-e2e-sqlite
```
## Run MySQL e2e tests
Setup a MySQL database inside docker
```
docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest #(just ctrl-c to stop db and clean the container)
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --rm --name elasticsearch elasticsearch:7.6.0 #(in a second terminal, just ctrl-c to stop db and clean the container)
```
Start tests based on the database container
```
TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-e2e-mysql
```
## Run pgsql e2e tests
Setup a pgsql database inside docker
```
docker run -e "POSTGRES_DB=test" -p 5432:5432 --rm --name pgsql postgres:latest #(just ctrl-c to stop db and clean the container)
```
Start tests based on the database container
```
TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-e2e-pgsql
```
## Run mssql e2e tests
Setup a mssql database inside docker
```
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container)
```
Start tests based on the database container
```
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql
```
## Running individual tests
Example command to run `example.test.e2e.js` test file:
_Note: unlike integration tests, this filtering is at the file level, not function_
For SQLite:
```
make test-e2e-sqlite#example
```
For other databases(replace `mssql` to `mysql`, `mysql8` or `pgsql`):
```
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql#example
```
## Visual testing
Although the main goal of e2e is assertion testing, we have added a framework for visual regress testing. If you are working on front-end features, please use the following:
- Check out `main`, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1` to generate outputs. This will initially fail, as no screenshots exist. You can run the e2e tests again to assert it passes.
- Check out your branch, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1`. You should be able to assert you front-end changes don't break any other tests unintentionally.
VISUAL_TEST=1 will create screenshots in tests/e2e/test-snapshots. The test will fail the first time this is enabled (until we get visual test image persistence figured out), because it will be testing against an empty screenshot folder.
ACCEPT_VISUAL=1 will overwrite the snapshot images with new images.

120
tests/e2e/e2e_test.go

@ -0,0 +1,120 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// This is primarily coped from /tests/integration/integration_test.go
// TODO: Move common functions to shared file
package e2e
import (
"bytes"
"context"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers"
"code.gitea.io/gitea/tests"
)
var c *web.Route
func TestMain(m *testing.M) {
defer log.Close()
managerCtx, cancel := context.WithCancel(context.Background())
graceful.InitManager(managerCtx)
defer cancel()
tests.InitTest(false)
c = routers.NormalRoutes(context.TODO())
os.Unsetenv("GIT_AUTHOR_NAME")
os.Unsetenv("GIT_AUTHOR_EMAIL")
os.Unsetenv("GIT_AUTHOR_DATE")
os.Unsetenv("GIT_COMMITTER_NAME")
os.Unsetenv("GIT_COMMITTER_EMAIL")
os.Unsetenv("GIT_COMMITTER_DATE")
err := unittest.InitFixtures(
unittest.FixturesOptions{
Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
},
)
if err != nil {
fmt.Printf("Error initializing test database: %v\n", err)
os.Exit(1)
}
exitVal := m.Run()
tests.WriterCloser.Reset()
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
fmt.Printf("util.RemoveAll: %v\n", err)
os.Exit(1)
}
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
fmt.Printf("Unable to remove repo indexer: %v\n", err)
os.Exit(1)
}
os.Exit(exitVal)
}
// This should be the only test e2e necessary. It will collect all "*.test.e2e.js"
// 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 test directory.
searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.js")
paths, err := filepath.Glob(searchGlob)
if err != nil {
t.Fatal(err)
} else if len(paths) == 0 {
t.Fatal(fmt.Errorf("No e2e tests found in %s", searchGlob))
}
runArgs := []string{"npx", "playwright", "test"}
// To update snapshot outputs
if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
runArgs = append(runArgs, "--update-snapshots")
}
// Create new test for each input file
for _, path := range paths {
_, filename := filepath.Split(path)
testname := filename[:len(filename)-len(filepath.Ext(path))]
t.Run(testname, func(t *testing.T) {
// Default 2 minute timeout
onGiteaRun(t, func(*testing.T, *url.URL) {
cmd := exec.Command(runArgs[0], runArgs...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("GITEA_URL=%s", setting.AppURL))
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
// Currently colored output is conflicting. Using Printf until that is resolved.
fmt.Printf("%v", stdout.String())
fmt.Printf("%v", stderr.String())
log.Fatal("Playwright Failed: %s", err)
} else {
fmt.Printf("%v", stdout.String())
}
})
})
}
}

57
tests/e2e/example.test.e2e.js

@ -0,0 +1,57 @@
// @ts-check
import {test, expect} from '@playwright/test';
import {login_user, save_visual, load_logged_in_context} from './utils_e2e.js';
test.beforeAll(async ({browser}, workerInfo) => {
await login_user(browser, workerInfo, 'user2');
});
test('Load Homepage', async ({page}) => {
const response = await page.goto('/');
await 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) => {
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]', 'test123');
await page.type('input[name=retype]', 'test123');
await page.click('form button.ui.green.button:visible');
// Make sure we routed to the home page. Else login failed.
await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
await expect(page.locator('.dashboard-navbar span>img.ui.avatar.image')).toBeVisible();
await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created.');
save_visual(page);
});
test('Test Login Form', async ({page}, workerInfo) => {
const response = await page.goto('/user/login');
await expect(response?.status()).toBe(200); // Status OK
await page.type('input[name=user_name]', `user2`);
await page.type('input[name=password]', `password`);
await page.click('form button.ui.green.button:visible');
await page.waitForLoadState('networkidle');
await expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
save_visual(page);
});
test('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}/`);
save_visual(page);
});

60
tests/e2e/utils_e2e.js

@ -0,0 +1,60 @@
import {expect} 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) {
// Set up a new context
const context = await browser.newContext();
const page = await context.newPage();
// 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
// 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.green.button:visible');
await page.waitForLoadState('networkidle');
await 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`});
return context;
}
export async function load_logged_in_context(browser, workerInfo, user) {
let context;
try {
context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error(`Could not find state for '${user}'. Did you call login_user(browser, workerInfo, '${user}') in test.beforeAll()?`);
}
}
return context;
}
export async function save_visual(page) {
// Optionally include visual testing
if (process.env.VISUAL_TEST) {
await page.waitForLoadState('networkidle');
// Mock page/version string
await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK');
await expect(page).toHaveScreenshot({
fullPage: true,
timeout: 20000,
mask: [
page.locator('.dashboard-navbar span>img.ui.avatar.image'),
page.locator('.ui.dropdown.jump.item.tooltip span>img.ui.avatar.image'),
],
});
}
}

57
tests/e2e/utils_e2e_test.go

@ -0,0 +1,57 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package e2e
import (
"context"
"net"
"net/http"
"net/url"
"testing"
"time"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func onGiteaRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) {
if len(prepare) == 0 || prepare[0] {
defer tests.PrepareTestEnv(t, 1)()
}
s := http.Server{
Handler: c,
}
u, err := url.Parse(setting.AppURL)
assert.NoError(t, err)
listener, err := net.Listen("tcp", u.Host)
i := 0
for err != nil && i <= 10 {
time.Sleep(100 * time.Millisecond)
listener, err = net.Listen("tcp", u.Host)
i++
}
assert.NoError(t, err)
u.Host = listener.Addr().String()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
s.Shutdown(ctx)
cancel()
}()
go s.Serve(listener)
// Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
callback(t, u)
}
func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...bool) {
onGiteaRunTB(t, func(t testing.TB, u *url.URL) {
callback(t.(*testing.T), u)
}, prepare...)
}

0
integrations/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/HEAD → tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/HEAD

0
integrations/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/config → tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/config

0
integrations/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description → tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/description

0
integrations/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude → tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/info/exclude

0
integrations/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/74/8bf557dfc9c6457998b5118a6c8b2129f56c30 → tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/74/8bf557dfc9c6457998b5118a6c8b2129f56c30

0
integrations/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/a5/46f86c7dd182592b96639045e176dde8df76ef → tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/a5/46f86c7dd182592b96639045e176dde8df76ef

0
integrations/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/b8/95782bd271fdd266dd06e5880ea4abdc3a0dc7 → tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/objects/b8/95782bd271fdd266dd06e5880ea4abdc3a0dc7

0
integrations/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/refs/heads/master → tests/gitea-repositories-meta/limited_org/private_repo_on_limited_org.git/refs/heads/master

0
integrations/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/HEAD → tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/HEAD

0
integrations/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/config → tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/config

0
integrations/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description → tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/description

0
integrations/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude → tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/info/exclude

0
integrations/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/21/2f14c8b713de38bd0b3fb23bd288369b01668a → tests/gitea-repositories-meta/limited_org/public_repo_on_limited_org.git/objects/21/2f14c8b713de38bd0b3fb23bd288369b01668a